vibeview 0.21.0 → 0.23.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,21 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  AGENT_COMMANDS,
4
+ DEV_PLATFORMS,
4
5
  PLATFORM_DEVICE,
5
6
  PLATFORM_NEEDS_METRO,
7
+ ROKU_LIVENESS_INTERVAL_MS,
6
8
  buildToolCall,
7
9
  checkMetro,
10
+ createRokuPush,
8
11
  decodeScreenshotResult,
12
+ filterDeviceModels,
13
+ formatDeviceModelsHuman,
9
14
  isDevPlatform,
10
15
  loadDevConfig,
11
16
  readDevState,
12
17
  renderEnvelope,
13
18
  startDevSession
14
- } from "./chunk-VVKF7GGL.js";
19
+ } from "./chunk-E2LZSLRF.js";
15
20
  import {
16
21
  ApiClient,
17
22
  resolveAuth
18
- } from "./chunk-KON2FOEC.js";
23
+ } from "./chunk-PSFXVUHG.js";
19
24
 
20
25
  // src/mcp/server.ts
21
26
  import { writeFileSync } from "fs";
@@ -26,7 +31,7 @@ var SESSION_ID_PROPERTY = {
26
31
  type: "string",
27
32
  description: "Session ID to target. Optional: falls back to the MCP-server-held dev_start session, then this project's `vibeview dev --detach` state, if either is available."
28
33
  };
29
- var MCP_DEV_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
34
+ var MCP_DEV_PLATFORMS = DEV_PLATFORMS;
30
35
  function isMcpDevPlatform(p) {
31
36
  return isDevPlatform(p) && MCP_DEV_PLATFORMS.includes(p);
32
37
  }
@@ -75,10 +80,27 @@ var DEV_START_DEF = {
75
80
  metro_port: {
76
81
  type: "number",
77
82
  description: "Local Metro port (default: 8081, or vibeview.json)."
83
+ },
84
+ model: {
85
+ 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).'
78
87
  }
79
88
  },
80
89
  required: ["platform"]
81
90
  };
