@beryl-so/cli 0.67.0 → 0.77.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -14
- package/dist/adapters/mcp.js +6 -4
- package/dist/beryl-test-skill.js +129 -107
- package/dist/commands/accounts.js +92 -10
- package/dist/commands/config-vars.js +87 -16
- package/dist/commands/environments.js +18 -1
- package/dist/commands/init.js +42 -108
- package/dist/commands/jobs.js +129 -0
- package/dist/commands/projects.js +68 -45
- package/dist/commands/runs.js +5 -432
- package/dist/commands/tests.js +34 -409
- package/dist/commands/third-party-integrations.js +26 -3
- package/dist/commands/watch.js +36 -0
- package/dist/lint.js +120 -6
- package/dist/registry/index.js +2 -0
- package/dist/schema.generated.js +152 -27
- package/dist/skill-tables.js +3 -0
- package/package.json +2 -3
- package/dist/email-pump.js +0 -138
- package/dist/local-exec.js +0 -239
- package/dist/local-run.js +0 -340
- package/dist/playwright-install.js +0 -220
- package/dist/progress.js +0 -44
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
// The two commands that turn "nothing Playwright-related installed" into "local runs work":
|
|
7
|
-
// the test runner as a dev dep, then its browser binary. Kept as data so the CLI can both
|
|
8
|
-
// run them and print them verbatim for the non-interactive / copy-paste path.
|
|
9
|
-
export const INSTALL_TEST_RUNNER = ["npm", "i", "-D", "@playwright/test"];
|
|
10
|
-
export const INSTALL_CHROMIUM = ["npx", "playwright", "install", "chromium"];
|
|
11
|
-
export const PLAYWRIGHT_INSTALL_COMMANDS = `${INSTALL_TEST_RUNNER.join(" ")} && ${INSTALL_CHROMIUM.join(" ")}`;
|
|
12
|
-
// Resolve from the project tree, not from wherever the globally-installed CLI happens to
|
|
13
|
-
// live — `createRequire` rooted at cwd walks up the same node_modules chain Playwright will.
|
|
14
|
-
const projectRequire = (cwd) => createRequire(path.join(cwd, "package.json"));
|
|
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) {
|
|
25
|
-
try {
|
|
26
|
-
return runnerAt(projectRequire(dir).resolve("@playwright/test/package.json"), "project");
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
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;
|
|
38
|
-
}
|
|
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;
|
|
66
|
-
// A local run launches chromium headless, which Playwright serves from a separate
|
|
67
|
-
// `chromium_headless_shell` build (`playwright install chromium` fetches both). That is the
|
|
68
|
-
// only engine a run ever launches, so it is the only one we install or check for.
|
|
69
|
-
const HEADLESS_SHELL = "chromium-headless-shell";
|
|
70
|
-
// Written last by Playwright's downloader, so its presence means a complete browser.
|
|
71
|
-
const INSTALL_MARKER = "INSTALLATION_COMPLETE";
|
|
72
|
-
function playwrightCoreDir(cwd) {
|
|
73
|
-
const req = projectRequire(cwd);
|
|
74
|
-
try {
|
|
75
|
-
return path.dirname(req.resolve("playwright-core/package.json"));
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
// Not hoisted (pnpm) — look from @playwright/test's own tree instead.
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
const fromRunner = createRequire(req.resolve("@playwright/test"));
|
|
82
|
-
return path.dirname(fromRunner.resolve("playwright-core/package.json"));
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
return undefined;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// Mirrors playwright-core's registry root: PLAYWRIGHT_BROWSERS_PATH ("0" means inside the
|
|
89
|
-
// package), else the per-platform cache dir. undefined on a platform Playwright doesn't
|
|
90
|
-
// support, where we have no verdict to offer.
|
|
91
|
-
function browsersRoot(coreDir) {
|
|
92
|
-
const override = process.env.PLAYWRIGHT_BROWSERS_PATH;
|
|
93
|
-
if (override === "0")
|
|
94
|
-
return coreDir ? path.join(coreDir, ".local-browsers") : undefined;
|
|
95
|
-
if (override)
|
|
96
|
-
return path.resolve(override);
|
|
97
|
-
const home = os.homedir();
|
|
98
|
-
if (process.platform === "darwin")
|
|
99
|
-
return path.join(home, "Library", "Caches", "ms-playwright");
|
|
100
|
-
if (process.platform === "win32") {
|
|
101
|
-
const local = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
|
|
102
|
-
return path.join(local, "ms-playwright");
|
|
103
|
-
}
|
|
104
|
-
if (process.platform === "linux") {
|
|
105
|
-
return path.join(process.env.XDG_CACHE_HOME || path.join(home, ".cache"), "ms-playwright");
|
|
106
|
-
}
|
|
107
|
-
return undefined;
|
|
108
|
-
}
|
|
109
|
-
// The exact directory this project's Playwright will look in: <root>/<browser name with
|
|
110
|
-
// dashes as underscores>-<revision>, straight out of playwright-core's browsers.json — so a
|
|
111
|
-
// browser downloaded for an older Playwright doesn't read as the one this one needs.
|
|
112
|
-
function wantedChromiumDir(coreDir) {
|
|
113
|
-
if (!coreDir)
|
|
114
|
-
return undefined;
|
|
115
|
-
try {
|
|
116
|
-
const parsed = JSON.parse(fs.readFileSync(path.join(coreDir, "browsers.json"), "utf8"));
|
|
117
|
-
const entry = parsed.browsers?.find((b) => b.name === HEADLESS_SHELL) ??
|
|
118
|
-
parsed.browsers?.find((b) => b.name === "chromium");
|
|
119
|
-
if (!entry?.name || !entry.revision)
|
|
120
|
-
return undefined;
|
|
121
|
-
return `${entry.name.replace(/-/g, "_")}-${entry.revision}`;
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
return undefined;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const isComplete = (dir) => fs.existsSync(path.join(dir, INSTALL_MARKER));
|
|
128
|
-
/**
|
|
129
|
-
* Is the browser binary a local run actually launches present? Without it every test in a
|
|
130
|
-
* run dies with the same `browserType.launch: Executable doesn't exist` — an environment
|
|
131
|
-
* problem that reads as a broken suite.
|
|
132
|
-
*/
|
|
133
|
-
export function hasChromiumBrowser(cwd) {
|
|
134
|
-
const coreDir = hasPlaywrightTest(cwd) ? playwrightCoreDir(cwd) : bundledCoreDir();
|
|
135
|
-
const root = browsersRoot(coreDir);
|
|
136
|
-
// Nowhere known to look — say nothing rather than block; the run surfaces Playwright's
|
|
137
|
-
// own error if it really is missing.
|
|
138
|
-
if (!root)
|
|
139
|
-
return true;
|
|
140
|
-
const wanted = wantedChromiumDir(coreDir);
|
|
141
|
-
if (wanted)
|
|
142
|
-
return isComplete(path.join(root, wanted));
|
|
143
|
-
// Revision unknown (no resolvable playwright-core): any completed chromium build is the
|
|
144
|
-
// best evidence there is, and guessing wrong only costs an idempotent re-install.
|
|
145
|
-
try {
|
|
146
|
-
return fs
|
|
147
|
-
.readdirSync(root)
|
|
148
|
-
.some((d) => /^chromium(_headless_shell)?-\d+$/.test(d) && isComplete(path.join(root, d)));
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
export function playwrightGaps(cwd) {
|
|
155
|
-
const runner = !hasPlaywrightTest(cwd) && !bundledRunnerCli();
|
|
156
|
-
// With no runner there is no revision to judge a cached browser against, so the honest
|
|
157
|
-
// answer is the full install — not the npm half and a second failure right after it.
|
|
158
|
-
return { runner, browser: runner || !hasChromiumBrowser(cwd) };
|
|
159
|
-
}
|
|
160
|
-
export const anyGap = (gaps) => gaps.runner || gaps.browser;
|
|
161
|
-
export function describeGaps(gaps) {
|
|
162
|
-
if (gaps.runner && gaps.browser)
|
|
163
|
-
return "@playwright/test + the Chromium browser";
|
|
164
|
-
return gaps.browser ? "the Chromium browser" : "@playwright/test";
|
|
165
|
-
}
|
|
166
|
-
/** Only the commands the missing halves need — a present @playwright/test isn't reinstalled. */
|
|
167
|
-
export function installCommandsFor(gaps, cwd = process.cwd()) {
|
|
168
|
-
const commands = [
|
|
169
|
-
...(gaps.runner ? [INSTALL_TEST_RUNNER.join(" ")] : []),
|
|
170
|
-
...(gaps.browser ? [browserInstallCommand(cwd)] : []),
|
|
171
|
-
];
|
|
172
|
-
return commands.length > 0 ? commands.join(" && ") : PLAYWRIGHT_INSTALL_COMMANDS;
|
|
173
|
-
}
|
|
174
|
-
export const installPrompt = (commands) => `Install local Playwright now (${commands})? [Y/n] `;
|
|
175
|
-
export const INSTALL_PROMPT = installPrompt(PLAYWRIGHT_INSTALL_COMMANDS);
|
|
176
|
-
// Ask (default-yes) whether to install. Returns false — not throwing — when there is no answer
|
|
177
|
-
// or the prompt fails, so callers uniformly fall back to printing the install hint.
|
|
178
|
-
export async function confirmInstall(prompt, commands = PLAYWRIGHT_INSTALL_COMMANDS) {
|
|
179
|
-
try {
|
|
180
|
-
return !/^n(o)?$/i.test(await prompt(installPrompt(commands)));
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
return false;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
const withCmdExt = (cmd) => process.platform === "win32" && (cmd === "npm" || cmd === "npx") ? `${cmd}.cmd` : cmd;
|
|
187
|
-
function runInherit(command, args, cwd) {
|
|
188
|
-
return new Promise((resolve, reject) => {
|
|
189
|
-
// stdio inherited: an install is slow and the user wants to watch npm/browser-download
|
|
190
|
-
// progress live, exactly as if they'd typed it themselves.
|
|
191
|
-
const child = spawn(withCmdExt(command), [...args], { cwd, stdio: "inherit" });
|
|
192
|
-
child.on("error", reject);
|
|
193
|
-
child.on("close", (code) => code === 0
|
|
194
|
-
? resolve()
|
|
195
|
-
: reject(new Error(`\`${command} ${args.join(" ")}\` exited with code ${code}`)));
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Install the local Playwright test runner + chromium into `cwd`, streaming each step's output.
|
|
200
|
-
* Throws if either step exits non-zero (so the caller surfaces the failure, not a silent partial
|
|
201
|
-
* install). Skips the `@playwright/test` step when it is already resolvable, but always ensures
|
|
202
|
-
* the browser binary — an installed runner with no browser still fails a real run.
|
|
203
|
-
*/
|
|
204
|
-
export async function installPlaywright(cwd, onStep) {
|
|
205
|
-
const bundled = bundledRunnerCli();
|
|
206
|
-
if (!hasPlaywrightTest(cwd) && !bundled) {
|
|
207
|
-
onStep?.(`Installing @playwright/test — ${INSTALL_TEST_RUNNER.join(" ")}`);
|
|
208
|
-
await runInherit(INSTALL_TEST_RUNNER[0], INSTALL_TEST_RUNNER.slice(1), cwd);
|
|
209
|
-
}
|
|
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
|
-
}
|
|
215
|
-
await runInherit(INSTALL_CHROMIUM[0], INSTALL_CHROMIUM.slice(1), cwd);
|
|
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/progress.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { dim } from "./output.js";
|
|
2
|
-
const BAR_WIDTH = 18;
|
|
3
|
-
export function renderProgressLine(state) {
|
|
4
|
-
const stepFraction = state.stepTotal && state.stepTotal > 0
|
|
5
|
-
? Math.min(1, (state.step ?? 0) / state.stepTotal)
|
|
6
|
-
: 0;
|
|
7
|
-
const fraction = state.total > 0 ? Math.min(1, (state.done + stepFraction) / state.total) : 0;
|
|
8
|
-
const filled = Math.round(fraction * BAR_WIDTH);
|
|
9
|
-
const bar = "█".repeat(filled) + "░".repeat(BAR_WIDTH - filled);
|
|
10
|
-
const pct = `${Math.round(fraction * 100)}%`.padStart(4);
|
|
11
|
-
const steps = state.stepTotal && state.stepTotal > 0
|
|
12
|
-
? ` · step ${Math.min(state.step ?? 0, state.stepTotal)}/${state.stepTotal}`
|
|
13
|
-
: "";
|
|
14
|
-
const tally = state.failed > 0 ? ` · ✓${state.passed} ✗${state.failed}` : ` · ✓${state.passed}`;
|
|
15
|
-
return `[${bar}]${pct} · test ${Math.min(state.done + 1, state.total)}/${state.total}${tally} · ${state.title}${steps}`;
|
|
16
|
-
}
|
|
17
|
-
/** A single rewriting status line on a TTY; quiet elsewhere (the caller prints
|
|
18
|
-
* per-test boundary lines instead, so MCP/CI logs stay readable). */
|
|
19
|
-
export class ProgressBar {
|
|
20
|
-
stream;
|
|
21
|
-
lastLen = 0;
|
|
22
|
-
tty;
|
|
23
|
-
constructor(stream = process.stderr) {
|
|
24
|
-
this.stream = stream;
|
|
25
|
-
this.tty = Boolean(stream.isTTY);
|
|
26
|
-
}
|
|
27
|
-
get active() {
|
|
28
|
-
return this.tty;
|
|
29
|
-
}
|
|
30
|
-
update(state) {
|
|
31
|
-
if (!this.tty)
|
|
32
|
-
return;
|
|
33
|
-
const line = renderProgressLine(state);
|
|
34
|
-
const padded = line.padEnd(this.lastLen);
|
|
35
|
-
this.lastLen = line.length;
|
|
36
|
-
this.stream.write(`\r${dim(padded)}`);
|
|
37
|
-
}
|
|
38
|
-
clear() {
|
|
39
|
-
if (!this.tty || this.lastLen === 0)
|
|
40
|
-
return;
|
|
41
|
-
this.stream.write(`\r${" ".repeat(this.lastLen)}\r`);
|
|
42
|
-
this.lastLen = 0;
|
|
43
|
-
}
|
|
44
|
-
}
|