vibeview 0.13.0 → 0.14.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.
@@ -297,6 +297,13 @@ var ApiClient = class {
297
297
  async cancelCloudBuild(id) {
298
298
  return this.requestRaw("POST", `/builds/${id}/cancel`);
299
299
  }
300
+ /** DELETE /api/v1/builds/{id}/artifact — deletes a production build's
301
+ * stored artifact file. The build record, its logs, and its cost stay
302
+ * intact; this only removes the downloadable file. 409 if the build has
303
+ * no artifact (never had one, or already deleted). */
304
+ async deleteCloudBuildArtifact(id) {
305
+ return this.requestRaw("DELETE", `/builds/${id}/artifact`);
306
+ }
300
307
  /**
301
308
  * Download a finished build's artifact (GET /api/v1/builds/{id}/artifact),
302
309
  * streaming the response body straight to a file in destDir. The filename
@@ -363,6 +370,10 @@ var ApiClient = class {
363
370
  formData.append("file", blob, body.filename);
364
371
  return this.requestFormData("POST", "/build-env/", formData);
365
372
  }
373
+ /** GET /api/v1/users/me — identity + roles for `vibeview whoami`. */
374
+ async whoAmI() {
375
+ return this.requestRaw("GET", "/users/me");
376
+ }
366
377
  /** DELETE /api/v1/build-env/{id}. Requires ADMIN role. */
367
378
  async deleteBuildEnvVar(id) {
368
379
  await this.requestRaw("DELETE", `/build-env/${id}`);
@@ -374,6 +385,44 @@ var ApiClient = class {
374
385
  const qs = app ? `?app=${encodeURIComponent(app)}` : "";
375
386
  return this.requestRaw("GET", `/build-env/pull${qs}`);
376
387
  }
388
+ /** GET /api/v1/signing-credentials/[?app=]. Requires VIEWER role. Never
389
+ * returns file bytes, profile bytes, or a password — see
390
+ * routes/signing.py's module docstring. */
391
+ async listSigningCredentials(app) {
392
+ const qs = app ? `?app=${encodeURIComponent(app)}` : "";
393
+ return this.requestRaw("GET", `/signing-credentials/${qs}`);
394
+ }
395
+ /**
396
+ * POST /api/v1/signing-credentials/ (multipart). Requires ADMIN role.
397
+ * `passwords` carries the route's own snake_case form field names
398
+ * (store_password, key_password, key_alias, p12_password) — the CLI's own
399
+ * camelCase objects are converted to this shape ONLY at this boundary
400
+ * (see credentials.ts), never anywhere else, so there is exactly one place
401
+ * the two naming conventions meet.
402
+ */
403
+ async addSigningCredential(body) {
404
+ const formData = new FormData();
405
+ formData.append("platform", body.platform);
406
+ formData.append("kind", body.kind);
407
+ formData.append("name", body.name);
408
+ if (body.app) formData.append("app", body.app);
409
+ for (const [key, value] of Object.entries(body.passwords)) {
410
+ formData.append(key, value);
411
+ }
412
+ formData.append("file", new Blob([new Uint8Array(body.file)]), body.filename);
413
+ if (body.profile) {
414
+ formData.append(
415
+ "profile",
416
+ new Blob([new Uint8Array(body.profile)]),
417
+ body.profileFilename ?? "profile"
418
+ );
419
+ }
420
+ return this.requestFormData("POST", "/signing-credentials/", formData);
421
+ }
422
+ /** DELETE /api/v1/signing-credentials/{id}. Requires ADMIN role. */
423
+ async deleteSigningCredential(id) {
424
+ await this.requestRaw("DELETE", `/signing-credentials/${id}`);
425
+ }
377
426
  };
378
427
  function parseContentDispositionFilename(header) {
379
428
  if (!header) return void 0;
@@ -410,6 +459,12 @@ function createSpinner(text) {
410
459
  var EXIT_PASS = 0;
411
460
  var EXIT_TEST_FAIL = 1;
412
461
  var EXIT_INFRA_ERROR = 2;
462
+ var UsageError = class extends Error {
463
+ constructor(message) {
464
+ super(message);
465
+ this.name = "UsageError";
466
+ }
467
+ };
413
468
 
414
469
  // src/commands/upload-app.ts
415
470
  async function uploadAppBuild(filePath, options) {
@@ -480,6 +535,7 @@ export {
480
535
  EXIT_PASS,
481
536
  EXIT_TEST_FAIL,
482
537
  EXIT_INFRA_ERROR,
538
+ UsageError,
483
539
  uploadAppBuild,
484
540
  uploadApp
485
541
  };
package/dist/cli.js CHANGED
@@ -13,19 +13,20 @@ import {
13
13
  readDevState,
14
14
  readMetroState,
15
15
  runAgentVerb
16
- } from "./chunk-YFXMXO5T.js";
16
+ } from "./chunk-J64ENS7W.js";
17
17
  import {
18
18
  ApiClient,
19
19
  EXIT_INFRA_ERROR,
20
20
  EXIT_PASS,
21
21
  EXIT_TEST_FAIL,
22
22
  NotFoundError,
23
+ UsageError,
23
24
  createSpinner,
24
25
  loadConfig,
25
26
  resolveAuth,
26
27
  saveConfig,
27
28
  uploadApp
28
- } from "./chunk-J7V3GYOI.js";
29
+ } from "./chunk-KUU4IUZU.js";
29
30
 
30
31
  // src/cli.ts
31
32
  import { createRequire } from "module";
@@ -909,9 +910,9 @@ async function resolveSteps(options) {
909
910
  return JSON.parse(readFileSync(options.stepsFile, "utf-8"));
910
911
  }
911
912
  if (!process.stdin.isTTY) {
912
- const input = await readStdin();
913
- if (input.trim()) {
914
- return JSON.parse(input.trim());
913
+ const input2 = await readStdin();
914
+ if (input2.trim()) {
915
+ return JSON.parse(input2.trim());
915
916
  }
916
917
  }
917
918
  throw new Error("No steps provided. Use --steps, --steps-file, or pipe JSON to stdin.");
@@ -1190,7 +1191,8 @@ function defaultDeps(client) {
1190
1191
  getCloudBuild: client.getCloudBuild.bind(client),
1191
1192
  getCloudBuildLogs: client.getCloudBuildLogs.bind(client),
1192
1193
  cancelCloudBuild: client.cancelCloudBuild.bind(client),
1193
- downloadCloudBuildArtifact: client.downloadCloudBuildArtifact.bind(client)
1194
+ downloadCloudBuildArtifact: client.downloadCloudBuildArtifact.bind(client),
1195
+ deleteCloudBuildArtifact: client.deleteCloudBuildArtifact.bind(client)
1194
1196
  };
1195
1197
  }
1196
1198
  var POLL_INTERVAL_MS = 2e3;
@@ -1347,16 +1349,91 @@ async function buildsCancel(id, opts, deps) {
1347
1349
  process.exitCode = 1;
1348
1350
  }
1349
1351
  }
1352
+ async function buildsDeleteArtifact(id, opts, deps) {
1353
+ const { token, baseUrl } = resolveAuth(opts);
1354
+ const client = new ApiClient(baseUrl, token);
1355
+ const d = deps ?? defaultDeps(client);
1356
+ try {
1357
+ const result = await d.deleteCloudBuildArtifact(id);
1358
+ if (opts.json) {
1359
+ console.log(JSON.stringify(result));
1360
+ } else {
1361
+ console.log(chalk11.green(`\u2713 deleted artifact for ${id}`));
1362
+ }
1363
+ process.exitCode = 0;
1364
+ } catch (err) {
1365
+ printError(opts, err);
1366
+ process.exitCode = 1;
1367
+ }
1368
+ }
1350
1369
  function printError(opts, err) {
1351
1370
  const message = err instanceof Error ? err.message : String(err);
1352
1371
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
1353
1372
  console.error(chalk11.red(`error: ${message}`));
1354
1373
  }
1355
1374
 
1375
+ // src/commands/whoami.ts
1376
+ import chalk12 from "chalk";
1377
+ function defaultDeps2(opts) {
1378
+ const { token, baseUrl } = resolveAuth(opts);
1379
+ const client = new ApiClient(baseUrl, token);
1380
+ return { fetchMe: () => client.whoAmI() };
1381
+ }
1382
+ function effectiveRole(role, tokenRole) {
1383
+ const RANK = { admin: 3, developer: 2, viewer: 1 };
1384
+ if (!role) return tokenRole;
1385
+ if (!tokenRole) return role;
1386
+ return (RANK[tokenRole] ?? 3) < (RANK[role] ?? 3) ? tokenRole : role;
1387
+ }
1388
+ async function whoAmI(opts, deps) {
1389
+ const d = deps ?? defaultDeps2(opts);
1390
+ try {
1391
+ const me = await d.fetchMe();
1392
+ const effective = effectiveRole(me.role, me.token_role);
1393
+ if (opts.json) {
1394
+ console.log(JSON.stringify({ ...me, effective_role: effective }));
1395
+ process.exitCode = 0;
1396
+ return;
1397
+ }
1398
+ console.log(`${chalk12.bold("Logged in as")} ${me.email}`);
1399
+ if (me.memberships.length === 1) {
1400
+ console.log(`${chalk12.bold("Organization")} ${me.memberships[0].organization_name}`);
1401
+ } else if (me.memberships.length > 1) {
1402
+ console.log(chalk12.bold("Organizations"));
1403
+ for (const m of me.memberships) {
1404
+ console.log(` ${m.organization_name} (${m.role})`);
1405
+ }
1406
+ }
1407
+ console.log(`${chalk12.bold("Member role")} ${me.role ?? "\u2014"}`);
1408
+ console.log(`${chalk12.bold("Token role")} ${me.token_role ?? "\u2014 (session auth)"}`);
1409
+ console.log(`${chalk12.bold("Effective role")} ${effective ?? "\u2014"}`);
1410
+ if (me.is_superadmin) {
1411
+ console.log(chalk12.dim("Superadmin (platform-wide; does not raise the org role above)"));
1412
+ }
1413
+ if (me.role === "admin" && me.token_role && me.token_role !== "admin") {
1414
+ console.log(
1415
+ chalk12.yellow(
1416
+ `
1417
+ Your account is admin, but this CLI token is ${me.token_role}. The effective role is the lower of the two, so admin-only commands (such as ${chalk12.bold("vibeview env set")}) will fail with a permissions error.
1418
+ Create an admin API token in the dashboard and use ${chalk12.bold("--token")} or ${chalk12.bold("VIBEVIEW_API_TOKEN")}.`
1419
+ )
1420
+ );
1421
+ }
1422
+ process.exitCode = 0;
1423
+ } catch (err) {
1424
+ const message = err instanceof Error ? err.message : String(err);
1425
+ if (opts.json) {
1426
+ console.log(JSON.stringify({ event: "error", message }));
1427
+ }
1428
+ console.error(chalk12.red(`error: ${message}`));
1429
+ process.exitCode = 1;
1430
+ }
1431
+ }
1432
+
1356
1433
  // src/commands/env.ts
1357
1434
  import { chmod, open as open2, readFile as readFileAsync } from "fs/promises";
1358
1435
  import { basename } from "path";
1359
- import chalk12 from "chalk";
1436
+ import chalk13 from "chalk";
1360
1437
  import { password } from "@inquirer/prompts";
1361
1438
 
1362
1439
  // src/lib/gitignore.ts
@@ -1383,7 +1460,7 @@ async function writeSecureFile(path, content) {
1383
1460
  }
1384
1461
  await chmod(path, 384);
1385
1462
  }
