@beryl-so/cli 0.29.0 → 0.33.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,69 @@
1
+ import { UsageError } from "../errors.js";
2
+ import { arg, findGroup, flagBool, projectPath } from "./util.js";
3
+ async function resolveGroup(ctx, workspaceId, projectId, ref) {
4
+ const groups = (await ctx.client.get(`${projectPath(workspaceId, projectId)}/groups`));
5
+ const match = findGroup(groups, ref);
6
+ if (!match) {
7
+ const names = groups.map((g) => g.name).join(", ") || "none yet";
8
+ throw new UsageError(`No group '${ref}' in this project (existing: ${names}).`);
9
+ }
10
+ return match;
11
+ }
12
+ export const groupCommands = [
13
+ {
14
+ name: "groups list",
15
+ summary: "List the project's test groups and how many tests each holds",
16
+ scope: "project",
17
+ groupDefault: true,
18
+ groupSummary: "Manage a project's test groups: labels a test can carry any number of, used to filter, run, or schedule a slice of the suite. Groups are created only here (or in Settings → Groups); assigning a test to an unknown name is an error.",
19
+ async run(ctx, input) {
20
+ const { workspaceId, projectId } = await ctx.requireProject(input);
21
+ return { data: await ctx.client.get(`${projectPath(workspaceId, projectId)}/groups`) };
22
+ },
23
+ },
24
+ {
25
+ name: "groups create",
26
+ summary: "Create a group",
27
+ scope: "project",
28
+ args: [{ name: "name", description: "Group name (unique per project)", required: true }],
29
+ examples: ["beryl groups create Smoke"],
30
+ async run(ctx, input) {
31
+ const { workspaceId, projectId } = await ctx.requireProject(input);
32
+ return {
33
+ data: await ctx.client.post(`${projectPath(workspaceId, projectId)}/groups`, {
34
+ name: arg(input, "name"),
35
+ }),
36
+ };
37
+ },
38
+ },
39
+ {
40
+ name: "groups rename",
41
+ summary: "Rename a group (tests keep their membership)",
42
+ scope: "project",
43
+ args: [
44
+ { name: "group", description: "Current group name or id", required: true },
45
+ { name: "name", description: "New name", required: true },
46
+ ],
47
+ async run(ctx, input) {
48
+ const { workspaceId, projectId } = await ctx.requireProject(input);
49
+ const group = await resolveGroup(ctx, workspaceId, projectId, arg(input, "group"));
50
+ return {
51
+ data: await ctx.client.patch(`${projectPath(workspaceId, projectId)}/groups/${group.id}`, { name: arg(input, "name") }),
52
+ };
53
+ },
54
+ },
55
+ {
56
+ name: "groups delete",
57
+ summary: "Delete a group: its tests stay; a schedule targeting only this group goes with it",
58
+ scope: "project",
59
+ args: [{ name: "group", description: "Group name or id", required: true }],
60
+ flags: [{ name: "force", type: "boolean", description: "Skip the confirmation prompt" }],
61
+ async run(ctx, input) {
62
+ const { workspaceId, projectId } = await ctx.requireProject(input);
63
+ const group = await resolveGroup(ctx, workspaceId, projectId, arg(input, "group"));
64
+ await ctx.confirm(`Delete group '${group.name}' (${group.test_count ?? 0} tests leave it)?`, flagBool(input, "force"));
65
+ await ctx.client.del(`${projectPath(workspaceId, projectId)}/groups/${group.id}`);
66
+ return { human: "Deleted." };
67
+ },
68
+ },
69
+ ];
@@ -8,7 +8,7 @@ export const healthCommands = [
8
8
  {
9
9
  name: "health get",
10
10
  groupDefault: true,
11
- groupSummary: "Site Health how your site reads to search engines and visitors: content, " +
11
+ groupSummary: "Site Health, or how your site reads to search engines and visitors: content, " +
12
12
  "speed, mobile, links and security, graded from a real check of your live pages.",
13
13
  summary: "Show the latest Site Health report for a project environment",
14
14
  description: "A check runs automatically when a project or environment gets its URL. While one " +
@@ -147,20 +147,20 @@ async function ensureLocalPlaywright(ctx, cwd) {
147
147
  export const initCommands = [
148
148
  {
149
149
  name: "init",
150
- summary: "Set up Beryl in this repo sign in and wire up your coding agent",
150
+ summary: "Set up Beryl in this repo: sign in and wire up your coding agent",
151
151
  description: "One-command onboarding: signs you in (browser confirm, or an emailed one-time code) " +
152
152
  "and wires up your coding " +
153
- "agent. Nothing is detected and nothing is conditional every run wires the beryl AND " +
153
+ "agent. Nothing is detected and nothing is conditional: every run wires the beryl AND " +
154
154
  "playwright MCP servers, writes the authoring skill (user scope: your home " +
155
155
  ".agents/skills/ + .claude/skills/, so it follows you into every session; --scope " +
156
156
  "project: the repo's own dirs, committed for teammates), and installs @playwright/test " +
157
- "+ chromium if missing browser authoring and local runs depend on it. By default the " +
157
+ "+ chromium if missing, since browser authoring and local runs depend on it. By default the " +
158
158
  "servers are wired per-user (matching where your login token lives) via `claude mcp add " +
159
159
  "-s user`; pass --scope project to write a committed .mcp.json for a shared repo " +
160
- "instead. No workspace/project pin and no URL prompt ask Claude to write tests for " +
160
+ "instead. No workspace/project pin and no URL prompt: ask Claude to write tests for " +
161
161
  "your site and it resolves the workspace, project, and URL. Safe to re-run: every run " +
162
162
  "refreshes the authoring skill to this CLI's version (overwriting an older or edited " +
163
- "copy `beryl guide` prints the same content), and skips whatever else is already set " +
163
+ "copy; `beryl guide` prints the same content), and skips whatever else is already set " +
164
164
  "up.",
165
165
  interactive: true,
166
166
  flags: [
@@ -170,7 +170,7 @@ export const initCommands = [
170
170
  enum: ["user", "project"],
171
171
  description: "Where to wire the MCP servers. `user` (default) configures them per-user (matching " +
172
172
  "where your Beryl login token lives) via `claude mcp add -s user`. `project` writes a " +
173
- "committed .mcp.json for a shared repo every teammate still runs `beryl login` to " +
173
+ "committed .mcp.json for a shared repo. Every teammate still runs `beryl login` to " +
174
174
  "authenticate",
175
175
  },
176
176
  {
@@ -285,8 +285,8 @@ export const initCommands = [
285
285
  description: "The full guide to authoring durable, healable tests: the ActionPlan shape, outcome " +
286
286
  "assertions, natural-language intent, test accounts and the project mailbox " +
287
287
  "({{login_email}}, {{mailbox_address}}, {{inbox_address}} + await_email), and the " +
288
- "local run-fix loop. Same content as the beryl-test skill `beryl init` installs " +
289
- `skip if a loaded beryl-test skill states v${cliVersion()}; else call this first ` +
288
+ "local run-fix loop. Same content as the beryl-test skill `beryl init` installs. " +
289
+ `Skip if a loaded beryl-test skill states v${cliVersion()}; else call this first ` +
290
290
  "(works without logging in).",
291
291
  examples: ["beryl guide"],
292
292
  async run() {
@@ -7,10 +7,10 @@ export const mailboxCommands = [
7
7
  name: "mailbox get",
8
8
  summary: "The project's mailbox address",
9
9
  groupDefault: true,
10
- groupSummary: "The project's standing email addresses where its tests receive sign-in mail.",
10
+ groupSummary: "The project's standing email addresses, where its tests receive sign-in mail.",
11
11
  description: "Returns the address {{mailbox_address}} resolves to, creating it on first ask. " +
12
12
  "Every test that reads mail receives here. A test needing an address the site has " +
13
- "never seen cites {{inbox_address}} instead that renders a `+tag` alias of this " +
13
+ "never seen cites {{inbox_address}} instead. That renders a `+tag` alias of this " +
14
14
  "same mailbox, so a signup stays repeatable without a second address to manage.",
15
15
  scope: "project",
16
16
  async run(ctx, input) {
@@ -46,7 +46,7 @@ export const mailboxCommands = [
46
46
  name: "mailbox create",
47
47
  summary: "Add a second mailbox to the project",
48
48
  description: "Only needed when a flow requires two genuinely separate inboxes at the same time " +
49
- "both sides of an invite handshake. For an address the site has not seen before, " +
49
+ "(both sides of an invite handshake). For an address the site has not seen before, " +
50
50
  "cite {{inbox_address}} in the plan instead: it aliases the existing mailbox and " +
51
51
  "costs nothing to manage.",
52
52
  scope: "project",
@@ -88,12 +88,12 @@ export const mailboxCommands = [
88
88
  name: "mailbox read",
89
89
  summary: "Read the latest email in a mailbox (waits for one to arrive)",
90
90
  description: "Waits up to --timeout-s for a matching email and returns it (one blocking request; " +
91
- "the server caps the wait at 50s re-run to keep waiting). With --extract-code, " +
91
+ "the server caps the wait at 50s, so re-run to keep waiting). With --extract-code, " +
92
92
  "also asks the server to pull the one-time code out of the email (AI-assisted when " +
93
93
  "the email is ambiguous; `code` is null if none was found). Use " +
94
94
  "--recipient-contains to read only one `+tag` alias's mail when several identities " +
95
95
  "share the mailbox. Exits non-zero if nothing arrives before the timeout. Waits for " +
96
- "and returns ONE latest matching email `mailbox emails` lists what has already " +
96
+ "and returns ONE latest matching email. `mailbox emails` lists what has already " +
97
97
  "arrived, without waiting.",
98
98
  scope: "project",
99
99
  args: [{ name: "mailbox-id", description: "Mailbox id from `beryl mailbox list`", required: true }],
@@ -154,7 +154,7 @@ export const mailboxCommands = [
154
154
  {
155
155
  name: "mailbox emails",
156
156
  summary: "List the emails a mailbox has received",
157
- description: "Returns the already-received emails without waiting `mailbox read` blocks for " +
157
+ description: "Returns the already-received emails without waiting. `mailbox read` blocks for " +
158
158
  "a matching one and returns just it.",
159
159
  scope: "project",
160
160
  args: [{ name: "mailbox-id", description: "Mailbox id from `beryl mailbox list`", required: true }],
@@ -3,9 +3,9 @@ export const mcpCommands = [
3
3
  {
4
4
  name: "version",
5
5
  summary: "Show the running CLI version, API URL, and Node version",
6
- description: "Answers \"which build am I actually talking to?\" the one question a long-lived " +
6
+ description: "Answers \"which build am I actually talking to?\" (the one question a long-lived " +
7
7
  "`beryl mcp` process can't otherwise answer, since it loads source at spawn and never " +
8
- "hot-reloads. Needs no login, so it still works when a token is missing or broken. " +
8
+ "hot-reloads). Needs no login, so it still works when a token is missing or broken. " +
9
9
  "Also reports whether the running build is behind npm's `latest` (best-effort: " +
10
10
  "`update_available` is null when the registry can't be reached, and the check is " +
11
11
  "skipped entirely under BERYL_NO_UPDATE_CHECK).",
@@ -26,7 +26,7 @@ export const mcpCommands = [
26
26
  },
27
27
  {
28
28
  name: "mcp",
29
- summary: "Run the Beryl MCP server (stdio) every CLI command as an agent tool",
29
+ summary: "Run the Beryl MCP server (stdio): every CLI command as an agent tool",
30
30
  description: "Exposes the CLI's commands as MCP tools over stdio, so coding agents (Claude Code, " +
31
31
  "Cursor, …) can create projects, trigger runs, watch the agent, and edit tests. " +
32
32
  "Authenticate via BERYL_API_KEY or a prior `beryl login`.",
@@ -37,7 +37,7 @@ export const projectCommands = [
37
37
  summary: "List projects in the workspace",
38
38
  scope: "workspace",
39
39
  groupDefault: true,
40
- groupSummary: "Create and manage projects a site Beryl explores, authors tests for, and runs.",
40
+ groupSummary: "Create and manage projects: a site Beryl explores, authors tests for, and runs.",
41
41
  async run(ctx, input) {
42
42
  const ws = await ctx.requireWorkspace(input);
43
43
  const rows = (await ctx.client.get(`/workspaces/${ws}/projects`));
@@ -70,7 +70,7 @@ export const projectCommands = [
70
70
  },
71
71
  {
72
72
  name: "projects create",
73
- summary: "Create a project with a URL the agent starts exploring; with just --name an empty one",
73
+ summary: "Create a project. With a URL the agent starts exploring; with just --name an empty one",
74
74
  scope: "workspace",
75
75
  args: [
76
76
  {
@@ -91,13 +91,13 @@ export const projectCommands = [
91
91
  type: "string",
92
92
  enum: ["public", "gated"],
93
93
  description: "Whether the site needs a login (gated) or not (public). Default: detected from the " +
94
- "site only asked when detection is genuinely unsure",
94
+ "site, and only asked when detection is genuinely unsure",
95
95
  },
96
96
  { name: "force-new-login", type: "boolean", description: "Ignore any reusable saved login" },
97
97
  {
98
98
  name: "no-explore",
99
99
  type: "boolean",
100
- description: "Create the project without starting the cloud exploration author tests yourself " +
100
+ description: "Create the project without starting the cloud exploration. Author tests yourself " +
101
101
  "via `beryl tests create` or your coding agent over MCP",
102
102
  },
103
103
  { name: "watch", type: "boolean", description: "Stream the agent's exploration live" },
@@ -190,7 +190,7 @@ export const projectCommands = [
190
190
  },
191
191
  {
192
192
  name: "projects re-explore",
193
- summary: "Send the agent back in run/heal existing tests and discover new flows",
193
+ summary: "Send the agent back in to run/heal existing tests and discover new flows",
194
194
  scope: "project",
195
195
  flags: [
196
196
  { name: "watch", type: "boolean", description: "Stream the agent's exploration live" },
@@ -7,7 +7,7 @@ import { countPlannedFrames, countWrittenFrames, PlaywrightMissingError, } from
7
7
  import { dim, green, red, yellow } from "../output.js";
8
8
  import { anyGap, confirmInstall, describeGaps, installCommandsFor, installPlaywright, playwrightGaps, } from "../playwright-install.js";
9
9
  import { ProgressBar } from "../progress.js";
10
- import { arg, flagBool, flagNum, flagStr, projectPath } from "./util.js";
10
+ import { arg, flagBool, flagNum, flagStr, idsInGroup, projectPath } from "./util.js";
11
11
  import { watchRun } from "./watch.js";
12
12
  const MAX_FAILURE_SCREENSHOTS = 5;
13
13
  // One filesystem check before a single spec is fetched. A missing browser binary otherwise
@@ -50,10 +50,19 @@ export const runCommands = [
50
50
  name: "runs trigger",
51
51
  summary: "Trigger a test run (whole suite, a subset, or one environment)",
52
52
  description: "Runs execute in Beryl's cloud. With --watch the CLI streams live progress and " +
53
- "exits 0 only if every test passed wire it straight into CI.",
53
+ "exits 0 only if every test passed, so wire it straight into CI. A run with a " +
54
+ "heal-eligible failure completes only after Beryl has tried to heal it: a repaired " +
55
+ "test is re-run inside the same run and counts as passed (reported as `healed`), so " +
56
+ "the final counts, the report and the completion email all reflect the repair.",
54
57
  scope: "project",
55
58
  flags: [
56
59
  { name: "test", type: "strings", description: "Run only these test ids (repeatable)" },
60
+ {
61
+ name: "group",
62
+ type: "string",
63
+ description: "Run only the active tests in this group (by name), resolved to ids before the run, " +
64
+ "so the run records exactly what it ran. Cannot be combined with --test.",
65
+ },
57
66
  { name: "env", type: "string", description: "Environment id to run against" },
58
67
  { name: "url-override", type: "string", description: "Replace the base URL (preview deploys)" },
59
68
  {
@@ -75,11 +84,22 @@ export const runCommands = [
75
84
  "beryl runs trigger --url-override https://preview-123.example.com --watch --timeout 30",
76
85
  "beryl runs trigger --url-override https://preview-123.example.com --header x-vercel-protection-bypass=<token> --watch",
77
86
  "beryl runs trigger --test 4f… --test 9a…",
87
+ "beryl runs trigger --group Smoke --watch",
78
88
  "beryl runs trigger --retries 0 --watch",
79
89
  ],
80
90
  async run(ctx, input) {
81
91
  const { workspaceId, projectId } = await ctx.requireProject(input);
82
- const tests = input.flags.test;
92
+ let tests = input.flags.test;
93
+ const group = flagStr(input, "group");
94
+ if (group && tests && tests.length > 0)
95
+ throw new UsageError("--group cannot be combined with --test");
96
+ if (group) {
97
+ const listed = (await ctx.client.get(`${projectPath(workspaceId, projectId)}/tests`));
98
+ tests = idsInGroup(listed, group);
99
+ // Exiting 0 here would be a green CI run that tested nothing.
100
+ if (tests.length === 0)
101
+ throw new UsageError(`No active tests in group '${group}'.`);
102
+ }
83
103
  const extraHeaders = parseHeaders(input.flags.header);
84
104
  const created = (await ctx.client.post(`${projectPath(workspaceId, projectId)}/runs`, {
85
105
  test_case_ids: tests && tests.length > 0 ? tests : null,
@@ -96,7 +116,7 @@ export const runCommands = [
96
116
  {
97
117
  name: "runs local",
98
118
  summary: "Run tests on your machine with your own Playwright; results sync to Beryl",
99
- description: "Unlike `runs trigger`, the browser runs on YOUR machine each test's rendered spec is " +
119
+ description: "Unlike `runs trigger`, the browser runs on YOUR machine: each test's rendered spec is " +
100
120
  "fetched and run with your local @playwright/test and the Chromium binary it drives. " +
101
121
  "Both are checked once before any spec is fetched, so a machine that can't run tests " +
102
122
  "says so once instead of failing every test (on a terminal the CLI offers to install " +
@@ -109,12 +129,12 @@ export const runCommands = [
109
129
  "does, and scrubbed from any error text or DOM snapshot before results upload; a test " +
110
130
  "whose account has no stored password is skipped with the exact fix-it command. When the " +
111
131
  "run finishes, the results and replay artifacts are imported into Beryl as a normal run " +
112
- "(trigger source `local`) history, replay, and reports all work; pass --no-sync to " +
132
+ "(trigger source `local`), so history, replay, and reports all work; pass --no-sync to " +
113
133
  "keep a run entirely off the record while iterating. Session-mode tests behave as " +
114
134
  "in the cloud: their account signs in once per invocation and every session-mode " +
115
135
  "test rides that session; a failed sign-in fails those tests with the same " +
116
136
  "SESSION_* reason a cloud run reports. Only a test that depends on a session Beryl " +
117
- "captured server-side is skipped, with a note run those with `runs trigger`. Point " +
137
+ "captured server-side is skipped, with a note. Run those with `runs trigger`. Point " +
118
138
  "--url-override at a local dev server or preview, and --dir to keep specs, artifacts, " +
119
139
  "and reports on disk. Exits 0 only if every executed test passed.",
120
140
  scope: "project",
@@ -131,6 +151,11 @@ export const runCommands = [
131
151
  type: "boolean",
132
152
  description: "Run every active test in the project (the default when no ids are given)",
133
153
  },
154
+ {
155
+ name: "group",
156
+ type: "string",
157
+ description: "Run only the active tests in this group (by name). Cannot be combined with test ids.",
158
+ },
134
159
  {
135
160
  name: "url-override",
136
161
  type: "string",
@@ -139,7 +164,7 @@ export const runCommands = [
139
164
  {
140
165
  name: "env",
141
166
  type: "string",
142
- description: "Environment id to run against and attach the imported run to use for a " +
167
+ description: "Environment id to run against and attach the imported run to. Use for a " +
143
168
  "standing environment; --url-override is for a throwaway host",
144
169
  },
145
170
  {
@@ -162,15 +187,23 @@ export const runCommands = [
162
187
  "beryl runs local 4f… --no-sync --dir ./beryl-local",
163
188
  ],
164
189
  async run(ctx, input) {
165
- await ensureRunnableLocally(ctx);
166
- const { workspaceId, projectId } = await ctx.requireProject(input);
190
+ // Argument validation runs BEFORE the Chromium/Playwright precondition: a bad flag
191
+ // combination should say so on any machine, not report a missing browser.
167
192
  // Deduped: a repeated id would put the same test twice in one imported run,
168
193
  // which the import manifest rejects.
169
194
  const explicitIds = [...new Set(input.args["test-ids"] ?? [])];
170
- // No ids means the whole suite — `beryl runs local` alone is a complete local run.
171
- const all = explicitIds.length === 0;
195
+ const group = flagStr(input, "group");
196
+ if (group && explicitIds.length > 0)
197
+ throw new UsageError("--group cannot be combined with explicit test ids");
172
198
  if (explicitIds.length > 0 && flagBool(input, "all"))
173
199
  throw new UsageError("--all cannot be combined with explicit test ids");
200
+ if (group && flagBool(input, "all"))
201
+ throw new UsageError("--all cannot be combined with --group");
202
+ // No ids and no group means the whole suite — `beryl runs local` alone is a
203
+ // complete local run.
204
+ const all = explicitIds.length === 0 && !group;
205
+ await ensureRunnableLocally(ctx);
206
+ const { workspaceId, projectId } = await ctx.requireProject(input);
174
207
  const sync = input.flags.sync !== false;
175
208
  const urlOverride = flagStr(input, "url-override");
176
209
  const dir = flagStr(input, "dir");
@@ -180,9 +213,15 @@ export const runCommands = [
180
213
  // The list rows carry the authored name as nl_title (title is the customer's
181
214
  // rename, usually unset) — fall through so the terminal shows names, not ids.
182
215
  const titles = new Map(listed.map((t) => [t.id, t.title || t.nl_title || t.id.slice(0, 8)]));
183
- const ids = all ? listed.filter((t) => t.is_active !== false).map((t) => t.id) : explicitIds;
216
+ const ids = all
217
+ ? listed.filter((t) => t.is_active !== false).map((t) => t.id)
218
+ : group
219
+ ? idsInGroup(listed, group)
220
+ : explicitIds;
184
221
  if (ids.length === 0)
185
- throw new CliError("This project has no tests to run.");
222
+ throw group
223
+ ? new UsageError(`No active tests in group '${group}'.`)
224
+ : new CliError("This project has no tests to run.");
186
225
  const fetchScript = (id) => ctx.client.get(`${projectPath(workspaceId, projectId)}/tests/${id}/script`, {
187
226
  // frames only matter when the run will be imported: they become the replay.
188
227
  // environment_id keeps the baked login_email on the same environment the
@@ -454,15 +493,36 @@ export const runCommands = [
454
493
  {
455
494
  name: "runs list",
456
495
  summary: "List recent runs",
496
+ description: "Returns the 50 most recent runs unless --page is given; pass --page to walk the " +
497
+ "full history a slice at a time.",
457
498
  scope: "project",
458
499
  groupDefault: true,
459
500
  groupSummary: "Trigger a run of a project's tests (e.g. in CI), then watch, inspect, and download results.",
460
- flags: [{ name: "env", type: "string", description: "Filter by environment id" }],
501
+ flags: [
502
+ { name: "env", type: "string", description: "Filter by environment id" },
503
+ {
504
+ name: "page",
505
+ type: "number",
506
+ description: "Return only this 1-indexed page instead of the 50 most recent runs",
507
+ },
508
+ {
509
+ name: "page-size",
510
+ type: "number",
511
+ description: "Runs per page when --page is given (default 8, max 100)",
512
+ },
513
+ ],
461
514
  async run(ctx, input) {
462
515
  const { workspaceId, projectId } = await ctx.requireProject(input);
463
- const rows = (await ctx.client.get(`${projectPath(workspaceId, projectId)}/runs`, {
464
- environment_id: flagStr(input, "env"),
465
- }));
516
+ const page = flagNum(input, "page");
517
+ const rows = page === undefined
518
+ ? (await ctx.client.get(`${projectPath(workspaceId, projectId)}/runs`, {
519
+ environment_id: flagStr(input, "env"),
520
+ }))
521
+ : (await ctx.client.get(`${projectPath(workspaceId, projectId)}/runs/page`, {
522
+ page,
523
+ page_size: flagNum(input, "page-size"),
524
+ environment_id: flagStr(input, "env"),
525
+ })).items;
466
526
  return { data: rows.map(({ test_results: _omit, ...row }) => row) };
467
527
  },
468
528
  },
@@ -471,8 +531,8 @@ export const runCommands = [
471
531
  summary: "Show one run with its per-test results",
472
532
  description: "Over MCP the failure screenshots come back as viewable image content, so an agent can " +
473
533
  "look at the page that broke instead of guessing from the error string. Set screenshots " +
474
- "to false to skip fetching them. Ignored outside MCP the terminal cannot show an image. " +
475
- "Returns the run row with its per-test results `runs report` returns the generated " +
534
+ "to false to skip fetching them. Ignored outside MCP (the terminal cannot show an image). " +
535
+ "Returns the run row with its per-test results. `runs report` returns the generated " +
476
536
  "report document, `runs explain` an AI explanation of one failed result.",
477
537
  scope: "project",
478
538
  args: [{ name: "run-id", description: "Run id", required: true }],
@@ -525,7 +585,7 @@ export const runCommands = [
525
585
  {
526
586
  name: "runs report",
527
587
  summary: "Show the generated report for a run",
528
- description: "Returns the run's stored generated report (404 until it has been generated) " +
588
+ description: "Returns the run's stored generated report (404 until it has been generated). " +
529
589
  "`runs get` returns the raw run row with per-test results.",
530
590
  scope: "project",
531
591
  args: [{ name: "run-id", description: "Run id", required: true }],
@@ -539,8 +599,8 @@ export const runCommands = [
539
599
  {
540
600
  name: "runs download",
541
601
  summary: "Download a run's results, with its artifacts, to disk",
542
- description: "With --dir, fetches the artifact bytes screenshots, DOM snapshots, the Playwright " +
543
- "trace zip, and the filmstrip frames of the failing tests into <dir>/<test-result-id>/ " +
602
+ description: "With --dir, fetches the artifact bytes (screenshots, DOM snapshots, the Playwright " +
603
+ "trace zip, and the filmstrip frames of the failing tests) into <dir>/<test-result-id>/ " +
544
604
  "alongside a run.json manifest. Artifact URLs are short-lived, so download rather than " +
545
605
  "stash them. With --out (or neither), writes only the JSON manifest.",
546
606
  scope: "project",
@@ -607,7 +667,7 @@ export const runCommands = [
607
667
  name: "runs explain",
608
668
  summary: "Explain, with AI, why a test result failed",
609
669
  description: "Takes a single test-RESULT id (not a run id) and returns an AI failure " +
610
- "explanation for that result `runs get` lists a run's results and their ids.",
670
+ "explanation for that result. `runs get` lists a run's results and their ids.",
611
671
  scope: "project",
612
672
  args: [{ name: "result-id", description: "Test result id (from `beryl runs get`)", required: true }],
613
673
  async run(ctx, input) {