vibeview 0.27.1 → 0.28.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.
@@ -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-2JLTC67A.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}\`)`
@@ -383,81 +409,24 @@ import { createInterface } from "readline";
383
409
  import { select as select2 } from "@inquirer/prompts";
384
410
  import chalk3 from "chalk";
385
411
 
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
412
  // src/lib/dev-setup.ts
444
413
  import chalk2 from "chalk";
445
414
  import { input, select } from "@inquirer/prompts";
446
415
 
447
416
  // 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";
417
+ import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
418
+ import { join as join3 } from "path";
450
419
  import ignore from "ignore";
451
420
  function readJson(cwd, name) {
452
421
  try {
453
- return JSON.parse(readFileSync3(join4(cwd, name), "utf-8"));
422
+ return JSON.parse(readFileSync3(join3(cwd, name), "utf-8"));
454
423
  } catch {
455
424
  return null;
456
425
  }
457
426
  }
458
427
  function isFile(path) {
459
428
  try {
460
- return statSync2(path).isFile();
429
+ return statSync(path).isFile();
461
430
  } catch {
462
431
  return false;
463
432
  }
@@ -465,19 +434,19 @@ function isFile(path) {
465
434
  function detectFlavor(cwd) {
466
435
  let deps = {};
467
436
  try {
468
- const pkg = JSON.parse(readFileSync3(join4(cwd, "package.json"), "utf-8"));
437
+ const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf-8"));
469
438
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
470
439
  } catch {
471
440
  }
472
441
  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";
442
+ if (existsSync2(join3(cwd, "android", "gradlew")) || iosWorkspaceName(cwd)) return "bare-rn";
443
+ if (isFile(join3(cwd, "manifest"))) return "roku";
475
444
  return "unknown";
476
445
  }
477
446
  function nativeDirShipped(cwd, dir) {
478
- if (!existsSync3(join4(cwd, dir))) return false;
447
+ if (!existsSync2(join3(cwd, dir))) return false;
479
448
  try {
480
- const ig = ignore().add(readFileSync3(join4(cwd, ".gitignore"), "utf-8"));
449
+ const ig = ignore().add(readFileSync3(join3(cwd, ".gitignore"), "utf-8"));
481
450
  return !ig.ignores(`${dir}/`);
482
451
  } catch {
483
452
  return true;
@@ -493,7 +462,7 @@ function isManagedExpo(cwd, platform) {
493
462
  }
494
463
  function iosWorkspaceName(cwd) {
495
464
  try {
496
- const found = readdirSync2(join4(cwd, "ios")).find((f) => f.endsWith(".xcworkspace"));
465
+ const found = readdirSync(join3(cwd, "ios")).find((f) => f.endsWith(".xcworkspace"));
497
466
  return found ? found.replace(/\.xcworkspace$/, "") : null;
498
467
  } catch {
499
468
  return null;
@@ -502,9 +471,9 @@ function iosWorkspaceName(cwd) {
502
471
  var IOS_ARTIFACT = "ios/build/Build/Products/Debug-iphonesimulator/*.app";
503
472
  var ANDROID_ARTIFACT = "android/app/build/outputs/apk/debug/*.apk";
504
473
  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";
474
+ if (existsSync2(join3(cwd, "yarn.lock"))) return "yarn install --immutable";
475
+ if (existsSync2(join3(cwd, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
476
+ if (existsSync2(join3(cwd, "package-lock.json"))) return "npm ci";
508
477
  return "yarn install --immutable";
509
478
  }
510
479
  function managedExpoAndroidCommand(cwd) {
@@ -518,7 +487,7 @@ var ROKU_ZIP_COMMAND = "zip -r out/channel.zip manifest source components images
518
487
  function rokuDefaults(cwd) {
519
488
  const pkg = readJson(cwd, "package.json");
520
489
  const scripts = pkg?.scripts ?? {};
521
- const runner = existsSync3(join4(cwd, "yarn.lock")) ? "yarn" : "npm run";
490
+ const runner = existsSync2(join3(cwd, "yarn.lock")) ? "yarn" : "npm run";
522
491
  const packageScript = Object.entries(scripts).find(
523
492
  ([name, body]) => !name.startsWith("deploy") && (name.startsWith("package") || body.includes("--package-only") || body.includes("zipPackage"))
524
493
  );
@@ -576,7 +545,7 @@ function expoSanitizedName(name) {
576
545
  return name.replace(/[\W_]+/g, "").normalize("NFD").replace(/[\u0300-\u036f]/g, "");
577
546
  }
578
547
  function managedExpoIosName(cwd) {
579
- if (existsSync3(join4(cwd, "app.config.js")) || existsSync3(join4(cwd, "app.config.ts"))) {
548
+ if (existsSync2(join3(cwd, "app.config.js")) || existsSync2(join3(cwd, "app.config.ts"))) {
580
549
  return null;
581
550
  }
582
551
  const raw = readJson(cwd, "app.config.json") ?? readJson(cwd, "app.json");
@@ -721,9 +690,9 @@ async function promptMetroCommand(cwd, platform) {
721
690
 
722
691
  // src/lib/source-pack.ts
723
692
  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";
693
+ import { lstatSync, mkdtempSync, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
694
+ import { tmpdir } from "os";
695
+ import { join as join4 } from "path";
727
696
  import ignore2 from "ignore";
728
697
  var NotAGitRepoError = class extends Error {
729
698
  constructor(message) {
@@ -749,7 +718,7 @@ async function listSourceFiles(projectRoot) {
749
718
  const stdout = await runGitLsFiles(projectRoot);
750
719
  const rawPaths = stdout.length ? stdout.split("\0").filter((p) => p.length > 0) : [];
751
720
  let filtered = rawPaths;
752
- const vvignorePath = join5(projectRoot, ".vvignore");
721
+ const vvignorePath = join4(projectRoot, ".vvignore");
753
722
  if (statOrNull(vvignorePath)?.isFile()) {
754
723
  const ig = ignore2().add(readFileSync4(vvignorePath, "utf-8"));
755
724
  filtered = filtered.filter((p) => !ig.ignores(p));
@@ -757,7 +726,7 @@ async function listSourceFiles(projectRoot) {
757
726
  const result = [];
758
727
  for (const relPath of filtered) {
759
728
  if (isForceExcluded(relPath)) continue;
760
- const st = statOrNull(join5(projectRoot, relPath));
729
+ const st = statOrNull(join4(projectRoot, relPath));
761
730
  if (!st) continue;
762
731
  result.push(relPath);
763
732
  }
@@ -807,7 +776,7 @@ async function packSource(projectRoot, files, opts = {}) {
807
776
  let totalBytes = 0;
808
777
  const sized = [];
809
778
  for (const relPath of files) {
810
- const bytes = lstatSync(join5(projectRoot, relPath)).size;
779
+ const bytes = lstatSync(join4(projectRoot, relPath)).size;
811
780
  totalBytes += bytes;
812
781
  sized.push({ path: relPath, bytes });
813
782
  }
@@ -818,13 +787,13 @@ async function packSource(projectRoot, files, opts = {}) {
818
787
  largest
819
788
  );
820
789
  }
821
- const tmpDir = mkdtempSync2(join5(tmpdir2(), "vibeview-source-pack-"));
822
- const cleanup = () => rmSync3(tmpDir, { recursive: true, force: true });
790
+ const tmpDir = mkdtempSync(join4(tmpdir(), "vibeview-source-pack-"));
791
+ const cleanup = () => rmSync2(tmpDir, { recursive: true, force: true });
823
792
  try {
824
793
  const supportsNull = await probeTarNullSupport();
825
- const tarPath = join5(tmpDir, "source.tgz");
794
+ const tarPath = join4(tmpDir, "source.tgz");
826
795
  if (supportsNull) {
827
- const listPath = join5(tmpDir, "files.list");
796
+ const listPath = join4(tmpDir, "files.list");
828
797
  writeFileSync3(listPath, files.map((f) => `${f}\0`).join(""));
829
798
  await runTar(["-czf", tarPath, "-C", projectRoot, "--null", "-T", listPath]);
830
799
  } else {
@@ -834,12 +803,12 @@ async function packSource(projectRoot, files, opts = {}) {
834
803
  `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
804
  );
836
805
  }
837
- const listPath = join5(tmpDir, "files.list");
806
+ const listPath = join4(tmpDir, "files.list");
838
807
  writeFileSync3(listPath, files.map((f) => `${f}
839
808
  `).join(""));
840
809
  await runTar(["-czf", tarPath, "-C", projectRoot, "-T", listPath]);
841
810
  }
842
- const sizeBytes = statSync3(tarPath).size;
811
+ const sizeBytes = statSync2(tarPath).size;
843
812
  return { tarPath, sizeBytes, cleanup };
844
813
  } catch (err) {
845
814
  cleanup();
@@ -855,12 +824,12 @@ var nullSupportCache = null;
855
824
  function probeTarNullSupport() {
856
825
  if (!nullSupportCache) {
857
826
  nullSupportCache = new Promise((resolve) => {
858
- const tmp = mkdtempSync2(join5(tmpdir2(), "vibeview-tar-probe-"));
859
- const listPath = join5(tmp, "empty.list");
827
+ const tmp = mkdtempSync(join4(tmpdir(), "vibeview-tar-probe-"));
828
+ const listPath = join4(tmp, "empty.list");
860
829
  writeFileSync3(listPath, "");
861
- const out = join5(tmp, "probe.tgz");
830
+ const out = join4(tmp, "probe.tgz");
862
831
  execFile("tar", ["-czf", out, "-C", tmp, "--null", "-T", listPath], (err) => {
863
- rmSync3(tmp, { recursive: true, force: true });
832
+ rmSync2(tmp, { recursive: true, force: true });
864
833
  resolve(!err);
865
834
  });
866
835
  });
@@ -1543,7 +1512,9 @@ async function waitForCloudBuilds(builds, opts, deps) {
1543
1512
  id: final.id,
1544
1513
  status: final.status,
1545
1514
  cost_micro: final.cost_micro,
1546
- error_summary: final.error_summary
1515
+ error_summary: final.error_summary,
1516
+ app_build_id: final.app_build_id ?? null,
1517
+ app_public_id: final.app_public_id ?? null
1547
1518
  });
1548
1519
  finalized.add(build.id);
1549
1520
  }
@@ -1731,7 +1702,7 @@ your build command must produce a zip \u2014 use the package/zip step, not deplo
1731
1702
  // src/commands/dev.ts
1732
1703
  import { spawn as spawn4 } from "child_process";
1733
1704
  import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
1734
- import { dirname as dirname5 } from "path";
1705
+ import { dirname as dirname4 } from "path";
1735
1706
  import { createInterface as createInterface3 } from "readline";
1736
1707
  import { select as select3 } from "@inquirer/prompts";
1737
1708
  import chalk4 from "chalk";
@@ -1926,15 +1897,15 @@ import { spawn as spawn3 } from "child_process";
1926
1897
  import { createInterface as createInterface2 } from "readline";
1927
1898
 
1928
1899
  // 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";
1900
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
1901
+ import { dirname as dirname2, join as join5 } from "path";
1931
1902
  var STATE_DIR2 = ".vibeview";
1932
1903
  var STATE_FILE2 = "metro-state.json";
1933
1904
  function statePath2(cwd) {
1934
- return join6(cwd, STATE_DIR2, STATE_FILE2);
1905
+ return join5(cwd, STATE_DIR2, STATE_FILE2);
1935
1906
  }
1936
1907
  function metroLogPath(cwd) {
1937
- return join6(cwd, STATE_DIR2, "metro.log");
1908
+ return join5(cwd, STATE_DIR2, "metro.log");
1938
1909
  }
1939
1910
  function isPidAlive2(pid) {
1940
1911
  try {
@@ -1946,7 +1917,7 @@ function isPidAlive2(pid) {
1946
1917
  }
1947
1918
  function writeMetroState(cwd, state) {
1948
1919
  const path = statePath2(cwd);
1949
- mkdirSync2(dirname3(path), { recursive: true });
1920
+ mkdirSync2(dirname2(path), { recursive: true });
1950
1921
  writeFileSync4(path, JSON.stringify(state, null, 2) + "\n");
1951
1922
  }
1952
1923
  function readMetroState(cwd) {
@@ -1964,7 +1935,7 @@ function readMetroState(cwd) {
1964
1935
  }
1965
1936
  function clearMetroState(cwd) {
1966
1937
  const path = statePath2(cwd);
1967
- if (existsSync4(path)) rmSync4(path, { force: true });
1938
+ if (existsSync3(path)) rmSync3(path, { force: true });
1968
1939
  }
1969
1940
  function killOwnedMetro(cwd) {
1970
1941
  const state = readMetroState(cwd);
@@ -2072,7 +2043,7 @@ async function waitFor(cond, timeoutMs, everyMs) {
2072
2043
 
2073
2044
  // src/lib/reload-loop.ts
2074
2045
  import { watch } from "fs";
2075
- import { basename as basename2, dirname as dirname4 } from "path";
2046
+ import { basename, dirname as dirname3 } from "path";
2076
2047
  function createReloadLoop(deps) {
2077
2048
  let inFlight = null;
2078
2049
  let pendingFollowUp = false;
@@ -2153,16 +2124,16 @@ function isIgnoredWatchPath(relPath, artifact, userIgnore = []) {
2153
2124
  if (IGNORED_FILES.includes(rel)) return true;
2154
2125
  if (userIgnore.some((entry) => isUnderPath(rel, entry))) return true;
2155
2126
  const artifactPattern = artifact.replace(/\\/g, "/").replace(/^\.\//, "");
2156
- const artifactDir = dirname4(artifactPattern);
2127
+ const artifactDir = dirname3(artifactPattern);
2157
2128
  if (artifactDir !== "." && (rel === artifactDir || rel.startsWith(`${artifactDir}/`))) {
2158
2129
  return true;
2159
2130
  }
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));
2131
+ if (dirname3(rel) !== artifactDir) return false;
2132
+ const name = basename(artifactPattern);
2133
+ const re = new RegExp(`^${name.split("*").map(escapeRegExp).join(".*")}$`);
2134
+ return re.test(basename(rel));
2164
2135
  }
2165
- function escapeRegExp2(s) {
2136
+ function escapeRegExp(s) {
2166
2137
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2167
2138
  }
2168
2139
  function watchProjectFiles(cwd, artifact, userIgnore = []) {
@@ -2198,7 +2169,7 @@ function emit(json, evt) {
2198
2169
  async function startDevSession(cfg, onEvent = () => {
2199
2170
  }, onTunnelState) {
2200
2171
  const api = new ApiClient(cfg.baseUrl, cfg.token);
2201
- const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform];
2172
+ const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform] && !cfg.standalone;
2202
2173
  const pool = cfg.model ? resolveDeviceModel(await api.listDeviceModels(), cfg.platform, cfg.model) : null;
2203
2174
  const created = await api.createSession(
2204
2175
  devSessionCreateBody(pool ? { ...cfg, deviceCategory: pool.category, model: pool.model } : cfg)
@@ -2286,7 +2257,7 @@ async function startDevSession(cfg, onEvent = () => {
2286
2257
  reportedWarnings.add(w);
2287
2258
  onEvent({ event: "warning", message: w });
2288
2259
  }
2289
- return { sessionId, pageUrl, stop: () => stopSession(0), reportedWarnings };
2260
+ return { sessionId, pageUrl, stop: () => stopSession(0), reportedWarnings, session: active };
2290
2261
  } catch (err) {
2291
2262
  await stopSession(1);
2292
2263
  throw err;
@@ -2297,8 +2268,9 @@ function devSessionCreateBody(cfg) {
2297
2268
  device_type: PLATFORM_DEVICE[cfg.platform].deviceType,
2298
2269
  device_category: cfg.deviceCategory,
2299
2270
  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],
2271
+ // Roku has no JS bundle to serve, so there is nothing to tunnel; a
2272
+ // standalone session runs the build as it is, without Metro.
2273
+ tunnel: PLATFORM_NEEDS_METRO[cfg.platform] && !cfg.standalone,
2302
2274
  ...cfg.model ? { model: cfg.model } : {},
2303
2275
  ...cfg.buildId ? { build_id: cfg.buildId } : {}
2304
2276
  };
@@ -2312,10 +2284,36 @@ function resolveDeviceModel(pools, platform, model) {
2312
2284
  const match = candidates.find((p) => p.model === wanted) ?? candidates.find((p) => p.model.toLowerCase() === wanted.toLowerCase());
2313
2285
  if (match) return match;
2314
2286
  const names = [...new Set(candidates.map((p) => p.model))].sort();
2287
+ const guess = closestModelName(wanted, names);
2315
2288
  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`
2289
+ 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
2290
  );
2318
2291
  }
2292
+ function closestModelName(wanted, names) {
2293
+ const squash = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
2294
+ const words = (s) => new Set(s.toLowerCase().match(/[a-z0-9]+/g) ?? []);
2295
+ const w = squash(wanted);
2296
+ if (!w) return null;
2297
+ const unique = (list) => list.length === 1 ? list[0] : null;
2298
+ const same = names.filter((n) => squash(n) === w);
2299
+ if (same.length) return unique(same);
2300
+ const containing = names.filter((n) => squash(n).includes(w) || w.includes(squash(n)));
2301
+ if (containing.length) return unique(containing);
2302
+ const wantedWords = words(wanted);
2303
+ let best = 0;
2304
+ let bestNames = [];
2305
+ for (const n of names) {
2306
+ let shared = 0;
2307
+ for (const word of words(n)) if (wantedWords.has(word)) shared++;
2308
+ if (shared > best) {
2309
+ best = shared;
2310
+ bestNames = [n];
2311
+ } else if (shared === best && shared > 0) {
2312
+ bestNames.push(n);
2313
+ }
2314
+ }
2315
+ return best >= 2 ? unique(bestNames) : null;
2316
+ }
2319
2317
  var BLOCKING_WARNING_PREFIXES = ["No debug build", "App install failed", "App not found"];
2320
2318
  function findBlockingWarning(info) {
2321
2319
  return (info.warnings ?? []).find((w) => BLOCKING_WARNING_PREFIXES.some((p) => w.includes(p)));
@@ -2607,6 +2605,12 @@ ${hint}`);
2607
2605
  });
2608
2606
  const commitConfig = () => {
2609
2607
  saveDevConfig(process.cwd(), cfg);
2608
+ if (existing) {
2609
+ const changes = describeConfigChanges(existing, cfg);
2610
+ if (changes.length) {
2611
+ console.error(chalk4.dim(`Updated vibeview.json: ${changes.join(", ")}`));
2612
+ }
2613
+ }
2610
2614
  if (isFirstRun) {
2611
2615
  console.error(chalk4.dim(`Created vibeview.json (platform: ${platform}, app: ${appId}).`));
2612
2616
  const hint = fillLaterHint(
@@ -2726,7 +2730,7 @@ async function runDevInProcess(opts) {
2726
2730
  const captureMetroLine = (line) => {
2727
2731
  try {
2728
2732
  if (!metroLogStream) {
2729
- mkdirSync3(dirname5(metroLogPath(process.cwd())), { recursive: true });
2733
+ mkdirSync3(dirname4(metroLogPath(process.cwd())), { recursive: true });
2730
2734
  metroLogStream = createWriteStream(metroLogPath(process.cwd()), { flags: "w" });
2731
2735
  }
2732
2736
  metroLogStream.write(line.replace(/^\[metro\] /, "") + "\n");
@@ -3078,20 +3082,34 @@ function filterDeviceModels(models, opts) {
3078
3082
  os_version: m.os_version,
3079
3083
  device_type: m.device_type,
3080
3084
  category: m.category,
3081
- form_factor: m.form_factor ?? null
3085
+ form_factor: m.form_factor ?? null,
3086
+ // MCP only: an agent has no other way to see whether a model is free.
3087
+ ...opts.availability && typeof m.free === "number" && typeof m.total === "number" ? { free: m.free, total: m.total } : {}
3082
3088
  }));
3083
3089
  }
3084
3090
  var MODEL_HEADERS = ["MODEL", "OS", "TYPE", "CATEGORY", "FORM FACTOR"];
3085
- function formatDeviceModelsHuman(models) {
3091
+ function formatDeviceModelsHuman(models, opts = {}) {
3086
3092
  if (models.length === 0) {
3087
3093
  return "No device models available right now.\n";
3088
3094
  }
3089
- const rows = models.map((m) => [m.model, m.os_version, m.device_type, m.category, m.form_factor ?? "-"]).sort(
3095
+ const headers = [...MODEL_HEADERS, ...opts.availability ? ["FREE", "BUSY"] : []];
3096
+ const rows = models.map((m) => [
3097
+ m.model,
3098
+ m.os_version,
3099
+ m.device_type,
3100
+ m.category,
3101
+ m.form_factor ?? "-",
3102
+ ...opts.availability ? availabilityCells(m) : []
3103
+ ]).sort(
3090
3104
  (a, b) => a[2].localeCompare(b[2]) || a[3].localeCompare(b[3]) || a[0].localeCompare(b[0]) || a[1].localeCompare(b[1])
3091
3105
  );
3092
- const widths = MODEL_HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3106
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3093
3107
  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";
3108
+ return [chalk5.bold(fmt(headers)), ...rows.map(fmt)].join("\n") + "\n";
3109
+ }
3110
+ function availabilityCells(m) {
3111
+ if (typeof m.free !== "number" || typeof m.total !== "number") return ["?", "?"];
3112
+ return [String(m.free), String(Math.max(0, m.total - m.free))];
3095
3113
  }
3096
3114
 
3097
3115
  // src/agent/registry.ts