@beryl-so/cli 0.34.4 → 0.36.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 +4 -1
- package/dist/adapters/cli.js +8 -1
- package/dist/adapters/mcp.js +1 -8
- package/dist/beryl-test-skill.js +8 -7
- package/dist/commands/accounts.js +16 -19
- package/dist/commands/auth.js +3 -0
- package/dist/commands/init.js +2 -2
- package/dist/config.js +11 -0
- package/dist/telemetry.js +28 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,9 @@ From the monorepo (development): `cd cli && npm install && npm run build && npm
|
|
|
30
30
|
latest npm release; a stale MCP server silently exposes fewer tools. Set
|
|
31
31
|
`BERYL_NO_UPDATE_CHECK=1` to opt out.
|
|
32
32
|
|
|
33
|
+
The CLI reports which commands run (never their arguments or output) so onboarding can
|
|
34
|
+
be measured; set `BERYL_TELEMETRY=0` or `DO_NOT_TRACK=1` to send nothing.
|
|
35
|
+
|
|
33
36
|
## Authenticate
|
|
34
37
|
|
|
35
38
|
```bash
|
|
@@ -329,7 +332,7 @@ Durable identities on the site under test: what an authenticated test signs in a
|
|
|
329
332
|
| --- | --- | --- |
|
|
330
333
|
| `beryl accounts list` | List the test accounts an environment's tests sign in as | `accounts_list` |
|
|
331
334
|
| `beryl accounts create` | Add a test account: the customer's own, or one Beryl signs up | `accounts_create` |
|
|
332
|
-
| `beryl accounts
|
|
335
|
+
| `beryl accounts signup <account-id>` | Store a test account's signup plan and sign it up on its environment | `accounts_signup` |
|
|
333
336
|
| `beryl accounts set-login <account-id>` | Store the sign-in plan a run replays once, plus the probe that proves it | `accounts_set_login` |
|
|
334
337
|
| `beryl accounts get-login <account-id>` | Read the stored sign-in plan, its probe, and the hash a safe write must cite | `accounts_get_login` |
|
|
335
338
|
| `beryl accounts check <account-id>` | Sign in now and prove the session survives into a fresh browser | `accounts_check` |
|
package/dist/adapters/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { CliError, EXIT_OK, EXIT_USAGE, UsageError } from "../errors.js";
|
|
|
4
4
|
import { ApiClient } from "../http.js";
|
|
5
5
|
import { autoFormat, bold, cyan, dim } from "../output.js";
|
|
6
6
|
import { commandGroups, findCommand, groupSummary } from "../registry/index.js";
|
|
7
|
-
import { withTool } from "../telemetry.js";
|
|
7
|
+
import { captureCliEvent, shutdownTelemetry, withTool, } from "../telemetry.js";
|
|
8
8
|
import { cliVersion } from "../version-check.js";
|
|
9
9
|
import { mcpToolFor } from "./mcp.js";
|
|
10
10
|
export const GLOBAL_FLAGS = [
|
|
@@ -324,6 +324,10 @@ export async function runCli(argv) {
|
|
|
324
324
|
config.token = parsed.token;
|
|
325
325
|
const client = new ApiClient(config.apiUrl, config.token);
|
|
326
326
|
const ctx = createContext({ client, config, json: parsed.json, mcp: false });
|
|
327
|
+
// The MCP server reports its own tool calls; every other invocation is one command.
|
|
328
|
+
if (spec.name !== "mcp") {
|
|
329
|
+
captureCliEvent("cli_command", config.apiUrl, { command: spec.name });
|
|
330
|
+
}
|
|
327
331
|
try {
|
|
328
332
|
const result = (await withTool(spec.name, () => spec.run(ctx, parsed.input))) ?? {};
|
|
329
333
|
if (parsed.json) {
|
|
@@ -345,4 +349,7 @@ export async function runCli(argv) {
|
|
|
345
349
|
}
|
|
346
350
|
throw err;
|
|
347
351
|
}
|
|
352
|
+
finally {
|
|
353
|
+
await shutdownTelemetry();
|
|
354
|
+
}
|
|
348
355
|
}
|
package/dist/adapters/mcp.js
CHANGED
|
@@ -250,14 +250,7 @@ export function identifyUser(baseCtx) {
|
|
|
250
250
|
if (me?.id) {
|
|
251
251
|
identity = {
|
|
252
252
|
distinctId: me.id,
|
|
253
|
-
|
|
254
|
-
// want our own MCP sessions visible when debugging, so dashboards exclude
|
|
255
|
-
// them by cohort instead.
|
|
256
|
-
properties: {
|
|
257
|
-
email: me.email,
|
|
258
|
-
name: me.name,
|
|
259
|
-
is_internal: Boolean(me.is_vibemonitor),
|
|
260
|
-
},
|
|
253
|
+
properties: { email: me.email, name: me.name },
|
|
261
254
|
};
|
|
262
255
|
}
|
|
263
256
|
}
|
package/dist/beryl-test-skill.js
CHANGED
|
@@ -154,13 +154,14 @@ An empty \`accounts list\` means nothing is set up. In order of preference:
|
|
|
154
154
|
\`beryl accounts create --email <email> --password <password>\`. A DEDICATED test
|
|
155
155
|
account only — never a real user's.
|
|
156
156
|
2. **No credentials, but the app has a signup form** → let Beryl make one.
|
|
157
|
-
\`beryl accounts create --type beryl\`, then
|
|
158
|
-
\`beryl accounts
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
157
|
+
\`beryl accounts create --type beryl\`, then sign it up with
|
|
158
|
+
\`beryl accounts signup <id> --file <signup-plan.json>\`. The plan is the app's own
|
|
159
|
+
signup form as an ActionPlan, ending LOGGED IN, filling \`{{mailbox_address}}\` and
|
|
160
|
+
\`{{login_password}}\`. It stays on the account, so a new environment signs the same
|
|
161
|
+
identity up again by itself. An account that already exists on the site (hand-made,
|
|
162
|
+
or the record was lost) skips signup: store its sign-in with \`accounts set-login\`.
|
|
163
|
+
No password sign-in? Pass \`--login-method otp\` (or \`magic_link\`) and let the plan
|
|
164
|
+
\`await_email\` through; later sign-ins read the same mailbox.
|
|
164
165
|
3. **Neither** → the flow is not testable authenticated. Say so rather than guessing.
|
|
165
166
|
|
|
166
167
|
A project with no accounts falls back to the \`LOGIN_EMAIL\` variable +
|
|
@@ -63,7 +63,7 @@ export const testAccountCommands = [
|
|
|
63
63
|
description: "Two kinds. `--type user_provided` records a dedicated account you already have on " +
|
|
64
64
|
"the site: pass --email and --password, and it is usable immediately. `--type beryl` " +
|
|
65
65
|
"reserves one Beryl will sign up itself, addressed at the project mailbox. It starts " +
|
|
66
|
-
"`pending` and becomes usable after `beryl accounts
|
|
66
|
+
"`pending` and becomes usable after `beryl accounts signup`. " +
|
|
67
67
|
"The first account an environment gets is its default whatever you pass.",
|
|
68
68
|
scope: "project",
|
|
69
69
|
flags: [
|
|
@@ -111,8 +111,8 @@ export const testAccountCommands = [
|
|
|
111
111
|
is_default: flagBool(input, "default"),
|
|
112
112
|
}));
|
|
113
113
|
const next = account.status === "pending"
|
|
114
|
-
? yellow(`\nNot
|
|
115
|
-
`with a plan that ends logged in
|
|
114
|
+
? yellow(`\nNot signed up yet. Run:\n beryl accounts signup ${account.id} --file <signup-plan.json>\n` +
|
|
115
|
+
`with the app's signup form as a plan that ends logged in.`)
|
|
116
116
|
: "";
|
|
117
117
|
return {
|
|
118
118
|
data: account,
|
|
@@ -121,15 +121,15 @@ export const testAccountCommands = [
|
|
|
121
121
|
},
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
|
-
name: "accounts
|
|
125
|
-
summary: "
|
|
126
|
-
description: "Runs the
|
|
127
|
-
"The plan is an ordinary ActionPlan
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
124
|
+
name: "accounts signup",
|
|
125
|
+
summary: "Store a test account's signup plan and sign it up on its environment",
|
|
126
|
+
description: "Runs the app's signup form once in a real browser and, if it ends logged in, marks " +
|
|
127
|
+
"the account ready. The plan is an ordinary ActionPlan of the SIGNUP flow. Type " +
|
|
128
|
+
"{{mailbox_address}} into the email field and {{login_password}} into the password " +
|
|
129
|
+
"field. Both resolve at replay time, and an `await_email` step reads the project " +
|
|
130
|
+
"mailbox, so a verification code works. The plan is kept on the account, so the " +
|
|
131
|
+
"same identity can be signed up again on a new environment. For an account that " +
|
|
132
|
+
"already exists on the site, store its sign-in with `accounts set-login` instead.",
|
|
133
133
|
scope: "project",
|
|
134
134
|
args: [{ name: "account-id", description: "Account id from `beryl accounts list`", required: true }],
|
|
135
135
|
flags: [
|
|
@@ -137,20 +137,17 @@ export const testAccountCommands = [
|
|
|
137
137
|
name: "file",
|
|
138
138
|
type: "string",
|
|
139
139
|
required: true,
|
|
140
|
-
description: "ActionPlan
|
|
140
|
+
description: "The signup ActionPlan, ending logged in, as JSON (path, or - for stdin)",
|
|
141
141
|
},
|
|
142
142
|
],
|
|
143
|
-
examples: [
|
|
144
|
-
"beryl accounts provision acc_123 --file signup-plan.json",
|
|
145
|
-
"beryl accounts provision acc_123 --file signin-plan.json",
|
|
146
|
-
],
|
|
143
|
+
examples: ["beryl accounts signup acc_123 --file signup-plan.json"],
|
|
147
144
|
async run(ctx, input) {
|
|
148
145
|
const { workspaceId, projectId } = await ctx.requireProject(input);
|
|
149
|
-
const account = (await ctx.client.post(`${accountsPath(workspaceId, projectId)}/${arg(input, "account-id")}/
|
|
146
|
+
const account = (await ctx.client.post(`${accountsPath(workspaceId, projectId)}/${arg(input, "account-id")}/signup`, { plan: readJsonFlag(input, "file") }));
|
|
150
147
|
if (account.status !== "ready")
|
|
151
148
|
return {
|
|
152
149
|
data: account,
|
|
153
|
-
human: `${yellow("The
|
|
150
|
+
human: `${yellow("The signup did not get in")}: ${account.last_error ?? account.status}`,
|
|
154
151
|
exitCode: 1,
|
|
155
152
|
};
|
|
156
153
|
return {
|
package/dist/commands/auth.js
CHANGED
|
@@ -5,6 +5,7 @@ import { deviceLogin, SWITCH_TO_OTP } from "../device-login.js";
|
|
|
5
5
|
import { CliError, UsageError } from "../errors.js";
|
|
6
6
|
import { ApiClient } from "../http.js";
|
|
7
7
|
import { dim, green } from "../output.js";
|
|
8
|
+
import { aliasMachineToAccount, flushTelemetry } from "../telemetry.js";
|
|
8
9
|
import { arg, flagBool, flagStr } from "./util.js";
|
|
9
10
|
function gitEmail() {
|
|
10
11
|
try {
|
|
@@ -172,6 +173,8 @@ export const authCommands = [
|
|
|
172
173
|
token,
|
|
173
174
|
api_url: apiUrl === "https://api.beryl.so" ? undefined : apiUrl,
|
|
174
175
|
});
|
|
176
|
+
aliasMachineToAccount(me.id);
|
|
177
|
+
flushTelemetry();
|
|
175
178
|
const human = `${green("Logged in")} as ${me.name} <${me.email}>` +
|
|
176
179
|
`\n${dim(`Token saved to ${saved}`)}`;
|
|
177
180
|
return {
|
package/dist/commands/init.js
CHANGED
|
@@ -8,7 +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 {
|
|
11
|
+
import { aliasMachineToAccount, captureCliEvent, flushTelemetry, shutdownTelemetry, } from "../telemetry.js";
|
|
12
12
|
import { cliVersion, warnIfStale } from "../version-check.js";
|
|
13
13
|
import { authCommands } from "./auth.js";
|
|
14
14
|
import { flagStr } from "./util.js";
|
|
@@ -227,7 +227,7 @@ export const initCommands = [
|
|
|
227
227
|
await signIn();
|
|
228
228
|
}
|
|
229
229
|
if (accountId) {
|
|
230
|
-
|
|
230
|
+
aliasMachineToAccount(accountId);
|
|
231
231
|
flushTelemetry();
|
|
232
232
|
}
|
|
233
233
|
const report = (label, fresh, where) => ctx.err(`${green("✓")} ${label} ${fresh ? "configured" : "already configured"} ${dim(where)}`);
|
package/dist/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
@@ -45,3 +46,13 @@ export function saveGlobalConfig(patch, env = process.env) {
|
|
|
45
46
|
}
|
|
46
47
|
return file;
|
|
47
48
|
}
|
|
49
|
+
// One id per install: every CLI event from this machine lands on it, and a login aliases
|
|
50
|
+
// it onto the account, so an install joins the same PostHog person as the signup.
|
|
51
|
+
export function machineId(env = process.env) {
|
|
52
|
+
const current = readJson(globalConfigPath(env)) ?? {};
|
|
53
|
+
if (current.machine_id)
|
|
54
|
+
return current.machine_id;
|
|
55
|
+
const id = randomUUID();
|
|
56
|
+
saveGlobalConfig({ machine_id: id }, env);
|
|
57
|
+
return id;
|
|
58
|
+
}
|
package/dist/telemetry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { PostHog } from "posthog-node";
|
|
4
|
+
import { machineId } from "./config.js";
|
|
4
5
|
import { cliVersion } from "./version-check.js";
|
|
5
6
|
// The webapp already ships this public (write-only) project key in its JS bundle, and
|
|
6
7
|
// dev/prod deploys share it — environments are separated by the `environment` property
|
|
@@ -229,22 +230,38 @@ export function baseEventProperties(apiUrl) {
|
|
|
229
230
|
};
|
|
230
231
|
}
|
|
231
232
|
let posthog;
|
|
233
|
+
export function telemetryDisabled(env = process.env) {
|
|
234
|
+
return env.BERYL_TELEMETRY === "0" || env.DO_NOT_TRACK === "1";
|
|
235
|
+
}
|
|
232
236
|
export function createPostHogClient() {
|
|
233
|
-
posthog ??= new PostHog(POSTHOG_KEY, {
|
|
237
|
+
posthog ??= new PostHog(POSTHOG_KEY, {
|
|
238
|
+
host: POSTHOG_HOST,
|
|
239
|
+
disableGeoip: false,
|
|
240
|
+
disabled: telemetryDisabled(),
|
|
241
|
+
});
|
|
234
242
|
return posthog;
|
|
235
243
|
}
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
// so the install joins the same person as the signup and the later MCP work.
|
|
244
|
+
// CLI events land on the install's machine id, which a login aliases onto the account:
|
|
245
|
+
// an install made before there is an account still joins the person who later signs up.
|
|
239
246
|
export function captureCliEvent(event, apiUrl, properties = {}) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
247
|
+
try {
|
|
248
|
+
createPostHogClient().capture({
|
|
249
|
+
distinctId: machineId(),
|
|
250
|
+
event,
|
|
251
|
+
properties: { ...baseEventProperties(apiUrl), ...properties },
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// telemetry must never break a command
|
|
256
|
+
}
|
|
245
257
|
}
|
|
246
|
-
export function
|
|
247
|
-
|
|
258
|
+
export function aliasMachineToAccount(userId) {
|
|
259
|
+
try {
|
|
260
|
+
createPostHogClient().alias({ distinctId: userId, alias: machineId() });
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// telemetry must never break a command
|
|
264
|
+
}
|
|
248
265
|
}
|
|
249
266
|
export function flushTelemetry() {
|
|
250
267
|
// Fire-and-forget: init keeps running (login, installs) while the batch goes out, and a
|
package/package.json
CHANGED