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.
@@ -1,18 +1,7 @@
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
  const CLOUD_CREDENTIAL_ENV_PREFIX = "LIBRETTO_CLOUD_";
4
- function requireApiKeyCredential() {
5
- const apiKey = process.env.LIBRETTO_API_KEY?.trim();
6
- if (!apiKey) {
7
- throw new Error(
8
- "LIBRETTO_API_KEY is required to manage Libretto Cloud credentials. Issue one with `libretto cloud auth api-key issue --label <label>`."
9
- );
10
- }
11
- return {
12
- apiUrl: resolveApiUrl(null),
13
- credential: { source: "env-api-key", apiKey }
14
- };
15
- }
16
5
  function parseEnvCredentials(prefix) {
17
6
  const credentials = {};
18
7
  for (const [key, value] of Object.entries(process.env)) {
@@ -29,22 +18,21 @@ const pushCredentialCommand = SimpleCLI.command({
29
18
  }).input(SimpleCLI.input({
30
19
  positionals: [],
31
20
  named: {}
32
- })).handle(async () => {
21
+ })).use(withCloudApiKey("manage Libretto Cloud credentials")).handle(async ({ ctx }) => {
33
22
  const credentials = parseEnvCredentials(CLOUD_CREDENTIAL_ENV_PREFIX);
34
23
  if (credentials.length === 0) {
35
24
  throw new Error(
36
25
  `No non-empty env vars found with prefix ${CLOUD_CREDENTIAL_ENV_PREFIX}.`
37
26
  );
38
27
  }
39
- const { apiUrl, credential } = requireApiKeyCredential();
40
28
  let created = 0;
41
29
  let updated = 0;
42
30
  for (const item of credentials) {
43
31
  const response = await orpcCall({
44
- apiUrl,
32
+ apiUrl: ctx.apiUrl,
45
33
  path: "/v1/credentials/upsert",
46
34
  input: item,
47
- credential
35
+ credential: ctx.credential
48
36
  });
49
37
  if (response.overwritten) {
50
38
  updated += 1;
@@ -0,0 +1,118 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { z } from "zod";
3
+ import { SimpleCLI } from "affordance";
4
+ import { orpcCall } from "../core/auth-fetch.js";
5
+ import { withCloudApiKey } from "./shared.js";
6
+ const createJobUsage = "Usage: libretto cloud jobs create <workflow> [--params <json> | --params-file <path>]";
7
+ function parseJsonObject(label, raw) {
8
+ let parsed;
9
+ try {
10
+ parsed = JSON.parse(raw);
11
+ } catch (error) {
12
+ throw new Error(
13
+ `Invalid JSON in ${label}: ${error instanceof Error ? error.message : String(error)}`
14
+ );
15
+ }
16
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
17
+ throw new Error(`${label} must be a JSON object.`);
18
+ }
19
+ return parsed;
20
+ }
21
+ function readJsonObjectFile(label, filePath) {
22
+ let content;
23
+ try {
24
+ content = readFileSync(filePath, "utf8");
25
+ } catch {
26
+ throw new Error(
27
+ `Could not read ${label} "${filePath}". Ensure the file exists and is readable.`
28
+ );
29
+ }
30
+ return parseJsonObject(label, content);
31
+ }
32
+ const createCloudJobInput = SimpleCLI.input({
33
+ positionals: [
34
+ SimpleCLI.positional("workflow", z.string().optional(), {
35
+ help: "Deployed workflow name to run"
36
+ })
37
+ ],
38
+ named: {
39
+ params: SimpleCLI.option(z.string().optional(), {
40
+ help: "Inline JSON params object"
41
+ }),
42
+ paramsFile: SimpleCLI.option(z.string().optional(), {
43
+ name: "params-file",
44
+ help: "Path to a JSON params file"
45
+ }),
46
+ credentialId: SimpleCLI.option(z.string().optional(), {
47
+ name: "credential-id",
48
+ help: "Stored cloud credential id to pass to the workflow"
49
+ }),
50
+ timeoutSeconds: SimpleCLI.option(z.coerce.number().int().min(1).optional(), {
51
+ name: "timeout-seconds",
52
+ help: "Job timeout in seconds"
53
+ }),
54
+ callbackUrl: SimpleCLI.option(z.string().optional(), {
55
+ name: "callback-url",
56
+ help: "Per-job callback URL"
57
+ }),
58
+ callbackSecret: SimpleCLI.option(z.string().optional(), {
59
+ name: "callback-secret",
60
+ help: "Secret used to sign the per-job callback"
61
+ }),
62
+ skipCallbacks: SimpleCLI.flag({
63
+ name: "skip-callbacks",
64
+ help: "Skip stored webhook callbacks for this job"
65
+ }),
66
+ residentialProxy: SimpleCLI.option(z.string().optional(), {
67
+ name: "residential-proxy",
68
+ help: "Residential proxy config as a JSON object"
69
+ })
70
+ }
71
+ }).refine((input) => Boolean(input.workflow), createJobUsage).refine(
72
+ (input) => !(input.params && input.paramsFile),
73
+ "Pass either --params or --params-file, not both."
74
+ ).refine(
75
+ (input) => !input.callbackUrl && !input.callbackSecret || Boolean(input.callbackUrl && input.callbackSecret),
76
+ "Pass both --callback-url and --callback-secret, or omit both."
77
+ );
78
+ const createCloudJobCommand = SimpleCLI.command({
79
+ description: "Create a Libretto Cloud job for a deployed workflow"
80
+ }).input(createCloudJobInput).use(withCloudApiKey("create Libretto Cloud jobs")).handle(async ({ input, ctx }) => {
81
+ const params = input.paramsFile ? readJsonObjectFile("--params-file", input.paramsFile) : input.params ? parseJsonObject("--params", input.params) : {};
82
+ const residentialProxy = input.residentialProxy ? parseJsonObject("--residential-proxy", input.residentialProxy) : void 0;
83
+ const payload = {
84
+ workflow: input.workflow,
85
+ params
86
+ };
87
+ if (input.credentialId) payload.credential_id = input.credentialId;
88
+ if (input.timeoutSeconds !== void 0) {
89
+ payload.timeout_seconds = input.timeoutSeconds;
90
+ }
91
+ if (input.callbackUrl) payload.callback_url = input.callbackUrl;
92
+ if (input.callbackSecret) payload.callback_secret = input.callbackSecret;
93
+ if (input.skipCallbacks) payload.skip_callbacks = true;
94
+ if (residentialProxy !== void 0) {
95
+ payload.residential_proxy = residentialProxy;
96
+ }
97
+ const response = await orpcCall({
98
+ apiUrl: ctx.apiUrl,
99
+ path: "/v1/jobs/create",
100
+ input: payload,
101
+ credential: ctx.credential
102
+ });
103
+ console.log(`Job created: ${response.job_id}`);
104
+ console.log(`Status: ${response.status}`);
105
+ console.log(response.message);
106
+ return response.job_id;
107
+ });
108
+ const cloudJobCommands = SimpleCLI.group({
109
+ description: "Create and manage hosted jobs",
110
+ routes: {
111
+ create: createCloudJobCommand
112
+ }
113
+ });
114
+ export {
115
+ cloudJobCommands,
116
+ createCloudJobCommand,
117
+ createCloudJobInput
118
+ };
@@ -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, resolveApiUrl } from "../core/auth-fetch.js";
4
- function requireCloudApiKey() {
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).handle(async ({ input }) => {
134
- const { apiUrl, credential } = await requireDeployApiKey();
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({
@@ -1,28 +1,16 @@
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
4
  import { normalizeProfileName } from "../core/profiles.js";
5
- function requireApiKeyCredential() {
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
  };
@@ -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 { createRequire } from "node:module";
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 require2 = createRequire(import.meta.url);
76
- const tsxCliPath = require2.resolve("tsx/cli");
77
- const childStderrFd = openSync(logPath, "a");
78
- const child = spawn(
79
- process.execPath,
80
- [
81
- tsxCliPath,
82
- ...config.workflow?.tsconfigPath ? ["--tsconfig", config.workflow.tsconfigPath] : [],
83
- daemonEntryPath,
84
- JSON.stringify(config)
85
- ],
86
- {
87
- detached: true,
88
- stdio: ["ignore", "ignore", childStderrFd, "ipc"]
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 });
@@ -489,13 +489,16 @@ function toPortableRelativePath(args) {
489
489
  }
490
490
  return relPath;
491
491
  }
492
+ function isShareableSourceRelPath(relPath) {
493
+ return !relPath.split("/").includes("node_modules");
494
+ }
492
495
  function writeShareableSourceFiles(args) {
493
496
  const relPaths = [...new Set(args.absSourcePaths.map(
494
497
  (absPath) => toPortableRelativePath({
495
498
  absPath,
496
499
  absSourceDir: args.absSourceDir
497
500
  })
498
- ))].sort();
501
+ ))].filter(isShareableSourceRelPath).sort();
499
502
  for (const relPath of relPaths) {
500
503
  const targetPath = join(args.outputDir, ".libretto-share", "source", relPath);
501
504
  mkdirSync(dirname(targetPath), { recursive: true });
@@ -888,7 +891,7 @@ async function writeBundledDeployEntrypoint(args) {
888
891
  (inputPath) => isAbsolute(inputPath) ? resolve(inputPath) : resolve(args.absSourceDir, inputPath)
889
892
  ).filter((absPath) => {
890
893
  const relPath = relative(args.absSourceDir, absPath);
891
- return relPath !== "" && !relPath.startsWith("../") && relPath !== ".." && !isAbsolute(relPath);
894
+ return relPath !== "" && !relPath.startsWith("../") && relPath !== ".." && !isAbsolute(relPath) && isShareableSourceRelPath(relPath.replaceAll("\\", "/"));
892
895
  });
893
896
  return { shareableSourceFiles, workflows };
894
897
  } catch (error) {