@beryl-so/cli 0.17.0 → 0.22.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 +35 -7
- package/dist/adapters/cli.js +15 -1
- package/dist/adapters/mcp.js +44 -5
- package/dist/beryl-test-skill.js +275 -90
- package/dist/commands/accounts.js +331 -0
- package/dist/commands/environments.js +0 -2
- package/dist/commands/init.js +43 -27
- package/dist/commands/mailboxes.js +159 -0
- package/dist/commands/mcp.js +25 -0
- package/dist/commands/projects.js +0 -6
- package/dist/commands/runs.js +159 -46
- package/dist/commands/tests.js +29 -6
- package/dist/email-pump.js +5 -2
- package/dist/http.js +4 -1
- package/dist/local-exec.js +86 -27
- package/dist/local-run.js +21 -3
- package/dist/playwright-install.js +118 -7
- package/dist/registry/index.js +4 -2
- package/dist/schema.generated.js +32 -0
- package/package.json +2 -2
- package/dist/commands/inboxes.js +0 -145
package/dist/local-run.js
CHANGED
|
@@ -21,12 +21,20 @@ const ISOLATING_CONFIG = (testDir, artifactsDir) => `import { defineConfig } fro
|
|
|
21
21
|
` outputDir: ${JSON.stringify(artifactsDir)},\n` +
|
|
22
22
|
` fullyParallel: false,\n` +
|
|
23
23
|
`});\n`;
|
|
24
|
+
export function scrubText(text, redact) {
|
|
25
|
+
let out = text;
|
|
26
|
+
for (const value of redact ?? []) {
|
|
27
|
+
if (value)
|
|
28
|
+
out = out.split(value).join("***");
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
24
32
|
export function copyTextScrubbed(src, dest, redact) {
|
|
25
|
-
if (!redact) {
|
|
33
|
+
if (!redact?.length) {
|
|
26
34
|
fs.copyFileSync(src, dest);
|
|
27
35
|
return;
|
|
28
36
|
}
|
|
29
|
-
fs.writeFileSync(dest, fs.readFileSync(src, "utf8")
|
|
37
|
+
fs.writeFileSync(dest, scrubText(fs.readFileSync(src, "utf8"), redact));
|
|
30
38
|
}
|
|
31
39
|
const PASSING = new Set(["passed", "expected"]);
|
|
32
40
|
const SKIPPED = new Set(["skipped"]);
|
|
@@ -224,8 +232,18 @@ export async function runSpecLocally(opts) {
|
|
|
224
232
|
// ephemeral run dir.
|
|
225
233
|
const outDir = opts.dir ? path.resolve(opts.dir) : runDir;
|
|
226
234
|
fs.mkdirSync(outDir, { recursive: true });
|
|
235
|
+
let specContent = opts.spec;
|
|
236
|
+
if (opts.authSession) {
|
|
237
|
+
// Mirror the cloud runner's staging: write the session files into the workdir and
|
|
238
|
+
// point the spec's storageState reference at the absolute path (auth-init.json is
|
|
239
|
+
// read relative to cwd, which is the run dir — same as the cloud workdir).
|
|
240
|
+
const storagePath = path.join(runDir, "auth-state.json");
|
|
241
|
+
fs.writeFileSync(storagePath, opts.authSession.storageState, "utf8");
|
|
242
|
+
fs.writeFileSync(path.join(runDir, "auth-init.json"), JSON.stringify(opts.authSession.initScripts), "utf8");
|
|
243
|
+
specContent = specContent.split('"auth-state.json"').join(JSON.stringify(storagePath));
|
|
244
|
+
}
|
|
227
245
|
const specPath = path.join(runDir, "beryl-local.spec.ts");
|
|
228
|
-
fs.writeFileSync(specPath,
|
|
246
|
+
fs.writeFileSync(specPath, specContent);
|
|
229
247
|
const configPath = path.join(runDir, "beryl-local.config.ts");
|
|
230
248
|
const artifactsDir = path.join(outDir, "artifacts");
|
|
231
249
|
fs.writeFileSync(configPath, ISOLATING_CONFIG(runDir, artifactsDir));
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import os from "node:os";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
// The two commands that turn "nothing Playwright-related installed" into "local runs work":
|
|
5
7
|
// the test runner as a dev dep, then its browser binary. Kept as data so the CLI can both
|
|
@@ -7,24 +9,133 @@ import path from "node:path";
|
|
|
7
9
|
export const INSTALL_TEST_RUNNER = ["npm", "i", "-D", "@playwright/test"];
|
|
8
10
|
export const INSTALL_CHROMIUM = ["npx", "playwright", "install", "chromium"];
|
|
9
11
|
export const PLAYWRIGHT_INSTALL_COMMANDS = `${INSTALL_TEST_RUNNER.join(" ")} && ${INSTALL_CHROMIUM.join(" ")}`;
|
|
10
|
-
// Resolve
|
|
11
|
-
//
|
|
12
|
-
|
|
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"));
|
|
13
15
|
export function hasPlaywrightTest(cwd) {
|
|
14
16
|
try {
|
|
15
|
-
|
|
17
|
+
projectRequire(cwd).resolve("@playwright/test");
|
|
16
18
|
return true;
|
|
17
19
|
}
|
|
18
20
|
catch {
|
|
19
21
|
return false;
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
|
-
|
|
24
|
+
// A local run launches chromium headless, which Playwright serves from a separate
|
|
25
|
+
// `chromium_headless_shell` build (`playwright install chromium` fetches both). That is the
|
|
26
|
+
// only engine a run ever launches, so it is the only one we install or check for.
|
|
27
|
+
const HEADLESS_SHELL = "chromium-headless-shell";
|
|
28
|
+
// Written last by Playwright's downloader, so its presence means a complete browser.
|
|
29
|
+
const INSTALL_MARKER = "INSTALLATION_COMPLETE";
|
|
30
|
+
function playwrightCoreDir(cwd) {
|
|
31
|
+
const req = projectRequire(cwd);
|
|
32
|
+
try {
|
|
33
|
+
return path.dirname(req.resolve("playwright-core/package.json"));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Not hoisted (pnpm) — look from @playwright/test's own tree instead.
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const fromRunner = createRequire(req.resolve("@playwright/test"));
|
|
40
|
+
return path.dirname(fromRunner.resolve("playwright-core/package.json"));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Mirrors playwright-core's registry root: PLAYWRIGHT_BROWSERS_PATH ("0" means inside the
|
|
47
|
+
// package), else the per-platform cache dir. undefined on a platform Playwright doesn't
|
|
48
|
+
// support, where we have no verdict to offer.
|
|
49
|
+
function browsersRoot(coreDir) {
|
|
50
|
+
const override = process.env.PLAYWRIGHT_BROWSERS_PATH;
|
|
51
|
+
if (override === "0")
|
|
52
|
+
return coreDir ? path.join(coreDir, ".local-browsers") : undefined;
|
|
53
|
+
if (override)
|
|
54
|
+
return path.resolve(override);
|
|
55
|
+
const home = os.homedir();
|
|
56
|
+
if (process.platform === "darwin")
|
|
57
|
+
return path.join(home, "Library", "Caches", "ms-playwright");
|
|
58
|
+
if (process.platform === "win32") {
|
|
59
|
+
const local = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
|
|
60
|
+
return path.join(local, "ms-playwright");
|
|
61
|
+
}
|
|
62
|
+
if (process.platform === "linux") {
|
|
63
|
+
return path.join(process.env.XDG_CACHE_HOME || path.join(home, ".cache"), "ms-playwright");
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
// The exact directory this project's Playwright will look in: <root>/<browser name with
|
|
68
|
+
// dashes as underscores>-<revision>, straight out of playwright-core's browsers.json — so a
|
|
69
|
+
// browser downloaded for an older Playwright doesn't read as the one this one needs.
|
|
70
|
+
function wantedChromiumDir(coreDir) {
|
|
71
|
+
if (!coreDir)
|
|
72
|
+
return undefined;
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(coreDir, "browsers.json"), "utf8"));
|
|
75
|
+
const entry = parsed.browsers?.find((b) => b.name === HEADLESS_SHELL) ??
|
|
76
|
+
parsed.browsers?.find((b) => b.name === "chromium");
|
|
77
|
+
if (!entry?.name || !entry.revision)
|
|
78
|
+
return undefined;
|
|
79
|
+
return `${entry.name.replace(/-/g, "_")}-${entry.revision}`;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const isComplete = (dir) => fs.existsSync(path.join(dir, INSTALL_MARKER));
|
|
86
|
+
/**
|
|
87
|
+
* Is the browser binary a local run actually launches present? Without it every test in a
|
|
88
|
+
* run dies with the same `browserType.launch: Executable doesn't exist` — an environment
|
|
89
|
+
* problem that reads as a broken suite.
|
|
90
|
+
*/
|
|
91
|
+
export function hasChromiumBrowser(cwd) {
|
|
92
|
+
const coreDir = playwrightCoreDir(cwd);
|
|
93
|
+
const root = browsersRoot(coreDir);
|
|
94
|
+
// Nowhere known to look — say nothing rather than block; the run surfaces Playwright's
|
|
95
|
+
// own error if it really is missing.
|
|
96
|
+
if (!root)
|
|
97
|
+
return true;
|
|
98
|
+
const wanted = wantedChromiumDir(coreDir);
|
|
99
|
+
if (wanted)
|
|
100
|
+
return isComplete(path.join(root, wanted));
|
|
101
|
+
// Revision unknown (no resolvable playwright-core): any completed chromium build is the
|
|
102
|
+
// best evidence there is, and guessing wrong only costs an idempotent re-install.
|
|
103
|
+
try {
|
|
104
|
+
return fs
|
|
105
|
+
.readdirSync(root)
|
|
106
|
+
.some((d) => /^chromium(_headless_shell)?-\d+$/.test(d) && isComplete(path.join(root, d)));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function playwrightGaps(cwd) {
|
|
113
|
+
const runner = !hasPlaywrightTest(cwd);
|
|
114
|
+
// With no runner there is no revision to judge a cached browser against, so the honest
|
|
115
|
+
// answer is the full install — not the npm half and a second failure right after it.
|
|
116
|
+
return { runner, browser: runner || !hasChromiumBrowser(cwd) };
|
|
117
|
+
}
|
|
118
|
+
export const anyGap = (gaps) => gaps.runner || gaps.browser;
|
|
119
|
+
export function describeGaps(gaps) {
|
|
120
|
+
if (gaps.runner && gaps.browser)
|
|
121
|
+
return "@playwright/test + the Chromium browser";
|
|
122
|
+
return gaps.browser ? "the Chromium browser" : "@playwright/test";
|
|
123
|
+
}
|
|
124
|
+
/** Only the commands the missing halves need — a present @playwright/test isn't reinstalled. */
|
|
125
|
+
export function installCommandsFor(gaps) {
|
|
126
|
+
const commands = [
|
|
127
|
+
...(gaps.runner ? [INSTALL_TEST_RUNNER.join(" ")] : []),
|
|
128
|
+
...(gaps.browser ? [INSTALL_CHROMIUM.join(" ")] : []),
|
|
129
|
+
];
|
|
130
|
+
return commands.length > 0 ? commands.join(" && ") : PLAYWRIGHT_INSTALL_COMMANDS;
|
|
131
|
+
}
|
|
132
|
+
export const installPrompt = (commands) => `Install local Playwright now (${commands})? [Y/n] `;
|
|
133
|
+
export const INSTALL_PROMPT = installPrompt(PLAYWRIGHT_INSTALL_COMMANDS);
|
|
23
134
|
// Ask (default-yes) whether to install. Returns false — not throwing — when there is no answer
|
|
24
135
|
// or the prompt fails, so callers uniformly fall back to printing the install hint.
|
|
25
|
-
export async function confirmInstall(prompt) {
|
|
136
|
+
export async function confirmInstall(prompt, commands = PLAYWRIGHT_INSTALL_COMMANDS) {
|
|
26
137
|
try {
|
|
27
|
-
return !/^n(o)?$/i.test(await prompt(
|
|
138
|
+
return !/^n(o)?$/i.test(await prompt(installPrompt(commands)));
|
|
28
139
|
}
|
|
29
140
|
catch {
|
|
30
141
|
return false;
|
package/dist/registry/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { accountCommands } from "../commands/account.js";
|
|
2
|
+
import { testAccountCommands } from "../commands/accounts.js";
|
|
2
3
|
import { authCommands } from "../commands/auth.js";
|
|
3
4
|
import { configCommands } from "../commands/config-vars.js";
|
|
4
5
|
import { environmentCommands } from "../commands/environments.js";
|
|
5
6
|
import { explorationCommands } from "../commands/explorations.js";
|
|
6
|
-
import {
|
|
7
|
+
import { mailboxCommands } from "../commands/mailboxes.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";
|
|
@@ -61,7 +62,8 @@ export const commands = [
|
|
|
61
62
|
...explorationCommands,
|
|
62
63
|
...configCommands,
|
|
63
64
|
...slackCommands,
|
|
64
|
-
...
|
|
65
|
+
...mailboxCommands,
|
|
66
|
+
...testAccountCommands,
|
|
65
67
|
...accountCommands,
|
|
66
68
|
...mcpCommands,
|
|
67
69
|
].map(withScopeFlags).map(hideFromMcp);
|
package/dist/schema.generated.js
CHANGED
|
@@ -25,6 +25,15 @@ export const ACTION_PLAN_SCHEMA = {
|
|
|
25
25
|
"title": "ActionType",
|
|
26
26
|
"type": "string"
|
|
27
27
|
},
|
|
28
|
+
"AuthMode": {
|
|
29
|
+
"description": "How an authenticated test reaches its logged-in start state. The AUTHOR sets\nthis, explicitly, on every plan that sets ``requires_auth`` \u2014 a submitted plan\nthat omits it is rejected, because the wrong guess banks a plan that runs logged\nout and fails somewhere misleading.\n\n``inline`` \u2014 the plan signs itself in: its own steps fill ``{{login_email}}`` /\n``{{login_password}}``, or it rides the project's captured session.\n\n``session`` \u2014 the test's account signs in ONCE per run, ahead of the tests; the\nresulting browser session is proved against a fresh context and then handed to every\nsession-mode test in the run. The plan itself carries NO sign-in steps, so it starts\nwhere the flow it actually tests begins. Requires the account to have a stored\nlogin plan (``accounts set-login``).",
|
|
30
|
+
"enum": [
|
|
31
|
+
"inline",
|
|
32
|
+
"session"
|
|
33
|
+
],
|
|
34
|
+
"title": "AuthMode",
|
|
35
|
+
"type": "string"
|
|
36
|
+
},
|
|
28
37
|
"DialogChoice": {
|
|
29
38
|
"enum": [
|
|
30
39
|
"accept",
|
|
@@ -865,6 +874,7 @@ export const ACTION_PLAN_SCHEMA = {
|
|
|
865
874
|
"inbox_address",
|
|
866
875
|
"login_email",
|
|
867
876
|
"login_password",
|
|
877
|
+
"mailbox_address",
|
|
868
878
|
"timestamp",
|
|
869
879
|
"unique",
|
|
870
880
|
"uuid"
|
|
@@ -1130,6 +1140,24 @@ export const ACTION_PLAN_SCHEMA = {
|
|
|
1130
1140
|
"$id": "https://api.beryl.so/api/v1/schemas/action-plan.schema.json",
|
|
1131
1141
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
1132
1142
|
"allOf": [
|
|
1143
|
+
{
|
|
1144
|
+
"$comment": "requires_auth demands an explicit auth_mode \u2014 there is no default. 'inline' when the plan signs itself in, 'session' when it rides its account's once-per-run session and carries no sign-in steps.",
|
|
1145
|
+
"if": {
|
|
1146
|
+
"properties": {
|
|
1147
|
+
"requires_auth": {
|
|
1148
|
+
"const": true
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
"required": [
|
|
1152
|
+
"requires_auth"
|
|
1153
|
+
]
|
|
1154
|
+
},
|
|
1155
|
+
"then": {
|
|
1156
|
+
"required": [
|
|
1157
|
+
"auth_mode"
|
|
1158
|
+
]
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1133
1161
|
{
|
|
1134
1162
|
"$comment": "The first executed step must be a goto, so the test loads a page before acting (that is before[0] when there is a setup section, else steps[0]).",
|
|
1135
1163
|
"else": {
|
|
@@ -1242,6 +1270,10 @@ export const ACTION_PLAN_SCHEMA = {
|
|
|
1242
1270
|
"default": null,
|
|
1243
1271
|
"title": "Auth Label"
|
|
1244
1272
|
},
|
|
1273
|
+
"auth_mode": {
|
|
1274
|
+
"$ref": "#/$defs/AuthMode",
|
|
1275
|
+
"default": "inline"
|
|
1276
|
+
},
|
|
1245
1277
|
"before": {
|
|
1246
1278
|
"items": {
|
|
1247
1279
|
"$ref": "#/$defs/PlanStep"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beryl-so/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Beryl on the command line
|
|
3
|
+
"version": "0.22.0",
|
|
4
|
+
"description": "Beryl on the command line \u2014 projects, runs, the exploring agent, and an MCP server over the same commands.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://beryl.so/docs/cli",
|
package/dist/commands/inboxes.js
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { extractCode } from "../email-extract.js";
|
|
2
|
-
import { dim, green } from "../output.js";
|
|
3
|
-
import { arg, flagBool, flagNum, flagStr } from "./util.js";
|
|
4
|
-
export const inboxCommands = [
|
|
5
|
-
{
|
|
6
|
-
name: "inbox create",
|
|
7
|
-
summary: "Mint an email inbox that Beryl receives mail for",
|
|
8
|
-
groupSummary: "Email inboxes for testing flows that send mail — signups, OTPs, receipts.",
|
|
9
|
-
description: "Creates a receiving address under Beryl's inbound email domain and returns it. " +
|
|
10
|
-
"Use it wherever a test needs a real, readable mailbox — e.g. as the --email for " +
|
|
11
|
-
"`beryl signup`, then read the code back with `beryl inbox read --extract-code`. " +
|
|
12
|
-
"Pass --permanent to mint the workspace's single permanent mailbox (no TTL).",
|
|
13
|
-
scope: "workspace",
|
|
14
|
-
flags: [
|
|
15
|
-
{
|
|
16
|
-
name: "permanent",
|
|
17
|
-
type: "boolean",
|
|
18
|
-
description: "Mint the workspace's permanent mailbox (no TTL); one per workspace",
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
name: "ttl-hours",
|
|
22
|
-
type: "number",
|
|
23
|
-
description: "Hours before the inbox expires and stops receiving (1-168, default 24; ignored with --permanent)",
|
|
24
|
-
},
|
|
25
|
-
{ name: "project", type: "string", description: "Attach the inbox to a project id" },
|
|
26
|
-
],
|
|
27
|
-
examples: [
|
|
28
|
-
"beryl inbox create --json",
|
|
29
|
-
"beryl inbox create --ttl-hours 2",
|
|
30
|
-
"beryl inbox create --permanent",
|
|
31
|
-
],
|
|
32
|
-
async run(ctx, input) {
|
|
33
|
-
const ws = await ctx.requireWorkspace(input);
|
|
34
|
-
const inbox = (await ctx.client.post(`/workspaces/${ws}/inboxes`, {
|
|
35
|
-
ttl_hours: flagBool(input, "permanent") ? null : (flagNum(input, "ttl-hours") ?? 24),
|
|
36
|
-
project_id: flagStr(input, "project") ?? null,
|
|
37
|
-
}));
|
|
38
|
-
return {
|
|
39
|
-
data: inbox,
|
|
40
|
-
human: `${green("Created")} inbox ${inbox.id}\n\n ${inbox.address}\n\n` +
|
|
41
|
-
dim(`Read it with: beryl inbox read ${inbox.id}`),
|
|
42
|
-
};
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
name: "inbox list",
|
|
47
|
-
summary: "List the workspace's inboxes, newest first",
|
|
48
|
-
description: "Every inbox the workspace has minted with `beryl inbox create`. Expired inboxes " +
|
|
49
|
-
"stop receiving and are hard-deleted by a background sweep, so they drop off " +
|
|
50
|
-
"this list shortly after their TTL. Pass --permanent for just the permanent mailbox.",
|
|
51
|
-
scope: "workspace",
|
|
52
|
-
flags: [
|
|
53
|
-
{
|
|
54
|
-
name: "permanent",
|
|
55
|
-
type: "boolean",
|
|
56
|
-
description: "Only the workspace's permanent mailbox (no TTL, not run/project scoped)",
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
async run(ctx, input) {
|
|
60
|
-
const ws = await ctx.requireWorkspace(input);
|
|
61
|
-
const query = flagBool(input, "permanent") ? { permanent: true } : undefined;
|
|
62
|
-
return { data: await ctx.client.get(`/workspaces/${ws}/inboxes`, query) };
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
name: "inbox delete",
|
|
67
|
-
summary: "Delete an inbox and every email it has received",
|
|
68
|
-
scope: "workspace",
|
|
69
|
-
args: [{ name: "inbox-id", description: "Inbox id from `beryl inbox create`", required: true }],
|
|
70
|
-
flags: [{ name: "force", type: "boolean", description: "Skip the confirmation prompt" }],
|
|
71
|
-
async run(ctx, input) {
|
|
72
|
-
const ws = await ctx.requireWorkspace(input);
|
|
73
|
-
const id = arg(input, "inbox-id");
|
|
74
|
-
await ctx.confirm(`Delete inbox ${id} and its emails?`, flagBool(input, "force"));
|
|
75
|
-
await ctx.client.del(`/workspaces/${ws}/inboxes/${id}`);
|
|
76
|
-
return { human: "Deleted." };
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
name: "inbox read",
|
|
81
|
-
summary: "Read the latest email from an inbox (waits for one to arrive)",
|
|
82
|
-
description: "Waits up to --timeout-s for a matching email and returns it (one blocking request; " +
|
|
83
|
-
"the server caps the wait at 50s — re-run to keep waiting). With --extract-code, " +
|
|
84
|
-
"also pulls the one-time code (4-8 digits) out of the body/subject — handy for " +
|
|
85
|
-
"completing `beryl login --email <addr> --code <code>` unattended. Exits non-zero " +
|
|
86
|
-
"if nothing arrives before the timeout.",
|
|
87
|
-
scope: "workspace",
|
|
88
|
-
args: [{ name: "inbox-id", description: "Inbox id from `beryl inbox create`", required: true }],
|
|
89
|
-
flags: [
|
|
90
|
-
{
|
|
91
|
-
name: "timeout-s",
|
|
92
|
-
type: "number",
|
|
93
|
-
description: "Seconds to wait for a matching email (0 = don't wait; max 50, default 30)",
|
|
94
|
-
},
|
|
95
|
-
{ name: "since", type: "string", description: "Only emails received after this ISO timestamp" },
|
|
96
|
-
{ name: "from-contains", type: "string", description: "Only emails whose sender contains this" },
|
|
97
|
-
{
|
|
98
|
-
name: "subject-contains",
|
|
99
|
-
type: "string",
|
|
100
|
-
description: "Only emails whose subject contains this",
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
name: "extract-code",
|
|
104
|
-
type: "boolean",
|
|
105
|
-
description: "Also return the one-time code found in the email as `code`",
|
|
106
|
-
},
|
|
107
|
-
],
|
|
108
|
-
examples: [
|
|
109
|
-
"beryl inbox read ibx_123 --timeout-s 45 --json",
|
|
110
|
-
"beryl inbox read ibx_123 --subject-contains code --extract-code --json",
|
|
111
|
-
],
|
|
112
|
-
async run(ctx, input) {
|
|
113
|
-
const ws = await ctx.requireWorkspace(input);
|
|
114
|
-
const email = (await ctx.client.get(`/workspaces/${ws}/inboxes/${arg(input, "inbox-id")}/emails/latest`, {
|
|
115
|
-
timeout_s: flagNum(input, "timeout-s"),
|
|
116
|
-
since: flagStr(input, "since"),
|
|
117
|
-
from_contains: flagStr(input, "from-contains"),
|
|
118
|
-
subject_contains: flagStr(input, "subject-contains"),
|
|
119
|
-
}));
|
|
120
|
-
if (!flagBool(input, "extract-code"))
|
|
121
|
-
return { data: email };
|
|
122
|
-
return { data: { ...email, code: extractCode(email) } };
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
name: "inbox emails",
|
|
127
|
-
summary: "List the emails an inbox has received",
|
|
128
|
-
scope: "workspace",
|
|
129
|
-
args: [{ name: "inbox-id", description: "Inbox id from `beryl inbox create`", required: true }],
|
|
130
|
-
flags: [
|
|
131
|
-
{ name: "since", type: "string", description: "Only emails received after this ISO timestamp" },
|
|
132
|
-
{
|
|
133
|
-
name: "limit",
|
|
134
|
-
type: "number",
|
|
135
|
-
description: "Return only the most recent N emails (newest first)",
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
async run(ctx, input) {
|
|
139
|
-
const ws = await ctx.requireWorkspace(input);
|
|
140
|
-
return {
|
|
141
|
-
data: await ctx.client.get(`/workspaces/${ws}/inboxes/${arg(input, "inbox-id")}/emails`, { since: flagStr(input, "since"), limit: flagNum(input, "limit") }),
|
|
142
|
-
};
|
|
143
|
-
},
|
|
144
|
-
},
|
|
145
|
-
];
|