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.
@@ -74,6 +74,25 @@ var AuthError = class extends Error {
74
74
  this.name = "AuthError";
75
75
  }
76
76
  };
77
+ var TokenScopeError = class extends AuthError {
78
+ constructor(message) {
79
+ super(message);
80
+ this.name = "TokenScopeError";
81
+ }
82
+ };
83
+ var SCOPE_REFUSAL_PREFIX = "This API token has the 'ci' scope";
84
+ function credentialError(status, text) {
85
+ const detail = extractDetail(text);
86
+ if (status === 401) {
87
+ return detail && /expired/i.test(detail) ? new AuthError(detail) : new AuthError();
88
+ }
89
+ if (status === 403 && detail?.startsWith(SCOPE_REFUSAL_PREFIX)) {
90
+ return new TokenScopeError(
91
+ `${detail} Set VIBEVIEW_API_TOKEN (or --token) to a full-access token for this command.`
92
+ );
93
+ }
94
+ return void 0;
95
+ }
77
96
  function errorExitCode(err, otherwise = 1) {
78
97
  return err instanceof AuthError ? 2 : otherwise;
79
98
  }
@@ -168,7 +187,8 @@ var ApiClient = class {
168
187
  });
169
188
  if (!resp.ok) {
170
189
  const text = await resp.text().catch(() => "");
171
- if (resp.status === 401) throw new AuthError();
190
+ const credErr = credentialError(resp.status, text);
191
+ if (credErr) throw credErr;
172
192
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
173
193
  throw new ApiStatusError(
174
194
  resp.status,
@@ -197,7 +217,9 @@ var ApiClient = class {
197
217
  device_category: options?.device ? void 0 : options?.deviceCategory,
198
218
  device_id: options?.device,
199
219
  build_id: options?.build,
200
- metadata: options?.metadata
220
+ metadata: options?.metadata,
221
+ platform: options?.platform,
222
+ app_id: options?.app
201
223
  });
202
224
  }
203
225
  async runTest(testId, options) {
@@ -210,7 +232,9 @@ var ApiClient = class {
210
232
  device_category: options?.device ? void 0 : options?.deviceCategory,
211
233
  device_id: options?.device,
212
234
  build_id: options?.build,
213
- metadata: options?.metadata
235
+ metadata: options?.metadata,
236
+ platform: options?.platform,
237
+ app_id: options?.app
214
238
  });
215
239
  }
216
240
  /** Cancel a still-queued CI run by its queue entry id. Idempotent best-effort. */
@@ -244,7 +268,8 @@ var ApiClient = class {
244
268
  });
