vibeview 0.16.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,6 +34,43 @@ vibeview dev
34
34
 
35
35
  [Live Development docs →](https://vibeview.io/docs/live-development)
36
36
 
37
+ ## Cloud Builds (beta)
38
+
39
+ Build your React Native app on VibeView's build fleet — no Mac, no CI
40
+ pipeline, no local toolchain:
41
+
42
+ ```bash
43
+ vibeview build --cloud # simulator/emulator build
44
+ vibeview build --cloud --production # signed release build (.ipa / .aab)
45
+ ```
46
+
47
+ A regular cloud build lands in your app's Build History, ready to run on a
48
+ cloud device. Add `--production` for a **signed release build** on iOS,
49
+ tvOS, Android, or Android TV, using signing credentials you store once:
50
+
51
+ ```bash
52
+ # Apple: your distribution certificate + provisioning profile
53
+ vibeview credentials add --platform ios --file cert.p12 --profile app.mobileprovision
54
+
55
+ # Android: upload a keystore — or have VibeView generate one (no Java needed)
56
+ vibeview credentials generate --platform android --name release
57
+ ```
58
+
59
+ Credentials are stored encrypted and never readable back. Build-time
60
+ secrets work the same way:
61
+
62
+ ```bash
63
+ vibeview env set NPM_TOKEN "..." # secret by default, injected into builds
64
+ vibeview webhooks add --url https://ci.example.com/hooks/vibeview
65
+ ```
66
+
67
+ Webhooks POST a signed payload to your endpoint whenever a build finishes.
68
+ Manage running builds with `vibeview builds list | logs | download | cancel`.
69
+
70
+ [Cloud Builds docs →](https://vibeview.io/docs/cloud-builds) ·
71
+ [App Signing →](https://vibeview.io/docs/app-signing) ·
72
+ [Build Webhooks →](https://vibeview.io/docs/build-webhooks)
73
+
37
74
  ## Let your AI agent drive the device
38
75
 
39
76
  The same session can be driven by a coding agent, so it can verify its own
@@ -88,9 +125,13 @@ Output formats: `human`, `json`, and `junit` for any CI system. The exit code re
88
125
 
89
126
  | Command | Purpose |
90
127
  | --- | --- |
91
- | `vibeview login` | Authenticate via browser |
128
+ | `vibeview login` / `logout` / `whoami` | Authenticate via browser, sign out, show who you are |
92
129
  | `vibeview dev` | Live development on a cloud device |
93
- | `vibeview build` | Build your app and upload it |
130
+ | `vibeview build` | Build your app — locally, or on the build fleet with `--cloud` (add `--production` for a signed release) |
131
+ | `vibeview builds list/logs/download/cancel/delete-artifact` | Manage cloud builds and their artifacts |
132
+ | `vibeview credentials add/generate/list/rm` | App-signing credentials for production builds |
133
+ | `vibeview env list/set/rm/pull` | Build variables and secrets injected into cloud builds |
134
+ | `vibeview webhooks add/list/rm/test` | Notify your systems when builds finish (signed payloads) |
94
135
  | `vibeview dev-status` / `dev-stop` | Check on, or stop, a backgrounded dev session |
95
136
  | `vibeview agent-setup` | Make VibeView discoverable to your coding agent (skill + MCP) |
96
137
  | `vibeview mcp` | Run the MCP server so an agent can drive the device |
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  resolveAuth,
7
7
  uploadAppBuild
8
- } from "./chunk-2WTLJ66O.js";
8
+ } from "./chunk-SYFMOPJW.js";
9
9
 
10
10
  // src/lib/dev-state.ts
11
11
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
@@ -424,6 +424,38 @@ var ApiClient = class {
424
424
  async deleteSigningCredential(id) {
425
425
  await this.requestRaw("DELETE", `/signing-credentials/${id}`);
426
426
  }
427
+ /**
428
+ * POST /api/v1/signing-credentials/generate. Requires ADMIN role. Generates an Android
429
+ * keystore server-side and stores it as a credential; the response is the
430
+ * ONLY time the keystore bytes and passwords are ever returned. As with
431
+ * addSigningCredential, the CLI's camelCase (`keyAlias`) is converted to
432
+ * the route's snake_case at this boundary and nowhere else.
433
+ */
434
+ async generateSigningCredential(body) {
435
+ return this.requestRaw("POST", "/signing-credentials/generate", {
436
+ name: body.name,
437
+ ...body.app !== void 0 ? { app: body.app } : {},
438
+ ...body.keyAlias !== void 0 ? { key_alias: body.keyAlias } : {},
439
+ ...body.overwrite !== void 0 ? { overwrite: body.overwrite } : {}
440
+ });
441
+ }
442
+ async listWebhooks() {
443
+ return this.requestRaw("GET", "/webhooks");
444
+ }
445
+ async addWebhook(body) {
446
+ return this.requestRaw("POST", "/webhooks", {
447
+ url: body.url,
448
+ ...body.secret !== void 0 ? { secret: body.secret } : {}
449
+ });
450
+ }
451
+ async deleteWebhook(id) {
452
+ await this.requestRaw("DELETE", `/webhooks/${id}`);
453
+ }
454
+ /** Kicks off an async `ping` delivery; the result lands in the endpoint's
455
+ * last_delivery within ~10–15 s — poll listWebhooks() for it. */
456
+ async testWebhook(id) {
457
+ return this.requestRaw("POST", `/webhooks/${id}/test`);
458
+ }
427
459
  };
428
460
  function parseContentDispositionFilename(header) {
429
461
  if (!header) return void 0;
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  readDevState,
14
14
  readMetroState,
15
15
  runAgentVerb
16
- } from "./chunk-BQRAY3BV.js";
16
+ } from "./chunk-3IMJ7FNN.js";
17
17
  import {
18
18
  ApiClient,
19
19
  EXIT_INFRA_ERROR,
@@ -26,7 +26,7 @@ import {
26
26
  resolveAuth,
27
27
  saveConfig,
28
28
  uploadApp
29
- } from "./chunk-2WTLJ66O.js";
29
+ } from "./chunk-SYFMOPJW.js";
30
30
 
31
31
  // src/cli.ts
32
32
  import { createRequire } from "module";
@@ -1173,13 +1173,13 @@ function formatBuildsHuman(builds2) {
1173
1173
  ]);
