vibeview 0.29.0 → 0.31.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.
@@ -13,7 +13,7 @@ import {
13
13
  resolveArtifact,
14
14
  resolveAuth,
15
15
  uploadAppBuild
16
- } from "./chunk-PBMB6TG4.js";
16
+ } from "./chunk-ZZK7YQSP.js";
17
17
 
18
18
  // src/lib/dev-config.ts
19
19
  import { readFileSync, renameSync, rmSync, writeFileSync } from "fs";
@@ -179,9 +179,33 @@ function saveDevConfig(cwd, cfg) {
179
179
  function updateDevConfig(cwd, platform, apply) {
180
180
  const fresh = loadDevConfig(cwd) ?? { defaultPlatform: platform, platforms: {} };
181
181
  if (apply(fresh) === false) return fresh;
182
+ const onDisk = readRawConfig(cwd);
183
+ if (onDisk !== void 0 && canonicalJson(onDisk) === canonicalJson(fresh)) return fresh;
182
184
  saveDevConfig(cwd, fresh);
183
185
  return fresh;
184
186
  }
187
+ function readRawConfig(cwd) {
188
+ try {
189
+ return JSON.parse(readFileSync(join(cwd, FILE), "utf-8"));
190
+ } catch {
191
+ return void 0;
192
+ }
193
+ }
194
+ function canonicalJson(value) {
195
+ const sortKeys = (v) => {
196
+ if (Array.isArray(v)) return v.map(sortKeys);
197
+ if (v && typeof v === "object") {
198
+ const out = {};
199
+ for (const k of Object.keys(v).sort()) {
200
+ const child = v[k];
201
+ if (child !== void 0) out[k] = sortKeys(child);
202
+ }
203
+ return out;
204
+ }
205
+ return v;
206
+ };
207
+ return JSON.stringify(sortKeys(value));
208
+ }
185
209
  function configLeaves(value, path = [], out = []) {
186
210
  if (value && typeof value === "object" && !Array.isArray(value)) {
187
211
  for (const [k, v] of Object.entries(value)) {
@@ -408,30 +432,25 @@ function isPidAlive(pid) {
408
432
  }
409
433
  }
410
434
  var WHOLE_DIR_LINE = /^(\/?)\.vibeview\/?$/;
435
+ var VIBEVIEW_DIR_GITIGNORE = "# VibeView local state (written by the vibeview CLI). test-spec.json is committed for CI.\n*\n!test-spec.json\n";
411
436
  function ensureGitignored(cwd) {
437
+ try {
438
+ const dir = join2(cwd, ".vibeview");
439
+ mkdirSync(dir, { recursive: true });
440
+ const inner = join2(dir, ".gitignore");
441
+ if (!existsSync(inner)) writeFileSync2(inner, VIBEVIEW_DIR_GITIGNORE);
442
+ } catch {
443
+ }
412
444
  try {
413
445
  const gitignore = join2(cwd, ".gitignore");
414
446
  if (!existsSync(gitignore)) return;
415
447
  const content = readFileSync2(gitignore, "utf-8");
416
448
  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
- }
426
- const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
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
- );
449
+ const i = lines.findIndex((l) => WHOLE_DIR_LINE.test(l.trim()));
450
+ if (i === -1) return;
451
+ const anchor = WHOLE_DIR_LINE.exec(lines[i].trim())[1];
452
+ lines.splice(i, 1, `${anchor}.vibeview/*`, `!${anchor}.vibeview/test-spec.json`);
453
+ writeFileSync2(gitignore, lines.join("\n"));
435
454
  } catch {
436
455
  }
437
456
  }
@@ -479,6 +498,108 @@ async function checkMetro(port, timeoutMs = 1500) {
479
498
  }
480
499
  }
481
500
 
