vibeview 0.28.0 → 0.29.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.
@@ -191,9 +191,10 @@ var ApiClient = class {
191
191
  model: options?.model,
192
192
  provider: options?.provider,
193
193
  mode: options?.mode,
194
- // When pinning a specific device, omit device_category — the backend
195
- // rejects the combo.
196
- device_category: options?.device ? void 0 : options?.deviceCategory ?? "phone",
194
+ // Omitted unless asked for: the backend picks tv for a TV suite and
195
+ // phone otherwise. Never sent with a pinned device (the backend
196
+ // rejects the combo).
197
+ device_category: options?.device ? void 0 : options?.deviceCategory,
197
198
  device_id: options?.device,
198
199
  build_id: options?.build,
199
200
  metadata: options?.metadata
@@ -206,7 +207,7 @@ var ApiClient = class {
206
207
  model: options?.model,
207
208
  provider: options?.provider,
208
209
  mode: options?.mode,
209
- device_category: options?.device ? void 0 : options?.deviceCategory ?? "phone",
210
+ device_category: options?.device ? void 0 : options?.deviceCategory,
210
211
  device_id: options?.device,
211
212
  build_id: options?.build,
212
213
  metadata: options?.metadata
@@ -13,10 +13,10 @@ import {
13
13
  resolveArtifact,
14
14
  resolveAuth,
15
15
  uploadAppBuild
16
- } from "./chunk-2JLTC67A.js";
16
+ } from "./chunk-PBMB6TG4.js";
17
17
 
18
18
  // src/lib/dev-config.ts
19
- import { readFileSync, writeFileSync } from "fs";
19
+ import { readFileSync, renameSync, rmSync, writeFileSync } from "fs";
20
20
  import { join } from "path";
21
21
  var DEV_PLATFORMS = ["ios", "android", "tvos", "androidtv", "roku"];
22
22
  var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -166,7 +166,58 @@ function validateBuildEnv(env, platformKey) {
166
166
  }
167
167
  }
168
168
  function saveDevConfig(cwd, cfg) {
169
- writeFileSync(join(cwd, FILE), JSON.stringify(cfg, null, 2) + "\n");
169
+ const target = join(cwd, FILE);
170
+ const tmp = join(cwd, `.${FILE}.${process.pid}.${Date.now()}.tmp`);
171
+ try {
172
+ writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n");
173
+ renameSync(tmp, target);
174
+ } catch (err) {
175
+ rmSync(tmp, { force: true });
176
+ throw err;
177
+ }
178
+ }
179
+ function updateDevConfig(cwd, platform, apply) {
180
+ const fresh = loadDevConfig(cwd) ?? { defaultPlatform: platform, platforms: {} };
181
+ if (apply(fresh) === false) return fresh;
182
+ saveDevConfig(cwd, fresh);
183
+ return fresh;
184
+ }
185
+ function configLeaves(value, path = [], out = []) {
186
+ if (value && typeof value === "object" && !Array.isArray(value)) {
187
+ for (const [k, v] of Object.entries(value)) {
188
+ configLeaves(v, [...path, k], out);
189
+ }
190
+ } else if (value !== void 0) {
191
+ out.push({ path, value });
192
+ }
193
+ return out;
194
+ }
195
+ function applyConfigChanges(target, before, after) {
196
+ const key = (path) => JSON.stringify(path);
197
+ const prev = new Map(configLeaves(before ?? {}).map((l) => [key(l.path), l.value]));
198
+ const root = target;
199
+ for (const { path, value } of configLeaves(after)) {
200
+ const old = prev.get(key(path));
201
+ prev.delete(key(path));
202
+ if (old !== void 0 && JSON.stringify(old) === JSON.stringify(value)) continue;
203
+ let node = root;
204
+ for (const segment of path.slice(0, -1)) {
205
+ const child = node[segment];
206
+ if (!child || typeof child !== "object" || Array.isArray(child)) node[segment] = {};
207
+ node = node[segment];
208
+ }
209
+ node[path[path.length - 1]] = value;
210
+ }
211
+ for (const k of prev.keys()) {
212
+ const path = JSON.parse(k);
213
+ let node = root;
214
+ for (const segment of path.slice(0, -1)) {
215
+ node = node && typeof node === "object" ? node[segment] : void 0;
216
+ }
217
+ if (node && typeof node === "object") {
218
+ delete node[path[path.length - 1]];
219
+ }
220
+ }
170
221
  }
