vibeview 0.43.0 → 0.45.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.
@@ -2744,9 +2744,10 @@ your build command must produce a zip \u2014 use the package/zip step, not deplo
2744
2744
 
2745
2745
  // src/commands/dev.ts
2746
2746
  import { spawn as spawn4 } from "child_process";
2747
- import { createWriteStream, mkdirSync as mkdirSync5 } from "fs";
2748
- import { dirname as dirname5 } from "path";
2747
+ import { closeSync, createWriteStream, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync8 } from "fs";
2748
+ import { dirname as dirname5, join as join9 } from "path";
2749
2749
  import { createInterface as createInterface3 } from "readline";
2750
+ import { stripVTControlCharacters } from "util";
2750
2751
  import { select as select3 } from "@inquirer/prompts";
2751
2752
  import chalk4 from "chalk";
2752
2753
  import openBrowser from "open";
@@ -3320,7 +3321,7 @@ async function startDevSession(cfg, onEvent = () => {
3320
3321
  }, onTunnelState) {
3321
3322
  const api = new ApiClient(cfg.baseUrl, cfg.token);
3322
3323
  const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform] && !cfg.standalone;
3323
- const pool = cfg.model ? resolveDeviceModel(await api.listDeviceModels(), cfg.platform, cfg.model) : null;
3324
+ const pool = cfg.model ? resolveDeviceModel(await api.listDeviceModels(), cfg.platform, cfg.model, cfg.modelCategory) : null;
3324
3325
  let app = null;
3325
3326
  let project = null;
3326
3327
  let pick = {
@@ -3544,14 +3545,21 @@ function devSessionCreateBody(cfg) {
3544
3545
  ...cfg.buildId ? { build_id: cfg.buildId } : {}
3545
3546
  };
3546
3547
  }
