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
|
@@ -1,5 +1,6 @@
|
|
|
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
|
|
|
4
5
|
const CLOUD_CREDENTIAL_ENV_PREFIX = "LIBRETTO_CLOUD_";
|
|
5
6
|
|
|
@@ -10,19 +11,6 @@ type UpsertCredentialResponse = {
|
|
|
10
11
|
message: string;
|
|
11
12
|
};
|
|
12
13
|
|
|
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
14
|
function parseEnvCredentials(prefix: string): Array<{ name: string; value: string }> {
|
|
27
15
|
const credentials: Record<string, string> = {};
|
|
28
16
|
for (const [key, value] of Object.entries(process.env)) {
|
|
@@ -44,22 +32,22 @@ export const pushCredentialCommand = SimpleCLI.command({
|
|
|
44
32
|
positionals: [],
|
|
45
33
|
named: {},
|
|
46
34
|
}))
|
|
47
|
-
.
|
|
35
|
+
.use(withCloudApiKey("manage Libretto Cloud credentials"))
|
|
36
|
+
.handle(async ({ ctx }) => {
|
|
48
37
|
const credentials = parseEnvCredentials(CLOUD_CREDENTIAL_ENV_PREFIX);
|
|
49
38
|
if (credentials.length === 0) {
|
|
50
39
|
throw new Error(
|
|
51
40
|
`No non-empty env vars found with prefix ${CLOUD_CREDENTIAL_ENV_PREFIX}.`,
|
|
52
41
|
);
|
|
53
42
|
}
|
|
54
|
-
const { apiUrl, credential } = requireApiKeyCredential();
|
|
55
43
|
let created = 0;
|
|
56
44
|
let updated = 0;
|
|
57
45
|
for (const item of credentials) {
|
|
58
46
|
const response = await orpcCall<UpsertCredentialResponse>({
|
|
59
|
-
apiUrl,
|
|
47
|
+
apiUrl: ctx.apiUrl,
|
|
60
48
|
path: "/v1/credentials/upsert",
|
|
61
49
|
input: item,
|
|
62
|
-
credential,
|
|
50
|
+
credential: ctx.credential,
|
|
63
51
|
});
|
|
64
52
|
if (response.overwritten) {
|
|
65
53
|
updated += 1;
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
|
|
7
|
+
type JobStatus = "queued" | "starting_browser" | "running";
|
|
8
|
+
|
|
9
|
+
type CreateJobResponse = {
|
|
10
|
+
success: true;
|
|
11
|
+
job_id: string;
|
|
12
|
+
status: JobStatus;
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const createJobUsage =
|
|
17
|
+
"Usage: libretto cloud jobs create <workflow> [--params <json> | --params-file <path>]";
|
|
18
|
+
|
|
19
|
+
function parseJsonObject(label: string, raw: string): Record<string, unknown> {
|
|
20
|
+
let parsed: unknown;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(raw);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Invalid JSON in ${label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
29
|
+
throw new Error(`${label} must be a JSON object.`);
|
|
30
|
+
}
|
|
31
|
+
return parsed as Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readJsonObjectFile(
|
|
35
|
+
label: string,
|
|
36
|
+
filePath: string,
|
|
37
|
+
): Record<string, unknown> {
|
|
38
|
+
let content: string;
|
|
39
|
+
try {
|
|
40
|
+
content = readFileSync(filePath, "utf8");
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Could not read ${label} "${filePath}". Ensure the file exists and is readable.`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return parseJsonObject(label, content);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const createCloudJobInput = SimpleCLI.input({
|
|
50
|
+
positionals: [
|
|
51
|
+
SimpleCLI.positional("workflow", z.string().optional(), {
|
|
52
|
+
help: "Deployed workflow name to run",
|
|
53
|
+
}),
|
|
54
|
+
],
|
|
55
|
+
named: {
|
|
56
|
+
params: SimpleCLI.option(z.string().optional(), {
|
|
57
|
+
help: "Inline JSON params object",
|
|
58
|
+
}),
|
|
59
|
+
paramsFile: SimpleCLI.option(z.string().optional(), {
|
|
60
|
+
name: "params-file",
|
|
61
|
+
help: "Path to a JSON params file",
|
|
62
|
+
}),
|
|
63
|
+
credentialId: SimpleCLI.option(z.string().optional(), {
|
|
64
|
+
name: "credential-id",
|
|
65
|
+
help: "Stored cloud credential id to pass to the workflow",
|
|
66
|
+
}),
|
|
67
|
+
timeoutSeconds: SimpleCLI.option(z.coerce.number().int().min(1).optional(), {
|
|
68
|
+
name: "timeout-seconds",
|
|
69
|
+
help: "Job timeout in seconds",
|
|
70
|
+
}),
|
|
71
|
+
headed: SimpleCLI.flag({ help: "Run browser in headed mode" }),
|
|
72
|
+
headless: SimpleCLI.flag({ help: "Run browser in headless mode" }),
|
|
73
|
+
callbackUrl: SimpleCLI.option(z.string().optional(), {
|
|
74
|
+
name: "callback-url",
|
|
75
|
+
help: "Per-job callback URL",
|
|
76
|
+
}),
|
|
77
|
+
callbackSecret: SimpleCLI.option(z.string().optional(), {
|
|
78
|
+
name: "callback-secret",
|
|
79
|
+
help: "Secret used to sign the per-job callback",
|
|
80
|
+
}),
|
|
81
|
+
skipCallbacks: SimpleCLI.flag({
|
|
82
|
+
name: "skip-callbacks",
|
|
83
|
+
help: "Skip stored webhook callbacks for this job",
|
|
84
|
+
}),
|
|
85
|
+
residentialProxy: SimpleCLI.option(z.string().optional(), {
|
|
86
|
+
name: "residential-proxy",
|
|
87
|
+
help: "Residential proxy config as a JSON object",
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
.refine((input) => Boolean(input.workflow), createJobUsage)
|
|
92
|
+
.refine(
|
|
93
|
+
(input) => !(input.params && input.paramsFile),
|
|
94
|
+
"Pass either --params or --params-file, not both.",
|
|
95
|
+
)
|
|
96
|
+
.refine(
|
|
97
|
+
(input) => !(input.headed && input.headless),
|
|
98
|
+
"Cannot pass both --headed and --headless.",
|
|
99
|
+
)
|
|
100
|
+
.refine(
|
|
101
|
+
(input) =>
|
|
102
|
+
(!input.callbackUrl && !input.callbackSecret) ||
|
|
103
|
+
Boolean(input.callbackUrl && input.callbackSecret),
|
|
104
|
+
"Pass both --callback-url and --callback-secret, or omit both.",
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
export const createCloudJobCommand = SimpleCLI.command({
|
|
108
|
+
description: "Create a Libretto Cloud job for a deployed workflow",
|
|
109
|
+
})
|
|
110
|
+
.input(createCloudJobInput)
|
|
111
|
+
.use(withCloudApiKey("create Libretto Cloud jobs"))
|
|
112
|
+
.handle(async ({ input, ctx }) => {
|
|
113
|
+
const params = input.paramsFile
|
|
114
|
+
? readJsonObjectFile("--params-file", input.paramsFile)
|
|
115
|
+
: input.params
|
|
116
|
+
? parseJsonObject("--params", input.params)
|
|
117
|
+
: {};
|
|
118
|
+
const residentialProxy = input.residentialProxy
|
|
119
|
+
? parseJsonObject("--residential-proxy", input.residentialProxy)
|
|
120
|
+
: undefined;
|
|
121
|
+
|
|
122
|
+
const payload: Record<string, unknown> = {
|
|
123
|
+
workflow: input.workflow!,
|
|
124
|
+
params,
|
|
125
|
+
};
|
|
126
|
+
if (input.credentialId) payload.credential_id = input.credentialId;
|
|
127
|
+
if (input.timeoutSeconds !== undefined) {
|
|
128
|
+
payload.timeout_seconds = input.timeoutSeconds;
|
|
129
|
+
}
|
|
130
|
+
if (input.headed) payload.headless = false;
|
|
131
|
+
if (input.headless) payload.headless = true;
|
|
132
|
+
if (input.callbackUrl) payload.callback_url = input.callbackUrl;
|
|
133
|
+
if (input.callbackSecret) payload.callback_secret = input.callbackSecret;
|
|
134
|
+
if (input.skipCallbacks) payload.skip_callbacks = true;
|
|
135
|
+
if (residentialProxy !== undefined) {
|
|
136
|
+
payload.residential_proxy = residentialProxy;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const response = await orpcCall<CreateJobResponse>({
|
|
140
|
+
apiUrl: ctx.apiUrl,
|
|
141
|
+
path: "/v1/jobs/create",
|
|
142
|
+
input: payload,
|
|
143
|
+
credential: ctx.credential,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
console.log(`Job created: ${response.job_id}`);
|
|
147
|
+
console.log(`Status: ${response.status}`);
|
|
148
|
+
console.log(response.message);
|
|
149
|
+
return response.job_id;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export const cloudJobCommands = SimpleCLI.group({
|
|
153
|
+
description: "Create and manage hosted jobs",
|
|
154
|
+
routes: {
|
|
155
|
+
create: createCloudJobCommand,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
|
|
7
|
+
type ScheduleResponse = {
|
|
8
|
+
id: string;
|
|
9
|
+
workflow: string;
|
|
10
|
+
cron_expr: string;
|
|
11
|
+
timezone: string;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
next_fire_at: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type CreateScheduleResponse = {
|
|
17
|
+
success: true;
|
|
18
|
+
schedule: ScheduleResponse;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const createScheduleUsage =
|
|
22
|
+
"Usage: libretto cloud schedules create <workflow> --cron <expr> [--params <json> | --params-file <path>]";
|
|
23
|
+
|
|
24
|
+
function parseJsonObject(label: string, raw: string): Record<string, unknown> {
|
|
25
|
+
let parsed: unknown;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(raw);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Invalid JSON in ${label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
34
|
+
throw new Error(`${label} must be a JSON object.`);
|
|
35
|
+
}
|
|
36
|
+
return parsed as Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readJsonObjectFile(
|
|
40
|
+
label: string,
|
|
41
|
+
filePath: string,
|
|
42
|
+
): Record<string, unknown> {
|
|
43
|
+
let content: string;
|
|
44
|
+
try {
|
|
45
|
+
content = readFileSync(filePath, "utf8");
|
|
46
|
+
} catch {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Could not read ${label} "${filePath}". Ensure the file exists and is readable.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return parseJsonObject(label, content);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const createCloudScheduleInput = SimpleCLI.input({
|
|
55
|
+
positionals: [
|
|
56
|
+
SimpleCLI.positional("workflow", z.string().optional(), {
|
|
57
|
+
help: "Deployed workflow name to schedule",
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
named: {
|
|
61
|
+
cron: SimpleCLI.option(z.string().optional(), {
|
|
62
|
+
help: "Standard 5-field cron expression",
|
|
63
|
+
}),
|
|
64
|
+
timezone: SimpleCLI.option(z.string().optional(), {
|
|
65
|
+
help: "IANA timezone name (default: UTC)",
|
|
66
|
+
}),
|
|
67
|
+
params: SimpleCLI.option(z.string().optional(), {
|
|
68
|
+
help: "Inline JSON params object",
|
|
69
|
+
}),
|
|
70
|
+
paramsFile: SimpleCLI.option(z.string().optional(), {
|
|
71
|
+
name: "params-file",
|
|
72
|
+
help: "Path to a JSON params file",
|
|
73
|
+
}),
|
|
74
|
+
timeoutSeconds: SimpleCLI.option(z.coerce.number().int().min(1).optional(), {
|
|
75
|
+
name: "timeout-seconds",
|
|
76
|
+
help: "Job timeout in seconds for each schedule fire",
|
|
77
|
+
}),
|
|
78
|
+
callbackUrl: SimpleCLI.option(z.string().optional(), {
|
|
79
|
+
name: "callback-url",
|
|
80
|
+
help: "Per-schedule callback URL",
|
|
81
|
+
}),
|
|
82
|
+
callbackSecret: SimpleCLI.option(z.string().optional(), {
|
|
83
|
+
name: "callback-secret",
|
|
84
|
+
help: "Secret used to sign per-schedule callbacks",
|
|
85
|
+
}),
|
|
86
|
+
skipCallbacks: SimpleCLI.flag({
|
|
87
|
+
name: "skip-callbacks",
|
|
88
|
+
help: "Skip stored webhook callbacks for jobs created by this schedule",
|
|
89
|
+
}),
|
|
90
|
+
residentialProxy: SimpleCLI.option(z.string().optional(), {
|
|
91
|
+
name: "residential-proxy",
|
|
92
|
+
help: "Residential proxy config as a JSON object",
|
|
93
|
+
}),
|
|
94
|
+
disabled: SimpleCLI.flag({
|
|
95
|
+
help: "Create the schedule disabled",
|
|
96
|
+
}),
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
.refine((input) => Boolean(input.workflow && input.cron), createScheduleUsage)
|
|
100
|
+
.refine(
|
|
101
|
+
(input) => !(input.params && input.paramsFile),
|
|
102
|
+
"Pass either --params or --params-file, not both.",
|
|
103
|
+
)
|
|
104
|
+
.refine(
|
|
105
|
+
(input) =>
|
|
106
|
+
(!input.callbackUrl && !input.callbackSecret) ||
|
|
107
|
+
Boolean(input.callbackUrl && input.callbackSecret),
|
|
108
|
+
"Pass both --callback-url and --callback-secret, or omit both.",
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
export const createCloudScheduleCommand = SimpleCLI.command({
|
|
112
|
+
description: "Create a recurring schedule for a deployed workflow",
|
|
113
|
+
})
|
|
114
|
+
.input(createCloudScheduleInput)
|
|
115
|
+
.use(withCloudApiKey("create Libretto Cloud schedules"))
|
|
116
|
+
.handle(async ({ input, ctx }) => {
|
|
117
|
+
const params = input.paramsFile
|
|
118
|
+
? readJsonObjectFile("--params-file", input.paramsFile)
|
|
119
|
+
: input.params
|
|
120
|
+
? parseJsonObject("--params", input.params)
|
|
121
|
+
: {};
|
|
122
|
+
const residentialProxy = input.residentialProxy
|
|
123
|
+
? parseJsonObject("--residential-proxy", input.residentialProxy)
|
|
124
|
+
: undefined;
|
|
125
|
+
|
|
126
|
+
const payload: Record<string, unknown> = {
|
|
127
|
+
workflow: input.workflow!,
|
|
128
|
+
cron_expr: input.cron!,
|
|
129
|
+
timezone: input.timezone ?? "UTC",
|
|
130
|
+
params,
|
|
131
|
+
enabled: !input.disabled,
|
|
132
|
+
};
|
|
133
|
+
if (input.timeoutSeconds !== undefined) {
|
|
134
|
+
payload.timeout_seconds = input.timeoutSeconds;
|
|
135
|
+
}
|
|
136
|
+
if (input.callbackUrl) payload.callback_url = input.callbackUrl;
|
|
137
|
+
if (input.callbackSecret) payload.callback_secret = input.callbackSecret;
|
|
138
|
+
if (input.skipCallbacks) payload.skip_callbacks = true;
|
|
139
|
+
if (residentialProxy !== undefined) {
|
|
140
|
+
payload.residential_proxy = residentialProxy;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const response = await orpcCall<CreateScheduleResponse>({
|
|
144
|
+
apiUrl: ctx.apiUrl,
|
|
145
|
+
path: "/v1/schedules/create",
|
|
146
|
+
input: payload,
|
|
147
|
+
credential: ctx.credential,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const { schedule } = response;
|
|
151
|
+
console.log(`Schedule created: ${schedule.id}`);
|
|
152
|
+
console.log(`Workflow: ${schedule.workflow}`);
|
|
153
|
+
console.log(`Cron: ${schedule.cron_expr} (${schedule.timezone})`);
|
|
154
|
+
console.log(`Next fire: ${schedule.next_fire_at}`);
|
|
155
|
+
console.log(`Enabled: ${schedule.enabled ? "yes" : "no"}`);
|
|
156
|
+
return schedule.id;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
export const cloudScheduleCommands = SimpleCLI.group({
|
|
160
|
+
description: "Create and manage hosted schedules",
|
|
161
|
+
routes: {
|
|
162
|
+
create: createCloudScheduleCommand,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SimpleCLI } from "affordance";
|
|
3
|
+
import { orpcCall } from "../core/auth-fetch.js";
|
|
4
|
+
import { withCloudApiKey, type CloudApiKeyContext } from "./shared.js";
|
|
5
|
+
|
|
6
|
+
type TenantSettingsResponse = {
|
|
7
|
+
code_sharing_enabled: boolean;
|
|
8
|
+
disable_job_failure_notifications: boolean;
|
|
9
|
+
debug_notification_email: string | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type TenantSettingsInput = {
|
|
13
|
+
code_sharing_enabled?: boolean;
|
|
14
|
+
disable_job_failure_notifications?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const settingState = z.enum(["enabled", "disabled"]);
|
|
18
|
+
|
|
19
|
+
function toBoolean(state: "enabled" | "disabled"): boolean {
|
|
20
|
+
return state === "enabled";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printTenantSettings(
|
|
24
|
+
settings: TenantSettingsResponse,
|
|
25
|
+
): TenantSettingsResponse {
|
|
26
|
+
const notificationsEnabled = !settings.disable_job_failure_notifications;
|
|
27
|
+
console.log(
|
|
28
|
+
`Code sharing: ${settings.code_sharing_enabled ? "enabled" : "disabled"}`,
|
|
29
|
+
);
|
|
30
|
+
console.log(
|
|
31
|
+
`Job failure notifications: ${notificationsEnabled ? "enabled" : "disabled"}`,
|
|
32
|
+
);
|
|
33
|
+
console.log(
|
|
34
|
+
`Notification recipient: ${settings.debug_notification_email ?? "not configured"}`,
|
|
35
|
+
);
|
|
36
|
+
return settings;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function tenantSettings(
|
|
40
|
+
ctx: CloudApiKeyContext,
|
|
41
|
+
input: TenantSettingsInput = {},
|
|
42
|
+
): Promise<TenantSettingsResponse> {
|
|
43
|
+
return orpcCall<TenantSettingsResponse>({
|
|
44
|
+
apiUrl: ctx.apiUrl,
|
|
45
|
+
path: "/v1/tenant/settings",
|
|
46
|
+
input,
|
|
47
|
+
credential: ctx.credential,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const settingsStatusCommand = SimpleCLI.command({
|
|
52
|
+
description: "Show Libretto Cloud tenant settings",
|
|
53
|
+
})
|
|
54
|
+
.input(SimpleCLI.input({ positionals: [], named: {} }))
|
|
55
|
+
.use(withCloudApiKey("manage Libretto Cloud settings"))
|
|
56
|
+
.handle(async ({ ctx }) => printTenantSettings(await tenantSettings(ctx)));
|
|
57
|
+
|
|
58
|
+
export const setSettingsCommand = SimpleCLI.command({
|
|
59
|
+
description: "Update one or more Libretto Cloud tenant settings",
|
|
60
|
+
})
|
|
61
|
+
.input(
|
|
62
|
+
SimpleCLI.input({
|
|
63
|
+
positionals: [],
|
|
64
|
+
named: {
|
|
65
|
+
codeSharing: SimpleCLI.option(settingState.optional(), {
|
|
66
|
+
help: "Set tenant code sharing: enabled or disabled",
|
|
67
|
+
}),
|
|
68
|
+
jobFailureNotifications: SimpleCLI.option(settingState.optional(), {
|
|
69
|
+
help: "Set hosted job failure notification emails: enabled or disabled",
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
.use(withCloudApiKey("manage Libretto Cloud settings"))
|
|
75
|
+
.handle(async ({ input, ctx }) => {
|
|
76
|
+
const updates: TenantSettingsInput = {};
|
|
77
|
+
if (input.codeSharing !== undefined) {
|
|
78
|
+
updates.code_sharing_enabled = toBoolean(input.codeSharing);
|
|
79
|
+
}
|
|
80
|
+
if (input.jobFailureNotifications !== undefined) {
|
|
81
|
+
updates.disable_job_failure_notifications = !toBoolean(
|
|
82
|
+
input.jobFailureNotifications,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (Object.keys(updates).length === 0) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"No settings provided. Use one or more flags, for example `libretto cloud settings set --code-sharing enabled --job-failure-notifications disabled`.",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return printTenantSettings(await tenantSettings(ctx, updates));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const settingsCommands = SimpleCLI.group({
|
|
96
|
+
description: "Manage Libretto Cloud tenant settings",
|
|
97
|
+
routes: {
|
|
98
|
+
status: settingsStatusCommand,
|
|
99
|
+
set: setSettingsCommand,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { SimpleCLI } from "affordance";
|
|
3
|
-
import { orpcCall
|
|
3
|
+
import { orpcCall } from "../core/auth-fetch.js";
|
|
4
|
+
import { withCloudApiKey, type CloudApiKeyContext } from "./shared.js";
|
|
4
5
|
|
|
5
6
|
type CodeSharingStatusResponse = {
|
|
6
7
|
enabled: boolean;
|
|
@@ -14,19 +15,6 @@ type ShareWorkflowResponse = {
|
|
|
14
15
|
code_url: string;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
|
-
function requireCloudApiKey() {
|
|
18
|
-
const apiKey = process.env.LIBRETTO_API_KEY?.trim();
|
|
19
|
-
if (!apiKey) {
|
|
20
|
-
throw new Error(
|
|
21
|
-
"LIBRETTO_API_KEY is required to share Libretto Cloud workflow code. Issue one with `libretto cloud auth api-key issue --label <label>`.",
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
return {
|
|
25
|
-
apiUrl: resolveApiUrl(null),
|
|
26
|
-
credential: { source: "env-api-key" as const, apiKey },
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
18
|
export const shareWorkflowCommand = SimpleCLI.command({
|
|
31
19
|
description: "Share one hosted workflow's code publicly",
|
|
32
20
|
})
|
|
@@ -42,13 +30,13 @@ export const shareWorkflowCommand = SimpleCLI.command({
|
|
|
42
30
|
}),
|
|
43
31
|
},
|
|
44
32
|
}))
|
|
45
|
-
.
|
|
46
|
-
|
|
33
|
+
.use(withCloudApiKey("share Libretto Cloud workflow code"))
|
|
34
|
+
.handle(async ({ input, ctx }) => {
|
|
47
35
|
const response = await orpcCall<ShareWorkflowResponse>({
|
|
48
|
-
apiUrl,
|
|
36
|
+
apiUrl: ctx.apiUrl,
|
|
49
37
|
path: "/v1/workflows/share",
|
|
50
38
|
input: { workflow: input.workflow, refresh: input.refresh },
|
|
51
|
-
credential,
|
|
39
|
+
credential: ctx.credential,
|
|
52
40
|
});
|
|
53
41
|
|
|
54
42
|
if (response.status === "existing") {
|
|
@@ -68,25 +56,27 @@ export const codeSharingStatusCommand = SimpleCLI.command({
|
|
|
68
56
|
description: "Show whether tenant code sharing is enabled",
|
|
69
57
|
})
|
|
70
58
|
.input(SimpleCLI.input({ positionals: [], named: {} }))
|
|
71
|
-
.
|
|
72
|
-
|
|
59
|
+
.use(withCloudApiKey("manage tenant workflow code sharing"))
|
|
60
|
+
.handle(async ({ ctx }) => {
|
|
73
61
|
const response = await orpcCall<CodeSharingStatusResponse>({
|
|
74
|
-
apiUrl,
|
|
62
|
+
apiUrl: ctx.apiUrl,
|
|
75
63
|
path: "/v1/tenant/codeSharing",
|
|
76
64
|
input: {},
|
|
77
|
-
credential,
|
|
65
|
+
credential: ctx.credential,
|
|
78
66
|
});
|
|
79
67
|
console.log(`Code sharing: ${response.enabled ? "enabled" : "disabled"}`);
|
|
80
68
|
return response.enabled;
|
|
81
69
|
});
|
|
82
70
|
|
|
83
|
-
async function updateCodeSharing(
|
|
84
|
-
|
|
71
|
+
async function updateCodeSharing(
|
|
72
|
+
enabled: boolean,
|
|
73
|
+
ctx: CloudApiKeyContext,
|
|
74
|
+
): Promise<boolean> {
|
|
85
75
|
const response = await orpcCall<CodeSharingStatusResponse>({
|
|
86
|
-
apiUrl,
|
|
76
|
+
apiUrl: ctx.apiUrl,
|
|
87
77
|
path: "/v1/tenant/updateCodeSharing",
|
|
88
78
|
input: { enabled },
|
|
89
|
-
credential,
|
|
79
|
+
credential: ctx.credential,
|
|
90
80
|
});
|
|
91
81
|
console.log(`Code sharing: ${response.enabled ? "enabled" : "disabled"}`);
|
|
92
82
|
return response.enabled;
|
|
@@ -96,13 +86,15 @@ export const enableCodeSharingCommand = SimpleCLI.command({
|
|
|
96
86
|
description: "Enable public workflow code sharing for this tenant",
|
|
97
87
|
})
|
|
98
88
|
.input(SimpleCLI.input({ positionals: [], named: {} }))
|
|
99
|
-
.
|
|
89
|
+
.use(withCloudApiKey("manage tenant workflow code sharing"))
|
|
90
|
+
.handle(async ({ ctx }) => updateCodeSharing(true, ctx));
|
|
100
91
|
|
|
101
92
|
export const disableCodeSharingCommand = SimpleCLI.command({
|
|
102
93
|
description: "Disable public workflow code sharing for this tenant",
|
|
103
94
|
})
|
|
104
95
|
.input(SimpleCLI.input({ positionals: [], named: {} }))
|
|
105
|
-
.
|
|
96
|
+
.use(withCloudApiKey("manage tenant workflow code sharing"))
|
|
97
|
+
.handle(async ({ ctx }) => updateCodeSharing(false, ctx));
|
|
106
98
|
|
|
107
99
|
export const codeSharingCommands = SimpleCLI.group({
|
|
108
100
|
description: "Manage tenant workflow code sharing",
|
|
@@ -1,15 +1,13 @@
|
|
|
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
|
type WorkflowDeployMetadata,
|
|
10
7
|
} from "../core/deploy-artifact.js";
|
|
11
8
|
import { readAuthState } from "../core/auth-storage.js";
|
|
12
9
|
import { SimpleCLI } from "affordance";
|
|
10
|
+
import { withCloudApiKey } from "./shared.js";
|
|
13
11
|
|
|
14
12
|
type DeploymentStatus = "building" | "ready" | "failed";
|
|
15
13
|
|
|
@@ -53,19 +51,6 @@ function deployApiKeyRequiredMessage(hasStoredSession: boolean): string {
|
|
|
53
51
|
].join("\n");
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
async function requireDeployApiKey() {
|
|
57
|
-
const apiKey = process.env.LIBRETTO_API_KEY?.trim();
|
|
58
|
-
|
|
59
|
-
if (!apiKey) {
|
|
60
|
-
throw new Error(deployApiKeyRequiredMessage(await hasStoredCloudSession()));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
apiUrl: resolveApiUrl(null),
|
|
65
|
-
credential: { source: "env-api-key" as const, apiKey },
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
54
|
async function hasStoredCloudSession(): Promise<boolean> {
|
|
70
55
|
try {
|
|
71
56
|
return Boolean((await readAuthState())?.session);
|
|
@@ -188,8 +173,12 @@ export const deployCommand = SimpleCLI.command({
|
|
|
188
173
|
description: "Deploy workflows to the hosted platform",
|
|
189
174
|
})
|
|
190
175
|
.input(deployInput)
|
|
191
|
-
.
|
|
192
|
-
|
|
176
|
+
.use(withCloudApiKey(
|
|
177
|
+
"deploy to Libretto Cloud",
|
|
178
|
+
async () => deployApiKeyRequiredMessage(await hasStoredCloudSession()),
|
|
179
|
+
))
|
|
180
|
+
.handle(async ({ input, ctx }) => {
|
|
181
|
+
const { apiUrl, credential } = ctx;
|
|
193
182
|
const deploymentName = generateDeploymentName();
|
|
194
183
|
|
|
195
184
|
// Hosted deploy uploads a generated artifact with a deploy entrypoint and
|
|
@@ -92,6 +92,7 @@ export function createRunBrowserConfig(args: {
|
|
|
92
92
|
return {
|
|
93
93
|
kind: "provider",
|
|
94
94
|
providerName: args.providerName,
|
|
95
|
+
headless: args.headless,
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -895,7 +896,7 @@ export const runCommand = SimpleCLI.command({
|
|
|
895
896
|
console.log(`Connecting to ${providerName} browser...`);
|
|
896
897
|
}
|
|
897
898
|
|
|
898
|
-
const headless =
|
|
899
|
+
const headless = headlessMode ?? false;
|
|
899
900
|
const windowPosition = headless
|
|
900
901
|
? undefined
|
|
901
902
|
: resolveWindowPosition(ctx.logger);
|