1386
- function defaultDeps2(client) {
1463
+ function defaultDeps3(client) {
1387
1464
  return {
1388
1465
  listVars: (app) => client.listBuildEnvVars(app),
1389
1466
  setVar: (body) => client.setBuildEnvVar(body),
@@ -1412,11 +1489,11 @@ function defaultDeps2(client) {
1412
1489
  isIgnored: (path) => isGitIgnored(path),
1413
1490
  writeFile: writeSecureFile,
1414
1491
  readFile: (path) => readFileAsync(path),
1415
- warn: (message) => console.error(chalk12.yellow(`warning: ${message}`))
1492
+ warn: (message) => console.error(chalk13.yellow(`warning: ${message}`))
1416
1493
  };
1417
1494
  }
1418
1495
  async function envList(opts, deps) {
1419
- const d = deps ?? defaultDeps2(clientFor(opts));
1496
+ const d = deps ?? defaultDeps3(clientFor(opts));
1420
1497
  try {
1421
1498
  const { variables } = await d.listVars(opts.app);
1422
1499
  if (opts.json) {
@@ -1444,12 +1521,12 @@ function formatEnvVarsHuman(variables) {
1444
1521
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1445
1522
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
1446
1523
  const join2 = (cells) => cells.join(" ").trimEnd();
1447
- const lines = [chalk12.bold(join2(padCells(HEADERS3)))];
1524
+ const lines = [chalk13.bold(join2(padCells(HEADERS3)))];
1448
1525
  for (const row of rows) lines.push(join2(padCells(row)));
1449
1526
  return lines.join("\n") + "\n";
1450
1527
  }
1451
1528
  async function envSet(name, value, opts, deps) {
1452
- const d = deps ?? defaultDeps2(clientFor(opts));
1529
+ const d = deps ?? defaultDeps3(clientFor(opts));
1453
1530
  let resolvedValue = value;
1454
1531
  if (resolvedValue === void 0 && !opts.file) {
1455
1532
  if (opts.json || !d.isTTY) {
@@ -1494,17 +1571,17 @@ function printSetResult(opts, result) {
1494
1571
  if (opts.json) {
1495
1572
  console.log(JSON.stringify(result));
1496
1573
  } else {
1497
- console.log(chalk12.green(`\u2713 set ${result.name} (${result.visibility})`));
1574
+ console.log(chalk13.green(`\u2713 set ${result.name} (${result.visibility})`));
1498
1575
  }
1499
1576
  }
1500
1577
  async function envRm(name, opts, deps) {
1501
- const d = deps ?? defaultDeps2(clientFor(opts));
1578
+ const d = deps ?? defaultDeps3(clientFor(opts));
1502
1579
  try {
1503
1580
  await d.rmVar({ name, app: opts.app });
1504
1581
  if (opts.json) {
1505
1582
  console.log(JSON.stringify({ event: "deleted", name }));
1506
1583
  } else {
1507
- console.log(chalk12.green(`\u2713 removed ${name}`));
1584
+ console.log(chalk13.green(`\u2713 removed ${name}`));
1508
1585
  }
1509
1586
  process.exitCode = 0;
1510
1587
  } catch (err) {
@@ -1513,7 +1590,7 @@ async function envRm(name, opts, deps) {
1513
1590
  }
1514
1591
  }
1515
1592
  async function envPull(opts, deps) {
1516
- const d = deps ?? defaultDeps2(clientFor(opts));
1593
+ const d = deps ?? defaultDeps3(clientFor(opts));
1517
1594
  const outPath = opts.out ?? ".env";
1518
1595
  try {
1519
1596
  const { variables } = await d.pullVars(opts.app);
@@ -1536,7 +1613,7 @@ async function envPull(opts, deps) {
1536
1613
  if (opts.json) {
1537
1614
  console.log(JSON.stringify({ event: "pulled", path: outPath, count: variables.length }));
1538
1615
  } else {
1539
- console.log(chalk12.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
1616
+ console.log(chalk13.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
1540
1617
  }
1541
1618
  process.exitCode = 0;
1542
1619
  } catch (err) {
@@ -1575,11 +1652,187 @@ function escapeEnvValue(value) {
1575
1652
  function printError2(opts, err) {
1576
1653
  const message = err instanceof Error ? err.message : String(err);
1577
1654
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
1578
- console.error(chalk12.red(`error: ${message}`));
1655
+ console.error(chalk13.red(`error: ${message}`));
1656
+ }
1657
+
1658
+ // src/commands/credentials.ts
1659
+ import { readFile as readFileAsync2 } from "fs/promises";
1660
+ import { basename as basename2, extname } from "path";
1661
+ import chalk14 from "chalk";
1662
+ import { input, password as password2 } from "@inquirer/prompts";
1663
+ function clientFor2(opts) {
1664
+ const { token, baseUrl } = resolveAuth(opts);
1665
+ return new ApiClient(baseUrl, token);
1666
+ }
1667
+ function defaultDeps4(client) {
1668
+ return {
1669
+ addCredential: (body) => client.addSigningCredential({
1670
+ platform: body.platform,
1671
+ kind: body.kind,
1672
+ name: body.name,
1673
+ app: body.app,
1674
+ // Convert to the route's snake_case form fields HERE, at the client
1675
+ // boundary, and nowhere else — see api-client.ts's
1676
+ // addSigningCredential doc comment.
1677
+ passwords: {
1678
+ ...body.storePassword !== void 0 ? { store_password: body.storePassword } : {},
1679
+ ...body.keyPassword !== void 0 ? { key_password: body.keyPassword } : {},
1680
+ ...body.keyAlias !== void 0 ? { key_alias: body.keyAlias } : {},
1681
+ ...body.p12Password !== void 0 ? { p12_password: body.p12Password } : {}
1682
+ },
1683
+ file: body.file,
1684
+ filename: body.filename,
1685
+ profile: body.profile,
1686
+ profileFilename: body.profileFilename
1687
+ }),
1688
+ listCredentials: (app) => client.listSigningCredentials(app),
1689
+ deleteCredential: async ({ name, app }) => {
1690
+ const { credentials: credentials2 } = await client.listSigningCredentials(app);
1691
+ const wantApp = app ?? null;
1692
+ const match = credentials2.find(
1693
+ (c) => c.name === name && (c.app_public_id ?? null) === wantApp
1694
+ );
1695
+ if (!match) {
1696
+ throw new Error(`No credential named "${name}" found${app ? ` for app "${app}"` : ""}.`);
1697
+ }
1698
+ await client.deleteSigningCredential(match.id);
1699
+ },
1700
+ prompt: ({ message, hidden }) => hidden ? password2({ message }) : input({ message }),
1701
+ isTTY: Boolean(process.stdin.isTTY),
1702
+ readFile: (path) => readFileAsync2(path),
1703
+ warn: (message) => console.error(chalk14.yellow(`warning: ${message}`))
1704
+ };
1705
+ }
1706
+ var ANDROID_KIND = "android_keystore";
1707
+ var IOS_KIND = "ios_distribution";
1708
+ async function credentialsAdd(opts, deps) {
1709
+ const d = deps ?? defaultDeps4(clientFor2(opts));
1710
+ if (!opts.platform || opts.platform !== "ios" && opts.platform !== "android") {
1711
+ throw new UsageError('--platform is required and must be "ios" or "android"');
1712
+ }
1713
+ if (!opts.file) {
1714
+ throw new UsageError("--file is required");
1715
+ }
1716
+ if (opts.platform === "ios" && !opts.profile) {
1717
+ throw new UsageError("--profile is required for ios credentials (a .mobileprovision file)");
1718
+ }
1719
+ const canPrompt = !opts.json && d.isTTY;
1720
+ if (!canPrompt) {
1721
+ throw new UsageError(
1722
+ "a password is required: run interactively to be prompted (passwords are never accepted as flags)."
1723
+ );
1724
+ }
1725
+ const name = opts.name ?? basename2(opts.file, extname(opts.file));
1726
+ try {
1727
+ const file = await d.readFile(opts.file);
1728
+ const filename = basename2(opts.file);
1729
+ let body;
1730
+ if (opts.platform === "android") {
1731
+ const storePassword = await d.prompt({ message: "Keystore (store) password", hidden: true });
1732
+ const keyPassword = await d.prompt({ message: "Key password", hidden: true });
1733
+ const keyAlias = await d.prompt({ message: "Key alias", hidden: false });
1734
+ body = {
1735
+ platform: "android",
1736
+ kind: ANDROID_KIND,
1737
+ name,
1738
+ app: opts.app,
1739
+ storePassword,
1740
+ keyPassword,
1741
+ keyAlias,
1742
+ file,
1743
+ filename
1744
+ };
1745
+ } else {
1746
+ const p12Password = await d.prompt({ message: ".p12 password", hidden: true });
1747
+ const profile = await d.readFile(opts.profile);
1748
+ body = {
1749
+ platform: "ios",
1750
+ kind: IOS_KIND,
1751
+ name,
1752
+ app: opts.app,
1753
+ p12Password,
1754
+ file,
1755
+ filename,
1756
+ profile,
1757
+ profileFilename: basename2(opts.profile)
1758
+ };
1759
+ }
1760
+ const result = await d.addCredential(body);
1761
+ if (opts.json) {
1762
+ console.log(JSON.stringify(result));
1763
+ } else {
1764
+ console.log(chalk14.green(`\u2713 added ${result.name} (${result.platform})`));
1765
+ }
1766
+ process.exitCode = 0;
1767
+ } catch (err) {
1768
+ printError3(opts, err);
1769
+ process.exitCode = 1;
1770
+ }
1771
+ }
1772
+ async function credentialsList(opts, deps) {
1773
+ const d = deps ?? defaultDeps4(clientFor2(opts));
1774
+ try {
1775
+ const { credentials: credentials2 } = await d.listCredentials(opts.app);
1776
+ for (const c of credentials2) {
1777
+ if (c.expires_soon) {
1778
+ d.warn(
1779
+ `${c.name} (${c.platform}) is expiring soon${c.expires_at ? ` \u2014 ${c.expires_at}` : ""}.`
1780
+ );
1781
+ }
1782
+ }
1783
+ if (opts.json) {
1784
+ console.log(JSON.stringify(credentials2, null, 2));
1785
+ } else {
1786
+ process.stdout.write(formatCredentialsHuman(credentials2));
1787
+ }
1788
+ process.exitCode = 0;
1789
+ } catch (err) {
1790
+ printError3(opts, err);
1791
+ process.exitCode = 1;
1792
+ }
1793
+ }
1794
+ function formatCredentialsHuman(credentials2) {
1795
+ if (credentials2.length === 0) {
1796
+ return "No signing credentials found.\n";
1797
+ }
1798
+ const HEADERS3 = ["NAME", "PLATFORM", "SCOPE", "FINGERPRINT", "EXPIRES"];
1799
+ const rows = credentials2.map((c) => [
1800
+ c.name,
1801
+ c.platform,
1802
+ c.app_public_id ?? "org-wide",
1803
+ c.fingerprint ?? "",
1804
+ c.expires_at ? c.expires_soon ? `${c.expires_at} (soon)` : c.expires_at : ""
1805
+ ]);
1806
+ const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1807
+ const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
1808
+ const join2 = (cells) => cells.join(" ").trimEnd();
1809
+ const lines = [chalk14.bold(join2(padCells(HEADERS3)))];
1810
+ for (const row of rows) lines.push(join2(padCells(row)));
1811
+ return lines.join("\n") + "\n";
1812
+ }
1813
+ async function credentialsRm(name, opts, deps) {
1814
+ const d = deps ?? defaultDeps4(clientFor2(opts));
1815
+ try {
1816
+ await d.deleteCredential({ name, app: opts.app });
1817
+ if (opts.json) {
1818
+ console.log(JSON.stringify({ event: "deleted", name }));
1819
+ } else {
1820
+ console.log(chalk14.green(`\u2713 removed ${name}`));
1821
+ }
1822
+ process.exitCode = 0;
1823
+ } catch (err) {
1824
+ printError3(opts, err);
1825
+ process.exitCode = 1;
1826
+ }
1827
+ }
1828
+ function printError3(opts, err) {
1829
+ const message = err instanceof Error ? err.message : String(err);
1830
+ if (opts.json) console.log(JSON.stringify({ event: "error", message }));
1831
+ console.error(chalk14.red(`error: ${message}`));
1579
1832
  }
1580
1833
 
1581
1834
  // src/lib/formatters/devices.ts
1582
- import chalk13 from "chalk";
1835
+ import chalk15 from "chalk";
1583
1836
  var HEADERS2 = ["DEVICE_ID", "NAME", "TYPE", "CATEGORY", "OS", "STATUS"];
1584
1837
  function formatDevicesHuman(devices) {
1585
1838
  if (devices.length === 0) {
@@ -1595,9 +1848,9 @@ function formatDevicesHuman(devices) {
1595
1848
  ]);
1596
1849
  const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1597
1850
  const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
1598
- const lines = [chalk13.bold(fmt(HEADERS2))];
1851
+ const lines = [chalk15.bold(fmt(HEADERS2))];
1599
1852
  for (const row of rows.sort(rowSort)) {
1600
- const statusColored = row[5] === "available" ? chalk13.green(row[5]) : row[5] === "busy" ? chalk13.yellow(row[5]) : chalk13.dim(row[5]);
1853
+ const statusColored = row[5] === "available" ? chalk15.green(row[5]) : row[5] === "busy" ? chalk15.yellow(row[5]) : chalk15.dim(row[5]);
1601
1854
  lines.push(fmt([row[0], row[1], row[2], row[3], row[4], statusColored]));
1602
1855
  }
1603
1856
  return lines.join("\n") + "\n";
@@ -1687,7 +1940,7 @@ import { dirname, join } from "path";
1687
1940
  import { fileURLToPath } from "url";
1688
1941
  import { spawnSync } from "child_process";
1689
1942
  import { confirm } from "@inquirer/prompts";
1690
- import chalk14 from "chalk";
1943
+ import chalk16 from "chalk";
1691
1944
  function resolveShippedSkillPath(fromDir) {
1692
1945
  for (const rel of ["..", join("..", "..")]) {
1693
1946
  const candidate = join(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
@@ -1715,23 +1968,23 @@ function registerMcp() {
1715
1968
  encoding: "utf8"
1716
1969
  });
1717
1970
  if (res.status === 0) {
1718
- console.log(chalk14.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
1971
+ console.log(chalk16.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
1719
1972
  return;
1720
1973
  }
1721
1974
  const out = `${res.stderr ?? ""}${res.stdout ?? ""}`;
1722
1975
  if (out.includes("already exists")) {
1723
- console.log(chalk14.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
1976
+ console.log(chalk16.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
1724
1977
  return;
1725
1978
  }
1726
- console.error(chalk14.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
1727
- console.error(chalk14.yellow(" register it manually: claude mcp add vibeview -- vibeview mcp"));
1979
+ console.error(chalk16.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
1980
+ console.error(chalk16.yellow(" register it manually: claude mcp add vibeview -- vibeview mcp"));
1728
1981
  }
1729
1982
  async function agentSetup(opts) {
1730
1983
  const moduleDir = dirname(fileURLToPath(import.meta.url));
1731
1984
  const shipped = resolveShippedSkillPath(moduleDir);
1732
1985
  if (!shipped) {
1733
1986
  console.error(
1734
- chalk14.red(
1987
+ chalk16.red(
1735
1988
  "error: could not find the packaged skill (skills/vibeview-agent/SKILL.md) \u2014 is this a complete install of the vibeview package?"
1736
1989
  )
1737
1990
  );
@@ -1740,9 +1993,9 @@ async function agentSetup(opts) {
1740
1993
  }
1741
1994
  const targetDir = skillInstallDir(opts.project ?? false, process.cwd(), homedir());
1742
1995
  const result = installSkill(shipped, targetDir);
1743
- console.log(chalk14.green(`\u2713 vibeview-agent skill ${result}: ${join(targetDir, "SKILL.md")}`));
1996
+ console.log(chalk16.green(`\u2713 vibeview-agent skill ${result}: ${join(targetDir, "SKILL.md")}`));
1744
1997
  console.log(
1745
- chalk14.dim(
1998
+ chalk16.dim(
1746
1999
  opts.project ? " Claude Code discovers it automatically for anyone working in this project." : " Claude Code discovers it automatically in every project on this machine."
1747
2000
  )
1748
2001
  );
@@ -1750,7 +2003,7 @@ async function agentSetup(opts) {
1750
2003
  if (!claudeCliAvailable()) {
1751
2004
  if (opts.mcp === true) {
1752
2005
  console.error(
1753
- chalk14.yellow(
2006
+ chalk16.yellow(
1754
2007
  "\u26A0 claude CLI not found \u2014 install Claude Code, then run: claude mcp add vibeview -- vibeview mcp"
1755
2008
  )
1756
2009
  );
@@ -1764,7 +2017,7 @@ async function agentSetup(opts) {
1764
2017
  }
1765
2018
  console.log(
1766
2019
  `
1767
- Suggested line for your project's ${chalk14.bold("CLAUDE.md")} (or ${chalk14.bold(
2020
+ Suggested line for your project's ${chalk16.bold("CLAUDE.md")} (or ${chalk16.bold(
1768
2021
  "AGENTS.md"
1769
2022
  )} for other agents):
1770
2023
  `
@@ -1772,7 +2025,7 @@ Suggested line for your project's ${chalk14.bold("CLAUDE.md")} (or ${chalk14.bol
1772
2025
  console.log(` ${CLAUDE_MD_SUGGESTION}
1773
2026
  `);
1774
2027
  console.log(
1775
- chalk14.dim(
2028
+ chalk16.dim(
1776
2029
  "Other agents (Cursor, Copilot, Codex, ...): point them at the skill file above, or\nadd the MCP server per client \u2014 see https://vibeview.io/docs/agent-control"
1777
2030
  )
1778
2031
  );
@@ -1931,12 +2184,20 @@ program.command("build").description("Run your configured build command and uplo
1931
2184
  "--download",
1932
2185
  "--cloud only: download the artifact of each succeeded build (implies wait)",
1933
2186
  false
2187
+ ).option(
2188
+ "--production",
2189
+ "--cloud only: build a signed, installable production artifact (.aab/.ipa) using uploaded signing credentials (see `vibeview credentials add`), instead of the default unsigned simulator build",
2190
+ false
1934
2191
  ).option("--json", "Print the result as one JSON line (NDJSON events with --cloud)", false).action((opts) => buildCommand(opts));
1935
2192
  var builds = program.command("builds").description("Manage cloud builds (see `vibeview build --cloud`)");
1936
2193
  builds.command("list").description("List cloud builds in your organization").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the raw build summaries as a JSON array", false).option("--limit <n>", "Max builds to return (default 50, max 200)").action((opts) => buildsList(opts));
1937
2194
  builds.command("logs <id>").description("Print a cloud build's log output").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print NDJSON log/outcome events instead of raw text", false).option("--follow", "Keep polling until the build reaches a terminal status", false).action((id, opts) => buildsLogs(id, opts));
1938
2195
  builds.command("download <id>").description("Download a succeeded cloud build's artifact").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the saved filename/path as one JSON line", false).option("--out <dir>", "Destination directory (default: current directory)").action((id, opts) => buildsDownload(id, opts));
1939
2196
  builds.command("cancel <id>").description("Cancel a queued or running cloud build").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the resulting build summary as one JSON line", false).action((id, opts) => buildsCancel(id, opts));
2197
+ builds.command("delete-artifact <id>").description(
2198
+ "Delete a production build's stored artifact file (irreversible; the build record, logs, and cost stay intact \u2014 only the downloadable file is removed)"
2199
+ ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the result as one JSON line", false).action((id, opts) => buildsDeleteArtifact(id, opts));
2200
+ program.command("whoami").description("Show which account and role this CLI is authenticated as").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the identity as one JSON line", false).action((opts) => whoAmI(opts));
1940
2201
  var env = program.command("env").description("Manage cloud build environment variables and secrets");
1941
2202
  env.command("list").description("List build variables and secrets (never prints a value)").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--app <id>", "Scope to a single app (default: org-wide variables only)").option("--json", "Print the raw variable list as a JSON array", false).action((opts) => envList(opts));
1942
2203
  env.command("set <name> [value]").description(
@@ -1949,12 +2210,18 @@ env.command("rm <name>").description("Remove a build variable or secret").option
1949
2210
  env.command("pull").description(
1950
2211
  "Write plain/sensitive variable values to a .env file (secrets and files are never included)"
1951
2212
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--app <id>", "Scope to a single app (default: org-wide variables only)").option("--out <path>", "Output file path (default: .env)").option("--json", "Print the result as one JSON line", false).action((opts) => envPull(opts));
2213
+ var credentials = program.command("credentials").description("Manage app-signing credentials for production cloud builds");
2214
+ credentials.command("add").description(
2215
+ "Add a signing credential. Prompts for passwords with hidden input \u2014 they are never accepted as flags (a flag lands in shell history and in `ps`)."
2216
+ ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--platform <platform>", "ios or android").option("--file <path>", "The keystore (.jks) or certificate (.p12) file").option("--profile <path>", "ios only: the .mobileprovision file (required for ios)").option("--app <id>", "Scope to a single app (default: org-wide)").option("--name <name>", "Credential name (default: the file basename without extension)").action((opts) => credentialsAdd(opts));
2217
+ credentials.command("list").description("List signing credentials (never prints key material)").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--app <id>", "Scope to a single app (default: org-wide credentials only)").option("--json", "Print the raw credential list as a JSON array", false).action((opts) => credentialsList(opts));
2218
+ credentials.command("rm <name>").description("Remove a signing credential (replace by rm then add)").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--app <id>", "Scope to a single app (default: org-wide)").option("--json", "Print the result as one JSON line", false).action((name, opts) => credentialsRm(name, opts));
1952
2219
  program.command("dev-status").description("Show the status of a `vibeview dev --detach` session in this project").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print as one JSON line", false).action((opts) => devStatus(opts));
1953
2220
  program.command("dev-stop").description("Stop a `vibeview dev --detach` session in this project").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print as one JSON line", false).action((opts) => devStop(opts));
1954
2221
  program.command("mcp").description(
1955
2222
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop as tools"
1956
2223
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
1957
- const { runMcpServer } = await import("./server-JA5A5PY6.js");
2224
+ const { runMcpServer } = await import("./server-VF3N2CO5.js");
1958
2225
  await runMcpServer(opts);
1959
2226
  });
1960
2227
  program.command("agent-setup").description(
@@ -1983,5 +2250,5 @@ program.parseAsync().catch((e) => {
1983
2250
  if (process.env.VIBEVIEW_DEBUG) throw e;
1984
2251
  process.stderr.write(`error: ${e instanceof Error ? e.message : String(e)}
1985
2252
  `);
1986
- process.exit(EXIT_INFRA_ERROR);
2253
+ process.exit(e instanceof UsageError ? EXIT_TEST_FAIL : EXIT_INFRA_ERROR);
1987
2254
  });
@@ -11,11 +11,11 @@ import {
11
11
  readDevState,
12
12
  renderEnvelope,
13
13
  startDevSession
14
- } from "./chunk-YFXMXO5T.js";
14
+ } from "./chunk-J64ENS7W.js";
15
15
  import {
16
16
  ApiClient,
17
17
  resolveAuth
18
- } from "./chunk-J7V3GYOI.js";
18
+ } from "./chunk-KUU4IUZU.js";
19
19
 
20
20
  // src/mcp/server.ts
21
21
  import { writeFileSync } from "fs";
@@ -239,7 +239,7 @@ async function handleUploadApp(args, deps) {
239
239
  }
240
240
  const name = typeof args.name === "string" && args.name ? args.name : void 0;
241
241
  const upload = deps.uploadApp ?? (async (f, n) => {
242
- const { uploadAppBuild } = await import("./upload-app-ZAWCZFQE.js");
242
+ const { uploadAppBuild } = await import("./upload-app-3WXNF57W.js");
243
243
  const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
244
244
  return result2;
245
245
  });
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  uploadApp,
4
4
  uploadAppBuild
5
- } from "./chunk-J7V3GYOI.js";
5
+ } from "./chunk-KUU4IUZU.js";
6
6
  export {
7
7
  uploadApp,
8
8
  uploadAppBuild
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "VibeView CLI -- run mobile tests from CI/CD pipelines",
5
5
  "type": "module",
6
6
  "bin": {