@beryl-so/cli 0.63.2 → 0.67.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # beryl CLI
4
4
 
5
- Beryl on the command line: create projects, watch the agent explore and author tests,
5
+ Beryl on the command line: create projects, author tests,
6
6
  trigger runs in CI, manage everything the web app can, plus an MCP server (`beryl mcp`)
7
7
  that exposes every command to coding agents.
8
8
 
@@ -166,7 +166,7 @@ Send, list, and revoke workspace invitations, and accept ones sent to you.
166
166
 
167
167
  ### projects
168
168
 
169
- Create and manage projects: a site Beryl explores, authors tests for, and runs.
169
+ Create and manage projects: a site you author tests for and run.
170
170
 
171
171
  `beryl projects` with no subcommand runs `projects list`.
172
172
 
@@ -174,7 +174,7 @@ Create and manage projects: a site Beryl explores, authors tests for, and runs.
174
174
  | --- | --- | --- |
175
175
  | `beryl projects list` | List projects in the workspace | `projects_list` |
176
176
  | `beryl projects get` | Show one project, including its current exploration state | `projects_get` |
177
- | `beryl projects create [url]` | Create a project. With a URL the agent starts exploring; with just --name an empty one | `projects_create` |
177
+ | `beryl projects create [url]` | Create a project for a URL, or an empty one with just --name | `projects_create` |
178
178
  | `beryl projects rename <name>` | Rename a project | `projects_rename` |
179
179
  | `beryl projects quarantine [state]` | Turn auto quarantine on or off and tune when it mutes and releases a test | `projects_quarantine` |
180
180
  | `beryl projects delete` | Delete a project and all its tests and runs | `projects_delete` |
@@ -354,6 +354,7 @@ Durable identities on the site under test: what an authenticated test signs in a
354
354
  | Command | Summary | MCP tool |
355
355
  | --- | --- | --- |
356
356
  | `beryl accounts list` | List the test accounts an environment's tests sign in as | `accounts_list` |
357
+ | `beryl accounts get <account-id>` | Show one test account: origin, lifetime, sign-in state | `accounts_get` |
357
358
  | `beryl accounts create` | Add a test account: the customer's own, or one Beryl signs up | `accounts_create` |
358
359
  | `beryl accounts signup <account-id>` | Store a test account's signup plan and sign it up on its environment | `accounts_signup` |
359
360
  | `beryl accounts set-login <account-id>` | Store the sign-in plan a run replays once, plus the probe that proves it | `accounts_set_login` |
