vibeview 0.27.1 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,10 +8,12 @@ import {
8
8
  errorExitCode,
9
9
  formatUtcTimestamp,
10
10
  loadConfig,
11
+ prepareUploadArtifact,
11
12
  queueReasonCopy,
13
+ resolveArtifact,
12
14
  resolveAuth,
13
15
  uploadAppBuild
14
- } from "./chunk-FTTWLCTE.js";
16
+ } from "./chunk-PBMB6TG4.js";
15
17
 
16
18
  // src/lib/dev-config.ts
17
19
  import { readFileSync, writeFileSync } from "fs";
@@ -166,6 +168,30 @@ function validateBuildEnv(env, platformKey) {
166
168
  function saveDevConfig(cwd, cfg) {
167
169
  writeFileSync(join(cwd, FILE), JSON.stringify(cfg, null, 2) + "\n");
168
170
  }
171
+ function describeConfigChanges(before, after) {
172
+ const flat = (value, prefix, out) => {
173
+ if (value && typeof value === "object" && !Array.isArray(value)) {
174
+ for (const [k, v] of Object.entries(value)) {
175
+ flat(v, prefix ? `${prefix}.${k}` : k, out);
176
+ }
177
+ } else if (value !== void 0) {
178
+ out.set(prefix, typeof value === "string" ? value : JSON.stringify(value));
179
+ }
180
+ return out;
181
+ };
182
+ const a = flat(before, "", /* @__PURE__ */ new Map());
183
+ const b = flat(after, "", /* @__PURE__ */ new Map());
184
+ const changes = [];
185
+ for (const [key, next] of b) {
186
+ const prev = a.get(key);
187
+ if (prev === void 0) changes.push(`${key} (added) ${next}`);
188
+ else if (prev !== next) changes.push(`${key} ${prev} \u2192 ${next}`);
189
+ }
190
+ for (const [key, prev] of a) {
191
+ if (!b.has(key)) changes.push(`${key} (removed, was ${prev})`);
192
+ }
193
+ return changes;
194
+ }
169
195
  function resolveMetro(cfg, platform, portFlag) {
170
196
  const entry = cfg?.platforms?.[platform];
171
197
  return {
@@ -223,7 +249,7 @@ function buildKind(isDebug) {
223
249
  if (isDebug === false) return "release";
224
250
  return "unknown";
225
251
  }
226
- function checkDevBuild(app, buildId, platform) {
252
+ function checkDevBuild(app, buildId, platform, opts = {}) {
227
253
  if (platform === "roku") {
228
254
  throw new Error(
229
255
  "a build id cannot be pinned on roku \u2014 `vibeview dev` packages and sideloads the channel itself"
@@ -240,7 +266,7 @@ function checkDevBuild(app, buildId, platform) {
240
266
  `build ${buildId} is not a build of app ${app.public_id} \u2014 see \`vibeview list-builds ${app.public_id}\``
241
267
  );
242
268
  }
243
- if (build.is_debug !== true) {
269
+ if (!opts.standalone && build.is_debug !== true) {
244
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";
245
271
  throw new Error(
246
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}\`)`
@@ -318,18 +344,31 @@ function isPidAlive(pid) {
318
344
  return err.code === "EPERM";
319
345
  }
320
346
  }
347
+ var WHOLE_DIR_LINE = /^(\/?)\.vibeview\/?$/;
321
348
  function ensureGitignored(cwd) {
322
349
  try {
323
350
  const gitignore = join2(cwd, ".gitignore");
324
351
  if (!existsSync(gitignore)) return;
325
352
  const content = readFileSync2(gitignore, "utf-8");
326
- const covered = content.split("\n").map((l) => l.trim()).some((l) => l === ".vibeview" || l === ".vibeview/" || l === "/.vibeview");
327
- if (covered) return;
353
+ const lines = content.split("\n");
354
+ const trimmed = lines.map((l) => l.trim());
355
+ if (trimmed.some((l) => /^\/?\.vibeview\/\*$/.test(l))) return;
356
+ const i = trimmed.findIndex((l) => WHOLE_DIR_LINE.test(l));
357
+ if (i !== -1) {
358
+ const anchor = WHOLE_DIR_LINE.exec(trimmed[i])[1];
359
+ lines.splice(i, 1, `${anchor}.vibeview/*`, `!${anchor}.vibeview/test-spec.json`);
360
+ writeFileSync2(gitignore, lines.join("\n"));
361
+ return;
362
+ }
328
363
  const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
329
- writeFileSync2(gitignore, `${content}${prefix}
330
- # VibeView local session state
331
- .vibeview/
332
- `);
364
+ writeFileSync2(
365
+ gitignore,
366
+ `${content}${prefix}
367
+ # VibeView local session state (test-spec.json is committed for CI)
368
+ .vibeview/*
369
+ !.vibeview/test-spec.json
370
+ `
371
+ );
333
372
  } catch {
334
373
  }
335
374
  }
@@ -383,81 +422,24 @@ import { createInterface } from "readline";
383
422
  import { select as select2 } from "@inquirer/prompts";
384
423
  import chalk3 from "chalk";
385
424
 
386
- // src/lib/artifact.ts
387
- import { existsSync as existsSync2, mkdtempSync, readdirSync, rmSync as rmSync2, statSync } from "fs";
388
- import { spawnSync } from "child_process";
389
- import { tmpdir } from "os";
390
- import { basename, dirname as dirname2, isAbsolute, join as join3 } from "path";
391
- function resolveArtifact(cwd, pattern) {
392
- const abs = isAbsolute(pattern) ? pattern : join3(cwd, pattern);
393
- if (!basename(abs).includes("*")) {
394
- if (!existsSync2(abs)) {
395
- throw new Error(
396
- `artifact not found: ${pattern} (looked at ${abs}) \u2014 did the build produce it?`
397
- );
398
- }
399
- return abs;
400
- }
401
- const dir = dirname2(abs);
402
- const re = new RegExp("^" + basename(abs).split("*").map(escapeRegExp).join(".*") + "$");
403
- let entries;
404
- try {
405
- entries = readdirSync(dir);
406
- } catch {
407
- throw new Error(`artifact not found: ${pattern} (directory ${dir} does not exist)`);
408
- }
409
- const matches = entries.filter((e) => re.test(e)).map((e) => join3(dir, e)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
410
- if (matches.length === 0) {
411
- throw new Error(`artifact not found: no match for ${pattern} in ${dir}`);
412
- }
413
- return matches[0];
414
- }
415
- function escapeRegExp(s) {
416
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
417
- }
418
- function prepareUploadArtifact(path) {
419
- if (!(statSync(path).isDirectory() && path.endsWith(".app"))) {
420
- return { uploadPath: path, cleanup: () => {
421
- } };
422
- }
423
- const tmp = mkdtempSync(join3(tmpdir(), "vibeview-zip-"));
424
- const out = join3(tmp, `${basename(path)}.zip`);
425
- const res = spawnSync("zip", ["-qry", out, basename(path)], { cwd: dirname2(path) });
426
- if (res.error) {
427
- rmSync2(tmp, { recursive: true, force: true });
428
- const errorCode = res.error.code;
429
- const errorMsg = res.error.message;
430
- throw new Error(
431
- `failed to zip ${path}: spawn zip ${errorCode} \u2014 ${errorMsg} \u2014 is the \`zip\` command installed?`
432
- );
433
- }
434
- if (res.status !== 0) {
435
- rmSync2(tmp, { recursive: true, force: true });
436
- throw new Error(
437
- `failed to zip ${path}: ${res.stderr?.toString().trim() || `zip exited ${res.status}`}`
438
- );
439
- }
440
- return { uploadPath: out, cleanup: () => rmSync2(tmp, { recursive: true, force: true }) };
441
- }
442
-
443
425
  // src/lib/dev-setup.ts
444
426
  import chalk2 from "chalk";
445
427
  import { input, select } from "@inquirer/prompts";
446
428
 
447
429
  // src/lib/project-detect.ts
448
- import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
449
- import { join as join4 } from "path";
430
+ import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
431
+ import { join as join3 } from "path";
450
432
  import ignore from "ignore";
451
433
  function readJson(cwd, name) {
452
434
  try {
453
- return JSON.parse(readFileSync3(join4(cwd, name), "utf-8"));
435
+ return JSON.parse(readFileSync3(join3(cwd, name), "utf-8"));
454
436
  } catch {
455
437
  return null;
456
438
  }
457
439
  }
458
440
  function isFile(path) {
459
441
  try {
460
- return statSync2(path).isFile();
442
+ return statSync(path).isFile();
461
443
  } catch {
462
444
  return false;
463
445
  }
@@ -465,19 +447,19 @@ function isFile(path) {
465
447
  function detectFlavor(cwd) {
466
448
  let deps = {};
467
449
  try {
468
- const pkg = JSON.parse(readFileSync3(join4(cwd, "package.json"), "utf-8"));
450
+ const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf-8"));
469
451
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
470
452
  } catch {
471
453
  }
472
454
  if ("expo" in deps) return "expo";
473
- if (existsSync3(join4(cwd, "android", "gradlew")) || iosWorkspaceName(cwd)) return "bare-rn";
474
- if (isFile(join4(cwd, "manifest"))) return "roku";
455
+ if (existsSync2(join3(cwd, "android", "gradlew")) || iosWorkspaceName(cwd)) return "bare-rn";
456
+ if (isFile(join3(cwd, "manifest"))) return "roku";
475
457
  return "unknown";
476
458
  }
477
459
  function nativeDirShipped(cwd, dir) {
478
- if (!existsSync3(join4(cwd, dir))) return false;
460
+ if (!existsSync2(join3(cwd, dir))) return false;
479
461
  try {
480
- const ig = ignore().add(readFileSync3(join4(cwd, ".gitignore"), "utf-8"));
462
+ const ig = ignore().add(readFileSync3(join3(cwd, ".gitignore"), "utf-8"));
481
463
  return !ig.ignores(`${dir}/`);
482
464
  } catch {
483
465
  return true;
@@ -493,7 +475,7 @@ function isManagedExpo(cwd, platform) {
493
475
  }
494
476
  function iosWorkspaceName(cwd) {
495
477
  try {
496
- const found = readdirSync2(join4(cwd, "ios")).find((f) => f.endsWith(".xcworkspace"));
478
+ const found = readdirSync(join3(cwd, "ios")).find((f) => f.endsWith(".xcworkspace"));
497
479
  return found ? found.replace(/\.xcworkspace$/, "") : null;
498
480
  } catch {
499
481
  return null;
@@ -502,9 +484,9 @@ function iosWorkspaceName(cwd) {
502
484
  var IOS_ARTIFACT = "ios/build/Build/Products/Debug-iphonesimulator/*.app";
503
485
  var ANDROID_ARTIFACT = "android/app/build/outputs/apk/debug/*.apk";
504
486
  function installCommand(cwd) {
505
- if (existsSync3(join4(cwd, "yarn.lock"))) return "yarn install --immutable";
506
- if (existsSync3(join4(cwd, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
507
- if (existsSync3(join4(cwd, "package-lock.json"))) return "npm ci";
487
+ if (existsSync2(join3(cwd, "yarn.lock"))) return "yarn install --immutable";
488
+ if (existsSync2(join3(cwd, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
489
+ if (existsSync2(join3(cwd, "package-lock.json"))) return "npm ci";
508
490
  return "yarn install --immutable";
509
491
  }
510
492
  function managedExpoAndroidCommand(cwd) {
@@ -518,7 +500,7 @@ var ROKU_ZIP_COMMAND = "zip -r out/channel.zip manifest source components images
518
500
  function rokuDefaults(cwd) {
519
501
  const pkg = readJson(cwd, "package.json");
520
502
  const scripts = pkg?.scripts ?? {};
521
- const runner = existsSync3(join4(cwd, "yarn.lock")) ? "yarn" : "npm run";
503
+ const runner = existsSync2(join3(cwd, "yarn.lock")) ? "yarn" : "npm run";
522
504
  const packageScript = Object.entries(scripts).find(
523
505
  ([name, body]) => !name.startsWith("deploy") && (name.startsWith("package") || body.includes("--package-only") || body.includes("zipPackage"))
524
506
  );
@@ -576,7 +558,7 @@ function expoSanitizedName(name) {
576
558
  return name.replace(/[\W_]+/g, "").normalize("NFD").replace(/[\u0300-\u036f]/g, "");
577
559
  }
578
560
  function managedExpoIosName(cwd) {
579
- if (existsSync3(join4(cwd, "app.config.js")) || existsSync3(join4(cwd, "app.config.ts"))) {
561
+ if (existsSync2(join3(cwd, "app.config.js")) || existsSync2(join3(cwd, "app.config.ts"))) {
580
562
  return null;
581
563
  }
582
564
  const raw = readJson(cwd, "app.config.json") ?? readJson(cwd, "app.json");
@@ -721,9 +703,9 @@ async function promptMetroCommand(cwd, platform) {
721
703
 
722
704
  // src/lib/source-pack.ts
723
705
  import { execFile, spawn } from "child_process";
724
- import { lstatSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
725
- import { tmpdir as tmpdir2 } from "os";
726
- import { join as join5 } from "path";
706
+ import { lstatSync, mkdtempSync, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
707
+ import { tmpdir } from "os";
708
+ import { join as join4 } from "path";
727
709
  import ignore2 from "ignore";
728
710
  var NotAGitRepoError = class extends Error {
729
711
  constructor(message) {
@@ -749,7 +731,7 @@ async function listSourceFiles(projectRoot) {
749
731
  const stdout = await runGitLsFiles(projectRoot);
750
732
  const rawPaths = stdout.length ? stdout.split("\0").filter((p) => p.length > 0) : [];
751
733
  let filtered = rawPaths;
752
- const vvignorePath = join5(projectRoot, ".vvignore");
734
+ const vvignorePath = join4(projectRoot, ".vvignore");
753
735
  if (statOrNull(vvignorePath)?.isFile()) {
754
736
  const ig = ignore2().add(readFileSync4(vvignorePath, "utf-8"));
755
737
  filtered = filtered.filter((p) => !ig.ignores(p));
@@ -757,7 +739,7 @@ async function listSourceFiles(projectRoot) {
757
739
  const result = [];
758
740
  for (const relPath of filtered) {
759
741
  if (isForceExcluded(relPath)) continue;
760
- const st = statOrNull(join5(projectRoot, relPath));
742
+ const st = statOrNull(join4(projectRoot, relPath));
761
743
  if (!st) continue;
762
744
  result.push(relPath);
763
745
  }
@@ -807,7 +789,7 @@ async function packSource(projectRoot, files, opts = {}) {
807
789
  let totalBytes = 0;
808
790
  const sized = [];
809
791
  for (const relPath of files) {
810
- const bytes = lstatSync(join5(projectRoot, relPath)).size;
792
+ const bytes = lstatSync(join4(projectRoot, relPath)).size;
811
793
  totalBytes += bytes;
812
794
  sized.push({ path: relPath, bytes });
813
795
  }
@@ -818,13 +800,13 @@ async function packSource(projectRoot, files, opts = {}) {
818
800
  largest
819
801
  );
820
802
  }
821
- const tmpDir = mkdtempSync2(join5(tmpdir2(), "vibeview-source-pack-"));
822
- const cleanup = () => rmSync3(tmpDir, { recursive: true, force: true });
803
+ const tmpDir = mkdtempSync(join4(tmpdir(), "vibeview-source-pack-"));
804
+ const cleanup = () => rmSync2(tmpDir, { recursive: true, force: true });
823
805
  try {
824
806
  const supportsNull = await probeTarNullSupport();
825
- const tarPath = join5(tmpDir, "source.tgz");
807
+ const tarPath = join4(tmpDir, "source.tgz");
826
808
  if (supportsNull) {
827
- const listPath = join5(tmpDir, "files.list");
809
+ const listPath = join4(tmpDir, "files.list");
828
810
  writeFileSync3(listPath, files.map((f) => `${f}\0`).join(""));
829
811
  await runTar(["-czf", tarPath, "-C", projectRoot, "--null", "-T", listPath]);
830
812
  } else {
@@ -834,12 +816,12 @@ async function packSource(projectRoot, files, opts = {}) {
834
816
  `cannot pack source: filename contains a newline (${JSON.stringify(badName)}) and the installed tar doesn't support --null -T; rename the file or install a tar that supports --null`
835
817
  );
836
818
  }
837
- const listPath = join5(tmpDir, "files.list");
819
+ const listPath = join4(tmpDir, "files.list");
838
820
  writeFileSync3(listPath, files.map((f) => `${f}
839
821
  `).join(""));
840
822
  await runTar(["-czf", tarPath, "-C", projectRoot, "-T", listPath]);
841
823
  }
842
- const sizeBytes = statSync3(tarPath).size;
824
+ const sizeBytes = statSync2(tarPath).size;
843
825
  return { tarPath, sizeBytes, cleanup };
844
826
  } catch (err) {
845
827
  cleanup();
@@ -855,12 +837,12 @@ var nullSupportCache = null;
855
837
  function probeTarNullSupport() {
856
838
  if (!nullSupportCache) {
857
839
  nullSupportCache = new Promise((resolve) => {
858
- const tmp = mkdtempSync2(join5(tmpdir2(), "vibeview-tar-probe-"));
859
- const listPath = join5(tmp, "empty.list");
840
+ const tmp = mkdtempSync(join4(tmpdir(), "vibeview-tar-probe-"));
841
+ const listPath = join4(tmp, "empty.list");
860
842
  writeFileSync3(listPath, "");
861
- const out = join5(tmp, "probe.tgz");
843
+ const out = join4(tmp, "probe.tgz");
862
844
  execFile("tar", ["-czf", out, "-C", tmp, "--null", "-T", listPath], (err) => {
863
- rmSync3(tmp, { recursive: true, force: true });
845
+ rmSync2(tmp, { recursive: true, force: true });
864
846
  resolve(!err);
865
847
  });
866
848
  });
@@ -1543,7 +1525,9 @@ async function waitForCloudBuilds(builds, opts, deps) {
1543
1525
  id: final.id,
1544
1526
  status: final.status,
1545
1527
  cost_micro: final.cost_micro,
1546
- error_summary: final.error_summary
1528
+ error_summary: final.error_summary,
1529
+ app_build_id: final.app_build_id ?? null,
1530
+ app_public_id: final.app_public_id ?? null
1547
1531
  });
1548
1532
  finalized.add(build.id);
1549
1533
  }
@@ -1731,7 +1715,7 @@ your build command must produce a zip \u2014 use the package/zip step, not deplo
1731
1715
  // src/commands/dev.ts
1732
1716
  import { spawn as spawn4 } from "child_process";
1733
1717
  import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
1734
- import { dirname as dirname5 } from "path";
1718
+ import { dirname as dirname4 } from "path";
1735
1719
  import { createInterface as createInterface3 } from "readline";
1736
1720
  import { select as select3 } from "@inquirer/prompts";
1737
1721
  import chalk4 from "chalk";
@@ -1926,15 +1910,15 @@ import { spawn as spawn3 } from "child_process";
1926
1910
  import { createInterface as createInterface2 } from "readline";
1927
1911
 
1928
1912
  // src/lib/metro-state.ts
1929
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
1930
- import { dirname as dirname3, join as join6 } from "path";
1913
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
1914
+ import { dirname as dirname2, join as join5 } from "path";
1931
1915
  var STATE_DIR2 = ".vibeview";
1932
1916
  var STATE_FILE2 = "metro-state.json";
1933
1917
  function statePath2(cwd) {
1934
- return join6(cwd, STATE_DIR2, STATE_FILE2);
1918
+ return join5(cwd, STATE_DIR2, STATE_FILE2);
1935
1919
  }
1936
1920
  function metroLogPath(cwd) {
1937
- return join6(cwd, STATE_DIR2, "metro.log");
1921
+ return join5(cwd, STATE_DIR2, "metro.log");
1938
1922
  }
1939
1923
  function isPidAlive2(pid) {
1940
1924
  try {
@@ -1946,7 +1930,7 @@ function isPidAlive2(pid) {
1946
1930
  }
1947
1931
  function writeMetroState(cwd, state) {
1948
1932
  const path = statePath2(cwd);
1949
- mkdirSync2(dirname3(path), { recursive: true });
1933
+ mkdirSync2(dirname2(path), { recursive: true });
1950
1934
  writeFileSync4(path, JSON.stringify(state, null, 2) + "\n");
1951
1935
  }
1952
1936
  function readMetroState(cwd) {
@@ -1964,7 +1948,7 @@ function readMetroState(cwd) {
1964
1948
  }
1965
1949
  function clearMetroState(cwd) {
1966
1950
  const path = statePath2(cwd);
1967
- if (existsSync4(path)) rmSync4(path, { force: true });
1951
+ if (existsSync3(path)) rmSync3(path, { force: true });
1968
1952
  }
1969
1953
  function killOwnedMetro(cwd) {
1970
1954
  const state = readMetroState(cwd);
@@ -2072,7 +2056,7 @@ async function waitFor(cond, timeoutMs, everyMs) {
2072
2056
 
2073
2057
  // src/lib/reload-loop.ts
2074
2058
  import { watch } from "fs";
2075
- import { basename as basename2, dirname as dirname4 } from "path";
2059
+ import { basename, dirname as dirname3 } from "path";
2076
2060
  function createReloadLoop(deps) {
2077
2061
  let inFlight = null;
2078
2062
  let pendingFollowUp = false;
@@ -2153,16 +2137,16 @@ function isIgnoredWatchPath(relPath, artifact, userIgnore = []) {
2153
2137
  if (IGNORED_FILES.includes(rel)) return true;
2154
2138
  if (userIgnore.some((entry) => isUnderPath(rel, entry))) return true;
2155
2139
  const artifactPattern = artifact.replace(/\\/g, "/").replace(/^\.\//, "");
2156
- const artifactDir = dirname4(artifactPattern);
2140
+ const artifactDir = dirname3(artifactPattern);
2157
2141
  if (artifactDir !== "." && (rel === artifactDir || rel.startsWith(`${artifactDir}/`))) {
2158
2142
  return true;
2159
2143
  }
2160
- if (dirname4(rel) !== artifactDir) return false;
2161
- const name = basename2(artifactPattern);
2162
- const re = new RegExp(`^${name.split("*").map(escapeRegExp2).join(".*")}$`);
2163
- return re.test(basename2(rel));
2144
+ if (dirname3(rel) !== artifactDir) return false;
2145
+ const name = basename(artifactPattern);
2146
+ const re = new RegExp(`^${name.split("*").map(escapeRegExp).join(".*")}$`);
2147
+ return re.test(basename(rel));
2164
2148
  }
2165
- function escapeRegExp2(s) {
2149
+ function escapeRegExp(s) {
2166
2150
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2167
2151
  }
2168
2152
  function watchProjectFiles(cwd, artifact, userIgnore = []) {
@@ -2198,7 +2182,7 @@ function emit(json, evt) {
2198
2182
  async function startDevSession(cfg, onEvent = () => {
2199
2183
  }, onTunnelState) {
2200
2184
  const api = new ApiClient(cfg.baseUrl, cfg.token);
2201
- const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform];
2185
+ const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform] && !cfg.standalone;
2202
2186
  const pool = cfg.model ? resolveDeviceModel(await api.listDeviceModels(), cfg.platform, cfg.model) : null;
2203
2187
  const created = await api.createSession(
2204
2188
  devSessionCreateBody(pool ? { ...cfg, deviceCategory: pool.category, model: pool.model } : cfg)
@@ -2286,7 +2270,7 @@ async function startDevSession(cfg, onEvent = () => {
2286
2270
  reportedWarnings.add(w);
2287
2271
  onEvent({ event: "warning", message: w });
2288
2272
  }
2289
- return { sessionId, pageUrl, stop: () => stopSession(0), reportedWarnings };
2273
+ return { sessionId, pageUrl, stop: () => stopSession(0), reportedWarnings, session: active };
2290
2274
  } catch (err) {
2291
2275
  await stopSession(1);
2292
2276
  throw err;
@@ -2297,8 +2281,9 @@ function devSessionCreateBody(cfg) {
2297
2281
  device_type: PLATFORM_DEVICE[cfg.platform].deviceType,
2298
2282
  device_category: cfg.deviceCategory,
2299
2283
  app_id: cfg.appId,
2300
- // Roku has no JS bundle to serve, so there is nothing to tunnel.
2301
- tunnel: PLATFORM_NEEDS_METRO[cfg.platform],
2284
+ // Roku has no JS bundle to serve, so there is nothing to tunnel; a
2285
+ // standalone session runs the build as it is, without Metro.
2286
+ tunnel: PLATFORM_NEEDS_METRO[cfg.platform] && !cfg.standalone,
2302
2287
  ...cfg.model ? { model: cfg.model } : {},
2303
2288
  ...cfg.buildId ? { build_id: cfg.buildId } : {}
2304
2289
  };
@@ -2312,10 +2297,36 @@ function resolveDeviceModel(pools, platform, model) {
2312
2297
  const match = candidates.find((p) => p.model === wanted) ?? candidates.find((p) => p.model.toLowerCase() === wanted.toLowerCase());
2313
2298
  if (match) return match;
2314
2299
  const names = [...new Set(candidates.map((p) => p.model))].sort();
2300
+ const guess = closestModelName(wanted, names);
2315
2301
  throw new Error(
2316
- names.length ? `no ${platform} device model named "${model}" \u2014 available: ${names.join(", ")} (see \`vibeview list-devices --models\`)` : `no ${platform} devices are available right now`
2302
+ names.length ? `no ${platform} device model named "${model}"` + (guess ? ` (did you mean "${guess}"?)` : "") + ` \u2014 available: ${names.join(", ")} (see \`vibeview list-devices --models\`)` : `no ${platform} devices are available right now`
2317
2303
  );
2318
2304
  }
2305
+ function closestModelName(wanted, names) {
2306
+ const squash = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
2307
+ const words = (s) => new Set(s.toLowerCase().match(/[a-z0-9]+/g) ?? []);
2308
+ const w = squash(wanted);
2309
+ if (!w) return null;
2310
+ const unique = (list) => list.length === 1 ? list[0] : null;
2311
+ const same = names.filter((n) => squash(n) === w);
2312
+ if (same.length) return unique(same);
2313
+ const containing = names.filter((n) => squash(n).includes(w) || w.includes(squash(n)));
2314
+ if (containing.length) return unique(containing);
2315
+ const wantedWords = words(wanted);
2316
+ let best = 0;
2317
+ let bestNames = [];
2318
+ for (const n of names) {
2319
+ let shared = 0;
2320
+ for (const word of words(n)) if (wantedWords.has(word)) shared++;
2321
+ if (shared > best) {
2322
+ best = shared;
2323
+ bestNames = [n];
2324
+ } else if (shared === best && shared > 0) {
2325
+ bestNames.push(n);
2326
+ }
2327
+ }
2328
+ return best >= 2 ? unique(bestNames) : null;
2329
+ }
2319
2330
  var BLOCKING_WARNING_PREFIXES = ["No debug build", "App install failed", "App not found"];
2320
2331
  function findBlockingWarning(info) {
2321
2332
  return (info.warnings ?? []).find((w) => BLOCKING_WARNING_PREFIXES.some((p) => w.includes(p)));
@@ -2607,6 +2618,12 @@ ${hint}`);
2607
2618
  });
2608
2619
  const commitConfig = () => {
2609
2620
  saveDevConfig(process.cwd(), cfg);
2621
+ if (existing) {
2622
+ const changes = describeConfigChanges(existing, cfg);
2623
+ if (changes.length) {
2624
+ console.error(chalk4.dim(`Updated vibeview.json: ${changes.join(", ")}`));
2625
+ }
2626
+ }
2610
2627
  if (isFirstRun) {
2611
2628
  console.error(chalk4.dim(`Created vibeview.json (platform: ${platform}, app: ${appId}).`));
2612
2629
  const hint = fillLaterHint(
@@ -2726,7 +2743,7 @@ async function runDevInProcess(opts) {
2726
2743
  const captureMetroLine = (line) => {
2727
2744
  try {
2728
2745
  if (!metroLogStream) {
2729
- mkdirSync3(dirname5(metroLogPath(process.cwd())), { recursive: true });
2746
+ mkdirSync3(dirname4(metroLogPath(process.cwd())), { recursive: true });
2730
2747
  metroLogStream = createWriteStream(metroLogPath(process.cwd()), { flags: "w" });
2731
2748
  }
2732
2749
  metroLogStream.write(line.replace(/^\[metro\] /, "") + "\n");
@@ -3078,20 +3095,34 @@ function filterDeviceModels(models, opts) {
3078
3095
  os_version: m.os_version,
3079
3096
  device_type: m.device_type,
3080
3097
  category: m.category,
3081
- form_factor: m.form_factor ?? null
3098
+ form_factor: m.form_factor ?? null,
3099
+ // MCP only: an agent has no other way to see whether a model is free.
3100
+ ...opts.availability && typeof m.free === "number" && typeof m.total === "number" ? { free: m.free, total: m.total } : {}
3082
3101
  }));
3083
3102
  }
3084
3103
  var MODEL_HEADERS = ["MODEL", "OS", "TYPE", "CATEGORY", "FORM FACTOR"];
3085
- function formatDeviceModelsHuman(models) {
3104
+ function formatDeviceModelsHuman(models, opts = {}) {
3086
3105
  if (models.length === 0) {
3087
3106
  return "No device models available right now.\n";
3088
3107
  }
3089
- const rows = models.map((m) => [m.model, m.os_version, m.device_type, m.category, m.form_factor ?? "-"]).sort(
3108
+ const headers = [...MODEL_HEADERS, ...opts.availability ? ["FREE", "BUSY"] : []];
3109
+ const rows = models.map((m) => [
3110
+ m.model,
3111
+ m.os_version,
3112
+ m.device_type,
3113
+ m.category,
3114
+ m.form_factor ?? "-",
3115
+ ...opts.availability ? availabilityCells(m) : []
3116
+ ]).sort(
3090
3117
  (a, b) => a[2].localeCompare(b[2]) || a[3].localeCompare(b[3]) || a[0].localeCompare(b[0]) || a[1].localeCompare(b[1])
3091
3118
  );
3092
- const widths = MODEL_HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3119
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3093
3120
  const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
3094
- return [chalk5.bold(fmt(MODEL_HEADERS)), ...rows.map(fmt)].join("\n") + "\n";
3121
+ return [chalk5.bold(fmt(headers)), ...rows.map(fmt)].join("\n") + "\n";
3122
+ }
3123
+ function availabilityCells(m) {
3124
+ if (typeof m.free !== "number" || typeof m.total !== "number") return ["?", "?"];
3125
+ return [String(m.free), String(Math.max(0, m.total - m.free))];
3095
3126
  }
3096
3127
 
3097
3128
  // src/agent/registry.ts