@beryl-so/cli 0.40.0 → 0.42.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 +2 -2
- package/dist/adapters/mcp.js +12 -0
- package/dist/beryl-test-skill.js +12 -14
- package/dist/commands/environments.js +11 -2
- package/dist/commands/init.js +10 -0
- package/dist/commands/tests.js +6 -35
- package/dist/local-exec.js +0 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -194,13 +194,13 @@ Manage a project's environments: the URLs and auth Beryl runs tests against.
|
|
|
194
194
|
|
|
195
195
|
### schedule
|
|
196
196
|
|
|
197
|
-
Manage the schedules on which Beryl runs a project's tests automatically. A project can hold many independent schedules; each has its own cadence (daily or weekly), local run time, timezone, and target groups (none = every active test).
|
|
197
|
+
Manage the schedules on which Beryl runs a project's tests automatically. A project can hold many independent schedules; each has its own environment, cadence (daily or weekly), local run time, timezone, and target groups (none = every active test).
|
|
198
198
|
|
|
199
199
|
| Command | Summary | MCP tool |
|
|
200
200
|
| --- | --- | --- |
|
|
201
201
|
| `beryl schedule list` | List the project's schedules | `schedule_list` |
|
|
202
202
|
| `beryl schedule add` | Add a schedule (daily, or weekly on a given day) | `schedule_add` |
|
|
203
|
-
| `beryl schedule update <schedule-id>` | Change a schedule's cadence, time, timezone, or
|
|
203
|
+
| `beryl schedule update <schedule-id>` | Change a schedule's cadence, time, timezone, groups, or environment | `schedule_update` |
|
|
204
204
|
| `beryl schedule remove <schedule-id>` | Remove a schedule (its tests and groups stay) | `schedule_remove` |
|
|
205
205
|
|
|
206
206
|
### tests
|
package/dist/adapters/mcp.js
CHANGED
|
@@ -10,6 +10,7 @@ import { ApiClient, ApiError } from "../http.js";
|
|
|
10
10
|
import { parseArgv } from "./cli.js";
|
|
11
11
|
import { commands } from "../registry/index.js";
|
|
12
12
|
import { analyticsOptions, createPostHogClient, setClient, setSurface, shutdownTelemetry, withTool, } from "../telemetry.js";
|
|
13
|
+
import { refreshInstalledSkills } from "../commands/init.js";
|
|
13
14
|
import { cliVersion, warnIfStale } from "../version-check.js";
|
|
14
15
|
export function toolName(spec) {
|
|
15
16
|
return spec.name.replace(/ /g, "_").replace(/-/g, "_");
|
|
@@ -292,6 +293,17 @@ export async function serveMcp(baseCtx) {
|
|
|
292
293
|
// Fire-and-forget staleness warning: a stale MCP server silently exposes fewer
|
|
293
294
|
// tools, and stderr is the one channel a stdio MCP server can safely log to.
|
|
294
295
|
void warnIfStale(cliVersion(), (msg) => console.error(msg));
|
|
296
|
+
// Installed skill copies only refresh on `init`; this server is respawned from @latest
|
|
297
|
+
// every session, so bring them up to this version here. Never fatal.
|
|
298
|
+
try {
|
|
299
|
+
for (const { outcome, file } of refreshInstalledSkills(process.cwd())) {
|
|
300
|
+
if (outcome !== "unchanged")
|
|
301
|
+
console.error(`beryl-test authoring skill ${outcome}: ${file}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
/* best-effort */
|
|
306
|
+
}
|
|
295
307
|
const server = new Server({ name: "beryl", version: cliVersion() }, {
|
|
296
308
|
capabilities: { tools: {} },
|
|
297
309
|
instructions: mcpInstructions(),
|
package/dist/beryl-test-skill.js
CHANGED
|
@@ -93,10 +93,10 @@ Then, per test:
|
|
|
93
93
|
2. **Write the ActionPlan**, with one real outcome assertion (§2).
|
|
94
94
|
3. \`beryl tests lint --file plan.json\` — schema check, offline, no network.
|
|
95
95
|
4. \`beryl tests create --title … --file … --description "<the intent>"\` — replays the
|
|
96
|
-
plan locally, banks it only on green; a red replay hands back the
|
|
97
|
-
re-run (§3 for the intent, §4 for the loop).
|
|
98
|
-
5.
|
|
99
|
-
|
|
96
|
+
plan locally, banks it only on green, records no run; a red replay hands back the
|
|
97
|
+
failure — fix and re-run (§3 for the intent, §4 for the loop).
|
|
98
|
+
5. When the batch is banked, run the new ids together ONCE (\`beryl runs local <id…>\`,
|
|
99
|
+
or \`beryl runs trigger\` for the cloud) so they land as one run with real first results.
|
|
100
100
|
|
|
101
101
|
## 1. First: who do your tests sign in as?
|
|
102
102
|
|
|
@@ -285,8 +285,8 @@ ${JSON.stringify(BERYL_TEST_SKILL_EXAMPLE_PLAN, null, 2)
|
|
|
285
285
|
--description "<the intent — see §3>"
|
|
286
286
|
\`\`\`
|
|
287
287
|
\`create\` banks the plan only on a green local replay; a red one hands back the
|
|
288
|
-
failure — fix and re-run. Replay mechanics (where it runs over
|
|
289
|
-
\`--no-verify\`, the cloud-session exception) are in \`tests create\`'s own
|
|
288
|
+
failure — fix and re-run. It records no run. Replay mechanics (where it runs over
|
|
289
|
+
MCP, \`--no-verify\`, the cloud-session exception) are in \`tests create\`'s own
|
|
290
290
|
description. Inspect the exact spec with \`beryl tests script --file plan.json\`.
|
|
291
291
|
Full ActionPlan JSON Schema:
|
|
292
292
|
https://api.beryl.so/api/v1/schemas/action-plan.schema.json.
|
|
@@ -378,14 +378,12 @@ beryl runs local <test-id> --no-sync --url-override http://localhost:3000 --dir
|
|
|
378
378
|
beryl runs local # the whole suite, results recorded in Beryl
|
|
379
379
|
\`\`\`
|
|
380
380
|
|
|
381
|
-
The loop: draft → \`tests lint\` → \`tests create\`
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
Once the test passes locally against a real outcome, it's ready to bank and let Beryl
|
|
388
|
-
run and heal it.
|
|
381
|
+
The loop: draft → \`tests lint\` → \`tests create\` (proves, records nothing) →
|
|
382
|
+
\`runs local --no-sync\` while iterating → read \`--dir\`'s \`report.json\` to see which
|
|
383
|
+
step or assertion failed and why → fix the plan → \`tests set-plan\` → run again. When
|
|
384
|
+
the batch is done, one synced \`runs local <id…>\` (or \`runs trigger\`) records it as a
|
|
385
|
+
single run. Flag semantics, session-mode behavior, \`await_email\` and
|
|
386
|
+
authenticated-test handling, and exit codes are in \`runs local\`'s own description.
|
|
389
387
|
|
|
390
388
|
## 5. Testing an OTP / signup flow (\`await_email\`)
|
|
391
389
|
|
|
@@ -107,7 +107,7 @@ export const environmentCommands = [
|
|
|
107
107
|
name: "schedule list",
|
|
108
108
|
summary: "List the project's schedules",
|
|
109
109
|
scope: "project",
|
|
110
|
-
groupSummary: "Manage the schedules on which Beryl runs a project's tests automatically. A project can hold many independent schedules; each has its own cadence (daily or weekly), local run time, timezone, and target groups (none = every active test).",
|
|
110
|
+
groupSummary: "Manage the schedules on which Beryl runs a project's tests automatically. A project can hold many independent schedules; each has its own environment, cadence (daily or weekly), local run time, timezone, and target groups (none = every active test).",
|
|
111
111
|
async run(ctx, input) {
|
|
112
112
|
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
113
113
|
return { data: await ctx.client.get(`${projectPath(workspaceId, projectId)}/schedules`) };
|
|
@@ -120,15 +120,22 @@ export const environmentCommands = [
|
|
|
120
120
|
flags: [
|
|
121
121
|
...SCHEDULE_CADENCE_FLAGS,
|
|
122
122
|
{ name: "groups", type: "string", description: SCHEDULE_GROUPS_FLAG },
|
|
123
|
+
{
|
|
124
|
+
name: "env",
|
|
125
|
+
type: "string",
|
|
126
|
+
description: "Environment id to run against (default: the project's default environment)",
|
|
127
|
+
},
|
|
123
128
|
],
|
|
124
129
|
examples: [
|
|
125
130
|
"beryl schedule add --frequency daily --hour 6 --tz UTC",
|
|
131
|
+
"beryl schedule add --env <env-id> --frequency daily --hour 6 --tz UTC",
|
|
126
132
|
'beryl schedule add --groups "Smoke,Checkout" --frequency weekly --day 0 --hour 9 --minute 15 --tz UTC',
|
|
127
133
|
],
|
|
128
134
|
async run(ctx, input) {
|
|
129
135
|
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
130
136
|
return {
|
|
131
137
|
data: await ctx.client.post(`${projectPath(workspaceId, projectId)}/schedules`, {
|
|
138
|
+
environment_id: flagStr(input, "env") ?? null,
|
|
132
139
|
frequency: flagStr(input, "frequency") ?? "daily",
|
|
133
140
|
day_of_week: flagNum(input, "day") ?? null,
|
|
134
141
|
run_hour: flagNum(input, "hour"),
|
|
@@ -141,12 +148,13 @@ export const environmentCommands = [
|
|
|
141
148
|
},
|
|
142
149
|
{
|
|
143
150
|
name: "schedule update",
|
|
144
|
-
summary: "Change a schedule's cadence, time, timezone, or
|
|
151
|
+
summary: "Change a schedule's cadence, time, timezone, groups, or environment",
|
|
145
152
|
scope: "project",
|
|
146
153
|
args: [{ name: "schedule-id", description: "Schedule id (see schedule list)", required: true }],
|
|
147
154
|
flags: [
|
|
148
155
|
...SCHEDULE_CADENCE_FLAGS,
|
|
149
156
|
{ name: "groups", type: "string", description: SCHEDULE_GROUPS_FLAG },
|
|
157
|
+
{ name: "env", type: "string", description: "Move the schedule to this environment id" },
|
|
150
158
|
{
|
|
151
159
|
name: "all-tests",
|
|
152
160
|
type: "boolean",
|
|
@@ -169,6 +177,7 @@ export const environmentCommands = [
|
|
|
169
177
|
: existing.group_ids;
|
|
170
178
|
return {
|
|
171
179
|
data: await ctx.client.put(`${projectPath(workspaceId, projectId)}/schedules/${scheduleId}`, {
|
|
180
|
+
...(flagStr(input, "env") ? { environment_id: flagStr(input, "env") } : {}),
|
|
172
181
|
frequency: flagStr(input, "frequency") ?? existing.frequency,
|
|
173
182
|
day_of_week: flagNum(input, "day") ?? existing.day_of_week,
|
|
174
183
|
run_hour: flagNum(input, "hour") ?? existing.run_hour,
|
package/dist/commands/init.js
CHANGED
|
@@ -118,6 +118,16 @@ function writeSkills(cwd, scope) {
|
|
|
118
118
|
writeSkillFile(skillLeaf(path.join(root, ".claude", "skills"))),
|
|
119
119
|
];
|
|
120
120
|
}
|
|
121
|
+
// The MCP server is respawned from `npx @beryl-so/cli@latest` at every session start, so
|
|
122
|
+
// its startup is the one moment new CLI code reliably runs on a user's machine. Refresh
|
|
123
|
+
// whatever copies `init` installed; never create one (scope is `init`'s call).
|
|
124
|
+
export function refreshInstalledSkills(cwd) {
|
|
125
|
+
const files = new Set([os.homedir(), cwd].flatMap((root) => [
|
|
126
|
+
skillLeaf(path.join(root, ".agents", "skills")),
|
|
127
|
+
skillLeaf(path.join(root, ".claude", "skills")),
|
|
128
|
+
]));
|
|
129
|
+
return [...files].filter((file) => fs.existsSync(file)).map(writeSkillFile);
|
|
130
|
+
}
|
|
121
131
|
// Local authoring drives a real browser, and the whole authoring workflow (walk the flow
|
|
122
132
|
// first, then bank the plan) depends on it — so the install is mandatory, not offered:
|
|
123
133
|
// missing means install now, no prompt, no opt-out. The runner ships with the CLI; the
|
package/dist/commands/tests.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import { CliError, UsageError } from "../errors.js";
|
|
3
3
|
import { ApiError } from "../http.js";
|
|
4
4
|
import { lintPlan } from "../lint.js";
|
|
5
|
-
import {
|
|
5
|
+
import { establishAccountSession, executeLocalSpec, } from "../local-exec.js";
|
|
6
6
|
import { PlaywrightMissingError } from "../local-run.js";
|
|
7
7
|
import { dim, green, red, table, yellow } from "../output.js";
|
|
8
8
|
import { confirmInstall, installCommandsFor, installPlaywright, playwrightGaps, } from "../playwright-install.js";
|
|
@@ -216,8 +216,9 @@ export const testCommands = [
|
|
|
216
216
|
"Beryl's; if the Chromium browser is missing there the tool returns the install command " +
|
|
217
217
|
"(`beryl init` installs it; on a terminal the CLI offers to). " +
|
|
218
218
|
"A red replay banks NOTHING: the failure evidence comes back (over MCP the screenshot is " +
|
|
219
|
-
"image content), you fix the plan file and re-run.
|
|
220
|
-
"
|
|
219
|
+
"image content), you fix the plan file and re-run. No run is recorded: once a batch of " +
|
|
220
|
+
"tests is banked, run their ids together once (`runs local`, or `runs trigger` for the " +
|
|
221
|
+
"cloud) so they land as ONE run with real first results. A plan that signs in with a session Beryl captured " +
|
|
221
222
|
"server-side cannot replay locally (that session never leaves Beryl's cloud). It falls " +
|
|
222
223
|
"back to server-side verification automatically, and says so. A session-mode plan replays locally " +
|
|
223
224
|
"fine: the server renders it with its account's stored sign-in steps in front, so " +
|
|
@@ -258,13 +259,6 @@ export const testCommands = [
|
|
|
258
259
|
"name is an error (create groups with `beryl groups create`). Omit for no group.",
|
|
259
260
|
},
|
|
260
261
|
{ name: "env", type: "string", description: "Environment id to compile and prove against" },
|
|
261
|
-
{
|
|
262
|
-
name: "sync",
|
|
263
|
-
type: "boolean",
|
|
264
|
-
default: true,
|
|
265
|
-
description: "Import the green proving replay as the test's first run (--no-sync: bank only, " +
|
|
266
|
-
"no run recorded)",
|
|
267
|
-
},
|
|
268
262
|
{
|
|
269
263
|
name: "dir",
|
|
270
264
|
type: "string",
|
|
@@ -285,7 +279,6 @@ export const testCommands = [
|
|
|
285
279
|
const description = flagStr(input, "description");
|
|
286
280
|
const urlOverride = flagStr(input, "url-override");
|
|
287
281
|
const env = flagStr(input, "env");
|
|
288
|
-
const sync = input.flags.sync !== false;
|
|
289
282
|
const groups = flagStrings(input, "group");
|
|
290
283
|
const bank = (extra) => ctx.client.post(`${projectPath(workspaceId, projectId)}/tests`, {
|
|
291
284
|
title,
|
|
@@ -306,9 +299,6 @@ export const testCommands = [
|
|
|
306
299
|
title,
|
|
307
300
|
base_url: urlOverride,
|
|
308
301
|
environment_id: env,
|
|
309
|
-
// The proof replay becomes the test's first run, so capture the filmstrip
|
|
310
|
-
// frames its replay view needs.
|
|
311
|
-
frames: sync ? true : undefined,
|
|
312
302
|
});
|
|
313
303
|
let compiled = await compile();
|
|
314
304
|
// Session-mode plan: establish the account's session once (server-side, reused
|
|
@@ -370,7 +360,6 @@ export const testCommands = [
|
|
|
370
360
|
inlinedLoginSteps: compiled.inlined_login_steps ?? 0,
|
|
371
361
|
email: compiled.email ?? undefined,
|
|
372
362
|
});
|
|
373
|
-
const startedAt = new Date().toISOString();
|
|
374
363
|
const attempts = 1 + Math.max(0, compiled.policy.error_retries);
|
|
375
364
|
let outcome;
|
|
376
365
|
let runError;
|
|
@@ -457,31 +446,13 @@ export const testCommands = [
|
|
|
457
446
|
return conflict;
|
|
458
447
|
throw err;
|
|
459
448
|
}
|
|
460
|
-
let runId;
|
|
461
|
-
if (sync) {
|
|
462
|
-
const entry = toRunEntry({ ...specOf(compiled.content), id: String(created.id) }, outcome, undefined, startedAt, 0, [...(loginPassword ? [loginPassword] : []), ...(session?.secrets ?? [])]);
|
|
463
|
-
const imported = (await ctx.client.request("POST", `${projectPath(workspaceId, projectId)}/runs/import`, {
|
|
464
|
-
form: buildImportForm([entry], {
|
|
465
|
-
environmentId: env,
|
|
466
|
-
targetUrlOverride: urlOverride,
|
|
467
|
-
// The proof replay is authoring machinery, not a run the customer
|
|
468
|
-
// asked for — it must never email/Slack "your report is ready".
|
|
469
|
-
notifications: false,
|
|
470
|
-
startedAt,
|
|
471
|
-
completedAt: new Date().toISOString(),
|
|
472
|
-
onNote: (line) => ctx.err(yellow(`! ${line}`)),
|
|
473
|
-
}),
|
|
474
|
-
}));
|
|
475
|
-
runId = imported.id;
|
|
476
|
-
}
|
|
477
449
|
if (urlOverride) {
|
|
478
450
|
ctx.err(yellow(`! The proof ran against ${urlOverride}, not the environment's root URL — ` +
|
|
479
451
|
"the banked test is unproven against its real target until a run there passes."));
|
|
480
452
|
}
|
|
481
453
|
return {
|
|
482
|
-
data:
|
|
483
|
-
human: green(`✓ Replay passed locally — banked as ${created.id}`)
|
|
484
|
-
(runId ? `\nProving run imported: \`beryl runs get ${runId}\`` : ""),
|
|
454
|
+
data: created,
|
|
455
|
+
human: green(`✓ Replay passed locally — banked as ${created.id}`),
|
|
485
456
|
};
|
|
486
457
|
},
|
|
487
458
|
},
|
package/dist/local-exec.js
CHANGED
|
@@ -117,7 +117,6 @@ export function buildImportForm(entries, opts) {
|
|
|
117
117
|
const manifest = {
|
|
118
118
|
environment_id: opts.environmentId ?? null,
|
|
119
119
|
target_url_override: opts.targetUrlOverride ?? null,
|
|
120
|
-
...(opts.notifications !== undefined ? { notifications: opts.notifications } : {}),
|
|
121
120
|
started_at: opts.startedAt,
|
|
122
121
|
completed_at: opts.completedAt,
|
|
123
122
|
results: entries.map(({ files: _files, ...rest }) => rest),
|
package/package.json
CHANGED