501
+ // src/lib/local-config.ts
502
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
503
+ import { join as join3 } from "path";
504
+ var DIR = ".vibeview";
505
+ var FILE2 = "local.json";
506
+ function localConfigPath(cwd) {
507
+ return join3(cwd, DIR, FILE2);
508
+ }
509
+ function loadLocalDevConfig(cwd) {
510
+ let parsed;
511
+ try {
512
+ parsed = JSON.parse(readFileSync3(localConfigPath(cwd), "utf-8"));
513
+ } catch {
514
+ return {};
515
+ }
516
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
517
+ const raw = parsed;
518
+ const out = {};
519
+ if (isDevPlatform(raw.defaultPlatform)) out.defaultPlatform = raw.defaultPlatform;
520
+ if (raw.platforms && typeof raw.platforms === "object") {
521
+ for (const [key, value] of Object.entries(raw.platforms)) {
522
+ if (!isDevPlatform(key) || !value || typeof value !== "object") continue;
523
+ const v = value;
524
+ const entry = {};
525
+ if (typeof v.appId === "string" && v.appId) entry.appId = v.appId;
526
+ if (typeof v.deviceCategory === "string" && v.deviceCategory) {
527
+ entry.deviceCategory = v.deviceCategory;
528
+ }
529
+ if (typeof v.metroPort === "number" && Number.isInteger(v.metroPort) && v.metroPort > 0) {
530
+ entry.metroPort = v.metroPort;
531
+ }
532
+ if (Object.keys(entry).length) (out.platforms ??= {})[key] = entry;
533
+ }
534
+ }
535
+ return out;
536
+ }
537
+ function prune(cfg) {
538
+ const out = {};
539
+ if (cfg.defaultPlatform) out.defaultPlatform = cfg.defaultPlatform;
540
+ for (const [key, entry] of Object.entries(cfg.platforms ?? {})) {
541
+ const kept = Object.fromEntries(
542
+ Object.entries(entry ?? {}).filter(([, v]) => v !== void 0)
543
+ );
544
+ if (Object.keys(kept).length) (out.platforms ??= {})[key] = kept;
545
+ }
546
+ return out;
547
+ }
548
+ function saveLocalDevConfig(cwd, cfg) {
549
+ const pruned = prune(cfg);
550
+ const target = localConfigPath(cwd);
551
+ if (!pruned.defaultPlatform && !pruned.platforms) {
552
+ rmSync3(target, { force: true });
553
+ return;
554
+ }
555
+ mkdirSync2(join3(cwd, DIR), { recursive: true });
556
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
557
+ try {
558
+ writeFileSync3(tmp, JSON.stringify(pruned, null, 2) + "\n");
559
+ renameSync2(tmp, target);
560
+ } catch (err) {
561
+ rmSync3(tmp, { force: true });
562
+ throw err;
563
+ }
564
+ ensureGitignored(cwd);
565
+ }
566
+ function effectiveDevConfig(committed, local) {
567
+ if (!committed && !local.defaultPlatform && !local.platforms) return null;
568
+ const base = committed ? { ...committed, platforms: { ...committed.platforms } } : { defaultPlatform: local.defaultPlatform ?? "ios", platforms: {} };
569
+ if (local.defaultPlatform) base.defaultPlatform = local.defaultPlatform;
570
+ for (const [key, entry] of Object.entries(local.platforms ?? {})) {
571
+ const platform = key;
572
+ const merged = { ...base.platforms[platform] };
573
+ if (entry?.appId) merged.appId = entry.appId;
574
+ if (entry?.deviceCategory) merged.deviceCategory = entry.deviceCategory;
575
+ if (entry?.metroPort !== void 0) merged.metroPort = entry.metroPort;
576
+ base.platforms[platform] = merged;
577
+ }
578
+ return base;
579
+ }
580
+ function describeLocalChanges(before, after) {
581
+ const changes = [];
582
+ const a = prune(before);
583
+ const b = prune(after);
584
+ if (a.defaultPlatform !== b.defaultPlatform) {
585
+ changes.push(
586
+ b.defaultPlatform ? `defaultPlatform ${b.defaultPlatform}` : `defaultPlatform (removed, was ${a.defaultPlatform})`
587
+ );
588
+ }
589
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a.platforms ?? {}), ...Object.keys(b.platforms ?? {})]);
590
+ for (const key of keys) {
591
+ const pa = a.platforms?.[key] ?? {};
592
+ const pb = b.platforms?.[key] ?? {};
593
+ for (const field of ["appId", "deviceCategory", "metroPort"]) {
594
+ if (pa[field] === pb[field]) continue;
595
+ changes.push(
596
+ pb[field] === void 0 ? `${key}.${field} (removed, was ${String(pa[field])})` : `${key}.${field} ${String(pb[field])}`
597
+ );
598
+ }
599
+ }
600
+ return changes;
601
+ }
602
+
482
603
  // src/commands/build.ts
