libretto 0.6.30 → 0.6.32

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.
@@ -2,6 +2,9 @@ import { authCommands } from "./commands/auth.js";
2
2
  import { billingCommands } from "./commands/billing.js";
3
3
  import { browserCommands } from "./commands/browser.js";
4
4
  import { cloudCredentialCommands } from "./commands/cloud-credentials.js";
5
+ import { cloudJobCommands } from "./commands/cloud-jobs.js";
6
+ import { cloudScheduleCommands } from "./commands/cloud-schedules.js";
7
+ import { settingsCommands } from "./commands/cloud-settings.js";
5
8
  import { codeSharingCommands, shareWorkflowCommand } from "./commands/cloud-sharing.js";
6
9
  import { deployCommand } from "./commands/deploy.js";
7
10
  import { executionCommands } from "./commands/execution.js";
@@ -24,7 +27,10 @@ const cliRoutes = {
24
27
  auth: authCommands,
25
28
  billing: billingCommands,
26
29
  credentials: cloudCredentialCommands,
30
+ jobs: cloudJobCommands,
27
31
  profiles: profileCommands,
32
+ schedules: cloudScheduleCommands,
33
+ settings: settingsCommands,
28
34
  share: shareWorkflowCommand,
29
35
  sharing: codeSharingCommands
30
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.6.30",
3
+ "version": "0.6.32",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "homepage": "https://libretto.sh",
@@ -4,7 +4,7 @@ description: "Browser automation CLI for building, maintaining, and running brow
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.30"
7
+ version: "0.6.32"
8
8
  ---
9
9
 
10
10
  ## How Libretto Works
@@ -58,6 +58,7 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
58
58
  - Validation requires a successful clean `run` on a fresh, unauthenticated session with confirmation of the actual returned output, not just process success. Use the same headed or headless mode that the workflow run is already using.
59
59
  - After validation, always show the user: (1) the output/results from the validation run, and (2) the same command so they can re-run it themselves. Include any `--params`, `--headed`, or `--headless` flags the workflow needs.
60
60
  - Treat exploration sessions as disposable unless the user explicitly wants one kept open.
61
+ - Close disposable sessions before your final response once exploration, debugging, or validation is complete. Open browsers keep consuming local or hosted resources.
61
62
  - Get explicit user confirmation before mutating actions or replaying network requests that may have side effects.
62
63
  - Never run multiple `exec` commands at the same time.
63
64
  - If the browser must remain read-only, switch to the `libretto-readonly` skill and use `readonly-exec` instead of `exec`.
@@ -183,6 +184,7 @@ npx libretto save app.example.com
183
184
  ### `close`
184
185
 
185
186
  - Use `close` when the user is done with the session or an exploration session is no longer helping progress (unless the user asked to keep watching that browser).
187
+ - Prefer closing sessions promptly after successful validation or diagnosis so unused browsers do not keep consuming resources.
186
188
  - `close --all` is available for workspace cleanup.
187
189
 
188
190
  ```bash
@@ -4,7 +4,7 @@ description: "Read-only Libretto workflow for diagnosing live browser state with
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.30"
7
+ version: "0.6.32"
8
8
  ---
9
9
 
10
10
  ## How Libretto Read-Only Works
@@ -23,6 +23,7 @@ metadata:
23
23
  - Prefer `snapshot` first when the visible page state is unclear.
24
24
  - Use `readonly-exec` for focused inspection: titles, HTML, locator text, counts, visibility checks, and GET requests.
25
25
  - Keep snippets small and purpose-built. Do not run multiple `readonly-exec` commands at the same time.
26
+ - Close disposable sessions before your final response once inspection is complete. Open browsers keep consuming local or hosted resources.
26
27
  - End with diagnosis and handoff guidance, not an attempted in-browser repair.
27
28
 
28
29
  ## Commands
@@ -81,7 +82,7 @@ libretto readonly-exec "await scrollBy(0, 500)" --session failed-job-debug
81
82
 
82
83
  ### `close`
83
84
 
84
- - Use `close` when the inspection session is no longer needed.
85
+ - Use `close` when the inspection session is no longer needed, unless the user explicitly asks to keep the browser open.
85
86
 
86
87
  ```bash
87
88
  libretto close --session failed-job-debug
@@ -1,5 +1,6 @@
1
1
  import { SimpleCLI } from "affordance";
2
- import { orpcCall, resolveApiUrl } from "../core/auth-fetch.js";
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
- .handle(async () => {
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,149 @@
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
+ callbackUrl: SimpleCLI.option(z.string().optional(), {
72
+ name: "callback-url",
73
+ help: "Per-job callback URL",
74
+ }),
75
+ callbackSecret: SimpleCLI.option(z.string().optional(), {
76
+ name: "callback-secret",
77
+ help: "Secret used to sign the per-job callback",
78
+ }),
79
+ skipCallbacks: SimpleCLI.flag({
80
+ name: "skip-callbacks",
81
+ help: "Skip stored webhook callbacks for this job",
82
+ }),
83
+ residentialProxy: SimpleCLI.option(z.string().optional(), {
84
+ name: "residential-proxy",
85
+ help: "Residential proxy config as a JSON object",
86
+ }),
87
+ },
88
+ })
89
+ .refine((input) => Boolean(input.workflow), createJobUsage)
90
+ .refine(
91
+ (input) => !(input.params && input.paramsFile),
92
+ "Pass either --params or --params-file, not both.",
93
+ )
94
+ .refine(
95
+ (input) =>
96
+ (!input.callbackUrl && !input.callbackSecret) ||
97
+ Boolean(input.callbackUrl && input.callbackSecret),
98
+ "Pass both --callback-url and --callback-secret, or omit both.",
99
+ );
100
+
101
+ export const createCloudJobCommand = SimpleCLI.command({
102
+ description: "Create a Libretto Cloud job for a deployed workflow",
103
+ })
104
+ .input(createCloudJobInput)
105
+ .use(withCloudApiKey("create Libretto Cloud jobs"))
106
+ .handle(async ({ input, ctx }) => {
107
+ const params = input.paramsFile
108
+ ? readJsonObjectFile("--params-file", input.paramsFile)
109
+ : input.params
110
+ ? parseJsonObject("--params", input.params)
111
+ : {};
112
+ const residentialProxy = input.residentialProxy
113
+ ? parseJsonObject("--residential-proxy", input.residentialProxy)
114
+ : undefined;
115
+
116
+ const payload: Record<string, unknown> = {
117
+ workflow: input.workflow!,
118
+ params,
119
+ };
120
+ if (input.credentialId) payload.credential_id = input.credentialId;
121
+ if (input.timeoutSeconds !== undefined) {
122
+ payload.timeout_seconds = input.timeoutSeconds;
123
+ }
124
+ if (input.callbackUrl) payload.callback_url = input.callbackUrl;
125
+ if (input.callbackSecret) payload.callback_secret = input.callbackSecret;
126
+ if (input.skipCallbacks) payload.skip_callbacks = true;
127
+ if (residentialProxy !== undefined) {
128
+ payload.residential_proxy = residentialProxy;
129
+ }
130
+
131
+ const response = await orpcCall<CreateJobResponse>({
132
+ apiUrl: ctx.apiUrl,
133
+ path: "/v1/jobs/create",
134
+ input: payload,
135
+ credential: ctx.credential,
136
+ });
137
+
138
+ console.log(`Job created: ${response.job_id}`);
139
+ console.log(`Status: ${response.status}`);
140
+ console.log(response.message);
141
+ return response.job_id;
142
+ });
143
+
144
+ export const cloudJobCommands = SimpleCLI.group({
145
+ description: "Create and manage hosted jobs",
146
+ routes: {
147
+ create: createCloudJobCommand,
148
+ },
149
+ });
@@ -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, resolveApiUrl } from "../core/auth-fetch.js";
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
- .handle(async ({ input }) => {
46
- const { apiUrl, credential } = requireCloudApiKey();
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
- .handle(async () => {
72
- const { apiUrl, credential } = requireCloudApiKey();
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(enabled: boolean): Promise<boolean> {
84
- const { apiUrl, credential } = requireCloudApiKey();
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
- .handle(async () => updateCodeSharing(true));
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
- .handle(async () => updateCodeSharing(false));
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",