245
269
  if (!resp.ok) {
246
270
  const text = await resp.text().catch(() => "");
247
- if (resp.status === 401) throw new AuthError();
271
+ const credErr = credentialError(resp.status, text);
272
+ if (credErr) throw credErr;
248
273
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
249
274
  throw new ApiStatusError(
250
275
  resp.status,
@@ -267,7 +292,8 @@ var ApiClient = class {
267
292
  });
268
293
  if (!resp.ok) {
269
294
  const text = await resp.text().catch(() => "");
270
- if (resp.status === 401) throw new AuthError();
295
+ const credErr = credentialError(resp.status, text);
296
+ if (credErr) throw credErr;
271
297
  if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
272
298
  throw new ApiStatusError(
273
299
  resp.status,
@@ -429,7 +455,8 @@ var ApiClient = class {
429
455
  const resp = await this.fetchOrExplain(url, { method: "GET", headers });
430
456
  if (!resp.ok) {
431
457
  const text = await resp.text().catch(() => "");
432
- if (resp.status === 401) throw new AuthError();
458
+ const credErr = credentialError(resp.status, text);
459
+ if (credErr) throw credErr;
433
460
  if (resp.status === 404)
434
461
  throw new NotFoundError(`Not found: ${extractDetail(text) ?? `/builds/${id}/artifact`}`);
435
462
  throw new ApiStatusError(
package/dist/cli.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  listBuilds,
16
16
  loadDevConfig,
17
17
  newTransitionState,
18
+ platformLabel,
18
19
  platformWithArticle,
19
20
  pressButtonsHelp,
20
21
  printOutcomeLine,
@@ -22,7 +23,7 @@ import {
22
23
  readDevState,
23
24
  readMetroState,
24
25
  runAgentVerb
25
- } from "./chunk-3DKY443B.js";
26
+ } from "./chunk-NOZGZT3G.js";
26
27
  import {
27
28
  ApiClient,
28
29
  ApiStatusError,
@@ -43,7 +44,7 @@ import {
43
44
  resolveAuth,
44
45
  saveConfig,
45
46
  uploadApp
46
- } from "./chunk-PBMB6TG4.js";
47
+ } from "./chunk-ZZK7YQSP.js";
47
48
 
48
49
  // src/cli.ts
49
50
  import { createRequire } from "module";
@@ -570,6 +571,29 @@ function mapExitCode(status) {
570
571
  return EXIT_INFRA_ERROR;
571
572
  }
572
573
 
574
+ // src/lib/run-target.ts
575
+ var RUN_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
576
+ function parseRunPlatform(value) {
577
+ if (value === void 0) return void 0;
578
+ if (!RUN_PLATFORMS.includes(value)) {
579
+ throw new Error(`--platform must be one of ${RUN_PLATFORMS.join(", ")}, got "${value}"`);
580
+ }
581
+ return value;
582
+ }
583
+ function describeRunTarget(response, options) {
584
+ const asked = Boolean(options.platform || options.app);
585
+ if (!response.app_id) {
586
+ if (asked) {
587
+ throw new Error(
588
+ "this VibeView server does not support --platform/--app on test runs yet; the run was stopped rather than run on the test\u2019s own platform"
589
+ );
590
+ }
591
+ return null;
592
+ }
593
+ if (!asked) return null;
594
+ return `${response.app_id} (${platformLabel(response.platform ?? "")})`;
595
+ }
596
+
573
597
  // src/commands/run-suite.ts
574
598
  async function runSuite(suiteId, options) {
575
599
  const { token, baseUrl } = resolveAuth(options);
@@ -635,14 +659,18 @@ ${signal} received.
635
659
  deviceCategory: options.deviceCategory,
636
660
  device: options.device,
637
661
  build: options.build,
638
- metadata: options.metadata
662
+ metadata: options.metadata,
663
+ platform: options.platform,
664
+ app: options.app
639
665
  });
640
666
  sessionId = response.session_id;
641
667
  runId = response.run_id;
668
+ const target = describeRunTarget(response, options);
642
669
  spinner.stop();
643
670
  process.stderr.write(
644
671
  `${chalk3.dim("Suite:")} ${suiteId}
645
- ${chalk3.dim("Run:")} ${response.run_id}
672
+ ` + (target ? `${chalk3.dim("App:")} ${target}
673
+ ` : "") + `${chalk3.dim("Run:")} ${response.run_id}
646
674
  ` + (sessionId ? `${chalk3.dim("Session:")} ${sessionId}
647
675
  ` : "")
648
676
  );
@@ -1009,16 +1037,20 @@ ${signal} received.
1009
1037
  deviceCategory: options.deviceCategory,
1010
1038
  device: options.device,
1011
1039
  build: options.build,
1012
- metadata: options.metadata
1040
+ metadata: options.metadata,
1041
+ platform: options.platform,
1042
+ app: options.app
1013
1043
  });
1014
1044
  sessionId = response.session_id;
1015
1045
  runId = response.run_id;
1046
+ const target = describeRunTarget(response, options);
1016
1047
  spinner.stop();
1017
1048
  process.stderr.write(
1018
1049
  `${chalk5.dim("Session:")} ${sessionId || "unknown"}
1019
1050
  ${chalk5.dim("Device:")} ${response.device_name || "allocating..."}
1020
1051
  ${chalk5.dim("Test:")} ${testId}
1021
- ${chalk5.dim("Run:")} ${response.run_id}
1052
+ ` + (target ? `${chalk5.dim("App:")} ${target}
1053
+ ` : "") + `${chalk5.dim("Run:")} ${response.run_id}
1022
1054
  `
1023
1055
  );
1024
1056
  spinner.start("Waiting for device...");
@@ -1785,7 +1817,8 @@ function defaultDeps2(client) {
1785
1817
  getSubmission: client.getSubmission.bind(client),
1786
1818
  getSubmissionLog: client.getSubmissionLog.bind(client),
1787
1819
  listCloudBuilds: client.listCloudBuilds.bind(client),
1788
- checkStorePreflight: client.checkStorePreflight.bind(client)
1820
+ checkStorePreflight: client.checkStorePreflight.bind(client),
1821
+ getApp: client.getApp.bind(client)
1789
1822
  };
1790
1823
  }
1791
1824
  var POLL_INTERVAL_MS2 = 2e3;
@@ -1910,7 +1943,7 @@ async function submitCommand(opts, deps) {
1910
1943
  }
1911
1944
  if (rollout !== void 0 && releaseStatus !== "inProgress") {
1912
1945
  throw new UsageError(
1913
- `a rollout fraction requires release status "inProgress" (a staged rollout) \u2014 got "${releaseStatus ?? "completed"}"`
1946
+ `a rollout fraction requires release status "inProgress" (a staged rollout) \u2014 got "${releaseStatus ?? (track === "production" ? "draft" : "completed")}"`
1914
1947
  );
1915
1948
  }
1916
1949
  if (releaseStatus === "inProgress" && rollout === void 0) {
@@ -1964,12 +1997,19 @@ async function submitCommand(opts, deps) {
1964
1997
  }
1965
1998
  }
1966
1999
  async function checkStore(platform, androidFamily, ascKey2, opts, deps) {
1967
- const identifier = androidFamily ? opts.package : opts.bundleId;
2000
+ const flagged = androidFamily ? opts.package : opts.bundleId;
2001
+ const usage = androidFamily ? `--check needs the package name to look up: --package <name> (e.g. --package io.example.app)` : `--check needs the bundle identifier to look up: --bundle-id <id> (e.g. --bundle-id io.example.app)`;
2002
+ let identifier = flagged;
1968
2003
  if (!identifier) {
1969
- throw new UsageError(
1970
- androidFamily ? `--check needs the package name to look up: --package <name> (e.g. --package io.example.app)` : `--check needs the bundle identifier to look up: --bundle-id <id> (e.g. --bundle-id io.example.app)`
1971
- );
2004
+ try {
2005
+ identifier = await linkedIdentifier(platform, opts, deps);
2006
+ } catch (err) {
2007
+ printError2(opts, err);
2008
+ process.exitCode = errorExitCode(err);
2009
+ return;
2010
+ }
1972
2011
  }
2012
+ if (!identifier) throw new UsageError(usage);
1973
2013
  try {
1974
2014
  const result = await deps.checkStorePreflight({
1975
2015
  platform,
@@ -1994,6 +2034,32 @@ async function checkStore(platform, androidFamily, ascKey2, opts, deps) {
1994
2034
  process.exitCode = errorExitCode(err);
1995
2035
  }
1996
2036
  }
2037
+ async function linkedIdentifier(platform, opts, deps) {
2038
+ if (!deps.getApp) return void 0;
2039
+ let appId;
2040
+ try {
2041
+ appId = loadDevConfig(process.cwd())?.platforms?.[platform]?.appId;
2042
+ } catch {
2043
+ return void 0;
2044
+ }
2045
+ if (!appId) return void 0;
2046
+ let app;
2047
+ try {
2048
+ app = await deps.getApp(appId);
2049
+ } catch (err) {
2050
+ if (err instanceof NotFoundError) {
2051
+ throw new Error(
2052
+ `the app linked in vibeview.json for ${platform} (${appId}) no longer exists \u2014 ` + (platform === "android" || platform === "androidtv" ? "pass --package <name>" : "pass --bundle-id <id>")
2053
+ );
2054
+ }
2055
+ throw err;
2056
+ }
2057
+ if (!app.package_name) return void 0;
2058
+ if (!opts.json) {
2059
+ console.error(chalk14.dim(`Checking ${app.package_name} (${app.name}, linked in vibeview.json)`));
2060
+ }
2061
+ return app.package_name;
2062
+ }
1997
2063
  async function resolveLatestBuild(platform, deps) {
1998
2064
  const { builds: builds2 } = await deps.listCloudBuilds({
1999
2065
  platform,
@@ -3534,11 +3600,24 @@ program.command("run-suite <suite-id>").description("Run all test cases in a sui
3534
3600
  ).option(
3535
3601
  "--device <device_id>",
3536
3602
  "Pin run to a specific device (use `vibeview list-devices` to find one). Conflicts with --device-category."
3603
+ ).option(
3604
+ "--platform <platform>",
3605
+ "Run the suite on another platform (ios, android, tvos, androidtv): runs your app there with the same bundle id / package"
3606
+ ).option(
3607
+ "--app <app-id>",
3608
+ "Run the suite against this app instead of its own (e.g. another platform build with a different bundle id)"
3537
3609
  ).option("--build <id>", "Specific AppBuild id to run against (defaults to latest)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").action((suiteId, opts) => {
3538
3610
  if (opts.device && opts.deviceCategory) {
3539
3611
  process.stderr.write("error: --device cannot be combined with --device-category\n");
3540
3612
  process.exit(2);
3541
3613
  }
3614
+ try {
3615
+ parseRunPlatform(opts.platform);
3616
+ } catch (err) {
3617
+ process.stderr.write(`error: ${err.message}
3618
+ `);
3619
+ process.exit(2);
3620
+ }
3542
3621
  const metadata = resolveMetadata(opts);
3543
3622
  return runSuite(suiteId, {
3544
3623
  ...opts,
@@ -3567,6 +3646,13 @@ program.command("run-suites").description("Run all suites matching a tag filter"
3567
3646
  process.stderr.write("error: --device cannot be combined with --device-category\n");
3568
3647
  process.exit(2);
3569
3648
  }
3649
+ try {
3650
+ parseRunPlatform(opts.platform);
3651
+ } catch (err) {
3652
+ process.stderr.write(`error: ${err.message}
3653
+ `);
3654
+ process.exit(2);
3655
+ }
3570
3656
  const metadata = resolveMetadata(opts);
3571
3657
  return runSuites({
3572
3658
  ...opts,
@@ -3582,11 +3668,24 @@ program.command("run-test <test-id>").description("Run a single test case").opti
3582
3668
  ).option(
3583
3669
  "--device <device_id>",
3584
3670
  "Pin run to a specific device (use `vibeview list-devices` to find one). Conflicts with --device-category."
3671
+ ).option(
3672
+ "--platform <platform>",
3673
+ "Run the test on another platform (ios, android, tvos, androidtv): runs your app there with the same bundle id / package"
3674
+ ).option(
3675
+ "--app <app-id>",
3676
+ "Run the test against this app instead of its own (e.g. another platform build with a different bundle id)"
3585
3677
  ).option("--build <id>", "Specific AppBuild id to run against (defaults to latest)").option("--metadata <json>", "JSON object passed through to webhooks (e.g. PR context)").option("--metadata-file <path>", "Read JSON object from a file (use for nested JSON in CI)").action((testId, opts) => {
3586
3678
  if (opts.device && opts.deviceCategory) {
3587
3679
  process.stderr.write("error: --device cannot be combined with --device-category\n");
3588
3680
  process.exit(2);
3589
3681
  }
3682
+ try {
3683
+ parseRunPlatform(opts.platform);
3684
+ } catch (err) {
3685
+ process.stderr.write(`error: ${err.message}
3686
+ `);
3687
+ process.exit(2);
3688
+ }
3590
3689
  const metadata = resolveMetadata(opts);
3591
3690
  return runTest(testId, {
3592
3691
  ...opts,
@@ -3635,7 +3734,10 @@ program.command("dev").description(
3635
3734
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--web-url <url>", "Dashboard URL if different from the API URL (local dev)").option(
3636
3735
  "--platform <platform>",
3637
3736
  "ios, android, tvos, androidtv, or roku (first run is interactive otherwise)"
3638
- ).option("--app <app_id>", "Uploaded debug build to run (see `vibeview list-apps`)").option("--metro-port <port>", "Local Metro port (default: 8081, remembered in vibeview.json)").option(
3737
+ ).option("--app <app_id>", "Uploaded debug build to run (see `vibeview list-apps`)").option(
3738
+ "--metro-port <port>",
3739
+ "Local Metro port (default: 8081, or vibeview.json); remembered for you in .vibeview/local.json"
3740
+ ).option(
3639
3741
  "--device-category <category>",
3640
3742
  "phone or tablet (default: phone; TV platforms always use tv)",
3641
3743
  parseDeviceCategory
@@ -3648,11 +3750,15 @@ program.command("dev").description(
3648
3750
  false
3649
3751
  ).option(
3650
3752
  "--build [id]",
3651
- "Bare: build and upload first (uses platforms.<platform>.build from vibeview.json). With an id: run that uploaded debug build instead of the newest one (see `vibeview list-builds <app>`)",
3753
+ "Bare: build and upload first (uses platforms.<platform>.build from vibeview.json). With an id: run that uploaded debug build instead of the newest one (any build with --standalone) (see `vibeview list-builds <app>`)",
3652
3754
  false
3653
3755
  ).option(
3654
3756
  "--watch [seconds]",
3655
3757
  "Roku only: auto-reload after file changes settle (default: 2 seconds of quiet)"
3758
+ ).option(
3759
+ "--standalone",
3760
+ "Run the build as it is, with no Metro and no tunnel (a release, CI or cloud build); without --build <id>, the newest release build. Code changes do not hot-reload",
3761
+ false
3656
3762
  ).action((opts) => dev(opts));
3657
3763
  program.command("build").description("Run your configured build command and upload the result (see vibeview.json)").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option(
3658
3764
  "--platform <platform>",
@@ -3687,13 +3793,13 @@ program.command("submit").description(
3687
3793
  "ios/tvos only: which stored App Store Connect key to use (default when exactly one \u2014 see `vibeview credentials asc-key add`)"
3688
3794
  ).option(
3689
3795
  "--release-status <status>",
3690
- "android/androidtv only: Play release status \u2014 completed (default), draft, or inProgress"
3796
+ "android/androidtv only: Play release status \u2014 completed, draft, or inProgress (default: draft on the production track, completed on the others)"
3691
3797
  ).option(
3692
3798
  "--rollout <fraction>",
3693
3799
  "android/androidtv only: staged-rollout fraction, 0 < f < 1 (requires --release-status inProgress)"
3694
3800
  ).option(
3695
3801
  "--check",
3696
- "Only check store readiness: does the store know this app with your stored key? Takes --platform plus --bundle-id (ios/tvos) or --package (android/androidtv); no build needed. Exit 0 when ready",
3802
+ "Only check store readiness: does the store know this app with your stored key? Takes --platform plus --bundle-id (ios/tvos) or --package (android/androidtv), defaulting to the app linked in vibeview.json; no build needed. Exit 0 when ready",
3697
3803
  false
3698
3804
  ).option("--bundle-id <id>", "ios/tvos, with --check: the bundle identifier to look up").option("--no-wait", "Print the submission id and exit without waiting for completion").option("--json", "Emit line-delimited JSON events on stdout instead of human output", false).action((opts) => submitCommand(opts));
3699
3805
  var builds = program.command("builds").description("Manage cloud builds (see `vibeview build --cloud`)");
@@ -3793,7 +3899,7 @@ program.command("dev-reload").description(
3793
3899
  program.command("mcp").description(
3794
3900
  "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
3795
3901
  ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
3796
- const { runMcpServer } = await import("./server-BGQJOVSN.js");
3902
+ const { runMcpServer } = await import("./server-2JNYWF5L.js");
3797
3903
  await runMcpServer({ ...opts, version: pkg.version });
3798
3904
  });
3799
3905
  program.command("agent-setup").description(
@@ -11,22 +11,25 @@ import {
11
11
  checkMetro,
12
12
  createRokuPush,
13
13
  decodeScreenshotResult,
14
+ effectiveDevConfig,
14
15
  filterDeviceModels,
15
16
  formatBuildRowsHuman,
16
17
  formatDeviceModelsHuman,
17
18
  isDevPlatform,
18
19
  loadDevConfig,
20
+ loadLocalDevConfig,
19
21
  parseBuildId,
20
22
  platformWithArticle,
21
23
  readDevState,
22
24
  renderEnvelope,
25
+ resolveMetro,
23
26
  startDevSession
24
- } from "./chunk-3DKY443B.js";
27
+ } from "./chunk-NOZGZT3G.js";
25
28
  import {
26
29
  ApiClient,
27
30
  NotFoundError,
28
31
  resolveAuth
29
- } from "./chunk-PBMB6TG4.js";
32
+ } from "./chunk-ZZK7YQSP.js";
30
33
 
31
34
  // src/mcp/server.ts
32
35
  import { writeFileSync } from "fs";
@@ -539,7 +542,7 @@ async function handleUploadApp(args, deps) {
539
542
  }
540
543
  const name = typeof args.name === "string" && args.name ? args.name : void 0;
541
544
  const upload = deps.uploadApp ?? (async (f, n) => {
542
- const { uploadAppBuild } = await import("./upload-app-FEZG23NU.js");
545
+ const { uploadAppBuild } = await import("./upload-app-2LVG4LMB.js");
543
546
  const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
544
547
  return result2;
545
548
  });
@@ -624,6 +627,7 @@ function createDevSessionController(auth, webUrl) {
624
627
  } catch {
625
628
  devConfig = null;
626
629
  }
630
+ devConfig = effectiveDevConfig(devConfig, loadLocalDevConfig(process.cwd()));
627
631
  const entry = devConfig?.platforms?.[cfg.platform];
628
632
  const appId = cfg.app || entry?.appId;
629
633
  if (!appId) {
@@ -649,7 +653,7 @@ function createDevSessionController(auth, webUrl) {
649
653
  }
650
654
  const standalone = cfg.standalone === true;
651
655
  const usesMetro = PLATFORM_NEEDS_METRO[cfg.platform] && !standalone;
652
- const metroPort = cfg.metroPort ?? devConfig?.metroPort ?? 8081;
656
+ const metroPort = resolveMetro(devConfig, cfg.platform, cfg.metroPort).port;
653
657
  if (usesMetro) {
654
658
  const metro = await checkMetro(metroPort);
655
659
  if (metro === "down") {
@@ -3,7 +3,7 @@ import {
3
3
  describeBuild,
4
4
  uploadApp,
5
5
  uploadAppBuild
6
- } from "./chunk-PBMB6TG4.js";
6
+ } from "./chunk-ZZK7YQSP.js";
7
7
  export {
8
8
  describeBuild,
9
9
  uploadApp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.28.1",
3
+ "version": "0.30.0",
4
4
  "description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku (beta) devices: a React Native dev loop, device control for you or your coding agent (CLI or MCP), and tests from CI",
5
5
  "mcpName": "io.github.vibeview/vibeview",
6
6
  "type": "module",
@@ -64,11 +64,15 @@ https://vibeview.io/docs/preparing-your-build.
64
64
  the page is `https://vibeview.io/sandbox/<session_id>`.)
65
65
 
66
66
  Running `dev` writes two things into the project root: a `.vibeview/`
67
- directory (live session state — local machine state, never commit) and a
68
- `vibeview.json` (your platform/app/build choices, which teammates DO want).
69
- Make sure `.vibeview/` is in the project's `.gitignore`, adding it if
70
- missing. Leave `vibeview.json` tracked, but tell the developer you created
71
- it rather than letting it appear unexplained in their `git status`.
67
+ directory (live session state, plus `local.json` with this developer's own
68
+ flag overrides such as `--metro-port` — local machine state, never commit)
69
+ and a `vibeview.json` (the platform/app/build setup, which teammates DO
70
+ want). Flags never rewrite a value `vibeview.json` already has; they are
71
+ saved to `.vibeview/local.json` instead, and the CLI prints what it saved.
72
+ Make sure the project's `.gitignore` has `.vibeview/*` and
73
+ `!.vibeview/test-spec.json`, adding them if missing. Leave `vibeview.json`
74
+ tracked, but tell the developer you created it rather than letting it
75
+ appear unexplained in their `git status`.
72
76
 
73
77
  ## 3. Verifying
74
78