libretto 0.6.31 → 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.
- package/dist/cli/commands/cloud-credentials.js +5 -17
- package/dist/cli/commands/cloud-jobs.js +118 -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/profiles.js +8 -21
- package/dist/cli/commands/shared.js +17 -0
- package/dist/cli/core/daemon/ipc.js +23 -16
- package/dist/cli/router.js +6 -0
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +1 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/cloud-credentials.ts +6 -18
- package/src/cli/commands/cloud-jobs.ts +149 -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/profiles.ts +10 -22
- package/src/cli/commands/shared.ts +29 -0
- package/src/cli/core/daemon/ipc.ts +27 -18
- package/src/cli/router.ts +6 -0
|
@@ -1,18 +1,7 @@
|
|
|
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
|
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
|
|
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({
|
|
@@ -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
|
};
|
|
@@ -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 });
|
package/dist/cli/router.js
CHANGED
|
@@ -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
|
}
|