3547
- function resolveDeviceModel(pools, platform, model) {
3548
+ function resolveDeviceModel(pools, platform, model, category) {
3548
3549
  const { deviceType, tv } = PLATFORM_DEVICE[platform];
3549
3550
  const candidates = pools.filter(
3550
3551
  (p) => p.device_type === deviceType && p.category === "tv" === tv
3551
3552
  );
3552
3553
  const wanted = model.trim();
3553
3554
  const match = candidates.find((p) => p.model === wanted) ?? candidates.find((p) => p.model.toLowerCase() === wanted.toLowerCase());
3554
- if (match) return match;
3555
+ if (match) {
3556
+ if (category && match.category !== category) {
3557
+ throw new Error(
3558
+ `"${match.model}" is a ${match.category}, not a ${category} \u2014 drop --device-category, the model already decides it`
3559
+ );
3560
+ }
3561
+ return match;
3562
+ }
3555
3563
  const names = [...new Set(candidates.map((p) => p.model))].sort();
3556
3564
  const guess = closestModelName(wanted, names);
3557
3565
  throw new Error(
@@ -3745,10 +3753,6 @@ async function ensureConfig(api, opts) {
3745
3753
  console.error(chalk4.red(`error: use --platform tvos or --platform androidtv for TV devices`));
3746
3754
  process.exit(2);
3747
3755
  }
3748
- if (opts.model !== void 0 && opts.deviceCategory) {
3749
- console.error(chalk4.red("error: pass --model or --device-category, not both"));
3750
- process.exit(2);
3751
- }
3752
3756
  if (opts.model !== void 0 && !opts.model.trim()) {
3753
3757
  console.error(
3754
3758
  chalk4.red("error: --model needs a model name (see `vibeview list-devices --models`)")
@@ -3859,7 +3863,8 @@ ${hint}`);
3859
3863
  choices: apps.map((a) => ({ value: a.public_id, name: a.name }))
3860
3864
  });
3861
3865
  }
3862
- const deviceCategory = isTv ? "tv" : opts.deviceCategory || entry?.deviceCategory || "phone";
3866
+ const categoryFlag = opts.model === void 0 ? opts.deviceCategory : void 0;
3867
+ const deviceCategory = isTv ? "tv" : categoryFlag || entry?.deviceCategory || "phone";
3863
3868
  const metroPortFlag = opts.metroPort ? parseInt(opts.metroPort, 10) : void 0;
3864
3869
  const metroResolved = resolveMetro(effective, platform, metroPortFlag);
3865
3870
  const committedEntry = existing?.platforms?.[platform];
@@ -3939,6 +3944,7 @@ ${hint}`);
3939
3944
  appId,
3940
3945
  deviceCategory,
3941
3946
  model: opts.model,
3947
+ ...opts.model !== void 0 && opts.deviceCategory ? { modelCategory: opts.deviceCategory } : {},
3942
3948
  metroPort: finalMetro.port,
3943
3949
  metroCommand: finalMetro.command,
3944
3950
  platformHasMetroOverride: finalMetro.platformHasOverride,
@@ -4171,6 +4177,7 @@ async function runDevInProcess(opts) {
4171
4177
  appId: cfg.appId,
4172
4178
  deviceCategory: cfg.deviceCategory,
4173
4179
  model: cfg.model,
4180
+ ...cfg.modelCategory ? { modelCategory: cfg.modelCategory } : {},
4174
4181
  buildId: pinnedBuildId,
4175
4182
  projectDir: process.cwd(),
4176
4183
  ...standalone ? { standalone } : {},
@@ -4389,10 +4396,13 @@ async function dev(opts) {
4389
4396
  return runDevInProcess(opts);
4390
4397
  }
4391
4398
  const rawArgs = process.argv.slice(2).filter((a) => a !== "--detach");
4399
+ const stderrLog = detachLogPath(process.cwd());
4400
+ const stderrFd = openDetachLog(stderrLog);
4392
4401
  const child = spawn4(process.execPath, [process.argv[1], ...rawArgs, "--json", "--no-open"], {
4393
4402
  detached: true,
4394
- stdio: ["ignore", "pipe", "ignore"]
4403
+ stdio: ["ignore", "pipe", stderrFd ?? "ignore"]
4395
4404
  });
4405
+ if (stderrFd !== null) closeSync(stderrFd);
4396
4406
  const rl = createInterface3({ input: child.stdout });
4397
4407
  let settled = false;
4398
4408
  await new Promise((resolve, reject) => {
@@ -4439,7 +4449,8 @@ async function dev(opts) {
4439
4449
  child.on("exit", (code) => {
4440
4450
  if (settled) return;
4441
4451
  rl.close();
4442
- const message = `dev session exited before starting (code ${code ?? "unknown"})`;
4452
+ const reason = lastErrorLine(stderrLog);
4453
+ const message = `dev session exited before starting (code ${code ?? "unknown"})` + (reason ? `: ${reason}` : "");
4443
4454
  emit(opts.json, { event: "error", message });
4444
4455
  if (!opts.json) console.error(chalk4.red(`error: ${message}`));
4445
4456
  reject(new Error(message));
@@ -4457,6 +4468,30 @@ async function dev(opts) {
4457
4468
  });
4458
4469
  if (process.exitCode === 1) return;
4459
4470
  }
4471
+ function detachLogPath(cwd) {
4472
+ return join9(cwd, ".vibeview", "dev-detach.log");
4473
+ }
4474
+ function openDetachLog(path) {
4475
+ try {
4476
+ mkdirSync5(dirname5(path), { recursive: true });
4477
+ ensureGitignored(dirname5(dirname5(path)));
4478
+ return openSync(path, "w");
4479
+ } catch {
4480
+ return null;
4481
+ }
4482
+ }
4483
+ function lastErrorLine(path) {
4484
+ let text;
4485
+ try {
4486
+ text = readFileSync8(path, "utf-8");
4487
+ } catch {
4488
+ return null;
4489
+ }
4490
+ const lines = stripVTControlCharacters(text).split("\n").map((l) => l.trim()).filter(Boolean);
4491
+ const error = [...lines].reverse().find((l) => /^error\b|error:/i.test(l));
4492
+ const line = error ?? lines[lines.length - 1];
4493
+ return line ? line.replace(/^error:\s*/i, "").slice(0, 300) : null;
4494
+ }
4460
4495
 
4461
4496
  // src/lib/formatters/devices.ts
4462
4497
  import chalk5 from "chalk";
@@ -4979,6 +5014,42 @@ var AGENT_COMMANDS = [
4979
5014
  }
4980
5015
  ]
4981
5016
  },
5017
+ {
5018
+ // set_app_language: { language: string | null } — the Device tab's App
5019
+ // language. The worker restarts the app in it; iOS/iPadOS/tvOS and
5020
+ // Android 13+, refused on Roku and embed sessions. 'default' maps to null.
5021
+ verb: "set-language",
5022
+ tool: "set_app_language",
5023
+ summary: "Restart the app under test in another language \u2014 a language tag such as de, fr, ja or pt-BR, or 'default' for the device's language \u2014 e.g. to check translations or take localized screenshots. Only the app changes; the device, its status bar and other apps keep their language. iPhone, iPad, Apple TV and Android 13+; not Roku or embed sessions.",
5024
+ mutating: true,
5025
+ params: [
5026
+ {
5027
+ name: "language",
5028
+ cli: "positional",
5029
+ type: "string",
5030
+ required: true,
5031
+ description: "A language tag (de, fr, ja, pt-BR, zh-Hans, \u2026) or 'default' for the device's own language."
5032
+ }
5033
+ ]
5034
+ },
5035
+ {
5036
+ // set_status_bar: { preset: 'clean' | 'default' } — the dashboard's
5037
+ // Device tab Clean status bar (simctl status_bar override). iOS phones and
5038
+ // tablets only; tool_dispatch refuses it elsewhere.
5039
+ verb: "set-status-bar",
5040
+ tool: "set_status_bar",
5041
+ summary: "Show a clean status bar for screenshots (9:41, full battery, full Wi-Fi and cellular signal) or put the device's own status bar back. It stays until you change it or the session ends. iOS phones and tablets only.",
5042
+ mutating: true,
5043
+ params: [
5044
+ {
5045
+ name: "preset",
5046
+ cli: "positional",
5047
+ type: "string",
5048
+ required: true,
5049
+ description: "'clean' for the screenshot status bar, 'default' for the device's own."
5050
+ }
5051
+ ]
5052
+ },
4982
5053
  {
4983
5054
  // tools.py wait: { seconds?: number } (default: 2). Note the wire arg is
4984
5055
  // "seconds", NOT "ms" — tool_dispatch.py reads args.get("seconds", 2)
@@ -5131,6 +5202,16 @@ function resultLines(result) {
5131
5202
  if (r.action === "location_set" && typeof r.latitude === "number" && typeof r.longitude === "number") {
5132
5203
  lines.push(`location: ${r.latitude}, ${r.longitude}`);
5133
5204
  }
5205
+ if (r.action === "app_language_set") {
5206
+ lines.push(
5207
+ `app language: ${typeof r.language === "string" && r.language ? r.language : "the device language"}`
5208
+ );
5209
+ }
5210
+ if (r.action === "status_bar_set" && typeof r.preset === "string") {
5211
+ lines.push(
5212
+ `status bar: ${r.preset === "clean" ? "clean (9:41, full battery and signal)" : "the device\u2019s own"}`
5213
+ );
5214
+ }
5134
5215
  if (typeof r.note === "string" && r.note) lines.push(chalk6.dim(r.note));
5135
5216
  if (typeof r.message === "string" && r.message) lines.push(r.message);
5136
5217
  if (typeof r.warning === "string" && r.warning) lines.push(r.warning);
package/dist/cli.js CHANGED
@@ -30,7 +30,7 @@ import {
30
30
  runAgentVerb,
31
31
  runStepsNotChecked,
32
32
  stepsNotCheckedLabel
33
- } from "./chunk-6XJIGZIG.js";
33
+ } from "./chunk-YF4L2WO2.js";
34
34
  import {
35
35
  ApiClient,
36
36
  ApiStatusError,
@@ -4461,7 +4461,7 @@ program.command("dev-reload").description(
4461
4461
  program.command("mcp").description(
4462
4462
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
4463
4463
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
4464
- const { runMcpServer } = await import("./server-6JK7PP5N.js");
4464
+ const { runMcpServer } = await import("./server-LC2EYAPO.js");
4465
4465
  await runMcpServer({ ...opts, version: pkg.version });
4466
4466
  });
4467
4467
  program.command("agent-setup").description(
@@ -30,7 +30,7 @@ import {
30
30
  renderEnvelope,
31
31
  resolveMetro,
32
32
  startDevSession
33
- } from "./chunk-6XJIGZIG.js";
33
+ } from "./chunk-YF4L2WO2.js";
34
34
  import {
35
35
  ApiClient,
36
36
  NotFoundError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.43.0",
3
+ "version": "0.45.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",
@@ -173,6 +173,14 @@ appear unexplained in their `git status`.
173
173
  - Use `set-location --lat 51.5074 --lon -0.1278` to place the device before
174
174
  a location-based flow (maps, store finders, region-gated screens); the app
175
175
  reads it like a GPS fix. Not on TV.
176
+ - Use `set-language <tag>` (`de`, `ja`, `pt-BR`, … or `default`) to restart the
177
+ app in another language — for checking translations or localized
178
+ screenshots. Only the app changes; old refs are stale after it. iPhone, iPad,
179
+ Apple TV and Android 13+; not Roku or embed sessions.
180
+ - Use `set-status-bar clean` before App Store screenshots: 9:41, a full
181
+ battery and full Wi-Fi and cellular signal, kept for the rest of the session
182
+ (`set-status-bar default` puts the real one back). iOS phones and tablets
183
+ only.
176
184
  - Use `open-url` to jump straight to a deep link instead of navigating by
177
185
  hand:
178
186
  ```bash
@@ -290,6 +298,8 @@ human-readable text.
290
298
  | `relaunch-app` | — | Close the app and start it again (cold start, data kept) — for "does it persist after a restart" checks. The app log keeps streaming. Not on embed sessions. |
291
299
  | `set-posture` | `<closed\|partial\|open>` or `--angle <0-180>` | Fold or unfold a foldable device (form factor `foldable`: the iPhone Duo, Android foldables) to a preset or an exact hinge angle — exactly one. Reports posture, angle and lit screen. Errors on a device without a hinge. |
292
300
  | `rotate` | `[--degrees <90\|180\|270>]` | Rotate the device and report the new orientation. Phones/tablets and Android foldables toggle portrait/landscape (90 only); the iPhone Duo turns a quarter clockwise by default. Not on TV. |
301
+ | `set-language` | `<tag\|default>` | Restart the app in another language (`de`, `pt-BR`, …) or the device's (`default`). Only the app changes. iPhone, iPad, Apple TV, Android 13+; not Roku or embed sessions. |
302
+ | `set-status-bar` | `<clean\|default>` | Show the clean screenshot status bar (9:41, full battery and signal) or the device's own. iOS phones and tablets only. |
293
303
  | `set-location` | `--lat <n> --lon <n>` | Set the device's simulated GPS position (decimal degrees; west and south are negative) for location-based flows. Phones, tablets and foldables; not on TV. |
294
304
  | `wait` | `[--ms <n>]` | Wait for a specified number of seconds before continuing (default: 2s). |
295
305
  | `find` | `<text>` `[--below <text>] [--above <text>] [--near <text>]` | Find an element by text with optional spatial constraints. |