vibeview 0.25.0 → 0.27.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.
@@ -1,170 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ApiClient,
4
+ AuthError,
5
+ NotFoundError,
4
6
  UsageError,
5
7
  createSpinner,
8
+ errorExitCode,
6
9
  loadConfig,
7
10
  resolveAuth,
8
11
  uploadAppBuild
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
- }
102
-
103
- // src/lib/dev-state.ts
104
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
105
- import { dirname, join } from "path";
106
- var STATE_DIR = ".vibeview";
107
- var STATE_FILE = "dev-state.json";
108
- function statePath(cwd) {
109
- return join(cwd, STATE_DIR, STATE_FILE);
110
- }
111
- function isPidAlive(pid) {
112
- try {
113
- process.kill(pid, 0);
114
- return true;
115
- } catch (err) {
116
- return err.code === "EPERM";
117
- }
118
- }
119
- function ensureGitignored(cwd) {
120
- try {
121
- const gitignore = join(cwd, ".gitignore");
122
- if (!existsSync(gitignore)) return;
123
- const content = readFileSync(gitignore, "utf-8");
124
- const covered = content.split("\n").map((l) => l.trim()).some((l) => l === ".vibeview" || l === ".vibeview/" || l === "/.vibeview");
125
- if (covered) return;
126
- const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
127
- writeFileSync(gitignore, `${content}${prefix}
128
- # VibeView local session state
129
- .vibeview/
130
- `);
131
- } catch {
132
- }
133
- }
134
- function writeDevState(cwd, state) {
135
- const path = statePath(cwd);
136
- mkdirSync(dirname(path), { recursive: true });
137
- writeFileSync(path, JSON.stringify(state, null, 2) + "\n");
138
- ensureGitignored(cwd);
139
- }
140
- function readDevState(cwd) {
141
- let raw;
142
- try {
143
- raw = readFileSync(statePath(cwd), "utf-8");
144
- } catch {
145
- return null;
146
- }
147
- let parsed;
148
- try {
149
- parsed = JSON.parse(raw);
150
- } catch {
151
- return null;
152
- }
153
- if (typeof parsed.pid !== "number" || !isPidAlive(parsed.pid)) {
154
- return null;
155
- }
156
- return parsed;
157
- }
158
- function clearDevState(cwd) {
159
- const path = statePath(cwd);
160
- if (existsSync(path)) {
161
- rmSync(path, { force: true });
162
- }
163
- }
12
+ } from "./chunk-U7FCGMWG.js";
164
13
 
165
14
  // src/lib/dev-config.ts
166
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
167
- import { join as join2 } from "path";
15
+ import { readFileSync, writeFileSync } from "fs";
16
+ import { join } from "path";
168
17
  var DEV_PLATFORMS = ["ios", "android", "tvos", "androidtv", "roku"];
169
18
  var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
