vibeview 0.24.0 → 0.25.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.
@@ -6,7 +6,99 @@ import {
6
6
  loadConfig,
7
7
  resolveAuth,
8
8
  uploadAppBuild
9
- } from "./chunk-2WNJYNDW.js";
9
+ } from "./chunk-RFFCCPGZ.js";
10
+
11
+ // src/lib/dev-build.ts
12
+ function parseBuildId(raw) {
13
+ const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw.trim() : "";
14
+ if (!/^\d+$/.test(text) || Number(text) < 1) {
15
+ throw new Error(
16
+ `--build expects a build id (a positive number, see \`vibeview list-builds <app>\`), got "${String(raw)}"`
17
+ );
18
+ }
19
+ return Number(text);
20
+ }
21
+ function buildKind(isDebug) {
22
+ if (isDebug === true) return "debug";
23
+ if (isDebug === false) return "release";
24
+ return "unknown";
25
+ }
26
+ function checkDevBuild(app, buildId, platform) {
27
+ if (platform === "roku") {
28
+ throw new Error(
29
+ "a build id cannot be pinned on roku \u2014 `vibeview dev` packages and sideloads the channel itself"
30
+ );
31
+ }
32
+ if (app.platform !== platform) {
33
+ throw new Error(`app ${app.public_id} is a ${app.platform} app, not ${platform}`);
34
+ }
35
+ const build = app.builds.find((b) => b.id === buildId);
36
+ if (!build) {
37
+ throw new Error(
38
+ `build ${buildId} is not a build of app ${app.public_id} \u2014 see \`vibeview list-builds ${app.public_id}\``
39
+ );
40
+ }
41
+ if (build.is_debug !== true) {
42
+ 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";
43
+ throw new Error(
44
+ `build ${buildId} is not a debug build \u2014 ${why}. \`vibeview dev\` needs a ${platform} debug simulator/emulator build without an embedded JS bundle (see \`vibeview list-builds ${app.public_id}\`)`
45
+ );
46
+ }
47
+ return build;
48
+ }
49
+
50
+ // src/commands/list-builds.ts
51
+ import chalk from "chalk";
52
+ function buildRows(app) {
53
+ return app.builds.map((b) => ({
54
+ id: b.id,
55
+ version: b.version ?? null,
56
+ build_number: b.build_number ?? null,
57
+ platform: app.platform,
58
+ kind: buildKind(b.is_debug),
59
+ is_debug: b.is_debug ?? null,
60
+ uploaded_at: b.uploaded_at ?? null,
61
+ note: b.note ?? null
62
+ }));
63
+ }
64
+ var HEADERS = ["ID", "VERSION", "PLATFORM", "KIND", "UPLOADED", "NOTE"];
65
+ function formatBuildRowsHuman(app, rows) {
66
+ if (rows.length === 0) {
67
+ return `No builds uploaded for ${app.name} (${app.public_id}) \u2014 upload one with \`vibeview upload-app\`.
68
+ `;
69
+ }
70
+ const cells = rows.map((r) => [
71
+ String(r.id),
72
+ r.version ? `${r.version}${r.build_number ? ` (${r.build_number})` : ""}` : r.build_number ?? "",
73
+ r.platform,
74
+ r.kind,
75
+ r.uploaded_at ?? "",
76
+ (r.note ?? "").split("\n")[0]
77
+ ]);
78
+ const widths = HEADERS.map((h, i) => Math.max(h.length, ...cells.map((c) => c[i].length)));
79
+ const pad = (row) => row.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
80
+ const lines = [chalk.bold(pad(HEADERS)), ...cells.map(pad)];
81
+ return lines.join("\n") + "\n";
82
+ }
83
+ async function listBuilds(appId, opts, deps) {
84
+ const { token, baseUrl } = resolveAuth(opts);
85
+ const d = deps ?? { getApp: (id) => new ApiClient(baseUrl, token).getApp(id) };
86
+ try {
87
+ const app = await d.getApp(appId);
88
+ const rows = buildRows(app);
89
+ if (opts.json) {
90
+ console.log(JSON.stringify(rows, null, 2));
91
+ } else {
92
+ process.stdout.write(formatBuildRowsHuman(app, rows));
93
+ }
94
+ process.exitCode = 0;
95
+ } catch (err) {
96
+ const message = err instanceof Error ? err.message : String(err);
97
+ if (opts.json) console.log(JSON.stringify({ event: "error", message }));
98
+ console.error(chalk.red(`error: ${message}`));
99
+ process.exitCode = 1;
100
+ }
101
+ }
10
102
 
11
103
  // src/lib/dev-state.ts
