@beryl-so/cli 0.34.0 → 0.34.3
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 +1 -1
- package/dist/beryl-test-skill.js +2 -2
- package/dist/commands/auth.js +1 -1
- package/dist/commands/init.js +27 -12
- package/dist/commands/runs.js +10 -7
- package/dist/commands/tests.js +10 -7
- package/dist/local-run.js +30 -18
- package/dist/playwright-install.js +61 -10
- package/dist/telemetry.js +18 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -252,7 +252,7 @@ Trigger a run of a project's tests (e.g. in CI), then watch, inspect, and downlo
|
|
|
252
252
|
| Command | Summary | MCP tool |
|
|
253
253
|
| --- | --- | --- |
|
|
254
254
|
| `beryl runs trigger` | Trigger a test run (whole suite, a subset, or one environment) | `runs_trigger` |
|
|
255
|
-
| `beryl runs local [test-ids...]` | Run tests on your machine with
|
|
255
|
+
| `beryl runs local [test-ids...]` | Run tests on your machine with Playwright; results sync to Beryl | `runs_local` |
|
|
256
256
|
| `beryl runs list` | List recent runs | `runs_list` |
|
|
257
257
|
| `beryl runs get <run-id>` | Show one run with its per-test results | `runs_get` |
|
|
258
258
|
| `beryl runs watch <run-id>` | Attach to a run and stream progress until it finishes | `runs_watch` |
|
package/dist/beryl-test-skill.js
CHANGED
|
@@ -73,7 +73,7 @@ exception are in \`tests create\`'s own description.
|
|
|
73
73
|
Before the first plan:
|
|
74
74
|
|
|
75
75
|
\`\`\`
|
|
76
|
-
|
|
76
|
+
npx @beryl-so/cli@latest init # once per machine: installs the Chromium browser (the runner ships with the CLI)
|
|
77
77
|
beryl envs list # the root URL gotos resolve against (§2)
|
|
78
78
|
beryl envs update <id> --url https://app.example.com # set it if root_url is empty — required
|
|
79
79
|
beryl accounts list # who do authenticated tests sign in as? (§1)
|
|
@@ -370,7 +370,7 @@ Weak (describes steps + asserts nothing meaningful):
|
|
|
370
370
|
## 4. The local run-fix loop
|
|
371
371
|
|
|
372
372
|
Iterate on your machine before you rely on the cloud. \`beryl runs local\` runs banked
|
|
373
|
-
tests with
|
|
373
|
+
tests with the Playwright runner bundled in the CLI — no cloud, no waiting for a scheduled run.
|
|
374
374
|
|
|
375
375
|
\`\`\`
|
|
376
376
|
beryl runs local <test-id> --no-sync --url-override http://localhost:3000 --dir ./beryl-local
|
package/dist/commands/auth.js
CHANGED
|
@@ -175,7 +175,7 @@ export const authCommands = [
|
|
|
175
175
|
const human = `${green("Logged in")} as ${me.name} <${me.email}>` +
|
|
176
176
|
`\n${dim(`Token saved to ${saved}`)}`;
|
|
177
177
|
return {
|
|
178
|
-
data: { email: me.email, name: me.name, config: saved },
|
|
178
|
+
data: { id: me.id, email: me.email, name: me.name, config: saved },
|
|
179
179
|
human,
|
|
180
180
|
};
|
|
181
181
|
},
|
package/dist/commands/init.js
CHANGED
|
@@ -8,6 +8,7 @@ import { AuthError, CliError } from "../errors.js";
|
|
|
8
8
|
import { ApiClient } from "../http.js";
|
|
9
9
|
import { bold, cyan, dim, green, red, yellow } from "../output.js";
|
|
10
10
|
import { anyGap, describeGaps, installCommandsFor, installPlaywright, playwrightGaps, } from "../playwright-install.js";
|
|
11
|
+
import { aliasSessionToAccount, captureCliEvent, flushTelemetry, shutdownTelemetry, } from "../telemetry.js";
|
|
11
12
|
import { cliVersion, warnIfStale } from "../version-check.js";
|
|
12
13
|
import { authCommands } from "./auth.js";
|
|
13
14
|
import { flagStr } from "./util.js";
|
|
@@ -117,16 +118,16 @@ function writeSkills(cwd, scope) {
|
|
|
117
118
|
writeSkillFile(skillLeaf(path.join(root, ".claude", "skills"))),
|
|
118
119
|
];
|
|
119
120
|
}
|
|
120
|
-
// Local authoring drives a real browser
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
121
|
+
// Local authoring drives a real browser, and the whole authoring workflow (walk the flow
|
|
122
|
+
// first, then bank the plan) depends on it — so the install is mandatory, not offered:
|
|
123
|
+
// missing means install now, no prompt, no opt-out. The runner ships with the CLI; the
|
|
124
|
+
// browser is the per-machine half, fetched for the runner's own revision. Never throws — a
|
|
125
|
+
// failed install must not fail `init`, which has already done its wiring; it prints the
|
|
126
|
+
// exact commands instead.
|
|
126
127
|
async function ensureLocalPlaywright(ctx, cwd) {
|
|
127
128
|
const gaps = playwrightGaps(cwd);
|
|
128
129
|
if (!anyGap(gaps)) {
|
|
129
|
-
ctx.err(`${green("✓")}
|
|
130
|
+
ctx.err(`${green("✓")} Playwright + Chromium already installed ${dim("(local runs ready)")}`);
|
|
130
131
|
return;
|
|
131
132
|
}
|
|
132
133
|
try {
|
|
@@ -141,7 +142,7 @@ async function ensureLocalPlaywright(ctx, cwd) {
|
|
|
141
142
|
catch (err) {
|
|
142
143
|
ctx.err(`${red("✗")} Playwright install failed: ${err.message}`);
|
|
143
144
|
ctx.err(`${dim("•")} Finish the install by hand — local runs and browser authoring need it:\n` +
|
|
144
|
-
` ${cyan(installCommandsFor(gaps))}`);
|
|
145
|
+
` ${cyan(installCommandsFor(gaps, cwd))}`);
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
export const initCommands = [
|
|
@@ -153,8 +154,9 @@ export const initCommands = [
|
|
|
153
154
|
"agent. Nothing is detected and nothing is conditional: every run wires the beryl AND " +
|
|
154
155
|
"playwright MCP servers, writes the authoring skill (user scope: your home " +
|
|
155
156
|
".agents/skills/ + .claude/skills/, so it follows you into every session; --scope " +
|
|
156
|
-
"project: the repo's own dirs, committed for teammates), and installs
|
|
157
|
-
"
|
|
157
|
+
"project: the repo's own dirs, committed for teammates), and installs the Chromium " +
|
|
158
|
+
"browser if missing (the Playwright runner ships with the CLI, so any repo can run tests " +
|
|
159
|
+
"locally), since browser authoring and local runs depend on it. By default the " +
|
|
158
160
|
"servers are wired per-user (matching where your login token lives) via `claude mcp add " +
|
|
159
161
|
"-s user`; pass --scope project to write a committed .mcp.json for a shared repo " +
|
|
160
162
|
"instead. No workspace/project pin and no URL prompt: ask Claude to write tests for " +
|
|
@@ -187,6 +189,10 @@ export const initCommands = [
|
|
|
187
189
|
async run(ctx, input) {
|
|
188
190
|
const cwd = process.cwd();
|
|
189
191
|
let { client, config } = ctx;
|
|
192
|
+
const scope = (flagStr(input, "scope") ?? "user");
|
|
193
|
+
captureCliEvent("cli_init_started", config.apiUrl, { scope });
|
|
194
|
+
flushTelemetry();
|
|
195
|
+
let accountId;
|
|
190
196
|
const signIn = async () => {
|
|
191
197
|
// Non-interactive skips the picker and lets `login` fail with its own
|
|
192
198
|
// "interactive input required" message, exactly as before.
|
|
@@ -194,7 +200,11 @@ export const initCommands = [
|
|
|
194
200
|
? await ctx.prompt("Sign in: [1] Browser (opens beryl.so) [2] Email me a code — [1]: ")
|
|
195
201
|
: "";
|
|
196
202
|
const login = authCommands.find((c) => c.name === "login");
|
|
197
|
-
await login.run(ctx, {
|
|
203
|
+
const result = await login.run(ctx, {
|
|
204
|
+
args: {},
|
|
205
|
+
flags: answer.trim() === "2" ? { otp: true } : {},
|
|
206
|
+
});
|
|
207
|
+
accountId = (result ?? {}).data?.id;
|
|
198
208
|
config = loadConfig();
|
|
199
209
|
if (!config.token)
|
|
200
210
|
throw new CliError("Login did not persist a token");
|
|
@@ -203,6 +213,7 @@ export const initCommands = [
|
|
|
203
213
|
if (config.token) {
|
|
204
214
|
try {
|
|
205
215
|
const me = (await client.get("/account/"));
|
|
216
|
+
accountId = me.id;
|
|
206
217
|
ctx.err(`${green("✓")} Signed in as ${me.email}`);
|
|
207
218
|
}
|
|
208
219
|
catch (err) {
|
|
@@ -215,7 +226,10 @@ export const initCommands = [
|
|
|
215
226
|
else {
|
|
216
227
|
await signIn();
|
|
217
228
|
}
|
|
218
|
-
|
|
229
|
+
if (accountId) {
|
|
230
|
+
aliasSessionToAccount(accountId);
|
|
231
|
+
flushTelemetry();
|
|
232
|
+
}
|
|
219
233
|
const report = (label, fresh, where) => ctx.err(`${green("✓")} ${label} ${fresh ? "configured" : "already configured"} ${dim(where)}`);
|
|
220
234
|
if (scope === "user") {
|
|
221
235
|
// Claude Code owns ~/.claude.json — shell out to `claude mcp add` rather than write it.
|
|
@@ -267,6 +281,7 @@ export const initCommands = [
|
|
|
267
281
|
// The .mcp.json entry is pinned to @latest, but a global install / old npx cache
|
|
268
282
|
// still wins resolution — so tell the user when the CLI they just ran is stale.
|
|
269
283
|
await warnIfStale(cliVersion(), (msg) => ctx.err(yellow(msg)));
|
|
284
|
+
await shutdownTelemetry();
|
|
270
285
|
return {
|
|
271
286
|
data: {
|
|
272
287
|
scope,
|
package/dist/commands/runs.js
CHANGED
|
@@ -5,7 +5,7 @@ import { CliError, UsageError } from "../errors.js";
|
|
|
5
5
|
import { buildImportForm, establishAccountSession, executeLocalSpec, IMPORT_MAX_ERROR_LEN, toRunEntry, } from "../local-exec.js";
|
|
6
6
|
import { countPlannedFrames, countWrittenFrames, PlaywrightMissingError, } from "../local-run.js";
|
|
7
7
|
import { dim, green, red, yellow } from "../output.js";
|
|
8
|
-
import { anyGap, confirmInstall, describeGaps, installCommandsFor, installPlaywright, playwrightGaps, } from "../playwright-install.js";
|
|
8
|
+
import { anyGap, confirmInstall, describeGaps, installCommandsFor, installHint, installPlaywright, playwrightGaps, } from "../playwright-install.js";
|
|
9
9
|
import { ProgressBar } from "../progress.js";
|
|
10
10
|
import { arg, flagBool, flagNum, flagStr, idsInGroup, projectPath } from "./util.js";
|
|
11
11
|
import { watchRun } from "./watch.js";
|
|
@@ -17,8 +17,8 @@ async function ensureRunnableLocally(ctx) {
|
|
|
17
17
|
const gaps = playwrightGaps(process.cwd());
|
|
18
18
|
if (!anyGap(gaps))
|
|
19
19
|
return;
|
|
20
|
-
const commands = installCommandsFor(gaps);
|
|
21
|
-
const hint =
|
|
20
|
+
const commands = installCommandsFor(gaps, process.cwd());
|
|
21
|
+
const hint = installHint(process.cwd(), gaps);
|
|
22
22
|
if (!ctx.interactive || !(await confirmInstall(ctx.prompt, commands)))
|
|
23
23
|
throw new CliError(hint);
|
|
24
24
|
try {
|
|
@@ -115,12 +115,13 @@ export const runCommands = [
|
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
117
|
name: "runs local",
|
|
118
|
-
summary: "Run tests on your machine with
|
|
118
|
+
summary: "Run tests on your machine with Playwright; results sync to Beryl",
|
|
119
119
|
description: "Unlike `runs trigger`, the browser runs on YOUR machine: each test's rendered spec is " +
|
|
120
|
-
"fetched and run with
|
|
120
|
+
"fetched and run with the Playwright runner bundled in the CLI (your repo's own " +
|
|
121
|
+
"@playwright/test wins when present) and the Chromium binary it drives. " +
|
|
121
122
|
"Both are checked once before any spec is fetched, so a machine that can't run tests " +
|
|
122
123
|
"says so once instead of failing every test (on a terminal the CLI offers to install " +
|
|
123
|
-
"
|
|
124
|
+
"the missing browser; over MCP it prints the exact install command). " +
|
|
124
125
|
"Signup/OTP flows work: the CLI answers the spec's await_email steps over the API " +
|
|
125
126
|
"against the same mailbox the cloud runner uses. Authenticated tests work too: for a plan " +
|
|
126
127
|
"that signs itself in with {{login_email}}/{{login_password}}, the email is baked into " +
|
|
@@ -412,7 +413,9 @@ export const runCommands = [
|
|
|
412
413
|
if (err instanceof PlaywrightMissingError && ctx.interactive && !installOffered) {
|
|
413
414
|
installOffered = true;
|
|
414
415
|
bar.clear();
|
|
415
|
-
|
|
416
|
+
const cwd = process.cwd();
|
|
417
|
+
const commands = installCommandsFor(playwrightGaps(cwd), cwd);
|
|
418
|
+
if (!(await confirmInstall(ctx.prompt, commands)))
|
|
416
419
|
throw err;
|
|
417
420
|
try {
|
|
418
421
|
await installPlaywright(process.cwd(), (line) => ctx.err(dim(line)));
|
package/dist/commands/tests.js
CHANGED
|
@@ -5,7 +5,7 @@ import { lintPlan } from "../lint.js";
|
|
|
5
5
|
import { buildImportForm, establishAccountSession, executeLocalSpec, toRunEntry, } from "../local-exec.js";
|
|
6
6
|
import { PlaywrightMissingError } from "../local-run.js";
|
|
7
7
|
import { dim, green, red, table, yellow } from "../output.js";
|
|
8
|
-
import { confirmInstall, installPlaywright } from "../playwright-install.js";
|
|
8
|
+
import { confirmInstall, installCommandsFor, installPlaywright, playwrightGaps, } from "../playwright-install.js";
|
|
9
9
|
import { ACTION_PLAN_SCHEMA } from "../schema.generated.js";
|
|
10
10
|
import { arg, argList, flagBool, flagNum, flagStr, flagStrings, inGroup, projectPath, readJsonFlag, } from "./util.js";
|
|
11
11
|
const testPath = (ws, p, id) => `${projectPath(ws, p)}/tests/${id}`;
|
|
@@ -207,13 +207,14 @@ export const testCommands = [
|
|
|
207
207
|
summary: "Create a test case from a JSON action plan (for tests authored locally, e.g. by your coding agent)",
|
|
208
208
|
description: "The plan is a JSON object whose steps are {action, selector, url, value, ...}: the first " +
|
|
209
209
|
"EXECUTED step must be a goto, and at least one step must be an expect. Before anything is " +
|
|
210
|
-
"banked, the plan is proven by replaying it in a browser ON YOUR MACHINE with
|
|
211
|
-
"
|
|
210
|
+
"banked, the plan is proven by replaying it in a browser ON YOUR MACHINE with the " +
|
|
211
|
+
"Playwright runner bundled in the CLI (your repo's own @playwright/test wins when " +
|
|
212
|
+
"present): the server renders the spec (`tests/compile`), the CLI runs it (minting " +
|
|
212
213
|
"a run inbox for await_email steps and resolving the saved login exactly as a cloud run " +
|
|
213
214
|
"would), and only a green replay creates the test (bound to the replayed plan by its hash). " +
|
|
214
215
|
"This holds over MCP too: the replay runs on the machine hosting the MCP server, never on " +
|
|
215
|
-
"Beryl's; if
|
|
216
|
-
"terminal the CLI offers to
|
|
216
|
+
"Beryl's; if the Chromium browser is missing there the tool returns the install command " +
|
|
217
|
+
"(`beryl init` installs it; on a terminal the CLI offers to). " +
|
|
217
218
|
"A red replay banks NOTHING: the failure evidence comes back (over MCP the screenshot is " +
|
|
218
219
|
"image content), you fix the plan file and re-run. The proving run is imported as the " +
|
|
219
220
|
"test's first run (--no-sync to skip). A plan that signs in with a session Beryl captured " +
|
|
@@ -399,8 +400,10 @@ export const testCommands = [
|
|
|
399
400
|
// pipe the hint is the answer.
|
|
400
401
|
if (ctx.interactive && !installOffered) {
|
|
401
402
|
installOffered = true;
|
|
402
|
-
|
|
403
|
-
|
|
403
|
+
const cwd = process.cwd();
|
|
404
|
+
const commands = installCommandsFor(playwrightGaps(cwd), cwd);
|
|
405
|
+
if (await confirmInstall(ctx.prompt, commands)) {
|
|
406
|
+
await installPlaywright(cwd, (line) => ctx.err(dim(line)));
|
|
404
407
|
attempt -= 1;
|
|
405
408
|
continue;
|
|
406
409
|
}
|
package/dist/local-run.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { PLAYWRIGHT_INSTALL_COMMANDS } from "./playwright-install.js";
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// exactly as the cloud runner does. The install commands come from playwright-install.ts so the
|
|
8
|
-
// hint text and the actual installer never drift apart.
|
|
4
|
+
import { installHint, PLAYWRIGHT_INSTALL_COMMANDS, resolvePlaywrightRunner, } from "./playwright-install.js";
|
|
5
|
+
// @playwright/test is invoked as an external, never bundled — the CLI's zero-runtime-dep rule.
|
|
6
|
+
// The install commands come from playwright-install.ts so hint and installer never drift apart.
|
|
9
7
|
export const PLAYWRIGHT_INSTALL_HINT = "Local Playwright not found. Install it in this project, then re-run:\n" +
|
|
10
8
|
` ${PLAYWRIGHT_INSTALL_COMMANDS}`;
|
|
11
9
|
// Isolate the run from any playwright.config.ts in the customer's repo: a stray `testMatch`
|
|
@@ -51,25 +49,36 @@ function runProcess(command, args, cwd, env) {
|
|
|
51
49
|
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
52
50
|
});
|
|
53
51
|
}
|
|
54
|
-
//
|
|
55
|
-
// the
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
return { command:
|
|
52
|
+
// Resolved from the run dir — the exact place the spec imports from — so the process we
|
|
53
|
+
// spawn and the module the spec loads are the same copy. `npx playwright` is the last
|
|
54
|
+
// resort for a CLI install that somehow lost its dependencies.
|
|
55
|
+
export function playwrightBase(runDir) {
|
|
56
|
+
const runner = resolvePlaywrightRunner(runDir);
|
|
57
|
+
if (runner?.source === "project") {
|
|
58
|
+
return { command: process.execPath, args: [runner.cli, "test"], label: "playwright test" };
|
|
59
|
+
}
|
|
60
|
+
if (runner) {
|
|
61
|
+
return {
|
|
62
|
+
command: process.execPath,
|
|
63
|
+
args: [runner.cli, "test"],
|
|
64
|
+
label: "playwright test (bundled with the CLI)",
|
|
65
|
+
nodePath: path.resolve(path.dirname(runner.cli), "..", ".."),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
61
68
|
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
62
|
-
return { command: npx, args: ["playwright", "test"] };
|
|
69
|
+
return { command: npx, args: ["playwright", "test"], label: "npx playwright test" };
|
|
63
70
|
}
|
|
64
71
|
// "unknown command 'test'" is the Python `playwright` shim (no `test` subcommand); the module
|
|
65
|
-
// errors mean @playwright/test isn't installed
|
|
66
|
-
//
|
|
72
|
+
// errors mean @playwright/test isn't installed; "Executable doesn't exist" is the runner
|
|
73
|
+
// launching a browser revision that was never downloaded. Either way the actionable answer
|
|
74
|
+
// is the same install hint, not a stack trace.
|
|
67
75
|
function looksLikePlaywrightMissing(r) {
|
|
68
76
|
if (r.spawnError?.code === "ENOENT")
|
|
69
77
|
return true;
|
|
70
78
|
const blob = `${r.stdout}\n${r.stderr}`;
|
|
71
79
|
return (/unknown command ['"]?test/i.test(blob) ||
|
|
72
80
|
/Cannot find module ['"]@playwright\/test/i.test(blob) ||
|
|
81
|
+
/Executable doesn't exist/i.test(blob) ||
|
|
73
82
|
/npm ERR!.*could not determine executable|npx.*not found/i.test(blob));
|
|
74
83
|
}
|
|
75
84
|
export function parsePlaywrightReport(data) {
|
|
@@ -249,12 +258,15 @@ export async function runSpecLocally(opts) {
|
|
|
249
258
|
fs.writeFileSync(configPath, ISOLATING_CONFIG(runDir, artifactsDir));
|
|
250
259
|
const reportPath = path.join(runDir, "report.json");
|
|
251
260
|
const cleanup = () => fs.rmSync(runDir, { recursive: true, force: true });
|
|
252
|
-
const { command, args: base } = playwrightBase(
|
|
261
|
+
const { command, args: base, label, nodePath } = playwrightBase(runDir);
|
|
253
262
|
const args = [...base, `--config=${configPath}`, "--reporter=json"];
|
|
254
|
-
opts.onProgress?.(`Running ${
|
|
263
|
+
opts.onProgress?.(`Running ${label} on ${opts.testName}…`);
|
|
255
264
|
await opts.setup?.(runDir);
|
|
256
265
|
const stop = opts.during?.(runDir);
|
|
257
266
|
const env = { ...process.env, PLAYWRIGHT_JSON_OUTPUT_NAME: reportPath };
|
|
267
|
+
if (nodePath) {
|
|
268
|
+
env.NODE_PATH = [nodePath, process.env.NODE_PATH].filter(Boolean).join(path.delimiter);
|
|
269
|
+
}
|
|
258
270
|
// cwd is the RUN dir, not the project: the spec's relative writes (frames/,
|
|
259
271
|
// email-inbox.json, dom-snapshot.html, phase.json) must land where we harvest and
|
|
260
272
|
// clean up, exactly as the cloud runner keys them to its per-run workdir.
|
|
@@ -267,7 +279,7 @@ export async function runSpecLocally(opts) {
|
|
|
267
279
|
}
|
|
268
280
|
if (looksLikePlaywrightMissing(result)) {
|
|
269
281
|
cleanup();
|
|
270
|
-
throw new PlaywrightMissingError(
|
|
282
|
+
throw new PlaywrightMissingError(installHint(cwd));
|
|
271
283
|
}
|
|
272
284
|
let results;
|
|
273
285
|
try {
|
|
@@ -12,15 +12,57 @@ export const PLAYWRIGHT_INSTALL_COMMANDS = `${INSTALL_TEST_RUNNER.join(" ")} &&
|
|
|
12
12
|
// Resolve from the project tree, not from wherever the globally-installed CLI happens to
|
|
13
13
|
// live — `createRequire` rooted at cwd walks up the same node_modules chain Playwright will.
|
|
14
14
|
const projectRequire = (cwd) => createRequire(path.join(cwd, "package.json"));
|
|
15
|
-
|
|
15
|
+
// @playwright/test is a dependency of the CLI, so a repo needs nothing installed to run
|
|
16
|
+
// locally: the runner comes with `npx @beryl-so/cli`, and only the browser is per-machine.
|
|
17
|
+
// A repo's own @playwright/test still wins when present (its version, its browser revision).
|
|
18
|
+
const cliRequire = createRequire(import.meta.url);
|
|
19
|
+
// cli.js is the package's bin, not an export, so it is located via package.json.
|
|
20
|
+
function runnerAt(pkgPath, source) {
|
|
21
|
+
const { version } = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
22
|
+
return { cli: path.join(path.dirname(pkgPath), "cli.js"), version, source };
|
|
23
|
+
}
|
|
24
|
+
function projectRunner(dir) {
|
|
16
25
|
try {
|
|
17
|
-
projectRequire(
|
|
18
|
-
return true;
|
|
26
|
+
return runnerAt(projectRequire(dir).resolve("@playwright/test/package.json"), "project");
|
|
19
27
|
}
|
|
20
28
|
catch {
|
|
21
|
-
return
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function bundledRunner() {
|
|
33
|
+
try {
|
|
34
|
+
return runnerAt(cliRequire.resolve("@playwright/test/package.json"), "bundled");
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
22
38
|
}
|
|
23
39
|
}
|
|
40
|
+
// The single answer to "which @playwright/test": what `import "@playwright/test"` from `dir`
|
|
41
|
+
// resolves to (any ancestor's node_modules), else the bundled copy. Runner and spec must load
|
|
42
|
+
// the same one or Playwright refuses to run ("did not expect test.use() to be called here").
|
|
43
|
+
export function resolvePlaywrightRunner(dir) {
|
|
44
|
+
return projectRunner(dir) ?? bundledRunner();
|
|
45
|
+
}
|
|
46
|
+
export const bundledRunnerCli = () => bundledRunner()?.cli;
|
|
47
|
+
// By hand, the browser must be fetched for the runner that will launch it: the repo's own
|
|
48
|
+
// (`npx playwright` resolves it) or, in a bare repo, the CLI's pinned version. An unpinned
|
|
49
|
+
// `npx playwright install` there downloads the registry's latest revision, which is not it.
|
|
50
|
+
export function browserInstallCommand(cwd) {
|
|
51
|
+
const bundled = bundledRunner();
|
|
52
|
+
if (hasPlaywrightTest(cwd) || !bundled)
|
|
53
|
+
return INSTALL_CHROMIUM.join(" ");
|
|
54
|
+
const [npx, pkg, ...rest] = INSTALL_CHROMIUM;
|
|
55
|
+
return [npx, `${pkg}@${bundled.version}`, ...rest].join(" ");
|
|
56
|
+
}
|
|
57
|
+
function bundledCoreDir() {
|
|
58
|
+
try {
|
|
59
|
+
return path.dirname(cliRequire.resolve("playwright-core/package.json"));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export const hasPlaywrightTest = (cwd) => projectRunner(cwd) !== undefined;
|
|
24
66
|
// A local run launches chromium headless, which Playwright serves from a separate
|
|
25
67
|
// `chromium_headless_shell` build (`playwright install chromium` fetches both). That is the
|
|
26
68
|
// only engine a run ever launches, so it is the only one we install or check for.
|
|
@@ -89,7 +131,7 @@ const isComplete = (dir) => fs.existsSync(path.join(dir, INSTALL_MARKER));
|
|
|
89
131
|
* problem that reads as a broken suite.
|
|
90
132
|
*/
|
|
91
133
|
export function hasChromiumBrowser(cwd) {
|
|
92
|
-
const coreDir = playwrightCoreDir(cwd);
|
|
134
|
+
const coreDir = hasPlaywrightTest(cwd) ? playwrightCoreDir(cwd) : bundledCoreDir();
|
|
93
135
|
const root = browsersRoot(coreDir);
|
|
94
136
|
// Nowhere known to look — say nothing rather than block; the run surfaces Playwright's
|
|
95
137
|
// own error if it really is missing.
|
|
@@ -110,7 +152,7 @@ export function hasChromiumBrowser(cwd) {
|
|
|
110
152
|
}
|
|
111
153
|
}
|
|
112
154
|
export function playwrightGaps(cwd) {
|
|
113
|
-
const runner = !hasPlaywrightTest(cwd);
|
|
155
|
+
const runner = !hasPlaywrightTest(cwd) && !bundledRunnerCli();
|
|
114
156
|
// With no runner there is no revision to judge a cached browser against, so the honest
|
|
115
157
|
// answer is the full install — not the npm half and a second failure right after it.
|
|
116
158
|
return { runner, browser: runner || !hasChromiumBrowser(cwd) };
|
|
@@ -122,10 +164,10 @@ export function describeGaps(gaps) {
|
|
|
122
164
|
return gaps.browser ? "the Chromium browser" : "@playwright/test";
|
|
123
165
|
}
|
|
124
166
|
/** Only the commands the missing halves need — a present @playwright/test isn't reinstalled. */
|
|
125
|
-
export function installCommandsFor(gaps) {
|
|
167
|
+
export function installCommandsFor(gaps, cwd = process.cwd()) {
|
|
126
168
|
const commands = [
|
|
127
169
|
...(gaps.runner ? [INSTALL_TEST_RUNNER.join(" ")] : []),
|
|
128
|
-
...(gaps.browser ? [
|
|
170
|
+
...(gaps.browser ? [browserInstallCommand(cwd)] : []),
|
|
129
171
|
];
|
|
130
172
|
return commands.length > 0 ? commands.join(" && ") : PLAYWRIGHT_INSTALL_COMMANDS;
|
|
131
173
|
}
|
|
@@ -160,10 +202,19 @@ function runInherit(command, args, cwd) {
|
|
|
160
202
|
* the browser binary — an installed runner with no browser still fails a real run.
|
|
161
203
|
*/
|
|
162
204
|
export async function installPlaywright(cwd, onStep) {
|
|
163
|
-
|
|
205
|
+
const bundled = bundledRunnerCli();
|
|
206
|
+
if (!hasPlaywrightTest(cwd) && !bundled) {
|
|
164
207
|
onStep?.(`Installing @playwright/test — ${INSTALL_TEST_RUNNER.join(" ")}`);
|
|
165
208
|
await runInherit(INSTALL_TEST_RUNNER[0], INSTALL_TEST_RUNNER.slice(1), cwd);
|
|
166
209
|
}
|
|
167
|
-
onStep?.(`Installing the Chromium browser — ${
|
|
210
|
+
onStep?.(`Installing the Chromium browser — ${browserInstallCommand(cwd)}`);
|
|
211
|
+
if (!hasPlaywrightTest(cwd) && bundled) {
|
|
212
|
+
await runInherit(process.execPath, [bundled, ...INSTALL_CHROMIUM.slice(2)], cwd);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
168
215
|
await runInherit(INSTALL_CHROMIUM[0], INSTALL_CHROMIUM.slice(1), cwd);
|
|
169
216
|
}
|
|
217
|
+
export function installHint(cwd, gaps = playwrightGaps(cwd)) {
|
|
218
|
+
const commands = installCommandsFor(gaps, cwd);
|
|
219
|
+
return `Local runs need ${describeGaps(gaps)}. Install it, then re-run:\n ${commands}`;
|
|
220
|
+
}
|
package/dist/telemetry.js
CHANGED
|
@@ -233,6 +233,24 @@ export function createPostHogClient() {
|
|
|
233
233
|
posthog ??= new PostHog(POSTHOG_KEY, { host: POSTHOG_HOST, disableGeoip: false });
|
|
234
234
|
return posthog;
|
|
235
235
|
}
|
|
236
|
+
// `beryl init` starts before there is an account to attribute to: its events land on the
|
|
237
|
+
// process session id, and the session is aliased onto the account once init has signed in,
|
|
238
|
+
// so the install joins the same person as the signup and the later MCP work.
|
|
239
|
+
export function captureCliEvent(event, apiUrl, properties = {}) {
|
|
240
|
+
createPostHogClient().capture({
|
|
241
|
+
distinctId: sessionId,
|
|
242
|
+
event,
|
|
243
|
+
properties: { ...baseEventProperties(apiUrl), ...properties },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
export function aliasSessionToAccount(userId) {
|
|
247
|
+
createPostHogClient().alias({ distinctId: userId, alias: sessionId });
|
|
248
|
+
}
|
|
249
|
+
export function flushTelemetry() {
|
|
250
|
+
// Fire-and-forget: init keeps running (login, installs) while the batch goes out, and a
|
|
251
|
+
// failed send must never surface to the user.
|
|
252
|
+
void posthog?.flush().catch(() => { });
|
|
253
|
+
}
|
|
236
254
|
export function analyticsOptions(apiUrl, identify) {
|
|
237
255
|
const properties = baseEventProperties(apiUrl);
|
|
238
256
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beryl-so/cli",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.3",
|
|
4
4
|
"description": "Beryl on the command line — projects, runs, the exploring agent, and an MCP server over the same commands.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
35
|
+
"@playwright/test": "^1.61.1",
|
|
35
36
|
"@posthog/mcp": "^0.11.6",
|
|
36
37
|
"posthog-node": "^5.49.1"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
|
-
"@playwright/test": "^1.61.1",
|
|
40
40
|
"@types/node": "^26.1.1",
|
|
41
41
|
"tsx": "^4.23.1",
|
|
42
42
|
"typescript": "^7.0.2",
|