170
19
  var PLATFORM_NEEDS_METRO = {
@@ -178,10 +27,25 @@ var FILE = "vibeview.json";
178
27
  function isDevPlatform(p) {
179
28
  return typeof p === "string" && DEV_PLATFORMS.includes(p);
180
29
  }
30
+ var PLATFORM_LABELS = {
31
+ ios: "iOS",
32
+ android: "Android",
33
+ tvos: "tvOS",
34
+ androidtv: "Android TV",
35
+ roku: "Roku"
36
+ };
37
+ function platformLabel(platform) {
38
+ return isDevPlatform(platform) ? PLATFORM_LABELS[platform] : platform;
39
+ }
40
+ function platformWithArticle(platform) {
41
+ const label = platformLabel(platform);
42
+ const article = /^[aeiou]/i.test(label) ? "an" : "a";
43
+ return `${article} ${label}`;
44
+ }
181
45
  function loadDevConfig(cwd) {
182
46
  let raw;
183
47
  try {
184
- raw = readFileSync2(join2(cwd, FILE), "utf-8");
48
+ raw = readFileSync(join(cwd, FILE), "utf-8");
185
49
  } catch {
186
50
  return null;
187
51
  }
@@ -298,7 +162,7 @@ function validateBuildEnv(env, platformKey) {
298
162
  }
299
163
  }
300
164
  function saveDevConfig(cwd, cfg) {
301
- writeFileSync2(join2(cwd, FILE), JSON.stringify(cfg, null, 2) + "\n");
165
+ writeFileSync(join(cwd, FILE), JSON.stringify(cfg, null, 2) + "\n");
302
166
  }
303
167
  function resolveMetro(cfg, platform, portFlag) {
304
168
  const entry = cfg?.platforms?.[platform];
@@ -342,6 +206,173 @@ function buildSavedConfig(input2) {
342
206
  };
343
207
  }
344
208
 
209
+ // src/lib/dev-build.ts
210
+ function parseBuildId(raw) {
211
+ const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw.trim() : "";
212
+ if (!/^\d+$/.test(text) || Number(text) < 1) {
213
+ throw new Error(
214
+ `--build expects a build id (a positive number, see \`vibeview list-builds <app>\`), got "${String(raw)}"`
215
+ );
216
+ }
217
+ return Number(text);
218
+ }
219
+ function buildKind(isDebug) {
220
+ if (isDebug === true) return "debug";
221
+ if (isDebug === false) return "release";
222
+ return "unknown";
223
+ }
224
+ function checkDevBuild(app, buildId, platform) {
225
+ if (platform === "roku") {
226
+ throw new Error(
227
+ "a build id cannot be pinned on roku \u2014 `vibeview dev` packages and sideloads the channel itself"
228
+ );
229
+ }
230
+ if (app.platform !== platform) {
231
+ throw new Error(
232
+ `app ${app.public_id} is ${platformWithArticle(app.platform)} app, not ${platformLabel(platform)}`
233
+ );
234
+ }
235
+ const build = app.builds.find((b) => b.id === buildId);
236
+ if (!build) {
237
+ throw new Error(
238
+ `build ${buildId} is not a build of app ${app.public_id} \u2014 see \`vibeview list-builds ${app.public_id}\``
239
+ );
240
+ }
241
+ if (build.is_debug !== true) {
242
+ 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";
243
+ throw new Error(
244
+ `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}\`)`
245
+ );
246
+ }
247
+ return build;
248
+ }
249
+
250
+ // src/commands/list-builds.ts
251
+ import chalk from "chalk";
252
+
253
+ // src/lib/formatters/time.ts
254
+ function formatUtcTimestamp(iso) {
255
+ if (!iso) return "";
256
+ const hasZone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(iso);
257
+ const d = new Date(hasZone ? iso : `${iso}Z`);
258
+ if (Number.isNaN(d.getTime())) return iso;
259
+ return `${d.toISOString().slice(0, 19).replace("T", " ")} UTC`;
260
+ }
261
+
262
+ // src/commands/list-builds.ts
263
+ function buildRows(app) {
264
+ return app.builds.map((b) => ({
265
+ id: b.id,
266
+ version: b.version ?? null,
267
+ build_number: b.build_number ?? null,
268
+ platform: app.platform,
269
+ kind: buildKind(b.is_debug),
270
+ is_debug: b.is_debug ?? null,
271
+ uploaded_at: b.uploaded_at ?? null,
272
+ note: b.note ?? null
273
+ }));
274
+ }
275
+ var HEADERS = ["ID", "VERSION", "PLATFORM", "KIND", "UPLOADED", "NOTE"];
276
+ function formatBuildRowsHuman(app, rows) {
277
+ if (rows.length === 0) {
278
+ return `No builds uploaded for ${app.name} (${app.public_id}) \u2014 upload one with \`vibeview upload-app\`.
279
+ `;
280
+ }
281
+ const cells = rows.map((r) => [
282
+ String(r.id),
283
+ r.version ? `${r.version}${r.build_number ? ` (${r.build_number})` : ""}` : r.build_number ?? "",
284
+ r.platform,
285
+ r.kind,
286
+ formatUtcTimestamp(r.uploaded_at),
287
+ (r.note ?? "").split("\n")[0]
288
+ ]);
289
+ const widths = HEADERS.map((h, i) => Math.max(h.length, ...cells.map((c) => c[i].length)));
290
+ const pad = (row) => row.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
291
+ const lines = [chalk.bold(pad(HEADERS)), ...cells.map(pad)];
292
+ return lines.join("\n") + "\n";
293
+ }
294
+ async function listBuilds(appId, opts, deps) {
295
+ const { token, baseUrl } = resolveAuth(opts);
296
+ const d = deps ?? { getApp: (id) => new ApiClient(baseUrl, token).getApp(id) };
297
+ try {
298
+ const app = await d.getApp(appId);
299
+ const rows = buildRows(app);
300
+ if (opts.json) {
301
+ console.log(JSON.stringify(rows, null, 2));
302
+ } else {
303
+ process.stdout.write(formatBuildRowsHuman(app, rows));
304
+ }
305
+ process.exitCode = 0;
306
+ } catch (err) {
307
+ const message = err instanceof Error ? err.message : String(err);
308
+ if (opts.json) console.log(JSON.stringify({ event: "error", message }));
309
+ console.error(chalk.red(`error: ${message}`));
310
+ process.exitCode = errorExitCode(err);
311
+ }
312
+ }
313
+
314
+ // src/lib/dev-state.ts
315
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
316
+ import { dirname, join as join2 } from "path";
317
+ var STATE_DIR = ".vibeview";
318
+ var STATE_FILE = "dev-state.json";
319
+ function statePath(cwd) {
320
+ return join2(cwd, STATE_DIR, STATE_FILE);
321
+ }
322
+ function isPidAlive(pid) {
323
+ try {
324
+ process.kill(pid, 0);
325
+ return true;
326
+ } catch (err) {
327
+ return err.code === "EPERM";
328
+ }
329
+ }
330
+ function ensureGitignored(cwd) {
331
+ try {
332
+ const gitignore = join2(cwd, ".gitignore");
333
+ if (!existsSync(gitignore)) return;
334
+ const content = readFileSync2(gitignore, "utf-8");
335
+ const covered = content.split("\n").map((l) => l.trim()).some((l) => l === ".vibeview" || l === ".vibeview/" || l === "/.vibeview");
336
+ if (covered) return;
337
+ const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
338
+ writeFileSync2(gitignore, `${content}${prefix}
339
+ # VibeView local session state
340
+ .vibeview/
341
+ `);
342
+ } catch {
343
+ }
344
+ }
345
+ function writeDevState(cwd, state) {
346
+ const path = statePath(cwd);
347
+ mkdirSync(dirname(path), { recursive: true });
348
+ writeFileSync2(path, JSON.stringify(state, null, 2) + "\n");
349
+ ensureGitignored(cwd);
350
+ }
351
+ function readDevState(cwd) {
352
+ let raw;
353
+ try {
354
+ raw = readFileSync2(statePath(cwd), "utf-8");
355
+ } catch {
356
+ return null;
357
+ }
358
+ let parsed;
359
+ try {
360
+ parsed = JSON.parse(raw);
361
+ } catch {
362
+ return null;
363
+ }
364
+ if (typeof parsed.pid !== "number" || !isPidAlive(parsed.pid)) {
365
+ return null;
366
+ }
367
+ return parsed;
368
+ }
369
+ function clearDevState(cwd) {
370
+ const path = statePath(cwd);
371
+ if (existsSync(path)) {
372
+ rmSync(path, { force: true });
373
+ }
374
+ }
375
+
345
376
  // src/lib/metro-check.ts
346
377
  async function checkMetro(port, timeoutMs = 1500) {
347
378
  try {
@@ -871,7 +902,7 @@ function runTar(args) {
871
902
  var CLOUD_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
872
903
  var PRODUCTION_SIGNING_PLATFORMS = ["ios", "android", "androidtv", "tvos"];
873
904
  function productionUnsupportedReason(platform) {
874
- return `--production doesn't support ${platform} \u2014 production signing supports ${PRODUCTION_SIGNING_PLATFORMS.join(", ")}`;
905
+ return `--production doesn't support ${platformLabel(platform)} \u2014 production signing supports ${PRODUCTION_SIGNING_PLATFORMS.join(", ")}`;
875
906
  }
876
907
  function isCloudPlatform(p) {
877
908
  return CLOUD_PLATFORMS.includes(p);
@@ -898,7 +929,7 @@ async function runBuildForPlatform(cwd, platform, build, opts, log, deps = { upl
898
929
  const { result } = await deps.upload(uploadPath, opts);
899
930
  if (result.platform !== platform) {
900
931
  throw new Error(
901
- `you built an ${result.platform} app (${result.name}) but the target platform is ${platform} \u2014 check platforms.${platform}.build in vibeview.json`
932
+ `you built ${platformWithArticle(result.platform)} app (${result.name}) but the target platform is ${platformLabel(platform)} \u2014 check platforms.${platform}.build in vibeview.json`
902
933
  );
903
934
  }
904
935
  return result;
@@ -1056,7 +1087,7 @@ async function buildCommand(opts, cloudDeps) {
1056
1087
  if (!(err instanceof BuildFailError)) {
1057
1088
  printFailure(opts, err instanceof Error ? err.message : String(err));
1058
1089
  }
1059
- process.exitCode = 1;
1090
+ process.exitCode = errorExitCode(err);
1060
1091
  }
1061
1092
  }
1062
1093
  function printFailure(opts, message, code) {
@@ -1135,7 +1166,7 @@ function requireCloudCapable(opts, platform) {
1135
1166
  if (!isCloudPlatform(platform)) {
1136
1167
  fail(
1137
1168
  opts,
1138
- `cloud builds don't support ${platform} \u2014 pass one of ${CLOUD_PLATFORMS.join(", ")}`
1169
+ `cloud builds don't support ${platformLabel(platform)} \u2014 pass one of ${CLOUD_PLATFORMS.join(", ")}`
1139
1170
  );
1140
1171
  }
1141
1172
  }
@@ -2393,13 +2424,13 @@ function buildAppStrategy(apps, buildFlag, json, isTTY) {
2393
2424
  if (json || !isTTY) return "pick";
2394
2425
  return "ask";
2395
2426
  }
2396
- function requireNonInteractive(json) {
2427
+ function noBuildConfigMessage(platform, interactive) {
2428
+ const addKey = `add platforms.${platform}.build {command, artifact} to vibeview.json`;
2429
+ return interactive ? `no build config for ${platform}: ${addKey}, or run \`vibeview build --platform ${platform}\` once to set it up` : `no build config for ${platform}: ${addKey}`;
2430
+ }
2431
+ function requireNonInteractive(json, message) {
2397
2432
  if (!json) return;
2398
- emit(json, {
2399
- event: "error",
2400
- code: "config_required",
2401
- message: "pass --platform and run once interactively, or commit vibeview.json"
2402
- });
2433
+ emit(json, { event: "error", code: "config_required", message });
2403
2434
  process.exit(1);
2404
2435
  }
2405
2436
  async function ensureConfig(api, opts) {
@@ -2409,7 +2440,10 @@ async function ensureConfig(api, opts) {
2409
2440
  process.exit(2);
2410
2441
  }
2411
2442
  if (!opts.platform && !existing?.defaultPlatform) {
2412
- requireNonInteractive(opts.json);
2443
+ requireNonInteractive(
2444
+ opts.json,
2445
+ `--platform is required (no vibeview.json default yet) \u2014 pass --platform <${DEV_PLATFORMS.join("|")}>, or run once interactively to set a default.`
2446
+ );
2413
2447
  }
2414
2448
  const platform = opts.platform || existing?.defaultPlatform || await select3({
2415
2449
  message: "Platform to run on",
@@ -2441,9 +2475,7 @@ async function ensureConfig(api, opts) {
2441
2475
  let isFirstRun = false;
2442
2476
  if (!existing) {
2443
2477
  isFirstRun = true;
2444
- if (opts.json) {
2445
- requireNonInteractive(opts.json);
2446
- } else if (process.stdin.isTTY) {
2478
+ if (!opts.json && process.stdin.isTTY) {
2447
2479
  firstRunBuild = await promptBuildConfig(process.cwd(), platform);
2448
2480
  if (PLATFORM_NEEDS_METRO[platform]) {
2449
2481
  firstRunMetro = await promptMetroCommand(process.cwd(), platform);
@@ -2524,7 +2556,10 @@ Run \`vibeview dev\` without --app to pick from the list.`
2524
2556
  emit(opts.json, { event: "error", code: "app_deleted", message });
2525
2557
  process.exit(1);
2526
2558
  }
2527
- requireNonInteractive(opts.json);
2559
+ requireNonInteractive(
2560
+ opts.json,
2561
+ `--app is required \u2014 no app configured for ${platform} yet; pass --app <id> (see \`vibeview list-apps\`), or run interactively to pick one.`
2562
+ );
2528
2563
  if (!process.stdin.isTTY) {
2529
2564
  console.error(
2530
2565
  chalk4.red(
@@ -2557,14 +2592,17 @@ Run \`vibeview dev\` without --app to pick from the list.`
2557
2592
  firstRunMetro,
2558
2593
  firstRunBuild
2559
2594
  });
2560
- saveDevConfig(process.cwd(), cfg);
2561
- if (isFirstRun) {
2562
- const hint = fillLaterHint(
2563
- { build: !firstRunBuild, metro: PLATFORM_NEEDS_METRO[platform] && !firstRunMetro },
2564
- platform
2565
- );
2566
- if (hint) console.error(chalk4.dim(hint));
2567
- }
2595
+ const commitConfig = () => {
2596
+ saveDevConfig(process.cwd(), cfg);
2597
+ if (isFirstRun) {
2598
+ console.error(chalk4.dim(`Created vibeview.json (platform: ${platform}, app: ${appId}).`));
2599
+ const hint = fillLaterHint(
2600
+ { build: !firstRunBuild, metro: PLATFORM_NEEDS_METRO[platform] && !firstRunMetro },
2601
+ platform
2602
+ );
2603
+ if (hint) console.error(chalk4.dim(hint));
2604
+ }
2605
+ };
2568
2606
  const finalMetro = isFirstRun ? resolveMetro(cfg, platform, opts.metroPort ? parseInt(opts.metroPort, 10) : void 0) : metroResolved;
2569
2607
  return {
2570
2608
  platform,
@@ -2574,7 +2612,9 @@ Run \`vibeview dev\` without --app to pick from the list.`
2574
2612
  metroPort: finalMetro.port,
2575
2613
  metroCommand: finalMetro.command,
2576
2614
  platformHasMetroOverride: finalMetro.platformHasOverride,
2577
- builtThisRun
2615
+ builtThisRun,
2616
+ savedConfig: cfg,
2617
+ commitConfig
2578
2618
  };
2579
2619
  }
2580
2620
  async function runDevInProcess(opts) {
@@ -2623,11 +2663,20 @@ async function runDevInProcess(opts) {
2623
2663
  process.exit(1);
2624
2664
  }
2625
2665
  }
2666
+ const interactive = !opts.json && Boolean(process.stdin.isTTY);
2667
+ const plannedBuild = cfg.savedConfig.platforms?.[cfg.platform]?.build;
2668
+ if (!plannedBuild && (opts.build === true && !cfg.builtThisRun || !PLATFORM_NEEDS_METRO[cfg.platform])) {
2669
+ const message = noBuildConfigMessage(cfg.platform, interactive);
2670
+ console.error(chalk4.red(`error: ${message}`));
2671
+ emit(opts.json, { event: "error", code: "config_required", message });
2672
+ process.exit(1);
2673
+ }
2674
+ cfg.commitConfig();
2626
2675
  if (opts.build === true && !cfg.builtThisRun) {
2627
2676
  const reloaded = loadDevConfig(process.cwd());
2628
2677
  const build = reloaded?.platforms?.[cfg.platform]?.build;
2629
2678
  if (!build) {
2630
- const message = `no build config for ${cfg.platform} \u2014 run vibeview build once to set it up`;
2679
+ const message = noBuildConfigMessage(cfg.platform, interactive);
2631
2680
  console.error(chalk4.red(`error: ${message}`));
2632
2681
  emit(opts.json, { event: "error", code: "config_required", message });
2633
2682
  process.exit(1);
@@ -2700,7 +2749,7 @@ async function runDevInProcess(opts) {
2700
2749
  rokuBuild = rokuEntry?.build;
2701
2750
  watchIgnore = rokuEntry?.watchIgnore ?? [];
2702
2751
  if (!rokuBuild) {
2703
- const message = `no build config for ${cfg.platform} \u2014 run \`vibeview build --platform ${cfg.platform}\` once to set it up`;
2752
+ const message = noBuildConfigMessage(cfg.platform, interactive);
2704
2753
  console.error(chalk4.red(`error: ${message}`));
2705
2754
  emit(opts.json, { event: "error", code: "config_required", message });
2706
2755
  process.exit(1);
@@ -2708,7 +2757,7 @@ async function runDevInProcess(opts) {
2708
2757
  }
2709
2758
  console.error(
2710
2759
  chalk4.dim(
2711
- `Starting a ${cfg.platform} dev session for ${cfg.appId}${pinnedBuildId !== void 0 ? ` (build ${pinnedBuildId})` : ""}...`
2760
+ `Starting ${platformWithArticle(cfg.platform)} dev session for ${cfg.appId}${pinnedBuildId !== void 0 ? ` (build ${pinnedBuildId})` : ""}...`
2712
2761
  )
2713
2762
  );
2714
2763
  let stopping = false;
@@ -3038,6 +3087,26 @@ function formatDeviceModelsHuman(models) {
3038
3087
  }
3039
3088
 
3040
3089
  // src/agent/registry.ts
3090
+ var PRESS_BUTTONS_BY_PLATFORM = [
3091
+ ["iPhone / iPad", ["home", "back", "lock", "siri"]],
3092
+ ["Android phone / tablet", ["home", "back", "lock"]],
3093
+ [
3094
+ "Apple TV / Android TV",
3095
+ ["dpad_up", "dpad_down", "dpad_left", "dpad_right", "dpad_center", "back", "home"]
3096
+ ],
3097
+ [
3098
+ "Roku (beta)",
3099
+ ["up", "down", "left", "right", "select", "back", "(dpad_* also work; home is not forwarded)"]
3100
+ ]
3101
+ ];
3102
+ function pressButtonsHelp() {
3103
+ return [
3104
+ "Valid buttons:",
3105
+ ...PRESS_BUTTONS_BY_PLATFORM.map(
3106
+ ([platform, buttons]) => ` ${platform}: ${buttons.join(", ")}`
3107
+ )
3108
+ ].join("\n");
3109
+ }
3041
3110
  var AGENT_COMMANDS = [
3042
3111
  {
3043
3112
  verb: "ui-tree",
@@ -3071,7 +3140,7 @@ var AGENT_COMMANDS = [
3071
3140
  {
3072
3141
  verb: "screenshot",
3073
3142
  tool: "screenshot",
3074
- summary: "Capture a screenshot of the current screen. Use it to check how something LOOKS \u2014 the UI tree carries no colour, contrast, or stacking information, so a screen can pass every check the tree can express and still be visibly broken. Saved to disk; over MCP, pass inline: true to receive the image itself.",
3143
+ summary: "Capture a screenshot of the current screen. Use it to check how something LOOKS \u2014 the UI tree carries no colour, contrast, or stacking information, so a screen can pass every check the tree can express and still be visibly broken. Saved to disk; over MCP, pass inline: true to receive the image itself. On a phone or tablet its pixels are the coordinates `tap`/`drag` take, so a point read off it can be tapped as is.",
3075
3144
  mutating: false,
3076
3145
  params: [
3077
3146
  {
@@ -3306,7 +3375,7 @@ var AGENT_COMMANDS = [
3306
3375
  cli: "positional",
3307
3376
  type: "string",
3308
3377
  required: true,
3309
- description: "Text to type into the focused field."
3378
+ description: "Text to type into the focused field. On a phone or tablet a line break presses Enter, which submits the field (bash: $'query\\n'); `press enter` does the same."
3310
3379
  }
3311
3380
  ]
3312
3381
  },
@@ -3332,7 +3401,7 @@ var AGENT_COMMANDS = [
3332
3401
  cli: "positional",
3333
3402
  type: "string",
3334
3403
  required: true,
3335
- description: "Button to press: home, back, lock, siri, dpad_up, dpad_down, dpad_left, dpad_right, or dpad_center."
3404
+ description: "Button to press. iPhone/iPad: home, back, lock, siri. Android: home, back, lock. Apple TV / Android TV: dpad_up, dpad_down, dpad_left, dpad_right, dpad_center, back, home. Roku: up, down, left, right, select, back (dpad_* also work; home is not forwarded)."
3336
3405
  }
3337
3406
  ]
3338
3407
  },
@@ -3340,7 +3409,7 @@ var AGENT_COMMANDS = [
3340
3409
  // tools.py open_url: { url: string }.
3341
3410
  verb: "open-url",
3342
3411
  tool: "open_url",
3343
- summary: "Open a deep link or URL in the app under test.",
3412
+ summary: "Open a deep link or URL in the app under test. The link goes to that app only; one it has no screen for fails rather than opening a browser.",
3344
3413
  mutating: true,
3345
3414
  params: [
3346
3415
  {
@@ -3360,7 +3429,7 @@ var AGENT_COMMANDS = [
3360
3429
  // Foldable devices only.
3361
3430
  verb: "set-posture",
3362
3431
  tool: "set_posture",
3363
- summary: "Fold or unfold a foldable device (iPhone Duo): a preset (closed, partial, open) or an exact hinge angle in degrees (--angle 0-180) \u2014 pass exactly one. Reports the posture it settled in, the angle, and which screen is lit. Errors on a device without a hinge.",
3432
+ summary: "Fold or unfold a foldable device (form factor `foldable` in `list-devices --models` \u2014 the iPhone Duo, Android foldables such as the Pixel 9 Pro Fold): a preset (closed, partial, open) or an exact hinge angle in degrees (--angle 0-180) \u2014 pass exactly one. Reports the posture it settled in, the angle, and which screen is lit. Errors on a device without a hinge.",
3364
3433
  mutating: true,
3365
3434
  params: [
3366
3435
  {
@@ -3386,7 +3455,7 @@ var AGENT_COMMANDS = [
3386
3455
  // portrait <-> landscape and accepts only 90. Refused on TV devices.
3387
3456
  verb: "rotate",
3388
3457
  tool: "rotate",
3389
- summary: "Rotate the device. A phone toggles between portrait and landscape; a foldable (iPhone Duo) turns a quarter clockwise, or by --degrees 180/270. Reports the new orientation. Not available on TV devices.",
3458
+ summary: "Rotate the device. A phone (and an Android foldable) toggles between portrait and landscape; the iPhone Duo turns a quarter clockwise, or by --degrees 180/270. Reports the new orientation. Not available on TV devices.",
3390
3459
  mutating: true,
3391
3460
  params: [
3392
3461
  {
@@ -3394,7 +3463,7 @@ var AGENT_COMMANDS = [
3394
3463
  cli: "--degrees",
3395
3464
  type: "number",
3396
3465
  required: false,
3397
- description: "Foldable only: 90 (default, a quarter turn clockwise), 180, or 270 (a quarter turn back). A phone accepts only 90."
3466
+ description: "iPhone Duo only: 90 (default, a quarter turn clockwise), 180, or 270 (a quarter turn back). Other devices accept only 90."
3398
3467
  }
3399
3468
  ]
3400
3469
  },
@@ -3769,13 +3838,13 @@ async function runAgentVerb(cmd, positionals, opts) {
3769
3838
  } catch (err) {
3770
3839
  const message = err instanceof Error ? err.message : String(err);
3771
3840
  if (opts.json) {
3772
- const code = /^authentication failed/i.test(message) ? "auth_failed" : /^not found:/i.test(message) ? "not_found" : void 0;
3841
+ const code = err instanceof AuthError ? "auth_failed" : err instanceof NotFoundError ? "not_found" : void 0;
3773
3842
  console.log(JSON.stringify({ event: "error", code, message }));
3774
3843
  } else {
3775
3844
  process.stderr.write(`error: ${message}
3776
3845
  `);
3777
3846
  }
3778
- process.exit(1);
3847
+ process.exit(errorExitCode(err));
3779
3848
  }
3780
3849
  if (cmd.verb === "screenshot" && envelope.ok && envelope.result) {
3781
3850
  const outPath = opts.out || defaultScreenshotPath();
@@ -3801,13 +3870,20 @@ async function runAgentVerb(cmd, positionals, opts) {
3801
3870
  console.log(JSON.stringify(envelope));
3802
3871
  } else {
3803
3872
  console.log(renderEnvelope(envelope));
3873
+ if (cmd.verb === "press" && !envelope.ok) process.stderr.write(pressButtonsHelp() + "\n");
3804
3874
  }
3805
3875
  process.exit(agentExitCode(envelope));
3806
3876
  }
3807
3877
 
3808
3878
  export {
3879
+ DEV_PLATFORMS,
3880
+ PLATFORM_NEEDS_METRO,
3881
+ isDevPlatform,
3882
+ platformWithArticle,
3883
+ loadDevConfig,
3809
3884
  parseBuildId,
3810
3885
  checkDevBuild,
3886
+ formatUtcTimestamp,
3811
3887
  buildRows,
3812
3888
  formatBuildRowsHuman,
3813
3889
  listBuilds,
@@ -3815,10 +3891,6 @@ export {
3815
3891
  clearDevState,
3816
3892
  readMetroState,
3817
3893
  killOwnedMetro,
3818
- DEV_PLATFORMS,
3819
- PLATFORM_NEEDS_METRO,
3820
- isDevPlatform,
3821
- loadDevConfig,
3822
3894
  checkMetro,
3823
3895
  buildCommand,
3824
3896
  formatDuration,
@@ -3835,6 +3907,7 @@ export {
3835
3907
  formatDevicesHuman,
3836
3908
  filterDeviceModels,
3837
3909
  formatDeviceModelsHuman,
3910
+ pressButtonsHelp,
3838
3911
  AGENT_COMMANDS,
3839
3912
  renderEnvelope,
3840
3913
  buildToolCall,