@@ -115,7 +115,9 @@ Set up (once): if you cannot call the Beryl tools yet, run \`beryl init\` in a t
115
115
  install the browser and wire the tools. Then, before the first plan:
116
116
 
117
117
  - \`envs_list\`: the root URL gotos resolve against (§2). If it is empty, set it with
118
- \`envs_update\`.
118
+ \`envs_update\` to a URL Beryl's cloud can reach, never \`localhost\`, \`127.0.0.1\`
119
+ or a private IP (refused). For a local dev server, pass \`url-override\` to
120
+ \`tests_create\` and \`runs_local\` instead.
119
121
  - \`accounts_list\`: who do authenticated tests sign in as (§1)? Store its sign-in with
120
122
  \`accounts_set_login\`, prove it with \`accounts_check\`.
121
123
  - \`mailbox_get\`: the address the account receives mail at (§5).
@@ -143,9 +145,9 @@ An authenticated test **signs in as an account that already exists** — it does
143
145
  one up. Start every authoring session with \`accounts_list\`:
144
146
 
145
147
  \`\`\`
146
- label email type login status
147
- default * qa@acme.test user_provided password ready
148
- admin qa+admin@x7k2p9.email.beryl.so beryl otp ready
148
+ label email type login status
149
+ default * qa@acme.test provided password ready
150
+ admin qa+admin@x7k2p9.email.beryl.so provisioned (permanent) otp ready
149
151
  \`\`\`
150
152
 
151
153
  Author the sign-in as ordinary opening steps citing the reserved handles — fill
@@ -26,7 +26,7 @@ function accountTable(rows) {
26
26
  return table(rows.map((a) => ({
27
27
  label: a.identity_label + (a.is_default ? " *" : ""),
28
28
  email: a.email,
29
- type: a.type,
29
+ type: a.type + (a.lifetime ? ` (${a.lifetime})` : ""),
30
30
  login: a.login_method,
31
31
  status: a.status,
32
32
  id: a.id,
@@ -58,14 +58,31 @@ export const testAccountCommands = [
58
58
  return { data: rows, human: accountTable(rows) };
59
59
  },
60
60
  },
61
+ {
62
+ name: "accounts get",
63
+ summary: "Show one test account: origin, lifetime, sign-in state",
64
+ description: "One test account by id. `type` is its origin: captured (a session from the Chrome " +
65
+ "plugin), provisioned (Beryl signed it up; `lifetime` permanent or throwaway), or " +
66
+ "provided (your own account).",
67
+ scope: "project",
68
+ args: [{ name: "account-id", description: "Account id from `beryl accounts list`", required: true }],
69
+ examples: ["beryl accounts get acc_123"],
70
+ async run(ctx, input) {
71
+ const { workspaceId, projectId } = await ctx.requireProject(input);
72
+ const account = (await ctx.client.get(`${accountsPath(workspaceId, projectId)}/${arg(input, "account-id")}`));
73
+ return { data: account, human: accountTable([account]) };
74
+ },
75
+ },
61
76
  {
62
77
  name: "accounts create",
63
78
  summary: "Add a test account: the customer's own, or one Beryl signs up",
64
- description: "Two kinds. `--type user_provided` records a dedicated account you already have on " +
65
- "the site: pass --email and --password, and it is usable immediately. `--type beryl` " +
66
- "reserves one Beryl will sign up itself, addressed at the project mailbox. It starts " +
67
- "`pending` and becomes usable after `beryl accounts signup`. " +
68
- "Works for Google-only apps too: bank a dedicated Google test account as user_provided " +
79
+ description: "Two kinds. `--type provided` records a dedicated account you already have on " +
80
+ "the site: pass --email and --password, and it is usable immediately. " +
81
+ "`--type provisioned` reserves one Beryl will sign up itself, addressed at the project " +
82
+ "mailbox; `--lifetime` says whether it is kept (permanent, the default) or a " +
83
+ "throwaway. It starts `pending` and becomes usable after `beryl accounts signup`. " +
84
+ "A captured account (a signed-in session from the Chrome plugin) is not created here. " +
85
+ "Works for Google-only apps too: bank a dedicated Google test account as provided " +
69
86
  "and author its sign-in plan through Google's form. " +
70
87
  "The first account an environment gets is its default whatever you pass.",
71
88
  scope: "project",
@@ -73,19 +90,25 @@ export const testAccountCommands = [
73
90
  {
74
91
  name: "type",
75
92
  type: "string",
76
- enum: ["user_provided", "beryl"],
77
- description: "Whose account this is (default: user_provided when --password is given)",
93
+ enum: ["provided", "provisioned"],
94
+ description: "Whose account this is (default: provided when --password is given)",
95
+ },
96
+ {
97
+ name: "lifetime",
98
+ type: "string",
99
+ enum: ["permanent", "throwaway"],
100
+ description: "provisioned only: keep it, or a throwaway (default permanent)",
78
101
  },
79
102
  {
80
103
  name: "label",
81
104
  type: "string",
82
105
  description: "Identity label a plan's auth_label names (default 'default')",
83
106
  },
84
- { name: "email", type: "string", description: "user_provided only: the account's email" },
107
+ { name: "email", type: "string", description: "provided only: the account's email" },
85
108
  {
86
109
  name: "password",
87
110
  type: "string",
88
- description: "user_provided only: stored encrypted as a project secret, never returned",
111
+ description: "provided only: stored encrypted as a project secret, never returned",
89
112
  },
90
113
  {
91
114
  name: "login-method",
@@ -98,7 +121,7 @@ export const testAccountCommands = [
98
121
  ],
99
122
  examples: [
100
123
  "beryl accounts create --email qa@acme.test --password 'hunter2'",
101
- "beryl accounts create --type beryl --login-method otp",
124
+ "beryl accounts create --type provisioned --login-method otp",
102
125
  "beryl accounts create --email admin@acme.test --password 'hunter2' --label admin",
103
126
  "beryl accounts create --email qa@gmail.com --password 'hunter2' # Google-only app",
104
127
  ],
@@ -106,7 +129,8 @@ export const testAccountCommands = [
106
129
  const { workspaceId, projectId } = await ctx.requireProject(input);
107
130
  const password = flagStr(input, "password");
108
131
  const account = (await ctx.client.post(accountsPath(workspaceId, projectId), {
109
- type: flagStr(input, "type") ?? (password ? "user_provided" : "beryl"),
132
+ type: flagStr(input, "type") ?? (password ? "provided" : "provisioned"),
133
+ lifetime: flagStr(input, "lifetime") ?? null,
110
134
  identity_label: flagStr(input, "label") ?? "default",
111
135
  email: flagStr(input, "email") ?? null,
112
136
  password: password ?? null,
@@ -29,6 +29,11 @@ export const environmentCommands = [
29
29
  {
30
30
  name: "envs get",
31
31
  summary: "Show one environment",
32
+ description: "`site_health_runs_reset_fields` and `site_health_tracker_reset_fields` name stored Site " +
33
+ "Health settings that were invalid and fell back to defaults (`*` = all of them). " +
34
+ "Scheduled and CI runs, or ticket filing, auto-close and the digest, stop until they are " +
35
+ "saved again: `envs update` with any `--health-*` flag rewrites the run settings, any " +
36
+ "`--tracker-*` flag the tracker settings.",
32
37
  scope: "project",
33
38
  args: [{ name: "env-id", description: "Environment id", required: true }],
34
39
  async run(ctx, input) {
@@ -277,7 +277,9 @@ export const healthCommands = [
277
277
  "and on desktop separately (`speeds`, each with its own Core Web Vitals) and takes " +
278
278
  "the worse of the two; one that couldn't be measured is left out. A category's `unchecked` lists checks this run " +
279
279
  "couldn't determine (e.g. robots.txt timed out): they neither pass nor fail. Pass --report for a single-page check started with " +
280
- "`health run --path`.",
280
+ "`health run --path`. `site_health_runs_reset_fields` and `site_health_tracker_reset_fields` " +
281
+ "name stored settings that were invalid and fell back to defaults (`*` = all of them): " +
282
+ "scheduled and CI runs, or ticket filing, stop until they are saved again with `envs update`.",
281
283
  scope: "project",
282
284
  flags: [
283
285
  ENV_FLAG,
@@ -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 you author tests for and run.",
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 for a URL, or an empty one with just --name",
74
74
  scope: "workspace",
75
75
  args: [
76
76
  {
@@ -95,24 +95,26 @@ export const projectCommands = [
95
95
  },
96
96
  { name: "force-new-login", type: "boolean", description: "Ignore any reusable saved login" },
97
97
  {
98
- name: "no-explore",
98
+ name: "explore",
99
99
  type: "boolean",
100
- description: "Create the project without starting the cloud exploration. Author tests yourself " +
101
- "via `beryl tests create` or your coding agent over MCP",
100
+ description: "Also start a cloud exploration that authors tests for you. Off by default: author " +
101
+ "tests with `beryl tests create` or your coding agent over MCP",
102
+ mcpDescription: "Leave unset: you author the tests yourself with `tests create`. Set only when the " +
103
+ "user explicitly asks for a cloud exploration",
102
104
  },
103
- { name: "watch", type: "boolean", description: "Stream the agent's exploration live" },
105
+ { name: "watch", type: "boolean", description: "Start the exploration (implies --explore) and stream it live" },
104
106
  { name: "timeout", type: "number", description: "With --watch: max minutes to wait" },
105
107
  ],
106
108
  examples: [
107
- "beryl projects create https://app.example.com --watch",
109
+ "beryl projects create https://app.example.com",
108
110
  "beryl projects create https://app.example.com --auth gated",
109
- "beryl projects create https://app.example.com --auth public --no-explore",
110
111
  'beryl projects create --name "Acme production"',
111
112
  ],
112
113
  async run(ctx, input) {
113
- const noExplore = flagBool(input, "no-explore");
114
- if (noExplore && flagBool(input, "watch"))
114
+ const watch = flagBool(input, "watch");
115
+ if (input.flags.explore === false && watch)
115
116
  throw new UsageError("--no-explore cannot be combined with --watch");
117
+ const explore = flagBool(input, "explore") || watch;
116
118
  const ws = await ctx.requireWorkspace(input);
117
119
  const rawUrl = input.args.url;
118
120
  const url = typeof rawUrl === "string" ? rawUrl : "";
@@ -121,7 +123,9 @@ export const projectCommands = [
121
123
  // later with `beryl envs update <env-id>` or author over the CLI/MCP.
122
124
  if (!url) {
123
125
  if (!name)
124
- throw new UsageError("Provide a URL to explore, or --name to create an empty project");
126
+ throw new UsageError("Provide a URL, or --name to create an empty project");
127
+ if (explore)
128
+ throw new UsageError("--explore and --watch need a URL to explore");
125
129
  const created = (await ctx.client.post(`/workspaces/${ws}/projects`, {
126
130
  name,
127
131
  }));
@@ -137,13 +141,13 @@ export const projectCommands = [
137
141
  root_url: url,
138
142
  requires_auth_choice: auth,
139
143
  force_new_login: flagBool(input, "force-new-login"),
140
- skip_exploration: noExplore,
144
+ skip_exploration: !explore,
141
145
  }));
142
- if (noExplore)
146
+ if (!explore)
143
147
  return {
144
148
  data: created,
145
- human: `${green("Project created")}: ${created.project_id} ${dim("(no exploration started)")}\n` +
146
- `Ready for your own tests — author a plan and push it with \`beryl tests create\`.`,
149
+ human: `${green("Project created")}: ${created.project_id}\n` +
150
+ `Ready for your own tests: author a plan and push it with \`beryl tests create\`.`,
147
151
  };
