libretto 0.6.24 → 0.6.26
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 +9 -1
- package/README.template.md +9 -1
- package/dist/cli/commands/browser.js +17 -10
- package/dist/cli/commands/cloud-credentials.js +70 -0
- package/dist/cli/commands/deploy.js +24 -2
- package/dist/cli/commands/execution.js +9 -30
- package/dist/cli/commands/import-chrome-profiles.js +46 -0
- package/dist/cli/commands/profiles.js +71 -0
- package/dist/cli/commands/shared.js +1 -3
- package/dist/cli/core/browser.js +89 -75
- package/dist/cli/core/daemon/daemon.js +47 -35
- package/dist/cli/core/daemon/ipc.js +3 -0
- package/dist/cli/core/deploy-artifact.js +85 -22
- package/dist/cli/core/profiles.js +47 -0
- package/dist/cli/core/prompt.js +9 -0
- package/dist/cli/core/providers/libretto-cloud.js +6 -2
- package/dist/cli/core/session-logs.js +325 -0
- package/dist/cli/core/telemetry.js +110 -311
- package/dist/cli/core/workflow-runner/runner.js +65 -0
- package/dist/cli/router.js +9 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -0
- package/dist/shared/workflow/auth-profile-name.d.ts +3 -0
- package/dist/shared/workflow/auth-profile-name.js +29 -0
- package/dist/shared/workflow/auth-profile-state.d.ts +20 -0
- package/dist/shared/workflow/auth-profile-state.js +105 -0
- package/dist/shared/workflow/authenticate.d.ts +17 -0
- package/dist/shared/workflow/authenticate.js +37 -0
- package/dist/shared/workflow/credentials.d.ts +5 -0
- package/dist/shared/workflow/credentials.js +68 -0
- package/dist/shared/workflow/workflow.d.ts +16 -1
- package/dist/shared/workflow/workflow.js +56 -4
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +3 -4
- package/skills/libretto/references/auth-profiles.md +61 -11
- package/skills/libretto/references/code-generation-rules.md +31 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/browser.ts +19 -11
- package/src/cli/commands/cloud-credentials.ts +82 -0
- package/src/cli/commands/deploy.ts +41 -2
- package/src/cli/commands/execution.ts +10 -31
- package/src/cli/commands/import-chrome-profiles.ts +46 -0
- package/src/cli/commands/profiles.ts +90 -0
- package/src/cli/commands/shared.ts +4 -8
- package/src/cli/core/browser.ts +102 -91
- package/src/cli/core/daemon/config.ts +4 -1
- package/src/cli/core/daemon/daemon.ts +52 -44
- package/src/cli/core/daemon/ipc.ts +15 -0
- package/src/cli/core/deploy-artifact.ts +131 -32
- package/src/cli/core/profiles.ts +53 -0
- package/src/cli/core/prompt.ts +15 -0
- package/src/cli/core/providers/libretto-cloud.ts +6 -2
- package/src/cli/core/providers/types.ts +4 -1
- package/src/cli/core/session-logs.ts +445 -0
- package/src/cli/core/telemetry.ts +142 -413
- package/src/cli/core/workflow-runner/runner.ts +86 -1
- package/src/cli/router.ts +8 -0
- package/src/index.ts +10 -0
- package/src/shared/workflow/auth-profile-name.ts +27 -0
- package/src/shared/workflow/auth-profile-state.ts +144 -0
- package/src/shared/workflow/authenticate.ts +63 -0
- package/src/shared/workflow/credentials.ts +91 -0
- package/src/shared/workflow/workflow.ts +89 -4
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { warnIfLibrettoVersionsDiffer } from "../core/skill-version.js";
|
|
22
22
|
import { SimpleCLI } from "affordance";
|
|
23
23
|
import {
|
|
24
|
+
type SessionContext,
|
|
24
25
|
sessionOption,
|
|
25
26
|
withAutoSession,
|
|
26
27
|
withExperiments,
|
|
@@ -80,7 +81,7 @@ export const openInput = SimpleCLI.input({
|
|
|
80
81
|
}),
|
|
81
82
|
authProfile: SimpleCLI.option(z.string().optional(), {
|
|
82
83
|
name: "auth-profile",
|
|
83
|
-
help: "
|
|
84
|
+
help: "Named auth profile to load before opening the browser",
|
|
84
85
|
}),
|
|
85
86
|
viewport: SimpleCLI.option(z.string().optional(), {
|
|
86
87
|
help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)",
|
|
@@ -105,7 +106,7 @@ export const openCommand = SimpleCLI.command({
|
|
|
105
106
|
})
|
|
106
107
|
.input(openInput)
|
|
107
108
|
.use(withAutoSession())
|
|
108
|
-
.use(withExperiments())
|
|
109
|
+
.use(withExperiments<SessionContext>())
|
|
109
110
|
.handle(async ({ input, ctx }) => {
|
|
110
111
|
warnIfLibrettoVersionsDiffer();
|
|
111
112
|
assertSessionAvailableForStart(ctx.session, ctx.logger);
|
|
@@ -119,10 +120,15 @@ export const openCommand = SimpleCLI.command({
|
|
|
119
120
|
input.readOnly,
|
|
120
121
|
input.writeAccess,
|
|
121
122
|
),
|
|
122
|
-
|
|
123
|
+
authProfileName: input.authProfile,
|
|
123
124
|
experiments: ctx.experiments,
|
|
124
125
|
});
|
|
125
126
|
} else {
|
|
127
|
+
if (input.authProfile) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
"--auth-profile is only supported for local browser sessions. Hosted provider sessions use workflow-declared authProfile settings.",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
126
132
|
await runOpenWithProvider(
|
|
127
133
|
input.url,
|
|
128
134
|
providerName,
|
|
@@ -166,7 +172,7 @@ export const connectCommand = SimpleCLI.command({
|
|
|
166
172
|
})
|
|
167
173
|
.input(connectInput)
|
|
168
174
|
.use(withAutoSession())
|
|
169
|
-
.use(withExperiments())
|
|
175
|
+
.use(withExperiments<SessionContext>())
|
|
170
176
|
.handle(async ({ input, ctx }) => {
|
|
171
177
|
warnIfLibrettoVersionsDiffer();
|
|
172
178
|
await runConnectWithLogger(
|
|
@@ -180,17 +186,17 @@ export const connectCommand = SimpleCLI.command({
|
|
|
180
186
|
|
|
181
187
|
export const saveInput = SimpleCLI.input({
|
|
182
188
|
positionals: [
|
|
183
|
-
SimpleCLI.positional("
|
|
184
|
-
help: "
|
|
189
|
+
SimpleCLI.positional("profileName", z.string(), {
|
|
190
|
+
help: "Profile name to save",
|
|
185
191
|
}),
|
|
186
192
|
],
|
|
187
193
|
named: {
|
|
188
194
|
session: sessionOption(),
|
|
195
|
+
sites: SimpleCLI.option(z.string(), {
|
|
196
|
+
help: "Comma-separated sites whose auth state should be saved",
|
|
197
|
+
}),
|
|
189
198
|
},
|
|
190
|
-
})
|
|
191
|
-
(input) => Boolean(input.urlOrDomain),
|
|
192
|
-
`Usage: libretto save <url|domain> --session <name>`,
|
|
193
|
-
);
|
|
199
|
+
});
|
|
194
200
|
|
|
195
201
|
export const saveCommand = SimpleCLI.command({
|
|
196
202
|
description: "Save current browser session",
|
|
@@ -198,7 +204,9 @@ export const saveCommand = SimpleCLI.command({
|
|
|
198
204
|
.input(saveInput)
|
|
199
205
|
.use(withRequiredSession())
|
|
200
206
|
.handle(async ({ input, ctx }) => {
|
|
201
|
-
await runSave(input.
|
|
207
|
+
await runSave(input.profileName, ctx.session, ctx.logger, {
|
|
208
|
+
sites: input.sites,
|
|
209
|
+
});
|
|
202
210
|
});
|
|
203
211
|
|
|
204
212
|
export const pagesInput = SimpleCLI.input({
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { SimpleCLI } from "affordance";
|
|
2
|
+
import { orpcCall, resolveApiUrl } from "../core/auth-fetch.js";
|
|
3
|
+
|
|
4
|
+
const CLOUD_CREDENTIAL_ENV_PREFIX = "LIBRETTO_CLOUD_";
|
|
5
|
+
|
|
6
|
+
type UpsertCredentialResponse = {
|
|
7
|
+
success: true;
|
|
8
|
+
credential_id: string;
|
|
9
|
+
overwritten: boolean;
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function requireApiKeyCredential() {
|
|
14
|
+
const apiKey = process.env.LIBRETTO_API_KEY?.trim();
|
|
15
|
+
if (!apiKey) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"LIBRETTO_API_KEY is required to manage Libretto Cloud credentials. Issue one with `libretto cloud auth api-key issue --label <label>`.",
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
apiUrl: resolveApiUrl(null),
|
|
22
|
+
credential: { source: "env-api-key" as const, apiKey },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseEnvCredentials(prefix: string): Array<{ name: string; value: string }> {
|
|
27
|
+
const credentials: Record<string, string> = {};
|
|
28
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
29
|
+
if (!key.startsWith(prefix) || value === undefined) continue;
|
|
30
|
+
if (value.trim().length === 0) continue;
|
|
31
|
+
const fieldName = key.slice(prefix.length).toLowerCase();
|
|
32
|
+
if (!fieldName || fieldName === "api_key") continue;
|
|
33
|
+
credentials[fieldName] = value;
|
|
34
|
+
}
|
|
35
|
+
return Object.entries(credentials)
|
|
36
|
+
.map(([name, value]) => ({ name, value }))
|
|
37
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const pushCredentialCommand = SimpleCLI.command({
|
|
41
|
+
description: "Push LIBRETTO_CLOUD-prefixed env credentials to Libretto Cloud",
|
|
42
|
+
})
|
|
43
|
+
.input(SimpleCLI.input({
|
|
44
|
+
positionals: [],
|
|
45
|
+
named: {},
|
|
46
|
+
}))
|
|
47
|
+
.handle(async () => {
|
|
48
|
+
const credentials = parseEnvCredentials(CLOUD_CREDENTIAL_ENV_PREFIX);
|
|
49
|
+
if (credentials.length === 0) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`No non-empty env vars found with prefix ${CLOUD_CREDENTIAL_ENV_PREFIX}.`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const { apiUrl, credential } = requireApiKeyCredential();
|
|
55
|
+
let created = 0;
|
|
56
|
+
let updated = 0;
|
|
57
|
+
for (const item of credentials) {
|
|
58
|
+
const response = await orpcCall<UpsertCredentialResponse>({
|
|
59
|
+
apiUrl,
|
|
60
|
+
path: "/v1/credentials/upsert",
|
|
61
|
+
input: item,
|
|
62
|
+
credential,
|
|
63
|
+
});
|
|
64
|
+
if (response.overwritten) {
|
|
65
|
+
updated += 1;
|
|
66
|
+
console.log(`Updated cloud credential: ${item.name}`);
|
|
67
|
+
} else {
|
|
68
|
+
created += 1;
|
|
69
|
+
console.log(`Created cloud credential: ${item.name}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
console.log(
|
|
73
|
+
`Pushed ${credentials.length} cloud ${credentials.length === 1 ? "credential" : "credentials"} (${created} created, ${updated} updated).`,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const cloudCredentialCommands = SimpleCLI.group({
|
|
78
|
+
description: "Manage hosted credentials",
|
|
79
|
+
routes: {
|
|
80
|
+
push: pushCredentialCommand,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -4,7 +4,10 @@ import {
|
|
|
4
4
|
orpcCall,
|
|
5
5
|
resolveApiUrl,
|
|
6
6
|
} from "../core/auth-fetch.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
buildHostedDeployTarball,
|
|
9
|
+
type WorkflowDeployMetadata,
|
|
10
|
+
} from "../core/deploy-artifact.js";
|
|
8
11
|
import { readAuthState } from "../core/auth-storage.js";
|
|
9
12
|
import { SimpleCLI } from "affordance";
|
|
10
13
|
|
|
@@ -19,6 +22,13 @@ type DeploymentResponse = {
|
|
|
19
22
|
};
|
|
20
23
|
};
|
|
21
24
|
|
|
25
|
+
type EnsureProfileResponse = {
|
|
26
|
+
success: true;
|
|
27
|
+
profile_id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
created: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
22
32
|
function generateDeploymentName(): string {
|
|
23
33
|
return `deploy-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
24
34
|
}
|
|
@@ -111,6 +121,33 @@ async function pollDeployment(
|
|
|
111
121
|
return deployment;
|
|
112
122
|
}
|
|
113
123
|
|
|
124
|
+
async function ensureWorkflowAuthProfiles(args: {
|
|
125
|
+
apiUrl: string;
|
|
126
|
+
credential: { source: "env-api-key"; apiKey: string };
|
|
127
|
+
workflows: readonly WorkflowDeployMetadata[];
|
|
128
|
+
}): Promise<void> {
|
|
129
|
+
const profileNames = [
|
|
130
|
+
...new Set(
|
|
131
|
+
args.workflows
|
|
132
|
+
.map((workflow) => workflow.authProfileName?.trim())
|
|
133
|
+
.filter((name): name is string => Boolean(name)),
|
|
134
|
+
),
|
|
135
|
+
];
|
|
136
|
+
if (profileNames.length === 0) return;
|
|
137
|
+
|
|
138
|
+
console.log(
|
|
139
|
+
`Ensuring cloud auth ${profileNames.length === 1 ? "profile" : "profiles"}: ${profileNames.join(", ")}`,
|
|
140
|
+
);
|
|
141
|
+
for (const name of profileNames) {
|
|
142
|
+
await orpcCall<EnsureProfileResponse>({
|
|
143
|
+
apiUrl: args.apiUrl,
|
|
144
|
+
path: "/v1/browserProfiles/ensure",
|
|
145
|
+
input: { name },
|
|
146
|
+
credential: args.credential,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
114
151
|
export const deployInput = SimpleCLI.input({
|
|
115
152
|
positionals: [
|
|
116
153
|
SimpleCLI.positional("sourceDir", z.string().default("."), {
|
|
@@ -159,13 +196,15 @@ export const deployCommand = SimpleCLI.command({
|
|
|
159
196
|
// a minimal manifest. Bundled code is embedded in the generated files;
|
|
160
197
|
// external packages are listed in the manifest for installation.
|
|
161
198
|
console.log("Bundling hosted deployment artifact...");
|
|
162
|
-
const { entryPoint, source } = await buildHostedDeployTarball({
|
|
199
|
+
const { entryPoint, source, workflows } = await buildHostedDeployTarball({
|
|
163
200
|
additionalExternals: input.external,
|
|
164
201
|
deploymentName,
|
|
165
202
|
entryPoint: input.entryPoint,
|
|
166
203
|
sourceDir: input.sourceDir,
|
|
167
204
|
});
|
|
168
205
|
|
|
206
|
+
await ensureWorkflowAuthProfiles({ apiUrl, credential, workflows });
|
|
207
|
+
|
|
169
208
|
const createPayload: Record<string, unknown> = {
|
|
170
209
|
source,
|
|
171
210
|
entry_point: entryPoint,
|
|
@@ -6,10 +6,6 @@ import type { LoggerApi } from "../../shared/logger/index.js";
|
|
|
6
6
|
import {
|
|
7
7
|
connect,
|
|
8
8
|
disconnectBrowser,
|
|
9
|
-
getProfilePath,
|
|
10
|
-
hasProfile,
|
|
11
|
-
normalizeDomain,
|
|
12
|
-
normalizeUrl,
|
|
13
9
|
runClose,
|
|
14
10
|
resolveViewport,
|
|
15
11
|
} from "../core/browser.js";
|
|
@@ -32,7 +28,9 @@ import {
|
|
|
32
28
|
getProviderStartupTimeoutMs,
|
|
33
29
|
resolveProviderName,
|
|
34
30
|
} from "../core/providers/index.js";
|
|
35
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
getAbsoluteIntegrationPath,
|
|
33
|
+
} from "../core/workflow-runtime.js";
|
|
36
34
|
import {
|
|
37
35
|
compileExecFunction,
|
|
38
36
|
stripEmptyCatchHandlers,
|
|
@@ -48,12 +46,13 @@ import {
|
|
|
48
46
|
readActionLog,
|
|
49
47
|
readNetworkLog,
|
|
50
48
|
wrapPageForActionLogging,
|
|
51
|
-
} from "../core/
|
|
49
|
+
} from "../core/session-logs.js";
|
|
52
50
|
import type { SessionAccessMode } from "../../shared/state/index.js";
|
|
53
51
|
import type { Experiments } from "../core/experiments.js";
|
|
54
52
|
import { SimpleCLI } from "affordance";
|
|
55
53
|
import {
|
|
56
54
|
pageOption,
|
|
55
|
+
type SessionContext,
|
|
57
56
|
sessionOption,
|
|
58
57
|
withAutoSession,
|
|
59
58
|
withExperiments,
|
|
@@ -68,7 +67,6 @@ type RunIntegrationCommandRequest = {
|
|
|
68
67
|
visualize: boolean;
|
|
69
68
|
viewport?: { width: number; height: number };
|
|
70
69
|
accessMode: SessionAccessMode;
|
|
71
|
-
authProfileDomain?: string;
|
|
72
70
|
providerName?: string;
|
|
73
71
|
stayOpenOnSuccess: boolean;
|
|
74
72
|
tsconfigPath?: string;
|
|
@@ -564,22 +562,6 @@ async function runIntegrationFromFile(
|
|
|
564
562
|
const absoluteIntegrationPath = getAbsoluteIntegrationPath(
|
|
565
563
|
args.integrationPath,
|
|
566
564
|
);
|
|
567
|
-
if (args.authProfileDomain) {
|
|
568
|
-
const normalizedDomain = normalizeDomain(normalizeUrl(args.authProfileDomain));
|
|
569
|
-
if (!hasProfile(normalizedDomain)) {
|
|
570
|
-
const profilePath = getProfilePath(normalizedDomain);
|
|
571
|
-
throw new Error(
|
|
572
|
-
[
|
|
573
|
-
`Local auth profile not found for domain "${normalizedDomain}".`,
|
|
574
|
-
`Expected profile file: ${profilePath}`,
|
|
575
|
-
"To create it:",
|
|
576
|
-
` 1. libretto open https://${normalizedDomain} --headed --session ${args.session}`,
|
|
577
|
-
" 2. Log in manually in the browser window.",
|
|
578
|
-
` 3. libretto save ${normalizedDomain} --session ${args.session}`,
|
|
579
|
-
].join("\n"),
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
565
|
|
|
584
566
|
const runLogPath = logFileForSession(args.session);
|
|
585
567
|
const workflowOutcome = createDeferred<WorkflowOutcome>();
|
|
@@ -594,7 +576,10 @@ async function runIntegrationFromFile(
|
|
|
594
576
|
session: args.session,
|
|
595
577
|
experiments: args.experiments,
|
|
596
578
|
browser: args.providerName
|
|
597
|
-
? {
|
|
579
|
+
? {
|
|
580
|
+
kind: "provider",
|
|
581
|
+
providerName: args.providerName,
|
|
582
|
+
}
|
|
598
583
|
: {
|
|
599
584
|
kind: "launch",
|
|
600
585
|
headed: !args.headless,
|
|
@@ -606,7 +591,6 @@ async function runIntegrationFromFile(
|
|
|
606
591
|
visualize: args.visualize,
|
|
607
592
|
stayOpenOnSuccess: args.stayOpenOnSuccess,
|
|
608
593
|
tsconfigPath: args.tsconfigPath,
|
|
609
|
-
authProfileDomain: args.authProfileDomain,
|
|
610
594
|
},
|
|
611
595
|
},
|
|
612
596
|
logger,
|
|
@@ -809,10 +793,6 @@ export const runInput = SimpleCLI.input({
|
|
|
809
793
|
name: "stay-open-on-success",
|
|
810
794
|
help: "Keep the browser session open after the workflow completes successfully",
|
|
811
795
|
}),
|
|
812
|
-
authProfile: SimpleCLI.option(z.string().optional(), {
|
|
813
|
-
name: "auth-profile",
|
|
814
|
-
help: "Domain for local auth profile (e.g. apps.example.com)",
|
|
815
|
-
}),
|
|
816
796
|
viewport: SimpleCLI.option(z.string().optional(), {
|
|
817
797
|
help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)",
|
|
818
798
|
}),
|
|
@@ -865,7 +845,7 @@ export const runCommand = SimpleCLI.command({
|
|
|
865
845
|
})
|
|
866
846
|
.input(runInput)
|
|
867
847
|
.use(withAutoSession())
|
|
868
|
-
.use(withExperiments())
|
|
848
|
+
.use(withExperiments<SessionContext>())
|
|
869
849
|
.handle(async ({ input, ctx }) => {
|
|
870
850
|
warnIfLibrettoVersionsDiffer();
|
|
871
851
|
await stopExistingFailedRunSession(ctx.session, ctx.logger);
|
|
@@ -903,7 +883,6 @@ export const runCommand = SimpleCLI.command({
|
|
|
903
883
|
tsconfigPath: input.tsconfig,
|
|
904
884
|
headless: daemonProviderName ? true : (headlessMode ?? false),
|
|
905
885
|
visualize,
|
|
906
|
-
authProfileDomain: input.authProfile,
|
|
907
886
|
viewport,
|
|
908
887
|
accessMode: input.readOnly ? "read-only" : input.writeAccess ? "write-access" : (readLibrettoConfig().sessionMode ?? "write-access"),
|
|
909
888
|
providerName: daemonProviderName,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SimpleCLI } from "affordance";
|
|
3
|
+
import { createLoggerForSession } from "../core/context.js";
|
|
4
|
+
import { runFetchChromeProfile } from "../core/browser.js";
|
|
5
|
+
import { promptConfirm } from "../core/prompt.js";
|
|
6
|
+
|
|
7
|
+
export const importChromeProfilesCommand = SimpleCLI.command({
|
|
8
|
+
description: "Fetch scoped auth state from a Chrome CDP session into a local profile",
|
|
9
|
+
})
|
|
10
|
+
.input(SimpleCLI.input({
|
|
11
|
+
positionals: [
|
|
12
|
+
SimpleCLI.positional("profileName", z.string(), {
|
|
13
|
+
help: "Profile name to save",
|
|
14
|
+
}),
|
|
15
|
+
],
|
|
16
|
+
named: {
|
|
17
|
+
cdpUrl: SimpleCLI.option(z.string(), {
|
|
18
|
+
name: "cdp-url",
|
|
19
|
+
help: "Chrome DevTools Protocol endpoint for the Chrome instance",
|
|
20
|
+
}),
|
|
21
|
+
sites: SimpleCLI.option(z.string(), {
|
|
22
|
+
help: "Comma-separated sites whose auth state should be imported",
|
|
23
|
+
}),
|
|
24
|
+
yes: SimpleCLI.flag({
|
|
25
|
+
help: "Skip confirmation before attaching to and disconnecting from Chrome",
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
}))
|
|
29
|
+
.handle(async ({ input }) => {
|
|
30
|
+
const logger = createLoggerForSession(`profile-fetch-${Date.now()}`);
|
|
31
|
+
try {
|
|
32
|
+
if (!input.yes) {
|
|
33
|
+
const confirmed = await promptConfirm(
|
|
34
|
+
"Importing from an existing Chrome CDP session may cause that Chrome window to close or relaunch when Libretto disconnects. Continue?",
|
|
35
|
+
);
|
|
36
|
+
if (!confirmed) {
|
|
37
|
+
throw new Error("Aborted Chrome profile import.");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
await runFetchChromeProfile(input.profileName, input.cdpUrl, logger, {
|
|
41
|
+
sites: input.sites,
|
|
42
|
+
});
|
|
43
|
+
} finally {
|
|
44
|
+
await logger.close();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SimpleCLI } from "affordance";
|
|
3
|
+
import { orpcCall, resolveApiUrl } from "../core/auth-fetch.js";
|
|
4
|
+
import { normalizeProfileName } from "../core/profiles.js";
|
|
5
|
+
|
|
6
|
+
type ListProfilesResponse = {
|
|
7
|
+
profiles: Array<{
|
|
8
|
+
profile_id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
providers: string[];
|
|
11
|
+
updated_at: string;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type DeleteProfileResponse = {
|
|
16
|
+
success: boolean;
|
|
17
|
+
deleted_count: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function requireApiKeyCredential() {
|
|
21
|
+
const apiKey = process.env.LIBRETTO_API_KEY?.trim();
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"LIBRETTO_API_KEY is required to manage Libretto Cloud profiles. Issue one with `libretto cloud auth api-key issue --label <label>`.",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
apiUrl: resolveApiUrl(null),
|
|
29
|
+
credential: { source: "env-api-key" as const, apiKey },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const listProfilesCommand = SimpleCLI.command({
|
|
34
|
+
description: "List Libretto Cloud auth profiles",
|
|
35
|
+
})
|
|
36
|
+
.input(SimpleCLI.input({ positionals: [], named: {} }))
|
|
37
|
+
.handle(async () => {
|
|
38
|
+
const { apiUrl, credential } = requireApiKeyCredential();
|
|
39
|
+
const response = await orpcCall<ListProfilesResponse>({
|
|
40
|
+
apiUrl,
|
|
41
|
+
path: "/v1/browserProfiles/list",
|
|
42
|
+
input: {},
|
|
43
|
+
credential,
|
|
44
|
+
});
|
|
45
|
+
if (response.profiles.length === 0) {
|
|
46
|
+
console.log("No cloud profiles found.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const profile of response.profiles) {
|
|
50
|
+
const providers = profile.providers.length
|
|
51
|
+
? ` (${profile.providers.join(", ")})`
|
|
52
|
+
: "";
|
|
53
|
+
console.log(`${profile.name}${providers}`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const deleteProfileCommand = SimpleCLI.command({
|
|
58
|
+
description: "Delete a Libretto Cloud auth profile",
|
|
59
|
+
})
|
|
60
|
+
.input(SimpleCLI.input({
|
|
61
|
+
positionals: [
|
|
62
|
+
SimpleCLI.positional("profileName", z.string(), {
|
|
63
|
+
help: "Cloud profile name to delete",
|
|
64
|
+
}),
|
|
65
|
+
],
|
|
66
|
+
named: {},
|
|
67
|
+
}))
|
|
68
|
+
.handle(async ({ input }) => {
|
|
69
|
+
const profileName = normalizeProfileName(input.profileName);
|
|
70
|
+
const { apiUrl, credential } = requireApiKeyCredential();
|
|
71
|
+
const response = await orpcCall<DeleteProfileResponse>({
|
|
72
|
+
apiUrl,
|
|
73
|
+
path: "/v1/browserProfiles/delete",
|
|
74
|
+
input: { name: profileName },
|
|
75
|
+
credential,
|
|
76
|
+
});
|
|
77
|
+
if (!response.success || response.deleted_count === 0) {
|
|
78
|
+
console.log(`No cloud profile found for ${profileName}.`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log(`Deleted cloud profile: ${profileName}`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const profileCommands = SimpleCLI.group({
|
|
85
|
+
description: "Manage hosted browser auth profiles",
|
|
86
|
+
routes: {
|
|
87
|
+
list: listProfilesCommand,
|
|
88
|
+
delete: deleteProfileCommand,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
} from "../core/session.js";
|
|
12
12
|
import {
|
|
13
13
|
SimpleCLI,
|
|
14
|
-
type SimpleCLIMiddlewareArgs,
|
|
15
14
|
type SimpleCLIContext,
|
|
16
15
|
type SimpleCLIMiddleware,
|
|
17
16
|
} from "affordance";
|
|
@@ -41,13 +40,10 @@ export type ExperimentsContext = {
|
|
|
41
40
|
experiments: Experiments;
|
|
42
41
|
};
|
|
43
42
|
|
|
44
|
-
export function withExperiments
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
TInput,
|
|
49
|
-
TContext
|
|
50
|
-
>): Promise<TContext & ExperimentsContext> => ({
|
|
43
|
+
export function withExperiments<
|
|
44
|
+
TContext extends SimpleCLIContext,
|
|
45
|
+
>(): SimpleCLIMiddleware<unknown, TContext, TContext & ExperimentsContext> {
|
|
46
|
+
return async ({ ctx }) => ({
|
|
51
47
|
...ctx,
|
|
52
48
|
experiments: resolveExperiments(),
|
|
53
49
|
});
|