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