@beryl-so/cli 0.2.0 → 0.6.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 +227 -149
- package/dist/adapters/cli.js +39 -11
- package/dist/adapters/mcp.js +6 -0
- package/dist/beryl-test-skill.js +136 -0
- package/dist/commands/account.js +3 -0
- package/dist/commands/auth.js +62 -7
- package/dist/commands/config-vars.js +1 -0
- package/dist/commands/credentials.js +35 -1
- package/dist/commands/environments.js +2 -0
- package/dist/commands/explorations.js +15 -0
- package/dist/commands/inboxes.js +117 -0
- package/dist/commands/init.js +93 -14
- package/dist/commands/projects.js +1 -0
- package/dist/commands/runs.js +104 -2
- package/dist/commands/slack.js +82 -0
- package/dist/commands/tests.js +23 -3
- package/dist/commands/workspaces.js +3 -0
- package/dist/local-run.js +168 -0
- package/dist/registry/index.js +7 -0
- package/dist/schema.generated.js +152 -1
- package/package.json +1 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
// The customer already has @playwright/test (the MCP `init` wires it), so we invoke it
|
|
5
|
+
// as an external rather than bundling it — the CLI's zero-runtime-dep rule. In a node_modules
|
|
6
|
+
// install `playwright` is on PATH; otherwise fall back to `npx playwright`, exactly as the
|
|
7
|
+
// cloud runner does.
|
|
8
|
+
export const PLAYWRIGHT_INSTALL_HINT = "Local Playwright not found. Install it in this project, then re-run:\n" +
|
|
9
|
+
" npm i -D @playwright/test && npx playwright install chromium";
|
|
10
|
+
// Isolate the run from any playwright.config.ts in the customer's repo: a stray `testMatch`
|
|
11
|
+
// would exclude our spec (a zero-test run that reads as a false pass), and a `use.baseURL` /
|
|
12
|
+
// `use.storageState` / `globalSetup` there would silently retarget or reauth the run we mean
|
|
13
|
+
// to be a clean local execution of exactly this one spec. `testDir` pins us to our own dir.
|
|
14
|
+
// Reporter is left to the CLI flag + PLAYWRIGHT_JSON_OUTPUT_NAME (as the cloud runner does), so
|
|
15
|
+
// the report lands at exactly our path regardless of what a reporter here would default to.
|
|
16
|
+
const ISOLATING_CONFIG = (testDir, artifactsDir) => `import { defineConfig } from "@playwright/test";\n` +
|
|
17
|
+
`export default defineConfig({\n` +
|
|
18
|
+
` testDir: ${JSON.stringify(testDir)},\n` +
|
|
19
|
+
` testMatch: "beryl-local.spec.ts",\n` +
|
|
20
|
+
` outputDir: ${JSON.stringify(artifactsDir)},\n` +
|
|
21
|
+
` fullyParallel: false,\n` +
|
|
22
|
+
`});\n`;
|
|
23
|
+
const PASSING = new Set(["passed", "expected"]);
|
|
24
|
+
const SKIPPED = new Set(["skipped"]);
|
|
25
|
+
export class PlaywrightMissingError extends Error {
|
|
26
|
+
}
|
|
27
|
+
function runProcess(command, args, cwd, env) {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const child = spawn(command, args, { cwd, env });
|
|
30
|
+
let stdout = "";
|
|
31
|
+
let stderr = "";
|
|
32
|
+
child.stdout.on("data", (d) => (stdout += d.toString()));
|
|
33
|
+
child.stderr.on("data", (d) => (stderr += d.toString()));
|
|
34
|
+
child.on("error", (err) => resolve({ code: null, stdout, stderr, spawnError: err }));
|
|
35
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// `npx playwright` is the portable fallback when no local binary is on PATH — it resolves
|
|
39
|
+
// the project's @playwright/test without us hard-coding a node_modules path.
|
|
40
|
+
function playwrightBase(cwd) {
|
|
41
|
+
const binName = process.platform === "win32" ? "playwright.cmd" : "playwright";
|
|
42
|
+
const local = path.join(cwd, "node_modules", ".bin", binName);
|
|
43
|
+
if (fs.existsSync(local))
|
|
44
|
+
return { command: local, args: ["test"] };
|
|
45
|
+
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
46
|
+
return { command: npx, args: ["playwright", "test"] };
|
|
47
|
+
}
|
|
48
|
+
// "unknown command 'test'" is the Python `playwright` shim (no `test` subcommand); the module
|
|
49
|
+
// errors mean @playwright/test isn't installed. Either way the actionable answer is the same
|
|
50
|
+
// install hint, not a stack trace.
|
|
51
|
+
function looksLikePlaywrightMissing(r) {
|
|
52
|
+
if (r.spawnError?.code === "ENOENT")
|
|
53
|
+
return true;
|
|
54
|
+
const blob = `${r.stdout}\n${r.stderr}`;
|
|
55
|
+
return (/unknown command ['"]?test/i.test(blob) ||
|
|
56
|
+
/Cannot find module ['"]@playwright\/test/i.test(blob) ||
|
|
57
|
+
/npm ERR!.*could not determine executable|npx.*not found/i.test(blob));
|
|
58
|
+
}
|
|
59
|
+
export function parsePlaywrightReport(data) {
|
|
60
|
+
const results = [];
|
|
61
|
+
const walk = (suites) => {
|
|
62
|
+
for (const suite of suites) {
|
|
63
|
+
for (const spec of suite.specs ?? []) {
|
|
64
|
+
for (const test of spec.tests ?? []) {
|
|
65
|
+
// Last attempt is the verdict — retries can precede it.
|
|
66
|
+
const last = test.results?.[test.results.length - 1];
|
|
67
|
+
const errors = (last?.errors ?? []).map((e) => e.message).filter(Boolean);
|
|
68
|
+
const artifacts = (last?.attachments ?? [])
|
|
69
|
+
.map((a) => a.path)
|
|
70
|
+
.filter((p) => Boolean(p));
|
|
71
|
+
results.push({
|
|
72
|
+
name: spec.title ?? "(unnamed)",
|
|
73
|
+
status: last?.status ?? "unknown",
|
|
74
|
+
duration_ms: last?.duration,
|
|
75
|
+
...(errors.length ? { error: errors.join("\n") } : {}),
|
|
76
|
+
artifacts,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (suite.suites)
|
|
81
|
+
walk(suite.suites);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
walk(data.suites ?? []);
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
// passed/failed exclude skips; an empty set is neither — the caller treats it as a failure
|
|
88
|
+
// because a run that executed nothing is not evidence of a pass.
|
|
89
|
+
export function tally(results) {
|
|
90
|
+
const passed = results.filter((r) => PASSING.has(r.status)).length;
|
|
91
|
+
const skipped = results.filter((r) => SKIPPED.has(r.status)).length;
|
|
92
|
+
return { passed, failed: results.length - passed - skipped };
|
|
93
|
+
}
|
|
94
|
+
function parseReport(reportPath) {
|
|
95
|
+
return parsePlaywrightReport(JSON.parse(fs.readFileSync(reportPath, "utf8")));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Write `spec` next to the caller's project, run it with their local @playwright/test, and
|
|
99
|
+
* parse the JSON report into a structured pass/fail summary. Artifacts + the spec + report land
|
|
100
|
+
* in `dir` when given (kept), otherwise in a temp dir that is cleaned up. Throws
|
|
101
|
+
* {@link PlaywrightMissingError} when no runnable @playwright/test is found so the caller can
|
|
102
|
+
* print an install hint.
|
|
103
|
+
*/
|
|
104
|
+
export async function runSpecLocally(opts) {
|
|
105
|
+
const cwd = process.cwd();
|
|
106
|
+
// The spec + isolating config MUST sit inside the project tree: Playwright resolves
|
|
107
|
+
// `@playwright/test` (imported by both) by walking UP from the config file, so a config in
|
|
108
|
+
// /tmp finds no node_modules and every run dies with "Cannot find module '@playwright/test'".
|
|
109
|
+
// A run dir under cwd walks up into the project's node_modules; it's always cleaned up.
|
|
110
|
+
const runDir = fs.mkdtempSync(path.join(cwd, ".beryl-local-"));
|
|
111
|
+
// Where the user-facing outputs (artifacts, spec copy, report) go: --dir if asked, else the
|
|
112
|
+
// ephemeral run dir.
|
|
113
|
+
const outDir = opts.dir ? path.resolve(opts.dir) : runDir;
|
|
114
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
115
|
+
const specPath = path.join(runDir, "beryl-local.spec.ts");
|
|
116
|
+
fs.writeFileSync(specPath, opts.spec);
|
|
117
|
+
const configPath = path.join(runDir, "beryl-local.config.ts");
|
|
118
|
+
const artifactsDir = path.join(outDir, "artifacts");
|
|
119
|
+
fs.writeFileSync(configPath, ISOLATING_CONFIG(runDir, artifactsDir));
|
|
120
|
+
const reportPath = path.join(runDir, "report.json");
|
|
121
|
+
const cleanup = () => fs.rmSync(runDir, { recursive: true, force: true });
|
|
122
|
+
const { command, args: base } = playwrightBase(cwd);
|
|
123
|
+
const args = [...base, `--config=${configPath}`, "--reporter=json"];
|
|
124
|
+
opts.onProgress?.(`Running ${command} ${base.join(" ")} on ${opts.testName}…`);
|
|
125
|
+
const env = { ...process.env, PLAYWRIGHT_JSON_OUTPUT_NAME: reportPath };
|
|
126
|
+
const result = await runProcess(command, args, cwd, env);
|
|
127
|
+
if (looksLikePlaywrightMissing(result)) {
|
|
128
|
+
cleanup();
|
|
129
|
+
throw new PlaywrightMissingError(PLAYWRIGHT_INSTALL_HINT);
|
|
130
|
+
}
|
|
131
|
+
let results;
|
|
132
|
+
try {
|
|
133
|
+
results = parseReport(reportPath);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
// No parsable report on a non-zero exit means the spec never ran (compile/launch error) —
|
|
137
|
+
// surface stderr/stdout so it isn't a silent failure.
|
|
138
|
+
const detail = (result.stderr || result.stdout || err.message).trim();
|
|
139
|
+
cleanup();
|
|
140
|
+
throw new Error(`Playwright produced no readable report — the spec did not run.\n${detail}`);
|
|
141
|
+
}
|
|
142
|
+
// A run that executed zero tests is a failure, not a pass — an isolating config matching
|
|
143
|
+
// exactly our spec should never yield an empty set, so an empty one means the spec was
|
|
144
|
+
// filtered/skipped away and there is no evidence it ran.
|
|
145
|
+
if (results.length === 0) {
|
|
146
|
+
const detail = (result.stderr || result.stdout || "no tests were run").trim();
|
|
147
|
+
cleanup();
|
|
148
|
+
throw new Error(`Playwright ran no tests from the rendered spec.\n${detail}`);
|
|
149
|
+
}
|
|
150
|
+
let keptSpec = specPath;
|
|
151
|
+
let keptReport = reportPath;
|
|
152
|
+
if (opts.dir) {
|
|
153
|
+
// Persist the exact spec + report next to the artifacts before the run dir is removed.
|
|
154
|
+
keptSpec = path.join(outDir, "beryl-local.spec.ts");
|
|
155
|
+
keptReport = path.join(outDir, "report.json");
|
|
156
|
+
fs.copyFileSync(specPath, keptSpec);
|
|
157
|
+
fs.copyFileSync(reportPath, keptReport);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Without --dir the artifacts lived in the run dir we're about to delete, so their paths
|
|
161
|
+
// would dangle — don't hand back paths to files that no longer exist.
|
|
162
|
+
for (const r of results)
|
|
163
|
+
r.artifacts = [];
|
|
164
|
+
}
|
|
165
|
+
cleanup();
|
|
166
|
+
const { passed, failed } = tally(results);
|
|
167
|
+
return { passed, failed, results, directory: outDir, spec: keptSpec, report: keptReport };
|
|
168
|
+
}
|
package/dist/registry/index.js
CHANGED
|
@@ -4,10 +4,12 @@ import { configCommands } from "../commands/config-vars.js";
|
|
|
4
4
|
import { credentialCommands } from "../commands/credentials.js";
|
|
5
5
|
import { environmentCommands } from "../commands/environments.js";
|
|
6
6
|
import { explorationCommands } from "../commands/explorations.js";
|
|
7
|
+
import { inboxCommands } from "../commands/inboxes.js";
|
|
7
8
|
import { initCommands } from "../commands/init.js";
|
|
8
9
|
import { mcpCommands } from "../commands/mcp.js";
|
|
9
10
|
import { projectCommands } from "../commands/projects.js";
|
|
10
11
|
import { runCommands } from "../commands/runs.js";
|
|
12
|
+
import { slackCommands } from "../commands/slack.js";
|
|
11
13
|
import { testCommands } from "../commands/tests.js";
|
|
12
14
|
import { workspaceCommands } from "../commands/workspaces.js";
|
|
13
15
|
export const WORKSPACE_FLAG = {
|
|
@@ -43,7 +45,9 @@ export const commands = [
|
|
|
43
45
|
...runCommands,
|
|
44
46
|
...explorationCommands,
|
|
45
47
|
...configCommands,
|
|
48
|
+
...slackCommands,
|
|
46
49
|
...credentialCommands,
|
|
50
|
+
...inboxCommands,
|
|
47
51
|
...accountCommands,
|
|
48
52
|
...mcpCommands,
|
|
49
53
|
].map(withScopeFlags);
|
|
@@ -73,6 +77,9 @@ export function findCommand(words) {
|
|
|
73
77
|
export function groupDefault(group) {
|
|
74
78
|
return commands.find((spec) => spec.groupDefault && spec.name.split(" ")[0] === group);
|
|
75
79
|
}
|
|
80
|
+
export function groupSummary(specs) {
|
|
81
|
+
return specs.find((s) => s.groupSummary)?.groupSummary;
|
|
82
|
+
}
|
|
76
83
|
export function commandGroups() {
|
|
77
84
|
const groups = new Map();
|
|
78
85
|
for (const spec of commands) {
|
package/dist/schema.generated.js
CHANGED
|
@@ -15,11 +15,24 @@ export const ACTION_PLAN_SCHEMA = {
|
|
|
15
15
|
"wait_for",
|
|
16
16
|
"expect",
|
|
17
17
|
"capture_count",
|
|
18
|
-
"await_email"
|
|
18
|
+
"await_email",
|
|
19
|
+
"upload",
|
|
20
|
+
"dialog",
|
|
21
|
+
"switch_tab",
|
|
22
|
+
"close_tab",
|
|
23
|
+
"drag"
|
|
19
24
|
],
|
|
20
25
|
"title": "ActionType",
|
|
21
26
|
"type": "string"
|
|
22
27
|
},
|
|
28
|
+
"DialogChoice": {
|
|
29
|
+
"enum": [
|
|
30
|
+
"accept",
|
|
31
|
+
"dismiss"
|
|
32
|
+
],
|
|
33
|
+
"title": "DialogChoice",
|
|
34
|
+
"type": "string"
|
|
35
|
+
},
|
|
23
36
|
"EmailExtract": {
|
|
24
37
|
"enum": [
|
|
25
38
|
"code",
|
|
@@ -356,6 +369,109 @@ export const ACTION_PLAN_SCHEMA = {
|
|
|
356
369
|
]
|
|
357
370
|
}
|
|
358
371
|
},
|
|
372
|
+
{
|
|
373
|
+
"if": {
|
|
374
|
+
"properties": {
|
|
375
|
+
"action": {
|
|
376
|
+
"const": "upload"
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
"required": [
|
|
380
|
+
"action"
|
|
381
|
+
]
|
|
382
|
+
},
|
|
383
|
+
"then": {
|
|
384
|
+
"properties": {
|
|
385
|
+
"selector": {
|
|
386
|
+
"minLength": 1,
|
|
387
|
+
"type": "string"
|
|
388
|
+
},
|
|
389
|
+
"value": {
|
|
390
|
+
"minLength": 1,
|
|
391
|
+
"type": "string"
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
"required": [
|
|
395
|
+
"selector",
|
|
396
|
+
"value"
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
"if": {
|
|
402
|
+
"properties": {
|
|
403
|
+
"action": {
|
|
404
|
+
"const": "dialog"
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
"required": [
|
|
408
|
+
"action"
|
|
409
|
+
]
|
|
410
|
+
},
|
|
411
|
+
"then": {
|
|
412
|
+
"properties": {
|
|
413
|
+
"dialog": {
|
|
414
|
+
"not": {
|
|
415
|
+
"type": "null"
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
"required": [
|
|
420
|
+
"dialog"
|
|
421
|
+
]
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
"if": {
|
|
426
|
+
"properties": {
|
|
427
|
+
"action": {
|
|
428
|
+
"const": "switch_tab"
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
"required": [
|
|
432
|
+
"action"
|
|
433
|
+
]
|
|
434
|
+
},
|
|
435
|
+
"then": {
|
|
436
|
+
"properties": {
|
|
437
|
+
"value": {
|
|
438
|
+
"minLength": 1,
|
|
439
|
+
"type": "string"
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
"required": [
|
|
443
|
+
"value"
|
|
444
|
+
]
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
"if": {
|
|
449
|
+
"properties": {
|
|
450
|
+
"action": {
|
|
451
|
+
"const": "drag"
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
"required": [
|
|
455
|
+
"action"
|
|
456
|
+
]
|
|
457
|
+
},
|
|
458
|
+
"then": {
|
|
459
|
+
"properties": {
|
|
460
|
+
"selector": {
|
|
461
|
+
"minLength": 1,
|
|
462
|
+
"type": "string"
|
|
463
|
+
},
|
|
464
|
+
"value": {
|
|
465
|
+
"minLength": 1,
|
|
466
|
+
"type": "string"
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
"required": [
|
|
470
|
+
"selector",
|
|
471
|
+
"value"
|
|
472
|
+
]
|
|
473
|
+
}
|
|
474
|
+
},
|
|
359
475
|
{
|
|
360
476
|
"if": {
|
|
361
477
|
"properties": {
|
|
@@ -774,6 +890,29 @@ export const ACTION_PLAN_SCHEMA = {
|
|
|
774
890
|
"default": null,
|
|
775
891
|
"title": "Capture Ref"
|
|
776
892
|
},
|
|
893
|
+
"dialog": {
|
|
894
|
+
"anyOf": [
|
|
895
|
+
{
|
|
896
|
+
"$ref": "#/$defs/DialogChoice"
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
"type": "null"
|
|
900
|
+
}
|
|
901
|
+
],
|
|
902
|
+
"default": null
|
|
903
|
+
},
|
|
904
|
+
"dialog_expect_text": {
|
|
905
|
+
"anyOf": [
|
|
906
|
+
{
|
|
907
|
+
"type": "string"
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
"type": "null"
|
|
911
|
+
}
|
|
912
|
+
],
|
|
913
|
+
"default": null,
|
|
914
|
+
"title": "Dialog Expect Text"
|
|
915
|
+
},
|
|
777
916
|
"expect_count": {
|
|
778
917
|
"anyOf": [
|
|
779
918
|
{
|
|
@@ -860,6 +999,18 @@ export const ACTION_PLAN_SCHEMA = {
|
|
|
860
999
|
"default": null,
|
|
861
1000
|
"title": "From Contains"
|
|
862
1001
|
},
|
|
1002
|
+
"iframe": {
|
|
1003
|
+
"anyOf": [
|
|
1004
|
+
{
|
|
1005
|
+
"type": "string"
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
"type": "null"
|
|
1009
|
+
}
|
|
1010
|
+
],
|
|
1011
|
+
"default": null,
|
|
1012
|
+
"title": "Iframe"
|
|
1013
|
+
},
|
|
863
1014
|
"key": {
|
|
864
1015
|
"anyOf": [
|
|
865
1016
|
{
|
package/package.json
CHANGED