12
104
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
@@ -267,7 +359,7 @@ async function checkMetro(port, timeoutMs = 1500) {
267
359
  import { spawn as spawn2 } from "child_process";
268
360
  import { createInterface } from "readline";
269
361
  import { select as select2 } from "@inquirer/prompts";
270
- import chalk2 from "chalk";
362
+ import chalk3 from "chalk";
271
363
 
272
364
  // src/lib/artifact.ts
273
365
  import { existsSync as existsSync2, mkdtempSync, readdirSync, rmSync as rmSync2, statSync } from "fs";
@@ -327,7 +419,7 @@ function prepareUploadArtifact(path) {
327
419
  }
328
420
 
329
421
  // src/lib/dev-setup.ts
330
- import chalk from "chalk";
422
+ import chalk2 from "chalk";
331
423
  import { input, select } from "@inquirer/prompts";
332
424
 
333
425
  // src/lib/project-detect.ts
@@ -556,7 +648,7 @@ async function promptBuildConfig(cwd, platform) {
556
648
  return { command: defaults.buildCommand, artifact: defaults.artifact };
557
649
  }
558
650
  console.log(
559
- chalk.dim(
651
+ chalk2.dim(
560
652
  " Tip: include your install step \u2014 dependencies are not uploaded.\n e.g. `yarn install --immutable && yarn build:android`"
561
653
  )
562
654
  );
@@ -582,7 +674,7 @@ async function promptProductionBuildConfig(cwd, platform) {
582
674
  };
583
675
  }
584
676
  console.log(
585
- chalk.dim(
677
+ chalk2.dim(
586
678
  " Tip: include your install step \u2014 dependencies are not uploaded.\n e.g. `yarn install --immutable && cd android && ./gradlew bundleRelease`"
587
679
  )
588
680
  );
@@ -831,7 +923,7 @@ function runShell(command, cwd, log) {
831
923
  }
832
924
  async function resolveBarePlatform(cwd, cfg, opts) {
833
925
  if (opts.platform && !isDevPlatform(opts.platform)) {
834
- console.error(chalk2.red(`error: --platform must be one of ${DEV_PLATFORMS.join(", ")}`));
926
+ console.error(chalk3.red(`error: --platform must be one of ${DEV_PLATFORMS.join(", ")}`));
835
927
  process.exit(2);
836
928
  }
837
929
  let platform = opts.platform || cfg?.defaultPlatform;
@@ -946,9 +1038,9 @@ async function buildCommand(opts, cloudDeps) {
946
1038
  platform,
947
1039
  build,
948
1040
  opts,
949
- (l) => console.error(chalk2.dim(l))
1041
+ (l) => console.error(chalk3.dim(l))
950
1042
  );
951
- for (const w of result.warnings ?? []) console.error(chalk2.yellow(`\u26A0 ${w}`));
1043
+ for (const w of result.warnings ?? []) console.error(chalk3.yellow(`\u26A0 ${w}`));
952
1044
  persistUploadedAppId(cwd, cfg, platform, build, result.public_id);
953
1045
  if (opts.json) {
954
1046
  console.log(
@@ -956,8 +1048,8 @@ async function buildCommand(opts, cloudDeps) {
956
1048
  );
957
1049
  } else {
958
1050
  console.error(
959
- chalk2.green(`\u2713 built and uploaded ${result.name}`) + `
960
- ${chalk2.cyan(result.public_id)} ${chalk2.dim(`[${result.platform}]`)}`
1051
+ chalk3.green(`\u2713 built and uploaded ${result.name}`) + `
1052
+ ${chalk3.cyan(result.public_id)} ${chalk3.dim(`[${result.platform}]`)}`
961
1053
  );
962
1054
  }
963
1055
  } catch (err) {
@@ -969,7 +1061,7 @@ async function buildCommand(opts, cloudDeps) {
969
1061
  }
970
1062
  function printFailure(opts, message, code) {
971
1063
  if (opts.json) console.log(JSON.stringify({ event: "error", code, message }));
972
- console.error(chalk2.red(`error: ${message}`));
1064
+ console.error(chalk3.red(`error: ${message}`));
973
1065
  }
974
1066
  function fail(opts, message, code) {
975
1067
  printFailure(opts, message, code);
@@ -992,7 +1084,7 @@ async function resolveCloudPlatforms(cwd, cfgRef, opts) {
992
1084
  for (const p of unsupported) {
993
1085
  const reason = productionUnsupportedReason(p);
994
1086
  emitCloud(opts.json, { event: "skipped", platform: p, reason });
995
- if (!opts.json) console.error(chalk2.yellow(`\u26A0 skipping ${p}: ${reason}`));
1087
+ if (!opts.json) console.error(chalk3.yellow(`\u26A0 skipping ${p}: ${reason}`));
996
1088
  }
997
1089
  chosenPlatforms = chosenPlatforms.filter((p) => !unsupported.includes(p));
998
1090
  }
@@ -1033,7 +1125,7 @@ async function resolveCloudPlatforms(cwd, cfgRef, opts) {
1033
1125
  function validateExplicitPlatform(raw) {
1034
1126
  if (!isDevPlatform(raw)) {
1035
1127
  console.error(
1036
- chalk2.red(`error: --platform must be one of ${DEV_PLATFORMS.join(", ")}, or "all"`)
1128
+ chalk3.red(`error: --platform must be one of ${DEV_PLATFORMS.join(", ")}, or "all"`)
1037
1129
  );
1038
1130
  process.exit(2);
1039
1131
  }
@@ -1093,7 +1185,7 @@ async function resolveCloudAppIds(cwd, cfgRef, platforms, opts, listApps) {
1093
1185
  createFromArtifact.add(platform);
1094
1186
  if (!opts.json) {
1095
1187
  console.error(
1096
- chalk2.dim(
1188
+ chalk3.dim(
1097
1189
  opts.production ? `no ${platform} apps yet \u2014 production builds aren't linked to an app; the signed artifact will be available from the build itself` : `no ${platform} apps yet \u2014 this build will create one from the artifact`
1098
1190
  )
1099
1191
  );
@@ -1270,7 +1362,7 @@ ${offenders}`);
1270
1362
  const autoIncrement = build.autoIncrement === true;
1271
1363
  if (autoIncrement && !production && !opts.json) {
1272
1364
  console.error(
1273
- chalk2.yellow(
1365
+ chalk3.yellow(
1274
1366
  `note: platforms.${p}.build.autoIncrement only applies to production builds (--production) \u2014 ignoring it for this simulator build`
1275
1367
  )
1276
1368
  );
@@ -1307,7 +1399,7 @@ ${offenders}`);
1307
1399
  }
1308
1400
  if (!opts.json) {
1309
1401
  for (const b of created) {
1310
- console.error(` ${chalk2.cyan(b.id)} ${chalk2.dim(`[${b.platform}]`)}`);
1402
+ console.error(` ${chalk3.cyan(b.id)} ${chalk3.dim(`[${b.platform}]`)}`);
1311
1403
  }
1312
1404
  }
1313
1405
  const shouldWait = opts.wait !== false || opts.download;
@@ -1332,7 +1424,7 @@ ${offenders}`);
1332
1424
  path: result.path
1333
1425
  });
1334
1426
  if (!opts.json) {
1335
- console.error(chalk2.green(`\u2713 saved ${result.filename}`));
1427
+ console.error(chalk3.green(`\u2713 saved ${result.filename}`));
1336
1428
  }
1337
1429
  } catch (err) {
1338
1430
  downloadFailed = true;
@@ -1341,7 +1433,7 @@ ${offenders}`);
1341
1433
  event: "error",
1342
1434
  message: `error downloading ${b.id}: ${message}`
1343
1435
  });
1344
- if (!opts.json) console.error(chalk2.red(`error downloading ${b.id}: ${message}`));
1436
+ if (!opts.json) console.error(chalk3.red(`error downloading ${b.id}: ${message}`));
1345
1437
  }
1346
1438
  }
1347
1439
  }
@@ -1398,7 +1490,7 @@ async function waitForCloudBuilds(builds, opts, deps) {
1398
1490
  const giveUpLocally = (id, message) => {
1399
1491
  abandoned.add(id);
1400
1492
  emitCloud(opts.json, { event: "error", message });
1401
- if (!opts.json) console.error(chalk2.red(message));
1493
+ if (!opts.json) console.error(chalk3.red(message));
1402
1494
  };
1403
1495
  const pollAndProcess = async (build) => {
1404
1496
  const offset = offsets.get(build.id) ?? 0;
@@ -1559,21 +1651,21 @@ function printTransition(id, platform, status, state, json, printLine) {
1559
1651
  if (json) {
1560
1652
  emitCloud(json, { event: "status", id, status: "queued" });
1561
1653
  } else {
1562
- printLine(id, platform, chalk2.dim("\u23F3 queued \u2014 waiting for a build slot"));
1654
+ printLine(id, platform, chalk3.dim("\u23F3 queued \u2014 waiting for a build slot"));
1563
1655
  }
1564
1656
  } else if (status === "running" && !state.printedRunning) {
1565
1657
  state.printedRunning = true;
1566
1658
  if (json) {
1567
1659
  emitCloud(json, { event: "status", id, status: "running" });
1568
1660
  } else {
1569
- printLine(id, platform, `${chalk2.cyan("\u25B6")} building`);
1661
+ printLine(id, platform, `${chalk3.cyan("\u25B6")} building`);
1570
1662
  }
1571
1663
  }
1572
1664
  }
1573
1665
  function printOutcomeLine(build, multi) {
1574
1666
  const prefix = multi ? `[${build.platform}] ` : "";
1575
1667
  const ok = build.status === "succeeded";
1576
- const mark = ok ? chalk2.green("\u2713") : chalk2.red("\u2717");
1668
+ const mark = ok ? chalk3.green("\u2713") : chalk3.red("\u2717");
1577
1669
  const duration = formatDuration(build.started_at, build.finished_at);
1578
1670
  let line = `${prefix}${mark} ${build.status}`;
1579
1671
  if (duration) line += ` (${duration})`;
@@ -1620,7 +1712,7 @@ import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
1620
1712
  import { dirname as dirname5 } from "path";
1621
1713
  import { createInterface as createInterface3 } from "readline";
1622
1714
  import { select as select3 } from "@inquirer/prompts";
1623
- import chalk3 from "chalk";
1715
+ import chalk4 from "chalk";
1624
1716
  import openBrowser from "open";
1625
1717
 
1626
1718
  // src/lib/tunnel-client.ts
@@ -2178,7 +2270,8 @@ function devSessionCreateBody(cfg) {
2178
2270
  app_id: cfg.appId,
2179
2271
  // Roku has no JS bundle to serve, so there is nothing to tunnel.
2180
2272
  tunnel: PLATFORM_NEEDS_METRO[cfg.platform],
2181
- ...cfg.model ? { model: cfg.model } : {}
2273
+ ...cfg.model ? { model: cfg.model } : {},
2274
+ ...cfg.buildId ? { build_id: cfg.buildId } : {}
2182
2275
  };
2183
2276
  }
2184
2277
  function resolveDeviceModel(pools, platform, model) {
@@ -2312,7 +2405,7 @@ function requireNonInteractive(json) {
2312
2405
  async function ensureConfig(api, opts) {
2313
2406
  const existing = loadDevConfig(process.cwd());
2314
2407
  if (opts.platform && !isDevPlatform(opts.platform)) {
2315
- console.error(chalk3.red(`error: --platform must be one of ${DEV_PLATFORMS.join(", ")}`));
2408
+ console.error(chalk4.red(`error: --platform must be one of ${DEV_PLATFORMS.join(", ")}`));
2316
2409
  process.exit(2);
2317
2410
  }
2318
2411
  if (!opts.platform && !existing?.defaultPlatform) {
@@ -2325,21 +2418,21 @@ async function ensureConfig(api, opts) {
2325
2418
  const isTv = PLATFORM_DEVICE[platform].tv;
2326
2419
  if (isTv && opts.deviceCategory) {
2327
2420
  console.error(
2328
- chalk3.red(`error: --device-category does not apply to ${platform} (always a tv device)`)
2421
+ chalk4.red(`error: --device-category does not apply to ${platform} (always a tv device)`)
2329
2422
  );
2330
2423
  process.exit(2);
2331
2424
  }
2332
2425
  if (!isTv && opts.deviceCategory === "tv") {
2333
- console.error(chalk3.red(`error: use --platform tvos or --platform androidtv for TV devices`));
2426
+ console.error(chalk4.red(`error: use --platform tvos or --platform androidtv for TV devices`));
2334
2427
  process.exit(2);
2335
2428
  }
2336
2429
  if (opts.model !== void 0 && opts.deviceCategory) {
2337
- console.error(chalk3.red("error: pass --model or --device-category, not both"));
2430
+ console.error(chalk4.red("error: pass --model or --device-category, not both"));
2338
2431
  process.exit(2);
2339
2432
  }
2340
2433
  if (opts.model !== void 0 && !opts.model.trim()) {
2341
2434
  console.error(
2342
- chalk3.red("error: --model needs a model name (see `vibeview list-devices --models`)")
2435
+ chalk4.red("error: --model needs a model name (see `vibeview list-devices --models`)")
2343
2436
  );
2344
2437
  process.exit(2);
2345
2438
  }
@@ -2363,7 +2456,7 @@ async function ensureConfig(api, opts) {
2363
2456
  allApps = await api.listApps();
2364
2457
  } catch (err) {
2365
2458
  const message = `could not fetch your app list \u2014 ${err instanceof Error ? err.message : String(err)}`;
2366
- console.error(chalk3.red(`error: ${message}`));
2459
+ console.error(chalk4.red(`error: ${message}`));
2367
2460
  emit(opts.json, { event: "error", message });
2368
2461
  process.exit(1);
2369
2462
  }
@@ -2372,7 +2465,7 @@ async function ensureConfig(api, opts) {
2372
2465
  if (choice.staleFlag) {
2373
2466
  const message = `app "${choice.staleFlag}" not found \u2014 it may have been deleted.`;
2374
2467
  console.error(
2375
- chalk3.red(`error: ${message}`) + `
2468
+ chalk4.red(`error: ${message}`) + `
2376
2469
  Run \`vibeview dev\` without --app to pick from the list.`
2377
2470
  );
2378
2471
  emit(opts.json, { event: "error", message });
@@ -2382,12 +2475,12 @@ Run \`vibeview dev\` without --app to pick from the list.`
2382
2475
  let builtThisRun = false;
2383
2476
  if (choice.staleCached) {
2384
2477
  console.error(
2385
- chalk3.yellow(`\u26A0 app "${choice.staleCached}" no longer exists \u2014 pick your debug build again`)
2478
+ chalk4.yellow(`\u26A0 app "${choice.staleCached}" no longer exists \u2014 pick your debug build again`)
2386
2479
  );
2387
2480
  }
2388
2481
  const buildConfig = firstRunBuild ?? entry?.build ?? null;
2389
- if (!appId && buildConfig) {
2390
- const strategy = buildAppStrategy(apps, Boolean(opts.build), opts.json, process.stdin.isTTY);
2482
+ if (!appId && buildConfig && typeof opts.build !== "string") {
2483
+ const strategy = buildAppStrategy(apps, opts.build === true, opts.json, process.stdin.isTTY);
2391
2484
  let action = strategy === "ask" ? "pick" : strategy;
2392
2485
  if (strategy === "ask") {
2393
2486
  action = await select3({
@@ -2399,20 +2492,20 @@ Run \`vibeview dev\` without --app to pick from the list.`
2399
2492
  });
2400
2493
  }
2401
2494
  if (action === "build") {
2402
- console.error(chalk3.dim(`Building ${platform}...`));
2495
+ console.error(chalk4.dim(`Building ${platform}...`));
2403
2496
  try {
2404
2497
  const result = await runBuildForPlatform(
2405
2498
  process.cwd(),
2406
2499
  platform,
2407
2500
  buildConfig,
2408
2501
  opts,
2409
- (l) => console.error(chalk3.dim(l))
2502
+ (l) => console.error(chalk4.dim(l))
2410
2503
  );
2411
2504
  appId = result.public_id;
2412
2505
  builtThisRun = true;
2413
2506
  } catch (err) {
2414
2507
  const message = err.message;
2415
- console.error(chalk3.red(`error: ${message}`));
2508
+ console.error(chalk4.red(`error: ${message}`));
2416
2509
  emit(opts.json, { event: "error", message });
2417
2510
  process.exit(1);
2418
2511
  }
@@ -2421,20 +2514,20 @@ Run \`vibeview dev\` without --app to pick from the list.`
2421
2514
  if (!appId) {
2422
2515
  if (apps.length === 0) {
2423
2516
  const message = noAppsMessage(platform, entry?.build !== void 0);
2424
- console.error(chalk3.red(message));
2517
+ console.error(chalk4.red(message));
2425
2518
  emit(opts.json, { event: "error", code: "no_apps", message });
2426
2519
  process.exit(1);
2427
2520
  }
2428
2521
  if (choice.staleCached) {
2429
2522
  const message = `app "${choice.staleCached}" for ${platform} no longer exists \u2014 pass --app <id> (see \`vibeview list-apps\`) or run interactively to pick one.`;
2430
- console.error(chalk3.red(`error: ${message}`));
2523
+ console.error(chalk4.red(`error: ${message}`));
2431
2524
  emit(opts.json, { event: "error", code: "app_deleted", message });
2432
2525
  process.exit(1);
2433
2526
  }
2434
2527
  requireNonInteractive(opts.json);
2435
2528
  if (!process.stdin.isTTY) {
2436
2529
  console.error(
2437
- chalk3.red(
2530
+ chalk4.red(
2438
2531
  `error: no valid app configured for ${platform} \u2014 run interactively to pick one, or pass --app <id>`
2439
2532
  )
2440
2533
  );
@@ -2470,7 +2563,7 @@ Run \`vibeview dev\` without --app to pick from the list.`
2470
2563
  { build: !firstRunBuild, metro: PLATFORM_NEEDS_METRO[platform] && !firstRunMetro },
2471
2564
  platform
2472
2565
  );
2473
- if (hint) console.error(chalk3.dim(hint));
2566
+ if (hint) console.error(chalk4.dim(hint));
2474
2567
  }
2475
2568
  const finalMetro = isFirstRun ? resolveMetro(cfg, platform, opts.metroPort ? parseInt(opts.metroPort, 10) : void 0) : metroResolved;
2476
2569
  return {
@@ -2490,10 +2583,21 @@ async function runDevInProcess(opts) {
2490
2583
  watchSeconds = parseWatchSeconds(opts.watch);
2491
2584
  } catch (err) {
2492
2585
  const message = err.message;
2493
- console.error(chalk3.red(`error: ${message}`));
2586
+ console.error(chalk4.red(`error: ${message}`));
2494
2587
  emit(opts.json, { event: "error", code: "invalid_flag", message });
2495
2588
  process.exit(2);
2496
2589
  }
2590
+ let pinnedBuildId;
2591
+ if (typeof opts.build === "string") {
2592
+ try {
2593
+ pinnedBuildId = parseBuildId(opts.build);
2594
+ } catch (err) {
2595
+ const message = err.message;
2596
+ console.error(chalk4.red(`error: ${message}`));
2597
+ emit(opts.json, { event: "error", code: "invalid_flag", message });
2598
+ process.exit(2);
2599
+ }
2600
+ }
2497
2601
  if (opts.json) {
2498
2602
  const hasToken = !!(opts.token || process.env.VIBEVIEW_API_TOKEN || loadConfig().token);
2499
2603
  if (!hasToken) {
@@ -2506,26 +2610,36 @@ async function runDevInProcess(opts) {
2506
2610
  }
2507
2611
  }
2508
2612
  const { token, baseUrl } = resolveAuth(opts);
2509
- console.error(chalk3.dim(`Using ${baseUrl}`));
2613
+ console.error(chalk4.dim(`Using ${baseUrl}`));
2510
2614
  const api = new ApiClient(baseUrl, token);
2511
2615
  const cfg = await ensureConfig(api, opts);
2512
- if (opts.build && !cfg.builtThisRun) {
2616
+ if (pinnedBuildId !== void 0) {
2617
+ try {
2618
+ checkDevBuild(await api.getApp(cfg.appId), pinnedBuildId, cfg.platform);
2619
+ } catch (err) {
2620
+ const message = err.message;
2621
+ console.error(chalk4.red(`error: ${message}`));
2622
+ emit(opts.json, { event: "error", code: "invalid_build", message });
2623
+ process.exit(1);
2624
+ }
2625
+ }
2626
+ if (opts.build === true && !cfg.builtThisRun) {
2513
2627
  const reloaded = loadDevConfig(process.cwd());
2514
2628
  const build = reloaded?.platforms?.[cfg.platform]?.build;
2515
2629
  if (!build) {
2516
2630
  const message = `no build config for ${cfg.platform} \u2014 run vibeview build once to set it up`;
2517
- console.error(chalk3.red(`error: ${message}`));
2631
+ console.error(chalk4.red(`error: ${message}`));
2518
2632
  emit(opts.json, { event: "error", code: "config_required", message });
2519
2633
  process.exit(1);
2520
2634
  }
2521
- console.error(chalk3.dim(`Building ${cfg.platform}...`));
2635
+ console.error(chalk4.dim(`Building ${cfg.platform}...`));
2522
2636
  try {
2523
2637
  const result = await runBuildForPlatform(
2524
2638
  process.cwd(),
2525
2639
  cfg.platform,
2526
2640
  build,
2527
2641
  { token, baseUrl },
2528
- (l) => console.error(chalk3.dim(l))
2642
+ (l) => console.error(chalk4.dim(l))
2529
2643
  );
2530
2644
  cfg.appId = result.public_id;
2531
2645
  if (reloaded && reloaded.platforms[cfg.platform] && reloaded.platforms[cfg.platform].appId !== result.public_id) {
@@ -2534,7 +2648,7 @@ async function runDevInProcess(opts) {
2534
2648
  }
2535
2649
  } catch (err) {
2536
2650
  const message = err.message;
2537
- console.error(chalk3.red(`error: ${message}`));
2651
+ console.error(chalk4.red(`error: ${message}`));
2538
2652
  emit(opts.json, { event: "error", message });
2539
2653
  process.exit(1);
2540
2654
  }
@@ -2556,7 +2670,7 @@ async function runDevInProcess(opts) {
2556
2670
  metroLogStream.write(line.replace(/^\[metro\] /, "") + "\n");
2557
2671
  } catch {
2558
2672
  }
2559
- if (shouldEchoMetroLine(line, opts.debug === true)) console.error(chalk3.dim(line));
2673
+ if (shouldEchoMetroLine(line, opts.debug === true)) console.error(chalk4.dim(line));
2560
2674
  };
2561
2675
  metroResult = await ensureMetro(
2562
2676
  process.cwd(),
@@ -2569,15 +2683,15 @@ async function runDevInProcess(opts) {
2569
2683
  );
2570
2684
  } catch (err) {
2571
2685
  const message = err.message;
2572
- console.error(chalk3.red(`error: ${message}`));
2686
+ console.error(chalk4.red(`error: ${message}`));
2573
2687
  emit(opts.json, { event: "error", code: "metro_down", message });
2574
2688
  process.exit(1);
2575
2689
  }
2576
- for (const w of metroResult.warnings) console.error(chalk3.yellow(`\u26A0 ${w}`));
2690
+ for (const w of metroResult.warnings) console.error(chalk4.yellow(`\u26A0 ${w}`));
2577
2691
  if (metroResult.status === "spawned" || metroResult.status === "respawned") {
2578
- console.error(chalk3.green(`\u2713 started Metro (${cfg.metroCommand})`));
2692
+ console.error(chalk4.green(`\u2713 started Metro (${cfg.metroCommand})`));
2579
2693
  console.error(
2580
- chalk3.dim(` full Metro output: ${metroLogPath(process.cwd())} (--debug echoes it here)`)
2694
+ chalk4.dim(` full Metro output: ${metroLogPath(process.cwd())} (--debug echoes it here)`)
2581
2695
  );
2582
2696
  }
2583
2697
  metroOwnedThisRun = metroResult.status === "spawned" || metroResult.status === "respawned";
@@ -2587,19 +2701,23 @@ async function runDevInProcess(opts) {
2587
2701
  watchIgnore = rokuEntry?.watchIgnore ?? [];
2588
2702
  if (!rokuBuild) {
2589
2703
  const message = `no build config for ${cfg.platform} \u2014 run \`vibeview build --platform ${cfg.platform}\` once to set it up`;
2590
- console.error(chalk3.red(`error: ${message}`));
2704
+ console.error(chalk4.red(`error: ${message}`));
2591
2705
  emit(opts.json, { event: "error", code: "config_required", message });
2592
2706
  process.exit(1);
2593
2707
  }
2594
2708
  }
2595
- console.error(chalk3.dim(`Starting a ${cfg.platform} dev session for ${cfg.appId}...`));
2709
+ console.error(
2710
+ chalk4.dim(
2711
+ `Starting a ${cfg.platform} dev session for ${cfg.appId}${pinnedBuildId !== void 0 ? ` (build ${pinnedBuildId})` : ""}...`
2712
+ )
2713
+ );
2596
2714
  let stopping = false;
2597
2715
  let handle = null;
2598
2716
  let reloadLoop = null;
2599
2717
  const stopSession = async (code) => {
2600
2718
  if (stopping) return;
2601
2719
  stopping = true;
2602
- console.error(chalk3.dim("\nStopping session..."));
2720
+ console.error(chalk4.dim("\nStopping session..."));
2603
2721
  reloadLoop?.stop();
2604
2722
  await handle?.stop();
2605
2723
  if (metroOwnedThisRun) killOwnedMetro(process.cwd());
@@ -2622,6 +2740,7 @@ async function runDevInProcess(opts) {
2622
2740
  appId: cfg.appId,
2623
2741
  deviceCategory: cfg.deviceCategory,
2624
2742
  model: cfg.model,
2743
+ buildId: pinnedBuildId,
2625
2744
  metroPort: cfg.metroPort ?? 8081,
2626
2745
  // startDevSession's own browser-open happens before we've written the
2627
2746
  // state file / emitted session_ready — open it ourselves afterward
@@ -2633,26 +2752,26 @@ async function runDevInProcess(opts) {
2633
2752
  if (evt.event === "warning") {
2634
2753
  if (nextWarningIsMetroUnreachable) {
2635
2754
  nextWarningIsMetroUnreachable = false;
2636
- console.error(chalk3.red(`\u2717 ${evt.message}`));
2755
+ console.error(chalk4.red(`\u2717 ${evt.message}`));
2637
2756
  } else {
2638
- console.error(chalk3.yellow(`\u26A0 ${evt.message}`));
2757
+ console.error(chalk4.yellow(`\u26A0 ${evt.message}`));
2639
2758
  }
2640
2759
  } else if (evt.event === "queued") {
2641
2760
  const where = evt.position ? ` (position ${evt.position})` : "";
2642
2761
  console.error(
2643
- chalk3.yellow(
2762
+ chalk4.yellow(
2644
2763
  `\u23F3 every device is busy \u2014 waiting in the queue${where}, Ctrl-C to give up`
2645
2764
  )
2646
2765
  );
2647
2766
  } else if (evt.event === "tunnel_connected") {
2648
2767
  everConnected = true;
2649
- console.error(chalk3.green("\u2713 tunnel connected \u2014 edit away, Fast Refresh is live"));
2768
+ console.error(chalk4.green("\u2713 tunnel connected \u2014 edit away, Fast Refresh is live"));
2650
2769
  } else if (evt.event === "installing") {
2651
2770
  console.error(
2652
- chalk3.dim(" Putting the channel on the Roku \u2014 the link appears once it is up.")
2771
+ chalk4.dim(" Putting the channel on the Roku \u2014 the link appears once it is up.")
2653
2772
  );
2654
2773
  } else if (evt.event === "session_ended" && !stopping && handle) {
2655
- console.error(chalk3.red("tunnel closed by server \u2014 session ended"));
2774
+ console.error(chalk4.red("tunnel closed by server \u2014 session ended"));
2656
2775
  stopping = true;
2657
2776
  endedEmitted = true;
2658
2777
  emit(opts.json, evt);
@@ -2671,20 +2790,20 @@ async function runDevInProcess(opts) {
2671
2790
  if (!waitingPrinted) {
2672
2791
  waitingPrinted = true;
2673
2792
  console.error(
2674
- chalk3.dim("\u2026 waiting for the device to come up (tunnel not ready yet)")
2793
+ chalk4.dim("\u2026 waiting for the device to come up (tunnel not ready yet)")
2675
2794
  );
2676
2795
  }
2677
2796
  return;
2678
2797
  }
2679
- console.error(chalk3.yellow("\u2026 tunnel dropped, reconnecting"));
2798
+ console.error(chalk4.yellow("\u2026 tunnel dropped, reconnecting"));
2680
2799
  } else if (s === "metro-unreachable") {
2681
2800
  nextWarningIsMetroUnreachable = true;
2682
2801
  }
2683
2802
  }
2684
2803
  );
2685
- const keysLine = rokuBuild ? `Press ${chalk3.bold("r")} to reload the channel, ${chalk3.bold("q")} or Ctrl-C to end the session.` : "Ctrl-C to end the session.";
2804
+ const keysLine = rokuBuild ? `Press ${chalk4.bold("r")} to reload the channel, ${chalk4.bold("q")} or Ctrl-C to end the session.` : "Ctrl-C to end the session.";
2686
2805
  console.error(`
2687
- Watch and interact: ${chalk3.cyan(handle.pageUrl)}
2806
+ Watch and interact: ${chalk4.cyan(handle.pageUrl)}
2688
2807
  ${keysLine}
2689
2808
  `);
2690
2809
  writeDevState(process.cwd(), {
@@ -2716,13 +2835,13 @@ async function runDevInProcess(opts) {
2716
2835
  // Build output and loop notices are human output, so they go to
2717
2836
  // stderr like every other line this command prints — stdout stays
2718
2837
  // reserved for the `--json` event stream.
2719
- log: (line) => console.error(chalk3.dim(line))
2838
+ log: (line) => console.error(chalk4.dim(line))
2720
2839
  }),
2721
- log: (line) => console.error(chalk3.yellow(line)),
2840
+ log: (line) => console.error(chalk4.yellow(line)),
2722
2841
  // A failed reload is an error, not a notice — same red as every other
2723
2842
  // failure this command prints, so it doesn't read like the yellow
2724
2843
  // "already reloading" line.
2725
- logError: (line) => console.error(chalk3.red(`\u2717 ${line}`)),
2844
+ logError: (line) => console.error(chalk4.red(`\u2717 ${line}`)),
2726
2845
  ...watchSeconds !== void 0 ? {
2727
2846
  watchSeconds,
2728
2847
  watch: watchProjectFiles(process.cwd(), rokuBuild.artifact, watchIgnore)
@@ -2730,7 +2849,7 @@ async function runDevInProcess(opts) {
2730
2849
  });
2731
2850
  if (watchSeconds !== void 0) {
2732
2851
  console.error(
2733
- chalk3.dim(` Watching for changes \u2014 reloading after ${watchSeconds}s of quiet.`)
2852
+ chalk4.dim(` Watching for changes \u2014 reloading after ${watchSeconds}s of quiet.`)
2734
2853
  );
2735
2854
  }
2736
2855
  if (process.stdin.isTTY) {
@@ -2765,7 +2884,7 @@ async function runDevInProcess(opts) {
2765
2884
  }
2766
2885
  const verdict = sessionLivenessVerdict(info);
2767
2886
  if (verdict.end && !stopping) {
2768
- console.error(chalk3.red(verdict.message));
2887
+ console.error(chalk4.red(verdict.message));
2769
2888
  emit(opts.json, { event: "session_ended", session_id: sessionId, code: 1 });
2770
2889
  endedEmitted = true;
2771
2890
  await stopSession(1);
@@ -2774,7 +2893,7 @@ async function runDevInProcess(opts) {
2774
2893
  const warningVerdict = lateWarningVerdict(info, reportedWarnings);
2775
2894
  for (const w of warningVerdict.toReport) {
2776
2895
  reportedWarnings.add(w);
2777
- console.error(chalk3.yellow(`\u26A0 ${w}`));
2896
+ console.error(chalk4.yellow(`\u26A0 ${w}`));
2778
2897
  emit(opts.json, { event: "warning", message: w });
2779
2898
  }
2780
2899
  if (warningVerdict.blocking && !stopping) {
@@ -2792,7 +2911,7 @@ async function runDevInProcess(opts) {
2792
2911
  }
2793
2912
  } catch (err) {
2794
2913
  const message = err.message;
2795
- console.error(chalk3.red(`error: ${message}`));
2914
+ console.error(chalk4.red(`error: ${message}`));
2796
2915
  emit(opts.json, { event: "error", message });
2797
2916
  await stopSession(1);
2798
2917
  }
@@ -2822,12 +2941,12 @@ async function dev(opts) {
2822
2941
  if (opts.json) {
2823
2942
  console.log(JSON.stringify(parsed));
2824
2943
  } else {
2825
- console.error(chalk3.dim(`Using ${resolveAuth(opts).baseUrl}`));
2826
- console.error(chalk3.green(`\u2713 dev session started in the background (pid ${parsed.pid})`));
2944
+ console.error(chalk4.dim(`Using ${resolveAuth(opts).baseUrl}`));
2945
+ console.error(chalk4.green(`\u2713 dev session started in the background (pid ${parsed.pid})`));
2827
2946
  console.error(`
2828
- Watch and interact: ${chalk3.cyan(parsed.url)}`);
2829
- console.error(chalk3.dim(` vibeview dev-status \u2014 check on it`));
2830
- console.error(chalk3.dim(` vibeview dev-stop \u2014 end it
2947
+ Watch and interact: ${chalk4.cyan(parsed.url)}`);
2948
+ console.error(chalk4.dim(` vibeview dev-status \u2014 check on it`));
2949
+ console.error(chalk4.dim(` vibeview dev-stop \u2014 end it
2831
2950
  `));
2832
2951
  }
2833
2952
  child.unref();
@@ -2837,13 +2956,13 @@ async function dev(opts) {
2837
2956
  settled = true;
2838
2957
  rl.close();
2839
2958
  emit(opts.json, parsed);
2840
- if (!opts.json) console.error(chalk3.red(`error: ${parsed.message}`));
2959
+ if (!opts.json) console.error(chalk4.red(`error: ${parsed.message}`));
2841
2960
  reject(new Error(parsed.message));
2842
2961
  } else if (parsed.event === "warning") {
2843
2962
  if (opts.json) {
2844
2963
  console.log(JSON.stringify(parsed));
2845
2964
  } else {
2846
- console.error(chalk3.yellow(`\u26A0 ${parsed.message}`));
2965
+ console.error(chalk4.yellow(`\u26A0 ${parsed.message}`));
2847
2966
  }
2848
2967
  }
2849
2968
  });
@@ -2852,14 +2971,14 @@ async function dev(opts) {
2852
2971
  rl.close();
2853
2972
  const message = `dev session exited before starting (code ${code ?? "unknown"})`;
2854
2973
  emit(opts.json, { event: "error", message });
2855
- if (!opts.json) console.error(chalk3.red(`error: ${message}`));
2974
+ if (!opts.json) console.error(chalk4.red(`error: ${message}`));
2856
2975
  reject(new Error(message));
2857
2976
  });
2858
2977
  child.on("error", (err) => {
2859
2978
  if (settled) return;
2860
2979
  rl.close();
2861
2980
  emit(opts.json, { event: "error", message: err.message });
2862
- if (!opts.json) console.error(chalk3.red(`error: ${err.message}`));
2981
+ if (!opts.json) console.error(chalk4.red(`error: ${err.message}`));
2863
2982
  reject(err);
2864
2983
  });
2865
2984
  }).catch(() => {
@@ -2870,8 +2989,8 @@ async function dev(opts) {
2870
2989
  }
2871
2990
 
2872
2991
  // src/lib/formatters/devices.ts
2873
- import chalk4 from "chalk";
2874
- var HEADERS = ["DEVICE_ID", "NAME", "TYPE", "CATEGORY", "OS", "STATUS"];
2992
+ import chalk5 from "chalk";
2993
+ var HEADERS2 = ["DEVICE_ID", "NAME", "TYPE", "CATEGORY", "OS", "STATUS"];
2875
2994
  function formatDevicesHuman(devices) {
2876
2995
  if (devices.length === 0) {
2877
2996
  return "No devices registered.\n";
@@ -2884,11 +3003,11 @@ function formatDevicesHuman(devices) {
2884
3003
  d.os_version,
2885
3004
  d.status
2886
3005
  ]);
2887
- const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
3006
+ const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2888
3007
  const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
2889
- const lines = [chalk4.bold(fmt(HEADERS))];
3008
+ const lines = [chalk5.bold(fmt(HEADERS2))];
2890
3009
  for (const row of rows.sort(rowSort)) {
2891
- const statusColored = row[5] === "available" ? chalk4.green(row[5]) : row[5] === "busy" ? chalk4.yellow(row[5]) : chalk4.dim(row[5]);
3010
+ const statusColored = row[5] === "available" ? chalk5.green(row[5]) : row[5] === "busy" ? chalk5.yellow(row[5]) : chalk5.dim(row[5]);
2892
3011
  lines.push(fmt([row[0], row[1], row[2], row[3], row[4], statusColored]));
2893
3012
  }
2894
3013
  return lines.join("\n") + "\n";
@@ -2915,7 +3034,7 @@ function formatDeviceModelsHuman(models) {
2915
3034
  );
2916
3035
  const widths = MODEL_HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
2917
3036
  const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
2918
- return [chalk4.bold(fmt(MODEL_HEADERS)), ...rows.map(fmt)].join("\n") + "\n";
3037
+ return [chalk5.bold(fmt(MODEL_HEADERS)), ...rows.map(fmt)].join("\n") + "\n";
2919
3038
  }
2920
3039
 
2921
3040
  // src/agent/registry.ts
@@ -3370,18 +3489,18 @@ var AGENT_COMMANDS = [
3370
3489
  ];
3371
3490
 
3372
3491
  // src/agent/render.ts
3373
- import chalk5 from "chalk";
3492
+ import chalk6 from "chalk";
3374
3493
  function renderEnvelope(env) {
3375
3494
  const lines = [];
3376
3495
  if (env.ok) {
3377
- lines.push(chalk5.green(`ok`) + chalk5.dim(` (${env.tool})`));
3496
+ lines.push(chalk6.green(`ok`) + chalk6.dim(` (${env.tool})`));
3378
3497
  } else {
3379
- lines.push(chalk5.red(`error`) + chalk5.dim(` (${env.tool})`));
3498
+ lines.push(chalk6.red(`error`) + chalk6.dim(` (${env.tool})`));
3380
3499
  }
3381
3500
  lines.push(...resultLines(env.result));
3382
3501
  if (env.observation) {
3383
3502
  if (env.observation.kind === "unavailable") {
3384
- lines.push(chalk5.yellow(`warning: ${env.observation.text}`));
3503
+ lines.push(chalk6.yellow(`warning: ${env.observation.text}`));
3385
3504
  } else {
3386
3505
  lines.push(env.observation.text);
3387
3506
  }
@@ -3394,17 +3513,17 @@ function resultLines(result) {
3394
3513
  const lines = [];
3395
3514
  if (Array.isArray(r.entries) && typeof r.cursor === "number") {
3396
3515
  if (r.entries.length === 0) {
3397
- lines.push(chalk5.dim("(no log lines)"));
3516
+ lines.push(chalk6.dim("(no log lines)"));
3398
3517
  }
3399
3518
  for (const e of r.entries) {
3400
3519
  if (!e || typeof e !== "object") continue;
3401
3520
  const entry = e;
3402
3521
  const ts = typeof entry.timestamp === "number" ? new Date(entry.timestamp).toISOString().slice(11, 23) : "??:??:??.???";
3403
3522
  const level = typeof entry.level === "string" ? entry.level : "info";
3404
- const levelTag = level === "error" ? chalk5.red(level) : level === "warn" ? chalk5.yellow(level) : chalk5.dim(level);
3405
- lines.push(`${chalk5.dim(ts)} ${levelTag} ${String(entry.message ?? "")}`);
3523
+ const levelTag = level === "error" ? chalk6.red(level) : level === "warn" ? chalk6.yellow(level) : chalk6.dim(level);
3524
+ lines.push(`${chalk6.dim(ts)} ${levelTag} ${String(entry.message ?? "")}`);
3406
3525
  }
3407
- lines.push(chalk5.dim(`cursor: ${r.cursor}`));
3526
+ lines.push(chalk6.dim(`cursor: ${r.cursor}`));
3408
3527
  }
3409
3528
  if (r.found === true && typeof r.ref === "string" && r.ref) {
3410
3529
  let found = `found: @${r.ref}`;
@@ -3416,14 +3535,14 @@ function resultLines(result) {
3416
3535
  const text = typeof a.text === "string" && a.text ? ` ${JSON.stringify(a.text)}` : "";
3417
3536
  return `${ref}${text}`;
3418
3537
  });
3419
- if (alts.length > 0) lines.push(chalk5.dim(`alternatives: ${alts.join(", ")}`));
3538
+ if (alts.length > 0) lines.push(chalk6.dim(`alternatives: ${alts.join(", ")}`));
3420
3539
  }
3421
3540
  }
3422
3541
  if (r.action === "posture_set" || r.action === "rotated") {
3423
3542
  const state = deviceStateLine(r);
3424
3543
  if (state) lines.push(state);
3425
3544
  }
3426
- if (typeof r.note === "string" && r.note) lines.push(chalk5.dim(r.note));
3545
+ if (typeof r.note === "string" && r.note) lines.push(chalk6.dim(r.note));
3427
3546
  if (typeof r.message === "string" && r.message) lines.push(r.message);
3428
3547
  if (typeof r.warning === "string" && r.warning) lines.push(r.warning);
3429
3548
  return lines;
@@ -3687,15 +3806,20 @@ async function runAgentVerb(cmd, positionals, opts) {
3687
3806
  }
3688
3807
 
3689
3808
  export {
3809
+ parseBuildId,
3810
+ checkDevBuild,
3811
+ buildRows,
3812
+ formatBuildRowsHuman,
3813
+ listBuilds,
3690
3814
  readDevState,
3691
3815
  clearDevState,
3816
+ readMetroState,
3817
+ killOwnedMetro,
3692
3818
  DEV_PLATFORMS,
3693
3819
  PLATFORM_NEEDS_METRO,
3694
3820
  isDevPlatform,
3695
3821
  loadDevConfig,
3696
3822
  checkMetro,
3697
- readMetroState,
3698
- killOwnedMetro,
3699
3823
  buildCommand,
3700
3824
  formatDuration,
3701
3825
  BUILD_TERMINAL_STATUSES,