vibeview 0.28.1 → 0.30.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,10 +13,10 @@ 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
- 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";
@@ -399,7 +462,7 @@ function readDevState(cwd) {
399
462
  function clearDevState(cwd) {
400
463
  const path = statePath(cwd);
401
464
  if (existsSync(path)) {
402
- rmSync(path, { force: true });
465
+ rmSync2(path, { force: true });
403
466
  }
404
467
  }
405
468
 
@@ -416,6 +479,108 @@ async function checkMetro(port, timeoutMs = 1500) {
416
479
  }
417
480
  }
418
481
 
482
+ // src/lib/local-config.ts
483
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
484
+ import { join as join3 } from "path";
485
+ var DIR = ".vibeview";
486
+ var FILE2 = "local.json";
487
+ function localConfigPath(cwd) {
488
+ return join3(cwd, DIR, FILE2);
489
+ }
490
+ function loadLocalDevConfig(cwd) {
491
+ let parsed;
492
+ try {
493
+ parsed = JSON.parse(readFileSync3(localConfigPath(cwd), "utf-8"));
494
+ } catch {
495
+ return {};
496
+ }
497
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
498
+ const raw = parsed;
499
+ const out = {};
500
+ if (isDevPlatform(raw.defaultPlatform)) out.defaultPlatform = raw.defaultPlatform;
501
+ if (raw.platforms && typeof raw.platforms === "object") {
502
+ for (const [key, value] of Object.entries(raw.platforms)) {
503
+ if (!isDevPlatform(key) || !value || typeof value !== "object") continue;
504
+ const v = value;
505
+ const entry = {};
506
+ if (typeof v.appId === "string" && v.appId) entry.appId = v.appId;
507
+ if (typeof v.deviceCategory === "string" && v.deviceCategory) {
508
+ entry.deviceCategory = v.deviceCategory;
509
+ }
510
+ if (typeof v.metroPort === "number" && Number.isInteger(v.metroPort) && v.metroPort > 0) {
511
+ entry.metroPort = v.metroPort;
512
+ }
513
+ if (Object.keys(entry).length) (out.platforms ??= {})[key] = entry;
514
+ }
515
+ }
516
+ return out;
517
+ }
518
+ function prune(cfg) {
519
+ const out = {};
520
+ if (cfg.defaultPlatform) out.defaultPlatform = cfg.defaultPlatform;
521
+ for (const [key, entry] of Object.entries(cfg.platforms ?? {})) {
522
+ const kept = Object.fromEntries(
523
+ Object.entries(entry ?? {}).filter(([, v]) => v !== void 0)
524
+ );
525
+ if (Object.keys(kept).length) (out.platforms ??= {})[key] = kept;
526
+ }
527
+ return out;
528
+ }
529
+ function saveLocalDevConfig(cwd, cfg) {
530
+ const pruned = prune(cfg);
531
+ const target = localConfigPath(cwd);
532
+ if (!pruned.defaultPlatform && !pruned.platforms) {
533
+ rmSync3(target, { force: true });
534
+ return;
535
+ }
536
+ mkdirSync2(join3(cwd, DIR), { recursive: true });
537
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
538
+ try {
539
+ writeFileSync3(tmp, JSON.stringify(pruned, null, 2) + "\n");
540
+ renameSync2(tmp, target);
541
+ } catch (err) {
542
+ rmSync3(tmp, { force: true });
543
+ throw err;
544
+ }
545
+ ensureGitignored(cwd);
546
+ }
547
+ function effectiveDevConfig(committed, local) {
548
+ if (!committed && !local.defaultPlatform && !local.platforms) return null;
549
+ const base = committed ? { ...committed, platforms: { ...committed.platforms } } : { defaultPlatform: local.defaultPlatform ?? "ios", platforms: {} };
550
+ if (local.defaultPlatform) base.defaultPlatform = local.defaultPlatform;
551
+ for (const [key, entry] of Object.entries(local.platforms ?? {})) {
552
+ const platform = key;
553
+ const merged = { ...base.platforms[platform] };
554
+ if (entry?.appId) merged.appId = entry.appId;
555
+ if (entry?.deviceCategory) merged.deviceCategory = entry.deviceCategory;
556
+ if (entry?.metroPort !== void 0) merged.metroPort = entry.metroPort;
557
+ base.platforms[platform] = merged;
558
+ }
559
+ return base;
560
+ }
561
+ function describeLocalChanges(before, after) {
562
+ const changes = [];
563
+ const a = prune(before);
564
+ const b = prune(after);
565
+ if (a.defaultPlatform !== b.defaultPlatform) {
566
+ changes.push(
567
+ b.defaultPlatform ? `defaultPlatform ${b.defaultPlatform}` : `defaultPlatform (removed, was ${a.defaultPlatform})`
568
+ );
569
+ }
570
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a.platforms ?? {}), ...Object.keys(b.platforms ?? {})]);
571
+ for (const key of keys) {
572
+ const pa = a.platforms?.[key] ?? {};
573
+ const pb = b.platforms?.[key] ?? {};
574
+ for (const field of ["appId", "deviceCategory", "metroPort"]) {
575
+ if (pa[field] === pb[field]) continue;
576
+ changes.push(
577
+ pb[field] === void 0 ? `${key}.${field} (removed, was ${String(pa[field])})` : `${key}.${field} ${String(pb[field])}`
578
+ );
579
+ }
580
+ }
581
+ return changes;
582
+ }
583
+
419
584
  // src/commands/build.ts
