vibeview 0.24.0 → 0.26.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.
@@ -5,22 +5,27 @@ import {
5
5
  PLATFORM_DEVICE,
6
6
  PLATFORM_NEEDS_METRO,
7
7
  ROKU_LIVENESS_INTERVAL_MS,
8
+ buildRows,
8
9
  buildToolCall,
10
+ checkDevBuild,
9
11
  checkMetro,
10
12
  createRokuPush,
11
13
  decodeScreenshotResult,
12
14
  filterDeviceModels,
15
+ formatBuildRowsHuman,
13
16
  formatDeviceModelsHuman,
14
17
  isDevPlatform,
15
18
  loadDevConfig,
19
+ parseBuildId,
20
+ platformWithArticle,
16
21
  readDevState,
17
22
  renderEnvelope,
18
23
  startDevSession
19
- } from "./chunk-MVNMGB6I.js";
24
+ } from "./chunk-JK3T3AMW.js";
20
25
  import {
21
26
  ApiClient,
22
27
  resolveAuth
23
- } from "./chunk-2WNJYNDW.js";
28
+ } from "./chunk-RFFCCPGZ.js";
24
29
 
25
30
  // src/mcp/server.ts
26
31
  import { writeFileSync } from "fs";
@@ -83,14 +88,36 @@ var DEV_START_DEF = {
83
88
  },
84
89
  model: {
85
90
  type: "string",
86
- description: 'Run on this exact device model, e.g. "iPhone Duo" for the foldable. Names come from list_device_models. Omit for a default phone (or TV on TV platforms).'
91
+ description: 'Run on this exact device model, e.g. "iPhone Duo" or "Pixel 9 Pro Fold" for a foldable (any model list_device_models marks form_factor "foldable"). Names come from list_device_models. Omit for a default phone (or TV on TV platforms).'
92
+ },
93
+ build_id: {
94
+ type: "number",
95
+ description: 'Run this uploaded build of the app instead of its newest debug build. Must be a debug build (kind "debug" in list_builds) for the platform \u2014 a release build or one with an embedded JS bundle is refused. Not for roku.'
87
96
  }
88
97
  },
89
98
  required: ["platform"]
90
99
  };
