vibeview 0.18.0 → 0.20.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/dist/cli.js CHANGED
@@ -6,14 +6,16 @@ import {
6
6
  clearDevState,
7
7
  dev,
8
8
  fetchFinalSummary,
9
+ formatDuration,
9
10
  killOwnedMetro,
11
+ loadDevConfig,
10
12
  newTransitionState,
11
13
  printOutcomeLine,
12
14
  printTransition,
13
15
  readDevState,
14
16
  readMetroState,
15
17
  runAgentVerb
16
- } from "./chunk-MGP3UR7P.js";
18
+ } from "./chunk-GUCKFB4K.js";
17
19
  import {
18
20
  ApiClient,
19
21
  EXIT_INFRA_ERROR,
@@ -26,7 +28,7 @@ import {
26
28
  resolveAuth,
27
29
  saveConfig,
28
30
  uploadApp
29
- } from "./chunk-BJEDH672.js";
31
+ } from "./chunk-MCCXPIGE.js";
30
32
 
31
33
  // src/cli.ts
32
34
  import { createRequire } from "module";
@@ -1210,7 +1212,7 @@ async function buildsList(opts, deps) {
1210
1212
  }
1211
1213
  }
1212
1214
  try {
1213
- const { builds: builds2 } = await d.listCloudBuilds(limit);
1215
+ const { builds: builds2 } = await d.listCloudBuilds(limit !== void 0 ? { limit } : void 0);
1214
1216
  if (opts.json) {
1215
1217
  console.log(JSON.stringify(builds2, null, 2));
1216
1218
  } else {
@@ -1378,9 +1380,357 @@ function printError(opts, err) {
1378
1380
  console.error(chalk11.red(`error: ${message}`));
1379
1381
  }
1380
1382
 
1381
- // src/commands/whoami.ts
1383
+ // src/commands/submit.ts
1382
1384
  import chalk12 from "chalk";
1383
- function defaultDeps2(opts) {
1385
+ function defaultDeps2(client) {
1386
+ return {
1387
+ createSubmission: client.createSubmission.bind(client),
1388
+ getSubmission: client.getSubmission.bind(client),
1389
+ getSubmissionLog: client.getSubmissionLog.bind(client),
1390
+ listCloudBuilds: client.listCloudBuilds.bind(client)
1391
+ };
1392
+ }
1393
+ var POLL_INTERVAL_MS2 = 2e3;
1394
+ var SUBMIT_PLATFORMS = ["ios", "tvos", "android", "androidtv"];
1395
+ function isSubmitPlatform(p) {
1396
+ return SUBMIT_PLATFORMS.includes(p);
1397
+ }
1398
+ var PLAY_TRACKS = ["internal", "alpha", "beta", "production"];
1399
+ var RELEASE_STATUSES = ["completed", "draft", "inProgress"];
1400
+ function submitConfigFor(cwd, platform) {
1401
+ let cfg;
1402
+ try {
1403
+ cfg = loadDevConfig(cwd);
1404
+ } catch (err) {
1405
+ console.error(
1406
+ chalk12.yellow(
1407
+ `warning: vibeview.json could not be read \u2014 ignoring submit defaults (${err instanceof Error ? err.message : String(err)})`
1408
+ )
1409
+ );
1410
+ return {};
1411
+ }
1412
+ const raw = cfg?.platforms?.[platform]?.submit;
1413
+ if (raw === void 0) return {};
1414
+ const path = `vibeview.json: platforms.${platform}.submit`;
1415
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1416
+ throw new UsageError(`${path} must be an object`);
1417
+ }
1418
+ const s = raw;
1419
+ for (const key of ["track", "releaseStatus", "ascKey"]) {
1420
+ if (s[key] !== void 0 && (typeof s[key] !== "string" || !s[key])) {
1421
+ throw new UsageError(`${path}.${key} must be a non-empty string`);
1422
+ }
1423
+ }
1424
+ if (s.rollout !== void 0 && typeof s.rollout !== "number") {
1425
+ throw new UsageError(`${path}.rollout must be a number strictly between 0 and 1`);
1426
+ }
1427
+ if (s.changesNotSentForReview !== void 0 && typeof s.changesNotSentForReview !== "boolean") {
1428
+ throw new UsageError(`${path}.changesNotSentForReview must be a boolean`);
1429
+ }
1430
+ return s;
1431
+ }
1432
+ function validateRolloutFraction(f, source, raw = f) {
1433
+ if (!Number.isFinite(f) || !(f > 0 && f < 1)) {
1434
+ throw new UsageError(
1435
+ `${source} must be a fraction strictly between 0 and 1 (e.g. 0.25) \u2014 got "${String(raw)}"`
1436
+ );
1437
+ }
1438
+ return f;
1439
+ }
1440
+ var SUBMISSION_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed"]);
1441
+ var MAX_CONSECUTIVE_POLL_FAILURES2 = 5;
1442
+ function emit(json, evt) {
1443
+ if (json) console.log(JSON.stringify(evt));
1444
+ }
1445
+ async function submitCommand(opts, deps) {
1446
+ if (!opts.platform || !isSubmitPlatform(opts.platform)) {
1447
+ throw new UsageError(
1448
+ `--platform is required and must be one of ${SUBMIT_PLATFORMS.join(", ")}`
1449
+ );
1450
+ }
1451
+ const platform = opts.platform;
1452
+ const androidFamily = platform === "android" || platform === "androidtv";
1453
+ if (opts.build && opts.latest) {
1454
+ throw new UsageError("pass either --build <id> or --latest, not both");
1455
+ }
1456
+ if (!opts.build && !opts.latest) {
1457
+ throw new UsageError(
1458
+ "pass --build <id> (see `vibeview builds list`) or --latest (the newest succeeded production build for this platform)"
1459
+ );
1460
+ }
1461
+ if (!androidFamily && opts.track !== void 0) {
1462
+ throw new UsageError(`--track only applies to android/androidtv (Play) \u2014 not ${platform}`);
1463
+ }
1464
+ if (!androidFamily && opts.package !== void 0) {
1465
+ throw new UsageError(`--package only applies to android/androidtv (Play) \u2014 not ${platform}`);
1466
+ }
1467
+ if (androidFamily && opts.ascKey !== void 0) {
1468
+ throw new UsageError(`--asc-key only applies to ios/tvos (TestFlight) \u2014 not ${platform}`);
1469
+ }
1470
+ if (!androidFamily && opts.releaseStatus !== void 0) {
1471
+ throw new UsageError(
1472
+ `--release-status only applies to android/androidtv (Play) \u2014 not ${platform}`
1473
+ );
1474
+ }
1475
+ if (!androidFamily && opts.rollout !== void 0) {
1476
+ throw new UsageError(`--rollout only applies to android/androidtv (Play) \u2014 not ${platform}`);
1477
+ }
1478
+ const cfgDefaults = submitConfigFor(process.cwd(), platform);
1479
+ const track = opts.track ?? (androidFamily ? cfgDefaults.track : void 0);
1480
+ const releaseStatus = opts.releaseStatus ?? (androidFamily ? cfgDefaults.releaseStatus : void 0);
1481
+ const statusUnchanged = opts.releaseStatus === void 0 || opts.releaseStatus === cfgDefaults.releaseStatus;
1482
+ const rollout = opts.rollout !== void 0 ? validateRolloutFraction(Number(opts.rollout), "--rollout", opts.rollout) : androidFamily && statusUnchanged && cfgDefaults.rollout !== void 0 ? validateRolloutFraction(
1483
+ cfgDefaults.rollout,
1484
+ `vibeview.json platforms.${platform}.submit.rollout`
1485
+ ) : void 0;
1486
+ const changesNotSentForReview = androidFamily ? cfgDefaults.changesNotSentForReview : void 0;
1487
+ const ascKey2 = opts.ascKey ?? (!androidFamily ? cfgDefaults.ascKey : void 0);
1488
+ if (track !== void 0 && !PLAY_TRACKS.includes(track)) {
1489
+ const source = opts.track !== void 0 ? "--track" : `vibeview.json platforms.${platform}.submit.track`;
1490
+ throw new UsageError(`${source} must be one of ${PLAY_TRACKS.join(", ")} \u2014 got "${track}"`);
1491
+ }
1492
+ if (releaseStatus !== void 0 && !RELEASE_STATUSES.includes(releaseStatus)) {
1493
+ const source = opts.releaseStatus !== void 0 ? "--release-status" : `vibeview.json platforms.${platform}.submit.releaseStatus`;
1494
+ throw new UsageError(
1495
+ `${source} must be one of ${RELEASE_STATUSES.join(", ")} \u2014 got "${releaseStatus}"`
1496
+ );
1497
+ }
1498
+ if (rollout !== void 0 && releaseStatus !== "inProgress") {
1499
+ throw new UsageError(
1500
+ `a rollout fraction requires release status "inProgress" (a staged rollout) \u2014 got "${releaseStatus ?? "completed"}"`
1501
+ );
1502
+ }
1503
+ if (releaseStatus === "inProgress" && rollout === void 0) {
1504
+ throw new UsageError(
1505
+ `release status "inProgress" requires a rollout fraction \u2014 pass --rollout (e.g. --rollout 0.1)`
1506
+ );
1507
+ }
1508
+ const { token, baseUrl } = resolveAuth(opts);
1509
+ const client = new ApiClient(baseUrl, token);
1510
+ const d = deps ?? defaultDeps2(client);
1511
+ try {
1512
+ const buildId = opts.build ?? await resolveLatestBuild(platform, d);
1513
+ const sub = await d.createSubmission({
1514
+ build: buildId,
1515
+ platform,
1516
+ ...track !== void 0 ? { track } : {},
1517
+ ...opts.package !== void 0 ? { package: opts.package } : {},
1518
+ ...ascKey2 !== void 0 ? { ascKey: ascKey2 } : {},
1519
+ ...releaseStatus !== void 0 ? { releaseStatus } : {},
1520
+ ...rollout !== void 0 ? { rollout } : {},
1521
+ ...changesNotSentForReview !== void 0 ? { changesNotSentForReview } : {}
1522
+ });
1523
+ emit(opts.json, {
1524
+ event: "submission_created",
1525
+ id: sub.id,
1526
+ build_id: sub.build_id,
1527
+ platform: sub.platform
1528
+ });
1529
+ if (opts.wait === false) {
1530
+ if (!opts.json) console.log(sub.id);
1531
+ process.exitCode = 0;
1532
+ return;
1533
+ }
1534
+ if (!opts.json) {
1535
+ console.error(` ${chalk12.cyan(sub.id)} ${chalk12.dim(`[${sub.platform}]`)}`);
1536
+ }
1537
+ const final = await followSubmission(sub, opts, d);
1538
+ if (!opts.json && final.status === "succeeded" && (final.release_status ?? releaseStatus) === "draft") {
1539
+ console.error(
1540
+ chalk12.dim("release status: draft \u2014 activate it from the Play Console when ready")
1541
+ );
1542
+ }
1543
+ process.exitCode = final.status === "succeeded" ? 0 : 1;
1544
+ } catch (err) {
1545
+ printError2(opts, err);
1546
+ process.exitCode = 1;
1547
+ }
1548
+ }
1549
+ async function resolveLatestBuild(platform, deps) {
1550
+ const { builds: builds2 } = await deps.listCloudBuilds({
1551
+ platform,
1552
+ status: "succeeded",
1553
+ distribution: "production",
1554
+ limit: 200
1555
+ });
1556
+ const match = builds2.find(
1557
+ (b) => b.platform === platform && b.distribution === "production" && b.status === "succeeded" && b.has_artifact
1558
+ );
1559
+ if (!match) {
1560
+ throw new Error(
1561
+ `no submittable ${platform} build found \u2014 --latest needs a succeeded production build whose artifact is still stored. Run \`vibeview build --cloud --production --platform ${platform}\` first, or pass --build <id> (see \`vibeview builds list\`).`
1562
+ );
1563
+ }
1564
+ if (match.platform !== platform) {
1565
+ throw new Error(
1566
+ `--latest resolved build ${match.id}, which is ${match.platform}, not ${platform} \u2014 refusing to submit it.`
1567
+ );
1568
+ }
1569
+ return match.id;
1570
+ }
1571
+ async function followSubmission(created, opts, deps) {
1572
+ const pollIntervalMs = deps.pollIntervalMs ?? POLL_INTERVAL_MS2;
1573
+ let offset = 0;
1574
+ let status = created.status;
1575
+ let consecutiveFailures = 0;
1576
+ let printedQueued = false;
1577
+ let printedRunning = false;
1578
+ while (!SUBMISSION_TERMINAL_STATUSES.has(status)) {
1579
+ try {
1580
+ const log = await deps.getSubmissionLog(created.id, offset);
1581
+ if (log.status === "queued" && !printedQueued) {
1582
+ printedQueued = true;
1583
+ if (opts.json) {
1584
+ emit(opts.json, { event: "status", id: created.id, status: "queued" });
1585
+ } else {
1586
+ console.error(chalk12.dim("\u23F3 queued \u2014 waiting for a submission slot"));
1587
+ }
1588
+ } else if (log.status === "running" && !printedRunning) {
1589
+ printedRunning = true;
1590
+ if (opts.json) {
1591
+ emit(opts.json, { event: "status", id: created.id, status: "running" });
1592
+ } else {
1593
+ console.error(`${chalk12.cyan("\u25B6")} submitting`);
1594
+ }
1595
+ }
1596
+ if (log.content) {
1597
+ if (opts.json) {
1598
+ emit(opts.json, { event: "submission_log", id: created.id, content: log.content });
1599
+ } else {
1600
+ process.stdout.write(log.content);
1601
+ }
1602
+ }
1603
+ offset = log.offset;
1604
+ status = log.status;
1605
+ consecutiveFailures = 0;
1606
+ } catch (err) {
1607
+ if (err instanceof NotFoundError) throw err;
1608
+ consecutiveFailures++;
1609
+ if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES2) {
1610
+ const message = err instanceof Error ? err.message : String(err);
1611
+ throw new Error(
1612
+ `submission poll failed ${consecutiveFailures} times in a row, giving up: ${message}
1613
+ The submission continues server-side \u2014 see its outcome on the Builds page in the dashboard (expand build ${created.build_id}'s row \u2192 Submissions).`
1614
+ );
1615
+ }
1616
+ }
1617
+ if (!SUBMISSION_TERMINAL_STATUSES.has(status)) {
1618
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
1619
+ }
1620
+ }
1621
+ let final;
1622
+ try {
1623
+ final = await deps.getSubmission(created.id);
1624
+ } catch {
1625
+ final = { ...created, status };
1626
+ }
1627
+ emit(opts.json, {
1628
+ event: "submission_finished",
1629
+ id: final.id,
1630
+ status: final.status,
1631
+ error_summary: final.error_summary
1632
+ });
1633
+ if (!opts.json) {
1634
+ const ok = final.status === "succeeded";
1635
+ const mark = ok ? chalk12.green("\u2713") : chalk12.red("\u2717");
1636
+ const duration = formatDuration(final.started_at, final.finished_at);
1637
+ let line = `${mark} ${final.status}`;
1638
+ if (duration) line += ` (${duration})`;
1639
+ if (final.error_summary) line += ` \u2014 ${final.error_summary}`;
1640
+ console.error(line);
1641
+ }
1642
+ return final;
1643
+ }
1644
+ function printError2(opts, err) {
1645
+ const message = err instanceof Error ? err.message : String(err);
1646
+ if (opts.json) console.log(JSON.stringify({ event: "error", message }));
1647
+ console.error(chalk12.red(`error: ${message}`));
1648
+ }
1649
+
1650
+ // src/commands/versions.ts
1651
+ import chalk13 from "chalk";
1652
+ function defaultDeps3(client) {
1653
+ return {
1654
+ listAppVersions: client.listAppVersions.bind(client),
1655
+ setAppVersion: client.setAppVersion.bind(client)
1656
+ };
1657
+ }
1658
+ var VERSION_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
1659
+ async function versionsList(opts, deps) {
1660
+ if (!opts.app) {
1661
+ throw new UsageError("--app <id> is required (see `vibeview list-apps` for ids)");
1662
+ }
1663
+ const { token, baseUrl } = resolveAuth(opts);
1664
+ const client = new ApiClient(baseUrl, token);
1665
+ const d = deps ?? defaultDeps3(client);
1666
+ try {
1667
+ const response = await d.listAppVersions(opts.app);
1668
+ if (opts.json) {
1669
+ console.log(JSON.stringify(response, null, 2));
1670
+ } else {
1671
+ process.stdout.write(formatVersionsHuman(response.versions));
1672
+ }
1673
+ process.exitCode = 0;
1674
+ } catch (err) {
1675
+ printError3(opts, err);
1676
+ process.exitCode = 1;
1677
+ }
1678
+ }
1679
+ async function versionsSet(buildNumber, opts, deps) {
1680
+ if (!opts.app) {
1681
+ throw new UsageError("--app <id> is required (see `vibeview list-apps` for ids)");
1682
+ }
1683
+ if (!opts.platform || !VERSION_PLATFORMS.includes(opts.platform)) {
1684
+ throw new UsageError(
1685
+ `--platform is required and must be one of ${VERSION_PLATFORMS.join(", ")}`
1686
+ );
1687
+ }
1688
+ const parsed = /^\d+$/.test(buildNumber) ? Number(buildNumber) : NaN;
1689
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
1690
+ throw new UsageError(`build number must be a non-negative integer \u2014 got "${buildNumber}"`);
1691
+ }
1692
+ const { token, baseUrl } = resolveAuth(opts);
1693
+ const client = new ApiClient(baseUrl, token);
1694
+ const d = deps ?? defaultDeps3(client);
1695
+ try {
1696
+ const version = await d.setAppVersion(opts.app, opts.platform, parsed);
1697
+ if (opts.json) {
1698
+ console.log(JSON.stringify(version));
1699
+ } else {
1700
+ console.log(
1701
+ chalk13.green(`\u2713 ${version.platform} build number set to ${version.build_number}`) + chalk13.dim(
1702
+ " \u2014 the next autoIncrement production build gets " + (version.build_number + 1)
1703
+ )
1704
+ );
1705
+ }
1706
+ process.exitCode = 0;
1707
+ } catch (err) {
1708
+ printError3(opts, err);
1709
+ process.exitCode = 1;
1710
+ }
1711
+ }
1712
+ var HEADERS2 = ["PLATFORM", "BUILD NUMBER", "UPDATED"];
1713
+ function formatVersionsHuman(versions2) {
1714
+ if (versions2.length === 0) {
1715
+ return "No version counters yet \u2014 they appear after the first autoIncrement production build, or seed one with `vibeview versions set`.\n";
1716
+ }
1717
+ const rows = versions2.map((v) => [v.platform, String(v.build_number), v.updated_at ?? ""]);
1718
+ const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1719
+ const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
1720
+ const join3 = (cells) => cells.join(" ").trimEnd();
1721
+ const lines = [chalk13.bold(join3(padCells(HEADERS2)))];
1722
+ for (const row of rows) lines.push(join3(padCells(row)));
1723
+ return lines.join("\n") + "\n";
1724
+ }
1725
+ function printError3(opts, err) {
1726
+ const message = err instanceof Error ? err.message : String(err);
1727
+ if (opts.json) console.log(JSON.stringify({ event: "error", message }));
1728
+ console.error(chalk13.red(`error: ${message}`));
1729
+ }
1730
+
1731
+ // src/commands/whoami.ts
1732
+ import chalk14 from "chalk";
1733
+ function defaultDeps4(opts) {
1384
1734
  const { token, baseUrl } = resolveAuth(opts);
1385
1735
  const client = new ApiClient(baseUrl, token);
1386
1736
  return { fetchMe: () => client.whoAmI() };
@@ -1392,7 +1742,7 @@ function effectiveRole(role, tokenRole) {
1392
1742
  return (RANK[tokenRole] ?? 3) < (RANK[role] ?? 3) ? tokenRole : role;
1393
1743
  }
1394
1744
  async function whoAmI(opts, deps) {
1395
- const d = deps ?? defaultDeps2(opts);
1745
+ const d = deps ?? defaultDeps4(opts);
1396
1746
  try {
1397
1747
  const me = await d.fetchMe();
1398
1748
  const effective = effectiveRole(me.role, me.token_role);
@@ -1401,27 +1751,27 @@ async function whoAmI(opts, deps) {
1401
1751
  process.exitCode = 0;
1402
1752
  return;
1403
1753
  }
1404
- console.log(`${chalk12.bold("Logged in as")} ${me.email}`);
1754
+ console.log(`${chalk14.bold("Logged in as")} ${me.email}`);
1405
1755
  if (me.memberships.length === 1) {
1406
- console.log(`${chalk12.bold("Organization")} ${me.memberships[0].organization_name}`);
1756
+ console.log(`${chalk14.bold("Organization")} ${me.memberships[0].organization_name}`);
1407
1757
  } else if (me.memberships.length > 1) {
1408
- console.log(chalk12.bold("Organizations"));
1758
+ console.log(chalk14.bold("Organizations"));
1409
1759
  for (const m of me.memberships) {
1410
1760
  console.log(` ${m.organization_name} (${m.role})`);
1411
1761
  }
1412
1762
  }
1413
- console.log(`${chalk12.bold("Member role")} ${me.role ?? "\u2014"}`);
1414
- console.log(`${chalk12.bold("Token role")} ${me.token_role ?? "\u2014 (session auth)"}`);
1415
- console.log(`${chalk12.bold("Effective role")} ${effective ?? "\u2014"}`);
1763
+ console.log(`${chalk14.bold("Member role")} ${me.role ?? "\u2014"}`);
1764
+ console.log(`${chalk14.bold("Token role")} ${me.token_role ?? "\u2014 (session auth)"}`);
1765
+ console.log(`${chalk14.bold("Effective role")} ${effective ?? "\u2014"}`);
1416
1766
  if (me.is_superadmin) {
1417
- console.log(chalk12.dim("Superadmin (platform-wide; does not raise the org role above)"));
1767
+ console.log(chalk14.dim("Superadmin (platform-wide; does not raise the org role above)"));
1418
1768
  }
1419
1769
  if (me.role === "admin" && me.token_role && me.token_role !== "admin") {
1420
1770
  console.log(
1421
- chalk12.yellow(
1771
+ chalk14.yellow(
1422
1772
  `
1423
- 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.
1424
- Create an admin API token in the dashboard and use ${chalk12.bold("--token")} or ${chalk12.bold("VIBEVIEW_API_TOKEN")}.`
1773
+ 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 ${chalk14.bold("vibeview env set")}) will fail with a permissions error.
1774
+ Create an admin API token in the dashboard and use ${chalk14.bold("--token")} or ${chalk14.bold("VIBEVIEW_API_TOKEN")}.`
1425
1775
  )
1426
1776
  );
1427
1777
  }
@@ -1431,7 +1781,7 @@ Create an admin API token in the dashboard and use ${chalk12.bold("--token")} or
1431
1781
  if (opts.json) {
1432
1782
  console.log(JSON.stringify({ event: "error", message }));
1433
1783
  }
1434
- console.error(chalk12.red(`error: ${message}`));
1784
+ console.error(chalk14.red(`error: ${message}`));
1435
1785
  process.exitCode = 1;
1436
1786
  }
1437
1787
  }
@@ -1439,7 +1789,7 @@ Create an admin API token in the dashboard and use ${chalk12.bold("--token")} or
1439
1789
  // src/commands/env.ts
1440
1790
  import { chmod, open as open2, readFile as readFileAsync } from "fs/promises";
1441
1791
  import { basename } from "path";
1442
- import chalk13 from "chalk";
1792
+ import chalk15 from "chalk";
1443
1793
  import { password } from "@inquirer/prompts";
1444
1794
 
1445
1795
  // src/lib/gitignore.ts
@@ -1466,7 +1816,7 @@ async function writeSecureFile(path, content) {
1466
1816
  }
1467
1817
  await chmod(path, 384);
1468
1818
  }
1469
- function defaultDeps3(client) {
1819
+ function defaultDeps5(client) {
1470
1820
  return {
1471
1821
  listVars: (app) => client.listBuildEnvVars(app),
1472
1822
  setVar: (body) => client.setBuildEnvVar(body),
@@ -1495,11 +1845,11 @@ function defaultDeps3(client) {
1495
1845
  isIgnored: (path) => isGitIgnored(path),
1496
1846
  writeFile: writeSecureFile,
1497
1847
  readFile: (path) => readFileAsync(path),
1498
- warn: (message) => console.error(chalk13.yellow(`warning: ${message}`))
1848
+ warn: (message) => console.error(chalk15.yellow(`warning: ${message}`))
1499
1849
  };
1500
1850
  }
1501
1851
  async function envList(opts, deps) {
1502
- const d = deps ?? defaultDeps3(clientFor(opts));
1852
+ const d = deps ?? defaultDeps5(clientFor(opts));
1503
1853
  try {
1504
1854
  const { variables } = await d.listVars(opts.app);
1505
1855
  if (opts.json) {
@@ -1509,7 +1859,7 @@ async function envList(opts, deps) {
1509
1859
  }
1510
1860
  process.exitCode = 0;
1511
1861
  } catch (err) {
1512
- printError2(opts, err);
1862
+ printError4(opts, err);
1513
1863
  process.exitCode = 1;
1514
1864
  }
1515
1865
  }
@@ -1517,22 +1867,22 @@ function formatEnvVarsHuman(variables) {
1517
1867
  if (variables.length === 0) {
1518
1868
  return "No build variables found.\n";
1519
1869
  }
1520
- const HEADERS3 = ["NAME", "VISIBILITY", "SCOPE", "HINT"];
1870
+ const HEADERS4 = ["NAME", "VISIBILITY", "SCOPE", "HINT"];
1521
1871
  const rows = variables.map((v) => [
1522
1872
  v.name,
1523
1873
  v.is_file ? `${v.visibility} (file)` : v.visibility,
1524
1874
  v.app_public_id ?? "org-wide",
1525
1875
  v.value_hint ?? ""
1526
1876
  ]);
1527
- const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1877
+ const widths = HEADERS4.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1528
1878
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
1529
1879
  const join3 = (cells) => cells.join(" ").trimEnd();
1530
- const lines = [chalk13.bold(join3(padCells(HEADERS3)))];
1880
+ const lines = [chalk15.bold(join3(padCells(HEADERS4)))];
1531
1881
  for (const row of rows) lines.push(join3(padCells(row)));
1532
1882
  return lines.join("\n") + "\n";
1533
1883
  }
1534
1884
  async function envSet(name, value, opts, deps) {
1535
- const d = deps ?? defaultDeps3(clientFor(opts));
1885
+ const d = deps ?? defaultDeps5(clientFor(opts));
1536
1886
  let resolvedValue = value;
1537
1887
  if (resolvedValue === void 0 && !opts.file) {
1538
1888
  if (opts.json || !d.isTTY) {
@@ -1558,7 +1908,7 @@ async function envSet(name, value, opts, deps) {
1558
1908
  printSetResult(opts, result);
1559
1909
  process.exitCode = 0;
1560
1910
  } catch (err) {
1561
- printError2(opts, err);
1911
+ printError4(opts, err);
1562
1912
  process.exitCode = 1;
1563
1913
  }
1564
1914
  }
@@ -1577,26 +1927,26 @@ function printSetResult(opts, result) {
1577
1927
  if (opts.json) {
1578
1928
  console.log(JSON.stringify(result));
1579
1929
  } else {
1580
- console.log(chalk13.green(`\u2713 set ${result.name} (${result.visibility})`));
1930
+ console.log(chalk15.green(`\u2713 set ${result.name} (${result.visibility})`));
1581
1931
  }
1582
1932
  }
1583
1933
  async function envRm(name, opts, deps) {
1584
- const d = deps ?? defaultDeps3(clientFor(opts));
1934
+ const d = deps ?? defaultDeps5(clientFor(opts));
1585
1935
  try {
1586
1936
  await d.rmVar({ name, app: opts.app });
1587
1937
  if (opts.json) {
1588
1938
  console.log(JSON.stringify({ event: "deleted", name }));
1589
1939
  } else {
1590
- console.log(chalk13.green(`\u2713 removed ${name}`));
1940
+ console.log(chalk15.green(`\u2713 removed ${name}`));
1591
1941
  }
1592
1942
  process.exitCode = 0;
1593
1943
  } catch (err) {
1594
- printError2(opts, err);
1944
+ printError4(opts, err);
1595
1945
  process.exitCode = 1;
1596
1946
  }
1597
1947
  }
1598
1948
  async function envPull(opts, deps) {
1599
- const d = deps ?? defaultDeps3(clientFor(opts));
1949
+ const d = deps ?? defaultDeps5(clientFor(opts));
1600
1950
  const outPath = opts.out ?? ".env";
1601
1951
  try {
1602
1952
  const { variables } = await d.pullVars(opts.app);
@@ -1619,11 +1969,11 @@ async function envPull(opts, deps) {
1619
1969
  if (opts.json) {
1620
1970
  console.log(JSON.stringify({ event: "pulled", path: outPath, count: variables.length }));
1621
1971
  } else {
1622
- console.log(chalk13.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
1972
+ console.log(chalk15.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
1623
1973
  }
1624
1974
  process.exitCode = 0;
1625
1975
  } catch (err) {
1626
- printError2(opts, err);
1976
+ printError4(opts, err);
1627
1977
  process.exitCode = 1;
1628
1978
  }
1629
1979
  }
@@ -1655,10 +2005,10 @@ function escapeEnvValue(value) {
1655
2005
  }
1656
2006
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1657
2007
  }
1658
- function printError2(opts, err) {
2008
+ function printError4(opts, err) {
1659
2009
  const message = err instanceof Error ? err.message : String(err);
1660
2010
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
1661
- console.error(chalk13.red(`error: ${message}`));
2011
+ console.error(chalk15.red(`error: ${message}`));
1662
2012
  }
1663
2013
 
1664
2014
  // src/commands/credentials.ts
@@ -1670,13 +2020,13 @@ import {
1670
2020
  } from "fs/promises";
1671
2021
  import { mkdirSync } from "fs";
1672
2022
  import { basename as basename2, extname, join } from "path";
1673
- import chalk14 from "chalk";
2023
+ import chalk16 from "chalk";
1674
2024
  import { input, password as password2 } from "@inquirer/prompts";
1675
2025
  function clientFor2(opts) {
1676
2026
  const { token, baseUrl } = resolveAuth(opts);
1677
2027
  return new ApiClient(baseUrl, token);
1678
2028
  }
1679
- function defaultDeps4(client) {
2029
+ function defaultDeps6(client) {
1680
2030
  return {
1681
2031
  addCredential: (body) => client.addSigningCredential({
1682
2032
  platform: body.platform,
@@ -1721,6 +2071,16 @@ function defaultDeps4(client) {
1721
2071
  }
1722
2072
  await client.deleteAscKey(match.id);
1723
2073
  },
2074
+ addPlayKey: (body) => client.addPlayKey(body),
2075
+ listPlayKeys: () => client.listPlayKeys(),
2076
+ deletePlayKey: async (name) => {
2077
+ const { keys } = await client.listPlayKeys();
2078
+ const match = keys.find((k) => k.name === name);
2079
+ if (!match) {
2080
+ throw new Error(`No Google Play key named "${name}" found.`);
2081
+ }
2082
+ await client.deletePlayKey(match.id);
2083
+ },
1724
2084
  prompt: ({ message, hidden }) => hidden ? password2({ message }) : input({ message }),
1725
2085
  isTTY: Boolean(process.stdin.isTTY),
1726
2086
  readFile: (path) => readFileAsync2(path),
@@ -1733,7 +2093,7 @@ function defaultDeps4(client) {
1733
2093
  mkdir: (dir) => mkdirSync(dir, { recursive: true }),
1734
2094
  writeFile: (path, data, mode) => writeFileAsync(path, data, mode !== void 0 ? { mode } : void 0),
1735
2095
  chmod: (path, mode) => chmodAsync(path, mode),
1736
- warn: (message) => console.error(chalk14.yellow(`warning: ${message}`))
2096
+ warn: (message) => console.error(chalk16.yellow(`warning: ${message}`))
1737
2097
  };
1738
2098
  }
1739
2099
  var ANDROID_KIND = "android_keystore";
@@ -1743,7 +2103,7 @@ function isCredentialPlatform(p) {
1743
2103
  return CREDENTIAL_PLATFORMS.includes(p);
1744
2104
  }
1745
2105
  async function credentialsAdd(opts, deps) {
1746
- const d = deps ?? defaultDeps4(clientFor2(opts));
2106
+ const d = deps ?? defaultDeps6(clientFor2(opts));
1747
2107
  if (!opts.platform || !isCredentialPlatform(opts.platform)) {
1748
2108
  throw new UsageError('--platform is required and must be "ios", "android", or "tvos"');
1749
2109
  }
@@ -1802,16 +2162,16 @@ async function credentialsAdd(opts, deps) {
1802
2162
  if (opts.json) {
1803
2163
  console.log(JSON.stringify(result));
1804
2164
  } else {
1805
- console.log(chalk14.green(`\u2713 added ${result.name} (${result.platform})`));
2165
+ console.log(chalk16.green(`\u2713 added ${result.name} (${result.platform})`));
1806
2166
  }
1807
2167
  process.exitCode = 0;
1808
2168
  } catch (err) {
1809
- printError3(opts, err);
2169
+ printError5(opts, err);
1810
2170
  process.exitCode = 1;
1811
2171
  }
1812
2172
  }
1813
2173
  async function credentialsGenerate(opts, deps) {
1814
- const d = deps ?? defaultDeps4(clientFor2(opts));
2174
+ const d = deps ?? defaultDeps6(clientFor2(opts));
1815
2175
  if (!opts.platform) {
1816
2176
  throw new UsageError('--platform is required and must be "android", "ios", or "tvos"');
1817
2177
  }
@@ -1860,27 +2220,27 @@ async function credentialsGenerate(opts, deps) {
1860
2220
  }
1861
2221
  if (opts.json) {
1862
2222
  } else {
1863
- console.log(chalk14.green(`\u2713 generated ${result.name} (android)`));
2223
+ console.log(chalk16.green(`\u2713 generated ${result.name} (android)`));
1864
2224
  console.log("");
1865
- console.log(` Keystore saved to: ${chalk14.bold(savedTo)}`);
1866
- console.log(` Key alias: ${chalk14.bold(result.key_alias)}`);
1867
- console.log(` Store password: ${chalk14.bold(result.store_password)}`);
1868
- console.log(` Key password: ${chalk14.bold(result.key_password)}`);
2225
+ console.log(` Keystore saved to: ${chalk16.bold(savedTo)}`);
2226
+ console.log(` Key alias: ${chalk16.bold(result.key_alias)}`);
2227
+ console.log(` Store password: ${chalk16.bold(result.store_password)}`);
2228
+ console.log(` Key password: ${chalk16.bold(result.key_password)}`);
1869
2229
  console.log("");
1870
2230
  console.log(
1871
- chalk14.yellow(
2231
+ chalk16.yellow(
1872
2232
  " These are shown ONCE \u2014 VibeView cannot recover the keystore file or its passwords. Back them up somewhere safe."
1873
2233
  )
1874
2234
  );
1875
2235
  console.log(
1876
- chalk14.yellow(
2236
+ chalk16.yellow(
1877
2237
  " The credential is already uploaded and active for android and androidtv production builds."
1878
2238
  )
1879
2239
  );
1880
2240
  }
1881
2241
  process.exitCode = 0;
1882
2242
  } catch (err) {
1883
- printError3(opts, err);
2243
+ printError5(opts, err);
1884
2244
  process.exitCode = 1;
1885
2245
  }
1886
2246
  }
@@ -1899,29 +2259,29 @@ async function generateAppleCredential(platform, opts, d) {
1899
2259
  if (opts.json) {
1900
2260
  console.log(JSON.stringify(result));
1901
2261
  } else {
1902
- console.log(chalk14.green(`\u2713 generated ${result.name} (${result.platform})`));
2262
+ console.log(chalk16.green(`\u2713 generated ${result.name} (${result.platform})`));
1903
2263
  console.log("");
1904
- console.log(` Scope: ${chalk14.bold(result.app_public_id ?? "org-wide")}`);
2264
+ console.log(` Scope: ${chalk16.bold(result.app_public_id ?? "org-wide")}`);
1905
2265
  const expires = formatExpires(result);
1906
2266
  if (expires) {
1907
- console.log(` Expires: ${chalk14.bold(expires)}`);
2267
+ console.log(` Expires: ${chalk16.bold(expires)}`);
1908
2268
  }
1909
2269
  console.log("");
1910
2270
  console.log(
1911
2271
  ` The certificate and provisioning profile are stored with VibeView and already active for ${result.platform} production builds.`
1912
2272
  );
1913
2273
  console.log(
1914
- ` Run ${chalk14.bold(`vibeview credentials download ${result.name}`)} if you need the files locally.`
2274
+ ` Run ${chalk16.bold(`vibeview credentials download ${result.name}`)} if you need the files locally.`
1915
2275
  );
1916
2276
  }
1917
2277
  process.exitCode = 0;
1918
2278
  } catch (err) {
1919
- printError3(opts, err);
2279
+ printError5(opts, err);
1920
2280
  process.exitCode = 1;
1921
2281
  }
1922
2282
  }
1923
2283
  async function credentialsDownload(name, opts, deps) {
1924
- const d = deps ?? defaultDeps4(clientFor2(opts));
2284
+ const d = deps ?? defaultDeps6(clientFor2(opts));
1925
2285
  const outDir = opts.out ?? process.cwd();
1926
2286
  try {
1927
2287
  const { credentials: credentials2 } = await d.listCredentials(opts.app);
@@ -1977,26 +2337,26 @@ async function credentialsDownload(name, opts, deps) {
1977
2337
  await d.chmod(path, 384);
1978
2338
  }
1979
2339
  if (!opts.json) {
1980
- console.log(chalk14.green(`\u2713 downloaded ${material.name} (${material.platform})`));
2340
+ console.log(chalk16.green(`\u2713 downloaded ${material.name} (${material.platform})`));
1981
2341
  console.log("");
1982
- console.log(` Saved to: ${chalk14.bold(savedTo)}`);
2342
+ console.log(` Saved to: ${chalk16.bold(savedTo)}`);
1983
2343
  if (profileSavedTo !== null) {
1984
- console.log(` Profile: ${chalk14.bold(profileSavedTo)}`);
2344
+ console.log(` Profile: ${chalk16.bold(profileSavedTo)}`);
1985
2345
  }
1986
2346
  for (const [key, value] of Object.entries(material.passwords)) {
1987
- console.log(` ${key}: ${chalk14.bold(value)}`);
2347
+ console.log(` ${key}: ${chalk16.bold(value)}`);
1988
2348
  }
1989
2349
  console.log("");
1990
- console.log(chalk14.yellow(" This download is recorded in your organization's audit log."));
2350
+ console.log(chalk16.yellow(" This download is recorded in your organization's audit log."));
1991
2351
  }
1992
2352
  process.exitCode = 0;
1993
2353
  } catch (err) {
1994
- printError3(opts, err);
2354
+ printError5(opts, err);
1995
2355
  process.exitCode = 1;
1996
2356
  }
1997
2357
  }
1998
2358
  async function ascKeyAdd(opts, deps) {
1999
- const d = deps ?? defaultDeps4(clientFor2(opts));
2359
+ const d = deps ?? defaultDeps6(clientFor2(opts));
2000
2360
  if (!opts.keyId) {
2001
2361
  throw new UsageError("--key-id is required (the 10-character App Store Connect key id)");
2002
2362
  }
@@ -2018,25 +2378,25 @@ async function ascKeyAdd(opts, deps) {
2018
2378
  if (opts.json) {
2019
2379
  console.log(JSON.stringify(result));
2020
2380
  } else {
2021
- console.log(chalk14.green(`\u2713 added App Store Connect key ${result.name}`));
2381
+ console.log(chalk16.green(`\u2713 added App Store Connect key ${result.name}`));
2022
2382
  console.log("");
2023
- console.log(` Key ID: ${chalk14.bold(result.key_id)}`);
2024
- console.log(` Issuer ID: ${chalk14.bold(result.issuer_id)}`);
2383
+ console.log(` Key ID: ${chalk16.bold(result.key_id)}`);
2384
+ console.log(` Issuer ID: ${chalk16.bold(result.issuer_id)}`);
2025
2385
  console.log("");
2026
2386
  console.log(
2027
- chalk14.yellow(
2387
+ chalk16.yellow(
2028
2388
  " The .p8 is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
2029
2389
  )
2030
2390
  );
2031
2391
  }
2032
2392
  process.exitCode = 0;
2033
2393
  } catch (err) {
2034
- printError3(opts, err);
2394
+ printError5(opts, err);
2035
2395
  process.exitCode = 1;
2036
2396
  }
2037
2397
  }
2038
2398
  async function ascKeyList(opts, deps) {
2039
- const d = deps ?? defaultDeps4(clientFor2(opts));
2399
+ const d = deps ?? defaultDeps6(clientFor2(opts));
2040
2400
  try {
2041
2401
  const { keys } = await d.listAscKeys();
2042
2402
  if (opts.json) {
@@ -2046,7 +2406,7 @@ async function ascKeyList(opts, deps) {
2046
2406
  }
2047
2407
  process.exitCode = 0;
2048
2408
  } catch (err) {
2049
- printError3(opts, err);
2409
+ printError5(opts, err);
2050
2410
  process.exitCode = 1;
2051
2411
  }
2052
2412
  }
@@ -2054,32 +2414,108 @@ function formatAscKeysHuman(keys) {
2054
2414
  if (keys.length === 0) {
2055
2415
  return "No App Store Connect keys found.\n";
2056
2416
  }
2057
- const HEADERS3 = ["NAME", "KEY ID", "ISSUER ID", "CREATED"];
2417
+ const HEADERS4 = ["NAME", "KEY ID", "ISSUER ID", "CREATED"];
2058
2418
  const rows = keys.map((k) => [k.name, k.key_id, k.issuer_id, k.created_at ?? ""]);
2059
- const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2419
+ const widths = HEADERS4.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2060
2420
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2061
2421
  const join3 = (cells) => cells.join(" ").trimEnd();
2062
- const lines = [chalk14.bold(join3(padCells(HEADERS3)))];
2422
+ const lines = [chalk16.bold(join3(padCells(HEADERS4)))];
2063
2423
  for (const row of rows) lines.push(join3(padCells(row)));
2064
2424
  return lines.join("\n") + "\n";
2065
2425
  }
2066
2426
  async function ascKeyRm(name, opts, deps) {
2067
- const d = deps ?? defaultDeps4(clientFor2(opts));
2427
+ const d = deps ?? defaultDeps6(clientFor2(opts));
2068
2428
  try {
2069
2429
  await d.deleteAscKey(name);
2070
2430
  if (opts.json) {
2071
2431
  console.log(JSON.stringify({ event: "deleted", name }));
2072
2432
  } else {
2073
- console.log(chalk14.green(`\u2713 removed App Store Connect key ${name}`));
2433
+ console.log(chalk16.green(`\u2713 removed App Store Connect key ${name}`));
2074
2434
  }
2075
2435
  process.exitCode = 0;
2076
2436
  } catch (err) {
2077
- printError3(opts, err);
2437
+ printError5(opts, err);
2438
+ process.exitCode = 1;
2439
+ }
2440
+ }
2441
+ async function playKeyAdd(opts, deps) {
2442
+ const d = deps ?? defaultDeps6(clientFor2(opts));
2443
+ if (!opts.file) {
2444
+ throw new UsageError(
2445
+ "--file is required (the service-account JSON from Google Play Console \u2192 API access)"
2446
+ );
2447
+ }
2448
+ try {
2449
+ const file = await d.readFile(opts.file);
2450
+ const result = await d.addPlayKey({
2451
+ ...opts.name !== void 0 ? { name: opts.name } : {},
2452
+ file,
2453
+ filename: basename2(opts.file)
2454
+ });
2455
+ if (opts.json) {
2456
+ console.log(JSON.stringify(result));
2457
+ } else {
2458
+ console.log(chalk16.green(`\u2713 added Google Play key ${result.name}`));
2459
+ console.log("");
2460
+ console.log(` Service account: ${chalk16.bold(result.client_email)}`);
2461
+ console.log("");
2462
+ console.log(
2463
+ chalk16.yellow(
2464
+ " The JSON key is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
2465
+ )
2466
+ );
2467
+ }
2468
+ process.exitCode = 0;
2469
+ } catch (err) {
2470
+ printError5(opts, err);
2471
+ process.exitCode = 1;
2472
+ }
2473
+ }
2474
+ async function playKeyList(opts, deps) {
2475
+ const d = deps ?? defaultDeps6(clientFor2(opts));
2476
+ try {
2477
+ const { keys } = await d.listPlayKeys();
2478
+ if (opts.json) {
2479
+ console.log(JSON.stringify(keys, null, 2));
2480
+ } else {
2481
+ process.stdout.write(formatPlayKeysHuman(keys));
2482
+ }
2483
+ process.exitCode = 0;
2484
+ } catch (err) {
2485
+ printError5(opts, err);
2486
+ process.exitCode = 1;
2487
+ }
2488
+ }
2489
+ function formatPlayKeysHuman(keys) {
2490
+ if (keys.length === 0) {
2491
+ return "No Google Play keys found.\n";
2492
+ }
2493
+ const HEADERS4 = ["NAME", "CLIENT EMAIL", "CREATED"];
2494
+ const rows = keys.map((k) => [k.name, k.client_email, k.created_at ?? ""]);
2495
+ const widths = HEADERS4.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2496
+ const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2497
+ const join3 = (cells) => cells.join(" ").trimEnd();
2498
+ const lines = [chalk16.bold(join3(padCells(HEADERS4)))];
2499
+ for (const row of rows) lines.push(join3(padCells(row)));
2500
+ return lines.join("\n") + "\n";
2501
+ }
2502
+ async function playKeyRm(name, opts, deps) {
2503
+ const d = deps ?? defaultDeps6(clientFor2(opts));
2504
+ try {
2505
+ await d.deletePlayKey(name);
2506
+ if (opts.json) {
2507
+ console.log(JSON.stringify({ event: "deleted", name }));
2508
+ } else {
2509
+ console.log(chalk16.green(`\u2713 removed Google Play key ${name}`));
2510
+ }
2511
+ process.exitCode = 0;
2512
+ } catch (err) {
2513
+ printError5(opts, err);
2078
2514
  process.exitCode = 1;
2079
2515
  }
2080
2516
  }
2081
2517
  async function credentialsList(opts, deps) {
2082
- const d = deps ?? defaultDeps4(clientFor2(opts));
2518
+ const d = deps ?? defaultDeps6(clientFor2(opts));
2083
2519
  try {
2084
2520
  const { credentials: credentials2 } = await d.listCredentials(opts.app);
2085
2521
  for (const c of credentials2) {
@@ -2095,7 +2531,7 @@ async function credentialsList(opts, deps) {
2095
2531
  }
2096
2532
  process.exitCode = 0;
2097
2533
  } catch (err) {
2098
- printError3(opts, err);
2534
+ printError5(opts, err);
2099
2535
  process.exitCode = 1;
2100
2536
  }
2101
2537
  }
@@ -2128,7 +2564,7 @@ function formatCredentialsHuman(credentials2) {
2128
2564
  if (credentials2.length === 0) {
2129
2565
  return "No signing credentials found.\n";
2130
2566
  }
2131
- const HEADERS3 = ["NAME", "PLATFORM", "SCOPE", "IDENTITY", "EXPIRES"];
2567
+ const HEADERS4 = ["NAME", "PLATFORM", "SCOPE", "IDENTITY", "EXPIRES"];
2132
2568
  const rows = credentials2.map((c) => [
2133
2569
  c.name,
2134
2570
  c.platform,
@@ -2136,25 +2572,25 @@ function formatCredentialsHuman(credentials2) {
2136
2572
  formatIdentity(c),
2137
2573
  formatExpires(c)
2138
2574
  ]);
2139
- const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2575
+ const widths = HEADERS4.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2140
2576
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2141
2577
  const join3 = (cells) => cells.join(" ").trimEnd();
2142
- const lines = [chalk14.bold(join3(padCells(HEADERS3)))];
2578
+ const lines = [chalk16.bold(join3(padCells(HEADERS4)))];
2143
2579
  for (const row of rows) lines.push(join3(padCells(row)));
2144
2580
  return lines.join("\n") + "\n";
2145
2581
  }
2146
2582
  async function credentialsRm(name, opts, deps) {
2147
- const d = deps ?? defaultDeps4(clientFor2(opts));
2583
+ const d = deps ?? defaultDeps6(clientFor2(opts));
2148
2584
  try {
2149
2585
  await d.deleteCredential({ name, app: opts.app });
2150
2586
  if (opts.json) {
2151
2587
  console.log(JSON.stringify({ event: "deleted", name }));
2152
2588
  } else {
2153
- console.log(chalk14.green(`\u2713 removed ${name}`));
2589
+ console.log(chalk16.green(`\u2713 removed ${name}`));
2154
2590
  }
2155
2591
  process.exitCode = 0;
2156
2592
  } catch (err) {
2157
- printError3(opts, err);
2593
+ printError5(opts, err);
2158
2594
  process.exitCode = 1;
2159
2595
  }
2160
2596
  }
@@ -2162,16 +2598,16 @@ function printKeystoreWriteFailure(opts, err, result) {
2162
2598
  const message = err instanceof Error ? err.message : String(err);
2163
2599
  if (opts.json) {
2164
2600
  console.log(JSON.stringify({ event: "error", message }));
2165
- console.error(chalk14.red(`error: ${message}`));
2601
+ console.error(chalk16.red(`error: ${message}`));
2166
2602
  return;
2167
2603
  }
2168
- console.error(chalk14.red(`error: ${message}`));
2604
+ console.error(chalk16.red(`error: ${message}`));
2169
2605
  console.error(
2170
- chalk14.red(
2606
+ chalk16.red(
2171
2607
  `The keystore could NOT be saved locally, but the credential "${result.name}" WAS created server-side and its key material can never be re-fetched.`
2172
2608
  )
2173
2609
  );
2174
- console.error(chalk14.red("Recover it NOW from the values below \u2014 they will not be shown again:"));
2610
+ console.error(chalk16.red("Recover it NOW from the values below \u2014 they will not be shown again:"));
2175
2611
  console.error("");
2176
2612
  console.error(` Key alias: ${result.key_alias}`);
2177
2613
  console.error(` Store password: ${result.store_password}`);
@@ -2182,19 +2618,19 @@ function printKeystoreWriteFailure(opts, err, result) {
2182
2618
  ` Restore the file with: echo '${result.keystore_base64}' | base64 -d > ${result.name}.keystore`
2183
2619
  );
2184
2620
  }
2185
- function printError3(opts, err) {
2621
+ function printError5(opts, err) {
2186
2622
  const message = err instanceof Error ? err.message : String(err);
2187
2623
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2188
- console.error(chalk14.red(`error: ${message}`));
2624
+ console.error(chalk16.red(`error: ${message}`));
2189
2625
  }
2190
2626
 
2191
2627
  // src/commands/webhooks.ts
2192
- import chalk15 from "chalk";
2628
+ import chalk17 from "chalk";
2193
2629
  function clientFor3(opts) {
2194
2630
  const { token, baseUrl } = resolveAuth(opts);
2195
2631
  return new ApiClient(baseUrl, token);
2196
2632
  }
2197
- function defaultDeps5(client) {
2633
+ function defaultDeps7(client) {
2198
2634
  return {
2199
2635
  addWebhook: (body) => client.addWebhook(body),
2200
2636
  listWebhooks: () => client.listWebhooks(),
@@ -2211,7 +2647,7 @@ async function webhooksAdd(opts, deps) {
2211
2647
  if (!opts.url.startsWith("https://")) {
2212
2648
  throw new UsageError(`--url must start with https:// (got "${opts.url}")`);
2213
2649
  }
2214
- const d = deps ?? defaultDeps5(clientFor3(opts));
2650
+ const d = deps ?? defaultDeps7(clientFor3(opts));
2215
2651
  try {
2216
2652
  const result = await d.addWebhook({
2217
2653
  url: opts.url,
@@ -2220,19 +2656,19 @@ async function webhooksAdd(opts, deps) {
2220
2656
  if (opts.json) {
2221
2657
  console.log(JSON.stringify(result));
2222
2658
  } else {
2223
- console.log(chalk15.green(`\u2713 added ${result.id} ${result.url}`));
2659
+ console.log(chalk17.green(`\u2713 added ${result.id} ${result.url}`));
2224
2660
  console.log("");
2225
- console.log(` Signing secret: ${chalk15.bold(result.secret)}`);
2661
+ console.log(` Signing secret: ${chalk17.bold(result.secret)}`);
2226
2662
  if (opts.secret !== void 0) {
2227
- console.log(chalk15.yellow(" (This is the secret you provided.)"));
2663
+ console.log(chalk17.yellow(" (This is the secret you provided.)"));
2228
2664
  }
2229
2665
  console.log(
2230
- chalk15.yellow(" This secret is shown ONCE and cannot be retrieved again \u2014 store it now.")
2666
+ chalk17.yellow(" This secret is shown ONCE and cannot be retrieved again \u2014 store it now.")
2231
2667
  );
2232
2668
  }
2233
2669
  process.exitCode = 0;
2234
2670
  } catch (err) {
2235
- printError4(opts, err);
2671
+ printError6(opts, err);
2236
2672
  process.exitCode = 1;
2237
2673
  }
2238
2674
  }
@@ -2247,7 +2683,7 @@ function formatWebhooksHuman(webhooks2) {
2247
2683
  if (webhooks2.length === 0) {
2248
2684
  return "No webhooks found.\n";
2249
2685
  }
2250
- const HEADERS3 = ["ID", "URL", "ACTIVE", "LAST DELIVERY", "CREATED"];
2686
+ const HEADERS4 = ["ID", "URL", "ACTIVE", "LAST DELIVERY", "CREATED"];
2251
2687
  const rows = webhooks2.map((w) => [
2252
2688
  w.id,
2253
2689
  w.url,
@@ -2255,15 +2691,15 @@ function formatWebhooksHuman(webhooks2) {
2255
2691
  formatLastDelivery(w.last_delivery),
2256
2692
  w.created_at
2257
2693
  ]);
2258
- const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2694
+ const widths = HEADERS4.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2259
2695
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2260
2696
  const join3 = (cells) => cells.join(" ").trimEnd();
2261
- const lines = [chalk15.bold(join3(padCells(HEADERS3)))];
2697
+ const lines = [chalk17.bold(join3(padCells(HEADERS4)))];
2262
2698
  for (const row of rows) lines.push(join3(padCells(row)));
2263
2699
  return lines.join("\n") + "\n";
2264
2700
  }
2265
2701
  async function webhooksList(opts, deps) {
2266
- const d = deps ?? defaultDeps5(clientFor3(opts));
2702
+ const d = deps ?? defaultDeps7(clientFor3(opts));
2267
2703
  try {
2268
2704
  const { webhooks: webhooks2 } = await d.listWebhooks();
2269
2705
  if (opts.json) {
@@ -2273,22 +2709,22 @@ async function webhooksList(opts, deps) {
2273
2709
  }
2274
2710
  process.exitCode = 0;
2275
2711
  } catch (err) {
2276
- printError4(opts, err);
2712
+ printError6(opts, err);
2277
2713
  process.exitCode = 1;
2278
2714
  }
2279
2715
  }
2280
2716
  async function webhooksRm(id, opts, deps) {
2281
- const d = deps ?? defaultDeps5(clientFor3(opts));
2717
+ const d = deps ?? defaultDeps7(clientFor3(opts));
2282
2718
  try {
2283
2719
  await d.deleteWebhook(id);
2284
2720
  if (opts.json) {
2285
2721
  console.log(JSON.stringify({ event: "deleted", id }));
2286
2722
  } else {
2287
- console.log(chalk15.green(`\u2713 removed ${id}`));
2723
+ console.log(chalk17.green(`\u2713 removed ${id}`));
2288
2724
  }
2289
2725
  process.exitCode = 0;
2290
2726
  } catch (err) {
2291
- printError4(opts, err);
2727
+ printError6(opts, err);
2292
2728
  process.exitCode = 1;
2293
2729
  }
2294
2730
  }
@@ -2305,7 +2741,7 @@ function parseUtcTimestamp(iso) {
2305
2741
  var TEST_POLL_INTERVAL_MS = 3e3;
2306
2742
  var TEST_POLL_BUDGET_MS = 6e4;
2307
2743
  async function webhooksTest(id, opts, deps) {
2308
- const d = deps ?? defaultDeps5(clientFor3(opts));
2744
+ const d = deps ?? defaultDeps7(clientFor3(opts));
2309
2745
  try {
2310
2746
  const startedAt = d.now();
2311
2747
  await d.testWebhook(id);
@@ -2317,13 +2753,13 @@ async function webhooksTest(id, opts, deps) {
2317
2753
  if (opts.json) console.log(JSON.stringify(ld));
2318
2754
  if (ld.status === "succeeded") {
2319
2755
  if (!opts.json) {
2320
- console.log(chalk15.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
2756
+ console.log(chalk17.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
2321
2757
  }
2322
2758
  process.exitCode = 0;
2323
2759
  } else {
2324
2760
  if (!opts.json) {
2325
2761
  const code = ld.response_code != null ? `HTTP ${ld.response_code}` : "no response";
2326
- console.error(chalk15.red(`\u2717 Failed (${code}) at ${ld.at}`));
2762
+ console.error(chalk17.red(`\u2717 Failed (${code}) at ${ld.at}`));
2327
2763
  }
2328
2764
  process.exitCode = 1;
2329
2765
  }
@@ -2332,25 +2768,25 @@ async function webhooksTest(id, opts, deps) {
2332
2768
  }
2333
2769
  if (opts.json) console.log(JSON.stringify({ status: "pending" }));
2334
2770
  console.error(
2335
- chalk15.yellow(
2771
+ chalk17.yellow(
2336
2772
  `Delivery still pending after ${TEST_POLL_BUDGET_MS / 1e3}s \u2014 check \`vibeview webhooks list\` later for the result.`
2337
2773
  )
2338
2774
  );
2339
2775
  process.exitCode = 1;
2340
2776
  } catch (err) {
2341
- printError4(opts, err);
2777
+ printError6(opts, err);
2342
2778
  process.exitCode = 1;
2343
2779
  }
2344
2780
  }
2345
- function printError4(opts, err) {
2781
+ function printError6(opts, err) {
2346
2782
  const message = err instanceof Error ? err.message : String(err);
2347
2783
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2348
- console.error(chalk15.red(`error: ${message}`));
2784
+ console.error(chalk17.red(`error: ${message}`));
2349
2785
  }
2350
2786
 
2351
2787
  // src/lib/formatters/devices.ts
2352
- import chalk16 from "chalk";
2353
- var HEADERS2 = ["DEVICE_ID", "NAME", "TYPE", "CATEGORY", "OS", "STATUS"];
2788
+ import chalk18 from "chalk";
2789
+ var HEADERS3 = ["DEVICE_ID", "NAME", "TYPE", "CATEGORY", "OS", "STATUS"];
2354
2790
  function formatDevicesHuman(devices) {
2355
2791
  if (devices.length === 0) {
2356
2792
  return "No devices registered.\n";
@@ -2363,11 +2799,11 @@ function formatDevicesHuman(devices) {
2363
2799
  d.os_version,
2364
2800
  d.status
2365
2801
  ]);
2366
- const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2802
+ const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2367
2803
  const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
2368
- const lines = [chalk16.bold(fmt(HEADERS2))];
2804
+ const lines = [chalk18.bold(fmt(HEADERS3))];
2369
2805
  for (const row of rows.sort(rowSort)) {
2370
- const statusColored = row[5] === "available" ? chalk16.green(row[5]) : row[5] === "busy" ? chalk16.yellow(row[5]) : chalk16.dim(row[5]);
2806
+ const statusColored = row[5] === "available" ? chalk18.green(row[5]) : row[5] === "busy" ? chalk18.yellow(row[5]) : chalk18.dim(row[5]);
2371
2807
  lines.push(fmt([row[0], row[1], row[2], row[3], row[4], statusColored]));
2372
2808
  }
2373
2809
  return lines.join("\n") + "\n";
@@ -2457,7 +2893,7 @@ import { dirname, join as join2 } from "path";
2457
2893
  import { fileURLToPath } from "url";
2458
2894
  import { spawnSync } from "child_process";
2459
2895
  import { confirm } from "@inquirer/prompts";
2460
- import chalk17 from "chalk";
2896
+ import chalk19 from "chalk";
2461
2897
  function resolveShippedSkillPath(fromDir) {
2462
2898
  for (const rel of ["..", join2("..", "..")]) {
2463
2899
  const candidate = join2(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
@@ -2485,23 +2921,23 @@ function registerMcp() {
2485
2921
  encoding: "utf8"
2486
2922
  });
2487
2923
  if (res.status === 0) {
2488
- console.log(chalk17.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
2924
+ console.log(chalk19.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
2489
2925
  return;
2490
2926
  }
2491
2927
  const out = `${res.stderr ?? ""}${res.stdout ?? ""}`;
2492
2928
  if (out.includes("already exists")) {
2493
- console.log(chalk17.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
2929
+ console.log(chalk19.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
2494
2930
  return;
2495
2931
  }
2496
- console.error(chalk17.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
2497
- console.error(chalk17.yellow(" register it manually: claude mcp add vibeview -- vibeview mcp"));
2932
+ console.error(chalk19.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
2933
+ console.error(chalk19.yellow(" register it manually: claude mcp add vibeview -- vibeview mcp"));
2498
2934
  }
2499
2935
  async function agentSetup(opts) {
2500
2936
  const moduleDir = dirname(fileURLToPath(import.meta.url));
2501
2937
  const shipped = resolveShippedSkillPath(moduleDir);
2502
2938
  if (!shipped) {
2503
2939
  console.error(
2504
- chalk17.red(
2940
+ chalk19.red(
2505
2941
  "error: could not find the packaged skill (skills/vibeview-agent/SKILL.md) \u2014 is this a complete install of the vibeview package?"
2506
2942
  )
2507
2943
  );
@@ -2510,9 +2946,9 @@ async function agentSetup(opts) {
2510
2946
  }
2511
2947
  const targetDir = skillInstallDir(opts.project ?? false, process.cwd(), homedir());
2512
2948
  const result = installSkill(shipped, targetDir);
2513
- console.log(chalk17.green(`\u2713 vibeview-agent skill ${result}: ${join2(targetDir, "SKILL.md")}`));
2949
+ console.log(chalk19.green(`\u2713 vibeview-agent skill ${result}: ${join2(targetDir, "SKILL.md")}`));
2514
2950
  console.log(
2515
- chalk17.dim(
2951
+ chalk19.dim(
2516
2952
  opts.project ? " Claude Code discovers it automatically for anyone working in this project." : " Claude Code discovers it automatically in every project on this machine."
2517
2953
  )
2518
2954
  );
@@ -2520,7 +2956,7 @@ async function agentSetup(opts) {
2520
2956
  if (!claudeCliAvailable()) {
2521
2957
  if (opts.mcp === true) {
2522
2958
  console.error(
2523
- chalk17.yellow(
2959
+ chalk19.yellow(
2524
2960
  "\u26A0 claude CLI not found \u2014 install Claude Code, then run: claude mcp add vibeview -- vibeview mcp"
2525
2961
  )
2526
2962
  );
@@ -2534,7 +2970,7 @@ async function agentSetup(opts) {
2534
2970
  }
2535
2971
  console.log(
2536
2972
  `
2537
- Suggested line for your project's ${chalk17.bold("CLAUDE.md")} (or ${chalk17.bold(
2973
+ Suggested line for your project's ${chalk19.bold("CLAUDE.md")} (or ${chalk19.bold(
2538
2974
  "AGENTS.md"
2539
2975
  )} for other agents):
2540
2976
  `
@@ -2542,7 +2978,7 @@ Suggested line for your project's ${chalk17.bold("CLAUDE.md")} (or ${chalk17.bol
2542
2978
  console.log(` ${CLAUDE_MD_SUGGESTION}
2543
2979
  `);
2544
2980
  console.log(
2545
- chalk17.dim(
2981
+ chalk19.dim(
2546
2982
  "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"
2547
2983
  )
2548
2984
  );
@@ -2706,6 +3142,28 @@ program.command("build").description("Run your configured build command and uplo
2706
3142
  "--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",
2707
3143
  false
2708
3144
  ).option("--json", "Print the result as one JSON line (NDJSON events with --cloud)", false).action((opts) => buildCommand(opts));
3145
+ program.command("submit").description(
3146
+ "Submit a succeeded production cloud build's artifact to TestFlight (ios/tvos) or a Google Play track (android/androidtv) \u2014 binary upload only; store listing and review stay in the store consoles"
3147
+ ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--platform <platform>", "ios, tvos, android, or androidtv").option("--build <id>", "Cloud build id to submit (see `vibeview builds list`)").option(
3148
+ "--latest",
3149
+ "Submit the newest succeeded production build for --platform (pass exactly one of --build/--latest)",
3150
+ false
3151
+ ).option(
3152
+ "--track <track>",
3153
+ "android/androidtv only: Play track \u2014 internal (default), alpha, beta, or production"
3154
+ ).option(
3155
+ "--package <name>",
3156
+ "android/androidtv only: package name, for a build not linked to an app"
3157
+ ).option(
3158
+ "--asc-key <name>",
3159
+ "ios/tvos only: which stored App Store Connect key to use (default when exactly one \u2014 see `vibeview credentials asc-key add`)"
3160
+ ).option(
3161
+ "--release-status <status>",
3162
+ "android/androidtv only: Play release status \u2014 completed (default), draft, or inProgress"
3163
+ ).option(
3164
+ "--rollout <fraction>",
3165
+ "android/androidtv only: staged-rollout fraction, 0 < f < 1 (requires --release-status inProgress)"
3166
+ ).option("--no-wait", "Print the submission id and exit without waiting for completion").option("--json", "Emit line-delimited JSON events on stdout instead of human output", false).action((opts) => submitCommand(opts));
2709
3167
  var builds = program.command("builds").description("Manage cloud builds (see `vibeview build --cloud`)");
2710
3168
  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));
2711
3169
  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));
@@ -2714,6 +3172,13 @@ builds.command("cancel <id>").description("Cancel a queued or running cloud buil
2714
3172
  builds.command("delete-artifact <id>").description(
2715
3173
  "Delete a production build's stored artifact file (irreversible; the build record, logs, and cost stay intact \u2014 only the downloadable file is removed)"
2716
3174
  ).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));
3175
+ var versions = program.command("versions").description(
3176
+ "Manage an app's server-side build-number counters (used by vibeview.json `build.autoIncrement` production builds)"
3177
+ );
3178
+ versions.command("list").description("List an app's build-number counters, one per platform").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--app <id>", "App public id (see `vibeview list-apps`)").option("--json", "Print the raw response as JSON", false).action((opts) => versionsList(opts));
3179
+ versions.command("set <number>").description(
3180
+ "Seed or overwrite one platform's counter \u2014 the next autoIncrement production build gets <number> + 1 (use it to start above a build number already in the store)"
3181
+ ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--app <id>", "App public id (see `vibeview list-apps`)").option("--platform <platform>", "ios, android, tvos, or androidtv").option("--json", "Print the updated version object as one JSON line", false).action((number, opts) => versionsSet(number, opts));
2717
3182
  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));
2718
3183
  var env = program.command("env").description("Manage cloud build environment variables and secrets");
2719
3184
  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));
@@ -2765,6 +3230,17 @@ ascKey.command("add").description(
2765
3230
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--key-id <id>", "The 10-character key id (shown next to the key in App Store Connect)").option("--issuer-id <id>", "The issuer id (UUID shown above the keys table)").option("--file <path>", "The AuthKey_<KEYID>.p8 file Apple let you download once").option("--name <name>", "Key name (default: server-chosen)").option("--json", "Print the raw response as one JSON line", false).action((opts) => ascKeyAdd(opts));
2766
3231
  ascKey.command("list").description("List stored App Store Connect keys (metadata only \u2014 never the .p8)").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the raw key list as a JSON array", false).action((opts) => ascKeyList(opts));
2767
3232
  ascKey.command("rm <name>").description("Remove a stored App Store Connect key").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the result as one JSON line", false).action((name, opts) => ascKeyRm(name, opts));
3233
+ var playKey = credentials.command("play-key").description(
3234
+ "Manage Google Play service-account keys (create one in Google Play Console \u2192 API access, or Google Cloud \u2192 IAM \u2192 service accounts) \u2014 they power `vibeview submit --platform android|androidtv`"
3235
+ );
3236
+ playKey.command("add").description(
3237
+ "Store a Google Play service-account JSON key. The key is stored encrypted and never shown again; uploading the same name again rotates the key."
3238
+ ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option(
3239
+ "--file <path>",
3240
+ "The service-account JSON key file (Google Play Console \u2192 API access / Google Cloud service accounts)"
3241
+ ).option("--name <name>", "Key name (default: server-chosen)").option("--json", "Print the raw response as one JSON line", false).action((opts) => playKeyAdd(opts));
3242
+ playKey.command("list").description("List stored Google Play keys (metadata only \u2014 never the JSON key)").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the raw key list as a JSON array", false).action((opts) => playKeyList(opts));
3243
+ playKey.command("rm <name>").description("Remove a stored Google Play key").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the result as one JSON line", false).action((name, opts) => playKeyRm(name, opts));
2768
3244
  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));
2769
3245
  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));
2770
3246
  var webhooks = program.command("webhooks").description("Manage build-completion webhook endpoints");
@@ -2779,7 +3255,7 @@ program.command("dev-stop").description("Stop a `vibeview dev --detach` session
2779
3255
  program.command("mcp").description(
2780
3256
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop as tools"
2781
3257
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
2782
- const { runMcpServer } = await import("./server-TJQKNXDR.js");
3258
+ const { runMcpServer } = await import("./server-NR3ZPQF5.js");
2783
3259
  await runMcpServer(opts);
2784
3260
  });
2785
3261
  program.command("agent-setup").description(