1174
1174
  const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1175
1175
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
1176
- const join2 = (cells) => cells.join(" ").trimEnd();
1177
- const lines = [chalk10.bold(join2(padCells(HEADERS)))];
1176
+ const join3 = (cells) => cells.join(" ").trimEnd();
1177
+ const lines = [chalk10.bold(join3(padCells(HEADERS)))];
1178
1178
  for (const row of rows) {
1179
1179
  const padded = padCells(row);
1180
1180
  const colorFn = STATUS_COLOR[row[2]] ?? ((s) => s);
1181
1181
  padded[2] = colorFn(row[2]) + padded[2].slice(row[2].length);
1182
- lines.push(join2(padded));
1182
+ lines.push(join3(padded));
1183
1183
  }
1184
1184
  return lines.join("\n") + "\n";
1185
1185
  }
@@ -1526,9 +1526,9 @@ function formatEnvVarsHuman(variables) {
1526
1526
  ]);
1527
1527
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1528
1528
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
1529
- const join2 = (cells) => cells.join(" ").trimEnd();
1530
- const lines = [chalk13.bold(join2(padCells(HEADERS3)))];
1531
- for (const row of rows) lines.push(join2(padCells(row)));
1529
+ const join3 = (cells) => cells.join(" ").trimEnd();
1530
+ const lines = [chalk13.bold(join3(padCells(HEADERS3)))];
1531
+ for (const row of rows) lines.push(join3(padCells(row)));
1532
1532
  return lines.join("\n") + "\n";
1533
1533
  }
1534
1534
  async function envSet(name, value, opts, deps) {
@@ -1662,8 +1662,14 @@ function printError2(opts, err) {
1662
1662
  }
1663
1663
 
1664
1664
  // src/commands/credentials.ts
1665
- import { readFile as readFileAsync2 } from "fs/promises";
1666
- import { basename as basename2, extname } from "path";
1665
+ import {
1666
+ access,
1667
+ chmod as chmodAsync,
1668
+ readFile as readFileAsync2,
1669
+ writeFile as writeFileAsync
1670
+ } from "fs/promises";
1671
+ import { mkdirSync } from "fs";
1672
+ import { basename as basename2, extname, join } from "path";
1667
1673
  import chalk14 from "chalk";
1668
1674
  import { input, password as password2 } from "@inquirer/prompts";
1669
1675
  function clientFor2(opts) {
@@ -1703,9 +1709,19 @@ function defaultDeps4(client) {
1703
1709
  }
1704
1710
  await client.deleteSigningCredential(match.id);
1705
1711
  },
1712
+ generateCredential: (body) => client.generateSigningCredential(body),
1706
1713
  prompt: ({ message, hidden }) => hidden ? password2({ message }) : input({ message }),
1707
1714
  isTTY: Boolean(process.stdin.isTTY),
1708
1715
  readFile: (path) => readFileAsync2(path),
1716
+ fileExists: (path) => access(path).then(
1717
+ () => true,
1718
+ () => false
1719
+ ),
1720
+ // Same precedent as api-client.ts's artifact download: recursive, so an
1721
+ // already-existing directory is a no-op rather than an error.
1722
+ mkdir: (dir) => mkdirSync(dir, { recursive: true }),
1723
+ writeFile: (path, data, mode) => writeFileAsync(path, data, mode !== void 0 ? { mode } : void 0),
1724
+ chmod: (path, mode) => chmodAsync(path, mode),
1709
1725
  warn: (message) => console.error(chalk14.yellow(`warning: ${message}`))
1710
1726
  };
