@beryl-so/cli 0.73.2 → 0.78.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 +6 -10
- package/dist/adapters/mcp.js +1 -1
- package/dist/beryl-test-skill.js +90 -52
- package/dist/commands/accounts.js +31 -2
- package/dist/commands/config-vars.js +86 -15
- package/dist/commands/environments.js +18 -1
- package/dist/commands/init.js +32 -98
- package/dist/commands/jobs.js +4 -0
- package/dist/commands/projects.js +19 -45
- package/dist/commands/runs.js +6 -433
- package/dist/commands/tests.js +5 -107
- package/dist/commands/third-party-integrations.js +26 -3
- package/dist/commands/watch.js +21 -10
- package/dist/lint.js +120 -0
- package/dist/schema.generated.js +183 -28
- package/dist/skill-tables.js +4 -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
package/dist/commands/init.js
CHANGED
|
@@ -6,31 +6,22 @@ import { BERYL_COPILOT_MARK_BEGIN, BERYL_COPILOT_MARK_END, BERYL_COPILOT_SECTION
|
|
|
6
6
|
import { loadConfig } from "../config.js";
|
|
7
7
|
import { AuthError, CliError, UsageError } from "../errors.js";
|
|
8
8
|
import { ApiClient } from "../http.js";
|
|
9
|
-
import { bold, cyan, dim, green,
|
|
10
|
-
import { anyGap, describeGaps, installCommandsFor, installPlaywright, playwrightGaps, } from "../playwright-install.js";
|
|
9
|
+
import { bold, cyan, dim, green, yellow } from "../output.js";
|
|
11
10
|
import { aliasMachineToAccount, captureCliEvent, flushTelemetry, shutdownTelemetry, } from "../telemetry.js";
|
|
12
11
|
import { cliVersion, warnIfStale } from "../version-check.js";
|
|
13
12
|
import { authCommands } from "./auth.js";
|
|
14
13
|
import { flagStr } from "./util.js";
|
|
15
|
-
// Pinned to @latest
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
14
|
+
// Pinned to @latest: an unpinned spec lets npx serve whatever stale global/cache
|
|
15
|
+
// install already resolves, so the agent's MCP server would silently miss newer tools.
|
|
16
|
+
// mergeMcpConfig compares entries verbatim, so re-running init also upgrades an old
|
|
17
|
+
// unpinned entry in place.
|
|
19
18
|
const MCP_SERVER_ENTRY = {
|
|
20
19
|
command: "npx",
|
|
21
20
|
args: ["-y", "@beryl-so/cli@latest", "mcp"],
|
|
22
21
|
};
|
|
23
|
-
// Headless: the coding agent drives this browser to set up sign-ins and repair plans; nobody
|
|
24
|
-
// watches the window, and a headed default breaks on CI / headless boxes. Matches how most devs
|
|
25
|
-
// already wire their own user-scoped playwright.
|
|
26
|
-
const PLAYWRIGHT_SERVER_ENTRY = {
|
|
27
|
-
command: "npx",
|
|
28
|
-
args: ["@playwright/mcp@latest", "--headless"],
|
|
29
|
-
};
|
|
30
22
|
// VS Code's `.vscode/mcp.json` uses a top-level `servers` key and tags each stdio server
|
|
31
|
-
// with `type`, unlike Cursor/Claude's `mcpServers`. Same pinned
|
|
23
|
+
// with `type`, unlike Cursor/Claude's `mcpServers`. Same pinned command, different shape.
|
|
32
24
|
const VSCODE_MCP_SERVER_ENTRY = { type: "stdio", ...MCP_SERVER_ENTRY };
|
|
33
|
-
const VSCODE_PLAYWRIGHT_SERVER_ENTRY = { type: "stdio", ...PLAYWRIGHT_SERVER_ENTRY };
|
|
34
25
|
// opencode flattens command + args into one array under `mcp`.
|
|
35
26
|
function opencodeEntry(entry) {
|
|
36
27
|
return { type: "local", command: [entry.command, ...entry.args], enabled: true };
|
|
@@ -45,14 +36,14 @@ function snippetEntry(name, entry) {
|
|
|
45
36
|
const MANUAL_CONFIG_SNIPPET = [
|
|
46
37
|
" {",
|
|
47
38
|
' "mcpServers": {',
|
|
48
|
-
|
|
49
|
-
snippetEntry("playwright", PLAYWRIGHT_SERVER_ENTRY),
|
|
39
|
+
snippetEntry("beryl", MCP_SERVER_ENTRY),
|
|
50
40
|
" }",
|
|
51
41
|
" }",
|
|
52
42
|
].join("\n");
|
|
53
43
|
// `key` is the container the editor expects: `mcpServers` for Claude/Cursor, `servers` for
|
|
54
|
-
// VS Code, `mcp` for opencode.
|
|
55
|
-
|
|
44
|
+
// VS Code, `mcp` for opencode. Only the beryl entry is ours; every other server (including
|
|
45
|
+
// a user-wired playwright) is left untouched.
|
|
46
|
+
function mergeMcpConfig(file, key, entry) {
|
|
56
47
|
let existing = {};
|
|
57
48
|
if (fs.existsSync(file)) {
|
|
58
49
|
try {
|
|
@@ -63,19 +54,14 @@ function mergeMcpConfig(file, key, entries) {
|
|
|
63
54
|
}
|
|
64
55
|
}
|
|
65
56
|
const servers = (existing[key] ?? {});
|
|
66
|
-
const beryl = JSON.stringify(servers.beryl) !== JSON.stringify(
|
|
67
|
-
if (beryl)
|
|
68
|
-
servers.beryl =
|
|
69
|
-
// Never clobber a playwright server the user already wired up.
|
|
70
|
-
const playwright = servers.playwright === undefined;
|
|
71
|
-
if (playwright)
|
|
72
|
-
servers.playwright = entries.playwright;
|
|
73
|
-
if (beryl || playwright) {
|
|
57
|
+
const beryl = JSON.stringify(servers.beryl) !== JSON.stringify(entry);
|
|
58
|
+
if (beryl) {
|
|
59
|
+
servers.beryl = entry;
|
|
74
60
|
existing[key] = servers;
|
|
75
61
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
76
62
|
fs.writeFileSync(file, JSON.stringify(existing, null, 2) + "\n");
|
|
77
63
|
}
|
|
78
|
-
return { beryl
|
|
64
|
+
return { beryl };
|
|
79
65
|
}
|
|
80
66
|
function claudeUserAdd(name, entry) {
|
|
81
67
|
try {
|
|
@@ -230,32 +216,6 @@ export function refreshInstalledSkills(cwd) {
|
|
|
230
216
|
}
|
|
231
217
|
return refreshed;
|
|
232
218
|
}
|
|
233
|
-
// Local runs drive a real browser, so the install is mandatory, not offered:
|
|
234
|
-
// missing means install now, no prompt, no opt-out. The runner ships with the CLI; the
|
|
235
|
-
// browser is the per-machine half, fetched for the runner's own revision. Never throws — a
|
|
236
|
-
// failed install must not fail `init`, which has already done its wiring; it prints the
|
|
237
|
-
// exact commands instead.
|
|
238
|
-
async function ensureLocalPlaywright(ctx, cwd) {
|
|
239
|
-
const gaps = playwrightGaps(cwd);
|
|
240
|
-
if (!anyGap(gaps)) {
|
|
241
|
-
ctx.err(`${green("✓")} Playwright + Chromium already installed ${dim("(local runs ready)")}`);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
try {
|
|
245
|
-
await installPlaywright(cwd, (line) => ctx.err(dim(line)));
|
|
246
|
-
// Believe the check, not the exit code — "local runs ready" is only honest if both
|
|
247
|
-
// halves are actually there afterwards.
|
|
248
|
-
const left = playwrightGaps(cwd);
|
|
249
|
-
if (anyGap(left))
|
|
250
|
-
throw new Error(`${describeGaps(left)} still missing afterwards`);
|
|
251
|
-
ctx.err(`${green("✓")} ${describeGaps(gaps)} installed ${dim("(local runs ready)")}`);
|
|
252
|
-
}
|
|
253
|
-
catch (err) {
|
|
254
|
-
ctx.err(`${red("✗")} Playwright install failed: ${err.message}`);
|
|
255
|
-
ctx.err(`${dim("•")} Finish the install by hand; local runs need it:\n` +
|
|
256
|
-
` ${cyan(installCommandsFor(gaps, cwd))}`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
219
|
const CLIENTS = [
|
|
260
220
|
["claude", "Claude Code + Claude desktop app"],
|
|
261
221
|
["cursor", "Cursor"],
|
|
@@ -290,8 +250,8 @@ export const initCommands = [
|
|
|
290
250
|
{
|
|
291
251
|
name: "init",
|
|
292
252
|
summary: "Set up Beryl in this repo: sign in and wire up your coding agent",
|
|
293
|
-
description: "Signs you in, wires the beryl
|
|
294
|
-
"
|
|
253
|
+
description: "Signs you in, wires the beryl MCP server into the coding agent you pick, and writes " +
|
|
254
|
+
"the beryl-test skill. " +
|
|
295
255
|
"Pick an agent with --claude, --cursor, --vscode and/or --opencode. With no flag it " +
|
|
296
256
|
"prompts (Enter picks Claude); with no flag and no TTY it wires nothing and prints the " +
|
|
297
257
|
"flags. --claude also wires the Claude DESKTOP app when installed, which needs a quit " +
|
|
@@ -394,51 +354,35 @@ export const initCommands = [
|
|
|
394
354
|
if (clients.has("claude") && scope === "user") {
|
|
395
355
|
// Claude Code owns ~/.claude.json — shell out to `claude mcp add` rather than write it.
|
|
396
356
|
const beryl = claudeUserAdd("beryl", MCP_SERVER_ENTRY);
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
ctx.err(`${green("✓")} beryl + playwright MCP configured ${dim("(user scope)")}`);
|
|
357
|
+
if (beryl.ok) {
|
|
358
|
+
ctx.err(`${green("✓")} beryl MCP configured ${dim("(user scope)")}`);
|
|
400
359
|
}
|
|
401
360
|
else {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
: "• `claude` not on PATH; run these to wire user-scope MCP servers:"));
|
|
361
|
+
ctx.err(yellow(beryl.reason === "failed"
|
|
362
|
+
? `• \`claude mcp add\` failed (${beryl.detail}); run this to wire the user-scope MCP server:`
|
|
363
|
+
: "• `claude` not on PATH; run this to wire the user-scope MCP server:"));
|
|
406
364
|
ctx.err(` ${cyan(claudeAddHint("beryl", MCP_SERVER_ENTRY))}`);
|
|
407
|
-
ctx.err(` ${cyan(claudeAddHint("playwright", PLAYWRIGHT_SERVER_ENTRY))}`);
|
|
408
365
|
}
|
|
409
366
|
}
|
|
410
367
|
else if (clients.has("claude")) {
|
|
411
368
|
const file = path.join(cwd, ".mcp.json");
|
|
412
|
-
const wrote = mergeMcpConfig(file, "mcpServers",
|
|
413
|
-
|
|
414
|
-
playwright: PLAYWRIGHT_SERVER_ENTRY,
|
|
415
|
-
});
|
|
416
|
-
const where = path.relative(cwd, file);
|
|
417
|
-
report("beryl MCP", wrote.beryl, where);
|
|
418
|
-
report("playwright MCP", wrote.playwright, where);
|
|
369
|
+
const wrote = mergeMcpConfig(file, "mcpServers", MCP_SERVER_ENTRY);
|
|
370
|
+
report("beryl MCP", wrote.beryl, path.relative(cwd, file));
|
|
419
371
|
}
|
|
420
372
|
// Separate surface from Claude Code, read only at launch.
|
|
421
373
|
const desktopFile = clients.has("claude") && scope === "user" ? claudeDesktopConfigFile() : null;
|
|
422
374
|
if (desktopFile) {
|
|
423
|
-
const wrote = mergeMcpConfig(desktopFile, "mcpServers",
|
|
424
|
-
beryl: MCP_SERVER_ENTRY,
|
|
425
|
-
playwright: PLAYWRIGHT_SERVER_ENTRY,
|
|
426
|
-
});
|
|
375
|
+
const wrote = mergeMcpConfig(desktopFile, "mcpServers", MCP_SERVER_ENTRY);
|
|
427
376
|
report("Claude desktop beryl MCP", wrote.beryl, "(Claude desktop app)");
|
|
428
|
-
|
|
429
|
-
if (wrote.beryl || wrote.playwright) {
|
|
377
|
+
if (wrote.beryl) {
|
|
430
378
|
ctx.err(yellow("• Quit and reopen Claude desktop: it reads that config at launch."));
|
|
431
379
|
}
|
|
432
380
|
}
|
|
433
381
|
if (clients.has("cursor")) {
|
|
434
382
|
const file = scope === "user" ? cursorUserConfigPath() : path.join(cwd, ".cursor", "mcp.json");
|
|
435
|
-
const wrote = mergeMcpConfig(file, "mcpServers",
|
|
436
|
-
beryl: MCP_SERVER_ENTRY,
|
|
437
|
-
playwright: PLAYWRIGHT_SERVER_ENTRY,
|
|
438
|
-
});
|
|
383
|
+
const wrote = mergeMcpConfig(file, "mcpServers", MCP_SERVER_ENTRY);
|
|
439
384
|
const where = scope === "user" ? file : path.relative(cwd, file);
|
|
440
385
|
report("cursor beryl MCP", wrote.beryl, where);
|
|
441
|
-
report("cursor playwright MCP", wrote.playwright, where);
|
|
442
386
|
const rule = writeCursorRule(cwd);
|
|
443
387
|
ctx.err(`${green("✓")} cursor authoring rule ${outcomeLabel(rule.outcome)} ${dim(path.relative(cwd, rule.file))}`);
|
|
444
388
|
if (scope === "user") {
|
|
@@ -455,13 +399,9 @@ export const initCommands = [
|
|
|
455
399
|
const file = userDir
|
|
456
400
|
? path.join(userDir, "mcp.json")
|
|
457
401
|
: path.join(cwd, ".vscode", "mcp.json");
|
|
458
|
-
const wrote = mergeMcpConfig(file, "servers",
|
|
459
|
-
beryl: VSCODE_MCP_SERVER_ENTRY,
|
|
460
|
-
playwright: VSCODE_PLAYWRIGHT_SERVER_ENTRY,
|
|
461
|
-
});
|
|
402
|
+
const wrote = mergeMcpConfig(file, "servers", VSCODE_MCP_SERVER_ENTRY);
|
|
462
403
|
const where = userDir ? file : path.relative(cwd, file);
|
|
463
404
|
report("vscode beryl MCP", wrote.beryl, where);
|
|
464
|
-
report("vscode playwright MCP", wrote.playwright, where);
|
|
465
405
|
if (scope === "user" && !userDir) {
|
|
466
406
|
ctx.err(dim(" • VS Code's user config dir was not found, so this went to a repo-local " +
|
|
467
407
|
".vscode/mcp.json. Run --vscode from your project, or add the server via " +
|
|
@@ -479,19 +419,14 @@ export const initCommands = [
|
|
|
479
419
|
const { file, jsonc } = opencodeConfigFile(scope, cwd);
|
|
480
420
|
if (jsonc) {
|
|
481
421
|
ctx.err(yellow(`• ${file} is JSONC and may hold comments, so it was left alone.`));
|
|
482
|
-
ctx.err(cyan(` Add
|
|
422
|
+
ctx.err(cyan(` Add this under "mcp": ${JSON.stringify({
|
|
483
423
|
beryl: opencodeEntry(MCP_SERVER_ENTRY),
|
|
484
|
-
playwright: opencodeEntry(PLAYWRIGHT_SERVER_ENTRY),
|
|
485
424
|
})}`));
|
|
486
425
|
}
|
|
487
426
|
else {
|
|
488
|
-
const wrote = mergeMcpConfig(file, "mcp",
|
|
489
|
-
beryl: opencodeEntry(MCP_SERVER_ENTRY),
|
|
490
|
-
playwright: opencodeEntry(PLAYWRIGHT_SERVER_ENTRY),
|
|
491
|
-
});
|
|
427
|
+
const wrote = mergeMcpConfig(file, "mcp", opencodeEntry(MCP_SERVER_ENTRY));
|
|
492
428
|
const where = scope === "user" ? file : path.relative(cwd, file);
|
|
493
429
|
report("opencode beryl MCP", wrote.beryl, where);
|
|
494
|
-
report("opencode playwright MCP", wrote.playwright, where);
|
|
495
430
|
}
|
|
496
431
|
}
|
|
497
432
|
const skills = writeSkills(cwd, scope);
|
|
@@ -508,7 +443,6 @@ export const initCommands = [
|
|
|
508
443
|
: skillWhere;
|
|
509
444
|
ctx.err(`${green("✓")} beryl-test skill ${what} ${dim(where)}`);
|
|
510
445
|
}
|
|
511
|
-
await ensureLocalPlaywright(ctx, cwd);
|
|
512
446
|
// Name the agent actually wired, not the raw flag: the picker sets `clients` too.
|
|
513
447
|
const ed = clients.has("claude")
|
|
514
448
|
? { open: "open your editor", agent: "Claude" }
|
|
@@ -528,8 +462,8 @@ export const initCommands = [
|
|
|
528
462
|
` ${yellow(`Details: ${MANUAL_SETUP_DOCS}`)}\n`
|
|
529
463
|
: `\n${bold(`Beryl is set up. Now ${ed.open} and ask ${ed.agent} to create tests.`)}\n` +
|
|
530
464
|
` ${dim(`• Say: "create tests for https://example.com from docs/checkout.md" and ${ed.agent} starts`)}\n` +
|
|
531
|
-
` ${dim(" a cloud test-creation job that authors them from that document.")}\n` +
|
|
532
|
-
` ${dim(`• Then ask ${ed.agent} to run them
|
|
465
|
+
` ${dim(" a cloud test-creation job (jobs create) that authors them from that document.")}\n` +
|
|
466
|
+
` ${dim(`• Then ask ${ed.agent} to run them in Beryl's cloud (runs trigger).`)}\n`;
|
|
533
467
|
// The .mcp.json entry is pinned to @latest, but a global install / old npx cache
|
|
534
468
|
// still wins resolution — so tell the user when the CLI they just ran is stale.
|
|
535
469
|
await warnIfStale(cliVersion(), (msg) => ctx.err(yellow(msg)));
|
|
@@ -552,7 +486,7 @@ export const initCommands = [
|
|
|
552
486
|
description: "The guide to durable, healable tests: where new tests come from (`jobs create`), the " +
|
|
553
487
|
"ActionPlan shape, outcome assertions, natural-language intent, test accounts and the " +
|
|
554
488
|
"project mailbox ({{login_email}}, {{mailbox_address}}, {{inbox_address}} + " +
|
|
555
|
-
"await_email), and the
|
|
489
|
+
"await_email), and the run-fix loop. Same content as the beryl-test skill " +
|
|
556
490
|
"`beryl init` installs. " +
|
|
557
491
|
`Skip if a loaded beryl-test skill states v${cliVersion()}; else call this first ` +
|
|
558
492
|
"(works without logging in).",
|
package/dist/commands/jobs.js
CHANGED
|
@@ -29,6 +29,10 @@ export function jobHuman(job) {
|
|
|
29
29
|
lines.push("Skipped scenarios:");
|
|
30
30
|
for (const s of skipped)
|
|
31
31
|
lines.push(` ${s.handle}: ${s.reason}`);
|
|
32
|
+
for (const step of job.surface_steps ?? []) {
|
|
33
|
+
for (const w of step.warnings ?? [])
|
|
34
|
+
lines.push(`Warning [${step.surface}]: ${w}`);
|
|
35
|
+
}
|
|
32
36
|
return lines.join("\n");
|
|
33
37
|
}
|
|
34
38
|
export const jobCommands = [
|
|
@@ -94,16 +94,6 @@ export const projectCommands = [
|
|
|
94
94
|
"site, and only asked when detection is genuinely unsure",
|
|
95
95
|
},
|
|
96
96
|
{ name: "force-new-login", type: "boolean", description: "Ignore any reusable saved login" },
|
|
97
|
-
{
|
|
98
|
-
name: "explore",
|
|
99
|
-
type: "boolean",
|
|
100
|
-
description: "Also start a cloud exploration that authors tests for you. Off by default: create " +
|
|
101
|
-
"tests from a document with `beryl jobs create`",
|
|
102
|
-
mcpDescription: "Leave unset: create tests from a document with `jobs create`. Set only when the " +
|
|
103
|
-
"user explicitly asks for a cloud exploration",
|
|
104
|
-
},
|
|
105
|
-
{ name: "watch", type: "boolean", description: "Start the exploration (implies --explore) and stream it live" },
|
|
106
|
-
{ name: "timeout", type: "number", description: "With --watch: max minutes to wait" },
|
|
107
97
|
],
|
|
108
98
|
examples: [
|
|
109
99
|
"beryl projects create https://app.example.com",
|
|
@@ -111,21 +101,15 @@ export const projectCommands = [
|
|
|
111
101
|
'beryl projects create --name "Acme production"',
|
|
112
102
|
],
|
|
113
103
|
async run(ctx, input) {
|
|
114
|
-
const watch = flagBool(input, "watch");
|
|
115
|
-
if (input.flags.explore === false && watch)
|
|
116
|
-
throw new UsageError("--no-explore cannot be combined with --watch");
|
|
117
|
-
const explore = flagBool(input, "explore") || watch;
|
|
118
104
|
const ws = await ctx.requireWorkspace(input);
|
|
119
105
|
const rawUrl = input.args.url;
|
|
120
106
|
const url = typeof rawUrl === "string" ? rawUrl : "";
|
|
121
107
|
const name = flagStr(input, "name");
|
|
122
|
-
// Name-only: create a real empty project (no URL
|
|
123
|
-
//
|
|
108
|
+
// Name-only: create a real empty project (no URL). Set a URL later with
|
|
109
|
+
// `beryl envs update <env-id>`.
|
|
124
110
|
if (!url) {
|
|
125
111
|
if (!name)
|
|
126
112
|
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");
|
|
129
113
|
const created = (await ctx.client.post(`/workspaces/${ws}/projects`, {
|
|
130
114
|
name,
|
|
131
115
|
}));
|
|
@@ -141,29 +125,12 @@ export const projectCommands = [
|
|
|
141
125
|
root_url: url,
|
|
142
126
|
requires_auth_choice: auth,
|
|
143
127
|
force_new_login: flagBool(input, "force-new-login"),
|
|
144
|
-
skip_exploration: !explore,
|
|
145
128
|
}));
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
};
|
|
152
|
-
// The agent cannot start until a login is captured, so there is nothing to watch.
|
|
153
|
-
if (created.auth_status === "needs_capture")
|
|
154
|
-
return {
|
|
155
|
-
data: created,
|
|
156
|
-
human: `${green("Project created")}: ${created.project_id}\n` +
|
|
157
|
-
`The site needs a login before the agent can explore it. Capture one in the ` +
|
|
158
|
-
`Beryl webapp (open the project — it walks you through the login) — the ` +
|
|
159
|
-
`exploration starts as soon as you do.`,
|
|
160
|
-
};
|
|
161
|
-
if (!flagBool(input, "watch"))
|
|
162
|
-
return { data: created };
|
|
163
|
-
ctx.err(dim(`project ${created.project_id} created — waiting for the agent to start…`));
|
|
164
|
-
const explorationId = await pollCurrentExploration(ctx, ws, created.project_id);
|
|
165
|
-
const watched = await watchExploration(ctx, ws, created.project_id, explorationId, flagNum(input, "timeout"));
|
|
166
|
-
return { data: { ...created, ...watched.data }, exitCode: watched.exitCode };
|
|
129
|
+
return {
|
|
130
|
+
data: created,
|
|
131
|
+
human: `${green("Project created")}: ${created.project_id}\n` +
|
|
132
|
+
`Create tests from a document describing the flows: \`beryl jobs create <file>\`.`,
|
|
133
|
+
};
|
|
167
134
|
},
|
|
168
135
|
},
|
|
169
136
|
{
|
|
@@ -231,19 +198,26 @@ export const projectCommands = [
|
|
|
231
198
|
name: "projects surfaces",
|
|
232
199
|
summary: "Set which surfaces are enabled for this project",
|
|
233
200
|
description: "Replaces the project's enabled surfaces with the ones listed: web-desktop, " +
|
|
234
|
-
"web-tablet, web-mobile. A new project has web-desktop only. At least
|
|
235
|
-
"required; `projects get` shows the current set. Turning a surface off
|
|
236
|
-
"no tests: delete a surface's tests to stop running them."
|
|
201
|
+
"web-tablet, web-mobile, android, ios. A new project has web-desktop only. At least " +
|
|
202
|
+
"one is required; `projects get` shows the current set. Turning a surface off " +
|
|
203
|
+
"removes no tests: delete a surface's tests to stop running them. android and ios " +
|
|
204
|
+
"test the app named by each environment's app id for that surface (`envs update " +
|
|
205
|
+
"--app-id android=<package> --app-id ios=<bundle id>`) from its newest uploaded build " +
|
|
206
|
+
"(`config files upload app.apk`, `config files upload App.zip --kind ios_build`), on " +
|
|
207
|
+
"Beryl's Android and iOS workers.",
|
|
237
208
|
scope: "project",
|
|
238
209
|
args: [
|
|
239
210
|
{
|
|
240
211
|
name: "surfaces",
|
|
241
|
-
description: "web-desktop | web-tablet | web-mobile, one or more",
|
|
212
|
+
description: "web-desktop | web-tablet | web-mobile | android | ios, one or more",
|
|
242
213
|
required: true,
|
|
243
214
|
variadic: true,
|
|
244
215
|
},
|
|
245
216
|
],
|
|
246
|
-
examples: [
|
|
217
|
+
examples: [
|
|
218
|
+
"beryl projects surfaces web-desktop web-mobile",
|
|
219
|
+
"beryl projects surfaces web-desktop android ios",
|
|
220
|
+
],
|
|
247
221
|
async run(ctx, input) {
|
|
248
222
|
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
249
223
|
const data = (await ctx.client.patch(projectPath(workspaceId, projectId), {
|