@beryl-so/cli 0.1.0

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.
@@ -0,0 +1,89 @@
1
+ import { arg, flagNum, flagStr } from "./util.js";
2
+ import { UsageError } from "../errors.js";
3
+ export const accountCommands = [
4
+ {
5
+ name: "account get",
6
+ summary: "Show your account profile",
7
+ async run(ctx) {
8
+ return { data: await ctx.client.get("/account/") };
9
+ },
10
+ },
11
+ {
12
+ name: "account update",
13
+ summary: "Update your profile",
14
+ flags: [
15
+ { name: "name", type: "string", description: "New display name" },
16
+ { name: "newsletter", type: "boolean", description: "Toggle the newsletter subscription" },
17
+ ],
18
+ async run(ctx, input) {
19
+ const body = {};
20
+ if (flagStr(input, "name"))
21
+ body.name = flagStr(input, "name");
22
+ if (input.flags.newsletter !== undefined)
23
+ body.newsletter_subscribed = input.flags.newsletter;
24
+ if (Object.keys(body).length === 0)
25
+ throw new UsageError("Nothing to update");
26
+ return { data: await ctx.client.patch("/account/", body) };
27
+ },
28
+ },
29
+ {
30
+ name: "account deletion-preview",
31
+ summary: "Preview what deleting your account would remove or leave",
32
+ async run(ctx) {
33
+ return { data: await ctx.client.get("/account/deletion-preview") };
34
+ },
35
+ },
36
+ {
37
+ name: "feedback send",
38
+ summary: "Send product feedback to the Beryl team",
39
+ args: [{ name: "message", description: "Your feedback", required: true }],
40
+ async run(ctx, input) {
41
+ return { data: await ctx.client.post("/feedback", { message: arg(input, "message") }) };
42
+ },
43
+ },
44
+ {
45
+ name: "billing usage",
46
+ summary: "Show plan usage: services and weekly AI units",
47
+ scope: "workspace",
48
+ async run(ctx, input) {
49
+ const ws = await ctx.requireWorkspace(input);
50
+ return { data: await ctx.client.get(`/workspaces/${ws}/billing/usage`) };
51
+ },
52
+ },
53
+ {
54
+ name: "billing subscription",
55
+ summary: "Show the workspace's subscription",
56
+ scope: "workspace",
57
+ async run(ctx, input) {
58
+ const ws = await ctx.requireWorkspace(input);
59
+ return { data: await ctx.client.get(`/workspaces/${ws}/billing/subscription`) };
60
+ },
61
+ },
62
+ {
63
+ name: "billing invoices",
64
+ summary: "List recent invoices",
65
+ scope: "workspace",
66
+ flags: [{ name: "limit", type: "number", description: "How many invoices" }],
67
+ async run(ctx, input) {
68
+ const ws = await ctx.requireWorkspace(input);
69
+ return {
70
+ data: await ctx.client.get(`/workspaces/${ws}/billing/invoices`, {
71
+ limit: flagNum(input, "limit"),
72
+ }),
73
+ };
74
+ },
75
+ },
76
+ {
77
+ name: "billing portal",
78
+ summary: "Get a Stripe billing-portal link for the workspace",
79
+ scope: "workspace",
80
+ async run(ctx, input) {
81
+ const ws = await ctx.requireWorkspace(input);
82
+ return {
83
+ data: await ctx.client.post(`/workspaces/${ws}/billing/portal`, {
84
+ return_url: "https://beryl.so/settings/billing",
85
+ }),
86
+ };
87
+ },
88
+ },
89
+ ];
@@ -0,0 +1,135 @@
1
+ import os from "node:os";
2
+ import { saveGlobalConfig } from "../config.js";
3
+ import { CliError, UsageError } from "../errors.js";
4
+ import { ApiClient } from "../http.js";
5
+ import { dim, green } from "../output.js";
6
+ import { arg, flagBool, flagStr } from "./util.js";
7
+ export const authCommands = [
8
+ {
9
+ name: "login",
10
+ summary: "Authenticate the CLI with your Beryl account",
11
+ description: "Signs in with an emailed one-time code and mints a personal access token, " +
12
+ "which is stored in the CLI config. Pass --token to use an existing token " +
13
+ "from Account → API tokens instead. In CI, prefer the BERYL_API_KEY environment variable.",
14
+ interactive: true,
15
+ flags: [
16
+ { name: "token", type: "string", description: "Use an existing personal access token" },
17
+ { name: "email", type: "string", description: "Email for the one-time code sign-in" },
18
+ {
19
+ name: "token-name",
20
+ type: "string",
21
+ description: "Name for the minted token (default: CLI on <hostname>)",
22
+ },
23
+ ],
24
+ examples: ["beryl login", "beryl login --token beryl_pat_…", "beryl login --email you@example.com"],
25
+ async run(ctx, input) {
26
+ const apiUrl = ctx.client.baseUrl;
27
+ let token = flagStr(input, "token");
28
+ if (!token) {
29
+ const email = flagStr(input, "email") ?? (await ctx.prompt("Email: "));
30
+ if (!email.includes("@"))
31
+ throw new UsageError(`"${email}" is not an email address`);
32
+ const anon = new ApiClient(apiUrl);
33
+ await anon.post("/auth/request-login-otp", { email });
34
+ ctx.err(dim(`Sent a 6-digit code to ${email}`));
35
+ const code = await ctx.prompt("Code: ");
36
+ const login = (await anon.post("/auth/verify-otp", { email, code }));
37
+ if (!login.access_token)
38
+ throw new CliError("Login did not return an access token");
39
+ const session = new ApiClient(apiUrl, login.access_token);
40
+ const minted = (await session.post("/account/tokens", {
41
+ name: flagStr(input, "token-name") ?? `CLI on ${os.hostname()}`,
42
+ }));
43
+ token = minted.token;
44
+ }
45
+ const authed = new ApiClient(apiUrl, token);
46
+ const me = (await authed.get("/account/"));
47
+ const saved = saveGlobalConfig({
48
+ token,
49
+ api_url: apiUrl === "https://api.beryl.so" ? undefined : apiUrl,
50
+ });
51
+ return {
52
+ data: { email: me.email, name: me.name, config: saved },
53
+ human: `${green("Logged in")} as ${me.name} <${me.email}>\n${dim(`Token saved to ${saved}`)}`,
54
+ };
55
+ },
56
+ },
57
+ {
58
+ name: "logout",
59
+ summary: "Remove the stored token from the CLI config",
60
+ flags: [
61
+ {
62
+ name: "revoke",
63
+ type: "boolean",
64
+ description: "Also revoke the token server-side so it can never be used again",
65
+ },
66
+ ],
67
+ async run(ctx, input) {
68
+ const token = ctx.config.token;
69
+ if (!token)
70
+ return { human: "Not logged in." };
71
+ if (flagBool(input, "revoke")) {
72
+ const tokens = (await ctx.client.get("/account/tokens"));
73
+ const match = tokens.find((t) => !t.revoked_at && token.startsWith(t.token_prefix));
74
+ if (match) {
75
+ await ctx.client.del(`/account/tokens/${match.id}`);
76
+ ctx.err(dim(`Revoked token "${match.name}"`));
77
+ }
78
+ }
79
+ saveGlobalConfig({ token: undefined });
80
+ return { human: "Logged out." };
81
+ },
82
+ },
83
+ {
84
+ name: "whoami",
85
+ summary: "Show the signed-in account and the CLI's resolved defaults",
86
+ async run(ctx) {
87
+ const me = (await ctx.client.get("/account/"));
88
+ return {
89
+ data: {
90
+ ...me,
91
+ api_url: ctx.client.baseUrl,
92
+ default_workspace: ctx.config.workspace ?? null,
93
+ default_project: ctx.config.project ?? null,
94
+ },
95
+ };
96
+ },
97
+ },
98
+ {
99
+ name: "tokens list",
100
+ summary: "List your personal access tokens",
101
+ async run(ctx) {
102
+ const tokens = (await ctx.client.get("/account/tokens"));
103
+ return { data: tokens };
104
+ },
105
+ },
106
+ {
107
+ name: "tokens create",
108
+ summary: "Mint a new personal access token (shown once)",
109
+ args: [{ name: "name", description: "A label for the token", required: true }],
110
+ flags: [
111
+ { name: "expires-at", type: "string", description: "Expiry as an ISO timestamp (default: never)" },
112
+ ],
113
+ async run(ctx, input) {
114
+ const created = (await ctx.client.post("/account/tokens", {
115
+ name: arg(input, "name"),
116
+ expires_at: flagStr(input, "expires-at") ?? null,
117
+ }));
118
+ return {
119
+ data: created,
120
+ human: `${green("Created")} token "${created.name}" (${created.id})\n\n` +
121
+ ` ${created.token}\n\n` +
122
+ dim("This is the only time the full token is shown — store it now."),
123
+ };
124
+ },
125
+ },
126
+ {
127
+ name: "tokens revoke",
128
+ summary: "Revoke a personal access token",
129
+ args: [{ name: "token-id", description: "Token id from `beryl tokens list`", required: true }],
130
+ async run(ctx, input) {
131
+ await ctx.client.del(`/account/tokens/${arg(input, "token-id")}`);
132
+ return { human: "Revoked." };
133
+ },
134
+ },
135
+ ];
@@ -0,0 +1,166 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { UsageError } from "../errors.js";
4
+ import { arg, flagBool, flagStr, projectPath } from "./util.js";
5
+ const configPath = (ws, p) => `${projectPath(ws, p)}/config`;
6
+ async function resolveByKey(rows, value, kind) {
7
+ const byId = rows.find((r) => r.id === value);
8
+ if (byId)
9
+ return byId.id;
10
+ const byKey = rows.filter((r) => r.key === value);
11
+ if (byKey.length === 1)
12
+ return byKey[0].id;
13
+ throw new UsageError(`No ${kind} with key or id "${value}"`);
14
+ }
15
+ export const configCommands = [
16
+ {
17
+ name: "config vars list",
18
+ summary: "List the project's config variables (visible to the agent during runs)",
19
+ scope: "project",
20
+ async run(ctx, input) {
21
+ const { workspaceId, projectId } = await ctx.requireProject(input);
22
+ return { data: await ctx.client.get(`${configPath(workspaceId, projectId)}/variables`) };
23
+ },
24
+ },
25
+ {
26
+ name: "config vars set",
27
+ summary: "Create or update a config variable",
28
+ scope: "project",
29
+ args: [
30
+ { name: "key", description: "Variable name", required: true },
31
+ { name: "value", description: "Variable value", required: true },
32
+ ],
33
+ flags: [{ name: "env", type: "string", description: "Scope to one environment id" }],
34
+ async run(ctx, input) {
35
+ const { workspaceId, projectId } = await ctx.requireProject(input);
36
+ const base = `${configPath(workspaceId, projectId)}/variables`;
37
+ const key = arg(input, "key");
38
+ const envId = flagStr(input, "env") ?? null;
39
+ const existing = (await ctx.client.get(base));
40
+ const match = existing.find((r) => r.key === key && (r.environment_id ?? null) === envId);
41
+ if (match) {
42
+ return { data: await ctx.client.put(`${base}/${match.id}`, { value: arg(input, "value") }) };
43
+ }
44
+ return {
45
+ data: await ctx.client.post(base, {
46
+ key,
47
+ value: arg(input, "value"),
48
+ environment_id: envId,
49
+ }),
50
+ };
51
+ },
52
+ },
53
+ {
54
+ name: "config vars delete",
55
+ summary: "Delete a config variable",
56
+ scope: "project",
57
+ args: [{ name: "key", description: "Variable key or id", required: true }],
58
+ async run(ctx, input) {
59
+ const { workspaceId, projectId } = await ctx.requireProject(input);
60
+ const base = `${configPath(workspaceId, projectId)}/variables`;
61
+ const rows = (await ctx.client.get(base));
62
+ const id = await resolveByKey(rows, arg(input, "key"), "variable");
63
+ await ctx.client.del(`${base}/${id}`);
64
+ return { human: "Deleted." };
65
+ },
66
+ },
67
+ {
68
+ name: "config secrets list",
69
+ summary: "List the project's secrets (values are never returned)",
70
+ scope: "project",
71
+ async run(ctx, input) {
72
+ const { workspaceId, projectId } = await ctx.requireProject(input);
73
+ return { data: await ctx.client.get(`${configPath(workspaceId, projectId)}/secrets`) };
74
+ },
75
+ },
76
+ {
77
+ name: "config secrets set",
78
+ summary: "Create a secret (write-only; re-setting a key replaces it)",
79
+ scope: "project",
80
+ args: [
81
+ { name: "key", description: "Secret name", required: true },
82
+ { name: "value", description: "Secret value (or - to read from stdin)", required: true },
83
+ ],
84
+ flags: [{ name: "env", type: "string", description: "Scope to one environment id" }],
85
+ async run(ctx, input) {
86
+ const { workspaceId, projectId } = await ctx.requireProject(input);
87
+ const base = `${configPath(workspaceId, projectId)}/secrets`;
88
+ const key = arg(input, "key");
89
+ let value = arg(input, "value");
90
+ if (value === "-")
91
+ value = fs.readFileSync(0, "utf8").trim();
92
+ const envId = flagStr(input, "env") ?? null;
93
+ return { data: await ctx.client.post(base, { key, value, environment_id: envId }) };
94
+ },
95
+ },
96
+ {
97
+ name: "config secrets delete",
98
+ summary: "Delete a secret",
99
+ scope: "project",
100
+ args: [{ name: "key", description: "Secret key or id", required: true }],
101
+ async run(ctx, input) {
102
+ const { workspaceId, projectId } = await ctx.requireProject(input);
103
+ const base = `${configPath(workspaceId, projectId)}/secrets`;
104
+ const rows = (await ctx.client.get(base));
105
+ const id = await resolveByKey(rows, arg(input, "key"), "secret");
106
+ await ctx.client.del(`${base}/${id}`);
107
+ return { human: "Deleted." };
108
+ },
109
+ },
110
+ {
111
+ name: "config files list",
112
+ summary: "List files uploaded for the agent to use (e.g. CSVs, upload fixtures)",
113
+ scope: "project",
114
+ async run(ctx, input) {
115
+ const { workspaceId, projectId } = await ctx.requireProject(input);
116
+ return { data: await ctx.client.get(`${configPath(workspaceId, projectId)}/files`) };
117
+ },
118
+ },
119
+ {
120
+ name: "config files upload",
121
+ summary: "Upload a file",
122
+ scope: "project",
123
+ args: [{ name: "file", description: "Path to the local file", required: true }],
124
+ flags: [{ name: "env", type: "string", description: "Scope to one environment id" }],
125
+ async run(ctx, input) {
126
+ const { workspaceId, projectId } = await ctx.requireProject(input);
127
+ const file = arg(input, "file");
128
+ const form = new FormData();
129
+ form.set("file", new Blob([fs.readFileSync(file)]), path.basename(file));
130
+ const envId = flagStr(input, "env");
131
+ if (envId)
132
+ form.set("environment_id", envId);
133
+ return {
134
+ data: await ctx.client.request("POST", `${configPath(workspaceId, projectId)}/files`, {
135
+ form,
136
+ }),
137
+ };
138
+ },
139
+ },
140
+ {
141
+ name: "config files download",
142
+ summary: "Get a short-lived download URL for a file",
143
+ scope: "project",
144
+ args: [{ name: "file-id", description: "File id", required: true }],
145
+ async run(ctx, input) {
146
+ const { workspaceId, projectId } = await ctx.requireProject(input);
147
+ return {
148
+ data: await ctx.client.get(`${configPath(workspaceId, projectId)}/files/${arg(input, "file-id")}/download`),
149
+ };
150
+ },
151
+ },
152
+ {
153
+ name: "config files delete",
154
+ summary: "Delete an uploaded file",
155
+ scope: "project",
156
+ args: [{ name: "file-id", description: "File id", required: true }],
157
+ flags: [{ name: "force", type: "boolean", description: "Skip the confirmation prompt" }],
158
+ async run(ctx, input) {
159
+ const { workspaceId, projectId } = await ctx.requireProject(input);
160
+ const fileId = arg(input, "file-id");
161
+ await ctx.confirm(`Delete file ${fileId}?`, flagBool(input, "force"));
162
+ await ctx.client.del(`${configPath(workspaceId, projectId)}/files/${fileId}`);
163
+ return { human: "Deleted." };
164
+ },
165
+ },
166
+ ];
@@ -0,0 +1,144 @@
1
+ import { dim, green, yellow } from "../output.js";
2
+ import { arg, flagBool } from "./util.js";
3
+ const capturePath = (ws, p) => `/auth-capture/workspaces/${ws}/projects/${p}/sessions`;
4
+ export const credentialCommands = [
5
+ {
6
+ name: "credentials list",
7
+ summary: "List the workspace's saved logins",
8
+ scope: "workspace",
9
+ async run(ctx, input) {
10
+ const ws = await ctx.requireWorkspace(input);
11
+ return { data: await ctx.client.get(`/workspaces/${ws}/credentials`) };
12
+ },
13
+ },
14
+ {
15
+ name: "credentials get",
16
+ summary: "Show one saved login (status and freshness — never the session itself)",
17
+ args: [{ name: "credential-id", description: "Credential id", required: true }],
18
+ async run(ctx, input) {
19
+ return { data: await ctx.client.get(`/credentials/${arg(input, "credential-id")}`) };
20
+ },
21
+ },
22
+ {
23
+ name: "credentials projects",
24
+ summary: "List the projects using a saved login",
25
+ args: [{ name: "credential-id", description: "Credential id", required: true }],
26
+ async run(ctx, input) {
27
+ return {
28
+ data: await ctx.client.get(`/credentials/${arg(input, "credential-id")}/projects`),
29
+ };
30
+ },
31
+ },
32
+ {
33
+ name: "credentials delete",
34
+ summary: "Delete a saved login",
35
+ args: [{ name: "credential-id", description: "Credential id", required: true }],
36
+ flags: [{ name: "force", type: "boolean", description: "Skip the confirmation prompt" }],
37
+ async run(ctx, input) {
38
+ const id = arg(input, "credential-id");
39
+ await ctx.confirm(`Delete credential ${id}?`, flagBool(input, "force"));
40
+ await ctx.client.del(`/credentials/${id}`);
41
+ return { human: "Deleted." };
42
+ },
43
+ },
44
+ {
45
+ name: "credentials attach",
46
+ summary: "Attach a saved login to a project",
47
+ scope: "project",
48
+ args: [{ name: "credential-id", description: "Credential id", required: true }],
49
+ async run(ctx, input) {
50
+ const { projectId } = await ctx.requireProject(input);
51
+ return {
52
+ data: await ctx.client.put(`/projects/${projectId}/credential`, {
53
+ credential_id: arg(input, "credential-id"),
54
+ }),
55
+ };
56
+ },
57
+ },
58
+ {
59
+ name: "credentials detach",
60
+ summary: "Detach the project's saved login",
61
+ scope: "project",
62
+ async run(ctx, input) {
63
+ const { projectId } = await ctx.requireProject(input);
64
+ await ctx.client.del(`/projects/${projectId}/credential`);
65
+ return { human: "Detached." };
66
+ },
67
+ },
68
+ {
69
+ name: "credentials recapture",
70
+ summary: "Start a re-capture for an expiring saved login (returns a live browser URL)",
71
+ args: [{ name: "credential-id", description: "Credential id", required: true }],
72
+ async run(ctx, input) {
73
+ return {
74
+ data: await ctx.client.post(`/credentials/${arg(input, "credential-id")}/recaptures`),
75
+ };
76
+ },
77
+ },
78
+ {
79
+ name: "credentials capture",
80
+ summary: "Capture a login for the project interactively: log in once in a real browser",
81
+ description: "Opens a live cloud-browser session on the project's site. Log in there like a normal " +
82
+ "user, come back, and press Enter — Beryl captures the session (encrypted at rest, " +
83
+ "never shown to anyone) so the agent can test the authenticated app.",
84
+ scope: "project",
85
+ interactive: true,
86
+ async run(ctx, input) {
87
+ const { workspaceId, projectId } = await ctx.requireProject(input);
88
+ const session = (await ctx.client.post(capturePath(workspaceId, projectId)));
89
+ ctx.err(`\nOpen this URL and log in to the site:\n\n ${yellow(session.live_view_url)}\n`);
90
+ await ctx.prompt("Press Enter once you are fully logged in… ");
91
+ try {
92
+ await ctx.client.post(`${capturePath(workspaceId, projectId)}/${session.session_id}/capture`);
93
+ }
94
+ finally {
95
+ await ctx.client
96
+ .del(`${capturePath(workspaceId, projectId)}/${session.session_id}`)
97
+ .catch(() => { });
98
+ }
99
+ return { human: `${green("Login captured.")} ${dim("The agent can now test the gated app.")}` };
100
+ },
101
+ },
102
+ {
103
+ name: "auth-capture start",
104
+ summary: "Start a login-capture browser session for the project (non-interactive)",
105
+ scope: "project",
106
+ async run(ctx, input) {
107
+ const { workspaceId, projectId } = await ctx.requireProject(input);
108
+ return { data: await ctx.client.post(capturePath(workspaceId, projectId)) };
109
+ },
110
+ },
111
+ {
112
+ name: "auth-capture capture",
113
+ summary: "Capture the session after the user has logged in via the live-view URL",
114
+ scope: "project",
115
+ args: [{ name: "session-id", description: "Session id from auth-capture start", required: true }],
116
+ async run(ctx, input) {
117
+ const { workspaceId, projectId } = await ctx.requireProject(input);
118
+ await ctx.client.post(`${capturePath(workspaceId, projectId)}/${arg(input, "session-id")}/capture`);
119
+ return { human: "Captured." };
120
+ },
121
+ },
122
+ {
123
+ name: "auth-capture refresh",
124
+ summary: "Capture a refreshed session for a project whose login is expiring",
125
+ scope: "project",
126
+ args: [{ name: "session-id", description: "Session id from auth-capture start", required: true }],
127
+ async run(ctx, input) {
128
+ const { workspaceId, projectId } = await ctx.requireProject(input);
129
+ await ctx.client.post(`${capturePath(workspaceId, projectId)}/${arg(input, "session-id")}/capture-refresh`);
130
+ return { human: "Captured." };
131
+ },
132
+ },
133
+ {
134
+ name: "auth-capture release",
135
+ summary: "Release a login-capture browser session without capturing",
136
+ scope: "project",
137
+ args: [{ name: "session-id", description: "Session id from auth-capture start", required: true }],
138
+ async run(ctx, input) {
139
+ const { workspaceId, projectId } = await ctx.requireProject(input);
140
+ await ctx.client.del(`${capturePath(workspaceId, projectId)}/${arg(input, "session-id")}`);
141
+ return { human: "Released." };
142
+ },
143
+ },
144
+ ];
@@ -0,0 +1,147 @@
1
+ import { UsageError } from "../errors.js";
2
+ import { arg, flagBool, flagNum, flagStr, projectPath } from "./util.js";
3
+ export const environmentCommands = [
4
+ {
5
+ name: "envs list",
6
+ summary: "List a project's environments",
7
+ scope: "project",
8
+ async run(ctx, input) {
9
+ const { workspaceId, projectId } = await ctx.requireProject(input);
10
+ return { data: await ctx.client.get(`${projectPath(workspaceId, projectId)}/environments`) };
11
+ },
12
+ },
13
+ {
14
+ name: "envs get",
15
+ summary: "Show one environment",
16
+ scope: "project",
17
+ args: [{ name: "env-id", description: "Environment id", required: true }],
18
+ async run(ctx, input) {
19
+ const { workspaceId, projectId } = await ctx.requireProject(input);
20
+ return {
21
+ data: await ctx.client.get(`${projectPath(workspaceId, projectId)}/environments/${arg(input, "env-id")}`),
22
+ };
23
+ },
24
+ },
25
+ {
26
+ name: "envs create",
27
+ summary: "Add an environment (e.g. staging) to a project",
28
+ scope: "project",
29
+ args: [
30
+ { name: "name", description: "Environment name", required: true },
31
+ { name: "url", description: "Root URL for this environment", required: true },
32
+ ],
33
+ flags: [
34
+ {
35
+ name: "auth",
36
+ type: "string",
37
+ enum: ["public", "gated"],
38
+ description: "Whether this environment needs a login",
39
+ },
40
+ { name: "allow-mutations", type: "boolean", description: "Allow state-changing actions" },
41
+ ],
42
+ async run(ctx, input) {
43
+ const { workspaceId, projectId } = await ctx.requireProject(input);
44
+ return {
45
+ data: await ctx.client.post(`${projectPath(workspaceId, projectId)}/environments`, {
46
+ name: arg(input, "name"),
47
+ root_url: arg(input, "url"),
48
+ requires_auth_choice: flagStr(input, "auth") ?? null,
49
+ mutation_choice: flagBool(input, "allow-mutations") ? "allow_mutations" : null,
50
+ }),
51
+ };
52
+ },
53
+ },
54
+ {
55
+ name: "envs update",
56
+ summary: "Update an environment's name, URL, or auth settings",
57
+ scope: "project",
58
+ args: [{ name: "env-id", description: "Environment id", required: true }],
59
+ flags: [
60
+ { name: "name", type: "string", description: "New name" },
61
+ { name: "url", type: "string", description: "New root URL" },
62
+ { name: "auth", type: "string", enum: ["public", "gated"], description: "New auth choice" },
63
+ ],
64
+ async run(ctx, input) {
65
+ const { workspaceId, projectId } = await ctx.requireProject(input);
66
+ const body = {};
67
+ if (flagStr(input, "name"))
68
+ body.name = flagStr(input, "name");
69
+ if (flagStr(input, "url"))
70
+ body.root_url = flagStr(input, "url");
71
+ if (flagStr(input, "auth"))
72
+ body.requires_auth_choice = flagStr(input, "auth");
73
+ if (Object.keys(body).length === 0)
74
+ throw new UsageError("Nothing to update");
75
+ return {
76
+ data: await ctx.client.patch(`${projectPath(workspaceId, projectId)}/environments/${arg(input, "env-id")}`, body),
77
+ };
78
+ },
79
+ },
80
+ {
81
+ name: "envs delete",
82
+ summary: "Delete an environment",
83
+ scope: "project",
84
+ args: [{ name: "env-id", description: "Environment id", required: true }],
85
+ flags: [{ name: "force", type: "boolean", description: "Skip the confirmation prompt" }],
86
+ async run(ctx, input) {
87
+ const { workspaceId, projectId } = await ctx.requireProject(input);
88
+ const envId = arg(input, "env-id");
89
+ await ctx.confirm(`Delete environment ${envId}?`, flagBool(input, "force"));
90
+ await ctx.client.del(`${projectPath(workspaceId, projectId)}/environments/${envId}`);
91
+ return { human: "Deleted." };
92
+ },
93
+ },
94
+ {
95
+ name: "schedule get",
96
+ summary: "Show the project's daily/weekly run schedule",
97
+ scope: "project",
98
+ async run(ctx, input) {
99
+ const { workspaceId, projectId } = await ctx.requireProject(input);
100
+ return { data: await ctx.client.get(`${projectPath(workspaceId, projectId)}/schedule`) };
101
+ },
102
+ },
103
+ {
104
+ name: "schedule set",
105
+ summary: "Enable scheduled runs (daily, or weekly on a given day)",
106
+ scope: "project",
107
+ flags: [
108
+ {
109
+ name: "frequency",
110
+ type: "string",
111
+ enum: ["daily", "weekly"],
112
+ description: "How often (default daily)",
113
+ },
114
+ { name: "day", type: "number", description: "Weekly only: day of week, 0=Monday … 6=Sunday" },
115
+ { name: "hour", type: "number", description: "Hour of day 0-23" },
116
+ { name: "minute", type: "number", description: "Minute 0-59" },
117
+ { name: "tz", type: "string", description: "IANA timezone (e.g. America/Los_Angeles)" },
118
+ ],
119
+ examples: ["beryl schedule set --frequency daily --hour 6 --tz UTC"],
120
+ async run(ctx, input) {
121
+ const { workspaceId, projectId } = await ctx.requireProject(input);
122
+ return {
123
+ data: await ctx.client.put(`${projectPath(workspaceId, projectId)}/schedule`, {
124
+ enabled: true,
125
+ frequency: flagStr(input, "frequency") ?? "daily",
126
+ day_of_week: flagNum(input, "day") ?? null,
127
+ run_hour: flagNum(input, "hour"),
128
+ run_minute: flagNum(input, "minute"),
129
+ timezone: flagStr(input, "tz"),
130
+ }),
131
+ };
132
+ },
133
+ },
134
+ {
135
+ name: "schedule disable",
136
+ summary: "Turn scheduled runs off",
137
+ scope: "project",
138
+ async run(ctx, input) {
139
+ const { workspaceId, projectId } = await ctx.requireProject(input);
140
+ return {
141
+ data: await ctx.client.put(`${projectPath(workspaceId, projectId)}/schedule`, {
142
+ enabled: false,
143
+ }),
144
+ };
145
+ },
146
+ },
147
+ ];