1711
1727
  }
@@ -1783,15 +1799,86 @@ async function credentialsAdd(opts, deps) {
1783
1799
  process.exitCode = 1;
1784
1800
  }
1785
1801
  }
1802
+ async function credentialsGenerate(opts, deps) {
1803
+ const d = deps ?? defaultDeps4(clientFor2(opts));
1804
+ if (!opts.platform) {
1805
+ throw new UsageError('--platform is required ("android" is the only platform available)');
1806
+ }
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
+ );
1811
+ }
1812
+ if (!opts.name) {
1813
+ throw new UsageError("--name is required");
1814
+ }
1815
+ const outDir = opts.out ?? process.cwd();
1816
+ try {
1817
+ d.mkdir(outDir);
1818
+ const expectedPath = join(outDir, `${opts.name}.keystore`);
1819
+ if (await d.fileExists(expectedPath)) {
1820
+ throw new Error(
1821
+ `${expectedPath} already exists \u2014 refusing to overwrite it. Move it away or pass a different --out/--name.`
1822
+ );
1823
+ }
1824
+ const result = await d.generateCredential({
1825
+ name: opts.name,
1826
+ ...opts.app !== void 0 ? { app: opts.app } : {},
1827
+ ...opts.alias !== void 0 ? { keyAlias: opts.alias } : {},
1828
+ // Only ever sent as `overwrite: true` — omitting the field entirely
1829
+ // otherwise keeps the server's default (409 on a name collision) in
1830
+ // charge, so a stale CLI can never accidentally opt out of it.
1831
+ ...opts.force ? { overwrite: true } : {}
1832
+ });
1833
+ const savedTo = join(outDir, basename2(result.filename));
1834
+ if (opts.json) {
1835
+ console.log(JSON.stringify({ ...result, saved_to: savedTo }));
1836
+ }
1837
+ try {
1838
+ if (await d.fileExists(savedTo)) {
1839
+ throw new Error(`${savedTo} already exists \u2014 refusing to overwrite it.`);
1840
+ }
1841
+ await d.writeFile(savedTo, Buffer.from(result.keystore_base64, "base64"), 384);
1842
+ await d.chmod(savedTo, 384);
1843
+ } catch (writeErr) {
1844
+ printKeystoreWriteFailure(opts, writeErr, result);
1845
+ process.exitCode = 1;
1846
+ return;
1847
+ }
1848
+ if (opts.json) {
1849
+ } else {
1850
+ console.log(chalk14.green(`\u2713 generated ${result.name} (android)`));
1851
+ 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)}`);
1856
+ console.log("");
1857
+ console.log(
1858
+ chalk14.yellow(
1859
+ " These are shown ONCE \u2014 VibeView cannot recover the keystore file or its passwords. Back them up somewhere safe."
1860
+ )
1861
+ );
1862
+ console.log(
1863
+ chalk14.yellow(
1864
+ " The credential is already uploaded and active for android and androidtv production builds."
1865
+ )
1866
+ );
1867
+ }
1868
+ process.exitCode = 0;
1869
+ } catch (err) {
1870
+ printError3(opts, err);
1871
+ process.exitCode = 1;
1872
+ }
1873
+ }
1786
1874
  async function credentialsList(opts, deps) {
1787
1875
  const d = deps ?? defaultDeps4(clientFor2(opts));
1788
1876
  try {
1789
1877
  const { credentials: credentials2 } = await d.listCredentials(opts.app);
1790
1878
  for (const c of credentials2) {
1791
1879
  if (c.expires_soon) {
1792
- d.warn(
1793
- `${c.name} (${c.platform}) is expiring soon${c.expires_at ? ` \u2014 ${c.expires_at}` : ""}.`
1794
- );
1880
+ const soonest = earliestExpiry(c);
1881
+ d.warn(`${c.name} (${c.platform}) is expiring soon${soonest ? ` \u2014 ${soonest}` : ""}.`);
1795
1882
  }
1796
1883
  }
1797
1884
  if (opts.json) {
@@ -1813,6 +1900,23 @@ function formatIdentity(c) {
1813
1900
  if (tag === "file-sha256") return `keystore-file sha256:${value}`;
1814
1901
  return c.platform === "android" ? `keystore-file sha256:${c.fingerprint}` : `cert sha1:${c.fingerprint}`;
1815
1902
  }
1903
+ function earliestExpiry(c) {
1904
+ const dates = [c.expires_at, c.profile_expires_at].filter((d) => d != null);
1905
+ if (dates.length === 0) return null;
1906
+ return dates.sort()[0];
1907
+ }
1908
+ function formatExpires(c) {
1909
+ const parts = [];
1910
+ if (c.profile_expires_at) {
1911
+ if (c.expires_at) parts.push(`cert ${c.expires_at}`);
1912
+ parts.push(`profile ${c.profile_expires_at}`);
1913
+ } else if (c.expires_at) {
1914
+ parts.push(c.expires_at);
1915
+ }
1916
+ if (parts.length === 0) return "";
1917
+ const cell = parts.join(" \xB7 ");
1918
+ return c.expires_soon ? `${cell} (soon)` : cell;
1919
+ }
1816
1920
  function formatCredentialsHuman(credentials2) {
1817
1921
  if (credentials2.length === 0) {
1818
1922
  return "No signing credentials found.\n";
@@ -1823,13 +1927,13 @@ function formatCredentialsHuman(credentials2) {
1823
1927
  c.platform,
1824
1928
  c.app_public_id ?? "org-wide",
1825
1929
  formatIdentity(c),
1826
- c.expires_at ? c.expires_soon ? `${c.expires_at} (soon)` : c.expires_at : ""
1930
+ formatExpires(c)
1827
1931
  ]);
1828
1932
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1829
1933
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
1830
- const join2 = (cells) => cells.join(" ").trimEnd();
1831
- const lines = [chalk14.bold(join2(padCells(HEADERS3)))];
1832
- for (const row of rows) lines.push(join2(padCells(row)));
1934
+ const join3 = (cells) => cells.join(" ").trimEnd();
1935
+ const lines = [chalk14.bold(join3(padCells(HEADERS3)))];
1936
+ for (const row of rows) lines.push(join3(padCells(row)));
1833
1937
  return lines.join("\n") + "\n";
1834
1938
  }
1835
1939
  async function credentialsRm(name, opts, deps) {
@@ -1847,14 +1951,198 @@ async function credentialsRm(name, opts, deps) {
1847
1951
  process.exitCode = 1;
1848
1952
  }
1849
1953
  }
1954
+ function printKeystoreWriteFailure(opts, err, result) {
1955
+ const message = err instanceof Error ? err.message : String(err);
1956
+ if (opts.json) {
1957
+ console.log(JSON.stringify({ event: "error", message }));
1958
+ console.error(chalk14.red(`error: ${message}`));
1959
+ return;
1960
+ }
1961
+ console.error(chalk14.red(`error: ${message}`));
1962
+ console.error(
1963
+ chalk14.red(
1964
+ `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
+ )
1966
+ );
1967
+ console.error(chalk14.red("Recover it NOW from the values below \u2014 they will not be shown again:"));
1968
+ console.error("");
1969
+ console.error(` Key alias: ${result.key_alias}`);
1970
+ console.error(` Store password: ${result.store_password}`);
1971
+ console.error(` Key password: ${result.key_password}`);
1972
+ console.error(` Keystore (base64): ${result.keystore_base64}`);
1973
+ console.error("");
1974
+ console.error(
1975
+ ` Restore the file with: echo '${result.keystore_base64}' | base64 -d > ${result.name}.keystore`
1976
+ );
1977
+ }
1850
1978
  function printError3(opts, err) {
1851
1979
  const message = err instanceof Error ? err.message : String(err);
1852
1980
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
1853
1981
  console.error(chalk14.red(`error: ${message}`));
1854
1982
  }
