vibeview 0.21.0 → 0.22.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,24 @@
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,
9
12
  isDevPlatform,
10
13
  loadDevConfig,
11
14
  readDevState,
12
15
  renderEnvelope,
13
16
  startDevSession
14
- } from "./chunk-VVKF7GGL.js";
17
+ } from "./chunk-SHS5PCSS.js";
15
18
  import {
16
19
  ApiClient,
17
20
  resolveAuth
18
- } from "./chunk-KON2FOEC.js";
21
+ } from "./chunk-UGCGJ6IN.js";
19
22
 
20
23
  // src/mcp/server.ts
21
24
  import { writeFileSync } from "fs";
@@ -26,7 +29,7 @@ var SESSION_ID_PROPERTY = {
26
29
  type: "string",
27
30
  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
31
  };
29
- var MCP_DEV_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
32
+ var MCP_DEV_PLATFORMS = DEV_PLATFORMS;
30
33
  function isMcpDevPlatform(p) {
31
34
  return isDevPlatform(p) && MCP_DEV_PLATFORMS.includes(p);
32
35
  }
@@ -86,9 +89,16 @@ var DEV_STOP_DEF = {
86
89
  properties: { session_id: SESSION_ID_PROPERTY },
87
90
  required: []
88
91
  };
92
+ var DEV_RELOAD_DEF = {
93
+ name: "dev_reload",
94
+ 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.",
95
+ mutating: true,
96
+ properties: { session_id: SESSION_ID_PROPERTY },
97
+ required: []
98
+ };
89
99
  var UPLOAD_APP_DEF = {
90
100
  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`.",
101
+ 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
102
  mutating: true,
93
103
  properties: {
94
104
  file: {
@@ -103,7 +113,13 @@ var UPLOAD_APP_DEF = {
103
113
  required: ["file"]
104
114
  };
105
115
  function buildToolDefinitions() {
106
- return [...AGENT_COMMANDS.map(toolDefFromCommand), DEV_START_DEF, DEV_STOP_DEF, UPLOAD_APP_DEF];
116
+ return [
117
+ ...AGENT_COMMANDS.map(toolDefFromCommand),
118
+ DEV_START_DEF,
119
+ DEV_STOP_DEF,
120
+ DEV_RELOAD_DEF,
121
+ UPLOAD_APP_DEF
122
+ ];
107
123
  }
108
124
  function textResult(text, structuredContent, isError = false) {
109
125
  return { content: [{ type: "text", text }], structuredContent, isError };
@@ -121,6 +137,9 @@ async function handleToolCall(toolName, args, deps) {
121
137
  if (toolName === "dev_stop") {
122
138
  return handleDevStop(deps);
123
139
  }
140
+ if (toolName === "dev_reload") {
141
+ return handleDevReload(args, deps);
142
+ }
124
143
  if (toolName === "upload_app") {
125
144
  return handleUploadApp(args, deps);
126
145
  }
@@ -198,11 +217,6 @@ function optsFor(cmd, args) {
198
217
  }
199
218
  async function handleDevStart(args, deps) {
200
219
  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
220
  if (!isMcpDevPlatform(platform)) {
207
221
  return errorResult(
208
222
  `dev_start: platform must be one of ${MCP_DEV_PLATFORMS.join(", ")}, got "${String(platform)}"`
@@ -221,6 +235,18 @@ ${payload.page_url}`, payload);
221
235
  return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
222
236
  }
223
237
  }