420
585
  import { spawn as spawn2 } from "child_process";
421
586
  import { createInterface } from "readline";
@@ -427,12 +592,12 @@ import chalk2 from "chalk";
427
592
  import { input, select } from "@inquirer/prompts";
428
593
 
429
594
  // src/lib/project-detect.ts
430
- import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync, statSync } from "fs";
431
- import { join as join3 } from "path";
595
+ import { existsSync as existsSync2, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
596
+ import { join as join4 } from "path";
432
597
  import ignore from "ignore";
433
598
  function readJson(cwd, name) {
434
599
  try {
435
- return JSON.parse(readFileSync3(join3(cwd, name), "utf-8"));
600
+ return JSON.parse(readFileSync4(join4(cwd, name), "utf-8"));
436
601
  } catch {
437
602
  return null;
438
603
  }
@@ -447,19 +612,19 @@ function isFile(path) {
447
612
  function detectFlavor(cwd) {
448
613
  let deps = {};
449
614
  try {
450
- const pkg = JSON.parse(readFileSync3(join3(cwd, "package.json"), "utf-8"));
615
+ const pkg = JSON.parse(readFileSync4(join4(cwd, "package.json"), "utf-8"));
451
616
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
452
617
  } catch {
453
618
  }
454
619
  if ("expo" in deps) return "expo";
455
- if (existsSync2(join3(cwd, "android", "gradlew")) || iosWorkspaceName(cwd)) return "bare-rn";
456
- if (isFile(join3(cwd, "manifest"))) return "roku";
620
+ if (existsSync2(join4(cwd, "android", "gradlew")) || iosWorkspaceName(cwd)) return "bare-rn";
621
+ if (isFile(join4(cwd, "manifest"))) return "roku";
457
622
  return "unknown";
458
623
  }
459
624
  function nativeDirShipped(cwd, dir) {
460
- if (!existsSync2(join3(cwd, dir))) return false;
625
+ if (!existsSync2(join4(cwd, dir))) return false;
461
626
  try {
462
- const ig = ignore().add(readFileSync3(join3(cwd, ".gitignore"), "utf-8"));
627
+ const ig = ignore().add(readFileSync4(join4(cwd, ".gitignore"), "utf-8"));
463
628
  return !ig.ignores(`${dir}/`);
464
629
  } catch {
465
630
  return true;
@@ -475,7 +640,7 @@ function isManagedExpo(cwd, platform) {
475
640
  }
476
641
  function iosWorkspaceName(cwd) {
477
642
  try {
478
- const found = readdirSync(join3(cwd, "ios")).find((f) => f.endsWith(".xcworkspace"));
643
+ const found = readdirSync(join4(cwd, "ios")).find((f) => f.endsWith(".xcworkspace"));
479
644
  return found ? found.replace(/\.xcworkspace$/, "") : null;
480
645
  } catch {
481
646
  return null;
@@ -484,9 +649,9 @@ function iosWorkspaceName(cwd) {
484
649
  var IOS_ARTIFACT = "ios/build/Build/Products/Debug-iphonesimulator/*.app";
485
650
  var ANDROID_ARTIFACT = "android/app/build/outputs/apk/debug/*.apk";
486
651
  function installCommand(cwd) {
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";
652
+ if (existsSync2(join4(cwd, "yarn.lock"))) return "yarn install --immutable";
653
+ if (existsSync2(join4(cwd, "pnpm-lock.yaml"))) return "pnpm install --frozen-lockfile";
654
+ if (existsSync2(join4(cwd, "package-lock.json"))) return "npm ci";
490
655
  return "yarn install --immutable";
491
656
  }
492
657
  function managedExpoAndroidCommand(cwd) {
@@ -500,7 +665,7 @@ var ROKU_ZIP_COMMAND = "zip -r out/channel.zip manifest source components images
500
665
  function rokuDefaults(cwd) {
501
666
  const pkg = readJson(cwd, "package.json");
502
667
  const scripts = pkg?.scripts ?? {};
503
- const runner = existsSync2(join3(cwd, "yarn.lock")) ? "yarn" : "npm run";
668
+ const runner = existsSync2(join4(cwd, "yarn.lock")) ? "yarn" : "npm run";
504
669
  const packageScript = Object.entries(scripts).find(
505
670
  ([name, body]) => !name.startsWith("deploy") && (name.startsWith("package") || body.includes("--package-only") || body.includes("zipPackage"))
506
671
  );
@@ -558,7 +723,7 @@ function expoSanitizedName(name) {
558
723
  return name.replace(/[\W_]+/g, "").normalize("NFD").replace(/[\u0300-\u036f]/g, "");
559
724
  }
560
725
  function managedExpoIosName(cwd) {
561
- if (existsSync2(join3(cwd, "app.config.js")) || existsSync2(join3(cwd, "app.config.ts"))) {
726
+ if (existsSync2(join4(cwd, "app.config.js")) || existsSync2(join4(cwd, "app.config.ts"))) {
562
727
  return null;
563
728
  }
564
729
  const raw = readJson(cwd, "app.config.json") ?? readJson(cwd, "app.json");
@@ -703,9 +868,9 @@ async function promptMetroCommand(cwd, platform) {
703
868
 
704
869
  // src/lib/source-pack.ts
705
870
  import { execFile, spawn } from "child_process";
706
- import { lstatSync, mkdtempSync, readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
871
+ import { lstatSync, mkdtempSync, readFileSync as readFileSync5, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
707
872
  import { tmpdir } from "os";
708
- import { join as join4 } from "path";
873
+ import { join as join5 } from "path";
709
874
  import ignore2 from "ignore";
710
875
  var NotAGitRepoError = class extends Error {
711
876
  constructor(message) {
@@ -731,15 +896,15 @@ async function listSourceFiles(projectRoot) {
731
896
  const stdout = await runGitLsFiles(projectRoot);
732
897
  const rawPaths = stdout.length ? stdout.split("\0").filter((p) => p.length > 0) : [];
733
898
  let filtered = rawPaths;
734
- const vvignorePath = join4(projectRoot, ".vvignore");
899
+ const vvignorePath = join5(projectRoot, ".vvignore");
735
900
  if (statOrNull(vvignorePath)?.isFile()) {
736
- const ig = ignore2().add(readFileSync4(vvignorePath, "utf-8"));
901
+ const ig = ignore2().add(readFileSync5(vvignorePath, "utf-8"));
737
902
  filtered = filtered.filter((p) => !ig.ignores(p));
738
903
  }
739
904
  const result = [];
740
905
  for (const relPath of filtered) {
741
906
  if (isForceExcluded(relPath)) continue;
742
- const st = statOrNull(join4(projectRoot, relPath));
907
+ const st = statOrNull(join5(projectRoot, relPath));
743
908
  if (!st) continue;
744
909
  result.push(relPath);
745
910
  }
@@ -789,7 +954,7 @@ async function packSource(projectRoot, files, opts = {}) {
789
954
  let totalBytes = 0;
790
955
  const sized = [];
791
956
  for (const relPath of files) {
792
- const bytes = lstatSync(join4(projectRoot, relPath)).size;
957
+ const bytes = lstatSync(join5(projectRoot, relPath)).size;
793
958
  totalBytes += bytes;
794
959
  sized.push({ path: relPath, bytes });
795
960
  }
@@ -800,14 +965,14 @@ async function packSource(projectRoot, files, opts = {}) {
800
965
  largest
801
966
  );
802
967
  }
803
- const tmpDir = mkdtempSync(join4(tmpdir(), "vibeview-source-pack-"));
804
- const cleanup = () => rmSync2(tmpDir, { recursive: true, force: true });
968
+ const tmpDir = mkdtempSync(join5(tmpdir(), "vibeview-source-pack-"));
969
+ const cleanup = () => rmSync4(tmpDir, { recursive: true, force: true });
805
970
  try {
806
971
  const supportsNull = await probeTarNullSupport();
807
- const tarPath = join4(tmpDir, "source.tgz");
972
+ const tarPath = join5(tmpDir, "source.tgz");
808
973
  if (supportsNull) {
809
- const listPath = join4(tmpDir, "files.list");
810
- writeFileSync3(listPath, files.map((f) => `${f}\0`).join(""));
974
+ const listPath = join5(tmpDir, "files.list");
975
+ writeFileSync4(listPath, files.map((f) => `${f}\0`).join(""));
811
976
  await runTar(["-czf", tarPath, "-C", projectRoot, "--null", "-T", listPath]);
812
977
  } else {
813
978
  const badName = files.find((f) => f.includes("\n"));
@@ -816,8 +981,8 @@ async function packSource(projectRoot, files, opts = {}) {
816
981
  `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`
817
982
  );
818
983
  }
819
- const listPath = join4(tmpDir, "files.list");
820
- writeFileSync3(listPath, files.map((f) => `${f}
984
+ const listPath = join5(tmpDir, "files.list");
985
+ writeFileSync4(listPath, files.map((f) => `${f}
821
986
  `).join(""));
822
987
  await runTar(["-czf", tarPath, "-C", projectRoot, "-T", listPath]);
823
988
  }
@@ -837,12 +1002,12 @@ var nullSupportCache = null;
837
1002
  function probeTarNullSupport() {
838
1003
  if (!nullSupportCache) {
839
1004
  nullSupportCache = new Promise((resolve) => {
840
- const tmp = mkdtempSync(join4(tmpdir(), "vibeview-tar-probe-"));
841
- const listPath = join4(tmp, "empty.list");
842
- writeFileSync3(listPath, "");
843
- const out = join4(tmp, "probe.tgz");
1005
+ const tmp = mkdtempSync(join5(tmpdir(), "vibeview-tar-probe-"));
1006
+ const listPath = join5(tmp, "empty.list");
1007
+ writeFileSync4(listPath, "");
1008
+ const out = join5(tmp, "probe.tgz");
844
1009
  execFile("tar", ["-czf", out, "-C", tmp, "--null", "-T", listPath], (err) => {
845
- rmSync2(tmp, { recursive: true, force: true });
1010
+ rmSync4(tmp, { recursive: true, force: true });
846
1011
  resolve(!err);
847
1012
  });
848
1013
  });
@@ -946,30 +1111,28 @@ async function resolveBarePlatform(cwd, cfg, opts) {
946
1111
  }
947
1112
  return platform;
948
1113
  }
949
- function persistPromptedBuild(cwd, cfg, platform, build) {
950
- const next = cfg ?? { defaultPlatform: platform, platforms: {} };
951
- const entry = next.platforms[platform];
952
- if (entry) {
953
- entry.build = build;
954
- } else {
955
- next.platforms[platform] = { build };
956
- }
957
- saveDevConfig(cwd, next);
958
- return next;
1114
+ function persistPromptedBuild(cwd, platform, build) {
1115
+ return updateDevConfig(cwd, platform, (next) => {
1116
+ const entry = next.platforms[platform];
1117
+ if (entry) {
1118
+ entry.build = build;
1119
+ } else {
1120
+ next.platforms[platform] = { build };
1121
+ }
1122
+ });
959
1123
  }
960
- function persistUploadedAppId(cwd, cfg, platform, build, appId) {
961
- const next = cfg ?? { defaultPlatform: platform, platforms: {} };
962
- const entry = next.platforms[platform];
963
- if (!entry) {
964
- next.platforms[platform] = { appId, build };
965
- saveDevConfig(cwd, next);
966
- return;
967
- }
968
- if (entry.appId !== appId || !entry.build) {
1124
+ function persistUploadedAppId(cwd, platform, build, appId) {
1125
+ updateDevConfig(cwd, platform, (next) => {
1126
+ const entry = next.platforms[platform];
1127
+ if (!entry) {
1128
+ next.platforms[platform] = { appId, build };
1129
+ return true;
1130
+ }
1131
+ if (entry.appId === appId && entry.build) return false;
969
1132
  entry.appId = appId;
970
- entry.build = build;
971
- saveDevConfig(cwd, next);
972
- }
1133
+ if (!entry.build) entry.build = build;
1134
+ return true;
1135
+ });
973
1136
  }
974
1137
  async function resolveBuildConfig(cwd, cfgRef, platform, opts) {
975
1138
  const existing = cfgRef.current?.platforms?.[platform]?.build;
@@ -985,11 +1148,15 @@ async function resolveBuildConfig(cwd, cfgRef, platform, opts) {
985
1148
  if (!answered) {
986
1149
  fail(opts, "build config is required to run vibeview build", "config_required");
987
1150
  }
988
- cfgRef.current = persistPromptedBuild(cwd, cfgRef.current, platform, answered);
1151
+ cfgRef.current = persistPromptedBuild(cwd, platform, answered);
989
1152
  return answered;
990
1153
  }
991
- function persistPromptedProductionBuild(cwd, cfg, platform, production) {
992
- const next = cfg ?? { defaultPlatform: platform, platforms: {} };
1154
+ function persistPromptedProductionBuild(cwd, platform, production) {
1155
+ return updateDevConfig(cwd, platform, (next) => {
1156
+ mergeProductionPair(next, platform, production);
1157
+ });
1158
+ }
1159
+ function mergeProductionPair(next, platform, production) {
993
1160
  const existingBuild = next.platforms[platform]?.build;
994
1161
  if (!existingBuild?.command || !existingBuild?.artifact) {
995
1162
  throw new Error(
@@ -997,8 +1164,6 @@ function persistPromptedProductionBuild(cwd, cfg, platform, production) {
997
1164
  );
998
1165
  }
999
1166
  next.platforms[platform].build = { ...existingBuild, ...production };
1000
- saveDevConfig(cwd, next);
1001
- return next;
1002
1167
  }
1003
1168
  async function resolveProductionBuildConfig(cwd, cfgRef, platform, opts) {
1004
1169
  const entry = cfgRef.current?.platforms?.[platform]?.build;
@@ -1016,7 +1181,7 @@ async function resolveProductionBuildConfig(cwd, cfgRef, platform, opts) {
1016
1181
  if (!answered) {
1017
1182
  fail(opts, "a production build config is required for --production", "config_required");
1018
1183
  }
1019
- cfgRef.current = persistPromptedProductionBuild(cwd, cfgRef.current, platform, answered);
1184
+ cfgRef.current = persistPromptedProductionBuild(cwd, platform, answered);
1020
1185
  return { command: answered.productionCommand, artifact: answered.productionArtifact };
1021
1186
  }
1022
1187
  async function buildCommand(opts, cloudDeps) {
@@ -1036,7 +1201,6 @@ async function buildCommand(opts, cloudDeps) {
1036
1201
  }
1037
1202
  const platform = await resolveBarePlatform(cwd, cfgRef.current, opts);
1038
1203
  const build = await resolveBuildConfig(cwd, cfgRef, platform, opts);
1039
- const cfg = cfgRef.current;
1040
1204
  const result = await runBuildForPlatform(
1041
1205
  cwd,
1042
1206
  platform,
@@ -1045,7 +1209,7 @@ async function buildCommand(opts, cloudDeps) {
1045
1209
  (l) => console.error(chalk3.dim(l))
1046
1210
  );
1047
1211
  for (const w of result.warnings ?? []) console.error(chalk3.yellow(`\u26A0 ${w}`));
1048
- persistUploadedAppId(cwd, cfg, platform, build, result.public_id);
1212
+ persistUploadedAppId(cwd, platform, build, result.public_id);
1049
1213
  if (opts.json) {
1050
1214
  console.log(
1051
1215
  JSON.stringify({ app_id: result.public_id, name: result.name, platform: result.platform })
@@ -1148,16 +1312,17 @@ function requireProductionCapable(opts, platform) {
1148
1312
  fail(opts, productionUnsupportedReason(platform));
1149
1313
  }
1150
1314
  }
1151
- function persistPickedAppId(cwd, cfg, platform, appId) {
1152
- const next = cfg ?? { defaultPlatform: platform, platforms: {} };
1153
- const entry = next.platforms[platform];
1154
- if (entry) {
1155
- entry.appId = appId;
1156
- } else {
1157
- next.platforms[platform] = { appId };
1158
- }
1159
- saveDevConfig(cwd, next);
1160
- return next;
1315
+ function persistPickedAppId(cwd, platform, appId, opts = {}) {
1316
+ return updateDevConfig(cwd, platform, (next) => {
1317
+ const entry = next.platforms[platform];
1318
+ if (opts.onlyIfUnset && entry?.appId) return false;
1319
+ if (entry) {
1320
+ entry.appId = appId;
1321
+ } else {
1322
+ next.platforms[platform] = { appId };
1323
+ }
1324
+ return true;
1325
+ });
1161
1326
  }
1162
1327
  var APP_PICKER_SENTINEL = {
1163
1328
  CREATE_NEW_APP: "__vibeview_create_new_app__",
@@ -1225,7 +1390,7 @@ async function resolveCloudAppIds(cwd, cfgRef, platforms, opts, listApps) {
1225
1390
  createFromArtifact.add(platform);
1226
1391
  continue;
1227
1392
  }
1228
- cfgRef.current = persistPickedAppId(cwd, cfgRef.current, platform, appId);
1393
+ cfgRef.current = persistPickedAppId(cwd, platform, appId);
1229
1394
  }
1230
1395
  return createFromArtifact;
1231
1396
  }
@@ -1235,7 +1400,7 @@ function persistResolvedAppIds(cwd, cfgRef, createFromArtifact, finished) {
1235
1400
  const platform = b.platform;
1236
1401
  if (!createFromArtifact.has(platform)) continue;
1237
1402
  if (cfgRef.current?.platforms?.[platform]?.appId) continue;
1238
- cfgRef.current = persistPickedAppId(cwd, cfgRef.current, platform, b.app_public_id);
1403
+ cfgRef.current = persistPickedAppId(cwd, platform, b.app_public_id, { onlyIfUnset: true });
1239
1404
  }
1240
1405
  }
1241
1406
  function describeAllExclusions(configured) {
@@ -1714,7 +1879,7 @@ your build command must produce a zip \u2014 use the package/zip step, not deplo
1714
1879
 
1715
1880
  // src/commands/dev.ts
1716
1881
  import { spawn as spawn4 } from "child_process";
1717
- import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
1882
+ import { createWriteStream, mkdirSync as mkdirSync4 } from "fs";
1718
1883
  import { dirname as dirname4 } from "path";
1719
1884
  import { createInterface as createInterface3 } from "readline";
1720
1885
  import { select as select3 } from "@inquirer/prompts";
@@ -1910,15 +2075,15 @@ import { spawn as spawn3 } from "child_process";
1910
2075
  import { createInterface as createInterface2 } from "readline";
1911
2076
 
1912
2077
  // src/lib/metro-state.ts
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";
2078
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
2079
+ import { dirname as dirname2, join as join6 } from "path";
1915
2080
  var STATE_DIR2 = ".vibeview";
1916
2081
  var STATE_FILE2 = "metro-state.json";
1917
2082
  function statePath2(cwd) {
1918
- return join5(cwd, STATE_DIR2, STATE_FILE2);
2083
+ return join6(cwd, STATE_DIR2, STATE_FILE2);
1919
2084
  }
1920
2085
  function metroLogPath(cwd) {
1921
- return join5(cwd, STATE_DIR2, "metro.log");
2086
+ return join6(cwd, STATE_DIR2, "metro.log");
1922
2087
  }
1923
2088
  function isPidAlive2(pid) {
1924
2089
  try {
@@ -1930,13 +2095,13 @@ function isPidAlive2(pid) {
1930
2095
  }
1931
2096
  function writeMetroState(cwd, state) {
1932
2097
  const path = statePath2(cwd);
1933
- mkdirSync2(dirname2(path), { recursive: true });
1934
- writeFileSync4(path, JSON.stringify(state, null, 2) + "\n");
2098
+ mkdirSync3(dirname2(path), { recursive: true });
2099
+ writeFileSync5(path, JSON.stringify(state, null, 2) + "\n");
1935
2100
  }
1936
2101
  function readMetroState(cwd) {
1937
2102
  let parsed;
1938
2103
  try {
1939
- parsed = JSON.parse(readFileSync5(statePath2(cwd), "utf-8"));
2104
+ parsed = JSON.parse(readFileSync6(statePath2(cwd), "utf-8"));
1940
2105
  } catch {
1941
2106
  return null;
1942
2107
  }
@@ -1948,7 +2113,7 @@ function readMetroState(cwd) {
1948
2113
  }
1949
2114
  function clearMetroState(cwd) {
1950
2115
  const path = statePath2(cwd);
1951
- if (existsSync3(path)) rmSync3(path, { force: true });
2116
+ if (existsSync3(path)) rmSync5(path, { force: true });
1952
2117
  }
1953
2118
  function killOwnedMetro(cwd) {
1954
2119
  const state = readMetroState(cwd);
@@ -2461,17 +2626,19 @@ function requireNonInteractive(json, message) {
2461
2626
  }
2462
2627
  async function ensureConfig(api, opts) {
2463
2628
  const existing = loadDevConfig(process.cwd());
2629
+ const local = loadLocalDevConfig(process.cwd());
2630
+ const effective = effectiveDevConfig(existing, local);
2464
2631
  if (opts.platform && !isDevPlatform(opts.platform)) {
2465
2632
  console.error(chalk4.red(`error: --platform must be one of ${DEV_PLATFORMS.join(", ")}`));
2466
2633
  process.exit(2);
2467
2634
  }
2468
- if (!opts.platform && !existing?.defaultPlatform) {
2635
+ if (!opts.platform && !effective?.defaultPlatform) {
2469
2636
  requireNonInteractive(
2470
2637
  opts.json,
2471
2638
  `--platform is required (no vibeview.json default yet) \u2014 pass --platform <${DEV_PLATFORMS.join("|")}>, or run once interactively to set a default.`
2472
2639
  );
2473
2640
  }
2474
- const platform = opts.platform || existing?.defaultPlatform || await select3({
2641
+ const platform = opts.platform || effective?.defaultPlatform || await select3({
2475
2642
  message: "Platform to run on",
2476
2643
  choices: platformChoices(process.cwd())
2477
2644
  });
@@ -2508,7 +2675,7 @@ async function ensureConfig(api, opts) {
2508
2675
  }
2509
2676
  }
2510
2677
  }
2511
- const entry = existing?.platforms?.[platform];
2678
+ const entry = effective?.platforms?.[platform];
2512
2679
  let allApps;
2513
2680
  try {
2514
2681
  allApps = await api.listApps();
@@ -2528,6 +2695,7 @@ ${hint}`);
2528
2695
  process.exit(2);
2529
2696
  }
2530
2697
  let appId = choice.appId;
2698
+ const appFromFlag = Boolean(opts.app) && appId === opts.app;
2531
2699
  let builtThisRun = false;
2532
2700
  if (choice.staleCached) {
2533
2701
  console.error(
@@ -2600,30 +2768,70 @@ ${hint}`);
2600
2768
  });
2601
2769
  }
2602
2770
  const deviceCategory = isTv ? "tv" : opts.deviceCategory || entry?.deviceCategory || "phone";
2603
- const metroResolved = resolveMetro(
2604
- existing,
2605
- platform,
2606
- opts.metroPort ? parseInt(opts.metroPort, 10) : void 0
2607
- );
2771
+ const metroPortFlag = opts.metroPort ? parseInt(opts.metroPort, 10) : void 0;
2772
+ const metroResolved = resolveMetro(effective, platform, metroPortFlag);
2773
+ const committedEntry = existing?.platforms?.[platform];
2774
+ const nextLocal = JSON.parse(JSON.stringify(local));
2775
+ const localEntry = (nextLocal.platforms ??= {})[platform] ??= {};
2776
+ let committedAppId = committedEntry?.appId;
2777
+ if (!committedAppId) {
2778
+ committedAppId = appId;
2779
+ } else if (appId !== committedAppId) {
2780
+ if (appFromFlag || local.platforms?.[platform]?.appId !== void 0) {
2781
+ localEntry.appId = appId;
2782
+ } else {
2783
+ committedAppId = appId;
2784
+ }
2785
+ }
2786
+ if (appId === committedAppId) delete localEntry.appId;
2787
+ let committedCategory = committedEntry?.deviceCategory;
2788
+ if (!isTv) {
2789
+ if (!committedCategory) committedCategory = deviceCategory;
2790
+ if (deviceCategory !== committedCategory) localEntry.deviceCategory = deviceCategory;
2791
+ else delete localEntry.deviceCategory;
2792
+ }
2793
+ const committedDefault = existing?.defaultPlatform ?? platform;
2794
+ if (opts.platform) {
2795
+ if (platform !== committedDefault) nextLocal.defaultPlatform = platform;
2796
+ else delete nextLocal.defaultPlatform;
2797
+ }
2608
2798
  const cfg = buildSavedConfig({
2609
2799
  existing,
2610
2800
  platform,
2611
- appId,
2612
- deviceCategory,
2801
+ appId: committedAppId,
2802
+ deviceCategory: committedCategory ?? deviceCategory,
2613
2803
  isTv,
2614
- metroPortFlag: opts.metroPort,
2804
+ // Never the flag: vibeview.json keeps (or, on the first run, seeds) the
2805
+ // shared default; the flag's port is saved locally below.
2806
+ metroPortFlag: void 0,
2615
2807
  isFirstRun,
2616
2808
  firstRunMetro,
2617
2809
  firstRunBuild
2618
2810
  });
2811
+ cfg.defaultPlatform = committedDefault;
2812
+ if (metroPortFlag !== void 0 && PLATFORM_NEEDS_METRO[platform]) {
2813
+ if (metroPortFlag !== resolveMetro(cfg, platform).port) localEntry.metroPort = metroPortFlag;
2814
+ else delete localEntry.metroPort;
2815
+ }
2619
2816
  const commitConfig = () => {
2620
- saveDevConfig(process.cwd(), cfg);
2817
+ updateDevConfig(process.cwd(), platform, (fresh) => {
2818
+ applyConfigChanges(fresh, existing, cfg);
2819
+ });
2621
2820
  if (existing) {
2622
2821
  const changes = describeConfigChanges(existing, cfg);
2623
2822
  if (changes.length) {
2624
2823
  console.error(chalk4.dim(`Updated vibeview.json: ${changes.join(", ")}`));
2625
2824
  }
2626
2825
  }
2826
+ const localChanges = describeLocalChanges(local, nextLocal);
2827
+ if (localChanges.length) {
2828
+ saveLocalDevConfig(process.cwd(), nextLocal);
2829
+ console.error(
2830
+ chalk4.dim(
2831
+ `Saved to .vibeview/local.json (yours only, not committed): ${localChanges.join(", ")}`
2832
+ )
2833
+ );
2834
+ }
2627
2835
  if (isFirstRun) {
2628
2836
  console.error(chalk4.dim(`Created vibeview.json (platform: ${platform}, app: ${appId}).`));
2629
2837
  const hint = fillLaterHint(
@@ -2633,7 +2841,7 @@ ${hint}`);
2633
2841
  if (hint) console.error(chalk4.dim(hint));
2634
2842
  }
2635
2843
  };
2636
- const finalMetro = isFirstRun ? resolveMetro(cfg, platform, opts.metroPort ? parseInt(opts.metroPort, 10) : void 0) : metroResolved;
2844
+ const finalMetro = isFirstRun ? resolveMetro(effectiveDevConfig(cfg, nextLocal), platform, metroPortFlag) : metroResolved;
2637
2845
  return {
2638
2846
  platform,
2639
2847
  appId,
@@ -2657,6 +2865,13 @@ async function runDevInProcess(opts) {
2657
2865
  emit(opts.json, { event: "error", code: "invalid_flag", message });
2658
2866
  process.exit(2);
2659
2867
  }
2868
+ const standalone = opts.standalone === true;
2869
+ if (standalone && opts.platform === "roku") {
2870
+ const message = "--standalone is not for roku \u2014 a Roku dev session has no Metro; it re-packages the channel";
2871
+ console.error(chalk4.red(`error: ${message}`));
2872
+ emit(opts.json, { event: "error", code: "invalid_flag", message });
2873
+ process.exit(2);
2874
+ }
2660
2875
  let pinnedBuildId;
2661
2876
  if (typeof opts.build === "string") {
2662
2877
  try {
@@ -2685,7 +2900,7 @@ async function runDevInProcess(opts) {
2685
2900
  const cfg = await ensureConfig(api, opts);
2686
2901
  if (pinnedBuildId !== void 0) {
2687
2902
  try {
2688
- checkDevBuild(await api.getApp(cfg.appId), pinnedBuildId, cfg.platform);
2903
+ checkDevBuild(await api.getApp(cfg.appId), pinnedBuildId, cfg.platform, { standalone });
2689
2904
  } catch (err) {
2690
2905
  const message = err.message;
2691
2906
  console.error(chalk4.red(`error: ${message}`));
@@ -2721,9 +2936,20 @@ async function runDevInProcess(opts) {
2721
2936
  (l) => console.error(chalk4.dim(l))
2722
2937
  );
2723
2938
  cfg.appId = result.public_id;
2724
- if (reloaded && reloaded.platforms[cfg.platform] && reloaded.platforms[cfg.platform].appId !== result.public_id) {
2725
- reloaded.platforms[cfg.platform].appId = result.public_id;
2726
- saveDevConfig(process.cwd(), reloaded);
2939
+ const localNow = loadLocalDevConfig(process.cwd());
2940
+ const localApp = localNow.platforms?.[cfg.platform];
2941
+ if (localApp?.appId !== void 0) {
2942
+ const committedApp = loadDevConfig(process.cwd())?.platforms?.[cfg.platform]?.appId;
2943
+ if (result.public_id === committedApp) delete localApp.appId;
2944
+ else localApp.appId = result.public_id;
2945
+ saveLocalDevConfig(process.cwd(), localNow);
2946
+ } else {
2947
+ updateDevConfig(process.cwd(), cfg.platform, (fresh) => {
2948
+ const entry = fresh.platforms[cfg.platform];
2949
+ if (!entry || entry.appId === result.public_id) return false;
2950
+ entry.appId = result.public_id;
2951
+ return true;
2952
+ });
2727
2953
  }
2728
2954
  } catch (err) {
2729
2955
  const message = err.message;
@@ -2735,7 +2961,14 @@ async function runDevInProcess(opts) {
2735
2961
  let metroOwnedThisRun = false;
2736
2962
  let rokuBuild;
2737
2963
  let watchIgnore = [];
2738
- if (PLATFORM_NEEDS_METRO[cfg.platform]) {
2964
+ if (standalone && cfg.platform === "roku") {
2965
+ const message = "--standalone is not for roku \u2014 a Roku dev session has no Metro";
2966
+ console.error(chalk4.red(`error: ${message}`));
2967
+ emit(opts.json, { event: "error", code: "invalid_flag", message });
2968
+ process.exit(2);
2969
+ }
2970
+ if (standalone) {
2971
+ } else if (PLATFORM_NEEDS_METRO[cfg.platform]) {
2739
2972
  const metroPort = cfg.metroPort ?? 8081;
2740
2973
  let metroResult;
2741
2974
  try {
@@ -2743,7 +2976,7 @@ async function runDevInProcess(opts) {
2743
2976
  const captureMetroLine = (line) => {
2744
2977
  try {
2745
2978
  if (!metroLogStream) {
2746
- mkdirSync3(dirname4(metroLogPath(process.cwd())), { recursive: true });
2979
+ mkdirSync4(dirname4(metroLogPath(process.cwd())), { recursive: true });
2747
2980
  metroLogStream = createWriteStream(metroLogPath(process.cwd()), { flags: "w" });
2748
2981
  }
2749
2982
  metroLogStream.write(line.replace(/^\[metro\] /, "") + "\n");
@@ -2787,7 +3020,7 @@ async function runDevInProcess(opts) {
2787
3020
  }
2788
3021
  console.error(
2789
3022
  chalk4.dim(
2790
- `Starting ${platformWithArticle(cfg.platform)} dev session for ${cfg.appId}${pinnedBuildId !== void 0 ? ` (build ${pinnedBuildId})` : ""}...`
3023
+ `Starting ${platformWithArticle(cfg.platform)} ${standalone ? "standalone session (no Metro)" : "dev session"} for ${cfg.appId}${pinnedBuildId !== void 0 ? ` (build ${pinnedBuildId})` : ""}...`
2791
3024
  )
2792
3025
  );
2793
3026
  let stopping = false;
@@ -2820,6 +3053,7 @@ async function runDevInProcess(opts) {
2820
3053
  deviceCategory: cfg.deviceCategory,
2821
3054
  model: cfg.model,
2822
3055
  buildId: pinnedBuildId,
3056
+ ...standalone ? { standalone } : {},
2823
3057
  metroPort: cfg.metroPort ?? 8081,
2824
3058
  // startDevSession's own browser-open happens before we've written the
2825
3059
  // state file / emitted session_ready — open it ourselves afterward
@@ -3725,7 +3959,7 @@ function agentExitCode(env) {
3725
3959
  }
3726
3960
 
3727
3961
  // src/commands/agent.ts
3728
- import { writeFileSync as writeFileSync5 } from "fs";
3962
+ import { writeFileSync as writeFileSync6 } from "fs";
3729
3963
  var REF_RE = /^@?(e\d+)$/;
3730
3964
  var COORDS_RE = /^(-?\d+),\s*(-?\d+)$/;
3731
3965
  var FlagError = class extends Error {
@@ -3935,7 +4169,7 @@ async function runAgentVerb(cmd, positionals, opts) {
3935
4169
  `);
3936
4170
  process.exit(1);
3937
4171
  }
3938
- writeFileSync5(outPath, imageBytes);
4172
+ writeFileSync6(outPath, imageBytes);
3939
4173
  if (opts.json) {
3940
4174
  console.log(JSON.stringify(envelope));
3941
4175
  } else {
@@ -3958,8 +4192,10 @@ export {
3958
4192
  DEV_PLATFORMS,
3959
4193
  PLATFORM_NEEDS_METRO,
3960
4194
  isDevPlatform,
4195
+ platformLabel,
3961
4196
  platformWithArticle,
3962
4197
  loadDevConfig,
4198
+ resolveMetro,
3963
4199
  parseBuildId,
3964
4200
  checkDevBuild,
3965
4201
  buildRows,
@@ -3970,6 +4206,8 @@ export {
3970
4206
  readMetroState,
3971
4207
  killOwnedMetro,
3972
4208
  checkMetro,
4209
+ loadLocalDevConfig,
4210
+ effectiveDevConfig,
3973
4211
  buildCommand,
3974
4212
  formatDuration,
3975
4213
  BUILD_TERMINAL_STATUSES,