1855
1983
 
1856
- // src/lib/formatters/devices.ts
1984
+ // src/commands/webhooks.ts
1857
1985
  import chalk15 from "chalk";
1986
+ function clientFor3(opts) {
1987
+ const { token, baseUrl } = resolveAuth(opts);
1988
+ return new ApiClient(baseUrl, token);
1989
+ }
1990
+ function defaultDeps5(client) {
1991
+ return {
1992
+ addWebhook: (body) => client.addWebhook(body),
1993
+ listWebhooks: () => client.listWebhooks(),
1994
+ deleteWebhook: (id) => client.deleteWebhook(id),
1995
+ testWebhook: (id) => client.testWebhook(id),
1996
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
1997
+ now: Date.now
1998
+ };
1999
+ }
2000
+ async function webhooksAdd(opts, deps) {
2001
+ if (!opts.url) {
2002
+ throw new UsageError("--url is required");
2003
+ }
2004
+ if (!opts.url.startsWith("https://")) {
2005
+ throw new UsageError(`--url must start with https:// (got "${opts.url}")`);
2006
+ }
2007
+ const d = deps ?? defaultDeps5(clientFor3(opts));
2008
+ try {
2009
+ const result = await d.addWebhook({
2010
+ url: opts.url,
2011
+ ...opts.secret !== void 0 ? { secret: opts.secret } : {}
2012
+ });
2013
+ if (opts.json) {
2014
+ console.log(JSON.stringify(result));
2015
+ } else {
2016
+ console.log(chalk15.green(`\u2713 added ${result.id} ${result.url}`));
2017
+ console.log("");
2018
+ console.log(` Signing secret: ${chalk15.bold(result.secret)}`);
2019
+ if (opts.secret !== void 0) {
2020
+ console.log(chalk15.yellow(" (This is the secret you provided.)"));
2021
+ }
2022
+ console.log(
2023
+ chalk15.yellow(" This secret is shown ONCE and cannot be retrieved again \u2014 store it now.")
2024
+ );
2025
+ }
2026
+ process.exitCode = 0;
2027
+ } catch (err) {
2028
+ printError4(opts, err);
2029
+ process.exitCode = 1;
2030
+ }
2031
+ }
2032
+ function formatLastDelivery(ld) {
2033
+ if (!ld) return "\u2014";
2034
+ if (ld.status === "pending") return "pending";
2035
+ const code = ld.response_code != null ? ` (${ld.response_code})` : "";
2036
+ const at = ld.at != null ? ` ${ld.at}` : "";
2037
+ return `${ld.status}${code}${at}`;
2038
+ }
2039
+ function formatWebhooksHuman(webhooks2) {
2040
+ if (webhooks2.length === 0) {
2041
+ return "No webhooks found.\n";
2042
+ }
2043
+ const HEADERS3 = ["ID", "URL", "ACTIVE", "LAST DELIVERY", "CREATED"];
2044
+ const rows = webhooks2.map((w) => [
2045
+ w.id,
2046
+ w.url,
2047
+ w.active ? "yes" : "no",
2048
+ formatLastDelivery(w.last_delivery),
2049
+ w.created_at
2050
+ ]);
2051
+ const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2052
+ const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2053
+ const join3 = (cells) => cells.join(" ").trimEnd();
2054
+ const lines = [chalk15.bold(join3(padCells(HEADERS3)))];
2055
+ for (const row of rows) lines.push(join3(padCells(row)));
2056
+ return lines.join("\n") + "\n";
2057
+ }
2058
+ async function webhooksList(opts, deps) {
2059
+ const d = deps ?? defaultDeps5(clientFor3(opts));
2060
+ try {
2061
+ const { webhooks: webhooks2 } = await d.listWebhooks();
2062
+ if (opts.json) {
2063
+ console.log(JSON.stringify(webhooks2, null, 2));
2064
+ } else {
2065
+ process.stdout.write(formatWebhooksHuman(webhooks2));
2066
+ }
2067
+ process.exitCode = 0;
2068
+ } catch (err) {
2069
+ printError4(opts, err);
2070
+ process.exitCode = 1;
2071
+ }
2072
+ }
2073
+ async function webhooksRm(id, opts, deps) {
2074
+ const d = deps ?? defaultDeps5(clientFor3(opts));
2075
+ try {
2076
+ await d.deleteWebhook(id);
2077
+ if (opts.json) {
2078
+ console.log(JSON.stringify({ event: "deleted", id }));
2079
+ } else {
2080
+ console.log(chalk15.green(`\u2713 removed ${id}`));
2081
+ }
2082
+ process.exitCode = 0;
2083
+ } catch (err) {
2084
+ printError4(opts, err);
2085
+ process.exitCode = 1;
2086
+ }
2087
+ }
2088
+ function parseUtcTimestamp(iso) {
2089
+ if (iso.endsWith("Z") || iso.endsWith("z")) {
2090
+ return Date.parse(iso);
2091
+ }
2092
+ const timePart = iso.slice(10);
2093
+ if (timePart.includes("+") || timePart.includes("-")) {
2094
+ return Date.parse(iso);
2095
+ }
2096
+ return Date.parse(iso + "Z");
2097
+ }
2098
+ var TEST_POLL_INTERVAL_MS = 3e3;
2099
+ var TEST_POLL_BUDGET_MS = 6e4;
2100
+ async function webhooksTest(id, opts, deps) {
2101
+ const d = deps ?? defaultDeps5(clientFor3(opts));
2102
+ try {
2103
+ const startedAt = d.now();
2104
+ await d.testWebhook(id);
2105
+ while (d.now() - startedAt < TEST_POLL_BUDGET_MS) {
2106
+ await d.sleep(TEST_POLL_INTERVAL_MS);
2107
+ const { webhooks: webhooks2 } = await d.listWebhooks();
2108
+ const ld = webhooks2.find((w) => w.id === id)?.last_delivery;
2109
+ if (ld && ld.status !== "pending" && ld.at != null && parseUtcTimestamp(ld.at) >= startedAt) {
2110
+ if (opts.json) console.log(JSON.stringify(ld));
2111
+ if (ld.status === "succeeded") {
2112
+ if (!opts.json) {
2113
+ console.log(chalk15.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
2114
+ }
2115
+ process.exitCode = 0;
2116
+ } else {
2117
+ if (!opts.json) {
2118
+ const code = ld.response_code != null ? `HTTP ${ld.response_code}` : "no response";
2119
+ console.error(chalk15.red(`\u2717 Failed (${code}) at ${ld.at}`));
2120
+ }
2121
+ process.exitCode = 1;
2122
+ }
2123
+ return;
2124
+ }
2125
+ }
2126
+ if (opts.json) console.log(JSON.stringify({ status: "pending" }));
2127
+ console.error(
2128
+ chalk15.yellow(
2129
+ `Delivery still pending after ${TEST_POLL_BUDGET_MS / 1e3}s \u2014 check \`vibeview webhooks list\` later for the result.`
2130
+ )
2131
+ );
2132
+ process.exitCode = 1;
2133
+ } catch (err) {
2134
+ printError4(opts, err);
2135
+ process.exitCode = 1;
2136
+ }
2137
+ }
2138
+ function printError4(opts, err) {
2139
+ const message = err instanceof Error ? err.message : String(err);
2140
+ if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2141
+ console.error(chalk15.red(`error: ${message}`));
2142
+ }
2143
+
2144
+ // src/lib/formatters/devices.ts
2145
+ import chalk16 from "chalk";
1858
2146
  var HEADERS2 = ["DEVICE_ID", "NAME", "TYPE", "CATEGORY", "OS", "STATUS"];
1859
2147
  function formatDevicesHuman(devices) {
1860
2148
  if (devices.length === 0) {
@@ -1870,9 +2158,9 @@ function formatDevicesHuman(devices) {
1870
2158
  ]);
1871
2159
  const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1872
2160
  const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
1873
- const lines = [chalk15.bold(fmt(HEADERS2))];
2161
+ const lines = [chalk16.bold(fmt(HEADERS2))];
1874
2162
  for (const row of rows.sort(rowSort)) {
1875
- const statusColored = row[5] === "available" ? chalk15.green(row[5]) : row[5] === "busy" ? chalk15.yellow(row[5]) : chalk15.dim(row[5]);
2163
+ const statusColored = row[5] === "available" ? chalk16.green(row[5]) : row[5] === "busy" ? chalk16.yellow(row[5]) : chalk16.dim(row[5]);
1876
2164
  lines.push(fmt([row[0], row[1], row[2], row[3], row[4], statusColored]));
1877
2165
  }
1878
2166
  return lines.join("\n") + "\n";
@@ -1956,28 +2244,28 @@ function readMetadataFile(path) {
1956
2244
  }
1957
2245
 
1958
2246
  // src/commands/agent-setup.ts
1959
- import { copyFileSync, existsSync as existsSync2, mkdirSync } from "fs";
2247
+ import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
1960
2248
  import { homedir } from "os";
1961
- import { dirname, join } from "path";
2249
+ import { dirname, join as join2 } from "path";
1962
2250
  import { fileURLToPath } from "url";
1963
2251
  import { spawnSync } from "child_process";
1964
2252
  import { confirm } from "@inquirer/prompts";
1965
- import chalk16 from "chalk";
2253
+ import chalk17 from "chalk";
1966
2254
  function resolveShippedSkillPath(fromDir) {
1967
- for (const rel of ["..", join("..", "..")]) {
1968
- const candidate = join(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
2255
+ for (const rel of ["..", join2("..", "..")]) {
2256
+ const candidate = join2(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
1969
2257
  if (existsSync2(candidate)) return candidate;
1970
2258
  }
1971
2259
  return null;
1972
2260
  }
1973
2261
  function skillInstallDir(project, cwd, home) {
1974
2262
  const base = project ? cwd : home;
1975
- return join(base, ".claude", "skills", "vibeview-agent");
2263
+ return join2(base, ".claude", "skills", "vibeview-agent");
1976
2264
  }
1977
2265
  function installSkill(shippedPath, targetDir) {
1978
- const target = join(targetDir, "SKILL.md");
2266
+ const target = join2(targetDir, "SKILL.md");
1979
2267
  const existed = existsSync2(target);
1980
- mkdirSync(targetDir, { recursive: true });
2268
+ mkdirSync2(targetDir, { recursive: true });
1981
2269
  copyFileSync(shippedPath, target);
1982
2270
  return existed ? "updated" : "installed";
1983
2271
  }
@@ -1990,23 +2278,23 @@ function registerMcp() {
1990
2278
  encoding: "utf8"
1991
2279
  });
1992
2280
  if (res.status === 0) {
1993
- console.log(chalk16.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
2281
+ console.log(chalk17.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
1994
2282
  return;
1995
2283
  }
1996
2284
  const out = `${res.stderr ?? ""}${res.stdout ?? ""}`;
1997
2285
  if (out.includes("already exists")) {
1998
- console.log(chalk16.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
2286
+ console.log(chalk17.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
1999
2287
  return;
2000
2288
  }
2001
- console.error(chalk16.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
2002
- console.error(chalk16.yellow(" register it manually: claude mcp add vibeview -- vibeview mcp"));
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"));
2003
2291
  }
2004
2292
  async function agentSetup(opts) {
2005
2293
  const moduleDir = dirname(fileURLToPath(import.meta.url));
2006
2294
  const shipped = resolveShippedSkillPath(moduleDir);
2007
2295
  if (!shipped) {
2008
2296
  console.error(
2009
- chalk16.red(
2297
+ chalk17.red(
2010
2298
  "error: could not find the packaged skill (skills/vibeview-agent/SKILL.md) \u2014 is this a complete install of the vibeview package?"
2011
2299
  )
2012
2300
  );
@@ -2015,9 +2303,9 @@ async function agentSetup(opts) {
2015
2303
  }
2016
2304
  const targetDir = skillInstallDir(opts.project ?? false, process.cwd(), homedir());
2017
2305
  const result = installSkill(shipped, targetDir);
2018
- console.log(chalk16.green(`\u2713 vibeview-agent skill ${result}: ${join(targetDir, "SKILL.md")}`));
2306
+ console.log(chalk17.green(`\u2713 vibeview-agent skill ${result}: ${join2(targetDir, "SKILL.md")}`));
2019
2307
  console.log(
2020
- chalk16.dim(
2308
+ chalk17.dim(
2021
2309
  opts.project ? " Claude Code discovers it automatically for anyone working in this project." : " Claude Code discovers it automatically in every project on this machine."
2022
2310
  )
2023
2311
  );
@@ -2025,7 +2313,7 @@ async function agentSetup(opts) {
2025
2313
  if (!claudeCliAvailable()) {
2026
2314
  if (opts.mcp === true) {
2027
2315
  console.error(
2028
- chalk16.yellow(
2316
+ chalk17.yellow(
2029
2317
  "\u26A0 claude CLI not found \u2014 install Claude Code, then run: claude mcp add vibeview -- vibeview mcp"
2030
2318
  )
2031
2319
  );
@@ -2039,7 +2327,7 @@ async function agentSetup(opts) {
2039
2327
  }
2040
2328
  console.log(
2041
2329
  `
2042
- Suggested line for your project's ${chalk16.bold("CLAUDE.md")} (or ${chalk16.bold(
2330
+ Suggested line for your project's ${chalk17.bold("CLAUDE.md")} (or ${chalk17.bold(
2043
2331
  "AGENTS.md"
2044
2332
  )} for other agents):
2045
2333
  `
@@ -2047,7 +2335,7 @@ Suggested line for your project's ${chalk16.bold("CLAUDE.md")} (or ${chalk16.bol
2047
2335
  console.log(` ${CLAUDE_MD_SUGGESTION}
2048
2336
  `);
2049
2337
  console.log(
2050
- chalk16.dim(
2338
+ chalk17.dim(
2051
2339
  "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"
2052
2340
  )
2053
2341
  );
@@ -2239,14 +2527,35 @@ credentials.command("add").description(
2239
2527
  "--profile <path>",
2240
2528
  "ios/tvos: the .mobileprovision file (required; tvOS profiles are distinct from iOS ones)"
2241
2529
  ).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
+ 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(
2536
+ "--force",
2537
+ "Replace an existing credential of the same name \u2014 irreversible; breaks Play Store updates signed with the old key",
2538
+ false
2539
+ ).option(
2540
+ "--json",
2541
+ "Print the raw generation response (including the one-time secrets) as JSON",
2542
+ false
2543
+ ).action((opts) => credentialsGenerate(opts));
2242
2544
  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));
2243
2545
  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
+ var webhooks = program.command("webhooks").description("Manage build-completion webhook endpoints");
2547
+ webhooks.command("add").description(
2548
+ "Add a webhook endpoint (HTTPS only). Prints the signing secret ONCE \u2014 it can never be retrieved again."
2549
+ ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--url <url>", "The HTTPS URL to POST build events to").option("--secret <secret>", "Your own signing secret (default: server-generated)").option("--json", "Print the raw creation response (including the secret) as JSON", false).action((opts) => webhooksAdd(opts));
2550
+ webhooks.command("list").description("List webhook endpoints with their last delivery status (never prints secrets)").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the raw webhook list as a JSON array", false).action((opts) => webhooksList(opts));
2551
+ webhooks.command("rm <id>").description("Remove a webhook endpoint (rotate the secret by rm then add)").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the result as one JSON line", false).action((id, opts) => webhooksRm(id, opts));
2552
+ webhooks.command("test <id>").description("Send a ping delivery and wait up to 60s for the result").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the final delivery result as one JSON line", false).action((id, opts) => webhooksTest(id, opts));
2244
2553
  program.command("dev-status").description("Show the status of a `vibeview dev --detach` session in this project").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print as one JSON line", false).action((opts) => devStatus(opts));
2245
2554
  program.command("dev-stop").description("Stop a `vibeview dev --detach` session in this project").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print as one JSON line", false).action((opts) => devStop(opts));
2246
2555
  program.command("mcp").description(
2247
2556
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop as tools"
2248
2557
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
2249
- const { runMcpServer } = await import("./server-GFRDOV2H.js");
2558
+ const { runMcpServer } = await import("./server-AZLB25BY.js");
2250
2559
  await runMcpServer(opts);
2251
2560
  });
2252
2561
  program.command("agent-setup").description(
@@ -11,11 +11,11 @@ import {
11
11
  readDevState,
12
12
  renderEnvelope,
13
13
  startDevSession
14
- } from "./chunk-BQRAY3BV.js";
14
+ } from "./chunk-3IMJ7FNN.js";
15
15
  import {
16
16
  ApiClient,
17
17
  resolveAuth
18
- } from "./chunk-2WTLJ66O.js";
18
+ } from "./chunk-SYFMOPJW.js";
19
19
 
20
20
  // src/mcp/server.ts
21
21
  import { writeFileSync } from "fs";
@@ -239,7 +239,7 @@ async function handleUploadApp(args, deps) {
239
239
  }
240
240
  const name = typeof args.name === "string" && args.name ? args.name : void 0;
241
241
  const upload = deps.uploadApp ?? (async (f, n) => {
242
- const { uploadAppBuild } = await import("./upload-app-NVPQ7KRO.js");
242
+ const { uploadAppBuild } = await import("./upload-app-XRB7CKA3.js");
243
243
  const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
244
244
  return result2;
245
245
  });
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  uploadApp,
4
4
  uploadAppBuild
5
- } from "./chunk-2WTLJ66O.js";
5
+ } from "./chunk-SYFMOPJW.js";
6
6
  export {
7
7
  uploadApp,
8
8
  uploadAppBuild
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "VibeView CLI -- run mobile tests from CI/CD pipelines",
5
5
  "type": "module",
6
6
  "bin": {