vibeview 0.17.1 → 0.19.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,6 +6,7 @@ import {
6
6
  clearDevState,
7
7
  dev,
8
8
  fetchFinalSummary,
9
+ formatDuration,
9
10
  killOwnedMetro,
10
11
  newTransitionState,
11
12
  printOutcomeLine,
@@ -13,7 +14,7 @@ import {
13
14
  readDevState,
14
15
  readMetroState,
15
16
  runAgentVerb
16
- } from "./chunk-3IMJ7FNN.js";
17
+ } from "./chunk-T6FS4ELD.js";
17
18
  import {
18
19
  ApiClient,
19
20
  EXIT_INFRA_ERROR,
@@ -26,7 +27,7 @@ import {
26
27
  resolveAuth,
27
28
  saveConfig,
28
29
  uploadApp
29
- } from "./chunk-SYFMOPJW.js";
30
+ } from "./chunk-FG2J2BGS.js";
30
31
 
31
32
  // src/cli.ts
32
33
  import { createRequire } from "module";
@@ -1210,7 +1211,7 @@ async function buildsList(opts, deps) {
1210
1211
  }
1211
1212
  }
1212
1213
  try {
1213
- const { builds: builds2 } = await d.listCloudBuilds(limit);
1214
+ const { builds: builds2 } = await d.listCloudBuilds(limit !== void 0 ? { limit } : void 0);
1214
1215
  if (opts.json) {
1215
1216
  console.log(JSON.stringify(builds2, null, 2));
1216
1217
  } else {
@@ -1378,9 +1379,192 @@ function printError(opts, err) {
1378
1379
  console.error(chalk11.red(`error: ${message}`));
1379
1380
  }
1380
1381
 
1381
- // src/commands/whoami.ts
1382
+ // src/commands/submit.ts
1382
1383
  import chalk12 from "chalk";
1383
- function defaultDeps2(opts) {
1384
+ function defaultDeps2(client) {
1385
+ return {
1386
+ createSubmission: client.createSubmission.bind(client),
1387
+ getSubmission: client.getSubmission.bind(client),
1388
+ getSubmissionLog: client.getSubmissionLog.bind(client),
1389
+ listCloudBuilds: client.listCloudBuilds.bind(client)
1390
+ };
1391
+ }
1392
+ var POLL_INTERVAL_MS2 = 2e3;
1393
+ var SUBMIT_PLATFORMS = ["ios", "tvos", "android", "androidtv"];
1394
+ function isSubmitPlatform(p) {
1395
+ return SUBMIT_PLATFORMS.includes(p);
1396
+ }
1397
+ var PLAY_TRACKS = ["internal", "alpha", "beta", "production"];
1398
+ var SUBMISSION_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["succeeded", "failed"]);
1399
+ var MAX_CONSECUTIVE_POLL_FAILURES2 = 5;
1400
+ function emit(json, evt) {
1401
+ if (json) console.log(JSON.stringify(evt));
1402
+ }
1403
+ async function submitCommand(opts, deps) {
1404
+ if (!opts.platform || !isSubmitPlatform(opts.platform)) {
1405
+ throw new UsageError(
1406
+ `--platform is required and must be one of ${SUBMIT_PLATFORMS.join(", ")}`
1407
+ );
1408
+ }
1409
+ const platform = opts.platform;
1410
+ const androidFamily = platform === "android" || platform === "androidtv";
1411
+ if (opts.build && opts.latest) {
1412
+ throw new UsageError("pass either --build <id> or --latest, not both");
1413
+ }
1414
+ if (!opts.build && !opts.latest) {
1415
+ throw new UsageError(
1416
+ "pass --build <id> (see `vibeview builds list`) or --latest (the newest succeeded production build for this platform)"
1417
+ );
1418
+ }
1419
+ if (!androidFamily && opts.track !== void 0) {
1420
+ throw new UsageError(`--track only applies to android/androidtv (Play) \u2014 not ${platform}`);
1421
+ }
1422
+ if (!androidFamily && opts.package !== void 0) {
1423
+ throw new UsageError(`--package only applies to android/androidtv (Play) \u2014 not ${platform}`);
1424
+ }
1425
+ if (androidFamily && opts.ascKey !== void 0) {
1426
+ throw new UsageError(`--asc-key only applies to ios/tvos (TestFlight) \u2014 not ${platform}`);
1427
+ }
1428
+ if (opts.track !== void 0 && !PLAY_TRACKS.includes(opts.track)) {
1429
+ throw new UsageError(`--track must be one of ${PLAY_TRACKS.join(", ")} \u2014 got "${opts.track}"`);
1430
+ }
1431
+ const { token, baseUrl } = resolveAuth(opts);
1432
+ const client = new ApiClient(baseUrl, token);
1433
+ const d = deps ?? defaultDeps2(client);
1434
+ try {
1435
+ const buildId = opts.build ?? await resolveLatestBuild(platform, d);
1436
+ const sub = await d.createSubmission({
1437
+ build: buildId,
1438
+ platform,
1439
+ ...opts.track !== void 0 ? { track: opts.track } : {},
1440
+ ...opts.package !== void 0 ? { package: opts.package } : {},
1441
+ ...opts.ascKey !== void 0 ? { asc_key: opts.ascKey } : {}
1442
+ });
1443
+ emit(opts.json, {
1444
+ event: "submission_created",
1445
+ id: sub.id,
1446
+ build_id: sub.build_id,
1447
+ platform: sub.platform
1448
+ });
1449
+ if (opts.wait === false) {
1450
+ if (!opts.json) console.log(sub.id);
1451
+ process.exitCode = 0;
1452
+ return;
1453
+ }
1454
+ if (!opts.json) {
1455
+ console.error(` ${chalk12.cyan(sub.id)} ${chalk12.dim(`[${sub.platform}]`)}`);
1456
+ }
1457
+ const final = await followSubmission(sub, opts, d);
1458
+ process.exitCode = final.status === "succeeded" ? 0 : 1;
1459
+ } catch (err) {
1460
+ printError2(opts, err);
1461
+ process.exitCode = 1;
1462
+ }
1463
+ }
1464
+ async function resolveLatestBuild(platform, deps) {
1465
+ const { builds: builds2 } = await deps.listCloudBuilds({
1466
+ platform,
1467
+ status: "succeeded",
1468
+ distribution: "production",
1469
+ limit: 200
1470
+ });
1471
+ const match = builds2.find(
1472
+ (b) => b.platform === platform && b.distribution === "production" && b.status === "succeeded" && b.has_artifact
1473
+ );
1474
+ if (!match) {
1475
+ throw new Error(
1476
+ `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\`).`
1477
+ );
1478
+ }
1479
+ if (match.platform !== platform) {
1480
+ throw new Error(
1481
+ `--latest resolved build ${match.id}, which is ${match.platform}, not ${platform} \u2014 refusing to submit it.`
1482
+ );
1483
+ }
1484
+ return match.id;
1485
+ }
1486
+ async function followSubmission(created, opts, deps) {
1487
+ const pollIntervalMs = deps.pollIntervalMs ?? POLL_INTERVAL_MS2;
1488
+ let offset = 0;
1489
+ let status = created.status;
1490
+ let consecutiveFailures = 0;
1491
+ let printedQueued = false;
1492
+ let printedRunning = false;
1493
+ while (!SUBMISSION_TERMINAL_STATUSES.has(status)) {
1494
+ try {
1495
+ const log = await deps.getSubmissionLog(created.id, offset);
1496
+ if (log.status === "queued" && !printedQueued) {
1497
+ printedQueued = true;
1498
+ if (opts.json) {
1499
+ emit(opts.json, { event: "status", id: created.id, status: "queued" });
1500
+ } else {
1501
+ console.error(chalk12.dim("\u23F3 queued \u2014 waiting for a submission slot"));
1502
+ }
1503
+ } else if (log.status === "running" && !printedRunning) {
1504
+ printedRunning = true;
1505
+ if (opts.json) {
1506
+ emit(opts.json, { event: "status", id: created.id, status: "running" });
1507
+ } else {
1508
+ console.error(`${chalk12.cyan("\u25B6")} submitting`);
1509
+ }
1510
+ }
1511
+ if (log.content) {
1512
+ if (opts.json) {
1513
+ emit(opts.json, { event: "submission_log", id: created.id, content: log.content });
1514
+ } else {
1515
+ process.stdout.write(log.content);
1516
+ }
1517
+ }
1518
+ offset = log.offset;
1519
+ status = log.status;
1520
+ consecutiveFailures = 0;
1521
+ } catch (err) {
1522
+ if (err instanceof NotFoundError) throw err;
1523
+ consecutiveFailures++;
1524
+ if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES2) {
1525
+ const message = err instanceof Error ? err.message : String(err);
1526
+ throw new Error(
1527
+ `submission poll failed ${consecutiveFailures} times in a row, giving up: ${message}
1528
+ 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).`
1529
+ );
1530
+ }
1531
+ }
1532
+ if (!SUBMISSION_TERMINAL_STATUSES.has(status)) {
1533
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
1534
+ }
1535
+ }
1536
+ let final;
1537
+ try {
1538
+ final = await deps.getSubmission(created.id);
1539
+ } catch {
1540
+ final = { ...created, status };
1541
+ }
1542
+ emit(opts.json, {
1543
+ event: "submission_finished",
1544
+ id: final.id,
1545
+ status: final.status,
1546
+ error_summary: final.error_summary
1547
+ });
1548
+ if (!opts.json) {
1549
+ const ok = final.status === "succeeded";
1550
+ const mark = ok ? chalk12.green("\u2713") : chalk12.red("\u2717");
1551
+ const duration = formatDuration(final.started_at, final.finished_at);
1552
+ let line = `${mark} ${final.status}`;
1553
+ if (duration) line += ` (${duration})`;
1554
+ if (final.error_summary) line += ` \u2014 ${final.error_summary}`;
1555
+ console.error(line);
1556
+ }
1557
+ return final;
1558
+ }
1559
+ function printError2(opts, err) {
1560
+ const message = err instanceof Error ? err.message : String(err);
1561
+ if (opts.json) console.log(JSON.stringify({ event: "error", message }));
1562
+ console.error(chalk12.red(`error: ${message}`));
1563
+ }
1564
+
1565
+ // src/commands/whoami.ts
1566
+ import chalk13 from "chalk";
1567
+ function defaultDeps3(opts) {
1384
1568
  const { token, baseUrl } = resolveAuth(opts);
1385
1569
  const client = new ApiClient(baseUrl, token);
1386
1570
  return { fetchMe: () => client.whoAmI() };
@@ -1392,7 +1576,7 @@ function effectiveRole(role, tokenRole) {
1392
1576
  return (RANK[tokenRole] ?? 3) < (RANK[role] ?? 3) ? tokenRole : role;
1393
1577
  }
1394
1578
  async function whoAmI(opts, deps) {
1395
- const d = deps ?? defaultDeps2(opts);
1579
+ const d = deps ?? defaultDeps3(opts);
1396
1580
  try {
1397
1581
  const me = await d.fetchMe();
1398
1582
  const effective = effectiveRole(me.role, me.token_role);
@@ -1401,27 +1585,27 @@ async function whoAmI(opts, deps) {
1401
1585
  process.exitCode = 0;
1402
1586
  return;
1403
1587
  }
1404
- console.log(`${chalk12.bold("Logged in as")} ${me.email}`);
1588
+ console.log(`${chalk13.bold("Logged in as")} ${me.email}`);
1405
1589
  if (me.memberships.length === 1) {
1406
- console.log(`${chalk12.bold("Organization")} ${me.memberships[0].organization_name}`);
1590
+ console.log(`${chalk13.bold("Organization")} ${me.memberships[0].organization_name}`);
1407
1591
  } else if (me.memberships.length > 1) {
1408
- console.log(chalk12.bold("Organizations"));
1592
+ console.log(chalk13.bold("Organizations"));
1409
1593
  for (const m of me.memberships) {
1410
1594
  console.log(` ${m.organization_name} (${m.role})`);
1411
1595
  }
1412
1596
  }
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"}`);
1597
+ console.log(`${chalk13.bold("Member role")} ${me.role ?? "\u2014"}`);
1598
+ console.log(`${chalk13.bold("Token role")} ${me.token_role ?? "\u2014 (session auth)"}`);
1599
+ console.log(`${chalk13.bold("Effective role")} ${effective ?? "\u2014"}`);
1416
1600
  if (me.is_superadmin) {
1417
- console.log(chalk12.dim("Superadmin (platform-wide; does not raise the org role above)"));
1601
+ console.log(chalk13.dim("Superadmin (platform-wide; does not raise the org role above)"));
1418
1602
  }
1419
1603
  if (me.role === "admin" && me.token_role && me.token_role !== "admin") {
1420
1604
  console.log(
1421
- chalk12.yellow(
1605
+ chalk13.yellow(
1422
1606
  `
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")}.`
1607
+ 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 ${chalk13.bold("vibeview env set")}) will fail with a permissions error.
1608
+ Create an admin API token in the dashboard and use ${chalk13.bold("--token")} or ${chalk13.bold("VIBEVIEW_API_TOKEN")}.`
1425
1609
  )
1426
1610
  );
1427
1611
  }
@@ -1431,7 +1615,7 @@ Create an admin API token in the dashboard and use ${chalk12.bold("--token")} or
1431
1615
  if (opts.json) {
1432
1616
  console.log(JSON.stringify({ event: "error", message }));
1433
1617
  }
1434
- console.error(chalk12.red(`error: ${message}`));
1618
+ console.error(chalk13.red(`error: ${message}`));
1435
1619
  process.exitCode = 1;
1436
1620
  }
1437
1621
  }
@@ -1439,7 +1623,7 @@ Create an admin API token in the dashboard and use ${chalk12.bold("--token")} or
1439
1623
  // src/commands/env.ts
1440
1624
  import { chmod, open as open2, readFile as readFileAsync } from "fs/promises";
1441
1625
  import { basename } from "path";
1442
- import chalk13 from "chalk";
1626
+ import chalk14 from "chalk";
1443
1627
  import { password } from "@inquirer/prompts";
1444
1628
 
1445
1629
  // src/lib/gitignore.ts
@@ -1466,7 +1650,7 @@ async function writeSecureFile(path, content) {
1466
1650
  }
1467
1651
  await chmod(path, 384);
1468
1652
  }
1469
- function defaultDeps3(client) {
1653
+ function defaultDeps4(client) {
1470
1654
  return {
1471
1655
  listVars: (app) => client.listBuildEnvVars(app),
1472
1656
  setVar: (body) => client.setBuildEnvVar(body),
@@ -1495,11 +1679,11 @@ function defaultDeps3(client) {
1495
1679
  isIgnored: (path) => isGitIgnored(path),
1496
1680
  writeFile: writeSecureFile,
1497
1681
  readFile: (path) => readFileAsync(path),
1498
- warn: (message) => console.error(chalk13.yellow(`warning: ${message}`))
1682
+ warn: (message) => console.error(chalk14.yellow(`warning: ${message}`))
1499
1683
  };
1500
1684
  }
1501
1685
  async function envList(opts, deps) {
1502
- const d = deps ?? defaultDeps3(clientFor(opts));
1686
+ const d = deps ?? defaultDeps4(clientFor(opts));
1503
1687
  try {
1504
1688
  const { variables } = await d.listVars(opts.app);
1505
1689
  if (opts.json) {
@@ -1509,7 +1693,7 @@ async function envList(opts, deps) {
1509
1693
  }
1510
1694
  process.exitCode = 0;
1511
1695
  } catch (err) {
1512
- printError2(opts, err);
1696
+ printError3(opts, err);
1513
1697
  process.exitCode = 1;
1514
1698
  }
1515
1699
  }
@@ -1527,12 +1711,12 @@ function formatEnvVarsHuman(variables) {
1527
1711
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1528
1712
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
1529
1713
  const join3 = (cells) => cells.join(" ").trimEnd();
1530
- const lines = [chalk13.bold(join3(padCells(HEADERS3)))];
1714
+ const lines = [chalk14.bold(join3(padCells(HEADERS3)))];
1531
1715
  for (const row of rows) lines.push(join3(padCells(row)));
1532
1716
  return lines.join("\n") + "\n";
1533
1717
  }
1534
1718
  async function envSet(name, value, opts, deps) {
1535
- const d = deps ?? defaultDeps3(clientFor(opts));
1719
+ const d = deps ?? defaultDeps4(clientFor(opts));
1536
1720
  let resolvedValue = value;
1537
1721
  if (resolvedValue === void 0 && !opts.file) {
1538
1722
  if (opts.json || !d.isTTY) {
@@ -1558,7 +1742,7 @@ async function envSet(name, value, opts, deps) {
1558
1742
  printSetResult(opts, result);
1559
1743
  process.exitCode = 0;
1560
1744
  } catch (err) {
1561
- printError2(opts, err);
1745
+ printError3(opts, err);
1562
1746
  process.exitCode = 1;
1563
1747
  }
1564
1748
  }
@@ -1577,26 +1761,26 @@ function printSetResult(opts, result) {
1577
1761
  if (opts.json) {
1578
1762
  console.log(JSON.stringify(result));
1579
1763
  } else {
1580
- console.log(chalk13.green(`\u2713 set ${result.name} (${result.visibility})`));
1764
+ console.log(chalk14.green(`\u2713 set ${result.name} (${result.visibility})`));
1581
1765
  }
1582
1766
  }
1583
1767
  async function envRm(name, opts, deps) {
1584
- const d = deps ?? defaultDeps3(clientFor(opts));
1768
+ const d = deps ?? defaultDeps4(clientFor(opts));
1585
1769
  try {
1586
1770
  await d.rmVar({ name, app: opts.app });
1587
1771
  if (opts.json) {
1588
1772
  console.log(JSON.stringify({ event: "deleted", name }));
1589
1773
  } else {
1590
- console.log(chalk13.green(`\u2713 removed ${name}`));
1774
+ console.log(chalk14.green(`\u2713 removed ${name}`));
1591
1775
  }
1592
1776
  process.exitCode = 0;
1593
1777
  } catch (err) {
1594
- printError2(opts, err);
1778
+ printError3(opts, err);
1595
1779
  process.exitCode = 1;
1596
1780
  }
1597
1781
  }
1598
1782
  async function envPull(opts, deps) {
1599
- const d = deps ?? defaultDeps3(clientFor(opts));
1783
+ const d = deps ?? defaultDeps4(clientFor(opts));
1600
1784
  const outPath = opts.out ?? ".env";
1601
1785
  try {
1602
1786
  const { variables } = await d.pullVars(opts.app);
@@ -1619,11 +1803,11 @@ async function envPull(opts, deps) {
1619
1803
  if (opts.json) {
1620
1804
  console.log(JSON.stringify({ event: "pulled", path: outPath, count: variables.length }));
1621
1805
  } else {
1622
- console.log(chalk13.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
1806
+ console.log(chalk14.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
1623
1807
  }
1624
1808
  process.exitCode = 0;
1625
1809
  } catch (err) {
1626
- printError2(opts, err);
1810
+ printError3(opts, err);
1627
1811
  process.exitCode = 1;
1628
1812
  }
1629
1813
  }
@@ -1655,10 +1839,10 @@ function escapeEnvValue(value) {
1655
1839
  }
1656
1840
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1657
1841
  }
1658
- function printError2(opts, err) {
1842
+ function printError3(opts, err) {
1659
1843
  const message = err instanceof Error ? err.message : String(err);
1660
1844
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
1661
- console.error(chalk13.red(`error: ${message}`));
1845
+ console.error(chalk14.red(`error: ${message}`));
1662
1846
  }
1663
1847
 
1664
1848
  // src/commands/credentials.ts
@@ -1670,13 +1854,13 @@ import {
1670
1854
  } from "fs/promises";
1671
1855
  import { mkdirSync } from "fs";
1672
1856
  import { basename as basename2, extname, join } from "path";
1673
- import chalk14 from "chalk";
1857
+ import chalk15 from "chalk";
1674
1858
  import { input, password as password2 } from "@inquirer/prompts";
1675
1859
  function clientFor2(opts) {
1676
1860
  const { token, baseUrl } = resolveAuth(opts);
1677
1861
  return new ApiClient(baseUrl, token);
1678
1862
  }
1679
- function defaultDeps4(client) {
1863
+ function defaultDeps5(client) {
1680
1864
  return {
1681
1865
  addCredential: (body) => client.addSigningCredential({
1682
1866
  platform: body.platform,
@@ -1710,6 +1894,27 @@ function defaultDeps4(client) {
1710
1894
  await client.deleteSigningCredential(match.id);
1711
1895
  },
1712
1896
  generateCredential: (body) => client.generateSigningCredential(body),
1897
+ getMaterial: (id) => client.getSigningCredentialMaterial(id),
1898
+ addAscKey: (body) => client.addAscKey(body),
1899
+ listAscKeys: () => client.listAscKeys(),
1900
+ deleteAscKey: async (name) => {
1901
+ const { keys } = await client.listAscKeys();
1902
+ const match = keys.find((k) => k.name === name);
1903
+ if (!match) {
1904
+ throw new Error(`No App Store Connect key named "${name}" found.`);
1905
+ }
1906
+ await client.deleteAscKey(match.id);
1907
+ },
1908
+ addPlayKey: (body) => client.addPlayKey(body),
1909
+ listPlayKeys: () => client.listPlayKeys(),
1910
+ deletePlayKey: async (name) => {
1911
+ const { keys } = await client.listPlayKeys();
1912
+ const match = keys.find((k) => k.name === name);
1913
+ if (!match) {
1914
+ throw new Error(`No Google Play key named "${name}" found.`);
1915
+ }
1916
+ await client.deletePlayKey(match.id);
1917
+ },
1713
1918
  prompt: ({ message, hidden }) => hidden ? password2({ message }) : input({ message }),
1714
1919
  isTTY: Boolean(process.stdin.isTTY),
1715
1920
  readFile: (path) => readFileAsync2(path),
@@ -1722,7 +1927,7 @@ function defaultDeps4(client) {
1722
1927
  mkdir: (dir) => mkdirSync(dir, { recursive: true }),
1723
1928
  writeFile: (path, data, mode) => writeFileAsync(path, data, mode !== void 0 ? { mode } : void 0),
1724
1929
  chmod: (path, mode) => chmodAsync(path, mode),
1725
- warn: (message) => console.error(chalk14.yellow(`warning: ${message}`))
1930
+ warn: (message) => console.error(chalk15.yellow(`warning: ${message}`))
1726
1931
  };
1727
1932
  }
1728
1933
  var ANDROID_KIND = "android_keystore";
@@ -1732,7 +1937,7 @@ function isCredentialPlatform(p) {
1732
1937
  return CREDENTIAL_PLATFORMS.includes(p);
1733
1938
  }
1734
1939
  async function credentialsAdd(opts, deps) {
1735
- const d = deps ?? defaultDeps4(clientFor2(opts));
1940
+ const d = deps ?? defaultDeps5(clientFor2(opts));
1736
1941
  if (!opts.platform || !isCredentialPlatform(opts.platform)) {
1737
1942
  throw new UsageError('--platform is required and must be "ios", "android", or "tvos"');
1738
1943
  }
@@ -1791,27 +1996,29 @@ async function credentialsAdd(opts, deps) {
1791
1996
  if (opts.json) {
1792
1997
  console.log(JSON.stringify(result));
1793
1998
  } else {
1794
- console.log(chalk14.green(`\u2713 added ${result.name} (${result.platform})`));
1999
+ console.log(chalk15.green(`\u2713 added ${result.name} (${result.platform})`));
1795
2000
  }
1796
2001
  process.exitCode = 0;
1797
2002
  } catch (err) {
1798
- printError3(opts, err);
2003
+ printError4(opts, err);
1799
2004
  process.exitCode = 1;
1800
2005
  }
1801
2006
  }
1802
2007
  async function credentialsGenerate(opts, deps) {
1803
- const d = deps ?? defaultDeps4(clientFor2(opts));
2008
+ const d = deps ?? defaultDeps5(clientFor2(opts));
1804
2009
  if (!opts.platform) {
1805
- throw new UsageError('--platform is required ("android" is the only platform available)');
2010
+ throw new UsageError('--platform is required and must be "android", "ios", or "tvos"');
1806
2011
  }
1807
- if (opts.platform !== "android") {
1808
- throw new UsageError(
1809
- "iOS/tvOS generation isn't available yet because it requires an Apple Developer account; upload an exported certificate with `vibeview credentials add` instead"
1810
- );
2012
+ if (!isCredentialPlatform(opts.platform)) {
2013
+ throw new UsageError(`--platform must be "android", "ios", or "tvos" \u2014 got "${opts.platform}"`);
1811
2014
  }
1812
2015
  if (!opts.name) {
1813
2016
  throw new UsageError("--name is required");
1814
2017
  }
2018
+ if (opts.platform !== "android") {
2019
+ await generateAppleCredential(opts.platform, opts, d);
2020
+ return;
2021
+ }
1815
2022
  const outDir = opts.out ?? process.cwd();
1816
2023
  try {
1817
2024
  d.mkdir(outDir);
@@ -1847,32 +2054,302 @@ async function credentialsGenerate(opts, deps) {
1847
2054
  }
1848
2055
  if (opts.json) {
1849
2056
  } else {
1850
- console.log(chalk14.green(`\u2713 generated ${result.name} (android)`));
2057
+ console.log(chalk15.green(`\u2713 generated ${result.name} (android)`));
1851
2058
  console.log("");
1852
- console.log(` Keystore saved to: ${chalk14.bold(savedTo)}`);
1853
- console.log(` Key alias: ${chalk14.bold(result.key_alias)}`);
1854
- console.log(` Store password: ${chalk14.bold(result.store_password)}`);
1855
- console.log(` Key password: ${chalk14.bold(result.key_password)}`);
2059
+ console.log(` Keystore saved to: ${chalk15.bold(savedTo)}`);
2060
+ console.log(` Key alias: ${chalk15.bold(result.key_alias)}`);
2061
+ console.log(` Store password: ${chalk15.bold(result.store_password)}`);
2062
+ console.log(` Key password: ${chalk15.bold(result.key_password)}`);
1856
2063
  console.log("");
1857
2064
  console.log(
1858
- chalk14.yellow(
2065
+ chalk15.yellow(
1859
2066
  " These are shown ONCE \u2014 VibeView cannot recover the keystore file or its passwords. Back them up somewhere safe."
1860
2067
  )
1861
2068
  );
1862
2069
  console.log(
1863
- chalk14.yellow(
2070
+ chalk15.yellow(
1864
2071
  " The credential is already uploaded and active for android and androidtv production builds."
1865
2072
  )
1866
2073
  );
1867
2074
  }
1868
2075
  process.exitCode = 0;
1869
2076
  } catch (err) {
1870
- printError3(opts, err);
2077
+ printError4(opts, err);
2078
+ process.exitCode = 1;
2079
+ }
2080
+ }
2081
+ async function generateAppleCredential(platform, opts, d) {
2082
+ try {
2083
+ const result = await d.generateCredential({
2084
+ name: opts.name,
2085
+ platform,
2086
+ ...opts.app !== void 0 ? { app: opts.app } : {},
2087
+ ...opts.bundleId !== void 0 ? { bundleId: opts.bundleId } : {},
2088
+ ...opts.ascKey !== void 0 ? { ascKey: opts.ascKey } : {},
2089
+ // Same rule as the android path: only ever `overwrite: true`, otherwise
2090
+ // the field is omitted and the server's 409-on-collision stays in charge.
2091
+ ...opts.force ? { overwrite: true } : {}
2092
+ });
2093
+ if (opts.json) {
2094
+ console.log(JSON.stringify(result));
2095
+ } else {
2096
+ console.log(chalk15.green(`\u2713 generated ${result.name} (${result.platform})`));
2097
+ console.log("");
2098
+ console.log(` Scope: ${chalk15.bold(result.app_public_id ?? "org-wide")}`);
2099
+ const expires = formatExpires(result);
2100
+ if (expires) {
2101
+ console.log(` Expires: ${chalk15.bold(expires)}`);
2102
+ }
2103
+ console.log("");
2104
+ console.log(
2105
+ ` The certificate and provisioning profile are stored with VibeView and already active for ${result.platform} production builds.`
2106
+ );
2107
+ console.log(
2108
+ ` Run ${chalk15.bold(`vibeview credentials download ${result.name}`)} if you need the files locally.`
2109
+ );
2110
+ }
2111
+ process.exitCode = 0;
2112
+ } catch (err) {
2113
+ printError4(opts, err);
2114
+ process.exitCode = 1;
2115
+ }
2116
+ }
2117
+ async function credentialsDownload(name, opts, deps) {
2118
+ const d = deps ?? defaultDeps5(clientFor2(opts));
2119
+ const outDir = opts.out ?? process.cwd();
2120
+ try {
2121
+ const { credentials: credentials2 } = await d.listCredentials(opts.app);
2122
+ const wantApp = opts.app ?? null;
2123
+ const matches = credentials2.filter(
2124
+ (c) => c.name === name && (c.app_public_id ?? null) === wantApp
2125
+ );
2126
+ if (matches.length === 0) {
2127
+ throw new Error(
2128
+ `No credential named "${name}" found${opts.app ? ` for app "${opts.app}"` : ""}.`
2129
+ );
2130
+ }
2131
+ if (matches.length > 1) {
2132
+ throw new Error(
2133
+ `Multiple credentials named "${name}" match (platforms: ${matches.map((c) => c.platform).join(", ")}) \u2014 narrow the lookup with --app.`
2134
+ );
2135
+ }
2136
+ const cred = matches[0];
2137
+ d.mkdir(outDir);
2138
+ let material;
2139
+ try {
2140
+ material = await d.getMaterial(cred.id);
2141
+ } catch (err) {
2142
+ if (err instanceof NotFoundError) {
2143
+ throw new Error(
2144
+ `"${name}" was uploaded, not generated by VibeView \u2014 uploaded credential material is never readable back. Only credentials created by \`vibeview credentials generate\` can be downloaded.`
2145
+ );
2146
+ }
2147
+ throw err;
2148
+ }
2149
+ const savedTo = join(outDir, basename2(material.filename));
2150
+ const profileSavedTo = material.profile_base64 != null ? join(outDir, `${basename2(material.filename, extname(material.filename))}.mobileprovision`) : null;
2151
+ if (opts.json) {
2152
+ console.log(
2153
+ JSON.stringify({
2154
+ ...material,
2155
+ saved_to: savedTo,
2156
+ ...profileSavedTo !== null ? { profile_saved_to: profileSavedTo } : {}
2157
+ })
2158
+ );
2159
+ }
2160
+ const targets = [[savedTo, material.file_base64]];
2161
+ if (profileSavedTo !== null) targets.push([profileSavedTo, material.profile_base64]);
2162
+ for (const [path] of targets) {
2163
+ if (await d.fileExists(path)) {
2164
+ throw new Error(
2165
+ `${path} already exists \u2014 refusing to overwrite it. Move it away or pass a different --out.`
2166
+ );
2167
+ }
2168
+ }
2169
+ for (const [path, base64] of targets) {
2170
+ await d.writeFile(path, Buffer.from(base64, "base64"), 384);
2171
+ await d.chmod(path, 384);
2172
+ }
2173
+ if (!opts.json) {
2174
+ console.log(chalk15.green(`\u2713 downloaded ${material.name} (${material.platform})`));
2175
+ console.log("");
2176
+ console.log(` Saved to: ${chalk15.bold(savedTo)}`);
2177
+ if (profileSavedTo !== null) {
2178
+ console.log(` Profile: ${chalk15.bold(profileSavedTo)}`);
2179
+ }
2180
+ for (const [key, value] of Object.entries(material.passwords)) {
2181
+ console.log(` ${key}: ${chalk15.bold(value)}`);
2182
+ }
2183
+ console.log("");
2184
+ console.log(chalk15.yellow(" This download is recorded in your organization's audit log."));
2185
+ }
2186
+ process.exitCode = 0;
2187
+ } catch (err) {
2188
+ printError4(opts, err);
2189
+ process.exitCode = 1;
2190
+ }
2191
+ }
2192
+ async function ascKeyAdd(opts, deps) {
2193
+ const d = deps ?? defaultDeps5(clientFor2(opts));
2194
+ if (!opts.keyId) {
2195
+ throw new UsageError("--key-id is required (the 10-character App Store Connect key id)");
2196
+ }
2197
+ if (!opts.issuerId) {
2198
+ throw new UsageError("--issuer-id is required (the UUID shown above the keys table)");
2199
+ }
2200
+ if (!opts.file) {
2201
+ throw new UsageError("--file is required (the AuthKey_<KEYID>.p8 file Apple let you download)");
2202
+ }
2203
+ try {
2204
+ const file = await d.readFile(opts.file);
2205
+ const result = await d.addAscKey({
2206
+ ...opts.name !== void 0 ? { name: opts.name } : {},
2207
+ keyId: opts.keyId,
2208
+ issuerId: opts.issuerId,
2209
+ file,
2210
+ filename: basename2(opts.file)
2211
+ });
2212
+ if (opts.json) {
2213
+ console.log(JSON.stringify(result));
2214
+ } else {
2215
+ console.log(chalk15.green(`\u2713 added App Store Connect key ${result.name}`));
2216
+ console.log("");
2217
+ console.log(` Key ID: ${chalk15.bold(result.key_id)}`);
2218
+ console.log(` Issuer ID: ${chalk15.bold(result.issuer_id)}`);
2219
+ console.log("");
2220
+ console.log(
2221
+ chalk15.yellow(
2222
+ " The .p8 is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
2223
+ )
2224
+ );
2225
+ }
2226
+ process.exitCode = 0;
2227
+ } catch (err) {
2228
+ printError4(opts, err);
2229
+ process.exitCode = 1;
2230
+ }
2231
+ }
2232
+ async function ascKeyList(opts, deps) {
2233
+ const d = deps ?? defaultDeps5(clientFor2(opts));
2234
+ try {
2235
+ const { keys } = await d.listAscKeys();
2236
+ if (opts.json) {
2237
+ console.log(JSON.stringify(keys, null, 2));
2238
+ } else {
2239
+ process.stdout.write(formatAscKeysHuman(keys));
2240
+ }
2241
+ process.exitCode = 0;
2242
+ } catch (err) {
2243
+ printError4(opts, err);
2244
+ process.exitCode = 1;
2245
+ }
2246
+ }
2247
+ function formatAscKeysHuman(keys) {
2248
+ if (keys.length === 0) {
2249
+ return "No App Store Connect keys found.\n";
2250
+ }
2251
+ const HEADERS3 = ["NAME", "KEY ID", "ISSUER ID", "CREATED"];
2252
+ const rows = keys.map((k) => [k.name, k.key_id, k.issuer_id, k.created_at ?? ""]);
2253
+ const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2254
+ const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2255
+ const join3 = (cells) => cells.join(" ").trimEnd();
2256
+ const lines = [chalk15.bold(join3(padCells(HEADERS3)))];
2257
+ for (const row of rows) lines.push(join3(padCells(row)));
2258
+ return lines.join("\n") + "\n";
2259
+ }
2260
+ async function ascKeyRm(name, opts, deps) {
2261
+ const d = deps ?? defaultDeps5(clientFor2(opts));
2262
+ try {
2263
+ await d.deleteAscKey(name);
2264
+ if (opts.json) {
2265
+ console.log(JSON.stringify({ event: "deleted", name }));
2266
+ } else {
2267
+ console.log(chalk15.green(`\u2713 removed App Store Connect key ${name}`));
2268
+ }
2269
+ process.exitCode = 0;
2270
+ } catch (err) {
2271
+ printError4(opts, err);
2272
+ process.exitCode = 1;
2273
+ }
2274
+ }
2275
+ async function playKeyAdd(opts, deps) {
2276
+ const d = deps ?? defaultDeps5(clientFor2(opts));
2277
+ if (!opts.file) {
2278
+ throw new UsageError(
2279
+ "--file is required (the service-account JSON from Google Play Console \u2192 API access)"
2280
+ );
2281
+ }
2282
+ try {
2283
+ const file = await d.readFile(opts.file);
2284
+ const result = await d.addPlayKey({
2285
+ ...opts.name !== void 0 ? { name: opts.name } : {},
2286
+ file,
2287
+ filename: basename2(opts.file)
2288
+ });
2289
+ if (opts.json) {
2290
+ console.log(JSON.stringify(result));
2291
+ } else {
2292
+ console.log(chalk15.green(`\u2713 added Google Play key ${result.name}`));
2293
+ console.log("");
2294
+ console.log(` Service account: ${chalk15.bold(result.client_email)}`);
2295
+ console.log("");
2296
+ console.log(
2297
+ chalk15.yellow(
2298
+ " The JSON key is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
2299
+ )
2300
+ );
2301
+ }
2302
+ process.exitCode = 0;
2303
+ } catch (err) {
2304
+ printError4(opts, err);
2305
+ process.exitCode = 1;
2306
+ }
2307
+ }
2308
+ async function playKeyList(opts, deps) {
2309
+ const d = deps ?? defaultDeps5(clientFor2(opts));
2310
+ try {
2311
+ const { keys } = await d.listPlayKeys();
2312
+ if (opts.json) {
2313
+ console.log(JSON.stringify(keys, null, 2));
2314
+ } else {
2315
+ process.stdout.write(formatPlayKeysHuman(keys));
2316
+ }
2317
+ process.exitCode = 0;
2318
+ } catch (err) {
2319
+ printError4(opts, err);
2320
+ process.exitCode = 1;
2321
+ }
2322
+ }
2323
+ function formatPlayKeysHuman(keys) {
2324
+ if (keys.length === 0) {
2325
+ return "No Google Play keys found.\n";
2326
+ }
2327
+ const HEADERS3 = ["NAME", "CLIENT EMAIL", "CREATED"];
2328
+ const rows = keys.map((k) => [k.name, k.client_email, k.created_at ?? ""]);
2329
+ const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2330
+ const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2331
+ const join3 = (cells) => cells.join(" ").trimEnd();
2332
+ const lines = [chalk15.bold(join3(padCells(HEADERS3)))];
2333
+ for (const row of rows) lines.push(join3(padCells(row)));
2334
+ return lines.join("\n") + "\n";
2335
+ }
2336
+ async function playKeyRm(name, opts, deps) {
2337
+ const d = deps ?? defaultDeps5(clientFor2(opts));
2338
+ try {
2339
+ await d.deletePlayKey(name);
2340
+ if (opts.json) {
2341
+ console.log(JSON.stringify({ event: "deleted", name }));
2342
+ } else {
2343
+ console.log(chalk15.green(`\u2713 removed Google Play key ${name}`));
2344
+ }
2345
+ process.exitCode = 0;
2346
+ } catch (err) {
2347
+ printError4(opts, err);
1871
2348
  process.exitCode = 1;
1872
2349
  }
1873
2350
  }
1874
2351
  async function credentialsList(opts, deps) {
1875
- const d = deps ?? defaultDeps4(clientFor2(opts));
2352
+ const d = deps ?? defaultDeps5(clientFor2(opts));
1876
2353
  try {
1877
2354
  const { credentials: credentials2 } = await d.listCredentials(opts.app);
1878
2355
  for (const c of credentials2) {
@@ -1888,7 +2365,7 @@ async function credentialsList(opts, deps) {
1888
2365
  }
1889
2366
  process.exitCode = 0;
1890
2367
  } catch (err) {
1891
- printError3(opts, err);
2368
+ printError4(opts, err);
1892
2369
  process.exitCode = 1;
1893
2370
  }
1894
2371
  }
@@ -1932,22 +2409,22 @@ function formatCredentialsHuman(credentials2) {
1932
2409
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1933
2410
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
1934
2411
  const join3 = (cells) => cells.join(" ").trimEnd();
1935
- const lines = [chalk14.bold(join3(padCells(HEADERS3)))];
2412
+ const lines = [chalk15.bold(join3(padCells(HEADERS3)))];
1936
2413
  for (const row of rows) lines.push(join3(padCells(row)));
1937
2414
  return lines.join("\n") + "\n";
1938
2415
  }
1939
2416
  async function credentialsRm(name, opts, deps) {
1940
- const d = deps ?? defaultDeps4(clientFor2(opts));
2417
+ const d = deps ?? defaultDeps5(clientFor2(opts));
1941
2418
  try {
1942
2419
  await d.deleteCredential({ name, app: opts.app });
1943
2420
  if (opts.json) {
1944
2421
  console.log(JSON.stringify({ event: "deleted", name }));
1945
2422
  } else {
1946
- console.log(chalk14.green(`\u2713 removed ${name}`));
2423
+ console.log(chalk15.green(`\u2713 removed ${name}`));
1947
2424
  }
1948
2425
  process.exitCode = 0;
1949
2426
  } catch (err) {
1950
- printError3(opts, err);
2427
+ printError4(opts, err);
1951
2428
  process.exitCode = 1;
1952
2429
  }
1953
2430
  }
@@ -1955,16 +2432,16 @@ function printKeystoreWriteFailure(opts, err, result) {
1955
2432
  const message = err instanceof Error ? err.message : String(err);
1956
2433
  if (opts.json) {
1957
2434
  console.log(JSON.stringify({ event: "error", message }));
1958
- console.error(chalk14.red(`error: ${message}`));
2435
+ console.error(chalk15.red(`error: ${message}`));
1959
2436
  return;
1960
2437
  }
1961
- console.error(chalk14.red(`error: ${message}`));
2438
+ console.error(chalk15.red(`error: ${message}`));
1962
2439
  console.error(
1963
- chalk14.red(
2440
+ chalk15.red(
1964
2441
  `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.`
1965
2442
  )
1966
2443
  );
1967
- console.error(chalk14.red("Recover it NOW from the values below \u2014 they will not be shown again:"));
2444
+ console.error(chalk15.red("Recover it NOW from the values below \u2014 they will not be shown again:"));
1968
2445
  console.error("");
1969
2446
  console.error(` Key alias: ${result.key_alias}`);
1970
2447
  console.error(` Store password: ${result.store_password}`);
@@ -1975,19 +2452,19 @@ function printKeystoreWriteFailure(opts, err, result) {
1975
2452
  ` Restore the file with: echo '${result.keystore_base64}' | base64 -d > ${result.name}.keystore`
1976
2453
  );
1977
2454
  }
1978
- function printError3(opts, err) {
2455
+ function printError4(opts, err) {
1979
2456
  const message = err instanceof Error ? err.message : String(err);
1980
2457
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
1981
- console.error(chalk14.red(`error: ${message}`));
2458
+ console.error(chalk15.red(`error: ${message}`));
1982
2459
  }
1983
2460
 
1984
2461
  // src/commands/webhooks.ts
1985
- import chalk15 from "chalk";
2462
+ import chalk16 from "chalk";
1986
2463
  function clientFor3(opts) {
1987
2464
  const { token, baseUrl } = resolveAuth(opts);
1988
2465
  return new ApiClient(baseUrl, token);
1989
2466
  }
1990
- function defaultDeps5(client) {
2467
+ function defaultDeps6(client) {
1991
2468
  return {
1992
2469
  addWebhook: (body) => client.addWebhook(body),
1993
2470
  listWebhooks: () => client.listWebhooks(),
@@ -2004,7 +2481,7 @@ async function webhooksAdd(opts, deps) {
2004
2481
  if (!opts.url.startsWith("https://")) {
2005
2482
  throw new UsageError(`--url must start with https:// (got "${opts.url}")`);
2006
2483
  }
2007
- const d = deps ?? defaultDeps5(clientFor3(opts));
2484
+ const d = deps ?? defaultDeps6(clientFor3(opts));
2008
2485
  try {
2009
2486
  const result = await d.addWebhook({
2010
2487
  url: opts.url,
@@ -2013,19 +2490,19 @@ async function webhooksAdd(opts, deps) {
2013
2490
  if (opts.json) {
2014
2491
  console.log(JSON.stringify(result));
2015
2492
  } else {
2016
- console.log(chalk15.green(`\u2713 added ${result.id} ${result.url}`));
2493
+ console.log(chalk16.green(`\u2713 added ${result.id} ${result.url}`));
2017
2494
  console.log("");
2018
- console.log(` Signing secret: ${chalk15.bold(result.secret)}`);
2495
+ console.log(` Signing secret: ${chalk16.bold(result.secret)}`);
2019
2496
  if (opts.secret !== void 0) {
2020
- console.log(chalk15.yellow(" (This is the secret you provided.)"));
2497
+ console.log(chalk16.yellow(" (This is the secret you provided.)"));
2021
2498
  }
2022
2499
  console.log(
2023
- chalk15.yellow(" This secret is shown ONCE and cannot be retrieved again \u2014 store it now.")
2500
+ chalk16.yellow(" This secret is shown ONCE and cannot be retrieved again \u2014 store it now.")
2024
2501
  );
2025
2502
  }
2026
2503
  process.exitCode = 0;
2027
2504
  } catch (err) {
2028
- printError4(opts, err);
2505
+ printError5(opts, err);
2029
2506
  process.exitCode = 1;
2030
2507
  }
2031
2508
  }
@@ -2051,12 +2528,12 @@ function formatWebhooksHuman(webhooks2) {
2051
2528
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2052
2529
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2053
2530
  const join3 = (cells) => cells.join(" ").trimEnd();
2054
- const lines = [chalk15.bold(join3(padCells(HEADERS3)))];
2531
+ const lines = [chalk16.bold(join3(padCells(HEADERS3)))];
2055
2532
  for (const row of rows) lines.push(join3(padCells(row)));
2056
2533
  return lines.join("\n") + "\n";
2057
2534
  }
2058
2535
  async function webhooksList(opts, deps) {
2059
- const d = deps ?? defaultDeps5(clientFor3(opts));
2536
+ const d = deps ?? defaultDeps6(clientFor3(opts));
2060
2537
  try {
2061
2538
  const { webhooks: webhooks2 } = await d.listWebhooks();
2062
2539
  if (opts.json) {
@@ -2066,22 +2543,22 @@ async function webhooksList(opts, deps) {
2066
2543
  }
2067
2544
  process.exitCode = 0;
2068
2545
  } catch (err) {
2069
- printError4(opts, err);
2546
+ printError5(opts, err);
2070
2547
  process.exitCode = 1;
2071
2548
  }
2072
2549
  }
2073
2550
  async function webhooksRm(id, opts, deps) {
2074
- const d = deps ?? defaultDeps5(clientFor3(opts));
2551
+ const d = deps ?? defaultDeps6(clientFor3(opts));
2075
2552
  try {
2076
2553
  await d.deleteWebhook(id);
2077
2554
  if (opts.json) {
2078
2555
  console.log(JSON.stringify({ event: "deleted", id }));
2079
2556
  } else {
2080
- console.log(chalk15.green(`\u2713 removed ${id}`));
2557
+ console.log(chalk16.green(`\u2713 removed ${id}`));
2081
2558
  }
2082
2559
  process.exitCode = 0;
2083
2560
  } catch (err) {
2084
- printError4(opts, err);
2561
+ printError5(opts, err);
2085
2562
  process.exitCode = 1;
2086
2563
  }
2087
2564
  }
@@ -2098,7 +2575,7 @@ function parseUtcTimestamp(iso) {
2098
2575
  var TEST_POLL_INTERVAL_MS = 3e3;
2099
2576
  var TEST_POLL_BUDGET_MS = 6e4;
2100
2577
  async function webhooksTest(id, opts, deps) {
2101
- const d = deps ?? defaultDeps5(clientFor3(opts));
2578
+ const d = deps ?? defaultDeps6(clientFor3(opts));
2102
2579
  try {
2103
2580
  const startedAt = d.now();
2104
2581
  await d.testWebhook(id);
@@ -2110,13 +2587,13 @@ async function webhooksTest(id, opts, deps) {
2110
2587
  if (opts.json) console.log(JSON.stringify(ld));
2111
2588
  if (ld.status === "succeeded") {
2112
2589
  if (!opts.json) {
2113
- console.log(chalk15.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
2590
+ console.log(chalk16.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
2114
2591
  }
2115
2592
  process.exitCode = 0;
2116
2593
  } else {
2117
2594
  if (!opts.json) {
2118
2595
  const code = ld.response_code != null ? `HTTP ${ld.response_code}` : "no response";
2119
- console.error(chalk15.red(`\u2717 Failed (${code}) at ${ld.at}`));
2596
+ console.error(chalk16.red(`\u2717 Failed (${code}) at ${ld.at}`));
2120
2597
  }
2121
2598
  process.exitCode = 1;
2122
2599
  }
@@ -2125,24 +2602,24 @@ async function webhooksTest(id, opts, deps) {
2125
2602
  }
2126
2603
  if (opts.json) console.log(JSON.stringify({ status: "pending" }));
2127
2604
  console.error(
2128
- chalk15.yellow(
2605
+ chalk16.yellow(
2129
2606
  `Delivery still pending after ${TEST_POLL_BUDGET_MS / 1e3}s \u2014 check \`vibeview webhooks list\` later for the result.`
2130
2607
  )
2131
2608
  );
2132
2609
  process.exitCode = 1;
2133
2610
  } catch (err) {
2134
- printError4(opts, err);
2611
+ printError5(opts, err);
2135
2612
  process.exitCode = 1;
2136
2613
  }
2137
2614
  }
2138
- function printError4(opts, err) {
2615
+ function printError5(opts, err) {
2139
2616
  const message = err instanceof Error ? err.message : String(err);
2140
2617
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2141
- console.error(chalk15.red(`error: ${message}`));
2618
+ console.error(chalk16.red(`error: ${message}`));
2142
2619
  }
2143
2620
 
2144
2621
  // src/lib/formatters/devices.ts
2145
- import chalk16 from "chalk";
2622
+ import chalk17 from "chalk";
2146
2623
  var HEADERS2 = ["DEVICE_ID", "NAME", "TYPE", "CATEGORY", "OS", "STATUS"];
2147
2624
  function formatDevicesHuman(devices) {
2148
2625
  if (devices.length === 0) {
@@ -2158,9 +2635,9 @@ function formatDevicesHuman(devices) {
2158
2635
  ]);
2159
2636
  const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2160
2637
  const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
2161
- const lines = [chalk16.bold(fmt(HEADERS2))];
2638
+ const lines = [chalk17.bold(fmt(HEADERS2))];
2162
2639
  for (const row of rows.sort(rowSort)) {
2163
- const statusColored = row[5] === "available" ? chalk16.green(row[5]) : row[5] === "busy" ? chalk16.yellow(row[5]) : chalk16.dim(row[5]);
2640
+ const statusColored = row[5] === "available" ? chalk17.green(row[5]) : row[5] === "busy" ? chalk17.yellow(row[5]) : chalk17.dim(row[5]);
2164
2641
  lines.push(fmt([row[0], row[1], row[2], row[3], row[4], statusColored]));
2165
2642
  }
2166
2643
  return lines.join("\n") + "\n";
@@ -2250,7 +2727,7 @@ import { dirname, join as join2 } from "path";
2250
2727
  import { fileURLToPath } from "url";
2251
2728
  import { spawnSync } from "child_process";
2252
2729
  import { confirm } from "@inquirer/prompts";
2253
- import chalk17 from "chalk";
2730
+ import chalk18 from "chalk";
2254
2731
  function resolveShippedSkillPath(fromDir) {
2255
2732
  for (const rel of ["..", join2("..", "..")]) {
2256
2733
  const candidate = join2(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
@@ -2278,23 +2755,23 @@ function registerMcp() {
2278
2755
  encoding: "utf8"
2279
2756
  });
2280
2757
  if (res.status === 0) {
2281
- console.log(chalk17.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
2758
+ console.log(chalk18.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
2282
2759
  return;
2283
2760
  }
2284
2761
  const out = `${res.stderr ?? ""}${res.stdout ?? ""}`;
2285
2762
  if (out.includes("already exists")) {
2286
- console.log(chalk17.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
2763
+ console.log(chalk18.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
2287
2764
  return;
2288
2765
  }
2289
- console.error(chalk17.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
2290
- console.error(chalk17.yellow(" register it manually: claude mcp add vibeview -- vibeview mcp"));
2766
+ console.error(chalk18.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
2767
+ console.error(chalk18.yellow(" register it manually: claude mcp add vibeview -- vibeview mcp"));
2291
2768
  }
2292
2769
  async function agentSetup(opts) {
2293
2770
  const moduleDir = dirname(fileURLToPath(import.meta.url));
2294
2771
  const shipped = resolveShippedSkillPath(moduleDir);
2295
2772
  if (!shipped) {
2296
2773
  console.error(
2297
- chalk17.red(
2774
+ chalk18.red(
2298
2775
  "error: could not find the packaged skill (skills/vibeview-agent/SKILL.md) \u2014 is this a complete install of the vibeview package?"
2299
2776
  )
2300
2777
  );
@@ -2303,9 +2780,9 @@ async function agentSetup(opts) {
2303
2780
  }
2304
2781
  const targetDir = skillInstallDir(opts.project ?? false, process.cwd(), homedir());
2305
2782
  const result = installSkill(shipped, targetDir);
2306
- console.log(chalk17.green(`\u2713 vibeview-agent skill ${result}: ${join2(targetDir, "SKILL.md")}`));
2783
+ console.log(chalk18.green(`\u2713 vibeview-agent skill ${result}: ${join2(targetDir, "SKILL.md")}`));
2307
2784
  console.log(
2308
- chalk17.dim(
2785
+ chalk18.dim(
2309
2786
  opts.project ? " Claude Code discovers it automatically for anyone working in this project." : " Claude Code discovers it automatically in every project on this machine."
2310
2787
  )
2311
2788
  );
@@ -2313,7 +2790,7 @@ async function agentSetup(opts) {
2313
2790
  if (!claudeCliAvailable()) {
2314
2791
  if (opts.mcp === true) {
2315
2792
  console.error(
2316
- chalk17.yellow(
2793
+ chalk18.yellow(
2317
2794
  "\u26A0 claude CLI not found \u2014 install Claude Code, then run: claude mcp add vibeview -- vibeview mcp"
2318
2795
  )
2319
2796
  );
@@ -2327,7 +2804,7 @@ async function agentSetup(opts) {
2327
2804
  }
2328
2805
  console.log(
2329
2806
  `
2330
- Suggested line for your project's ${chalk17.bold("CLAUDE.md")} (or ${chalk17.bold(
2807
+ Suggested line for your project's ${chalk18.bold("CLAUDE.md")} (or ${chalk18.bold(
2331
2808
  "AGENTS.md"
2332
2809
  )} for other agents):
2333
2810
  `
@@ -2335,7 +2812,7 @@ Suggested line for your project's ${chalk17.bold("CLAUDE.md")} (or ${chalk17.bol
2335
2812
  console.log(` ${CLAUDE_MD_SUGGESTION}
2336
2813
  `);
2337
2814
  console.log(
2338
- chalk17.dim(
2815
+ chalk18.dim(
2339
2816
  "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"
2340
2817
  )
2341
2818
  );
@@ -2499,6 +2976,22 @@ program.command("build").description("Run your configured build command and uplo
2499
2976
  "--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",
2500
2977
  false
2501
2978
  ).option("--json", "Print the result as one JSON line (NDJSON events with --cloud)", false).action((opts) => buildCommand(opts));
2979
+ program.command("submit").description(
2980
+ "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"
2981
+ ).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(
2982
+ "--latest",
2983
+ "Submit the newest succeeded production build for --platform (pass exactly one of --build/--latest)",
2984
+ false
2985
+ ).option(
2986
+ "--track <track>",
2987
+ "android/androidtv only: Play track \u2014 internal (default), alpha, beta, or production"
2988
+ ).option(
2989
+ "--package <name>",
2990
+ "android/androidtv only: package name, for a build not linked to an app"
2991
+ ).option(
2992
+ "--asc-key <name>",
2993
+ "ios/tvos only: which stored App Store Connect key to use (default when exactly one \u2014 see `vibeview credentials asc-key add`)"
2994
+ ).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));
2502
2995
  var builds = program.command("builds").description("Manage cloud builds (see `vibeview build --cloud`)");
2503
2996
  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));
2504
2997
  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));
@@ -2528,19 +3021,47 @@ credentials.command("add").description(
2528
3021
  "ios/tvos: the .mobileprovision file (required; tvOS profiles are distinct from iOS ones)"
2529
3022
  ).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));
2530
3023
  credentials.command("generate").description(
2531
- "Generate an Android keystore server-side and store it as a signing credential. Saves the keystore file locally and prints its passwords ONCE \u2014 they can never be retrieved again."
2532
- ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option(
2533
- "--platform <platform>",
2534
- "android (iOS/tvOS certificates require an Apple Developer account)"
2535
- ).option("--name <name>", "Credential name").option("--app <id>", "Scope to a single app (default: org-wide)").option("--alias <alias>", "Key alias (default: server-chosen)").option("--out <dir>", "Directory to save the keystore file into (default: current directory)").option(
3024
+ "Generate a signing credential server-side. android: saves the keystore file locally and prints its passwords ONCE. ios/tvos: creates the distribution certificate and provisioning profile through the App Store Connect API (requires a stored key \u2014 see `vibeview credentials asc-key add`); fetch the files later with `vibeview credentials download`."
3025
+ ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--platform <platform>", "android, ios, or tvos").option("--name <name>", "Credential name").option("--app <id>", "Scope to a single app (default: org-wide)").option("--alias <alias>", "android: key alias (default: server-chosen)").option(
3026
+ "--bundle-id <id>",
3027
+ "ios/tvos: bundle id for the provisioning profile (default: the --app's package id)"
3028
+ ).option(
3029
+ "--asc-key <name>",
3030
+ "ios/tvos: which stored App Store Connect key to use (default when exactly one)"
3031
+ ).option(
3032
+ "--out <dir>",
3033
+ "android: directory to save the keystore file into (default: current directory)"
3034
+ ).option(
2536
3035
  "--force",
2537
3036
  "Replace an existing credential of the same name \u2014 irreversible; breaks Play Store updates signed with the old key",
2538
3037
  false
2539
3038
  ).option(
2540
3039
  "--json",
2541
- "Print the raw generation response (including the one-time secrets) as JSON",
3040
+ "Print the raw generation response (android: including the one-time secrets) as JSON",
2542
3041
  false
2543
3042
  ).action((opts) => credentialsGenerate(opts));
3043
+ credentials.command("download <name>").description(
3044
+ "Download the files of a credential VibeView GENERATED (uploaded credentials are never readable back). Admin-only; the download is recorded in the audit log."
3045
+ ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--app <id>", "Scope to a single app (default: org-wide)").option("--out <dir>", "Directory to save the files into (default: current directory)").option("--json", "Print the material (including passwords) and saved paths as JSON", false).action((name, opts) => credentialsDownload(name, opts));
3046
+ var ascKey = credentials.command("asc-key").description(
3047
+ "Manage App Store Connect API keys (create one in App Store Connect \u2192 Users and Access \u2192 Integrations) \u2014 they power `credentials generate --platform ios|tvos`"
3048
+ );
3049
+ ascKey.command("add").description(
3050
+ "Store an App Store Connect API key. The .p8 is stored encrypted and never shown again; uploading the same name again rotates the key."
3051
+ ).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));
3052
+ 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));
3053
+ 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));
3054
+ var playKey = credentials.command("play-key").description(
3055
+ "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`"
3056
+ );
3057
+ playKey.command("add").description(
3058
+ "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."
3059
+ ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option(
3060
+ "--file <path>",
3061
+ "The service-account JSON key file (Google Play Console \u2192 API access / Google Cloud service accounts)"
3062
+ ).option("--name <name>", "Key name (default: server-chosen)").option("--json", "Print the raw response as one JSON line", false).action((opts) => playKeyAdd(opts));
3063
+ 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));
3064
+ 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));
2544
3065
  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));
2545
3066
  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));
2546
3067
  var webhooks = program.command("webhooks").description("Manage build-completion webhook endpoints");
@@ -2555,7 +3076,7 @@ program.command("dev-stop").description("Stop a `vibeview dev --detach` session
2555
3076
  program.command("mcp").description(
2556
3077
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop as tools"
2557
3078
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
2558
- const { runMcpServer } = await import("./server-AZLB25BY.js");
3079
+ const { runMcpServer } = await import("./server-GMC6F545.js");
2559
3080
  await runMcpServer(opts);
2560
3081
  });
2561
3082
  program.command("agent-setup").description(