171
222
  function describeConfigChanges(before, after) {
172
223
  const flat = (value, prefix, out) => {
@@ -220,6 +271,10 @@ function buildSavedConfig(input2) {
220
271
  platforms: {
221
272
  ...existing?.platforms,
222
273
  [platform]: {
274
+ // Everything else on the entry (`submit` defaults, keys a newer CLI
275
+ // wrote) is carried forward untouched; the fields below are the ones
276
+ // `vibeview dev` owns.
277
+ ...existing?.platforms?.[platform],
223
278
  appId,
224
279
  deviceCategory: isTv ? void 0 : deviceCategory,
225
280
  ...existing?.platforms?.[platform]?.build ? { build: { ...existing.platforms[platform].build } } : {},
@@ -235,6 +290,11 @@ function buildSavedConfig(input2) {
235
290
  }
236
291
 
237
292
  // src/lib/dev-build.ts
293
+ function canUseMetro(build) {
294
+ if (build.can_use_metro === true || build.can_use_metro === false) return build.can_use_metro;
295
+ if (build.is_debug === true) return true;
296
+ return build.is_debug === false ? false : null;
297
+ }
238
298
  function parseBuildId(raw) {
239
299
  const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw.trim() : "";
240
300
  if (!/^\d+$/.test(text) || Number(text) < 1) {
@@ -244,9 +304,10 @@ function parseBuildId(raw) {
244
304
  }
245
305
  return Number(text);
246
306
  }
247
- function buildKind(isDebug) {
248
- if (isDebug === true) return "debug";
249
- if (isDebug === false) return "release";
307
+ function buildKind(build) {
308
+ const usable = build !== null && typeof build === "object" ? canUseMetro(build) : canUseMetro({ is_debug: build });
309
+ if (usable === true) return "debug";
310
+ if (usable === false) return "release";
250
311
  return "unknown";
251
312
  }
252
313
  function checkDevBuild(app, buildId, platform, opts = {}) {
@@ -266,10 +327,11 @@ function checkDevBuild(app, buildId, platform, opts = {}) {
266
327
  `build ${buildId} is not a build of app ${app.public_id} \u2014 see \`vibeview list-builds ${app.public_id}\``
267
328
  );
268
329
  }
269
- if (!opts.standalone && build.is_debug !== true) {
270
- const why = build.is_debug === false ? "it runs standalone (a release build, or one with an embedded JS bundle)" : "it could not be identified as a Metro debug build";
330
+ const usable = canUseMetro(build);
331
+ if (!opts.standalone && usable !== true) {
332
+ const why = usable === false ? "it is a release build (or not a React Native build), so it cannot load JavaScript from Metro" : "it could not be identified as a React Native debug build";
271
333
  throw new Error(
272
- `build ${buildId} is not a debug build \u2014 ${why}. \`vibeview dev\` needs ${platformWithArticle(platform)} debug simulator/emulator build without an embedded JS bundle (see \`vibeview list-builds ${app.public_id}\`)`
334
+ `build ${buildId} is not a debug build \u2014 ${why}. \`vibeview dev\` needs ${platformWithArticle(platform)} React Native debug simulator/emulator build (see \`vibeview list-builds ${app.public_id}\`); to run this build as it is, with no Metro, use \`vibeview dev --standalone --build ${buildId}\``
273
335
  );
274
336
  }
275
337
  return build;
@@ -283,8 +345,9 @@ function buildRows(app) {
283
345
  version: b.version ?? null,
284
346
  build_number: b.build_number ?? null,
285
347
  platform: app.platform,
286
- kind: buildKind(b.is_debug),
348
+ kind: buildKind(b),
287
349
  is_debug: b.is_debug ?? null,
350
+ can_use_metro: canUseMetro(b),
288
351
  uploaded_at: b.uploaded_at ?? null,
289
352
  note: b.note ?? null
290
353
  }));
@@ -329,7 +392,7 @@ async function listBuilds(appId, opts, deps) {
329
392
  }
330
393
 
331
394
  // src/lib/dev-state.ts
332
- import { existsSync, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
395
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
333
396
  import { dirname, join as join2 } from "path";
334
397
  var STATE_DIR = ".vibeview";
335
398
  var STATE_FILE = "dev-state.json";
@@ -344,18 +407,31 @@ function isPidAlive(pid) {
344
407
  return err.code === "EPERM";
345
408
  }
346
409
  }
410
+ var WHOLE_DIR_LINE = /^(\/?)\.vibeview\/?$/;
347
411
  function ensureGitignored(cwd) {
348
412
  try {
349
413
  const gitignore = join2(cwd, ".gitignore");
350
414
  if (!existsSync(gitignore)) return;
351
415
  const content = readFileSync2(gitignore, "utf-8");
352
- const covered = content.split("\n").map((l) => l.trim()).some((l) => l === ".vibeview" || l === ".vibeview/" || l === "/.vibeview");
353
- if (covered) return;
416
+ const lines = content.split("\n");
417
+ const trimmed = lines.map((l) => l.trim());
418
+ if (trimmed.some((l) => /^\/?\.vibeview\/\*$/.test(l))) return;
419
+ const i = trimmed.findIndex((l) => WHOLE_DIR_LINE.test(l));
420
+ if (i !== -1) {
421
+ const anchor = WHOLE_DIR_LINE.exec(trimmed[i])[1];
422
+ lines.splice(i, 1, `${anchor}.vibeview/*`, `!${anchor}.vibeview/test-spec.json`);
423
+ writeFileSync2(gitignore, lines.join("\n"));
424
+ return;
425
+ }
354
426
  const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
355
- writeFileSync2(gitignore, `${content}${prefix}
356
- # VibeView local session state
357
- .vibeview/
358
- `);
427
+ writeFileSync2(
428
+ gitignore,
429
+ `${content}${prefix}
430
+ # VibeView local session state (test-spec.json is committed for CI)
431
+ .vibeview/*
432
+ !.vibeview/test-spec.json
433
+ `
434
+ );
359
435
  } catch {
360
436
  }
361
437
  }
@@ -386,7 +462,7 @@ function readDevState(cwd) {
386
462
  function clearDevState(cwd) {
387
463
  const path = statePath(cwd);
388
464
  if (existsSync(path)) {
389
- rmSync(path, { force: true });
465
+ rmSync2(path, { force: true });
390
466
  }
391
467
  }
392
468
 
@@ -690,7 +766,7 @@ async function promptMetroCommand(cwd, platform) {
690
766
 
691
767
  // src/lib/source-pack.ts
692
768
  import { execFile, spawn } from "child_process";
693
- import { lstatSync, mkdtempSync, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
769
+ import { lstatSync, mkdtempSync, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
694
770
  import { tmpdir } from "os";
695
771
  import { join as join4 } from "path";
696
772
  import ignore2 from "ignore";
@@ -788,7 +864,7 @@ async function packSource(projectRoot, files, opts = {}) {
788
864
  );
789
865
  }
790
866
  const tmpDir = mkdtempSync(join4(tmpdir(), "vibeview-source-pack-"));
791
- const cleanup = () => rmSync2(tmpDir, { recursive: true, force: true });
867
+ const cleanup = () => rmSync3(tmpDir, { recursive: true, force: true });
792
868
  try {
793
869
  const supportsNull = await probeTarNullSupport();
794
870
  const tarPath = join4(tmpDir, "source.tgz");
@@ -829,7 +905,7 @@ function probeTarNullSupport() {
829
905
  writeFileSync3(listPath, "");
830
906
  const out = join4(tmp, "probe.tgz");
831
907
  execFile("tar", ["-czf", out, "-C", tmp, "--null", "-T", listPath], (err) => {
832
- rmSync2(tmp, { recursive: true, force: true });
908
+ rmSync3(tmp, { recursive: true, force: true });
833
909
  resolve(!err);
834
910
  });
835
911
  });
@@ -933,30 +1009,28 @@ async function resolveBarePlatform(cwd, cfg, opts) {
933
1009
  }
934
1010
  return platform;
935
1011
  }
936
- function persistPromptedBuild(cwd, cfg, platform, build) {
937
- const next = cfg ?? { defaultPlatform: platform, platforms: {} };
938
- const entry = next.platforms[platform];
939
- if (entry) {
940
- entry.build = build;
941
- } else {
942
- next.platforms[platform] = { build };
943
- }
944
- saveDevConfig(cwd, next);
945
- return next;
1012
+ function persistPromptedBuild(cwd, platform, build) {
1013
+ return updateDevConfig(cwd, platform, (next) => {
1014
+ const entry = next.platforms[platform];
1015
+ if (entry) {
1016
+ entry.build = build;
1017
+ } else {
1018
+ next.platforms[platform] = { build };
1019
+ }
1020
+ });
946
1021
  }
947
- function persistUploadedAppId(cwd, cfg, platform, build, appId) {
948
- const next = cfg ?? { defaultPlatform: platform, platforms: {} };
949
- const entry = next.platforms[platform];
950
- if (!entry) {
951
- next.platforms[platform] = { appId, build };
952
- saveDevConfig(cwd, next);
953
- return;
954
- }
955
- if (entry.appId !== appId || !entry.build) {
1022
+ function persistUploadedAppId(cwd, platform, build, appId) {
1023
+ updateDevConfig(cwd, platform, (next) => {
1024
+ const entry = next.platforms[platform];
1025
+ if (!entry) {
1026
+ next.platforms[platform] = { appId, build };
1027
+ return true;
1028
+ }
1029
+ if (entry.appId === appId && entry.build) return false;
956
1030
  entry.appId = appId;
957
- entry.build = build;
958
- saveDevConfig(cwd, next);
959
- }
1031
+ if (!entry.build) entry.build = build;
1032
+ return true;
1033
+ });
960
1034
  }
961
1035
  async function resolveBuildConfig(cwd, cfgRef, platform, opts) {
962
1036
  const existing = cfgRef.current?.platforms?.[platform]?.build;
@@ -972,11 +1046,15 @@ async function resolveBuildConfig(cwd, cfgRef, platform, opts) {
972
1046
  if (!answered) {
973
1047
  fail(opts, "build config is required to run vibeview build", "config_required");
974
1048
  }
975
- cfgRef.current = persistPromptedBuild(cwd, cfgRef.current, platform, answered);
1049
+ cfgRef.current = persistPromptedBuild(cwd, platform, answered);
976
1050
  return answered;
977
1051
  }
978
- function persistPromptedProductionBuild(cwd, cfg, platform, production) {
979
- const next = cfg ?? { defaultPlatform: platform, platforms: {} };
1052
+ function persistPromptedProductionBuild(cwd, platform, production) {
1053
+ return updateDevConfig(cwd, platform, (next) => {
1054
+ mergeProductionPair(next, platform, production);
1055
+ });
1056
+ }
1057
+ function mergeProductionPair(next, platform, production) {
980
1058
  const existingBuild = next.platforms[platform]?.build;
981
1059
  if (!existingBuild?.command || !existingBuild?.artifact) {
982
1060
  throw new Error(
@@ -984,8 +1062,6 @@ function persistPromptedProductionBuild(cwd, cfg, platform, production) {
984
1062
  );
985
1063
  }
986
1064
  next.platforms[platform].build = { ...existingBuild, ...production };
987
- saveDevConfig(cwd, next);
988
- return next;
989
1065
  }
990
1066
  async function resolveProductionBuildConfig(cwd, cfgRef, platform, opts) {
991
1067
  const entry = cfgRef.current?.platforms?.[platform]?.build;
@@ -1003,7 +1079,7 @@ async function resolveProductionBuildConfig(cwd, cfgRef, platform, opts) {
1003
1079
  if (!answered) {
1004
1080
  fail(opts, "a production build config is required for --production", "config_required");
1005
1081
  }
1006
- cfgRef.current = persistPromptedProductionBuild(cwd, cfgRef.current, platform, answered);
1082
+ cfgRef.current = persistPromptedProductionBuild(cwd, platform, answered);
1007
1083
  return { command: answered.productionCommand, artifact: answered.productionArtifact };
1008
1084
  }
1009
1085
  async function buildCommand(opts, cloudDeps) {
@@ -1023,7 +1099,6 @@ async function buildCommand(opts, cloudDeps) {
1023
1099
  }
1024
1100
  const platform = await resolveBarePlatform(cwd, cfgRef.current, opts);
1025
1101
  const build = await resolveBuildConfig(cwd, cfgRef, platform, opts);
1026
- const cfg = cfgRef.current;
1027
1102
  const result = await runBuildForPlatform(
1028
1103
  cwd,
1029
1104
  platform,
@@ -1032,7 +1107,7 @@ async function buildCommand(opts, cloudDeps) {
1032
1107
  (l) => console.error(chalk3.dim(l))
1033
1108
  );
1034
1109
  for (const w of result.warnings ?? []) console.error(chalk3.yellow(`\u26A0 ${w}`));
1035
- persistUploadedAppId(cwd, cfg, platform, build, result.public_id);
1110
+ persistUploadedAppId(cwd, platform, build, result.public_id);
1036
1111
  if (opts.json) {
1037
1112
  console.log(
1038
1113
  JSON.stringify({ app_id: result.public_id, name: result.name, platform: result.platform })
@@ -1135,16 +1210,17 @@ function requireProductionCapable(opts, platform) {
1135
1210
  fail(opts, productionUnsupportedReason(platform));
1136
1211
  }
1137
1212
  }
1138
- function persistPickedAppId(cwd, cfg, platform, appId) {
1139
- const next = cfg ?? { defaultPlatform: platform, platforms: {} };
1140
- const entry = next.platforms[platform];
1141
- if (entry) {
1142
- entry.appId = appId;
1143
- } else {
1144
- next.platforms[platform] = { appId };
1145
- }
1146
- saveDevConfig(cwd, next);
1147
- return next;
1213
+ function persistPickedAppId(cwd, platform, appId, opts = {}) {
1214
+ return updateDevConfig(cwd, platform, (next) => {
1215
+ const entry = next.platforms[platform];
1216
+ if (opts.onlyIfUnset && entry?.appId) return false;
1217
+ if (entry) {
1218
+ entry.appId = appId;
1219
+ } else {
1220
+ next.platforms[platform] = { appId };
1221
+ }
1222
+ return true;
1223
+ });
1148
1224
  }
1149
1225
  var APP_PICKER_SENTINEL = {
1150
1226
  CREATE_NEW_APP: "__vibeview_create_new_app__",
@@ -1212,7 +1288,7 @@ async function resolveCloudAppIds(cwd, cfgRef, platforms, opts, listApps) {
1212
1288
  createFromArtifact.add(platform);
1213
1289
  continue;
1214
1290
  }
1215
- cfgRef.current = persistPickedAppId(cwd, cfgRef.current, platform, appId);
1291
+ cfgRef.current = persistPickedAppId(cwd, platform, appId);
1216
1292
  }
1217
1293
  return createFromArtifact;
1218
1294
  }
@@ -1222,7 +1298,7 @@ function persistResolvedAppIds(cwd, cfgRef, createFromArtifact, finished) {
1222
1298
  const platform = b.platform;
1223
1299
  if (!createFromArtifact.has(platform)) continue;
1224
1300
  if (cfgRef.current?.platforms?.[platform]?.appId) continue;
1225
- cfgRef.current = persistPickedAppId(cwd, cfgRef.current, platform, b.app_public_id);
1301
+ cfgRef.current = persistPickedAppId(cwd, platform, b.app_public_id, { onlyIfUnset: true });
1226
1302
  }
1227
1303
  }
1228
1304
  function describeAllExclusions(configured) {
@@ -1897,7 +1973,7 @@ import { spawn as spawn3 } from "child_process";
1897
1973
  import { createInterface as createInterface2 } from "readline";
1898
1974
 
1899
1975
  // src/lib/metro-state.ts
1900
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
1976
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
1901
1977
  import { dirname as dirname2, join as join5 } from "path";
1902
1978
  var STATE_DIR2 = ".vibeview";
1903
1979
  var STATE_FILE2 = "metro-state.json";
@@ -1935,7 +2011,7 @@ function readMetroState(cwd) {
1935
2011
  }
1936
2012
  function clearMetroState(cwd) {
1937
2013
  const path = statePath2(cwd);
1938
- if (existsSync3(path)) rmSync3(path, { force: true });
2014
+ if (existsSync3(path)) rmSync4(path, { force: true });
1939
2015
  }
1940
2016
  function killOwnedMetro(cwd) {
1941
2017
  const state = readMetroState(cwd);
@@ -2604,7 +2680,9 @@ ${hint}`);
2604
2680
  firstRunBuild
2605
2681
  });
2606
2682
  const commitConfig = () => {
2607
- saveDevConfig(process.cwd(), cfg);
2683
+ updateDevConfig(process.cwd(), platform, (fresh) => {
2684
+ applyConfigChanges(fresh, existing, cfg);
2685
+ });
2608
2686
  if (existing) {
2609
2687
  const changes = describeConfigChanges(existing, cfg);
2610
2688
  if (changes.length) {
@@ -2644,6 +2722,13 @@ async function runDevInProcess(opts) {
2644
2722
  emit(opts.json, { event: "error", code: "invalid_flag", message });
2645
2723
  process.exit(2);
2646
2724
  }
2725
+ const standalone = opts.standalone === true;
2726
+ if (standalone && opts.platform === "roku") {
2727
+ const message = "--standalone is not for roku \u2014 a Roku dev session has no Metro; it re-packages the channel";
2728
+ console.error(chalk4.red(`error: ${message}`));
2729
+ emit(opts.json, { event: "error", code: "invalid_flag", message });
2730
+ process.exit(2);
2731
+ }
2647
2732
  let pinnedBuildId;
2648
2733
  if (typeof opts.build === "string") {
2649
2734
  try {
@@ -2672,7 +2757,7 @@ async function runDevInProcess(opts) {
2672
2757
  const cfg = await ensureConfig(api, opts);
2673
2758
  if (pinnedBuildId !== void 0) {
2674
2759
  try {
2675
- checkDevBuild(await api.getApp(cfg.appId), pinnedBuildId, cfg.platform);
2760
+ checkDevBuild(await api.getApp(cfg.appId), pinnedBuildId, cfg.platform, { standalone });
2676
2761
  } catch (err) {
2677
2762
  const message = err.message;
2678
2763
  console.error(chalk4.red(`error: ${message}`));
@@ -2708,10 +2793,12 @@ async function runDevInProcess(opts) {
2708
2793
  (l) => console.error(chalk4.dim(l))
2709
2794
  );
2710
2795
  cfg.appId = result.public_id;
2711
- if (reloaded && reloaded.platforms[cfg.platform] && reloaded.platforms[cfg.platform].appId !== result.public_id) {
2712
- reloaded.platforms[cfg.platform].appId = result.public_id;
2713
- saveDevConfig(process.cwd(), reloaded);
2714
- }
2796
+ updateDevConfig(process.cwd(), cfg.platform, (fresh) => {
2797
+ const entry = fresh.platforms[cfg.platform];
2798
+ if (!entry || entry.appId === result.public_id) return false;
2799
+ entry.appId = result.public_id;
2800
+ return true;
2801
+ });
2715
2802
  } catch (err) {
2716
2803
  const message = err.message;
2717
2804
  console.error(chalk4.red(`error: ${message}`));
@@ -2722,7 +2809,14 @@ async function runDevInProcess(opts) {
2722
2809
  let metroOwnedThisRun = false;
2723
2810
  let rokuBuild;
2724
2811
  let watchIgnore = [];
2725
- if (PLATFORM_NEEDS_METRO[cfg.platform]) {
2812
+ if (standalone && cfg.platform === "roku") {
2813
+ const message = "--standalone is not for roku \u2014 a Roku dev session has no Metro";
2814
+ console.error(chalk4.red(`error: ${message}`));
2815
+ emit(opts.json, { event: "error", code: "invalid_flag", message });
2816
+ process.exit(2);
2817
+ }
2818
+ if (standalone) {
2819
+ } else if (PLATFORM_NEEDS_METRO[cfg.platform]) {
2726
2820
  const metroPort = cfg.metroPort ?? 8081;
2727
2821
  let metroResult;
2728
2822
  try {
@@ -2774,7 +2868,7 @@ async function runDevInProcess(opts) {
2774
2868
  }
2775
2869
  console.error(
2776
2870
  chalk4.dim(
2777
- `Starting ${platformWithArticle(cfg.platform)} dev session for ${cfg.appId}${pinnedBuildId !== void 0 ? ` (build ${pinnedBuildId})` : ""}...`
2871
+ `Starting ${platformWithArticle(cfg.platform)} ${standalone ? "standalone session (no Metro)" : "dev session"} for ${cfg.appId}${pinnedBuildId !== void 0 ? ` (build ${pinnedBuildId})` : ""}...`
2778
2872
  )
2779
2873
  );
2780
2874
  let stopping = false;
@@ -2807,6 +2901,7 @@ async function runDevInProcess(opts) {
2807
2901
  deviceCategory: cfg.deviceCategory,
2808
2902
  model: cfg.model,
2809
2903
  buildId: pinnedBuildId,
2904
+ ...standalone ? { standalone } : {},
2810
2905
  metroPort: cfg.metroPort ?? 8081,
2811
2906
  // startDevSession's own browser-open happens before we've written the
2812
2907
  // state file / emitted session_ready — open it ourselves afterward
package/dist/cli.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  readDevState,
23
23
  readMetroState,
24
24
  runAgentVerb
25
- } from "./chunk-OKOAWGWV.js";
25
+ } from "./chunk-T6EQQV6E.js";
26
26
  import {
27
27
  ApiClient,
28
28
  ApiStatusError,
@@ -38,11 +38,12 @@ import {
38
38
  formatUtcTimestamp,
39
39
  loadConfig,
40
40
  printJsonError,
41
+ queueReasonCopy,
41
42
  reportCommandError,
42
43
  resolveAuth,
43
44
  saveConfig,
44
45
  uploadApp
45
- } from "./chunk-2JLTC67A.js";
46
+ } from "./chunk-PBMB6TG4.js";
46
47
 
47
48
  // src/cli.ts
48
49
  import { createRequire } from "module";
@@ -338,6 +339,7 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
338
339
  let currentStepLabel = "";
339
340
  const printedSteps = /* @__PURE__ */ new Set();
340
341
  let cachedDeviceLabel = null;
342
+ let lastQueueReason = null;
341
343
  const caseStateBefore = /* @__PURE__ */ new Map();
342
344
  const caseStartedPrinted = /* @__PURE__ */ new Set();
343
345
  while (Date.now() < deadline) {
@@ -415,6 +417,11 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
415
417
  label = "the device pool";
416
418
  }
417
419
  spinner.text = `Queued \u2014 position ${q.position} in line for ${label}`;
420
+ const why = queueReasonCopy(q.reason);
421
+ if (why && why !== lastQueueReason) {
422
+ printBelowSpinner(spinner, `${chalk2.yellow("\u23F3")} Waiting for a device: ${why}`);
423
+ }
424
+ lastQueueReason = why;
418
425
  }
419
426
  if (progress.length === 0) {
420
427
  if (result.status === "pending") {
@@ -434,7 +441,8 @@ async function pollUntilComplete(client, runId, timeoutMs, spinner, onQueueObser
434
441
  }
435
442
  }
436
443
  if (keepAlive && Date.now() - lastLogTime >= 3e4) {
437
- const where = result.status === "queued" && result.queue ? ` (position ${result.queue.position} in line for a device)` : "";
444
+ const why = result.status === "queued" ? queueReasonCopy(result.queue?.reason) : null;
445
+ const where = result.status === "queued" && result.queue ? ` (position ${result.queue.position} in line for a device${why ? `: ${why}` : ""})` : "";
438
446
  process.stderr.write(`Status: ${result.status}${where}...
439
447
  `);
440
448
  lastLogTime = Date.now();
@@ -742,7 +750,8 @@ ${signal} received.
742
750
  model: opts.model,
743
751
  provider: opts.provider,
744
752
  mode: opts.mode,
745
- device_category: opts.device ? void 0 : opts.deviceCategory ?? "phone",
753
+ // Omitted unless asked for: the backend picks tv for each TV suite.
754
+ device_category: opts.device ? void 0 : opts.deviceCategory,
746
755
  device_id: opts.device,
747
756
  build_id: opts.build,
748
757
  metadata: opts.metadata
@@ -792,6 +801,7 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
792
801
  const deadline = Date.now() + totalTimeout * 1e3;
793
802
  const suiteStateBefore = /* @__PURE__ */ new Map();
794
803
  const suiteStartedPrinted = /* @__PURE__ */ new Set();
804
+ let lastQueueReason = null;
795
805
  const print = (line) => {
796
806
  spinner.stop();
797
807
  process.stderr.write(line + "\n");
@@ -870,8 +880,16 @@ ${chalk4.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
870
880
  if (queuedChild && queuedChild.queue) {
871
881
  const q = queuedChild.queue;
872
882
  const target = q.specific_device_id ?? "the device pool";
883
+ const why = queueReasonCopy(q.reason);
884
+ if (why && why !== lastQueueReason) {
885
+ print(
886
+ `${chalk4.yellow("\u23F3")} ${chalk4.bold(queuedChild.suite_name)} is waiting for a device: ${why}`
887
+ );
888
+ }
889
+ lastQueueReason = why;
873
890
  spinner.text = `Queued \u2014 ${chalk4.bold(queuedChild.suite_name)} position ${q.position} for ${target} (${doneCount}/${totalMatched} suite(s) complete)`;
874
891
  } else {
892
+ lastQueueReason = null;
875
893
  spinner.text = `Running suites \u2014 ${doneCount}/${totalMatched} complete...`;
876
894
  }
877
895
  if (TERMINAL.has(last.status)) break;
@@ -1767,7 +1785,8 @@ function defaultDeps2(client) {
1767
1785
  getSubmission: client.getSubmission.bind(client),
1768
1786
  getSubmissionLog: client.getSubmissionLog.bind(client),
1769
1787
  listCloudBuilds: client.listCloudBuilds.bind(client),
1770
- checkStorePreflight: client.checkStorePreflight.bind(client)
1788
+ checkStorePreflight: client.checkStorePreflight.bind(client),
1789
+ getApp: client.getApp.bind(client)
1771
1790
  };
1772
1791
  }
1773
1792
  var POLL_INTERVAL_MS2 = 2e3;
@@ -1892,7 +1911,7 @@ async function submitCommand(opts, deps) {
1892
1911
  }
1893
1912
  if (rollout !== void 0 && releaseStatus !== "inProgress") {
1894
1913
  throw new UsageError(
1895
- `a rollout fraction requires release status "inProgress" (a staged rollout) \u2014 got "${releaseStatus ?? "completed"}"`
1914
+ `a rollout fraction requires release status "inProgress" (a staged rollout) \u2014 got "${releaseStatus ?? (track === "production" ? "draft" : "completed")}"`
1896
1915
  );
1897
1916
  }
1898
1917
  if (releaseStatus === "inProgress" && rollout === void 0) {
@@ -1946,12 +1965,19 @@ async function submitCommand(opts, deps) {
1946
1965
  }
1947
1966
  }
1948
1967
  async function checkStore(platform, androidFamily, ascKey2, opts, deps) {
1949
- const identifier = androidFamily ? opts.package : opts.bundleId;
1968
+ const flagged = androidFamily ? opts.package : opts.bundleId;
1969
+ const usage = androidFamily ? `--check needs the package name to look up: --package <name> (e.g. --package io.example.app)` : `--check needs the bundle identifier to look up: --bundle-id <id> (e.g. --bundle-id io.example.app)`;
1970
+ let identifier = flagged;
1950
1971
  if (!identifier) {
1951
- throw new UsageError(
1952
- androidFamily ? `--check needs the package name to look up: --package <name> (e.g. --package io.example.app)` : `--check needs the bundle identifier to look up: --bundle-id <id> (e.g. --bundle-id io.example.app)`
1953
- );
1972
+ try {
1973
+ identifier = await linkedIdentifier(platform, opts, deps);
1974
+ } catch (err) {
1975
+ printError2(opts, err);
1976
+ process.exitCode = errorExitCode(err);
1977
+ return;
1978
+ }
1954
1979
  }
1980
+ if (!identifier) throw new UsageError(usage);
1955
1981
  try {
1956
1982
  const result = await deps.checkStorePreflight({
1957
1983
  platform,
@@ -1976,6 +2002,32 @@ async function checkStore(platform, androidFamily, ascKey2, opts, deps) {
1976
2002
  process.exitCode = errorExitCode(err);
1977
2003
  }
1978
2004
  }
2005
+ async function linkedIdentifier(platform, opts, deps) {
2006
+ if (!deps.getApp) return void 0;
2007
+ let appId;
2008
+ try {
2009
+ appId = loadDevConfig(process.cwd())?.platforms?.[platform]?.appId;
2010
+ } catch {
2011
+ return void 0;
2012
+ }
2013
+ if (!appId) return void 0;
2014
+ let app;
2015
+ try {
2016
+ app = await deps.getApp(appId);
2017
+ } catch (err) {
2018
+ if (err instanceof NotFoundError) {
2019
+ throw new Error(
2020
+ `the app linked in vibeview.json for ${platform} (${appId}) no longer exists \u2014 ` + (platform === "android" || platform === "androidtv" ? "pass --package <name>" : "pass --bundle-id <id>")
2021
+ );
2022
+ }
2023
+ throw err;
2024
+ }
2025
+ if (!app.package_name) return void 0;
2026
+ if (!opts.json) {
2027
+ console.error(chalk14.dim(`Checking ${app.package_name} (${app.name}, linked in vibeview.json)`));
2028
+ }
2029
+ return app.package_name;
2030
+ }
1979
2031
  async function resolveLatestBuild(platform, deps) {
1980
2032
  const { builds: builds2 } = await deps.listCloudBuilds({
1981
2033
  platform,
@@ -3511,7 +3563,7 @@ program.name("vibeview").description(
3511
3563
  ).version(pkg.version);
3512
3564
  program.command("run-suite <suite-id>").description("Run all test cases in a suite").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show per-case breakdown", false).option("--timeout <seconds>", "Max wait time in seconds (queue wait plus execution)", "1800").option("--commit-sha <sha>", "Git commit SHA for GitHub status checks").option("--model <model>", "AI model to use").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
3513
3565
  "--device-category <category>",
3514
- "Device category: phone, tablet, tv (default: phone when --device is not set)",
3566
+ "Device category: phone, tablet, tv (default: tv for a TV platform, else phone)",
3515
3567
  parseDeviceCategory
3516
3568
  ).option(
3517
3569
  "--device <device_id>",
@@ -3539,7 +3591,7 @@ program.command("run-suites").description("Run all suites matching a tag filter"
3539
3591
  "Filter matched suites to one platform: ios, android, tvos, androidtv (default: no filter)"
3540
3592
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show per-suite progress", false).option("--timeout <seconds>", "Per-suite timeout, capped at 2 hours total", "1800").option("--commit-sha <sha>", "Git commit SHA propagated to every SuiteRun").option("--model <model>", "AI model").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
3541
3593
  "--device-category <category>",
3542
- "Device category: phone, tablet, tv (default: phone when --device is not set)",
3594
+ "Device category: phone, tablet, tv (default: tv for a TV platform, else phone)",
3543
3595
  parseDeviceCategory
3544
3596
  ).option(
3545
3597
  "--device <device_id>",
@@ -3559,7 +3611,7 @@ program.command("run-suites").description("Run all suites matching a tag filter"
3559
3611
  });
3560
3612
  program.command("run-test <test-id>").description("Run a single test case").option("--token <token>", "API token").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show step details", false).option("--timeout <seconds>", "Max wait time in seconds (queue wait plus execution)", "1800").option("--commit-sha <sha>", "Git commit SHA for GitHub status checks").option("--model <model>", "AI model to use").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
3561
3613
  "--device-category <category>",
3562
- "Device category: phone, tablet, tv (default: phone when --device is not set)",
3614
+ "Device category: phone, tablet, tv (default: tv for a TV platform, else phone)",
3563
3615
  parseDeviceCategory
3564
3616
  ).option(
3565
3617
  "--device <device_id>",
@@ -3630,11 +3682,15 @@ program.command("dev").description(
3630
3682
  false
3631
3683
  ).option(
3632
3684
  "--build [id]",
3633
- "Bare: build and upload first (uses platforms.<platform>.build from vibeview.json). With an id: run that uploaded debug build instead of the newest one (see `vibeview list-builds <app>`)",
3685
+ "Bare: build and upload first (uses platforms.<platform>.build from vibeview.json). With an id: run that uploaded debug build instead of the newest one (any build with --standalone) (see `vibeview list-builds <app>`)",
3634
3686
  false
3635
3687
  ).option(
3636
3688
  "--watch [seconds]",
3637
3689
  "Roku only: auto-reload after file changes settle (default: 2 seconds of quiet)"
3690
+ ).option(
3691
+ "--standalone",
3692
+ "Run the build as it is, with no Metro and no tunnel (a release, CI or cloud build); without --build <id>, the newest release build. Code changes do not hot-reload",
3693
+ false
3638
3694
  ).action((opts) => dev(opts));
3639
3695
  program.command("build").description("Run your configured build command and upload the result (see vibeview.json)").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option(
3640
3696
  "--platform <platform>",
@@ -3669,13 +3725,13 @@ program.command("submit").description(
3669
3725
  "ios/tvos only: which stored App Store Connect key to use (default when exactly one \u2014 see `vibeview credentials asc-key add`)"
3670
3726
  ).option(
3671
3727
  "--release-status <status>",
3672
- "android/androidtv only: Play release status \u2014 completed (default), draft, or inProgress"
3728
+ "android/androidtv only: Play release status \u2014 completed, draft, or inProgress (default: draft on the production track, completed on the others)"
3673
3729
  ).option(
3674
3730
  "--rollout <fraction>",
3675
3731
  "android/androidtv only: staged-rollout fraction, 0 < f < 1 (requires --release-status inProgress)"
3676
3732
  ).option(
3677
3733
  "--check",
3678
- "Only check store readiness: does the store know this app with your stored key? Takes --platform plus --bundle-id (ios/tvos) or --package (android/androidtv); no build needed. Exit 0 when ready",
3734
+ "Only check store readiness: does the store know this app with your stored key? Takes --platform plus --bundle-id (ios/tvos) or --package (android/androidtv), defaulting to the app linked in vibeview.json; no build needed. Exit 0 when ready",
3679
3735
  false
3680
3736
  ).option("--bundle-id <id>", "ios/tvos, with --check: the bundle identifier to look up").option("--no-wait", "Print the submission id and exit without waiting for completion").option("--json", "Emit line-delimited JSON events on stdout instead of human output", false).action((opts) => submitCommand(opts));
3681
3737
  var builds = program.command("builds").description("Manage cloud builds (see `vibeview build --cloud`)");
@@ -3775,7 +3831,7 @@ program.command("dev-reload").description(
3775
3831
  program.command("mcp").description(
3776
3832
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
3777
3833
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
3778
- const { runMcpServer } = await import("./server-6SRFOQ6B.js");
3834
+ const { runMcpServer } = await import("./server-IGY2VPYT.js");
3779
3835
  await runMcpServer({ ...opts, version: pkg.version });
3780
3836
  });
3781
3837
  program.command("agent-setup").description(
@@ -21,12 +21,12 @@ import {
21
21
  readDevState,
22
22
  renderEnvelope,
23
23
  startDevSession
24
- } from "./chunk-OKOAWGWV.js";
24
+ } from "./chunk-T6EQQV6E.js";
25
25
  import {
26
26
  ApiClient,
27
27
  NotFoundError,
28
28
  resolveAuth
29
- } from "./chunk-2JLTC67A.js";
29
+ } from "./chunk-PBMB6TG4.js";
30
30
 
31
31
  // src/mcp/server.ts
32
32
  import { writeFileSync } from "fs";
@@ -539,7 +539,7 @@ async function handleUploadApp(args, deps) {
539
539
  }
540
540
  const name = typeof args.name === "string" && args.name ? args.name : void 0;
541
541
  const upload = deps.uploadApp ?? (async (f, n) => {
542
- const { uploadAppBuild } = await import("./upload-app-WROTRZCA.js");
542
+ const { uploadAppBuild } = await import("./upload-app-FEZG23NU.js");
543
543
  const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
544
544
  return result2;
545
545
  });
@@ -3,7 +3,7 @@ import {
3
3
  describeBuild,
4
4
  uploadApp,
5
5
  uploadAppBuild
6
- } from "./chunk-2JLTC67A.js";
6
+ } from "./chunk-PBMB6TG4.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.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku (beta) 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",