@beryl-so/cli 0.7.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -38
- package/dist/adapters/mcp.js +1 -1
- package/dist/beryl-test-skill.js +68 -0
- package/dist/commands/auth.js +6 -24
- package/dist/commands/init.js +90 -131
- package/dist/commands/projects.js +0 -19
- package/dist/commands/workspaces.js +0 -19
- package/dist/config.js +5 -21
- package/dist/context.js +4 -5
- package/dist/registry/index.js +21 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,9 +37,10 @@ beryl login # emailed one-time code; mints + stores a personal
|
|
|
37
37
|
export BERYL_API_KEY=beryl_pat_… # CI: use a token from Account → API tokens
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
Config stores your token + API URL only — nothing else persists. Point a command at a
|
|
41
|
+
workspace/project with the `--workspace` / `--project` flags or the `BERYL_WORKSPACE` /
|
|
42
|
+
`BERYL_PROJECT` env vars; with exactly one, the CLI auto-picks it.
|
|
43
|
+
Precedence: flags > `BERYL_*` env > auto-pick-if-one.
|
|
43
44
|
|
|
44
45
|
## CI in one line
|
|
45
46
|
|
|
@@ -56,18 +57,19 @@ claude mcp add beryl -- npx -y @beryl-so/cli mcp # no install needed
|
|
|
56
57
|
claude mcp add beryl -- beryl mcp # or, with the CLI installed
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
(spaces and dashes become underscores).
|
|
60
|
+
Most non-interactive commands below are exposed as an MCP tool with the same name
|
|
61
|
+
(spaces and dashes become underscores) — see the MCP tool column. `—` marks commands
|
|
62
|
+
kept CLI-only: account/billing/team admin, Slack alerts, feedback, and cloud exploration.
|
|
61
63
|
|
|
62
64
|
## Commands
|
|
63
65
|
|
|
64
66
|
### init
|
|
65
67
|
|
|
66
|
-
Set up Beryl in this repo — sign in
|
|
68
|
+
Set up Beryl in this repo — sign in and wire up your coding agent
|
|
67
69
|
|
|
68
70
|
| Command | Summary | MCP tool |
|
|
69
71
|
| --- | --- | --- |
|
|
70
|
-
| `beryl init` | Set up Beryl in this repo — sign in
|
|
72
|
+
| `beryl init` | Set up Beryl in this repo — sign in and wire up your coding agent | — |
|
|
71
73
|
|
|
72
74
|
### login
|
|
73
75
|
|
|
@@ -95,11 +97,11 @@ Remove the stored token from the CLI config
|
|
|
95
97
|
|
|
96
98
|
### whoami
|
|
97
99
|
|
|
98
|
-
Show the signed-in account
|
|
100
|
+
Show the signed-in account
|
|
99
101
|
|
|
100
102
|
| Command | Summary | MCP tool |
|
|
101
103
|
| --- | --- | --- |
|
|
102
|
-
| `beryl whoami` | Show the signed-in account
|
|
104
|
+
| `beryl whoami` | Show the signed-in account | `whoami` |
|
|
103
105
|
|
|
104
106
|
### tokens
|
|
105
107
|
|
|
@@ -122,7 +124,6 @@ Create and manage workspaces, and pick the one your commands act on by default.
|
|
|
122
124
|
| `beryl workspaces create <name>` | Create a workspace | `workspaces_create` |
|
|
123
125
|
| `beryl workspaces update` | Rename a workspace or change its visibility | `workspaces_update` |
|
|
124
126
|
| `beryl workspaces delete` | Delete a workspace and everything in it | `workspaces_delete` |
|
|
125
|
-
| `beryl workspaces use <workspace>` | Set the default workspace for future commands | `workspaces_use` |
|
|
126
127
|
| `beryl workspaces history` | Show the workspace's action history (who did what, when) | `workspaces_history` |
|
|
127
128
|
| `beryl workspaces leave` | Leave a workspace you are a member of | `workspaces_leave` |
|
|
128
129
|
|
|
@@ -132,9 +133,9 @@ Manage who belongs to a workspace and their roles.
|
|
|
132
133
|
|
|
133
134
|
| Command | Summary | MCP tool |
|
|
134
135
|
| --- | --- | --- |
|
|
135
|
-
| `beryl members list` | List workspace members |
|
|
136
|
-
| `beryl members set-role <user-id> <role>` | Change a member's role |
|
|
137
|
-
| `beryl members remove <user-id>` | Remove a member from the workspace |
|
|
136
|
+
| `beryl members list` | List workspace members | — |
|
|
137
|
+
| `beryl members set-role <user-id> <role>` | Change a member's role | — |
|
|
138
|
+
| `beryl members remove <user-id>` | Remove a member from the workspace | — |
|
|
138
139
|
|
|
139
140
|
### invites
|
|
140
141
|
|
|
@@ -142,12 +143,12 @@ Send, list, and revoke workspace invitations, and accept ones sent to you.
|
|
|
142
143
|
|
|
143
144
|
| Command | Summary | MCP tool |
|
|
144
145
|
| --- | --- | --- |
|
|
145
|
-
| `beryl invites send <email>` | Invite someone to the workspace by email |
|
|
146
|
-
| `beryl invites list` | List the workspace's outstanding invitations |
|
|
147
|
-
| `beryl invites revoke <invitation-id>` | Revoke a pending invitation |
|
|
148
|
-
| `beryl invites mine` | List invitations sent to you |
|
|
149
|
-
| `beryl invites accept <invitation>` | Accept an invitation (by id, or by the token from the invite email) |
|
|
150
|
-
| `beryl invites decline <invitation-id>` | Decline an invitation |
|
|
146
|
+
| `beryl invites send <email>` | Invite someone to the workspace by email | — |
|
|
147
|
+
| `beryl invites list` | List the workspace's outstanding invitations | — |
|
|
148
|
+
| `beryl invites revoke <invitation-id>` | Revoke a pending invitation | — |
|
|
149
|
+
| `beryl invites mine` | List invitations sent to you | — |
|
|
150
|
+
| `beryl invites accept <invitation>` | Accept an invitation (by id, or by the token from the invite email) | — |
|
|
151
|
+
| `beryl invites decline <invitation-id>` | Decline an invitation | — |
|
|
151
152
|
|
|
152
153
|
### projects
|
|
153
154
|
|
|
@@ -162,10 +163,9 @@ Create and manage projects — a site Beryl explores, authors tests for, and run
|
|
|
162
163
|
| `beryl projects create <url>` | Create a project — the agent starts exploring and authoring tests immediately | `projects_create` |
|
|
163
164
|
| `beryl projects rename <name>` | Rename a project | `projects_rename` |
|
|
164
165
|
| `beryl projects delete` | Delete a project and all its tests and runs | `projects_delete` |
|
|
165
|
-
| `beryl projects re-explore` | Send the agent back in — run/heal existing tests and discover new flows |
|
|
166
|
+
| `beryl projects re-explore` | Send the agent back in — run/heal existing tests and discover new flows | — |
|
|
166
167
|
| `beryl projects report` | Aggregate quality report across recent runs (pass rates, flaky tests, trend) | `projects_report` |
|
|
167
168
|
| `beryl projects reusable-auth <url>` | Check whether a saved login can be reused for a URL before creating a project | `projects_reusable_auth` |
|
|
168
|
-
| `beryl projects use <project>` | Set the default project for future commands | `projects_use` |
|
|
169
169
|
|
|
170
170
|
### envs
|
|
171
171
|
|
|
@@ -241,11 +241,11 @@ Inspect the agent's exploration runs — how it crawled a site and authored its
|
|
|
241
241
|
|
|
242
242
|
| Command | Summary | MCP tool |
|
|
243
243
|
| --- | --- | --- |
|
|
244
|
-
| `beryl explorations list` | List the agent's exploration passes for a project |
|
|
245
|
-
| `beryl explorations get <exploration-id>` | Show one exploration: authored tests, abandoned flows, coverage, frontier |
|
|
246
|
-
| `beryl explorations steps <exploration-id>` | List every step the agent took in an exploration |
|
|
247
|
-
| `beryl explorations cancel <exploration-id>` | Cancel an in-flight exploration |
|
|
248
|
-
| `beryl explorations watch <exploration-id>` | Stream an exploration live — watch the agent explore and author tests |
|
|
244
|
+
| `beryl explorations list` | List the agent's exploration passes for a project | — |
|
|
245
|
+
| `beryl explorations get <exploration-id>` | Show one exploration: authored tests, abandoned flows, coverage, frontier | — |
|
|
246
|
+
| `beryl explorations steps <exploration-id>` | List every step the agent took in an exploration | — |
|
|
247
|
+
| `beryl explorations cancel <exploration-id>` | Cancel an in-flight exploration | — |
|
|
248
|
+
| `beryl explorations watch <exploration-id>` | Stream an exploration live — watch the agent explore and author tests | — |
|
|
249
249
|
|
|
250
250
|
### config
|
|
251
251
|
|
|
@@ -275,10 +275,10 @@ Send run outcomes to a Slack channel via a per-project incoming webhook.
|
|
|
275
275
|
|
|
276
276
|
| Command | Summary | MCP tool |
|
|
277
277
|
| --- | --- | --- |
|
|
278
|
-
| `beryl slack show` | Show the project's Slack alert config (webhook is masked) |
|
|
279
|
-
| `beryl slack set-webhook <url>` | Set the Slack incoming-webhook URL run alerts post to |
|
|
280
|
-
| `beryl slack clear` | Remove the project's Slack webhook (stops all alerts) |
|
|
281
|
-
| `beryl slack test` | Post a sample alert to the configured webhook |
|
|
278
|
+
| `beryl slack show` | Show the project's Slack alert config (webhook is masked) | — |
|
|
279
|
+
| `beryl slack set-webhook <url>` | Set the Slack incoming-webhook URL run alerts post to | — |
|
|
280
|
+
| `beryl slack clear` | Remove the project's Slack webhook (stops all alerts) | — |
|
|
281
|
+
| `beryl slack test` | Post a sample alert to the configured webhook | — |
|
|
282
282
|
|
|
283
283
|
### credentials
|
|
284
284
|
|
|
@@ -324,9 +324,9 @@ View and update your personal account profile.
|
|
|
324
324
|
|
|
325
325
|
| Command | Summary | MCP tool |
|
|
326
326
|
| --- | --- | --- |
|
|
327
|
-
| `beryl account get` | Show your account profile |
|
|
328
|
-
| `beryl account update` | Update your profile |
|
|
329
|
-
| `beryl account deletion-preview` | Preview what deleting your account would remove or leave |
|
|
327
|
+
| `beryl account get` | Show your account profile | — |
|
|
328
|
+
| `beryl account update` | Update your profile | — |
|
|
329
|
+
| `beryl account deletion-preview` | Preview what deleting your account would remove or leave | — |
|
|
330
330
|
|
|
331
331
|
### feedback
|
|
332
332
|
|
|
@@ -334,7 +334,7 @@ Send product feedback to the Beryl team.
|
|
|
334
334
|
|
|
335
335
|
| Command | Summary | MCP tool |
|
|
336
336
|
| --- | --- | --- |
|
|
337
|
-
| `beryl feedback send <message>` | Send product feedback to the Beryl team |
|
|
337
|
+
| `beryl feedback send <message>` | Send product feedback to the Beryl team | — |
|
|
338
338
|
|
|
339
339
|
### billing
|
|
340
340
|
|
|
@@ -342,10 +342,10 @@ Review a workspace's plan usage, subscription, and invoices.
|
|
|
342
342
|
|
|
343
343
|
| Command | Summary | MCP tool |
|
|
344
344
|
| --- | --- | --- |
|
|
345
|
-
| `beryl billing usage` | Show plan usage: services and weekly AI units |
|
|
346
|
-
| `beryl billing subscription` | Show the workspace's subscription |
|
|
347
|
-
| `beryl billing invoices` | List recent invoices |
|
|
348
|
-
| `beryl billing portal` | Get a Stripe billing-portal link for the workspace |
|
|
345
|
+
| `beryl billing usage` | Show plan usage: services and weekly AI units | — |
|
|
346
|
+
| `beryl billing subscription` | Show the workspace's subscription | — |
|
|
347
|
+
| `beryl billing invoices` | List recent invoices | — |
|
|
348
|
+
| `beryl billing portal` | Get a Stripe billing-portal link for the workspace | — |
|
|
349
349
|
|
|
350
350
|
### mcp
|
|
351
351
|
|
package/dist/adapters/mcp.js
CHANGED
|
@@ -9,7 +9,7 @@ export function toolName(spec) {
|
|
|
9
9
|
return spec.name.replace(/ /g, "_").replace(/-/g, "_");
|
|
10
10
|
}
|
|
11
11
|
export function mcpTools() {
|
|
12
|
-
return commands.filter((c) => !c.interactive && !c.hidden && c.name !== "mcp");
|
|
12
|
+
return commands.filter((c) => !c.interactive && !c.hidden && !c.mcpHidden && c.name !== "mcp");
|
|
13
13
|
}
|
|
14
14
|
const mcpToolNames = new Set(mcpTools().map(toolName));
|
|
15
15
|
/** The tool name a command is exposed as under `beryl mcp`, or undefined if it isn't exposed. */
|
package/dist/beryl-test-skill.js
CHANGED
|
@@ -19,6 +19,26 @@ export const BERYL_TEST_SKILL_EXAMPLE_PLAN = {
|
|
|
19
19
|
{ action: "expect", expect_kind: "visible", selector: "text=Pro plan" },
|
|
20
20
|
],
|
|
21
21
|
};
|
|
22
|
+
// The OTP/signup example the skill shows verbatim (§5). Exported so the test suite lints
|
|
23
|
+
// it with lintPlan, same as the minimal example — the documented await_email shape must
|
|
24
|
+
// pass `beryl tests lint` on the first try.
|
|
25
|
+
export const BERYL_TEST_SKILL_OTP_EXAMPLE_PLAN = {
|
|
26
|
+
steps: [
|
|
27
|
+
{ action: "goto", url: "https://app.example.com/signup" },
|
|
28
|
+
{ action: "fill", selector: "input[name=email]", value: "{{inbox_address}}" },
|
|
29
|
+
{ action: "click", selector: "button[type=submit]" },
|
|
30
|
+
{
|
|
31
|
+
action: "await_email",
|
|
32
|
+
extract: "code",
|
|
33
|
+
subject_contains: "verification code",
|
|
34
|
+
capture_as: "otp",
|
|
35
|
+
wait_s: 45,
|
|
36
|
+
},
|
|
37
|
+
{ action: "fill", selector: "input[name=code]", value: "{{otp}}" },
|
|
38
|
+
{ action: "click", selector: "text=Verify" },
|
|
39
|
+
{ action: "expect", expect_kind: "visible", selector: "text=Welcome" },
|
|
40
|
+
],
|
|
41
|
+
};
|
|
22
42
|
export const BERYL_TEST_SKILL = `---
|
|
23
43
|
name: beryl-test
|
|
24
44
|
description: Author durable, healable end-to-end tests for a web app with Beryl. Use when writing, running, or fixing a Beryl test locally with your own coding agent — drafting the plan over the Playwright MCP, writing the natural-language intent, and running it with \`beryl runs local\`.
|
|
@@ -176,4 +196,52 @@ beryl runs local <test-id> --url-override http://localhost:3000 --dir ./beryl-lo
|
|
|
176
196
|
|
|
177
197
|
Once the test passes locally against a real outcome, it's ready to bank and let Beryl run
|
|
178
198
|
and heal it.
|
|
199
|
+
|
|
200
|
+
## 5. Testing an OTP / signup flow (\`await_email\`)
|
|
201
|
+
|
|
202
|
+
A flow that emails the user — a signup verification code, a magic sign-in link, a receipt
|
|
203
|
+
— is testable with the \`await_email\` action. Beryl mints a **run-scoped inbox**
|
|
204
|
+
automatically whenever a plan contains an \`await_email\` step (or cites
|
|
205
|
+
\`{{inbox_address}}\`): no setup, no environment configuration, no flag to turn on. The
|
|
206
|
+
minted address is in scope from step 1 as the reserved \`{{inbox_address}}\` handle.
|
|
207
|
+
|
|
208
|
+
The wiring is a three-part chain:
|
|
209
|
+
|
|
210
|
+
1. **Type the minted address into the app** — a \`fill\` with \`value: "{{inbox_address}}"\`.
|
|
211
|
+
Every run gets a fresh address, so a signup flow is repeatable by construction (no
|
|
212
|
+
\`{{unique}}\` needed for the email itself; use \`{{unique}}\` for other must-not-collide
|
|
213
|
+
values like a username).
|
|
214
|
+
2. **Await the mail and bank the extracted value** — an \`await_email\` step with:
|
|
215
|
+
- \`extract\` (required): \`code\` (an OTP), \`link\` (the sign-in/verify URL), or
|
|
216
|
+
\`pattern\` (your own regex in \`extract_pattern\`, exactly one capture group).
|
|
217
|
+
- \`capture_as\` (required): the handle name the extracted string is banked under.
|
|
218
|
+
- \`subject_contains\` / \`from_contains\` (optional): match the right mail when the app
|
|
219
|
+
sends more than one.
|
|
220
|
+
- \`wait_s\` (optional, 1–50, default 30): how many seconds the step blocks waiting
|
|
221
|
+
for the mail to land.
|
|
222
|
+
3. **Use the banked value** — cite \`{{<capture_as>}}\` in a later step's \`value\` (fill the
|
|
223
|
+
code) or \`url\` (goto the magic link). A captured handle is legal **only** in
|
|
224
|
+
\`value\`/\`url\`; in a \`selector\`, \`option\`, or \`expect_text\` it would be used as
|
|
225
|
+
literal text, and the linter rejects it there.
|
|
226
|
+
|
|
227
|
+
A fully valid signup-with-OTP plan:
|
|
228
|
+
|
|
229
|
+
\`\`\`json
|
|
230
|
+
${JSON.stringify(BERYL_TEST_SKILL_OTP_EXAMPLE_PLAN, null, 2)}
|
|
231
|
+
\`\`\`
|
|
232
|
+
|
|
233
|
+
For a magic-link flow, replace the code steps with
|
|
234
|
+
\`{ "action": "await_email", "extract": "link", "capture_as": "signin_link" }\` followed by
|
|
235
|
+
\`{ "action": "goto", "url": "{{signin_link}}" }\`.
|
|
236
|
+
|
|
237
|
+
Two caveats:
|
|
238
|
+
|
|
239
|
+
- \`beryl tests create\` verifies an \`await_email\` plan like any other — the replay mints
|
|
240
|
+
its own inbox, so the app's mail really is received and extracted before the test is
|
|
241
|
+
accepted. (Note the replay signs up / sends mail for real; pass \`--no-verify\` only if
|
|
242
|
+
that side effect is unwanted.) \`runs local\` cannot serve \`await_email\` — the inbox
|
|
243
|
+
lives in Beryl's cloud — so iterate on these flows with
|
|
244
|
+
\`beryl runs trigger --test <id> --watch\`.
|
|
245
|
+
- The outcome assertion discipline from §1 still applies: the green signal is the
|
|
246
|
+
post-verification state (the welcome screen, the dashboard), not "an email arrived".
|
|
179
247
|
`;
|
package/dist/commands/auth.js
CHANGED
|
@@ -119,32 +119,16 @@ export const authCommands = [
|
|
|
119
119
|
}
|
|
120
120
|
const authed = new ApiClient(apiUrl, token);
|
|
121
121
|
const me = (await authed.get("/account/"));
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const workspaces = (await authed.get("/workspaces/"));
|
|
126
|
-
workspaceCount = workspaces.length;
|
|
127
|
-
if (workspaces.length === 1)
|
|
128
|
-
sole = workspaces[0];
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
// pinning a default is a nicety — never fail the login over it
|
|
132
|
-
}
|
|
122
|
+
// Persist identity only — PAT + api_url. Workspace/project are never pinned;
|
|
123
|
+
// they resolve per-command from env/flags or auto-pick when there's exactly one.
|
|
133
124
|
const saved = saveGlobalConfig({
|
|
134
125
|
token,
|
|
135
126
|
api_url: apiUrl === "https://api.beryl.so" ? undefined : apiUrl,
|
|
136
|
-
...(sole && sole.id !== ctx.config.workspace
|
|
137
|
-
? { workspace: sole.id, project: undefined }
|
|
138
|
-
: {}),
|
|
139
127
|
});
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
human += `\n${dim(`Default workspace: ${sole.name} (${sole.id})`)}`;
|
|
143
|
-
else if (workspaceCount > 1 && !ctx.config.workspace)
|
|
144
|
-
human += `\n${dim("Pick a default workspace with `beryl workspaces use <id>`")}`;
|
|
145
|
-
human += `\n${dim(`Token saved to ${saved}`)}`;
|
|
128
|
+
const human = `${green("Logged in")} as ${me.name} <${me.email}>` +
|
|
129
|
+
`\n${dim(`Token saved to ${saved}`)}`;
|
|
146
130
|
return {
|
|
147
|
-
data: { email: me.email, name: me.name,
|
|
131
|
+
data: { email: me.email, name: me.name, config: saved },
|
|
148
132
|
human,
|
|
149
133
|
};
|
|
150
134
|
},
|
|
@@ -207,15 +191,13 @@ export const authCommands = [
|
|
|
207
191
|
},
|
|
208
192
|
{
|
|
209
193
|
name: "whoami",
|
|
210
|
-
summary: "Show the signed-in account
|
|
194
|
+
summary: "Show the signed-in account",
|
|
211
195
|
async run(ctx) {
|
|
212
196
|
const me = (await ctx.client.get("/account/"));
|
|
213
197
|
return {
|
|
214
198
|
data: {
|
|
215
199
|
...me,
|
|
216
200
|
api_url: ctx.client.baseUrl,
|
|
217
|
-
default_workspace: ctx.config.workspace ?? null,
|
|
218
|
-
default_project: ctx.config.project ?? null,
|
|
219
201
|
},
|
|
220
202
|
};
|
|
221
203
|
},
|
package/dist/commands/init.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { BERYL_TEST_SKILL, BERYL_TEST_SKILL_DIR, BERYL_TEST_SKILL_FILENAME, } from "../beryl-test-skill.js";
|
|
4
|
-
import {
|
|
5
|
-
import { CliError
|
|
6
|
+
import { loadConfig } from "../config.js";
|
|
7
|
+
import { CliError } from "../errors.js";
|
|
6
8
|
import { ApiClient } from "../http.js";
|
|
7
9
|
import { bold, cyan, dim, green, yellow } from "../output.js";
|
|
8
10
|
import { cliVersion, warnIfStale } from "../version-check.js";
|
|
9
11
|
import { authCommands } from "./auth.js";
|
|
10
|
-
import {
|
|
12
|
+
import { flagStr } from "./util.js";
|
|
11
13
|
// Pinned to @latest (like the Playwright entry below) — an unpinned spec lets npx
|
|
12
14
|
// serve whatever stale global/cache install already resolves, so the agent's MCP
|
|
13
15
|
// server would silently miss newer tools. mergeMcpConfig compares entries verbatim,
|
|
@@ -16,38 +18,14 @@ const MCP_SERVER_ENTRY = {
|
|
|
16
18
|
command: "npx",
|
|
17
19
|
args: ["-y", "@beryl-so/cli@latest", "mcp"],
|
|
18
20
|
};
|
|
21
|
+
// Headless: the coding agent drives this browser to author tests — nobody watches the
|
|
22
|
+
// window, and a headed default breaks on CI / headless boxes. Matches how most devs
|
|
23
|
+
// already wire their own user-scoped playwright.
|
|
19
24
|
const PLAYWRIGHT_SERVER_ENTRY = {
|
|
20
25
|
command: "npx",
|
|
21
|
-
args: ["@playwright/mcp@latest"],
|
|
26
|
+
args: ["@playwright/mcp@latest", "--headless"],
|
|
22
27
|
};
|
|
23
28
|
const ACTION_PLAN_SCHEMA_URL = "https://api.beryl.so/api/v1/schemas/action-plan.schema.json";
|
|
24
|
-
// The authoring fork only appears when we create a fresh project with no tests yet; an
|
|
25
|
-
// explicit flag wins, otherwise a TTY prompt defaults to local on enter, and a
|
|
26
|
-
// non-interactive run defaults to local so CI never burns server exploration on a project
|
|
27
|
-
// the user means to hand-author.
|
|
28
|
-
async function resolveAuthoring(ctx, flag) {
|
|
29
|
-
if (flag === "local" || flag === "agent")
|
|
30
|
-
return flag;
|
|
31
|
-
if (!ctx.interactive)
|
|
32
|
-
return "local";
|
|
33
|
-
ctx.err(`\n${bold("How do you want to author tests for this project?")}`);
|
|
34
|
-
ctx.err(` ${green("1")}. Local / manual — with your own coding agent (Playwright MCP) or by hand ${dim("(default)")}`);
|
|
35
|
-
ctx.err(` 2. Beryl agent explores the site and authors tests for you`);
|
|
36
|
-
const answer = await ctx.prompt("Authoring [1-2]: ");
|
|
37
|
-
return answer.trim() === "2" ? "agent" : "local";
|
|
38
|
-
}
|
|
39
|
-
async function pick(ctx, kind, items) {
|
|
40
|
-
if (items.length === 1)
|
|
41
|
-
return items[0];
|
|
42
|
-
ctx.err(`\n${bold(`Which ${kind}?`)}`);
|
|
43
|
-
items.forEach((item, i) => ctx.err(` ${i + 1}. ${item.name ?? item.root_url ?? item.id} ${dim(item.id)}`));
|
|
44
|
-
const answer = await ctx.prompt(`${kind} [1-${items.length}]: `);
|
|
45
|
-
const index = Number(answer) - 1;
|
|
46
|
-
const chosen = items[index];
|
|
47
|
-
if (!chosen)
|
|
48
|
-
throw new UsageError(`"${answer}" is not between 1 and ${items.length}`);
|
|
49
|
-
return chosen;
|
|
50
|
-
}
|
|
51
29
|
function mergeMcpConfig(file, withPlaywright) {
|
|
52
30
|
let existing = {};
|
|
53
31
|
if (fs.existsSync(file)) {
|
|
@@ -76,6 +54,36 @@ function mergeMcpConfig(file, withPlaywright) {
|
|
|
76
54
|
}
|
|
77
55
|
return { beryl, playwright };
|
|
78
56
|
}
|
|
57
|
+
// A tool's config lives per-user (same as the PAT it needs, in ~/.config/beryl) — so wire the
|
|
58
|
+
// MCP servers per-user too, not in a committed .mcp.json that 401s for every teammate until
|
|
59
|
+
// they run `beryl login` anyway. We DON'T hand-edit ~/.claude.json (Claude Code owns it; a
|
|
60
|
+
// corrupt write breaks the user's whole CLI) — we shell out to `claude mcp add` and let Claude
|
|
61
|
+
// own the edit. Returns false when the `claude` binary isn't on PATH so the caller can fall
|
|
62
|
+
// back to a copy-paste command.
|
|
63
|
+
function claudeUserAdd(name, entry) {
|
|
64
|
+
try {
|
|
65
|
+
execFileSync("claude", ["mcp", "add", name, "-s", "user", "--", entry.command, ...entry.args], {
|
|
66
|
+
stdio: "ignore",
|
|
67
|
+
});
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
// A non-zero exit ALSO covers "already exists" — but so does a missing binary. Distinguish:
|
|
72
|
+
// ENOENT means no `claude` on PATH (fall back), anything else means it ran and declined
|
|
73
|
+
// (already configured — treat as success, nothing to do).
|
|
74
|
+
if (err.code === "ENOENT")
|
|
75
|
+
return false;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function claudeAddHint(name, entry) {
|
|
80
|
+
return `claude mcp add ${name} -s user -- ${entry.command} ${entry.args.join(" ")}`;
|
|
81
|
+
}
|
|
82
|
+
// ~/.cursor/mcp.json is Cursor's user-scope config — small and ours to write safely, unlike
|
|
83
|
+
// ~/.claude.json — so we merge it directly rather than shelling out.
|
|
84
|
+
function cursorUserConfigPath() {
|
|
85
|
+
return path.join(os.homedir(), ".cursor", "mcp.json");
|
|
86
|
+
}
|
|
79
87
|
function detectEditors(cwd) {
|
|
80
88
|
const editors = [];
|
|
81
89
|
if (fs.existsSync(path.join(cwd, ".claude")) || fs.existsSync(path.join(cwd, "CLAUDE.md")))
|
|
@@ -106,38 +114,31 @@ function writeSkill(cwd) {
|
|
|
106
114
|
export const initCommands = [
|
|
107
115
|
{
|
|
108
116
|
name: "init",
|
|
109
|
-
summary: "Set up Beryl in this repo — sign in
|
|
110
|
-
description: "One-command onboarding: signs you in (emailed one-time code)
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
117
|
+
summary: "Set up Beryl in this repo — sign in and wire up your coding agent",
|
|
118
|
+
description: "One-command onboarding: signs you in (emailed one-time code) and wires the MCP servers " +
|
|
119
|
+
"for your coding agent, then hands off to Claude. By default they're wired per-user " +
|
|
120
|
+
"(matching where your login token lives) — via `claude mcp add -s user` for Claude Code, " +
|
|
121
|
+
"~/.cursor/mcp.json for Cursor; pass --scope project to write a committed .mcp.json for a " +
|
|
122
|
+
"shared repo instead. No workspace/project pin and no URL prompt — open your editor and ask " +
|
|
123
|
+
"Claude to write tests for your site; it resolves the workspace/project and sets the URL for " +
|
|
124
|
+
"you. Safe to re-run; every step skips what is already set up.",
|
|
116
125
|
interactive: true,
|
|
117
126
|
flags: [
|
|
118
|
-
{ name: "workspace", type: "string", description: "Workspace id or name to pin" },
|
|
119
|
-
{
|
|
120
|
-
name: "project",
|
|
121
|
-
type: "string",
|
|
122
|
-
description: "Project id, name, or URL to pin. With no projects yet, a URL here is the root URL of " +
|
|
123
|
-
"the project to create — supply it (plus --authoring) to run init non-interactively in CI",
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
name: "authoring",
|
|
127
|
-
type: "string",
|
|
128
|
-
enum: ["local", "agent"],
|
|
129
|
-
description: "How to author tests for a newly created project: `local` (author yourself with your " +
|
|
130
|
-
"coding agent or by hand — the project is created without server exploration) or " +
|
|
131
|
-
"`agent` (Beryl's agent explores the site and authors tests). Default: local. " +
|
|
132
|
-
"Pass this to skip the interactive prompt in CI",
|
|
133
|
-
},
|
|
134
127
|
{
|
|
135
128
|
name: "editor-tools",
|
|
136
129
|
type: "string",
|
|
137
130
|
enum: ["claude-code", "cursor", "both", "none"],
|
|
138
131
|
description: "Which coding agent to write MCP config for (default: auto-detect)",
|
|
139
132
|
},
|
|
140
|
-
{
|
|
133
|
+
{
|
|
134
|
+
name: "scope",
|
|
135
|
+
type: "string",
|
|
136
|
+
enum: ["user", "project"],
|
|
137
|
+
description: "Where to wire the MCP servers. `user` (default) configures them per-user (matching " +
|
|
138
|
+
"where your Beryl login token lives) via `claude mcp add -s user` / ~/.cursor/mcp.json. " +
|
|
139
|
+
"`project` writes a committed .mcp.json for a shared repo — every teammate still runs " +
|
|
140
|
+
"`beryl login` to authenticate",
|
|
141
|
+
},
|
|
141
142
|
{
|
|
142
143
|
name: "local",
|
|
143
144
|
type: "boolean",
|
|
@@ -149,8 +150,8 @@ export const initCommands = [
|
|
|
149
150
|
examples: [
|
|
150
151
|
"npx @beryl-so/cli@latest init",
|
|
151
152
|
"beryl init --editor-tools claude-code",
|
|
152
|
-
"beryl init --project
|
|
153
|
-
"beryl init --
|
|
153
|
+
"beryl init --scope project",
|
|
154
|
+
"beryl init --editor-tools none",
|
|
154
155
|
],
|
|
155
156
|
async run(ctx, input) {
|
|
156
157
|
const cwd = process.cwd();
|
|
@@ -167,59 +168,6 @@ export const initCommands = [
|
|
|
167
168
|
throw new CliError("Login did not persist a token");
|
|
168
169
|
client = new ApiClient(config.apiUrl, config.token);
|
|
169
170
|
}
|
|
170
|
-
let workspaceId;
|
|
171
|
-
let projectId;
|
|
172
|
-
let authoredLocally = false;
|
|
173
|
-
if (!flagBool(input, "no-pin")) {
|
|
174
|
-
const workspaces = (await client.get("/workspaces/"));
|
|
175
|
-
if (workspaces.length === 0)
|
|
176
|
-
throw new CliError("You have no workspaces yet — create one with `beryl workspaces create`");
|
|
177
|
-
const wanted = flagStr(input, "workspace");
|
|
178
|
-
const workspace = wanted
|
|
179
|
-
? workspaces.find((w) => w.id === wanted || w.name?.toLowerCase() === wanted.toLowerCase())
|
|
180
|
-
: await pick(ctx, "workspace", workspaces);
|
|
181
|
-
if (!workspace)
|
|
182
|
-
throw new CliError(`No workspace named "${wanted}" found`);
|
|
183
|
-
workspaceId = workspace.id;
|
|
184
|
-
const projects = (await client.get(`/workspaces/${workspaceId}/projects`));
|
|
185
|
-
const wantedProject = flagStr(input, "project");
|
|
186
|
-
// With projects already present, --project pins an existing one; with none, it's the
|
|
187
|
-
// root URL of the project to create — which is what makes a fresh init CI-drivable.
|
|
188
|
-
if (wantedProject && projects.length > 0) {
|
|
189
|
-
projectId = (await ctx.requireProject({
|
|
190
|
-
args: {},
|
|
191
|
-
flags: { workspace: workspaceId, project: wantedProject },
|
|
192
|
-
})).projectId;
|
|
193
|
-
}
|
|
194
|
-
else if (projects.length > 0) {
|
|
195
|
-
projectId = (await pick(ctx, "project", projects)).id;
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
const url = wantedProject ?? (await ctx.prompt("No projects yet. Root URL of the site to test: "));
|
|
199
|
-
const authoring = await resolveAuthoring(ctx, flagStr(input, "authoring"));
|
|
200
|
-
authoredLocally = authoring === "local";
|
|
201
|
-
const created = (await client.post(`/workspaces/${workspaceId}/projects`, {
|
|
202
|
-
root_url: url,
|
|
203
|
-
skip_exploration: authoredLocally,
|
|
204
|
-
}));
|
|
205
|
-
projectId = created.project_id;
|
|
206
|
-
if (authoredLocally)
|
|
207
|
-
ctx.err(`${green("✓")} Project created for ${url} ${dim("(no server exploration — you'll author the tests)")}`);
|
|
208
|
-
else
|
|
209
|
-
ctx.err(`${green("✓")} Project created — the agent is exploring ${url} and authoring tests ` +
|
|
210
|
-
`(watch with \`beryl explorations watch\`)`);
|
|
211
|
-
}
|
|
212
|
-
const localFile = path.join(cwd, LOCAL_CONFIG_FILENAME);
|
|
213
|
-
let local = {};
|
|
214
|
-
try {
|
|
215
|
-
local = JSON.parse(fs.readFileSync(localFile, "utf8"));
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
// no local config yet
|
|
219
|
-
}
|
|
220
|
-
fs.writeFileSync(localFile, JSON.stringify({ ...local, workspace: workspaceId, project: projectId }, null, 2) + "\n");
|
|
221
|
-
ctx.err(`${green("✓")} Pinned to ${LOCAL_CONFIG_FILENAME} ${dim(`(${projectId})`)}`);
|
|
222
|
-
}
|
|
223
171
|
const choice = flagStr(input, "editor-tools") ?? "auto";
|
|
224
172
|
const editors = choice === "auto"
|
|
225
173
|
? detectEditors(cwd)
|
|
@@ -232,14 +180,35 @@ export const initCommands = [
|
|
|
232
180
|
// and local authoring needs the Playwright MCP — so default it on. `--no-local` (parsed
|
|
233
181
|
// as an explicit false) opts out.
|
|
234
182
|
const local = input.flags.local ?? editors.length > 0;
|
|
183
|
+
const scope = (flagStr(input, "scope") ?? "user");
|
|
235
184
|
for (const editor of editors) {
|
|
185
|
+
if (scope === "user" && editor === "claude-code") {
|
|
186
|
+
// Claude Code owns ~/.claude.json — shell out to `claude mcp add` rather than write it.
|
|
187
|
+
const berylOk = claudeUserAdd("beryl", MCP_SERVER_ENTRY);
|
|
188
|
+
const playwrightOk = local ? claudeUserAdd("playwright", PLAYWRIGHT_SERVER_ENTRY) : undefined;
|
|
189
|
+
if (berylOk) {
|
|
190
|
+
ctx.err(`${green("✓")} claude-code MCP configured ${dim("(user scope)")}`);
|
|
191
|
+
if (playwrightOk)
|
|
192
|
+
ctx.err(`${green("✓")} claude-code Playwright MCP configured ${dim("(user scope)")}`);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
ctx.err(yellow("• `claude` not on PATH — run these to wire user-scope MCP servers:"));
|
|
196
|
+
ctx.err(` ${cyan(claudeAddHint("beryl", MCP_SERVER_ENTRY))}`);
|
|
197
|
+
if (local)
|
|
198
|
+
ctx.err(` ${cyan(claudeAddHint("playwright", PLAYWRIGHT_SERVER_ENTRY))}`);
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
236
202
|
const file = editor === "claude-code"
|
|
237
203
|
? path.join(cwd, ".mcp.json")
|
|
238
|
-
:
|
|
204
|
+
: scope === "user"
|
|
205
|
+
? cursorUserConfigPath()
|
|
206
|
+
: path.join(cwd, ".cursor", "mcp.json");
|
|
239
207
|
const wrote = mergeMcpConfig(file, local);
|
|
240
|
-
|
|
208
|
+
const where = scope === "user" ? file : path.relative(cwd, file);
|
|
209
|
+
ctx.err(`${green("✓")} ${editor} MCP ${wrote.beryl ? "configured" : "already configured"} ${dim(where)}`);
|
|
241
210
|
if (wrote.playwright !== undefined)
|
|
242
|
-
ctx.err(`${green("✓")} ${editor} Playwright MCP ${wrote.playwright ? "configured" : "already configured"} ${dim(
|
|
211
|
+
ctx.err(`${green("✓")} ${editor} Playwright MCP ${wrote.playwright ? "configured" : "already configured"} ${dim(where)}`);
|
|
243
212
|
}
|
|
244
213
|
if (choice === "auto" && editors.length === 0)
|
|
245
214
|
ctx.err(dim("No coding agent detected — pass --editor-tools claude-code|cursor to wire one."));
|
|
@@ -251,28 +220,18 @@ export const initCommands = [
|
|
|
251
220
|
ctx.err(`${dim("•")} Beryl authoring skill left as-is ${dim(`(${skillRel} — you edited it; delete it to reinstall)`)}`);
|
|
252
221
|
else
|
|
253
222
|
ctx.err(`${green("✓")} Beryl authoring skill ${skill.outcome === "wrote" ? "installed" : "already installed"} ${dim(skillRel)}`);
|
|
254
|
-
const nextSteps =
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
` ${
|
|
260
|
-
|
|
261
|
-
` ${cyan("beryl runs trigger --watch")} run the suite\n\n` +
|
|
262
|
-
`Author against the ActionPlan JSON Schema: ${cyan(ACTION_PLAN_SCHEMA_URL)}`
|
|
263
|
-
: `\n${bold("Beryl is set up.")} Try:\n` +
|
|
264
|
-
` ${cyan("beryl runs trigger --watch")} run the suite\n` +
|
|
265
|
-
` ${cyan("beryl explorations watch")} watch the agent work\n` +
|
|
266
|
-
` ${cyan("beryl tests list")} see authored tests` +
|
|
267
|
-
(local
|
|
268
|
-
? `\n\nPlaywright MCP is wired — your coding agent can explore the site locally ` +
|
|
269
|
-
`and push tests with ${cyan("beryl tests create")}.`
|
|
270
|
-
: "");
|
|
223
|
+
const nextSteps = `\n${bold("Beryl is set up — now open your editor and ask Claude to write tests.")}\n` +
|
|
224
|
+
` ${dim('• Say: "write tests for https://your-app.com" — Claude picks your workspace/project')}\n` +
|
|
225
|
+
` ${dim(" and sets the URL for you (no pin, no prompt).")}\n` +
|
|
226
|
+
(local
|
|
227
|
+
? ` ${dim("• Playwright MCP is wired — Claude can drive a real browser to author from your plan.")}\n`
|
|
228
|
+
: ` ${dim("• Re-run with --editor-tools to wire the Playwright MCP for local authoring.")}\n`) +
|
|
229
|
+
`\nAuthor against the ActionPlan JSON Schema: ${cyan(ACTION_PLAN_SCHEMA_URL)}`;
|
|
271
230
|
// The .mcp.json entry is pinned to @latest, but a global install / old npx cache
|
|
272
231
|
// still wins resolution — so tell the user when the CLI they just ran is stale.
|
|
273
232
|
await warnIfStale(cliVersion(), (msg) => ctx.err(yellow(msg)));
|
|
274
233
|
return {
|
|
275
|
-
data: {
|
|
234
|
+
data: { editors },
|
|
276
235
|
human: nextSteps,
|
|
277
236
|
};
|
|
278
237
|
},
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { saveGlobalConfig } from "../config.js";
|
|
2
1
|
import { detectAuthGating } from "../detect.js";
|
|
3
2
|
import { UsageError } from "../errors.js";
|
|
4
3
|
import { dim, green } from "../output.js";
|
|
@@ -198,22 +197,4 @@ export const projectCommands = [
|
|
|
198
197
|
};
|
|
199
198
|
},
|
|
200
199
|
},
|
|
201
|
-
{
|
|
202
|
-
name: "projects use",
|
|
203
|
-
summary: "Set the default project for future commands",
|
|
204
|
-
scope: "workspace",
|
|
205
|
-
args: [{ name: "project", description: "Project id, name, or URL", required: true }],
|
|
206
|
-
async run(ctx, input) {
|
|
207
|
-
const value = arg(input, "project");
|
|
208
|
-
const { workspaceId, projectId } = await ctx.requireProject({
|
|
209
|
-
args: {},
|
|
210
|
-
flags: { ...input.flags, project: value },
|
|
211
|
-
});
|
|
212
|
-
saveGlobalConfig({ workspace: workspaceId, project: projectId });
|
|
213
|
-
return {
|
|
214
|
-
data: { workspace: workspaceId, project: projectId },
|
|
215
|
-
human: `${green("Default project set")}: ${projectId} ${dim(`(workspace ${workspaceId})`)}`,
|
|
216
|
-
};
|
|
217
|
-
},
|
|
218
|
-
},
|
|
219
200
|
];
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { saveGlobalConfig } from "../config.js";
|
|
2
|
-
import { green } from "../output.js";
|
|
3
1
|
import { arg, flagBool, flagStr } from "./util.js";
|
|
4
2
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5
3
|
export const workspaceCommands = [
|
|
@@ -75,23 +73,6 @@ export const workspaceCommands = [
|
|
|
75
73
|
return { human: "Deleted." };
|
|
76
74
|
},
|
|
77
75
|
},
|
|
78
|
-
{
|
|
79
|
-
name: "workspaces use",
|
|
80
|
-
summary: "Set the default workspace for future commands",
|
|
81
|
-
args: [{ name: "workspace", description: "Workspace id or name", required: true }],
|
|
82
|
-
async run(ctx, input) {
|
|
83
|
-
const value = arg(input, "workspace");
|
|
84
|
-
const id = UUID_RE.test(value)
|
|
85
|
-
? value
|
|
86
|
-
: await ctx.requireWorkspace({ args: {}, flags: { workspace: value } });
|
|
87
|
-
const detail = (await ctx.client.get(`/workspaces/${id}`));
|
|
88
|
-
saveGlobalConfig({ workspace: id, project: undefined });
|
|
89
|
-
return {
|
|
90
|
-
data: { workspace: id },
|
|
91
|
-
human: `${green("Default workspace set")}: ${detail.name} (${id})`,
|
|
92
|
-
};
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
76
|
{
|
|
96
77
|
name: "workspaces history",
|
|
97
78
|
summary: "Show the workspace's action history (who did what, when)",
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,6 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
export const DEFAULT_API_URL = "https://api.beryl.so";
|
|
5
|
-
export const LOCAL_CONFIG_FILENAME = ".beryl.json";
|
|
6
5
|
export function globalConfigPath(env = process.env) {
|
|
7
6
|
const base = env.BERYL_CONFIG_DIR ??
|
|
8
7
|
(env.XDG_CONFIG_HOME ? path.join(env.XDG_CONFIG_HOME, "beryl") : undefined) ??
|
|
@@ -17,30 +16,15 @@ function readJson(file) {
|
|
|
17
16
|
return undefined;
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
|
-
export function
|
|
21
|
-
let dir = path.resolve(cwd);
|
|
22
|
-
for (;;) {
|
|
23
|
-
const candidate = path.join(dir, LOCAL_CONFIG_FILENAME);
|
|
24
|
-
if (fs.existsSync(candidate))
|
|
25
|
-
return candidate;
|
|
26
|
-
const parent = path.dirname(dir);
|
|
27
|
-
if (parent === dir)
|
|
28
|
-
return undefined;
|
|
29
|
-
dir = parent;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
export function loadConfig(env = process.env, cwd = process.cwd()) {
|
|
19
|
+
export function loadConfig(env = process.env) {
|
|
33
20
|
const globalPath = globalConfigPath(env);
|
|
34
21
|
const global = readJson(globalPath) ?? {};
|
|
35
|
-
const localPath = findLocalConfig(cwd);
|
|
36
|
-
const local = localPath ? (readJson(localPath) ?? {}) : {};
|
|
37
22
|
return {
|
|
38
|
-
apiUrl: env.BERYL_API_URL ??
|
|
39
|
-
token: env.BERYL_API_KEY ?? env.BERYL_TOKEN ??
|
|
40
|
-
workspace: env.BERYL_WORKSPACE
|
|
41
|
-
project: env.BERYL_PROJECT
|
|
23
|
+
apiUrl: env.BERYL_API_URL ?? global.api_url ?? DEFAULT_API_URL,
|
|
24
|
+
token: env.BERYL_API_KEY ?? env.BERYL_TOKEN ?? global.token,
|
|
25
|
+
workspace: env.BERYL_WORKSPACE,
|
|
26
|
+
project: env.BERYL_PROJECT,
|
|
42
27
|
globalConfigPath: globalPath,
|
|
43
|
-
localConfigPath: localPath,
|
|
44
28
|
};
|
|
45
29
|
}
|
|
46
30
|
export function saveGlobalConfig(patch, env = process.env) {
|
package/dist/context.js
CHANGED
|
@@ -65,8 +65,7 @@ export function createContext(options) {
|
|
|
65
65
|
throw new CliError("You have no workspaces yet — create one with `beryl workspaces create`");
|
|
66
66
|
}
|
|
67
67
|
else {
|
|
68
|
-
throw new UsageError("Multiple workspaces — pass --workspace <id|name
|
|
69
|
-
"or run `beryl workspaces use <id|name>`:\n" +
|
|
68
|
+
throw new UsageError("Multiple workspaces — pass --workspace <id|name> or set BERYL_WORKSPACE:\n" +
|
|
70
69
|
workspaces.map((w) => ` ${w.id} ${w.name ?? ""}`).join("\n"));
|
|
71
70
|
}
|
|
72
71
|
return workspaceCache;
|
|
@@ -87,11 +86,11 @@ export function createContext(options) {
|
|
|
87
86
|
projectId = projects[0].id;
|
|
88
87
|
}
|
|
89
88
|
else if (projects.length === 0) {
|
|
90
|
-
throw new CliError("
|
|
89
|
+
throw new CliError("This workspace has no project yet — create one with " +
|
|
90
|
+
"`beryl projects create <url>` (add --no-explore to author tests yourself).");
|
|
91
91
|
}
|
|
92
92
|
else {
|
|
93
|
-
throw new UsageError("Multiple projects — pass --project <id|name|url
|
|
94
|
-
"or run `beryl projects use <id|name>`:\n" +
|
|
93
|
+
throw new UsageError("Multiple projects — pass --project <id|name|url> or set BERYL_PROJECT:\n" +
|
|
95
94
|
projects.map((p) => ` ${p.id} ${p.name ?? p.root_url ?? ""}`).join("\n"));
|
|
96
95
|
}
|
|
97
96
|
projectCache = { workspaceId, projectId };
|
package/dist/registry/index.js
CHANGED
|
@@ -15,14 +15,12 @@ import { workspaceCommands } from "../commands/workspaces.js";
|
|
|
15
15
|
export const WORKSPACE_FLAG = {
|
|
16
16
|
name: "workspace",
|
|
17
17
|
type: "string",
|
|
18
|
-
description: "Workspace id or name (defaults to BERYL_WORKSPACE,
|
|
19
|
-
"or your only workspace)",
|
|
18
|
+
description: "Workspace id or name (defaults to BERYL_WORKSPACE, or your only workspace)",
|
|
20
19
|
};
|
|
21
20
|
export const PROJECT_FLAG = {
|
|
22
21
|
name: "project",
|
|
23
22
|
type: "string",
|
|
24
|
-
description: "Project id, name, or URL (defaults to BERYL_PROJECT,
|
|
25
|
-
"or the workspace's only project)",
|
|
23
|
+
description: "Project id, name, or URL (defaults to BERYL_PROJECT, or the workspace's only project)",
|
|
26
24
|
};
|
|
27
25
|
function withScopeFlags(spec) {
|
|
28
26
|
if (!spec.scope || spec.scope === "none")
|
|
@@ -35,6 +33,24 @@ function withScopeFlags(spec) {
|
|
|
35
33
|
extra.push(PROJECT_FLAG);
|
|
36
34
|
return { ...spec, flags: [...(spec.flags ?? []), ...extra] };
|
|
37
35
|
}
|
|
36
|
+
// Command groups kept OUT of the `beryl mcp` tool surface while staying full CLI commands:
|
|
37
|
+
// account/billing/team admin, Slack alerts, feedback, and the cloud-exploration mode
|
|
38
|
+
// (incl. `projects re-explore`) — none of it is part of authoring/editing/running tests,
|
|
39
|
+
// which is all an agent drives over MCP.
|
|
40
|
+
const MCP_HIDDEN_PREFIXES = [
|
|
41
|
+
"slack",
|
|
42
|
+
"account",
|
|
43
|
+
"billing",
|
|
44
|
+
"feedback",
|
|
45
|
+
"explorations",
|
|
46
|
+
"members",
|
|
47
|
+
"invites",
|
|
48
|
+
"projects re-explore",
|
|
49
|
+
];
|
|
50
|
+
function hideFromMcp(spec) {
|
|
51
|
+
const hidden = MCP_HIDDEN_PREFIXES.some((p) => spec.name === p || spec.name.startsWith(`${p} `));
|
|
52
|
+
return hidden ? { ...spec, mcpHidden: true } : spec;
|
|
53
|
+
}
|
|
38
54
|
export const commands = [
|
|
39
55
|
...initCommands,
|
|
40
56
|
...authCommands,
|
|
@@ -50,7 +66,7 @@ export const commands = [
|
|
|
50
66
|
...inboxCommands,
|
|
51
67
|
...accountCommands,
|
|
52
68
|
...mcpCommands,
|
|
53
|
-
].map(withScopeFlags);
|
|
69
|
+
].map(withScopeFlags).map(hideFromMcp);
|
|
54
70
|
export function findCommand(words) {
|
|
55
71
|
let best;
|
|
56
72
|
for (const spec of commands) {
|
package/package.json
CHANGED