91
+ var LIST_DEVICE_MODELS_DEF = {
92
+ 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.',
94
+ mutating: false,
95
+ properties: {
96
+ device_type: {
97
+ type: "string",
98
+ description: "Only models of this device type.",
99
+ enum: ["ios", "android", "roku"]
100
+ }
101
+ },
102
+ required: []
103
+ };
82
104
  var DEV_STOP_DEF = {
83
105
  name: "dev_stop",
84
106
  description: "Stop the dev-loop session started by dev_start.",
@@ -86,9 +108,16 @@ var DEV_STOP_DEF = {
86
108
  properties: { session_id: SESSION_ID_PROPERTY },
87
109
  required: []
88
110
  };
111
+ var DEV_RELOAD_DEF = {
112
+ name: "dev_reload",
113
+ description: "Roku only: re-package the channel with the build command from vibeview.json (platforms.roku.build), upload it, and reload it into the session dev_start is holding (or, with no held session, this project's `vibeview dev --detach` session) \u2014 the MCP equivalent of pressing `r` in `vibeview dev`. Returns once the device restarts the channel (a few seconds); call ui_tree afterwards. Other platforms hot-reload through Metro and need no call.",
114
+ mutating: true,
115
+ properties: { session_id: SESSION_ID_PROPERTY },
116
+ required: []
117
+ };
89
118
  var UPLOAD_APP_DEF = {
90
119
  name: "upload_app",
91
- description: "Upload an app build (zipped simulator .app for iOS, .apk for Android) and return its app_id for dev_start. The MCP equivalent of `vibeview upload-app`.",
120
+ description: "Upload an app build (zipped simulator .app for iOS/tvOS, .apk for Android/Android TV, or a Roku channel .zip) and return its app_id for dev_start. The MCP equivalent of `vibeview upload-app`.",
92
121
  mutating: true,
93
122
  properties: {
94
123
  file: {
@@ -103,7 +132,14 @@ var UPLOAD_APP_DEF = {
103
132
  required: ["file"]
104
133
  };
105
134
  function buildToolDefinitions() {
106
- return [...AGENT_COMMANDS.map(toolDefFromCommand), DEV_START_DEF, DEV_STOP_DEF, UPLOAD_APP_DEF];
135
+ return [
136
+ ...AGENT_COMMANDS.map(toolDefFromCommand),
137
+ DEV_START_DEF,
138
+ DEV_STOP_DEF,
139
+ DEV_RELOAD_DEF,
140
+ UPLOAD_APP_DEF,
141
+ LIST_DEVICE_MODELS_DEF
142
+ ];
107
143
  }
108
144
  function textResult(text, structuredContent, isError = false) {
109
145
  return { content: [{ type: "text", text }], structuredContent, isError };
@@ -121,9 +157,15 @@ async function handleToolCall(toolName, args, deps) {
121
157
  if (toolName === "dev_stop") {
122
158
  return handleDevStop(deps);
123
159
  }
160
+ if (toolName === "dev_reload") {
161
+ return handleDevReload(args, deps);
162
+ }
124
163
  if (toolName === "upload_app") {
125
164
  return handleUploadApp(args, deps);
126
165
  }
166
+ if (toolName === "list_device_models") {
167
+ return handleListDeviceModels(args, deps);
168
+ }
127
169
  const cmd = findCommand(toolName);
128
170
  if (!cmd) {
129
171
  return errorResult(`Unknown tool: ${toolName}`);
@@ -198,11 +240,6 @@ function optsFor(cmd, args) {
198
240
  }
199
241
  async function handleDevStart(args, deps) {
200
242
  const platform = args.platform;
201
- if (platform === "roku") {
202
- return errorResult(
203
- "dev_start does not support roku \u2014 the reload loop needs a foreground terminal; run `vibeview dev --platform roku` instead"
204
- );
205
- }
206
243
  if (!isMcpDevPlatform(platform)) {
207
244
  return errorResult(
208
245
  `dev_start: platform must be one of ${MCP_DEV_PLATFORMS.join(", ")}, got "${String(platform)}"`
@@ -212,7 +249,8 @@ async function handleDevStart(args, deps) {
212
249
  const result = await deps.devSession.start({
213
250
  platform,
214
251
  app: typeof args.app === "string" ? args.app : void 0,
215
- metroPort: typeof args.metro_port === "number" ? args.metro_port : void 0
252
+ metroPort: typeof args.metro_port === "number" ? args.metro_port : void 0,
253
+ model: typeof args.model === "string" && args.model.trim() ? args.model : void 0
216
254
  });
217
255
  const payload = { session_id: result.sessionId, page_url: result.pageUrl };
218
256
  return textResult(`dev session started: ${payload.session_id}
@@ -221,6 +259,30 @@ ${payload.page_url}`, payload);
221
259
  return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
222
260
  }
223
261
  }
262
+ async function handleListDeviceModels(args, deps) {
263
+ const list = deps.listDeviceModels ?? (() => new ApiClient(deps.auth.baseUrl, deps.auth.token).listDeviceModels());
264
+ let models;
265
+ try {
266
+ models = filterDeviceModels(await list(), {
267
+ type: typeof args.device_type === "string" ? args.device_type : void 0
268
+ });
269
+ } catch (err) {
270
+ return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
271
+ }
272
+ return textResult(formatDeviceModelsHuman(models).trimEnd(), { models });
273
+ }
274
+ async function handleDevReload(args, deps) {
275
+ const requested = typeof args.session_id === "string" ? args.session_id : void 0;
276
+ try {
277
+ const { sessionId, seconds } = await deps.devSession.reload(requested);
278
+ return textResult(`channel reloaded in ${seconds.toFixed(1)}s`, {
279
+ session_id: sessionId,
280
+ seconds
281
+ });
282
+ } catch (err) {
283
+ return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
284
+ }
285
+ }
224
286
  async function handleDevStop(deps) {
225
287
  const current = deps.devSession.current();
226
288
  try {
@@ -239,7 +301,7 @@ async function handleUploadApp(args, deps) {
239
301
  }
240
302
  const name = typeof args.name === "string" && args.name ? args.name : void 0;
241
303
  const upload = deps.uploadApp ?? (async (f, n) => {
242
- const { uploadAppBuild } = await import("./upload-app-W6NOSJPA.js");
304
+ const { uploadAppBuild } = await import("./upload-app-ZAKAOJKE.js");
243
305
  const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
244
306
  return result2;
245
307
  });
@@ -268,6 +330,22 @@ async function handleUploadApp(args, deps) {
268
330
  }
269
331
  function createDevSessionController(auth, webUrl) {
270
332
  let handle = null;
333
+ let platform = null;
334
+ let presence = null;
335
+ let reloading = null;
336
+ const stopPresence = () => {
337
+ if (presence) clearInterval(presence);
338
+ presence = null;
339
+ };
340
+ const readRokuBuild = () => {
341
+ try {
342
+ return loadDevConfig(process.cwd())?.platforms?.roku?.build ?? null;
343
+ } catch (err) {
344
+ throw new Error(
345
+ `vibeview.json could not be read: ${err instanceof Error ? err.message : String(err)}`
346
+ );
347
+ }
348
+ };
271
349
  return {
272
350
  async start(cfg) {
273
351
  if (handle) {
@@ -298,6 +376,7 @@ function createDevSessionController(auth, webUrl) {
298
376
  }
299
377
  }
300
378
  const isTv = PLATFORM_DEVICE[cfg.platform].tv;
379
+ platform = cfg.platform;
301
380
  handle = await startDevSession({
302
381
  token: auth.token,
303
382
  baseUrl: auth.baseUrl,
@@ -305,19 +384,92 @@ function createDevSessionController(auth, webUrl) {
305
384
  platform: cfg.platform,
306
385
  appId,
307
386
  deviceCategory: isTv ? "tv" : entry?.deviceCategory ?? "phone",
387
+ // A named model decides the category itself (startDevSession).
388
+ model: cfg.model,
308
389
  metroPort,
309
390
  open: false
310
391
  });
392
+ if (cfg.platform === "roku") {
393
+ const api = new ApiClient(auth.baseUrl, auth.token);
394
+ const sessionId = handle.sessionId;
395
+ presence = setInterval(() => {
396
+ api.touchSession(sessionId).catch(() => {
397
+ });
398
+ }, ROKU_LIVENESS_INTERVAL_MS);
399
+ presence.unref?.();
400
+ }
311
401
  return handle;
312
402
  },
313
403
  async stop() {
404
+ stopPresence();
314
405
  await handle?.stop();
315
406
  handle = null;
407
+ platform = null;
316
408
  },
317
409
  current() {
318
410
  return handle ? { sessionId: handle.sessionId, pageUrl: handle.pageUrl } : null;
411
+ },
412
+ async reload(requested) {
413
+ if (reloading) {
414
+ throw new Error(
415
+ "a reload is already in progress \u2014 wait for it to finish, then call dev_reload again"
416
+ );
417
+ }
418
+ reloading = reloadOnce(requested).finally(() => {
419
+ reloading = null;
420
+ });
421
+ return reloading;
319
422
  }
320
423
  };
424
+ async function reloadOnce(requested) {
425
+ {
426
+ let target;
427
+ if (handle) {
428
+ if (requested && requested !== handle.sessionId) {
429
+ throw new Error(
430
+ `dev_reload can only reload the session this server holds (${handle.sessionId}); got session_id "${requested}". Call dev_stop first, or omit session_id.`
431
+ );
432
+ }
433
+ target = {
434
+ sessionId: handle.sessionId,
435
+ platform: platform ?? "",
436
+ build: platform === "roku" ? readRokuBuild() : null
437
+ };
438
+ } else {
439
+ const state = readDevState(process.cwd());
440
+ if (!state || requested && requested !== state.session_id) {
441
+ throw new Error(
442
+ requested ? `session "${requested}" is neither held by dev_start nor this project's \`vibeview dev --detach\` session \u2014 dev_reload needs one of those two` : 'no dev session to reload \u2014 call dev_start with platform "roku" first'
443
+ );
444
+ }
445
+ target = { sessionId: state.session_id, platform: state.platform, build: readRokuBuild() };
446
+ }
447
+ if (target.platform !== "roku") {
448
+ throw new Error(
449
+ `dev_reload is for Roku channels; a ${target.platform} session hot-reloads through Metro on its own`
450
+ );
451
+ }
452
+ if (!target.build) {
453
+ throw new Error(
454
+ "no build config for roku \u2014 add platforms.roku.build ({ command, artifact }) to vibeview.json, or run `vibeview build --platform roku` once to set it up"
455
+ );
456
+ }
457
+ const started = Date.now();
458
+ const push = createRokuPush({
459
+ cwd: process.cwd(),
460
+ sessionId: target.sessionId,
461
+ build: target.build,
462
+ token: auth.token,
463
+ baseUrl: auth.baseUrl,
464
+ api: new ApiClient(auth.baseUrl, auth.token),
465
+ // Build output has no stdout to go to over MCP (stdout IS the
466
+ // protocol); stderr is the server's log.
467
+ log: (line) => console.error(line)
468
+ });
469
+ await push();
470
+ return { sessionId: target.sessionId, seconds: (Date.now() - started) / 1e3 };
471
+ }
472
+ }
321
473
  }
322
474
  function makeSessionResolver(devSession) {
323
475
  return () => devSession.current()?.sessionId ?? readDevState(process.cwd())?.session_id;
@@ -373,6 +525,7 @@ async function runMcpServer(opts) {
373
525
  }
374
526
  export {
375
527
  buildToolDefinitions,
528
+ createDevSessionController,
376
529
  handleToolCall,
377
530
  runMcpServer
378
531
  };
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  uploadApp,
4
4
  uploadAppBuild
5
- } from "./chunk-KON2FOEC.js";
5
+ } from "./chunk-PSFXVUHG.js";
6
6
  export {
7
7
  uploadApp,
8
8
  uploadAppBuild
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibeview",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "VibeView CLI -- run mobile tests from CI/CD pipelines",
5
5
  "mcpName": "io.github.vibeview/vibeview",
6
6
  "type": "module",
@@ -156,6 +156,37 @@ it rather than letting it appear unexplained in their `git status`.
156
156
  (`dpad_up`/`dpad_down`/`dpad_left`/`dpad_right`/`dpad_center`), or move
157
157
  focus directly with `tap-focused` (moves focus AND activates) / `focus`
158
158
  (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:
164
+ ```bash
165
+ vibeview set-posture open --session <id> # closed | partial | open
166
+ 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)
169
+ ```
170
+ 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
173
+ switches screens of different sizes and rotating turns the picture, so run
174
+ `ui-tree` afterwards — old refs are stale. On other phones and tablets
175
+ `rotate` toggles portrait/landscape (only 90 is accepted); TV devices don't
176
+ rotate.
177
+ - **Roku channels (beta)**: start the session with
178
+ `vibeview dev --platform roku --detach` (or `dev_start` with platform
179
+ `roku` over MCP); after an edit run `vibeview dev-reload` (MCP:
180
+ `dev_reload`) to re-package, upload and restart the channel in the same
181
+ session — about five seconds. The remote's names are `press up/down/left/right/select/back`
182
+ (`dpad_*` also works); `home` is not forwarded. `type` lands in a focused
183
+ on-screen keyboard; the typed text shows up in the tree as the text box's
184
+ child label, which is what the response verifies against. `logs` returns
185
+ the channel's console output (its `print` lines, why it exited, and the
186
+ error plus backtrace after a runtime error); `open-url` is unavailable. A
187
+ Back on the channel's root
188
+ screen exits it; the session relaunches it within seconds, so a fresh
189
+ first screen after `press back` is not a crash.
159
190
 
160
191
  ## 4. When only a human can act — stop and hand off
161
192
 
@@ -207,6 +238,8 @@ human-readable text.
207
238
  | `clear-text` | — | Clear the text in the currently focused input field. |
208
239
  | `press` | `<button>` | Press a device button or perform a system gesture (home, back, d-pad, etc). |
209
240
  | `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. |
210
243
  | `wait` | `[--ms <n>]` | Wait for a specified number of seconds before continuing (default: 2s). |
211
244
  | `find` | `<text>` `[--below <text>] [--above <text>] [--near <text>]` | Find an element by text with optional spatial constraints. |
212
245
  | `tap-focused` | `<ref>` | TV: move focus to an element and press SELECT in one step. |
@@ -216,9 +249,11 @@ Dev-loop lifecycle commands (not registry verbs, but needed for every run):
216
249
 
217
250
  | Command | Purpose |
218
251
  |---------|---------|
219
- | `dev --detach --json` | Start a device + Metro-tunnel session in the background, emitting `session_ready`/`warning`/`error` JSON events on stdout. |
252
+ | `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. |
253
+ | `list-devices --models` | List the device models you can start (the names `--model` takes); foldables are marked `foldable`. |
220
254
  | `dev-status` | Check on the current project's detached dev session. |
221
255
  | `dev-stop` | End the current project's detached dev session. |
256
+ | `dev-reload` | Roku only: re-package, upload and restart the channel in the detached session (the `r` key of the foreground loop). |
222
257
 
223
258
  ## 7. If you're calling VibeView over MCP
224
259
 
@@ -230,13 +265,14 @@ hyphens replaced by underscores (`ui_tree`, `tap`, `scroll_to`, `long_press`,
230
265
  `drag {from: "@e5", to: "300,400"}`, `alert {action: "get"}`) plus an
231
266
  optional `session_id`.
232
267
 
233
- Three tools exist **only** over MCP, with no registry verb — they cover the
268
+ These tools exist **only** over MCP, with no registry verb — they cover the
234
269
  steps the CLI does with plain shell commands:
235
270
 
236
271
  | MCP tool | Args | Purpose |
237
272
  |----------|------|---------|
238
273
  | `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`. |
239
- | `dev_start` | `platform` (required: `ios`/`android`/`tvos`/`androidtv`), `app`, `metro_port` | Start a dev-loop session held open by the MCP server. The MCP equivalent of `vibeview dev --detach`. Returns `page_url` — relay it to the developer immediately, same as step 5 of the loop. |
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. |
275
+ | `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`. |
240
276
  | `dev_stop` | — | Stop the session `dev_start` started. The MCP equivalent of `vibeview dev-stop`. |
241
277
 
242
278
  **`dev_start` is stateful — it changes the default session for every later