@botiverse/testbed-cli 0.4.0 → 0.5.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.
Files changed (2) hide show
  1. package/lib/main.mjs +49 -0
  2. package/package.json +1 -1
package/lib/main.mjs CHANGED
@@ -37,6 +37,12 @@ export const USAGE = `testbed — cloud phones + the acceptance testbed in the R
37
37
  install for any runtime via "npx skills add" (Claude Code, Codex, Cursor, Gemini, OpenCode, ...)
38
38
  testbed skill --copy-to DIR plain copy of SKILL.md (offline / unsupported runtime)
39
39
 
40
+ testbed qa-account list QA account pool (labels, environments; never secrets)
41
+ testbed qa-account get <label> [--env staging|production] [--out FILE]
42
+ one QA login for a device; credential goes to --out (0600) or --json
43
+ testbed qa-account add <label> --env staging|production --username EMAIL
44
+ owner only; password is read from stdin, never from an argument
45
+
40
46
  testbed cases acceptance case library
41
47
  testbed run <case-id...> [--app --ref --apk --apk-run --release --by]
42
48
  testbed run --all [...] the whole case table in one run (release smoke)
@@ -66,6 +72,11 @@ function table(rows, cols, out) {
66
72
  for (const r of rows) out(cols.map((c, i) => String(r[c] ?? "").padEnd(w[i])).join(" "));
67
73
  }
68
74
 
75
+ async function readStdin(stream = process.stdin) {
76
+ if (stream.isTTY) return "";
77
+ let data = ""; for await (const chunk of stream) data += chunk; return data;
78
+ }
79
+
69
80
  function writeKeyFile(path, key) {
70
81
  const fd = openSync(path, "wx", 0o600);
71
82
  try { writeSync(fd, key.endsWith("\n") ? key : key + "\n"); } finally { closeSync(fd); }
@@ -292,6 +303,44 @@ export async function main(argv, opts) {
292
303
  out(`installed skill 'testbed' via npx skills${agents.length ? ` for ${agents.join(", ")}` : ""}${global ? " (global)" : ""}`);
293
304
  return exit(0);
294
305
  }
306
+ case "qa-account": {
307
+ const sub = args.shift();
308
+ switch (sub) {
309
+ case "list": {
310
+ const d = await api("GET", "/device-broker/qa-accounts");
311
+ const rows = d.accounts ?? [];
312
+ emit(d, () => rows.length
313
+ ? table(rows.map((a) => ({ env: a.environment, label: a.label, email: a.login_email ?? "-", visibility: a.visibility ?? "-", status: a.status ?? "-" })), ["env", "label", "email", "visibility", "status"], out)
314
+ : out("no QA accounts visible to you"));
315
+ return exit(0);
316
+ }
317
+ case "get": {
318
+ const label = args[0]; if (!label) throw new Error("usage: testbed qa-account get <label> [--env ENV] [--out FILE]");
319
+ const envName = takeFlag(args, "env"); const outPath = takeFlag(args, "out");
320
+ const q = envName ? `?environment=${encodeURIComponent(envName)}` : "";
321
+ const d = await api("GET", `/device-broker/qa-account/${encodeURIComponent(label)}${q}`);
322
+ if (json) { out(JSON.stringify(d, null, 2)); return exit(0); }
323
+ if (outPath) {
324
+ writeKeyFile(outPath, `${d.username ?? d.login_email ?? ""}\n${d.password ?? ""}\n`);
325
+ out(`qa account ${d.label ?? label} (${d.environment ?? envName ?? "?"}): username ${d.username ?? d.login_email ?? "?"}; credential written to ${outPath} (0600)`);
326
+ } else {
327
+ out(`qa account ${d.label ?? label} (${d.environment ?? envName ?? "?"}): username ${d.username ?? d.login_email ?? "?"}; password not shown — pass --out FILE or --json`);
328
+ }
329
+ return exit(0);
330
+ }
331
+ case "add": {
332
+ const label = args[0]; const envName = takeFlag(args, "env"); const username = takeFlag(args, "username");
333
+ if (!label || !envName || !username) throw new Error("usage: testbed qa-account add <label> --env ENV --username EMAIL (password on stdin)");
334
+ const password = (await readStdin(opts.stdin)).replace(/\r?\n$/, "");
335
+ if (!password) throw new Error("password must be provided on stdin (e.g. printf '%s' \"$PW\" | testbed qa-account add …)");
336
+ const d = await api("PUT", `/device-broker/qa-account/${encodeURIComponent(label)}`, { environment: envName, username, password });
337
+ emit(d, () => out(`qa account ${d.label} (${d.environment}) ${d.secret_version > 1 ? "rotated" : "created"}, secret version ${d.secret_version}`));
338
+ return exit(0);
339
+ }
340
+ default:
341
+ throw new Error("usage: testbed qa-account <list|get|add> …");
342
+ }
343
+ }
295
344
  case "cases": {
296
345
  const d = await api("GET", "/cases");
297
346
  emit(d, () => table(d.cases.map((c) => ({ id: c.id, tier: c.tier, rev: c.revision, title: c.title })), ["id", "tier", "rev", "title"], out));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/testbed-cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "CLI for testbed \u2014 cloud phones and the acceptance testbed in the Raft release loop",
5
5
  "license": "MIT",
6
6
  "type": "module",