238
+ async function handleDevReload(args, deps) {
239
+ const requested = typeof args.session_id === "string" ? args.session_id : void 0;
240
+ try {
241
+ const { sessionId, seconds } = await deps.devSession.reload(requested);
242
+ return textResult(`channel reloaded in ${seconds.toFixed(1)}s`, {
243
+ session_id: sessionId,
244
+ seconds
245
+ });
246
+ } catch (err) {
247
+ return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
248
+ }
249
+ }
224
250
  async function handleDevStop(deps) {
225
251
  const current = deps.devSession.current();
226
252
  try {
@@ -239,7 +265,7 @@ async function handleUploadApp(args, deps) {
239
265
  }
240
266
  const name = typeof args.name === "string" && args.name ? args.name : void 0;
241
267
  const upload = deps.uploadApp ?? (async (f, n) => {
242
- const { uploadAppBuild } = await import("./upload-app-W6NOSJPA.js");
268
+ const { uploadAppBuild } = await import("./upload-app-LJH3PETS.js");
243
269
  const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
244
270
  return result2;
245
271
  });
@@ -268,6 +294,22 @@ async function handleUploadApp(args, deps) {
268
294
  }
269
295
  function createDevSessionController(auth, webUrl) {
270
296
  let handle = null;
297
+ let platform = null;
298
+ let presence = null;
299
+ let reloading = null;
300
+ const stopPresence = () => {
301
+ if (presence) clearInterval(presence);
302
+ presence = null;
303
+ };
304
+ const readRokuBuild = () => {
305
+ try {
306
+ return loadDevConfig(process.cwd())?.platforms?.roku?.build ?? null;
307
+ } catch (err) {
308
+ throw new Error(
309
+ `vibeview.json could not be read: ${err instanceof Error ? err.message : String(err)}`
310
+ );
311
+ }
312
+ };
271
313
  return {
272
314
  async start(cfg) {
273
315
  if (handle) {
@@ -298,6 +340,7 @@ function createDevSessionController(auth, webUrl) {
298
340
  }
299
341
  }
300
342
  const isTv = PLATFORM_DEVICE[cfg.platform].tv;
343
+ platform = cfg.platform;
301
344
  handle = await startDevSession({
302
345
  token: auth.token,
303
346
  baseUrl: auth.baseUrl,
@@ -308,16 +351,87 @@ function createDevSessionController(auth, webUrl) {
308
351
  metroPort,
309
352
  open: false
310
353
  });
354
+ if (cfg.platform === "roku") {
355
+ const api = new ApiClient(auth.baseUrl, auth.token);
356
+ const sessionId = handle.sessionId;
357
+ presence = setInterval(() => {
358
+ api.touchSession(sessionId).catch(() => {
359
+ });
360
+ }, ROKU_LIVENESS_INTERVAL_MS);
361
+ presence.unref?.();
362
+ }
311
363
  return handle;
312
364
  },
313
365
  async stop() {
366
+ stopPresence();
314
367
  await handle?.stop();
315
368
  handle = null;
369
+ platform = null;
316
370
  },
317
371
  current() {
318
372
  return handle ? { sessionId: handle.sessionId, pageUrl: handle.pageUrl } : null;
373
+ },
374
+ async reload(requested) {
375
+ if (reloading) {
376
+ throw new Error(
377
+ "a reload is already in progress \u2014 wait for it to finish, then call dev_reload again"
378
+ );
379
+ }
380
+ reloading = reloadOnce(requested).finally(() => {
381
+ reloading = null;
382
+ });
383
+ return reloading;
319
384
  }
320
385
  };
386
+ async function reloadOnce(requested) {
387
+ {
388
+ let target;
389
+ if (handle) {
390
+ if (requested && requested !== handle.sessionId) {
391
+ throw new Error(
392
+ `dev_reload can only reload the session this server holds (${handle.sessionId}); got session_id "${requested}". Call dev_stop first, or omit session_id.`
393
+ );
394
+ }
395
+ target = {
396
+ sessionId: handle.sessionId,
397
+ platform: platform ?? "",
398
+ build: platform === "roku" ? readRokuBuild() : null
399
+ };
400
+ } else {
401
+ const state = readDevState(process.cwd());
402
+ if (!state || requested && requested !== state.session_id) {
403
+ throw new Error(
404
+ 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'
405
+ );
406
+ }
407
+ target = { sessionId: state.session_id, platform: state.platform, build: readRokuBuild() };
408
+ }
409
+ if (target.platform !== "roku") {
410
+ throw new Error(
411
+ `dev_reload is for Roku channels; a ${target.platform} session hot-reloads through Metro on its own`
412
+ );
413
+ }
414
+ if (!target.build) {
415
+ throw new Error(
416
+ "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"
417
+ );
418
+ }
419
+ const started = Date.now();
420
+ const push = createRokuPush({
421
+ cwd: process.cwd(),
422
+ sessionId: target.sessionId,
423
+ build: target.build,
424
+ token: auth.token,
425
+ baseUrl: auth.baseUrl,
426
+ api: new ApiClient(auth.baseUrl, auth.token),
427
+ // Build output has no stdout to go to over MCP (stdout IS the
428
+ // protocol); stderr is the server's log.
429
+ log: (line) => console.error(line)
430
+ });
431
+ await push();
432
+ return { sessionId: target.sessionId, seconds: (Date.now() - started) / 1e3 };
433
+ }
434
+ }
321
435
  }
322
436
  function makeSessionResolver(devSession) {
323
437
  return () => devSession.current()?.sessionId ?? readDevState(process.cwd())?.session_id;
@@ -373,6 +487,7 @@ async function runMcpServer(opts) {
373
487
  }
374
488
  export {
375
489
  buildToolDefinitions,
490
+ createDevSessionController,
376
491
  handleToolCall,
377
492
  runMcpServer
378
493
  };
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  uploadApp,
4
4
  uploadAppBuild
5
- } from "./chunk-KON2FOEC.js";
5
+ } from "./chunk-UGCGJ6IN.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.22.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,19 @@ 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
+ - **Roku channels (beta)**: start the session with
160
+ `vibeview dev --platform roku --detach` (or `dev_start` with platform
161
+ `roku` over MCP); after an edit run `vibeview dev-reload` (MCP:
162
+ `dev_reload`) to re-package, upload and restart the channel in the same
163
+ session — about five seconds. The remote's names are `press up/down/left/right/select/back`
164
+ (`dpad_*` also works); `home` is not forwarded. `type` lands in a focused
165
+ on-screen keyboard; the typed text shows up in the tree as the text box's
166
+ child label, which is what the response verifies against. `logs` returns
167
+ the channel's console output (its `print` lines, why it exited, and the
168
+ error plus backtrace after a runtime error); `open-url` is unavailable. A
169
+ Back on the channel's root
170
+ screen exits it; the session relaunches it within seconds, so a fresh
171
+ first screen after `press back` is not a crash.
159
172
 
160
173
  ## 4. When only a human can act — stop and hand off
161
174
 
@@ -219,6 +232,7 @@ Dev-loop lifecycle commands (not registry verbs, but needed for every run):
219
232
  | `dev --detach --json` | Start a device + Metro-tunnel session in the background, emitting `session_ready`/`warning`/`error` JSON events on stdout. |
220
233
  | `dev-status` | Check on the current project's detached dev session. |
221
234
  | `dev-stop` | End the current project's detached dev session. |
235
+ | `dev-reload` | Roku only: re-package, upload and restart the channel in the detached session (the `r` key of the foreground loop). |
222
236
 
223
237
  ## 7. If you're calling VibeView over MCP
224
238