vibeview 0.42.0 → 0.44.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.
@@ -982,6 +982,16 @@ function nativeAndroidDefaults(cwd, production) {
982
982
  artifact: `${m}/build/outputs/apk/debug/*.apk`
983
983
  };
984
984
  }
985
+ var FVM_CONFIGS = [".fvmrc", ".fvm/fvm_config.json"];
986
+ function flutterCommand(cwd) {
987
+ let ig = null;
988
+ try {
989
+ ig = ignore().add(readFileSync5(join5(cwd, ".gitignore"), "utf-8"));
990
+ } catch {
991
+ }
992
+ const pinned = FVM_CONFIGS.some((f) => isFile(join5(cwd, f)) && !ig?.ignores(f));
993
+ return pinned ? "fvm flutter" : "flutter";
994
+ }
985
995
  var FLUTTER_ANDROID_ARTIFACT = "build/app/outputs/flutter-apk/app-debug.apk";
986
996
  var FLUTTER_IOS_ARTIFACT = "build/ios/iphonesimulator/Runner.app";
987
997
  var FLUTTER_ANDROID_PRODUCTION_ARTIFACT = "build/app/outputs/bundle/release/app-release.aab";
@@ -1117,13 +1127,17 @@ function defaultsFor(cwd, platform, evaluatedIosName) {
1117
1127
  function otherFrameworkDefaults(cwd, flavor, platform, production) {
1118
1128
  if (flavor === "flutter") {
1119
1129
  if (!existsSync3(join5(cwd, platform))) return {};
1130
+ const flutter = flutterCommand(cwd);
1120
1131
  if (platform === "android") {
1121
1132
  return production ? {
1122
- buildCommand: "flutter build appbundle --release",
1133
+ buildCommand: `${flutter} build appbundle --release`,
1123
1134
  artifact: FLUTTER_ANDROID_PRODUCTION_ARTIFACT
1124
- } : { buildCommand: "flutter build apk --debug", artifact: FLUTTER_ANDROID_ARTIFACT };
1135
+ } : { buildCommand: `${flutter} build apk --debug`, artifact: FLUTTER_ANDROID_ARTIFACT };
1125
1136
  }
1126
- return production ? {} : { buildCommand: "flutter build ios --simulator --debug", artifact: FLUTTER_IOS_ARTIFACT };
1137
+ return production ? {} : {
1138
+ buildCommand: `${flutter} build ios --simulator --debug`,
1139
+ artifact: FLUTTER_IOS_ARTIFACT
1140
+ };
1127
1141
  }
1128
1142
  if (flavor === "capacitor") return capacitorDefaults(cwd, platform, production);
1129
1143
  if (platform === "android") return nativeAndroidDefaults(cwd, production);
@@ -2730,9 +2744,10 @@ your build command must produce a zip \u2014 use the package/zip step, not deplo
2730
2744
 
2731
2745
  // src/commands/dev.ts
2732
2746
  import { spawn as spawn4 } from "child_process";
2733
- import { createWriteStream, mkdirSync as mkdirSync5 } from "fs";
2734
- 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";
2735
2749
  import { createInterface as createInterface3 } from "readline";
2750
+ import { stripVTControlCharacters } from "util";
2736
2751
  import { select as select3 } from "@inquirer/prompts";
2737
2752
  import chalk4 from "chalk";
2738
2753
  import openBrowser from "open";
@@ -3306,7 +3321,7 @@ async function startDevSession(cfg, onEvent = () => {
3306
3321
  }, onTunnelState) {
3307
3322
  const api = new ApiClient(cfg.baseUrl, cfg.token);
3308
3323
  const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform] && !cfg.standalone;
3309
- 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;
3310
3325
  let app = null;
3311
3326
  let project = null;
3312
3327
  let pick = {
@@ -3530,14 +3545,21 @@ function devSessionCreateBody(cfg) {
3530
3545
  ...cfg.buildId ? { build_id: cfg.buildId } : {}
3531
3546
  };
3532
3547
  }
3533
- function resolveDeviceModel(pools, platform, model) {
3548
+ function resolveDeviceModel(pools, platform, model, category) {
3534
3549
  const { deviceType, tv } = PLATFORM_DEVICE[platform];
3535
3550
  const candidates = pools.filter(
3536
3551
  (p) => p.device_type === deviceType && p.category === "tv" === tv
3537
3552
  );
3538
3553
  const wanted = model.trim();
3539
3554
  const match = candidates.find((p) => p.model === wanted) ?? candidates.find((p) => p.model.toLowerCase() === wanted.toLowerCase());
3540
- 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
+ }
3541
3563
  const names = [...new Set(candidates.map((p) => p.model))].sort();
3542
3564
  const guess = closestModelName(wanted, names);
3543
3565
  throw new Error(
@@ -3731,10 +3753,6 @@ async function ensureConfig(api, opts) {
3731
3753
  console.error(chalk4.red(`error: use --platform tvos or --platform androidtv for TV devices`));
3732
3754
  process.exit(2);
3733
3755
  }
3734
- if (opts.model !== void 0 && opts.deviceCategory) {
3735
- console.error(chalk4.red("error: pass --model or --device-category, not both"));
3736
- process.exit(2);
3737
- }
3738
3756
  if (opts.model !== void 0 && !opts.model.trim()) {
3739
3757
  console.error(
3740
3758
  chalk4.red("error: --model needs a model name (see `vibeview list-devices --models`)")
@@ -3845,7 +3863,8 @@ ${hint}`);
3845
3863
  choices: apps.map((a) => ({ value: a.public_id, name: a.name }))
3846
3864
  });
3847
3865
  }
3848
- 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";
3849
3868
  const metroPortFlag = opts.metroPort ? parseInt(opts.metroPort, 10) : void 0;
3850
3869
  const metroResolved = resolveMetro(effective, platform, metroPortFlag);
3851
3870
  const committedEntry = existing?.platforms?.[platform];
@@ -3925,6 +3944,7 @@ ${hint}`);
3925
3944
  appId,
3926
3945
  deviceCategory,
3927
3946
  model: opts.model,
3947
+ ...opts.model !== void 0 && opts.deviceCategory ? { modelCategory: opts.deviceCategory } : {},
3928
3948
  metroPort: finalMetro.port,
3929
3949
  metroCommand: finalMetro.command,
3930
3950
  platformHasMetroOverride: finalMetro.platformHasOverride,
@@ -4157,6 +4177,7 @@ async function runDevInProcess(opts) {
4157
4177
  appId: cfg.appId,
4158
4178
  deviceCategory: cfg.deviceCategory,
4159
4179
  model: cfg.model,
4180
+ ...cfg.modelCategory ? { modelCategory: cfg.modelCategory } : {},
4160
4181
  buildId: pinnedBuildId,
4161
4182
  projectDir: process.cwd(),
4162
4183
  ...standalone ? { standalone } : {},
@@ -4375,10 +4396,13 @@ async function dev(opts) {
4375
4396
  return runDevInProcess(opts);
4376
4397
  }
4377
4398
  const rawArgs = process.argv.slice(2).filter((a) => a !== "--detach");
4399
+ const stderrLog = detachLogPath(process.cwd());
4400
+ const stderrFd = openDetachLog(stderrLog);
4378
4401
  const child = spawn4(process.execPath, [process.argv[1], ...rawArgs, "--json", "--no-open"], {
4379
4402
  detached: true,
4380
- stdio: ["ignore", "pipe", "ignore"]
4403
+ stdio: ["ignore", "pipe", stderrFd ?? "ignore"]
4381
4404
  });
4405
+ if (stderrFd !== null) closeSync(stderrFd);
4382
4406
  const rl = createInterface3({ input: child.stdout });
4383
4407
  let settled = false;
4384
4408
  await new Promise((resolve, reject) => {
@@ -4425,7 +4449,8 @@ async function dev(opts) {
4425
4449
  child.on("exit", (code) => {
4426
4450
  if (settled) return;
4427
4451
  rl.close();
4428
- 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}` : "");
4429
4454
  emit(opts.json, { event: "error", message });
4430
4455
  if (!opts.json) console.error(chalk4.red(`error: ${message}`));
4431
4456
  reject(new Error(message));
@@ -4443,6 +4468,30 @@ async function dev(opts) {
4443
4468
  });
4444
4469
  if (process.exitCode === 1) return;
4445
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
+ }
4446
4495
 
4447
4496
  // src/lib/formatters/devices.ts
4448
4497
  import chalk5 from "chalk";
@@ -4965,6 +5014,24 @@ var AGENT_COMMANDS = [
4965
5014
  }
4966
5015
  ]
4967
5016
  },
5017
+ {
5018
+ // set_status_bar: { preset: 'clean' | 'default' } — the dashboard's
5019
+ // Device tab Clean status bar (simctl status_bar override). iOS phones and
5020
+ // tablets only; tool_dispatch refuses it elsewhere.
5021
+ verb: "set-status-bar",
5022
+ tool: "set_status_bar",
5023
+ 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.",
5024
+ mutating: true,
5025
+ params: [
5026
+ {
5027
+ name: "preset",
5028
+ cli: "positional",
5029
+ type: "string",
5030
+ required: true,
5031
+ description: "'clean' for the screenshot status bar, 'default' for the device's own."
5032
+ }
5033
+ ]
5034
+ },
4968
5035
  {
4969
5036
  // tools.py wait: { seconds?: number } (default: 2). Note the wire arg is
4970
5037
  // "seconds", NOT "ms" — tool_dispatch.py reads args.get("seconds", 2)
@@ -5117,6 +5184,11 @@ function resultLines(result) {
5117
5184
  if (r.action === "location_set" && typeof r.latitude === "number" && typeof r.longitude === "number") {
5118
5185
  lines.push(`location: ${r.latitude}, ${r.longitude}`);
5119
5186
  }
5187
+ if (r.action === "status_bar_set" && typeof r.preset === "string") {
5188
+ lines.push(
5189
+ `status bar: ${r.preset === "clean" ? "clean (9:41, full battery and signal)" : "the device\u2019s own"}`
5190
+ );
5191
+ }
5120
5192
  if (typeof r.note === "string" && r.note) lines.push(chalk6.dim(r.note));
5121
5193
  if (typeof r.message === "string" && r.message) lines.push(r.message);
5122
5194
  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-4WIEZBTP.js";
33
+ } from "./chunk-JUNGGSS3.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-VPNWTXOY.js");
4464
+ const { runMcpServer } = await import("./server-MLDOTK2G.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-4WIEZBTP.js";
33
+ } from "./chunk-JUNGGSS3.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.42.0",
3
+ "version": "0.44.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,10 @@ 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-status-bar clean` before App Store screenshots: 9:41, a full
177
+ battery and full Wi-Fi and cellular signal, kept for the rest of the session
178
+ (`set-status-bar default` puts the real one back). iOS phones and tablets
179
+ only.
176
180
  - Use `open-url` to jump straight to a deep link instead of navigating by
177
181
  hand:
178
182
  ```bash
@@ -290,6 +294,7 @@ human-readable text.
290
294
  | `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
295
  | `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
296
  | `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. |
297
+ | `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
298
  | `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
299
  | `wait` | `[--ms <n>]` | Wait for a specified number of seconds before continuing (default: 2s). |
295
300
  | `find` | `<text>` `[--below <text>] [--above <text>] [--near <text>]` | Find an element by text with optional spatial constraints. |