100
+ var LIST_BUILDS_DEF = {
101
+ name: "list_builds",
102
+ description: "List an app's uploaded builds, newest first: id, version, build number, platform, kind (debug/release/unknown), upload time and note. Pass a debug build's id to dev_start as build_id.",
103
+ mutating: false,
104
+ properties: {
105
+ app: { type: "string", description: "App id (public_id, from upload_app or list-apps)." }
106
+ },
107
+ required: ["app"]
108
+ };
109
+ var STOP_SESSION_DEF = {
110
+ name: "stop_session",
111
+ description: "Stop any session by id \u2014 e.g. one created through the API, the sandbox, or a collaboration link. For the session dev_start started, dev_stop does the same without an id.",
112
+ mutating: true,
113
+ properties: {
114
+ session_id: { type: "string", description: "The session to stop." }
115
+ },
116
+ required: ["session_id"]
117
+ };
91
118
  var LIST_DEVICE_MODELS_DEF = {
92
119
  name: "list_device_models",
93
- description: 'List the device models you can start right now (model, OS version, platform, category, and form_factor "foldable" for a device that folds, like the iPhone Duo). Pass a model name to dev_start to run on it.',
120
+ description: 'List the device models you can start right now (model, OS version, platform, category, and form_factor "foldable" for any device with a hinge \u2014 the iPhone Duo, Android foldables such as the Pixel 9 Pro Fold). Pass a model name to dev_start to run on it.',
94
121
  mutating: false,
95
122
  properties: {
96
123
  device_type: {
@@ -138,7 +165,9 @@ function buildToolDefinitions() {
138
165
  DEV_STOP_DEF,
139
166
  DEV_RELOAD_DEF,
140
167
  UPLOAD_APP_DEF,
141
- LIST_DEVICE_MODELS_DEF
168
+ LIST_DEVICE_MODELS_DEF,
169
+ LIST_BUILDS_DEF,
170
+ STOP_SESSION_DEF
142
171
  ];
143
172
  }
144
173
  function textResult(text, structuredContent, isError = false) {
@@ -166,6 +195,12 @@ async function handleToolCall(toolName, args, deps) {
166
195
  if (toolName === "list_device_models") {
167
196
  return handleListDeviceModels(args, deps);
168
197
  }
198
+ if (toolName === "list_builds") {
199
+ return handleListBuilds(args, deps);
200
+ }
201
+ if (toolName === "stop_session") {
202
+ return handleStopSession(args, deps);
203
+ }
169
204
  const cmd = findCommand(toolName);
170
205
  if (!cmd) {
171
206
  return errorResult(`Unknown tool: ${toolName}`);
@@ -245,12 +280,23 @@ async function handleDevStart(args, deps) {
245
280
  `dev_start: platform must be one of ${MCP_DEV_PLATFORMS.join(", ")}, got "${String(platform)}"`
246
281
  );
247
282
  }
283
+ let buildId;
284
+ if (args.build_id !== void 0 && args.build_id !== null) {
285
+ try {
286
+ buildId = parseBuildId(args.build_id);
287
+ } catch {
288
+ return errorResult(
289
+ `dev_start: build_id must be a positive build id (see list_builds), got "${String(args.build_id)}"`
290
+ );
291
+ }
292
+ }
248
293
  try {
249
294
  const result = await deps.devSession.start({
250
295
  platform,
251
296
  app: typeof args.app === "string" ? args.app : void 0,
252
297
  metroPort: typeof args.metro_port === "number" ? args.metro_port : void 0,
253
- model: typeof args.model === "string" && args.model.trim() ? args.model : void 0
298
+ model: typeof args.model === "string" && args.model.trim() ? args.model : void 0,
299
+ ...buildId !== void 0 ? { buildId } : {}
254
300
  });
255
301
  const payload = { session_id: result.sessionId, page_url: result.pageUrl };
256
302
  return textResult(`dev session started: ${payload.session_id}
@@ -271,6 +317,36 @@ async function handleListDeviceModels(args, deps) {
271
317
  }
272
318
  return textResult(formatDeviceModelsHuman(models).trimEnd(), { models });
273
319
  }
320
+ async function handleListBuilds(args, deps) {
321
+ const appId = typeof args.app === "string" ? args.app.trim() : "";
322
+ if (!appId) return errorResult("list_builds: `app` is required \u2014 an app id from list-apps");
323
+ const getApp = deps.getApp ?? ((id) => new ApiClient(deps.auth.baseUrl, deps.auth.token).getApp(id));
324
+ try {
325
+ const app = await getApp(appId);
326
+ const builds = buildRows(app);
327
+ return textResult(formatBuildRowsHuman(app, builds).trimEnd(), {
328
+ app_id: app.public_id,
329
+ builds
330
+ });
331
+ } catch (err) {
332
+ return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
333
+ }
334
+ }
335
+ async function handleStopSession(args, deps) {
336
+ const sessionId = typeof args.session_id === "string" ? args.session_id.trim() : "";
337
+ if (!sessionId) return errorResult("stop_session: `session_id` is required");
338
+ const stop = deps.stopSession ?? ((id) => new ApiClient(deps.auth.baseUrl, deps.auth.token).stopSession(id));
339
+ try {
340
+ if (deps.devSession.current()?.sessionId === sessionId) {
341
+ await deps.devSession.stop();
342
+ } else {
343
+ await stop(sessionId);
344
+ }
345
+ } catch (err) {
346
+ return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
347
+ }
348
+ return textResult(`session stopped: ${sessionId}`, { session_id: sessionId, stopped: true });
349
+ }
274
350
  async function handleDevReload(args, deps) {
275
351
  const requested = typeof args.session_id === "string" ? args.session_id : void 0;
276
352
  try {
@@ -301,7 +377,7 @@ async function handleUploadApp(args, deps) {
301
377
  }
302
378
  const name = typeof args.name === "string" && args.name ? args.name : void 0;
303
379
  const upload = deps.uploadApp ?? (async (f, n) => {
304
- const { uploadAppBuild } = await import("./upload-app-BXXKH65S.js");
380
+ const { uploadAppBuild } = await import("./upload-app-E32EZXYQ.js");
305
381
  const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
306
382
  return result2;
307
383
  });
@@ -381,6 +457,13 @@ function createDevSessionController(auth, webUrl) {
381
457
  );
382
458
  }
383
459
  }
460
+ if (cfg.buildId !== void 0) {
461
+ checkDevBuild(
462
+ await new ApiClient(auth.baseUrl, auth.token).getApp(appId),
463
+ cfg.buildId,
464
+ cfg.platform
465
+ );
466
+ }
384
467
  const isTv = PLATFORM_DEVICE[cfg.platform].tv;
385
468
  platform = cfg.platform;
386
469
  handle = await startDevSession({
@@ -392,6 +475,7 @@ function createDevSessionController(auth, webUrl) {
392
475
  deviceCategory: isTv ? "tv" : entry?.deviceCategory ?? "phone",
393
476
  // A named model decides the category itself (startDevSession).
394
477
  model: cfg.model,
478
+ buildId: cfg.buildId,
395
479
  metroPort,
396
480
  open: false
397
481
  });
@@ -452,7 +536,7 @@ function createDevSessionController(auth, webUrl) {
452
536
  }
453
537
  if (target.platform !== "roku") {
454
538
  throw new Error(
455
- `dev_reload is for Roku channels; a ${target.platform} session hot-reloads through Metro on its own`
539
+ `dev_reload is for Roku channels; ${platformWithArticle(target.platform)} session hot-reloads through Metro on its own`
456
540
  );
457
541
  }
458
542
  if (!target.build) {
@@ -3,7 +3,7 @@ import {
3
3
  describeBuild,
4
4
  uploadApp,
5
5
  uploadAppBuild
6
- } from "./chunk-2WNJYNDW.js";
6
+ } from "./chunk-RFFCCPGZ.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.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "VibeView CLI -- run mobile tests from CI/CD pipelines",
5
5
  "mcpName": "io.github.vibeview/vibeview",
6
6
  "type": "module",
@@ -38,10 +38,17 @@ https://vibeview.io/docs/preparing-your-build.
38
38
  ```
39
39
  2. Start Metro in the React Native project (`yarn start` or equivalent) and
40
40
  leave it running.
41
- 3. Start a dev session in the background and get structured output:
41
+ 3. Start a dev session in the background and get structured output. No
42
+ `vibeview.json` yet in this project (first run here)? Pass `--platform`
43
+ and `--app` explicitly — non-interactive runs never prompt, so both must
44
+ be given up front (`--app` is `app_id` from step 1's upload output):
42
45
  ```bash
43
- vibeview dev --detach --json
46
+ vibeview dev --platform ios --app <app_id> --detach --json
44
47
  ```
48
+ Once `vibeview.json` exists (this run or an earlier one), a bare
49
+ `vibeview dev --detach --json` reruns the same platform/app. A
50
+ `{"event":"error","code":"config_required",...}` line names exactly what's
51
+ still missing (usually `--platform` or `--app`) — pass it and retry.
45
52
  4. Parse the `session_ready` line from stdout:
46
53
  ```json
47
54
  {"event":"session_ready","session_id":"...","url":"...","pid":1234}
@@ -156,20 +163,29 @@ it rather than letting it appear unexplained in their `git status`.
156
163
  (`dpad_up`/`dpad_down`/`dpad_left`/`dpad_right`/`dpad_center`), or move
157
164
  focus directly with `tap-focused` (moves focus AND activates) / `focus`
158
165
  (moves focus only, no activation).
159
- - **Foldable (iPhone Duo)**: start on it by model name —
160
- `vibeview dev --detach --json --model "iPhone Duo"` (MCP: `dev_start` with
161
- `model`). Exact names come from `vibeview list-devices --models` (MCP:
162
- `list_device_models`); a foldable is marked `foldable`. It starts closed, on
163
- the cover screen. Fold with a preset or an exact hinge angle, and rotate:
166
+ - **Foldables**: any model `vibeview list-devices --models` (MCP:
167
+ `list_device_models`) marks form factor `foldable` — the iPhone Duo,
168
+ Android foldables such as the Pixel 9 Pro Fold. Start on one by its exact
169
+ model name from that list, e.g.
170
+ `vibeview dev --detach --json --model "iPhone Duo"` or
171
+ `vibeview dev --platform android --detach --json --model "Pixel 9 Pro Fold"`
172
+ (MCP: `dev_start` with `model`). It starts closed, on the cover screen. Fold with a preset
173
+ or an exact hinge angle, and rotate:
164
174
  ```bash
165
175
  vibeview set-posture open --session <id> # closed | partial | open
166
176
  vibeview set-posture --angle 75 --session <id> # 0 (shut) to 180 (flat)
167
- vibeview rotate --session <id> # quarter turn clockwise
168
- vibeview rotate --degrees 270 --session <id> # quarter turn back (180: half)
177
+ vibeview rotate --session <id> # Duo: quarter turn clockwise
178
+ vibeview rotate --degrees 270 --session <id> # Duo: quarter turn back (180: half)
169
179
  ```
170
180
  Each response says where the device ended up — posture, angle and which
171
- screen is lit (`cover` or `inner`; the device decides — a small angle can
172
- keep the inner screen lit on the way down), or the new orientation. Folding
181
+ screen is lit (`cover` or `inner`; the device decides — on the Duo a small
182
+ angle can keep the inner screen lit on the way down), or the new
183
+ orientation. The posture names come from each device's own angle ranges,
184
+ so trust the reported `posture` over the angle you asked for: the Duo
185
+ reads 120-169 as `partial`, 170 up as `open`, and 1-119 by direction of
186
+ travel; an Android foldable uses its own ranges (the Pixel 9 Pro Fold: below
187
+ 30 `closed`, 30-149 `partial`, 150 up `open`) and its inner screen whenever
188
+ it is not closed. An Android foldable rotates like a phone. Folding
173
189
  switches screens of different sizes and rotating turns the picture, so run
174
190
  `ui-tree` afterwards — old refs are stale. On other phones and tablets
175
191
  `rotate` toggles portrait/landscape (only 90 is accepted); TV devices don't
@@ -214,6 +230,11 @@ streaming minutes even when idle:
214
230
  ```bash
215
231
  vibeview dev-stop
216
232
  ```
233
+ A session you did not start with `dev` (one created through the API, the
234
+ sandbox, or a collaboration link) is stopped by id:
235
+ ```bash
236
+ vibeview stop <session-id>
237
+ ```
217
238
 
218
239
  ## 6. Command reference
219
240
 
@@ -238,8 +259,8 @@ human-readable text.
238
259
  | `clear-text` | — | Clear the text in the currently focused input field. |
239
260
  | `press` | `<button>` | Press a device button or perform a system gesture (home, back, d-pad, etc). |
240
261
  | `open-url` | `<url>` | Open a deep link or URL in the app under test. |
241
- | `set-posture` | `<closed\|partial\|open>` or `--angle <0-180>` | Fold or unfold a foldable device (iPhone Duo) to a preset or an exact hinge angle — exactly one. Reports posture, angle and lit screen. Errors on a device without a hinge. |
242
- | `rotate` | `[--degrees <90\|180\|270>]` | Rotate the device and report the new orientation. Phones/tablets toggle portrait/landscape (90 only); a foldable turns a quarter clockwise by default. Not on TV. |
262
+ | `set-posture` | `<closed\|partial\|open>` or `--angle <0-180>` | Fold or unfold a foldable device (form factor `foldable`: the iPhone Duo, Android foldables) to a preset or an exact hinge angle — exactly one. Reports posture, angle and lit screen. Errors on a device without a hinge. |
263
+ | `rotate` | `[--degrees <90\|180\|270>]` | Rotate the device and report the new orientation. Phones/tablets and Android foldables toggle portrait/landscape (90 only); the iPhone Duo turns a quarter clockwise by default. Not on TV. |
243
264
  | `wait` | `[--ms <n>]` | Wait for a specified number of seconds before continuing (default: 2s). |
244
265
  | `find` | `<text>` `[--below <text>] [--above <text>] [--near <text>]` | Find an element by text with optional spatial constraints. |
245
266
  | `tap-focused` | `<ref>` | TV: move focus to an element and press SELECT in one step. |
@@ -250,9 +271,12 @@ Dev-loop lifecycle commands (not registry verbs, but needed for every run):
250
271
  | Command | Purpose |
251
272
  |---------|---------|
252
273
  | `dev --detach --json` | Start a device + Metro-tunnel session in the background, emitting `session_ready`/`warning`/`error` JSON events on stdout. Add `--model "<name>"` to pick an exact device model. |
274
+ | `dev --build <id>` | Run that uploaded build instead of the app's newest debug build (combine with `--app`, `--platform`, `--model`, `--detach --json`). Only a debug build (kind `debug` in `list-builds`) is accepted; a release build or one with an embedded JS bundle is refused. Not on Roku. |
275
+ | `list-builds <app>` | List an app's uploaded builds, newest first: id, version (build number), platform, kind (`debug`/`release`/`unknown`), upload time, note. `--json` prints an array. |
253
276
  | `list-devices --models` | List the device models you can start (the names `--model` takes); foldables are marked `foldable`. |
254
277
  | `dev-status` | Check on the current project's detached dev session. |
255
278
  | `dev-stop` | End the current project's detached dev session. |
279
+ | `vibeview stop <session-id>` | End any session by id — one created through the API, the sandbox, or a collaboration link. |
256
280
  | `dev-reload` | Roku only: re-package, upload and restart the channel in the detached session (the `r` key of the foreground loop). |
257
281
 
258
282
  ## 7. If you're calling VibeView over MCP
@@ -271,9 +295,11 @@ steps the CLI does with plain shell commands:
271
295
  | MCP tool | Args | Purpose |
272
296
  |----------|------|---------|
273
297
  | `upload_app` | `file` (required path), `name` | Upload a debug build. The MCP equivalent of `vibeview upload-app`, so step 1 of the loop needs no shell. Returns the `app_id` to pass to `dev_start`. |
274
- | `dev_start` | `platform` (required: `ios`/`android`/`tvos`/`androidtv`/`roku`), `app`, `metro_port`, `model` | Start a dev-loop session held open by the MCP server. The MCP equivalent of `vibeview dev --detach`; `model` picks an exact device model (e.g. `"iPhone Duo"`). Returns `page_url` — relay it to the developer immediately, same as step 5 of the loop. |
298
+ | `dev_start` | `platform` (required: `ios`/`android`/`tvos`/`androidtv`/`roku`), `app`, `metro_port`, `model`, `build_id` | Start a dev-loop session held open by the MCP server. The MCP equivalent of `vibeview dev --detach`; `model` picks an exact device model (e.g. `"iPhone Duo"`); `build_id` runs that debug build instead of the newest one (see `list_builds`). Returns `page_url` — relay it to the developer immediately, same as step 5 of the loop. |
275
299
  | `list_device_models` | `device_type` | List the device models you can start (model, OS, platform, category, `foldable` marker). The MCP equivalent of `vibeview list-devices --models`. |
300
+ | `list_builds` | `app` (required) | List an app's uploaded builds with their kind (`debug`/`release`/`unknown`). The MCP equivalent of `vibeview list-builds`. |
276
301
  | `dev_stop` | — | Stop the session `dev_start` started. The MCP equivalent of `vibeview dev-stop`. |
302
+ | `stop_session` | `session_id` (required) | Stop any session by id. The MCP equivalent of `vibeview stop <session-id>`. |
277
303
 
278
304
  **`dev_start` is stateful — it changes the default session for every later
279
305
  call.** Once it succeeds, any tool called without an explicit `session_id`