vibeview 0.46.0 → 0.48.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.
@@ -331,6 +331,11 @@ var ApiClient = class {
331
331
  async listApps() {
332
332
  return this.requestRaw("GET", "/apps/");
333
333
  }
334
+ /** POST /api/v1/agent-reports — a bug, friction or idea about VibeView
335
+ * itself. The server attaches the session's context. */
336
+ async sendFeedback(body) {
337
+ return this.requestRaw("POST", "/agent-reports", body);
338
+ }
334
339
  /** GET /api/v1/apps/{appPublicId} — the app with its uploaded builds,
335
340
  * newest first. */
336
341
  async getApp(appId) {
@@ -13,7 +13,7 @@ import {
13
13
  resolveArtifact,
14
14
  resolveAuth,
15
15
  uploadAppBuild
16
- } from "./chunk-GDTLLJFL.js";
16
+ } from "./chunk-3LXR2YCS.js";
17
17
 
18
18
  // src/lib/formatters/not-checked.ts
19
19
  function notCheckedSteps(steps) {
@@ -190,6 +190,24 @@ function validateBuildEnv(env, platformKey) {
190
190
  }
191
191
  }
192
192
  }
193
+ var XCODE_PLATFORMS = ["ios", "tvos"];
194
+ var XCODE_VERSION_RE = /^\d+\.\d+$/;
195
+ function validateBuildXcode(xcode, platformKey) {
196
+ if (xcode === void 0) return;
197
+ if (!XCODE_PLATFORMS.includes(platformKey)) {
198
+ throw new Error(
199
+ `vibeview.json: platforms.${platformKey}.build.xcode only applies to ios and tvos builds \u2014 remove it from the ${platformKey} block`
200
+ );
201
+ }
202
+ if (typeof xcode !== "string" || !XCODE_VERSION_RE.test(xcode)) {
203
+ throw new Error(
204
+ `vibeview.json: platforms.${platformKey}.build.xcode must be a version string like "27.1"`
205
+ );
206
+ }
207
+ }
208
+ function platformUsesXcode(platformKey) {
209
+ return XCODE_PLATFORMS.includes(platformKey);
210
+ }
193
211
  function saveDevConfig(cwd, cfg) {
194
212
  const target = join(cwd, FILE);
195
213
  const tmp = join(cwd, `.${FILE}.${process.pid}.${Date.now()}.tmp`);
@@ -617,6 +635,19 @@ function clearDevStateIfOwned(cwd, pid) {
617
635
  if (found && found.state.pid === pid) clearDevState(cwd);
618
636
  }
619
637
 
638
+ // src/lib/feedback.ts
639
+ var FEEDBACK_KINDS = ["bug", "friction", "idea"];
640
+ function isFeedbackKind(value) {
641
+ return typeof value === "string" && FEEDBACK_KINDS.includes(value);
642
+ }
643
+ var FEEDBACK_RULES = "Only for problems with VibeView itself (the CLI, MCP tools, devices, sessions, runs, dashboard, docs) \u2014 never for bugs in the user's own app. Show the user what you will send and get their OK first. Never include passwords, tokens, API keys, test data values or personal data. One report per problem; do not resend it. kind: bug (something broke), friction (it worked but was confusing, slow or awkward), idea (a missing capability). user_quote: the user's own words, verbatim \u2014 never a paraphrase.";
644
+ function feedbackSentLine(result) {
645
+ if (result.duplicate) {
646
+ return `This was already sent (report ${result.id}); no need to send it again.`;
647
+ }
648
+ return `Sent (report ${result.id}). The VibeView team reads every report; you'll get a notification if we reply.`;
649
+ }
650
+
620
651
  // src/lib/dev-use.ts
621
652
  import {
622
653
  existsSync as existsSync2,
@@ -2129,6 +2160,13 @@ async function buildCommand(opts, cloudDeps) {
2129
2160
  "usage"
2130
2161
  );
2131
2162
  }
2163
+ if (opts.xcode !== void 0) {
2164
+ fail(
2165
+ opts,
2166
+ "--xcode requires --cloud: a local build uses the Xcode selected on this machine",
2167
+ "usage"
2168
+ );
2169
+ }
2132
2170
  const platform = await resolveBarePlatform(cwd, cfgRef.current, opts);
2133
2171
  const build = await resolveBuildConfig(cwd, cfgRef, platform, opts);
2134
2172
  const result = await runBuildForPlatform(
@@ -2395,7 +2433,17 @@ function formatCost(costMicro) {
2395
2433
  return `$${(costMicro / 1e6).toFixed(2)}`;
2396
2434
  }
2397
2435
  async function runCloudBuildCommand(cwd, cfgRef, opts, deps) {
2436
+ if (opts.xcode !== void 0 && !XCODE_VERSION_RE.test(opts.xcode)) {
2437
+ fail(opts, `--xcode must be a version like 27.1, got "${opts.xcode}"`, "usage");
2438
+ }
2398
2439
  const platforms = await resolveCloudPlatforms(cwd, cfgRef, opts);
2440
+ if (opts.xcode !== void 0 && !platforms.some(platformUsesXcode)) {
2441
+ fail(
2442
+ opts,
2443
+ `--xcode only applies to ios and tvos builds \u2014 this run builds ${platforms.join(", ")}`,
2444
+ "usage"
2445
+ );
2446
+ }
2399
2447
  const { token, baseUrl } = resolveAuth(opts);
2400
2448
  const client = new ApiClient(baseUrl, token);
2401
2449
  const d = deps ?? defaultCloudBuildDeps(client);
@@ -2410,6 +2458,7 @@ async function runCloudBuildCommand(cwd, cfgRef, opts, deps) {
2410
2458
  for (const p of platforms) {
2411
2459
  try {
2412
2460
  validateBuildEnv(cfg?.platforms?.[p]?.build?.env, p);
2461
+ validateBuildXcode(cfg?.platforms?.[p]?.build?.xcode, p);
2413
2462
  if (cfg?.platforms?.[p]?.build?.autoIncrement === true && opts.production && createFromArtifact.has(p)) {
2414
2463
  throw new UsageError(
2415
2464
  `platforms.${p}.build.autoIncrement needs the build linked to an app \u2014 set appId in vibeview.json (a brand-new app's first build can't auto-increment)`
@@ -2486,6 +2535,7 @@ ${offenders}`);
2486
2535
  )
2487
2536
  );
2488
2537
  }
2538
+ const xcode = platformUsesXcode(p) ? opts.xcode ?? build.xcode : void 0;
2489
2539
  return {
2490
2540
  platform: p,
2491
2541
  // OMITTED, never invented, for a platform whose app the artifact will
@@ -2498,7 +2548,8 @@ ${offenders}`);
2498
2548
  artifact_path: artifactPath,
2499
2549
  distribution: production ? "production" : "simulator",
2500
2550
  ...build.env ? { env: build.env } : {},
2501
- ...autoIncrement && production ? { auto_increment: true } : {}
2551
+ ...autoIncrement && production ? { auto_increment: true } : {},
2552
+ ...xcode ? { xcode } : {}
2502
2553
  };
2503
2554
  });
2504
2555
  const uploadSpinner = !opts.json ? createSpinner("uploading source...") : null;
@@ -4933,7 +4984,7 @@ var AGENT_COMMANDS = [
4933
4984
  cli: "positional",
4934
4985
  type: "string",
4935
4986
  required: true,
4936
- description: "'get' returns the alert message and button labels; 'accept' taps a button (first by default); 'dismiss' taps a button (last by default)."
4987
+ description: "'get' returns the alert message and button labels; 'accept' taps a button (first by default); 'dismiss' taps a button (by default a Cancel, Don't Allow or Not Now style button, else the last)."
4937
4988
  },
4938
4989
  {
4939
4990
  name: "button",
@@ -5603,6 +5654,10 @@ export {
5603
5654
  clearDevState,
5604
5655
  readMetroState,
5605
5656
  killOwnedMetro,
5657
+ FEEDBACK_KINDS,
5658
+ isFeedbackKind,
5659
+ FEEDBACK_RULES,
5660
+ feedbackSentLine,
5606
5661
  recordDevUse,
5607
5662
  checkMetro,
5608
5663
  loadLocalDevConfig,
package/dist/cli.js CHANGED
@@ -2,15 +2,19 @@
2
2
  import {
3
3
  AGENT_COMMANDS,
4
4
  BUILD_TERMINAL_STATUSES,
5
+ FEEDBACK_KINDS,
6
+ FEEDBACK_RULES,
5
7
  buildCommand,
6
8
  clearDevState,
7
9
  createRokuPush,
8
10
  dev,
11
+ feedbackSentLine,
9
12
  fetchFinalSummary,
10
13
  filterDeviceModels,
11
14
  formatDeviceModelsHuman,
12
15
  formatDevicesHuman,
13
16
  formatDuration,
17
+ isFeedbackKind,
14
18
  killOwnedMetro,
15
19
  listBuilds,
16
20
  loadDevConfig,
@@ -30,7 +34,7 @@ import {
30
34
  runAgentVerb,
31
35
  runStepsNotChecked,
32
36
  stepsNotCheckedLabel
33
- } from "./chunk-NHM2FHA2.js";
37
+ } from "./chunk-OMIZWILH.js";
34
38
  import {
35
39
  ApiClient,
36
40
  ApiStatusError,
@@ -53,7 +57,7 @@ import {
53
57
  runCommandParseExitCode,
54
58
  saveConfig,
55
59
  uploadApp
56
- } from "./chunk-GDTLLJFL.js";
60
+ } from "./chunk-3LXR2YCS.js";
57
61
 
58
62
  // src/cli.ts
59
63
  import { createRequire } from "module";
@@ -1502,8 +1506,40 @@ async function stopCommand(sessionId, opts, deps) {
1502
1506
  process.exitCode = 0;
1503
1507
  }
1504
1508
 
1505
- // src/commands/create-test.ts
1509
+ // src/commands/feedback.ts
1506
1510
  import chalk10 from "chalk";
1511
+ async function feedbackCommand(kind, summary, opts, version, deps) {
1512
+ if (!isFeedbackKind(kind)) {
1513
+ throw new UsageError(`kind must be one of ${FEEDBACK_KINDS.join(", ")}`);
1514
+ }
1515
+ const line = summary?.trim();
1516
+ if (!line) throw new UsageError('a summary is required: vibeview feedback <kind> "<summary>"');
1517
+ const { token, baseUrl } = resolveAuth(opts);
1518
+ const d = deps ?? {
1519
+ sendFeedback: (b) => new ApiClient(baseUrl, token).sendFeedback(b)
1520
+ };
1521
+ const body = {
1522
+ kind,
1523
+ summary: line,
1524
+ client: "cli",
1525
+ client_version: version
1526
+ };
1527
+ if (opts.details?.trim()) body.details = opts.details;
1528
+ if (opts.quote?.trim()) body.user_quote = opts.quote;
1529
+ const sessionId = opts.session?.trim() || readDevState(process.cwd())?.session_id;
1530
+ if (sessionId) body.session_id = sessionId;
1531
+ try {
1532
+ const result = await d.sendFeedback(body);
1533
+ console.log(opts.json ? JSON.stringify(result) : feedbackSentLine(result));
1534
+ } catch (err) {
1535
+ const message = err instanceof Error ? err.message : String(err);
1536
+ console.error(chalk10.red(`error: ${message}`));
1537
+ process.exitCode = errorExitCode(err);
1538
+ }
1539
+ }
1540
+
1541
+ // src/commands/create-test.ts
1542
+ import chalk11 from "chalk";
1507
1543
  import { existsSync, readFileSync } from "fs";
1508
1544
  async function readStdin() {
1509
1545
  return new Promise((resolve2, reject) => {
@@ -1618,23 +1654,23 @@ async function createTest(options) {
1618
1654
  );
1619
1655
  } else {
1620
1656
  console.log(`
1621
- ${chalk10.cyan(result.public_id)} ${result.name}`);
1657
+ ${chalk11.cyan(result.public_id)} ${result.name}`);
1622
1658
  if (result.app_name) {
1623
- console.log(` ${chalk10.dim(`App: ${result.app_name}`)}`);
1659
+ console.log(` ${chalk11.dim(`App: ${result.app_name}`)}`);
1624
1660
  }
1625
1661
  if (result.platform) {
1626
- console.log(` ${chalk10.dim(`Platform: ${result.platform}`)}`);
1662
+ console.log(` ${chalk11.dim(`Platform: ${result.platform}`)}`);
1627
1663
  }
1628
1664
  if (result.groups?.length) {
1629
- console.log(` ${chalk10.dim(`Groups: ${result.groups.map((g) => g.name).join(", ")}`)}`);
1665
+ console.log(` ${chalk11.dim(`Groups: ${result.groups.map((g) => g.name).join(", ")}`)}`);
1630
1666
  }
1631
- console.log(` ${chalk10.dim(`Steps: ${steps.length}`)}`);
1632
- console.log(chalk10.dim(` ${testCaseUrl(baseUrl, result.public_id)}
1667
+ console.log(` ${chalk11.dim(`Steps: ${steps.length}`)}`);
1668
+ console.log(chalk11.dim(` ${testCaseUrl(baseUrl, result.public_id)}
1633
1669
  `));
1634
1670
  }
1635
1671
  } catch (err) {
1636
1672
  if (options.output === "json") printJsonError(err);
1637
- console.error(chalk10.red(err instanceof Error ? err.message : "Failed to create test"));
1673
+ console.error(chalk11.red(err instanceof Error ? err.message : "Failed to create test"));
1638
1674
  process.exit(
1639
1675
  err instanceof ApiStatusError && err.status === 409 ? EXIT_TEST_FAIL : EXIT_INFRA_ERROR
1640
1676
  );
@@ -1642,7 +1678,7 @@ async function createTest(options) {
1642
1678
  }
1643
1679
 
1644
1680
  // src/commands/import-maestro.ts
1645
- import chalk11 from "chalk";
1681
+ import chalk12 from "chalk";
1646
1682
  import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
1647
1683
  import { basename, extname, join, relative, resolve, sep } from "path";
1648
1684
  import { zipSync } from "fflate";
@@ -1716,8 +1752,8 @@ var START_STATE_LABEL = {
1716
1752
  };
1717
1753
  function flagLine(f) {
1718
1754
  const where = [f.file, f.where, f.command].filter(Boolean).join(" \xB7 ");
1719
- const label = f.severity === "rewrite" ? chalk11.red(FLAG_LABEL.rewrite) : FLAG_LABEL[f.severity];
1720
- return ` ${label} ${chalk11.dim(where)} ${f.message}`;
1755
+ const label = f.severity === "rewrite" ? chalk12.red(FLAG_LABEL.rewrite) : FLAG_LABEL[f.severity];
1756
+ return ` ${label} ${chalk12.dim(where)} ${f.message}`;
1721
1757
  }
1722
1758
  function formatImport(result, baseUrl) {
1723
1759
  const lines = [];
@@ -1734,16 +1770,16 @@ function formatImport(result, baseUrl) {
1734
1770
  const meta = [`${t.steps.length} ${t.steps.length === 1 ? "step" : "steps"}`];
1735
1771
  if (t.setup_keys.length) meta.push(`before this test: ${t.setup_keys.join(", ")}`);
1736
1772
  if (t.start_state) meta.push(`start: ${START_STATE_LABEL[t.start_state] ?? t.start_state}`);
1737
- const head = t.importable ? t.name : chalk11.red(`${t.name} (not imported)`);
1738
- lines.push("", ` ${head} ${chalk11.dim(`${t.key} \xB7 ${meta.join(" \xB7 ")}`)}`);
1739
- if (t.created_id) lines.push(` ${chalk11.dim(testCaseUrl(baseUrl, t.created_id))}`);
1773
+ const head = t.importable ? t.name : chalk12.red(`${t.name} (not imported)`);
1774
+ lines.push("", ` ${head} ${chalk12.dim(`${t.key} \xB7 ${meta.join(" \xB7 ")}`)}`);
1775
+ if (t.created_id) lines.push(` ${chalk12.dim(testCaseUrl(baseUrl, t.created_id))}`);
1740
1776
  for (const f of t.flags) lines.push(flagLine(f));
1741
1777
  }
1742
1778
  if (result.test_data.length) {
1743
1779
  lines.push("", "These tests use test data:");
1744
1780
  for (const d of result.test_data) {
1745
1781
  const note = d.status === "needs_value" || d.status === "skipped" || d.suggested_value === null ? "add the value under App notes & data" : d.status === "kept" ? "already set, kept" : d.status === "created" ? "added from the flow" : d.secret ? "stored as a secret" : `flow value: ${d.suggested_value}`;
1746
- lines.push(` ${d.key} ${chalk11.dim(note)}`);
1782
+ lines.push(` ${d.key} ${chalk12.dim(note)}`);
1747
1783
  }
1748
1784
  }
1749
1785
  return lines;
@@ -1782,7 +1818,7 @@ async function importMaestro(path, options) {
1782
1818
  for (const line of formatImport(result, baseUrl)) console.log(line);
1783
1819
  } catch (err) {
1784
1820
  if (options.output === "json") printJsonError(err);
1785
- console.error(chalk11.red(err instanceof Error ? err.message : "Import failed"));
1821
+ console.error(chalk12.red(err instanceof Error ? err.message : "Import failed"));
1786
1822
  const rejected = err instanceof UsageError || err instanceof NotFoundError || err instanceof ApiStatusError && err.status >= 400 && err.status < 500 ? EXIT_TEST_FAIL : EXIT_INFRA_ERROR;
1787
1823
  process.exit(errorExitCode(err, rejected));
1788
1824
  }
@@ -1865,7 +1901,7 @@ async function logout() {
1865
1901
  }
1866
1902
 
1867
1903
  // src/commands/dev-status.ts
1868
- import chalk12 from "chalk";
1904
+ import chalk13 from "chalk";
1869
1905
  function buildLine(info) {
1870
1906
  if (!info || typeof info.app_build_id !== "number") return null;
1871
1907
  const version = info.app_version ? ` version ${info.app_version}${info.app_build_number ? ` (${info.app_build_number})` : ""}` : "";
@@ -1926,7 +1962,7 @@ async function devStatus(opts) {
1926
1962
  console.error(` url: ${state.page_url}`);
1927
1963
  console.error(` started: ${state.started_at}`);
1928
1964
  }
1929
- if (metro) console.error(chalk12.dim(`Metro: pid ${metro.pid} on :${metro.port} (CLI-owned)`));
1965
+ if (metro) console.error(chalk13.dim(`Metro: pid ${metro.pid} on :${metro.port} (CLI-owned)`));
1930
1966
  process.exit(0);
1931
1967
  }
1932
1968
  var LIVE_STATUSES = /* @__PURE__ */ new Set(["pending", "queued", "starting", "active"]);
@@ -1951,7 +1987,7 @@ async function reportOrphan(api, state, opts) {
1951
1987
  );
1952
1988
  } else if (live) {
1953
1989
  console.error(
1954
- chalk12.yellow(
1990
+ chalk13.yellow(
1955
1991
  `\u26A0 The dev process (pid ${state.pid}) has exited, but session ${state.session_id} is still ${status}.`
1956
1992
  )
1957
1993
  );
@@ -1999,12 +2035,12 @@ async function devStop(opts) {
1999
2035
  }
2000
2036
 
2001
2037
  // src/commands/dev-reload.ts
2002
- import chalk13 from "chalk";
2038
+ import chalk14 from "chalk";
2003
2039
  function fail(opts, code, message, exit) {
2004
2040
  if (opts.json) {
2005
2041
  console.log(JSON.stringify({ reloaded: false, code, message }));
2006
2042
  } else {
2007
- console.error(chalk13.red(`error: ${message}`));
2043
+ console.error(chalk14.red(`error: ${message}`));
2008
2044
  }
2009
2045
  process.exit(exit);
2010
2046
  }
@@ -2057,7 +2093,7 @@ async function devReload(opts) {
2057
2093
  api,
2058
2094
  // Build output and the "reloaded in" line are human output: stderr, so
2059
2095
  // stdout stays reserved for the --json result.
2060
- log: (line) => console.error(chalk13.dim(line))
2096
+ log: (line) => console.error(chalk14.dim(line))
2061
2097
  });
2062
2098
  const started = Date.now();
2063
2099
  try {
@@ -2079,10 +2115,10 @@ async function devReload(opts) {
2079
2115
  }
2080
2116
 
2081
2117
  // src/commands/builds.ts
2082
- import chalk15 from "chalk";
2118
+ import chalk16 from "chalk";
2083
2119
 
2084
2120
  // src/lib/formatters/builds.ts
2085
- import chalk14 from "chalk";
2121
+ import chalk15 from "chalk";
2086
2122
  var HEADERS = ["ID", "PLATFORM", "STATUS", "CREATED", "COST", "APP"];
2087
2123
  var UNBILLED_STATUSES = /* @__PURE__ */ new Set(["queued", "running"]);
2088
2124
  function formatCostCell(costMicro, status) {
@@ -2090,13 +2126,13 @@ function formatCostCell(costMicro, status) {
2090
2126
  return `$${(Math.max(costMicro, 0) / 1e6).toFixed(2)}`;
2091
2127
  }
2092
2128
  var STATUS_COLOR = {
2093
- succeeded: chalk14.green,
2094
- failed: chalk14.red,
2095
- timed_out: chalk14.red,
2096
- infra_failed: chalk14.red,
2097
- canceled: chalk14.dim,
2098
- running: chalk14.yellow,
2099
- queued: chalk14.dim
2129
+ succeeded: chalk15.green,
2130
+ failed: chalk15.red,
2131
+ timed_out: chalk15.red,
2132
+ infra_failed: chalk15.red,
2133
+ canceled: chalk15.dim,
2134
+ running: chalk15.yellow,
2135
+ queued: chalk15.dim
2100
2136
  };
2101
2137
  function formatBuildsHuman(builds2) {
2102
2138
  if (builds2.length === 0) {
@@ -2113,7 +2149,7 @@ function formatBuildsHuman(builds2) {
2113
2149
  const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2114
2150
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2115
2151
  const join5 = (cells) => cells.join(" ").trimEnd();
2116
- const lines = [chalk14.bold(join5(padCells(HEADERS)))];
2152
+ const lines = [chalk15.bold(join5(padCells(HEADERS)))];
2117
2153
  for (const row of rows) {
2118
2154
  const padded = padCells(row);
2119
2155
  const colorFn = STATUS_COLOR[row[2]] ?? ((s) => s);
@@ -2172,7 +2208,7 @@ async function buildsLogs(id, opts, deps) {
2172
2208
  console.log(JSON.stringify(logs));
2173
2209
  } else {
2174
2210
  if (logs.content) process.stdout.write(logs.content);
2175
- console.error(chalk15.dim(`status: ${logs.status}`));
2211
+ console.error(chalk16.dim(`status: ${logs.status}`));
2176
2212
  }
2177
2213
  process.exitCode = 0;
2178
2214
  return;
@@ -2269,7 +2305,7 @@ async function buildsDownload(id, opts, deps) {
2269
2305
  if (opts.json) {
2270
2306
  console.log(JSON.stringify(result));
2271
2307
  } else {
2272
- console.log(chalk15.green(`\u2713 saved ${result.path}`));
2308
+ console.log(chalk16.green(`\u2713 saved ${result.path}`));
2273
2309
  }
2274
2310
  process.exitCode = 0;
2275
2311
  } catch (err) {
@@ -2286,7 +2322,7 @@ async function buildsCancel(id, opts, deps) {
2286
2322
  if (opts.json) {
2287
2323
  console.log(JSON.stringify(build));
2288
2324
  } else {
2289
- console.log(`${chalk15.cyan(build.id)} ${build.status}`);
2325
+ console.log(`${chalk16.cyan(build.id)} ${build.status}`);
2290
2326
  }
2291
2327
  process.exitCode = 0;
2292
2328
  } catch (err) {
@@ -2303,7 +2339,7 @@ async function buildsDeleteArtifact(id, opts, deps) {
2303
2339
  if (opts.json) {
2304
2340
  console.log(JSON.stringify(result));
2305
2341
  } else {
2306
- console.log(chalk15.green(`\u2713 deleted artifact for ${id}`));
2342
+ console.log(chalk16.green(`\u2713 deleted artifact for ${id}`));
2307
2343
  }
2308
2344
  process.exitCode = 0;
2309
2345
  } catch (err) {
@@ -2314,11 +2350,11 @@ async function buildsDeleteArtifact(id, opts, deps) {
2314
2350
  function printError(opts, err) {
2315
2351
  const message = err instanceof Error ? err.message : String(err);
2316
2352
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2317
- console.error(chalk15.red(`error: ${message}`));
2353
+ console.error(chalk16.red(`error: ${message}`));
2318
2354
  }
2319
2355
 
2320
2356
  // src/commands/submit.ts
2321
- import chalk16 from "chalk";
2357
+ import chalk17 from "chalk";
2322
2358
  function defaultDeps2(client) {
2323
2359
  return {
2324
2360
  createSubmission: client.createSubmission.bind(client),
@@ -2342,7 +2378,7 @@ function submitConfigFor(cwd, platform) {
2342
2378
  cfg = loadDevConfig(cwd);
2343
2379
  } catch (err) {
2344
2380
  console.error(
2345
- chalk16.yellow(
2381
+ chalk17.yellow(
2346
2382
  `warning: vibeview.json could not be read \u2014 ignoring submit defaults (${err instanceof Error ? err.message : String(err)})`
2347
2383
  )
2348
2384
  );
@@ -2490,12 +2526,12 @@ async function submitCommand(opts, deps) {
2490
2526
  return;
2491
2527
  }
2492
2528
  if (!opts.json) {
2493
- console.error(` ${chalk16.cyan(sub.id)} ${chalk16.dim(`[${sub.platform}]`)}`);
2529
+ console.error(` ${chalk17.cyan(sub.id)} ${chalk17.dim(`[${sub.platform}]`)}`);
2494
2530
  }
2495
2531
  const final = await followSubmission(sub, opts, d);
2496
2532
  if (!opts.json && final.status === "succeeded" && (final.release_status ?? releaseStatus) === "draft") {
2497
2533
  console.error(
2498
- chalk16.dim("release status: draft \u2014 activate it from the Play Console when ready")
2534
+ chalk17.dim("release status: draft \u2014 activate it from the Play Console when ready")
2499
2535
  );
2500
2536
  }
2501
2537
  process.exitCode = final.status === "succeeded" ? 0 : 1;
@@ -2526,11 +2562,11 @@ async function checkStore(platform, androidFamily, ascKey2, opts, deps) {
2526
2562
  });
2527
2563
  emit(opts.json, { event: "preflight", ...result });
2528
2564
  if (!opts.json) {
2529
- const mark = result.ready ? chalk16.green("\u2713") : chalk16.red("\u2717");
2565
+ const mark = result.ready ? chalk17.green("\u2713") : chalk17.red("\u2717");
2530
2566
  console.error(`${mark} ${result.message}`);
2531
2567
  if (result.ready) {
2532
2568
  console.error(
2533
- chalk16.dim(
2569
+ chalk17.dim(
2534
2570
  ` next: vibeview build --cloud --production --platform ${platform}, then vibeview submit --platform ${platform} --latest`
2535
2571
  )
2536
2572
  );
@@ -2564,7 +2600,7 @@ async function linkedIdentifier(platform, opts, deps) {
2564
2600
  }
2565
2601
  if (!app.package_name) return void 0;
2566
2602
  if (!opts.json) {
2567
- console.error(chalk16.dim(`Checking ${app.package_name} (${app.name}, linked in vibeview.json)`));
2603
+ console.error(chalk17.dim(`Checking ${app.package_name} (${app.name}, linked in vibeview.json)`));
2568
2604
  }
2569
2605
  return app.package_name;
2570
2606
  }
@@ -2605,14 +2641,14 @@ async function followSubmission(created, opts, deps) {
2605
2641
  if (opts.json) {
2606
2642
  emit(opts.json, { event: "status", id: created.id, status: "queued" });
2607
2643
  } else {
2608
- console.error(chalk16.dim("\u23F3 queued \u2014 waiting for a submission slot"));
2644
+ console.error(chalk17.dim("\u23F3 queued \u2014 waiting for a submission slot"));
2609
2645
  }
2610
2646
  } else if (log.status === "running" && !printedRunning) {
2611
2647
  printedRunning = true;
2612
2648
  if (opts.json) {
2613
2649
  emit(opts.json, { event: "status", id: created.id, status: "running" });
2614
2650
  } else {
2615
- console.error(`${chalk16.cyan("\u25B6")} submitting`);
2651
+ console.error(`${chalk17.cyan("\u25B6")} submitting`);
2616
2652
  }
2617
2653
  }
2618
2654
  if (log.content) {
@@ -2654,7 +2690,7 @@ The submission continues server-side \u2014 see its outcome on the Builds page i
2654
2690
  });
2655
2691
  if (!opts.json) {
2656
2692
  const ok = final.status === "succeeded";
2657
- const mark = ok ? chalk16.green("\u2713") : chalk16.red("\u2717");
2693
+ const mark = ok ? chalk17.green("\u2713") : chalk17.red("\u2717");
2658
2694
  const duration = formatDuration(final.started_at, final.finished_at);
2659
2695
  let line = `${mark} ${final.status}`;
2660
2696
  if (duration) line += ` (${duration})`;
@@ -2666,11 +2702,11 @@ The submission continues server-side \u2014 see its outcome on the Builds page i
2666
2702
  function printError2(opts, err) {
2667
2703
  const message = err instanceof Error ? err.message : String(err);
2668
2704
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2669
- console.error(chalk16.red(`error: ${message}`));
2705
+ console.error(chalk17.red(`error: ${message}`));
2670
2706
  }
2671
2707
 
2672
2708
  // src/commands/versions.ts
2673
- import chalk17 from "chalk";
2709
+ import chalk18 from "chalk";
2674
2710
  function defaultDeps3(client) {
2675
2711
  return {
2676
2712
  listAppVersions: client.listAppVersions.bind(client),
@@ -2720,7 +2756,7 @@ async function versionsSet(buildNumber, opts, deps) {
2720
2756
  console.log(JSON.stringify(version));
2721
2757
  } else {
2722
2758
  console.log(
2723
- chalk17.green(`\u2713 ${version.platform} build number set to ${version.build_number}`) + chalk17.dim(
2759
+ chalk18.green(`\u2713 ${version.platform} build number set to ${version.build_number}`) + chalk18.dim(
2724
2760
  " \u2014 the next autoIncrement production build gets " + (version.build_number + 1)
2725
2761
  )
2726
2762
  );
@@ -2740,18 +2776,18 @@ function formatVersionsHuman(versions2) {
2740
2776
  const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2741
2777
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2742
2778
  const join5 = (cells) => cells.join(" ").trimEnd();
2743
- const lines = [chalk17.bold(join5(padCells(HEADERS2)))];
2779
+ const lines = [chalk18.bold(join5(padCells(HEADERS2)))];
2744
2780
  for (const row of rows) lines.push(join5(padCells(row)));
2745
2781
  return lines.join("\n") + "\n";
2746
2782
  }
2747
2783
  function printError3(opts, err) {
2748
2784
  const message = err instanceof Error ? err.message : String(err);
2749
2785
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
2750
- console.error(chalk17.red(`error: ${message}`));
2786
+ console.error(chalk18.red(`error: ${message}`));
2751
2787
  }
2752
2788
 
2753
2789
  // src/commands/whoami.ts
2754
- import chalk18 from "chalk";
2790
+ import chalk19 from "chalk";
2755
2791
  function defaultDeps4(opts) {
2756
2792
  const { token, baseUrl } = resolveAuth(opts);
2757
2793
  const client = new ApiClient(baseUrl, token);
@@ -2782,33 +2818,33 @@ async function whoAmI(opts, deps) {
2782
2818
  process.exitCode = 0;
2783
2819
  return;
2784
2820
  }
2785
- console.log(`${chalk18.bold("Logged in as")} ${me.email}`);
2821
+ console.log(`${chalk19.bold("Logged in as")} ${me.email}`);
2786
2822
  if (me.memberships.length === 1) {
2787
- console.log(`${chalk18.bold("Organization")} ${me.memberships[0].organization_name}`);
2823
+ console.log(`${chalk19.bold("Organization")} ${me.memberships[0].organization_name}`);
2788
2824
  } else if (me.memberships.length > 1) {
2789
- console.log(chalk18.bold("Organizations"));
2825
+ console.log(chalk19.bold("Organizations"));
2790
2826
  for (const m of me.memberships) {
2791
2827
  console.log(` ${m.organization_name} (${m.role})`);
2792
2828
  }
2793
2829
  }
2794
- console.log(`${chalk18.bold("Member role")} ${me.role ?? "\u2014"}`);
2795
- console.log(`${chalk18.bold("Token role")} ${me.token_role ?? "\u2014 (session auth)"}`);
2796
- console.log(`${chalk18.bold("Effective role")} ${effective ?? "\u2014"}`);
2830
+ console.log(`${chalk19.bold("Member role")} ${me.role ?? "\u2014"}`);
2831
+ console.log(`${chalk19.bold("Token role")} ${me.token_role ?? "\u2014 (session auth)"}`);
2832
+ console.log(`${chalk19.bold("Effective role")} ${effective ?? "\u2014"}`);
2797
2833
  if (me.token_access) {
2798
2834
  console.log(
2799
- `${chalk18.bold("Access")} ${me.token_access === "ci" ? "CI only (upload builds, run tests, read results)" : "Full access"}`
2835
+ `${chalk19.bold("Access")} ${me.token_access === "ci" ? "CI only (upload builds, run tests, read results)" : "Full access"}`
2800
2836
  );
2801
- console.log(`${chalk18.bold("Expires")} ${describeExpiry(me.token_expires_at)}`);
2837
+ console.log(`${chalk19.bold("Expires")} ${describeExpiry(me.token_expires_at)}`);
2802
2838
  }
2803
2839
  if (me.is_superadmin) {
2804
- console.log(chalk18.dim("Superadmin (platform-wide; does not raise the org role above)"));
2840
+ console.log(chalk19.dim("Superadmin (platform-wide; does not raise the org role above)"));
2805
2841
  }
2806
2842
  if (me.role === "admin" && me.token_role && me.token_role !== "admin" && me.token_access !== "ci") {
2807
2843
  console.log(
2808
- chalk18.yellow(
2844
+ chalk19.yellow(
2809
2845
  `
2810
- 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 ${chalk18.bold("vibeview env set")}) will fail with a permissions error.
2811
- Create an admin API token in the dashboard and use ${chalk18.bold("--token")} or ${chalk18.bold("VIBEVIEW_API_TOKEN")}.`
2846
+ 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 ${chalk19.bold("vibeview env set")}) will fail with a permissions error.
2847
+ Create an admin API token in the dashboard and use ${chalk19.bold("--token")} or ${chalk19.bold("VIBEVIEW_API_TOKEN")}.`
2812
2848
  )
2813
2849
  );
2814
2850
  }
@@ -2818,7 +2854,7 @@ Create an admin API token in the dashboard and use ${chalk18.bold("--token")} or
2818
2854
  if (opts.json) {
2819
2855
  console.log(JSON.stringify({ event: "error", message }));
2820
2856
  }
2821
- console.error(chalk18.red(`error: ${message}`));
2857
+ console.error(chalk19.red(`error: ${message}`));
2822
2858
  process.exitCode = errorExitCode(err);
2823
2859
  }
2824
2860
  }
@@ -2826,7 +2862,7 @@ Create an admin API token in the dashboard and use ${chalk18.bold("--token")} or
2826
2862
  // src/commands/env.ts
2827
2863
  import { chmod, open as open2, readFile as readFileAsync } from "fs/promises";
2828
2864
  import { basename as basename2 } from "path";
2829
- import chalk19 from "chalk";
2865
+ import chalk20 from "chalk";
2830
2866
  import { password } from "@inquirer/prompts";
2831
2867
 
2832
2868
  // src/lib/gitignore.ts
@@ -2882,7 +2918,7 @@ function defaultDeps5(client) {
2882
2918
  isIgnored: (path) => isGitIgnored(path),
2883
2919
  writeFile: writeSecureFile,
2884
2920
  readFile: (path) => readFileAsync(path),
2885
- warn: (message) => console.error(chalk19.yellow(`warning: ${message}`))
2921
+ warn: (message) => console.error(chalk20.yellow(`warning: ${message}`))
2886
2922
  };
2887
2923
  }
2888
2924
  async function envList(opts, deps) {
@@ -2914,7 +2950,7 @@ function formatEnvVarsHuman(variables) {
2914
2950
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2915
2951
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
2916
2952
  const join5 = (cells) => cells.join(" ").trimEnd();
2917
- const lines = [chalk19.bold(join5(padCells(HEADERS3)))];
2953
+ const lines = [chalk20.bold(join5(padCells(HEADERS3)))];
2918
2954
  for (const row of rows) lines.push(join5(padCells(row)));
2919
2955
  return lines.join("\n") + "\n";
2920
2956
  }
@@ -2964,7 +3000,7 @@ function printSetResult(opts, result) {
2964
3000
  if (opts.json) {
2965
3001
  console.log(JSON.stringify(result));
2966
3002
  } else {
2967
- console.log(chalk19.green(`\u2713 set ${result.name} (${result.visibility})`));
3003
+ console.log(chalk20.green(`\u2713 set ${result.name} (${result.visibility})`));
2968
3004
  }
2969
3005
  }
2970
3006
  async function envRm(name, opts, deps) {
@@ -2974,7 +3010,7 @@ async function envRm(name, opts, deps) {
2974
3010
  if (opts.json) {
2975
3011
  console.log(JSON.stringify({ event: "deleted", name }));
2976
3012
  } else {
2977
- console.log(chalk19.green(`\u2713 removed ${name}`));
3013
+ console.log(chalk20.green(`\u2713 removed ${name}`));
2978
3014
  }
2979
3015
  process.exitCode = 0;
2980
3016
  } catch (err) {
@@ -3006,7 +3042,7 @@ async function envPull(opts, deps) {
3006
3042
  if (opts.json) {
3007
3043
  console.log(JSON.stringify({ event: "pulled", path: outPath, count: variables.length }));
3008
3044
  } else {
3009
- console.log(chalk19.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
3045
+ console.log(chalk20.green(`\u2713 wrote ${variables.length} variable(s) to ${outPath}`));
3010
3046
  }
3011
3047
  process.exitCode = 0;
3012
3048
  } catch (err) {
@@ -3045,7 +3081,7 @@ function escapeEnvValue(value) {
3045
3081
  function printError4(opts, err) {
3046
3082
  const message = err instanceof Error ? err.message : String(err);
3047
3083
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
3048
- console.error(chalk19.red(`error: ${message}`));
3084
+ console.error(chalk20.red(`error: ${message}`));
3049
3085
  }
3050
3086
 
3051
3087
  // src/commands/credentials.ts
@@ -3057,7 +3093,7 @@ import {
3057
3093
  } from "fs/promises";
3058
3094
  import { mkdirSync } from "fs";
3059
3095
  import { basename as basename3, extname as extname2, join as join2 } from "path";
3060
- import chalk20 from "chalk";
3096
+ import chalk21 from "chalk";
3061
3097
  import { input, password as password2 } from "@inquirer/prompts";
3062
3098
  function clientFor2(opts) {
3063
3099
  const { token, baseUrl } = resolveAuth(opts);
@@ -3130,7 +3166,7 @@ function defaultDeps6(client) {
3130
3166
  mkdir: (dir) => mkdirSync(dir, { recursive: true }),
3131
3167
  writeFile: (path, data, mode) => writeFileAsync(path, data, mode !== void 0 ? { mode } : void 0),
3132
3168
  chmod: (path, mode) => chmodAsync(path, mode),
3133
- warn: (message) => console.error(chalk20.yellow(`warning: ${message}`))
3169
+ warn: (message) => console.error(chalk21.yellow(`warning: ${message}`))
3134
3170
  };
3135
3171
  }
3136
3172
  var ANDROID_KIND = "android_keystore";
@@ -3199,7 +3235,7 @@ async function credentialsAdd(opts, deps) {
3199
3235
  if (opts.json) {
3200
3236
  console.log(JSON.stringify(result));
3201
3237
  } else {
3202
- console.log(chalk20.green(`\u2713 added ${result.name} (${result.platform})`));
3238
+ console.log(chalk21.green(`\u2713 added ${result.name} (${result.platform})`));
3203
3239
  }
3204
3240
  process.exitCode = 0;
3205
3241
  } catch (err) {
@@ -3257,20 +3293,20 @@ async function credentialsGenerate(opts, deps) {
3257
3293
  }
3258
3294
  if (opts.json) {
3259
3295
  } else {
3260
- console.log(chalk20.green(`\u2713 generated ${result.name} (android)`));
3296
+ console.log(chalk21.green(`\u2713 generated ${result.name} (android)`));
3261
3297
  console.log("");
3262
- console.log(` Keystore saved to: ${chalk20.bold(savedTo)}`);
3263
- console.log(` Key alias: ${chalk20.bold(result.key_alias)}`);
3264
- console.log(` Store password: ${chalk20.bold(result.store_password)}`);
3265
- console.log(` Key password: ${chalk20.bold(result.key_password)}`);
3298
+ console.log(` Keystore saved to: ${chalk21.bold(savedTo)}`);
3299
+ console.log(` Key alias: ${chalk21.bold(result.key_alias)}`);
3300
+ console.log(` Store password: ${chalk21.bold(result.store_password)}`);
3301
+ console.log(` Key password: ${chalk21.bold(result.key_password)}`);
3266
3302
  console.log("");
3267
3303
  console.log(
3268
- chalk20.yellow(
3304
+ chalk21.yellow(
3269
3305
  " These are shown ONCE \u2014 VibeView cannot recover the keystore file or its passwords. Back them up somewhere safe."
3270
3306
  )
3271
3307
  );
3272
3308
  console.log(
3273
- chalk20.yellow(
3309
+ chalk21.yellow(
3274
3310
  " The credential is already uploaded and active for android and androidtv production builds."
3275
3311
  )
3276
3312
  );
@@ -3296,19 +3332,19 @@ async function generateAppleCredential(platform, opts, d) {
3296
3332
  if (opts.json) {
3297
3333
  console.log(JSON.stringify(result));
3298
3334
  } else {
3299
- console.log(chalk20.green(`\u2713 generated ${result.name} (${result.platform})`));
3335
+ console.log(chalk21.green(`\u2713 generated ${result.name} (${result.platform})`));
3300
3336
  console.log("");
3301
- console.log(` Scope: ${chalk20.bold(result.app_public_id ?? "org-wide")}`);
3337
+ console.log(` Scope: ${chalk21.bold(result.app_public_id ?? "org-wide")}`);
3302
3338
  const expires = formatExpires(result);
3303
3339
  if (expires) {
3304
- console.log(` Expires: ${chalk20.bold(expires)}`);
3340
+ console.log(` Expires: ${chalk21.bold(expires)}`);
3305
3341
  }
3306
3342
  console.log("");
3307
3343
  console.log(
3308
3344
  ` The certificate and provisioning profile are stored with VibeView and already active for ${result.platform} production builds.`
3309
3345
  );
3310
3346
  console.log(
3311
- ` Run ${chalk20.bold(`vibeview credentials download ${result.name}`)} if you need the files locally.`
3347
+ ` Run ${chalk21.bold(`vibeview credentials download ${result.name}`)} if you need the files locally.`
3312
3348
  );
3313
3349
  }
3314
3350
  process.exitCode = 0;
@@ -3374,17 +3410,17 @@ async function credentialsDownload(name, opts, deps) {
3374
3410
  await d.chmod(path, 384);
3375
3411
  }
3376
3412
  if (!opts.json) {
3377
- console.log(chalk20.green(`\u2713 downloaded ${material.name} (${material.platform})`));
3413
+ console.log(chalk21.green(`\u2713 downloaded ${material.name} (${material.platform})`));
3378
3414
  console.log("");
3379
- console.log(` Saved to: ${chalk20.bold(savedTo)}`);
3415
+ console.log(` Saved to: ${chalk21.bold(savedTo)}`);
3380
3416
  if (profileSavedTo !== null) {
3381
- console.log(` Profile: ${chalk20.bold(profileSavedTo)}`);
3417
+ console.log(` Profile: ${chalk21.bold(profileSavedTo)}`);
3382
3418
  }
3383
3419
  for (const [key, value] of Object.entries(material.passwords)) {
3384
- console.log(` ${key}: ${chalk20.bold(value)}`);
3420
+ console.log(` ${key}: ${chalk21.bold(value)}`);
3385
3421
  }
3386
3422
  console.log("");
3387
- console.log(chalk20.yellow(" This download is recorded in your organization's audit log."));
3423
+ console.log(chalk21.yellow(" This download is recorded in your organization's audit log."));
3388
3424
  }
3389
3425
  process.exitCode = 0;
3390
3426
  } catch (err) {
@@ -3415,13 +3451,13 @@ async function ascKeyAdd(opts, deps) {
3415
3451
  if (opts.json) {
3416
3452
  console.log(JSON.stringify(result));
3417
3453
  } else {
3418
- console.log(chalk20.green(`\u2713 added App Store Connect key ${result.name}`));
3454
+ console.log(chalk21.green(`\u2713 added App Store Connect key ${result.name}`));
3419
3455
  console.log("");
3420
- console.log(` Key ID: ${chalk20.bold(result.key_id)}`);
3421
- console.log(` Issuer ID: ${chalk20.bold(result.issuer_id)}`);
3456
+ console.log(` Key ID: ${chalk21.bold(result.key_id)}`);
3457
+ console.log(` Issuer ID: ${chalk21.bold(result.issuer_id)}`);
3422
3458
  console.log("");
3423
3459
  console.log(
3424
- chalk20.yellow(
3460
+ chalk21.yellow(
3425
3461
  " The .p8 is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
3426
3462
  )
3427
3463
  );
@@ -3456,7 +3492,7 @@ function formatAscKeysHuman(keys) {
3456
3492
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3457
3493
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3458
3494
  const join5 = (cells) => cells.join(" ").trimEnd();
3459
- const lines = [chalk20.bold(join5(padCells(HEADERS3)))];
3495
+ const lines = [chalk21.bold(join5(padCells(HEADERS3)))];
3460
3496
  for (const row of rows) lines.push(join5(padCells(row)));
3461
3497
  return lines.join("\n") + "\n";
3462
3498
  }
@@ -3467,7 +3503,7 @@ async function ascKeyRm(name, opts, deps) {
3467
3503
  if (opts.json) {
3468
3504
  console.log(JSON.stringify({ event: "deleted", name }));
3469
3505
  } else {
3470
- console.log(chalk20.green(`\u2713 removed App Store Connect key ${name}`));
3506
+ console.log(chalk21.green(`\u2713 removed App Store Connect key ${name}`));
3471
3507
  }
3472
3508
  process.exitCode = 0;
3473
3509
  } catch (err) {
@@ -3492,12 +3528,12 @@ async function playKeyAdd(opts, deps) {
3492
3528
  if (opts.json) {
3493
3529
  console.log(JSON.stringify(result));
3494
3530
  } else {
3495
- console.log(chalk20.green(`\u2713 added Google Play key ${result.name}`));
3531
+ console.log(chalk21.green(`\u2713 added Google Play key ${result.name}`));
3496
3532
  console.log("");
3497
- console.log(` Service account: ${chalk20.bold(result.client_email)}`);
3533
+ console.log(` Service account: ${chalk21.bold(result.client_email)}`);
3498
3534
  console.log("");
3499
3535
  console.log(
3500
- chalk20.yellow(
3536
+ chalk21.yellow(
3501
3537
  " The JSON key is stored encrypted and will never be shown again \u2014 keep your own copy somewhere safe."
3502
3538
  )
3503
3539
  );
@@ -3532,7 +3568,7 @@ function formatPlayKeysHuman(keys) {
3532
3568
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3533
3569
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3534
3570
  const join5 = (cells) => cells.join(" ").trimEnd();
3535
- const lines = [chalk20.bold(join5(padCells(HEADERS3)))];
3571
+ const lines = [chalk21.bold(join5(padCells(HEADERS3)))];
3536
3572
  for (const row of rows) lines.push(join5(padCells(row)));
3537
3573
  return lines.join("\n") + "\n";
3538
3574
  }
@@ -3543,7 +3579,7 @@ async function playKeyRm(name, opts, deps) {
3543
3579
  if (opts.json) {
3544
3580
  console.log(JSON.stringify({ event: "deleted", name }));
3545
3581
  } else {
3546
- console.log(chalk20.green(`\u2713 removed Google Play key ${name}`));
3582
+ console.log(chalk21.green(`\u2713 removed Google Play key ${name}`));
3547
3583
  }
3548
3584
  process.exitCode = 0;
3549
3585
  } catch (err) {
@@ -3612,7 +3648,7 @@ function formatCredentialsHuman(credentials2) {
3612
3648
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3613
3649
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3614
3650
  const join5 = (cells) => cells.join(" ").trimEnd();
3615
- const lines = [chalk20.bold(join5(padCells(HEADERS3)))];
3651
+ const lines = [chalk21.bold(join5(padCells(HEADERS3)))];
3616
3652
  for (const row of rows) lines.push(join5(padCells(row)));
3617
3653
  return lines.join("\n") + "\n";
3618
3654
  }
@@ -3623,7 +3659,7 @@ async function credentialsRm(name, opts, deps) {
3623
3659
  if (opts.json) {
3624
3660
  console.log(JSON.stringify({ event: "deleted", name }));
3625
3661
  } else {
3626
- console.log(chalk20.green(`\u2713 removed ${name}`));
3662
+ console.log(chalk21.green(`\u2713 removed ${name}`));
3627
3663
  }
3628
3664
  process.exitCode = 0;
3629
3665
  } catch (err) {
@@ -3635,16 +3671,16 @@ function printKeystoreWriteFailure(opts, err, result) {
3635
3671
  const message = err instanceof Error ? err.message : String(err);
3636
3672
  if (opts.json) {
3637
3673
  console.log(JSON.stringify({ event: "error", message }));
3638
- console.error(chalk20.red(`error: ${message}`));
3674
+ console.error(chalk21.red(`error: ${message}`));
3639
3675
  return;
3640
3676
  }
3641
- console.error(chalk20.red(`error: ${message}`));
3677
+ console.error(chalk21.red(`error: ${message}`));
3642
3678
  console.error(
3643
- chalk20.red(
3679
+ chalk21.red(
3644
3680
  `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.`
3645
3681
  )
3646
3682
  );
3647
- console.error(chalk20.red("Recover it NOW from the values below \u2014 they will not be shown again:"));
3683
+ console.error(chalk21.red("Recover it NOW from the values below \u2014 they will not be shown again:"));
3648
3684
  console.error("");
3649
3685
  console.error(` Key alias: ${result.key_alias}`);
3650
3686
  console.error(` Store password: ${result.store_password}`);
@@ -3658,11 +3694,11 @@ function printKeystoreWriteFailure(opts, err, result) {
3658
3694
  function printError5(opts, err) {
3659
3695
  const message = err instanceof Error ? err.message : String(err);
3660
3696
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
3661
- console.error(chalk20.red(`error: ${message}`));
3697
+ console.error(chalk21.red(`error: ${message}`));
3662
3698
  }
3663
3699
 
3664
3700
  // src/commands/webhooks.ts
3665
- import chalk21 from "chalk";
3701
+ import chalk22 from "chalk";
3666
3702
  function clientFor3(opts) {
3667
3703
  const { token, baseUrl } = resolveAuth(opts);
3668
3704
  return new ApiClient(baseUrl, token);
@@ -3693,14 +3729,14 @@ async function webhooksAdd(opts, deps) {
3693
3729
  if (opts.json) {
3694
3730
  console.log(JSON.stringify(result));
3695
3731
  } else {
3696
- console.log(chalk21.green(`\u2713 added ${result.id} ${result.url}`));
3732
+ console.log(chalk22.green(`\u2713 added ${result.id} ${result.url}`));
3697
3733
  console.log("");
3698
- console.log(` Signing secret: ${chalk21.bold(result.secret)}`);
3734
+ console.log(` Signing secret: ${chalk22.bold(result.secret)}`);
3699
3735
  if (opts.secret !== void 0) {
3700
- console.log(chalk21.yellow(" (This is the secret you provided.)"));
3736
+ console.log(chalk22.yellow(" (This is the secret you provided.)"));
3701
3737
  }
3702
3738
  console.log(
3703
- chalk21.yellow(" This secret is shown ONCE and cannot be retrieved again \u2014 store it now.")
3739
+ chalk22.yellow(" This secret is shown ONCE and cannot be retrieved again \u2014 store it now.")
3704
3740
  );
3705
3741
  }
3706
3742
  process.exitCode = 0;
@@ -3731,7 +3767,7 @@ function formatWebhooksHuman(webhooks2) {
3731
3767
  const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3732
3768
  const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
3733
3769
  const join5 = (cells) => cells.join(" ").trimEnd();
3734
- const lines = [chalk21.bold(join5(padCells(HEADERS3)))];
3770
+ const lines = [chalk22.bold(join5(padCells(HEADERS3)))];
3735
3771
  for (const row of rows) lines.push(join5(padCells(row)));
3736
3772
  return lines.join("\n") + "\n";
3737
3773
  }
@@ -3757,7 +3793,7 @@ async function webhooksRm(id, opts, deps) {
3757
3793
  if (opts.json) {
3758
3794
  console.log(JSON.stringify({ event: "deleted", id }));
3759
3795
  } else {
3760
- console.log(chalk21.green(`\u2713 removed ${id}`));
3796
+ console.log(chalk22.green(`\u2713 removed ${id}`));
3761
3797
  }
3762
3798
  process.exitCode = 0;
3763
3799
  } catch (err) {
@@ -3790,13 +3826,13 @@ async function webhooksTest(id, opts, deps) {
3790
3826
  if (opts.json) console.log(JSON.stringify(ld));
3791
3827
  if (ld.status === "succeeded") {
3792
3828
  if (!opts.json) {
3793
- console.log(chalk21.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
3829
+ console.log(chalk22.green(`\u2713 Delivered (HTTP ${ld.response_code})`));
3794
3830
  }
3795
3831
  process.exitCode = 0;
3796
3832
  } else {
3797
3833
  if (!opts.json) {
3798
3834
  const code = ld.response_code != null ? `HTTP ${ld.response_code}` : "no response";
3799
- console.error(chalk21.red(`\u2717 Failed (${code}) at ${formatUtcTimestamp(ld.at)}`));
3835
+ console.error(chalk22.red(`\u2717 Failed (${code}) at ${formatUtcTimestamp(ld.at)}`));
3800
3836
  }
3801
3837
  process.exitCode = 1;
3802
3838
  }
@@ -3805,7 +3841,7 @@ async function webhooksTest(id, opts, deps) {
3805
3841
  }
3806
3842
  if (opts.json) console.log(JSON.stringify({ status: "pending" }));
3807
3843
  console.error(
3808
- chalk21.yellow(
3844
+ chalk22.yellow(
3809
3845
  `Delivery still pending after ${TEST_POLL_BUDGET_MS / 1e3}s \u2014 check \`vibeview webhooks list\` later for the result.`
3810
3846
  )
3811
3847
  );
@@ -3818,7 +3854,7 @@ async function webhooksTest(id, opts, deps) {
3818
3854
  function printError6(opts, err) {
3819
3855
  const message = err instanceof Error ? err.message : String(err);
3820
3856
  if (opts.json) console.log(JSON.stringify({ event: "error", message }));
3821
- console.error(chalk21.red(`error: ${message}`));
3857
+ console.error(chalk22.red(`error: ${message}`));
3822
3858
  }
3823
3859
 
3824
3860
  // src/commands/list-devices.ts
@@ -3916,7 +3952,7 @@ import { dirname, join as join3 } from "path";
3916
3952
  import { fileURLToPath } from "url";
3917
3953
  import { spawnSync } from "child_process";
3918
3954
  import { confirm } from "@inquirer/prompts";
3919
- import chalk22 from "chalk";
3955
+ import chalk23 from "chalk";
3920
3956
  function resolveShippedSkillPath(fromDir) {
3921
3957
  for (const rel of ["..", join3("..", "..")]) {
3922
3958
  const candidate = join3(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
@@ -3946,23 +3982,23 @@ function registerMcp() {
3946
3982
  encoding: "utf8"
3947
3983
  });
3948
3984
  if (res.status === 0) {
3949
- console.log(chalk22.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
3985
+ console.log(chalk23.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
3950
3986
  return;
3951
3987
  }
3952
3988
  const out = `${res.stderr ?? ""}${res.stdout ?? ""}`;
3953
3989
  if (out.includes("already exists")) {
3954
- console.log(chalk22.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
3990
+ console.log(chalk23.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
3955
3991
  return;
3956
3992
  }
3957
- console.error(chalk22.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
3958
- console.error(chalk22.yellow(` register it manually: ${MCP_ADD_HINT}`));
3993
+ console.error(chalk23.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
3994
+ console.error(chalk23.yellow(` register it manually: ${MCP_ADD_HINT}`));
3959
3995
  }
3960
3996
  async function agentSetup(opts) {
3961
3997
  const moduleDir = dirname(fileURLToPath(import.meta.url));
3962
3998
  const shipped = resolveShippedSkillPath(moduleDir);
3963
3999
  if (!shipped) {
3964
4000
  console.error(
3965
- chalk22.red(
4001
+ chalk23.red(
3966
4002
  "error: could not find the packaged skill (skills/vibeview-agent/SKILL.md) \u2014 is this a complete install of the vibeview package?"
3967
4003
  )
3968
4004
  );
@@ -3971,9 +4007,9 @@ async function agentSetup(opts) {
3971
4007
  }
3972
4008
  const targetDir = skillInstallDir(opts.project ?? false, process.cwd(), homedir());
3973
4009
  const result = installSkill(shipped, targetDir);
3974
- console.log(chalk22.green(`\u2713 vibeview-agent skill ${result}: ${join3(targetDir, "SKILL.md")}`));
4010
+ console.log(chalk23.green(`\u2713 vibeview-agent skill ${result}: ${join3(targetDir, "SKILL.md")}`));
3975
4011
  console.log(
3976
- chalk22.dim(
4012
+ chalk23.dim(
3977
4013
  opts.project ? " Claude Code discovers it automatically for anyone working in this project." : " Claude Code discovers it automatically in every project on this machine."
3978
4014
  )
3979
4015
  );
@@ -3981,7 +4017,7 @@ async function agentSetup(opts) {
3981
4017
  if (!claudeCliAvailable()) {
3982
4018
  if (opts.mcp === true) {
3983
4019
  console.error(
3984
- chalk22.yellow(`\u26A0 claude CLI not found \u2014 install Claude Code, then run: ${MCP_ADD_HINT}`)
4020
+ chalk23.yellow(`\u26A0 claude CLI not found \u2014 install Claude Code, then run: ${MCP_ADD_HINT}`)
3985
4021
  );
3986
4022
  }
3987
4023
  } else if (opts.mcp === true || process.stdin.isTTY && await confirm({
@@ -3993,7 +4029,7 @@ async function agentSetup(opts) {
3993
4029
  }
3994
4030
  console.log(
3995
4031
  `
3996
- Suggested line for your project's ${chalk22.bold("CLAUDE.md")} (or ${chalk22.bold(
4032
+ Suggested line for your project's ${chalk23.bold("CLAUDE.md")} (or ${chalk23.bold(
3997
4033
  "AGENTS.md"
3998
4034
  )} for other agents):
3999
4035
  `
@@ -4001,7 +4037,7 @@ Suggested line for your project's ${chalk22.bold("CLAUDE.md")} (or ${chalk22.bol
4001
4037
  console.log(` ${CLAUDE_MD_SUGGESTION}
4002
4038
  `);
4003
4039
  console.log(
4004
- chalk22.dim(
4040
+ chalk23.dim(
4005
4041
  "Other agents (Cursor, Copilot, Codex, ...): install the same skill from GitHub with\n npx skills add vibeview/skills\nor point them at the skill file above, or add the MCP server per client \u2014\nsee https://vibeview.io/docs/agent-control"
4006
4042
  )
4007
4043
  );
@@ -4337,6 +4373,9 @@ program.command("build").description("Run your configured build command and uplo
4337
4373
  "--production",
4338
4374
  "--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",
4339
4375
  false
4376
+ ).option(
4377
+ "--xcode <version>",
4378
+ "--cloud only, ios/tvos: the Xcode version to build with (e.g. 27.1), overriding vibeview.json build.xcode for this run"
4340
4379
  ).option("--json", "Print the result as one JSON line (NDJSON events with --cloud)", false).action((opts) => buildCommand(opts));
4341
4380
  program.command("submit").description(
4342
4381
  "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"
@@ -4455,13 +4494,21 @@ program.command("dev-stop").description("Stop a `vibeview dev --detach` session
4455
4494
  program.command("stop <session-id>").description(
4456
4495
  "Stop any session by id \u2014 e.g. one started from the API, the sandbox, or a collaboration link (`dev-stop` stops this project's detached dev session without an id)"
4457
4496
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the result as one JSON line", false).action((sessionId, opts) => stopCommand(sessionId, opts));
4497
+ program.command("feedback <kind> <summary>").description(
4498
+ "Send the VibeView team a bug report, friction note or feature idea about VibeView itself (kind: bug, friction or idea)"
4499
+ ).addHelpText("after", `
4500
+ ${FEEDBACK_RULES}
4501
+ `).option("--details <text>", "What you tried, what happened, what you expected").option(
4502
+ "--session <id>",
4503
+ "Session the report is about (default: this project's dev --detach session, if any)"
4504
+ ).option("--quote <text>", "The user's own words, verbatim").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the result as one JSON line", false).action((kind, summary, opts) => feedbackCommand(kind, summary, opts, pkg.version));
4458
4505
  program.command("dev-reload").description(
4459
4506
  "Roku only: re-package, upload and reload the channel into this project's `vibeview dev --detach` session (the `r` key of the foreground loop)"
4460
4507
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print as one JSON line", false).action((opts) => devReload(opts));
4461
4508
  program.command("mcp").description(
4462
4509
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
4463
4510
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
4464
- const { runMcpServer } = await import("./server-W66J6PY5.js");
4511
+ const { runMcpServer } = await import("./server-3JZ2MN5N.js");
4465
4512
  await runMcpServer({ ...opts, version: pkg.version });
4466
4513
  });
4467
4514
  program.command("agent-setup").description(
@@ -2,6 +2,8 @@
2
2
  import {
3
3
  AGENT_COMMANDS,
4
4
  DEV_PLATFORMS,
5
+ FEEDBACK_KINDS,
6
+ FEEDBACK_RULES,
5
7
  PLATFORM_DEVICE,
6
8
  PLATFORM_NEEDS_METRO,
7
9
  buildLines,
@@ -14,10 +16,12 @@ import {
14
16
  decodeScreenshotResult,
15
17
  describeDevBuild,
16
18
  effectiveDevConfig,
19
+ feedbackSentLine,
17
20
  filterDeviceModels,
18
21
  formatBuildRowsHuman,
19
22
  formatDeviceModelsHuman,
20
23
  isDevPlatform,
24
+ isFeedbackKind,
21
25
  loadDevConfig,
22
26
  loadLocalDevConfig,
23
27
  outcomeOf,
@@ -30,12 +34,12 @@ import {
30
34
  renderEnvelope,
31
35
  resolveMetro,
32
36
  startDevSession
33
- } from "./chunk-NHM2FHA2.js";
37
+ } from "./chunk-OMIZWILH.js";
34
38
  import {
35
39
  ApiClient,
36
40
  NotFoundError,
37
41
  resolveAuth
38
- } from "./chunk-GDTLLJFL.js";
42
+ } from "./chunk-3LXR2YCS.js";
39
43
 
40
44
  // src/mcp/server.ts
41
45
  import { writeFileSync } from "fs";
@@ -243,6 +247,29 @@ var LIST_APPS_DEF = {
243
247
  },
244
248
  required: []
245
249
  };
250
+ var SEND_FEEDBACK_DEF = {
251
+ name: "send_feedback",
252
+ description: `Send the VibeView team a bug report, a friction note or a feature idea about VibeView itself. ${FEEDBACK_RULES} The report includes the session's device and status when it names one.`,
253
+ mutating: true,
254
+ properties: {
255
+ kind: {
256
+ type: "string",
257
+ description: "bug, friction or idea.",
258
+ enum: [...FEEDBACK_KINDS]
259
+ },
260
+ summary: { type: "string", description: "One line; kept to 200 characters." },
261
+ details: {
262
+ type: "string",
263
+ description: "What you tried, what happened, what you expected; kept to 4000 characters."
264
+ },
265
+ session_id: SESSION_ID_PROPERTY,
266
+ user_quote: {
267
+ type: "string",
268
+ description: "The user's own words about the problem, verbatim (optional)."
269
+ }
270
+ },
271
+ required: ["kind", "summary"]
272
+ };
246
273
  var DEV_STOP_DEF = {
247
274
  name: "dev_stop",
248
275
  description: "Stop the dev-loop session started by dev_start.",
@@ -287,7 +314,8 @@ function buildToolDefinitions() {
287
314
  CREATE_TEST_DEF,
288
315
  RUN_TEST_DEF,
289
316
  RUN_GROUP_DEF,
290
- GET_RUN_DEF
317
+ GET_RUN_DEF,
318
+ SEND_FEEDBACK_DEF
291
319
  ];
292
320
  }
293
321
  var runClientOf = (deps) => deps.runClient ?? new ApiClient(deps.auth.baseUrl, deps.auth.token, { client: "mcp" });
@@ -551,6 +579,9 @@ async function dispatchToolCall(toolName, args, deps, extra = {}) {
551
579
  if (toolName === "list_apps") {
552
580
  return handleListApps(args, deps);
553
581
  }
582
+ if (toolName === "send_feedback") {
583
+ return handleSendFeedback(args, deps);
584
+ }
554
585
  if (toolName === "stop_session") {
555
586
  return handleStopSession(args, deps);
556
587
  }
@@ -839,6 +870,28 @@ async function handleListDeviceModels(args, deps) {
839
870
  }))
840
871
  });
841
872
  }
873
+ async function handleSendFeedback(args, deps) {
874
+ if (!isFeedbackKind(args.kind)) {
875
+ return errorResult(`send_feedback: \`kind\` must be one of ${FEEDBACK_KINDS.join(", ")}`);
876
+ }
877
+ const summary = typeof args.summary === "string" ? args.summary.trim() : "";
878
+ if (!summary) return errorResult("send_feedback: `summary` is required");
879
+ const sessionId = typeof args.session_id === "string" && args.session_id.trim() ? args.session_id.trim() : deps.resolveSessionId();
880
+ const body = { kind: args.kind, summary, client: "mcp" };
881
+ if (typeof args.details === "string" && args.details.trim()) body.details = args.details;
882
+ if (typeof args.user_quote === "string" && args.user_quote.trim()) {
883
+ body.user_quote = args.user_quote;
884
+ }
885
+ if (sessionId) body.session_id = sessionId;
886
+ if (deps.clientVersion) body.client_version = deps.clientVersion;
887
+ const send = deps.sendFeedback ?? ((b) => new ApiClient(deps.auth.baseUrl, deps.auth.token, { client: "mcp" }).sendFeedback(b));
888
+ try {
889
+ const result = await send(body);
890
+ return textResult(feedbackSentLine(result), result);
891
+ } catch (err) {
892
+ return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
893
+ }
894
+ }
842
895
  async function handleListApps(args, deps) {
843
896
  const list = deps.listApps ?? (() => new ApiClient(deps.auth.baseUrl, deps.auth.token).listApps());
844
897
  const platform = typeof args.platform === "string" && args.platform ? args.platform : void 0;
@@ -945,7 +998,7 @@ async function handleUploadApp(args, deps) {
945
998
  }
946
999
  const name = typeof args.name === "string" && args.name ? args.name : void 0;
947
1000
  const upload = deps.uploadApp ?? (async (f, n) => {
948
- const { uploadAppBuild } = await import("./upload-app-ZEQMGZIT.js");
1001
+ const { uploadAppBuild } = await import("./upload-app-TDR6PIWD.js");
949
1002
  const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
950
1003
  return result2;
951
1004
  });
@@ -1339,7 +1392,8 @@ var MCP_INSTRUCTIONS = [
1339
1392
  "screenshot writes a PNG and returns its path; pass inline: true to see the image. logs reads the app output after a crash or blank screen.",
1340
1393
  "Tests: create_test, then run_test (or run_group) and get_run. With session_id a run uses the session you hold; without it, a run starts its own new device (never your dev_start session).",
1341
1394
  "A session stopped elsewhere is reported as ended; call dev_start again for a new one.",
1342
- 'A session nobody uses ends after the idle timeout. A result that ends with "Session \u2026 ends in N s for inactivity" means act (tap, type, swipe, press) to keep it, or dev_stop if you are done; ui_tree, screenshot, logs, find and wait do not count as use.'
1395
+ 'A session nobody uses ends after the idle timeout. A result that ends with "Session \u2026 ends in N s for inactivity" means act (tap, type, swipe, press) to keep it, or dev_stop if you are done; ui_tree, screenshot, logs, find and wait do not count as use.',
1396
+ "If VibeView itself gets in your way (an error you cannot explain, a missing capability, confusing output), offer the user to send it with send_feedback."
1343
1397
  ].join("\n");
1344
1398
  function toolInputSchema(def, z) {
1345
1399
  const shape = {};
@@ -1370,7 +1424,8 @@ async function createMcpServer(opts) {
1370
1424
  auth,
1371
1425
  resolveSessionId: makeSessionResolver(devSession),
1372
1426
  agentTool: client.agentTool.bind(client),
1373
- devSession
1427
+ devSession,
1428
+ clientVersion: opts.version
1374
1429
  };
1375
1430
  const server = new McpServer(
1376
1431
  { name: "vibeview", version: opts.version ?? "0.0.0" },
@@ -3,7 +3,7 @@ import {
3
3
  describeBuild,
4
4
  uploadApp,
5
5
  uploadAppBuild
6
- } from "./chunk-GDTLLJFL.js";
6
+ } from "./chunk-3LXR2YCS.js";
7
7
  export {
8
8
  describeBuild,
9
9
  uploadApp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.46.0",
3
+ "version": "0.48.0",
4
4
  "description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku devices: a React Native dev loop, device control for you or your coding agent (CLI or MCP), and tests from CI",
5
5
  "mcpName": "io.github.vibeview/vibeview",
6
6
  "type": "module",
@@ -252,6 +252,19 @@ hit one:
252
252
  the confirmation alone. If it's still there, say what you still see and
253
253
  ask again.
254
254
 
255
+ ### When VibeView itself gets in your way
256
+
257
+ If VibeView — not the user's app — gets in your way (an error you can't
258
+ explain, a missing capability, confusing output), offer to tell the VibeView
259
+ team. Show the user what you will send and send it only with their OK:
260
+
261
+ vibeview feedback bug "dev_start says build not found for a build I just uploaded" \
262
+ --details "upload-app returned build 123; dev --build 123 failed" --session <id>
263
+
264
+ `kind` is `bug`, `friction` (worked, but confusing, slow or awkward) or `idea`.
265
+ Never include passwords, tokens, test data values or personal data. Over MCP
266
+ the same tool is `send_feedback`.
267
+
255
268
  ## 5. Cleanup
256
269
 
257
270
  **Always** stop the session when done verifying — a running session bills
@@ -318,6 +331,7 @@ Dev-loop lifecycle commands (not registry verbs, but needed for every run):
318
331
  | `dev-stop` | End the current project's detached dev session. |
319
332
  | `vibeview stop <session-id>` | End any session by id — one created through the API, the sandbox, or a collaboration link. |
320
333
  | `dev-reload` | Roku only: re-package, upload and restart the channel in the detached session (the `r` key of the foreground loop). |
334
+ | `vibeview feedback <bug\|friction\|idea> "<summary>"` | Send the VibeView team a report about VibeView itself (not your app), only after the user OKs it: `--details`, `--session` (defaults to the detached session, if any), `--quote` (the user's own words). Works with no session. |
321
335
 
322
336
  ## 7. If you're calling VibeView over MCP
323
337