148
152
  // The agent cannot start until a login is captured, so there is nothing to watch.
149
153
  if (created.auth_status === "needs_capture")
@@ -89,7 +89,7 @@ function verifyFailureResult(err) {
89
89
  // The concise human table for `tests list` — the full TestResponse is a 24-column
90
90
  // firehose of internal ids that wraps unreadably in a normal terminal. `--wide`
91
91
  // (and `--json`) still expose every field.
92
- const LIST_COLUMNS = ["title", "status", "access", "id", "last_result_status", "last_run_at"];
92
+ const LIST_COLUMNS = ["title", "surface", "status", "access", "id", "last_result_status", "last_run_at"];
93
93
  const ACCESS_VALUES = ["read", "write"];
94
94
  function accessFlag(input, fallback) {
95
95
  const raw = flagStr(input, "access") ?? fallback;
@@ -101,6 +101,7 @@ function accessFlag(input, fallback) {
101
101
  function conciseTestRow(row) {
102
102
  return {
103
103
  title: row.nl_title,
104
+ surface: row.surface,
104
105
  status: row.quarantined ? "quarantined" : row.is_active ? "active" : "inactive",
105
106
  access: row.access,
106
107
  id: row.id,
@@ -188,8 +189,8 @@ export const testCommands = [
188
189
  {
189
190
  name: "status",
190
191
  type: "string",
191
- description: "With --page, show only this bucket: passed, failed, errored, cancelled, " +
192
- "quarantined, or unstable (failed no longer includes errored)",
192
+ description: "With --page, show only this bucket: passed, failing (failed or errored), " +
193
+ "failed, errored, cancelled, quarantined, or unstable",
193
194
  },
194
195
  {
195
196
  name: "ungrouped",
@@ -291,7 +292,8 @@ export const testCommands = [
291
292
  "reconcile with that test (`tests get` / `tests set-plan`), don't rename-and-retry; a " +
292
293
  "409 `plan_hash_mismatch` means the submitted plan is not the bytes that were replayed " +
293
294
  "(re-run `tests create`); a 429 with Retry-After 30 means the verify slots are " +
294
- "saturated (wait and retry).",
295
+ "saturated (wait and retry). A server-side verify that could not run (verdict `flag`) " +
296
+ "still banks the test but returns verified:false and exits 1; prove it with `tests recompile`.",
295
297
  scope: "project",
296
298
  flags: [
297
299
  { name: "title", type: "string", required: true, description: "Title for the new test" },
@@ -374,7 +376,18 @@ export const testCommands = [
374
376
  const verifyInCloud = async (reason) => {
375
377
  ctx.err(yellow(`! ${reason}, so this plan is verified server-side instead.`));
376
378
  try {
377
- return { data: await bank({ verify: true, plan_hash: compiled.plan_hash }) };
379
+ const created = await bank({ verify: true, plan_hash: compiled.plan_hash });
380
+ if (created.verified === false) {
381
+ // The replay could not run (verdict "flag"): saved, but nothing was proven.
382
+ return {
383
+ data: created,
384
+ human: yellow(`Banked UNVERIFIED: ${created.id}. The server-side replay could not run` +
385
+ (created.verify_message ? ` (${String(created.verify_message)})` : "") +
386
+ "; the test is saved but unproven. Prove it with `tests recompile`."),
387
+ exitCode: 1,
388
+ };
389
+ }
390
+ return { data: created };
378
391
  }
379
392
  catch (err) {
380
393
  const failure = verifyFailureResult(err);
@@ -524,8 +537,9 @@ export const testCommands = [
524
537
  ctx.err(yellow(`! The proof ran against ${urlOverride}, not the environment's root URL — ` +
525
538
  "the banked test is unproven against its real target until a run there passes."));
526
539
  }
540
+ // The server saw verify:false; the proof is the local replay, unless it ran off-target.
527
541
  return {
528
- data: created,
542
+ data: { ...created, verified: !urlOverride, verdict: "keep", verified_by: "local" },
529
543
  human: green(`✓ Replay passed locally — banked as ${created.id}`),
530
544
  };
531
545
  },
@@ -583,7 +597,8 @@ export const testCommands = [
583
597
  description: "Groups are project-defined labels used to filter, run, or schedule a slice of the " +
584
598
  "suite (`beryl runs trigger --group <name>`). This REPLACES the whole set: pass every " +
585
599
  "group you want, or none to clear it. Names must already exist in the project " +
586
- "(`beryl groups list`); an unknown name is an error, never a new group.",
600
+ "(`beryl groups list`); an unknown name is an error, never a new group. Every " +
601
+ "surface of the test carries the same set.",
587
602
  scope: "project",
588
603
  args: [{ name: "test-id", description: "Test id", required: true }],
589
604
  flags: [
@@ -611,7 +626,8 @@ export const testCommands = [
611
626
  summary: "Mark a test read or write",
612
627
  description: "read: the test only reads the app. write: it creates, edits, renames, deletes or " +
613
628
  "toggles something that persists. The label is yours to set; Beryl does not infer it " +
614
- "from the plan. A write test must clean up in `after`, ending with a proof expect.",
629
+ "from the plan. A write test must clean up in `after`, ending with a proof expect. " +
630
+ "The label covers every surface of the test, so each surface's plan must clean up.",
615
631
  scope: "project",
616
632
  args: [
617
633
  { name: "test-id", description: "Test id", required: true },
@@ -195,7 +195,10 @@ export const trackerCommands = [
195
195
  "revoked; ticket changes queue until you reconnect), the number of queued changes, " +
196
196
  "open tickets (filed in the tracker), `unfiled_tickets` the tracker hasn't accepted " +
197
197
  "yet with its `last_sync_error`, digest findings, and `last_sync`: what the latest run filed, " +
198
- "updated, closed or skipped. Change settings with `envs update --tracker-*`.",
198
+ "updated, closed or skipped. `reset_fields` names stored settings that were invalid and " +
199
+ "fell back to defaults (`*` = all of them); filing, auto-close and the digest stop " +
200
+ "(`last_sync.skipped: settings_invalid`) until they are saved again. Change settings " +
201
+ "with `envs update --tracker-*`.",
199
202
  scope: "project",
200
203
  flags: [ENV_FLAG],
201
204
  async run(ctx, input) {
package/dist/context.js CHANGED
@@ -133,8 +133,7 @@ export function createContext(options) {
133
133
  projectId = projects[0].id;
134
134
  }
135
135
  else if (projects.length === 0) {
136
- throw new CliError("This workspace has no project yet — create one with " +
137
- "`beryl projects create <url>` (add --no-explore to author tests yourself).");
136
+ throw new CliError("This workspace has no project yet. Create one with `beryl projects create <url>`.");
138
137
  }
139
138
  else {
140
139
  projectId = await pickOne(ctx, projects, "project", ambiguousMessage(ctx, "project", projects));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@beryl-so/cli",
3
- "version": "0.63.2",
4
- "description": "Beryl on the command line: projects, runs, the exploring agent, and an MCP server over the same commands.",
3
+ "version": "0.67.0",
4
+ "description": "Beryl on the command line: projects, tests, runs, and an MCP server over the same commands.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "homepage": "https://beryl.so/docs/cli",