483
604
  import { spawn as spawn2 } from "child_process";
484
605
  import { createInterface } from "readline";
@@ -490,12 +611,12 @@ import chalk2 from "chalk";
490
611
  import { input, select } from "@inquirer/prompts";
491
612
 
492
613
  // src/lib/project-detect.ts
493
- import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
494
- import { join as join3 } from "path";
614
+ import { existsSync as existsSync2, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
615
+ import { join as join4 } from "path";
495
616
  import ignore from "ignore";
496
617
  function readJson(cwd, name) {
497
618
  try {
498
- return JSON.parse(readFileSync3(join3(cwd, name), "utf-8"));
619
+ return JSON.parse(readFileSync4(join4(cwd, name), "utf-8"));
499
620
  } catch {
500
621
  return null;
501
622
  }
@@ -510,19 +631,19 @@ function isFile(path) {
510
631
  function detectFlavor(cwd) {
511
632
  let deps = {};
512
633
  try {
513
- const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf-8"));
634
+ const pkg = JSON.parse(readFileSync4(join4(cwd, "package.json"), "utf-8"));
514
635
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
515
636
  } catch {
516
637
  }
517
638
  if ("expo" in deps) return "expo";
518
- if (existsSync2(join3(cwd, "android", "gradlew")) || iosWorkspaceName(cwd)) return "bare-rn";
519
- if (isFile(join3(cwd, "manifest"))) return "roku";
639
+ if (existsSync2(join4(cwd, "android", "gradlew")) || iosWorkspaceName(cwd)) return "bare-rn";
640
+ if (isFile(join4(cwd, "manifest"))) return "roku";
520
641
  return "unknown";
521
642
  }
522
643
  function nativeDirShipped(cwd, dir) {
523
- if (!existsSync2(join3(cwd, dir))) return false;
644
+ if (!existsSync2(join4(cwd, dir))) return false;
524
645
  try {
525
- const ig = ignore().add(readFileSync3(join3(cwd, ".gitignore"), "utf-8"));
646
+ const ig = ignore().add(readFileSync4(join4(cwd, ".gitignore"), "utf-8"));
526
647
  return !ig.ignores(`${dir}/`);
527
648
  } catch {
528
649
  return true;
@@ -538,7 +659,7 @@ function isManagedExpo(cwd, platform) {
538
659
  }
539
660
  function iosWorkspaceName(cwd) {
540
661
  try {
541
- const found = readdirSync(join3(cwd, "ios")).find((f) => f.endsWith(".xcworkspace"));
662
+ const found = readdirSync(join4(cwd, "ios")).find((f) => f.endsWith(".xcworkspace"));
542
663
  return found ? found.replace(/\.xcworkspace$/, "") : null;
543
664
  } catch {
544
665
  return null;
@@ -547,9 +668,9 @@ function iosWorkspaceName(cwd) {
547
668
  var IOS_ARTIFACT = "ios/build/Build/Products/Debug-iphonesimulator/*.app";
548
669
  var ANDROID_ARTIFACT = "android/app/build/outputs/apk/debug/*.apk";
549
670
  function installCommand(cwd) {
550
- if (existsSync2(join3(cwd, "yarn.lock"))) return "yarn install --immutable";
551
- if (existsSync2(join3(cwd, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
552
- if (existsSync2(join3(cwd, "package-lock.json"))) return "npm ci";
671
+ if (existsSync2(join4(cwd, "yarn.lock"))) return "yarn install --immutable";
672
+ if (existsSync2(join4(cwd, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
673
+ if (existsSync2(join4(cwd, "package-lock.json"))) return "npm ci";
553
674
  return "yarn install --immutable";
554
675
  }
555
676
  function managedExpoAndroidCommand(cwd) {
@@ -563,7 +684,7 @@ var ROKU_ZIP_COMMAND = "zip -r out/channel.zip manifest source components images
563
684
  function rokuDefaults(cwd) {
564
685
  const pkg = readJson(cwd, "package.json");
565
686
  const scripts = pkg?.scripts ?? {};
566
- const runner = existsSync2(join3(cwd, "yarn.lock")) ? "yarn" : "npm run";
687
+ const runner = existsSync2(join4(cwd, "yarn.lock")) ? "yarn" : "npm run";
567
688
  const packageScript = Object.entries(scripts).find(
568
689
  ([name, body]) => !name.startsWith("deploy") && (name.startsWith("package") || body.includes("--package-only") || body.includes("zipPackage"))
569
690
  );
@@ -621,7 +742,7 @@ function expoSanitizedName(name) {
621
742
  return name.replace(/[\W_]+/g, "").normalize("NFD").replace(/[\u0300-\u036f]/g, "");
622
743
  }
623
744
  function managedExpoIosName(cwd) {
624
- if (existsSync2(join3(cwd, "app.config.js")) || existsSync2(join3(cwd, "app.config.ts"))) {
745
+ if (existsSync2(join4(cwd, "app.config.js")) || existsSync2(join4(cwd, "app.config.ts"))) {
625
746
  return null;
626
747
  }
627
748
  const raw = readJson(cwd, "app.config.json") ?? readJson(cwd, "app.json");
@@ -766,9 +887,9 @@ async function promptMetroCommand(cwd, platform) {
766
887
 
767
888
  // src/lib/source-pack.ts
768
889
  import { execFile, spawn } from "child_process";
769
- import { lstatSync, mkdtempSync, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
890
+ import { lstatSync, mkdtempSync, readFileSync as readFileSync5, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
770
891
  import { tmpdir } from "os";
771
- import { join as join4 } from "path";
892
+ import { join as join5 } from "path";
772
893
  import ignore2 from "ignore";
773
894
  var NotAGitRepoError = class extends Error {
774
895
  constructor(message) {
@@ -794,15 +915,15 @@ async function listSourceFiles(projectRoot) {
794
915
  const stdout = await runGitLsFiles(projectRoot);
795
916
  const rawPaths = stdout.length ? stdout.split("\0").filter((p) => p.length > 0) : [];
796
917
  let filtered = rawPaths;
797
- const vvignorePath = join4(projectRoot, ".vvignore");
918
+ const vvignorePath = join5(projectRoot, ".vvignore");
798
919
  if (statOrNull(vvignorePath)?.isFile()) {
799
- const ig = ignore2().add(readFileSync4(vvignorePath, "utf-8"));
920
+ const ig = ignore2().add(readFileSync5(vvignorePath, "utf-8"));
800
921
  filtered = filtered.filter((p) => !ig.ignores(p));
801
922
  }
802
923
  const result = [];
803
924
  for (const relPath of filtered) {
804
925
  if (isForceExcluded(relPath)) continue;
805
- const st = statOrNull(join4(projectRoot, relPath));
926
+ const st = statOrNull(join5(projectRoot, relPath));
806
927
  if (!st) continue;
807
928
  result.push(relPath);
808
929
  }
@@ -852,7 +973,7 @@ async function packSource(projectRoot, files, opts = {}) {
852
973
  let totalBytes = 0;
853
974
  const sized = [];
854
975
  for (const relPath of files) {
855
- const bytes = lstatSync(join4(projectRoot, relPath)).size;
976
+ const bytes = lstatSync(join5(projectRoot, relPath)).size;
856
977
  totalBytes += bytes;
857
978
  sized.push({ path: relPath, bytes });
858
979
  }
@@ -863,14 +984,14 @@ async function packSource(projectRoot, files, opts = {}) {
863
984
  largest
864
985
  );
865
986
  }
866
- const tmpDir = mkdtempSync(join4(tmpdir(), "vibeview-source-pack-"));
867
- const cleanup = () => rmSync3(tmpDir, { recursive: true, force: true });
987
+ const tmpDir = mkdtempSync(join5(tmpdir(), "vibeview-source-pack-"));
988
+ const cleanup = () => rmSync4(tmpDir, { recursive: true, force: true });
868
989
  try {
869
990
  const supportsNull = await probeTarNullSupport();
870
- const tarPath = join4(tmpDir, "source.tgz");
991
+ const tarPath = join5(tmpDir, "source.tgz");
871
992
  if (supportsNull) {
872
- const listPath = join4(tmpDir, "files.list");
873
- writeFileSync3(listPath, files.map((f) => `${f}\0`).join(""));
993
+ const listPath = join5(tmpDir, "files.list");
994
+ writeFileSync4(listPath, files.map((f) => `${f}\0`).join(""));
874
995
  await runTar(["-czf", tarPath, "-C", projectRoot, "--null", "-T", listPath]);
875
996
  } else {
876
997
  const badName = files.find((f) => f.includes("\n"));
@@ -879,8 +1000,8 @@ async function packSource(projectRoot, files, opts = {}) {
879
1000
  `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`
880
1001
  );
881
1002
  }
882
- const listPath = join4(tmpDir, "files.list");
883
- writeFileSync3(listPath, files.map((f) => `${f}
1003
+ const listPath = join5(tmpDir, "files.list");
1004
+ writeFileSync4(listPath, files.map((f) => `${f}
884
1005
  `).join(""));
885
1006
  await runTar(["-czf", tarPath, "-C", projectRoot, "-T", listPath]);
886
1007
  }
@@ -900,12 +1021,12 @@ var nullSupportCache = null;
900
1021
  function probeTarNullSupport() {
901
1022
  if (!nullSupportCache) {
902
1023
  nullSupportCache = new Promise((resolve) => {
903
- const tmp = mkdtempSync(join4(tmpdir(), "vibeview-tar-probe-"));
904
- const listPath = join4(tmp, "empty.list");
905
- writeFileSync3(listPath, "");
906
- const out = join4(tmp, "probe.tgz");
1024
+ const tmp = mkdtempSync(join5(tmpdir(), "vibeview-tar-probe-"));
1025
+ const listPath = join5(tmp, "empty.list");
1026
+ writeFileSync4(listPath, "");
1027
+ const out = join5(tmp, "probe.tgz");
907
1028
  execFile("tar", ["-czf", out, "-C", tmp, "--null", "-T", listPath], (err) => {
908
- rmSync3(tmp, { recursive: true, force: true });
1029
+ rmSync4(tmp, { recursive: true, force: true });
909
1030
  resolve(!err);
910
1031
  });
911
1032
  });
@@ -1777,7 +1898,7 @@ your build command must produce a zip \u2014 use the package/zip step, not deplo
1777
1898
 
1778
1899
  // src/commands/dev.ts
1779
1900
  import { spawn as spawn4 } from "child_process";
1780
- import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
1901
+ import { createWriteStream, mkdirSync as mkdirSync4 } from "fs";
1781
1902
  import { dirname as dirname4 } from "path";
1782
1903
  import { createInterface as createInterface3 } from "readline";
1783
1904
  import { select as select3 } from "@inquirer/prompts";
@@ -1973,15 +2094,15 @@ import { spawn as spawn3 } from "child_process";
1973
2094
  import { createInterface as createInterface2 } from "readline";
1974
2095
 
1975
2096
  // src/lib/metro-state.ts
1976
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
1977
- import { dirname as dirname2, join as join5 } from "path";
2097
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
2098
+ import { dirname as dirname2, join as join6 } from "path";
1978
2099
  var STATE_DIR2 = ".vibeview";
1979
2100
  var STATE_FILE2 = "metro-state.json";
1980
2101
  function statePath2(cwd) {
1981
- return join5(cwd, STATE_DIR2, STATE_FILE2);
2102
+ return join6(cwd, STATE_DIR2, STATE_FILE2);
1982
2103
  }
1983
2104
  function metroLogPath(cwd) {
1984
- return join5(cwd, STATE_DIR2, "metro.log");
2105
+ return join6(cwd, STATE_DIR2, "metro.log");
1985
2106
  }
1986
2107
  function isPidAlive2(pid) {
1987
2108
  try {
@@ -1993,13 +2114,13 @@ function isPidAlive2(pid) {
1993
2114
  }
1994
2115
  function writeMetroState(cwd, state) {
1995
2116
  const path = statePath2(cwd);
1996
- mkdirSync2(dirname2(path), { recursive: true });
1997
- writeFileSync4(path, JSON.stringify(state, null, 2) + "\n");
2117
+ mkdirSync3(dirname2(path), { recursive: true });
2118
+ writeFileSync5(path, JSON.stringify(state, null, 2) + "\n");
1998
2119
  }
1999
2120
  function readMetroState(cwd) {
2000
2121
  let parsed;
2001
2122
  try {
2002
- parsed = JSON.parse(readFileSync5(statePath2(cwd), "utf-8"));
2123
+ parsed = JSON.parse(readFileSync6(statePath2(cwd), "utf-8"));
2003
2124
  } catch {
2004
2125
  return null;
2005
2126
  }
@@ -2011,7 +2132,7 @@ function readMetroState(cwd) {
2011
2132
  }
2012
2133
  function clearMetroState(cwd) {
2013
2134
  const path = statePath2(cwd);
2014
- if (existsSync3(path)) rmSync4(path, { force: true });
2135
+ if (existsSync3(path)) rmSync5(path, { force: true });
2015
2136
  }
2016
2137
  function killOwnedMetro(cwd) {
2017
2138
  const state = readMetroState(cwd);
@@ -2524,17 +2645,19 @@ function requireNonInteractive(json, message) {
2524
2645
  }
2525
2646
  async function ensureConfig(api, opts) {
2526
2647
  const existing = loadDevConfig(process.cwd());
2648
+ const local = loadLocalDevConfig(process.cwd());
2649
+ const effective = effectiveDevConfig(existing, local);
2527
2650
  if (opts.platform && !isDevPlatform(opts.platform)) {
2528
2651
  console.error(chalk4.red(`error: --platform must be one of ${DEV_PLATFORMS.join(", ")}`));
2529
2652
  process.exit(2);
2530
2653
  }
2531
- if (!opts.platform && !existing?.defaultPlatform) {
2654
+ if (!opts.platform && !effective?.defaultPlatform) {
2532
2655
  requireNonInteractive(
2533
2656
  opts.json,
2534
2657
  `--platform is required (no vibeview.json default yet) \u2014 pass --platform <${DEV_PLATFORMS.join("|")}>, or run once interactively to set a default.`
2535
2658
  );
2536
2659
  }
2537
- const platform = opts.platform || existing?.defaultPlatform || await select3({
2660
+ const platform = opts.platform || effective?.defaultPlatform || await select3({
2538
2661
  message: "Platform to run on",
2539
2662
  choices: platformChoices(process.cwd())
2540
2663
  });
@@ -2571,7 +2694,7 @@ async function ensureConfig(api, opts) {
2571
2694
  }
2572
2695
  }
2573
2696
  }
2574
- const entry = existing?.platforms?.[platform];
2697
+ const entry = effective?.platforms?.[platform];
2575
2698
  let allApps;
2576
2699
  try {
2577
2700
  allApps = await api.listApps();
@@ -2591,6 +2714,7 @@ ${hint}`);
2591
2714
  process.exit(2);
2592
2715
  }
2593
2716
  let appId = choice.appId;
2717
+ const appFromFlag = Boolean(opts.app) && appId === opts.app;
2594
2718
  let builtThisRun = false;
2595
2719
  if (choice.staleCached) {
2596
2720
  console.error(
@@ -2663,22 +2787,51 @@ ${hint}`);
2663
2787
  });
2664
2788
  }
2665
2789
  const deviceCategory = isTv ? "tv" : opts.deviceCategory || entry?.deviceCategory || "phone";
2666
- const metroResolved = resolveMetro(
2667
- existing,
2668
- platform,
2669
- opts.metroPort ? parseInt(opts.metroPort, 10) : void 0
2670
- );
2790
+ const metroPortFlag = opts.metroPort ? parseInt(opts.metroPort, 10) : void 0;
2791
+ const metroResolved = resolveMetro(effective, platform, metroPortFlag);
2792
+ const committedEntry = existing?.platforms?.[platform];
2793
+ const nextLocal = JSON.parse(JSON.stringify(local));
2794
+ const localEntry = (nextLocal.platforms ??= {})[platform] ??= {};
2795
+ let committedAppId = committedEntry?.appId;
2796
+ if (!committedAppId) {
2797
+ committedAppId = appId;
2798
+ } else if (appId !== committedAppId) {
2799
+ if (appFromFlag || local.platforms?.[platform]?.appId !== void 0) {
2800
+ localEntry.appId = appId;
2801
+ } else {
2802
+ committedAppId = appId;
2803
+ }
2804
+ }
2805
+ if (appId === committedAppId) delete localEntry.appId;
2806
+ let committedCategory = committedEntry?.deviceCategory;
2807
+ if (!isTv) {
2808
+ if (!committedCategory) committedCategory = deviceCategory;
2809
+ if (deviceCategory !== committedCategory) localEntry.deviceCategory = deviceCategory;
2810
+ else delete localEntry.deviceCategory;
2811
+ }
2812
+ const committedDefault = existing?.defaultPlatform ?? platform;
2813
+ if (opts.platform) {
2814
+ if (platform !== committedDefault) nextLocal.defaultPlatform = platform;
2815
+ else delete nextLocal.defaultPlatform;
2816
+ }
2671
2817
  const cfg = buildSavedConfig({
2672
2818
  existing,
2673
2819
  platform,
2674
- appId,
2675
- deviceCategory,
2820
+ appId: committedAppId,
2821
+ deviceCategory: committedCategory ?? deviceCategory,
2676
2822
  isTv,
2677
- metroPortFlag: opts.metroPort,
2823
+ // Never the flag: vibeview.json keeps (or, on the first run, seeds) the
2824
+ // shared default; the flag's port is saved locally below.
2825
+ metroPortFlag: void 0,
2678
2826
  isFirstRun,
2679
2827
  firstRunMetro,
2680
2828
  firstRunBuild
2681
2829
  });
2830
+ cfg.defaultPlatform = committedDefault;
2831
+ if (metroPortFlag !== void 0 && PLATFORM_NEEDS_METRO[platform]) {
2832
+ if (metroPortFlag !== resolveMetro(cfg, platform).port) localEntry.metroPort = metroPortFlag;
2833
+ else delete localEntry.metroPort;
2834
+ }
2682
2835
  const commitConfig = () => {
2683
2836
  updateDevConfig(process.cwd(), platform, (fresh) => {
2684
2837
  applyConfigChanges(fresh, existing, cfg);
@@ -2689,6 +2842,15 @@ ${hint}`);
2689
2842
  console.error(chalk4.dim(`Updated vibeview.json: ${changes.join(", ")}`));
2690
2843
  }
2691
2844
  }
2845
+ const localChanges = describeLocalChanges(local, nextLocal);
2846
+ if (localChanges.length) {
2847
+ saveLocalDevConfig(process.cwd(), nextLocal);
2848
+ console.error(
2849
+ chalk4.dim(
2850
+ `Saved to .vibeview/local.json (yours only, not committed): ${localChanges.join(", ")}`
2851
+ )
2852
+ );
2853
+ }
2692
2854
  if (isFirstRun) {
2693
2855
  console.error(chalk4.dim(`Created vibeview.json (platform: ${platform}, app: ${appId}).`));
2694
2856
  const hint = fillLaterHint(
@@ -2698,7 +2860,7 @@ ${hint}`);
2698
2860
  if (hint) console.error(chalk4.dim(hint));
2699
2861
  }
2700
2862
  };
2701
- const finalMetro = isFirstRun ? resolveMetro(cfg, platform, opts.metroPort ? parseInt(opts.metroPort, 10) : void 0) : metroResolved;
2863
+ const finalMetro = isFirstRun ? resolveMetro(effectiveDevConfig(cfg, nextLocal), platform, metroPortFlag) : metroResolved;
2702
2864
  return {
2703
2865
  platform,
2704
2866
  appId,
@@ -2793,12 +2955,21 @@ async function runDevInProcess(opts) {
2793
2955
  (l) => console.error(chalk4.dim(l))
2794
2956
  );
2795
2957
  cfg.appId = result.public_id;
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
- });
2958
+ const localNow = loadLocalDevConfig(process.cwd());
2959
+ const localApp = localNow.platforms?.[cfg.platform];
2960
+ if (localApp?.appId !== void 0) {
2961
+ const committedApp = loadDevConfig(process.cwd())?.platforms?.[cfg.platform]?.appId;
2962
+ if (result.public_id === committedApp) delete localApp.appId;
2963
+ else localApp.appId = result.public_id;
2964
+ saveLocalDevConfig(process.cwd(), localNow);
2965
+ } else {
2966
+ updateDevConfig(process.cwd(), cfg.platform, (fresh) => {
2967
+ const entry = fresh.platforms[cfg.platform];
2968
+ if (!entry || entry.appId === result.public_id) return false;
2969
+ entry.appId = result.public_id;
2970
+ return true;
2971
+ });
2972
+ }
2802
2973
  } catch (err) {
2803
2974
  const message = err.message;
2804
2975
  console.error(chalk4.red(`error: ${message}`));
@@ -2824,7 +2995,7 @@ async function runDevInProcess(opts) {
2824
2995
  const captureMetroLine = (line) => {
2825
2996
  try {
2826
2997
  if (!metroLogStream) {
2827
- mkdirSync3(dirname4(metroLogPath(process.cwd())), { recursive: true });
2998
+ mkdirSync4(dirname4(metroLogPath(process.cwd())), { recursive: true });
2828
2999
  metroLogStream = createWriteStream(metroLogPath(process.cwd()), { flags: "w" });
2829
3000
  }
2830
3001
  metroLogStream.write(line.replace(/^\[metro\] /, "") + "\n");
@@ -3807,7 +3978,7 @@ function agentExitCode(env) {
3807
3978
  }
3808
3979
 
3809
3980
  // src/commands/agent.ts
3810
- import { writeFileSync as writeFileSync5 } from "fs";
3981
+ import { writeFileSync as writeFileSync6 } from "fs";
3811
3982
  var REF_RE = /^@?(e\d+)$/;
3812
3983
  var COORDS_RE = /^(-?\d+),\s*(-?\d+)$/;
3813
3984
  var FlagError = class extends Error {
@@ -4017,7 +4188,7 @@ async function runAgentVerb(cmd, positionals, opts) {
4017
4188
  `);
4018
4189
  process.exit(1);
4019
4190
  }
4020
- writeFileSync5(outPath, imageBytes);
4191
+ writeFileSync6(outPath, imageBytes);
4021
4192
  if (opts.json) {
4022
4193
  console.log(JSON.stringify(envelope));
4023
4194
  } else {
@@ -4040,8 +4211,10 @@ export {
4040
4211
  DEV_PLATFORMS,
4041
4212
  PLATFORM_NEEDS_METRO,
4042
4213
  isDevPlatform,
4214
+ platformLabel,
4043
4215
  platformWithArticle,
4044
4216
  loadDevConfig,
4217
+ resolveMetro,
4045
4218
  parseBuildId,
4046
4219
  checkDevBuild,
4047
4220
  buildRows,
@@ -4052,6 +4225,8 @@ export {
4052
4225
  readMetroState,
4053
4226
  killOwnedMetro,
4054
4227
  checkMetro,
4228
+ loadLocalDevConfig,
4229
+ effectiveDevConfig,
4055
4230
  buildCommand,
4056
4231
  formatDuration,
4057
4232
  BUILD_TERMINAL_STATUSES,