vibeview 0.7.1 → 0.8.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.
- package/README.md +31 -0
- package/dist/chunk-NZ4G3JKV.js +1990 -0
- package/dist/chunk-YD2X6TQV.js +304 -0
- package/dist/cli.js +147 -722
- package/dist/server-JZAEEHZT.js +367 -0
- package/dist/upload-app-GAZVQZYR.js +9 -0
- package/package.json +9 -7
- package/skills/vibeview-agent/SKILL.md +205 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
AGENT_COMMANDS,
|
|
4
|
+
PLATFORM_DEVICE,
|
|
5
|
+
buildToolCall,
|
|
6
|
+
checkMetro,
|
|
7
|
+
decodeScreenshotResult,
|
|
8
|
+
isDevPlatform,
|
|
9
|
+
loadDevConfig,
|
|
10
|
+
readDevState,
|
|
11
|
+
renderEnvelope,
|
|
12
|
+
startDevSession
|
|
13
|
+
} from "./chunk-NZ4G3JKV.js";
|
|
14
|
+
import {
|
|
15
|
+
ApiClient,
|
|
16
|
+
resolveAuth
|
|
17
|
+
} from "./chunk-YD2X6TQV.js";
|
|
18
|
+
|
|
19
|
+
// src/mcp/server.ts
|
|
20
|
+
import { writeFileSync } from "fs";
|
|
21
|
+
import { tmpdir } from "os";
|
|
22
|
+
import { join } from "path";
|
|
23
|
+
import chalk from "chalk";
|
|
24
|
+
var SESSION_ID_PROPERTY = {
|
|
25
|
+
type: "string",
|
|
26
|
+
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."
|
|
27
|
+
};
|
|
28
|
+
var DEV_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
|
|
29
|
+
function paramToProperty(param) {
|
|
30
|
+
return { type: param.type, description: param.description };
|
|
31
|
+
}
|
|
32
|
+
function toolNameFor(verb) {
|
|
33
|
+
return verb.replace(/-/g, "_");
|
|
34
|
+
}
|
|
35
|
+
function toolDefFromCommand(cmd) {
|
|
36
|
+
const properties = { session_id: SESSION_ID_PROPERTY };
|
|
37
|
+
const required = [];
|
|
38
|
+
for (const param of cmd.params) {
|
|
39
|
+
properties[param.name] = paramToProperty(param);
|
|
40
|
+
if (param.required) required.push(param.name);
|
|
41
|
+
}
|
|
42
|
+
if (cmd.verb === "screenshot") {
|
|
43
|
+
properties.inline = {
|
|
44
|
+
type: "boolean",
|
|
45
|
+
description: "Return the image itself so you can see it, not just the file path. Set this to true whenever you need to judge how the screen LOOKS, or whenever you cannot open files on the machine running this server \u2014 otherwise the screenshot is only written to disk and you never see it. It costs a lot of context, so skip it when you only need to confirm an action happened (the response already tells you what changed)."
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
name: toolNameFor(cmd.verb),
|
|
50
|
+
description: cmd.summary,
|
|
51
|
+
mutating: cmd.mutating,
|
|
52
|
+
properties,
|
|
53
|
+
required
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
var DEV_START_DEF = {
|
|
57
|
+
name: "dev_start",
|
|
58
|
+
description: "Start a VibeView dev-loop session (device + tunnel) held in-process by this MCP server, and use it as the default session for every other tool call until dev_stop.",
|
|
59
|
+
mutating: true,
|
|
60
|
+
properties: {
|
|
61
|
+
session_id: SESSION_ID_PROPERTY,
|
|
62
|
+
platform: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "Platform to run on.",
|
|
65
|
+
enum: [...DEV_PLATFORMS]
|
|
66
|
+
},
|
|
67
|
+
app: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Uploaded debug build (app_id) to run. Defaults to vibeview.json config."
|
|
70
|
+
},
|
|
71
|
+
metro_port: {
|
|
72
|
+
type: "number",
|
|
73
|
+
description: "Local Metro port (default: 8081, or vibeview.json)."
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
required: ["platform"]
|
|
77
|
+
};
|
|
78
|
+
var DEV_STOP_DEF = {
|
|
79
|
+
name: "dev_stop",
|
|
80
|
+
description: "Stop the dev-loop session started by dev_start.",
|
|
81
|
+
mutating: true,
|
|
82
|
+
properties: { session_id: SESSION_ID_PROPERTY },
|
|
83
|
+
required: []
|
|
84
|
+
};
|
|
85
|
+
var UPLOAD_APP_DEF = {
|
|
86
|
+
name: "upload_app",
|
|
87
|
+
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`.",
|
|
88
|
+
mutating: true,
|
|
89
|
+
properties: {
|
|
90
|
+
file: {
|
|
91
|
+
type: "string",
|
|
92
|
+
description: "Path to the build file on this machine (.apk, or a zipped iOS simulator .app)."
|
|
93
|
+
},
|
|
94
|
+
name: {
|
|
95
|
+
type: "string",
|
|
96
|
+
description: "App name (auto-detected from the build if omitted)."
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
required: ["file"]
|
|
100
|
+
};
|
|
101
|
+
function buildToolDefinitions() {
|
|
102
|
+
return [...AGENT_COMMANDS.map(toolDefFromCommand), DEV_START_DEF, DEV_STOP_DEF, UPLOAD_APP_DEF];
|
|
103
|
+
}
|
|
104
|
+
function textResult(text, structuredContent, isError = false) {
|
|
105
|
+
return { content: [{ type: "text", text }], structuredContent, isError };
|
|
106
|
+
}
|
|
107
|
+
function errorResult(message) {
|
|
108
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
109
|
+
}
|
|
110
|
+
function findCommand(toolName) {
|
|
111
|
+
return AGENT_COMMANDS.find((c) => toolNameFor(c.verb) === toolName);
|
|
112
|
+
}
|
|
113
|
+
async function handleToolCall(toolName, args, deps) {
|
|
114
|
+
if (toolName === "dev_start") {
|
|
115
|
+
return handleDevStart(args, deps);
|
|
116
|
+
}
|
|
117
|
+
if (toolName === "dev_stop") {
|
|
118
|
+
return handleDevStop(deps);
|
|
119
|
+
}
|
|
120
|
+
if (toolName === "upload_app") {
|
|
121
|
+
return handleUploadApp(args, deps);
|
|
122
|
+
}
|
|
123
|
+
const cmd = findCommand(toolName);
|
|
124
|
+
if (!cmd) {
|
|
125
|
+
return errorResult(`Unknown tool: ${toolName}`);
|
|
126
|
+
}
|
|
127
|
+
const sessionId = typeof args.session_id === "string" ? args.session_id : deps.resolveSessionId();
|
|
128
|
+
if (!sessionId) {
|
|
129
|
+
return errorResult(
|
|
130
|
+
"No session to target. Call dev_start first, or pass session_id (e.g. from a running `vibeview dev --detach`)."
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
let call;
|
|
134
|
+
try {
|
|
135
|
+
const { session_id: _sessionId, ...rest } = args;
|
|
136
|
+
const positionals = positionalsFor(cmd, rest);
|
|
137
|
+
const opts = optsFor(cmd, rest);
|
|
138
|
+
call = buildToolCall(cmd, positionals, opts);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
141
|
+
}
|
|
142
|
+
let envelope;
|
|
143
|
+
try {
|
|
144
|
+
envelope = await deps.agentTool(sessionId, call.tool, call.args);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
147
|
+
}
|
|
148
|
+
if (cmd.verb === "screenshot" && envelope.ok) {
|
|
149
|
+
return screenshotToolResult(envelope, args);
|
|
150
|
+
}
|
|
151
|
+
return textResult(renderEnvelope(envelope), envelope, !envelope.ok);
|
|
152
|
+
}
|
|
153
|
+
function screenshotToolResult(envelope, args) {
|
|
154
|
+
let imageBytes;
|
|
155
|
+
try {
|
|
156
|
+
imageBytes = decodeScreenshotResult(envelope.result);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
159
|
+
}
|
|
160
|
+
const outPath = typeof args.out === "string" && args.out ? args.out : join(tmpdir(), `vibeview-screenshot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.png`);
|
|
161
|
+
try {
|
|
162
|
+
writeFileSync(outPath, imageBytes);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
return errorResult(
|
|
165
|
+
`error: could not write screenshot to ${outPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const sanitized = {
|
|
169
|
+
...envelope,
|
|
170
|
+
result: { written_to: outPath, bytes: imageBytes.length }
|
|
171
|
+
};
|
|
172
|
+
const content = [
|
|
173
|
+
{ type: "text", text: `${renderEnvelope(sanitized)}
|
|
174
|
+
screenshot written to ${outPath}` }
|
|
175
|
+
];
|
|
176
|
+
if (args.inline === true) {
|
|
177
|
+
content.push({ type: "image", data: imageBytes.toString("base64"), mimeType: "image/png" });
|
|
178
|
+
}
|
|
179
|
+
return { content, structuredContent: sanitized };
|
|
180
|
+
}
|
|
181
|
+
function positionalsFor(cmd, args) {
|
|
182
|
+
return cmd.params.filter((p) => p.cli === "positional").map((p) => args[p.name] === void 0 ? void 0 : String(args[p.name])).filter((v) => v !== void 0);
|
|
183
|
+
}
|
|
184
|
+
function optsFor(cmd, args) {
|
|
185
|
+
const opts = {};
|
|
186
|
+
for (const param of cmd.params) {
|
|
187
|
+
if (param.cli === "positional") continue;
|
|
188
|
+
if (args[param.name] === void 0) continue;
|
|
189
|
+
const key = param.cli.replace(/^--/, "").replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
190
|
+
const value = args[param.name];
|
|
191
|
+
opts[key] = cmd.verb === "wait" && key === "ms" && typeof value === "number" ? value * 1e3 : value;
|
|
192
|
+
}
|
|
193
|
+
return opts;
|
|
194
|
+
}
|
|
195
|
+
async function handleDevStart(args, deps) {
|
|
196
|
+
const platform = args.platform;
|
|
197
|
+
if (!isDevPlatform(platform)) {
|
|
198
|
+
return errorResult(
|
|
199
|
+
`dev_start: platform must be one of ${DEV_PLATFORMS.join(", ")}, got "${String(platform)}"`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const result = await deps.devSession.start({
|
|
204
|
+
platform,
|
|
205
|
+
app: typeof args.app === "string" ? args.app : void 0,
|
|
206
|
+
metroPort: typeof args.metro_port === "number" ? args.metro_port : void 0
|
|
207
|
+
});
|
|
208
|
+
const payload = { session_id: result.sessionId, page_url: result.pageUrl };
|
|
209
|
+
return textResult(`dev session started: ${payload.session_id}
|
|
210
|
+
${payload.page_url}`, payload);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function handleDevStop(deps) {
|
|
216
|
+
const current = deps.devSession.current();
|
|
217
|
+
try {
|
|
218
|
+
await deps.devSession.stop();
|
|
219
|
+
} catch (err) {
|
|
220
|
+
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
221
|
+
}
|
|
222
|
+
return textResult(current ? `dev session stopped: ${current.sessionId}` : "dev session stopped", {
|
|
223
|
+
session_id: current?.sessionId ?? null
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async function handleUploadApp(args, deps) {
|
|
227
|
+
const file = args.file;
|
|
228
|
+
if (typeof file !== "string" || !file) {
|
|
229
|
+
return errorResult("upload_app: `file` is required \u2014 the path to a .apk or zipped iOS .app");
|
|
230
|
+
}
|
|
231
|
+
const name = typeof args.name === "string" && args.name ? args.name : void 0;
|
|
232
|
+
const upload = deps.uploadApp ?? (async (f, n) => {
|
|
233
|
+
const { uploadAppBuild } = await import("./upload-app-GAZVQZYR.js");
|
|
234
|
+
const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
|
|
235
|
+
return result2;
|
|
236
|
+
});
|
|
237
|
+
let result;
|
|
238
|
+
try {
|
|
239
|
+
result = await upload(file, name);
|
|
240
|
+
} catch (err) {
|
|
241
|
+
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
242
|
+
}
|
|
243
|
+
const isNew = result.is_new !== false;
|
|
244
|
+
const warnings = result.warnings ?? [];
|
|
245
|
+
const payload = {
|
|
246
|
+
app_id: result.public_id,
|
|
247
|
+
name: result.name,
|
|
248
|
+
package_name: result.package_name,
|
|
249
|
+
platform: result.platform,
|
|
250
|
+
is_new: isNew,
|
|
251
|
+
warnings
|
|
252
|
+
};
|
|
253
|
+
const lines = [
|
|
254
|
+
`${isNew ? "New app created" : "Build added to existing app"}: ${result.name}`,
|
|
255
|
+
`app_id: ${result.public_id} [${result.platform}] ${result.package_name}`,
|
|
256
|
+
...warnings.map((w) => `warning: ${w}`)
|
|
257
|
+
];
|
|
258
|
+
return textResult(lines.join("\n"), payload);
|
|
259
|
+
}
|
|
260
|
+
function createDevSessionController(auth, webUrl) {
|
|
261
|
+
let handle = null;
|
|
262
|
+
return {
|
|
263
|
+
async start(cfg) {
|
|
264
|
+
if (handle) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`a dev session is already running (${handle.sessionId}) \u2014 call dev_stop first`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
let devConfig = null;
|
|
270
|
+
try {
|
|
271
|
+
devConfig = loadDevConfig(process.cwd());
|
|
272
|
+
} catch {
|
|
273
|
+
devConfig = null;
|
|
274
|
+
}
|
|
275
|
+
const entry = devConfig?.platforms?.[cfg.platform];
|
|
276
|
+
const appId = cfg.app || entry?.appId;
|
|
277
|
+
if (!appId) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`dev_start: no app to run for platform "${cfg.platform}" \u2014 pass the app arg (an app id from list-apps) or run \`vibeview dev\` once in this project to write vibeview.json`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
const metroPort = cfg.metroPort ?? devConfig?.metroPort ?? 8081;
|
|
283
|
+
const metro = await checkMetro(metroPort);
|
|
284
|
+
if (metro === "down") {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`Metro isn't running on localhost:${metroPort} \u2014 start it in the project first (yarn start), or pass metro_port if Metro uses another port`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const isTv = PLATFORM_DEVICE[cfg.platform].tv;
|
|
290
|
+
handle = await startDevSession({
|
|
291
|
+
token: auth.token,
|
|
292
|
+
baseUrl: auth.baseUrl,
|
|
293
|
+
webUrl,
|
|
294
|
+
platform: cfg.platform,
|
|
295
|
+
appId,
|
|
296
|
+
deviceCategory: isTv ? "tv" : entry?.deviceCategory ?? "phone",
|
|
297
|
+
metroPort,
|
|
298
|
+
open: false
|
|
299
|
+
});
|
|
300
|
+
return handle;
|
|
301
|
+
},
|
|
302
|
+
async stop() {
|
|
303
|
+
await handle?.stop();
|
|
304
|
+
handle = null;
|
|
305
|
+
},
|
|
306
|
+
current() {
|
|
307
|
+
return handle ? { sessionId: handle.sessionId, pageUrl: handle.pageUrl } : null;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function makeSessionResolver(devSession) {
|
|
312
|
+
return () => devSession.current()?.sessionId ?? readDevState(process.cwd())?.session_id;
|
|
313
|
+
}
|
|
314
|
+
async function createMcpServer(opts) {
|
|
315
|
+
const [{ McpServer }, { z }] = await Promise.all([
|
|
316
|
+
import("@modelcontextprotocol/sdk/server/mcp.js"),
|
|
317
|
+
import("zod")
|
|
318
|
+
]);
|
|
319
|
+
const auth = resolveAuth(opts);
|
|
320
|
+
const client = new ApiClient(auth.baseUrl, auth.token);
|
|
321
|
+
const devSession = createDevSessionController(auth, opts.webUrl);
|
|
322
|
+
const deps = {
|
|
323
|
+
auth,
|
|
324
|
+
resolveSessionId: makeSessionResolver(devSession),
|
|
325
|
+
agentTool: client.agentTool.bind(client),
|
|
326
|
+
devSession
|
|
327
|
+
};
|
|
328
|
+
const server = new McpServer({ name: "vibeview", version: "1.0.0" });
|
|
329
|
+
function zodShape(def) {
|
|
330
|
+
const shape = {};
|
|
331
|
+
for (const [key, prop] of Object.entries(def.properties)) {
|
|
332
|
+
const base = prop.enum ? z.enum(prop.enum) : z[prop.type]();
|
|
333
|
+
const described = base.describe(prop.description);
|
|
334
|
+
shape[key] = def.required.includes(key) ? described : described.optional();
|
|
335
|
+
}
|
|
336
|
+
return shape;
|
|
337
|
+
}
|
|
338
|
+
for (const def of buildToolDefinitions()) {
|
|
339
|
+
server.registerTool(
|
|
340
|
+
def.name,
|
|
341
|
+
{
|
|
342
|
+
description: def.description,
|
|
343
|
+
inputSchema: zodShape(def),
|
|
344
|
+
annotations: { readOnlyHint: !def.mutating }
|
|
345
|
+
},
|
|
346
|
+
async (args) => {
|
|
347
|
+
const result = await handleToolCall(def.name, args ?? {}, deps);
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return server;
|
|
353
|
+
}
|
|
354
|
+
async function runMcpServer(opts) {
|
|
355
|
+
chalk.level = 0;
|
|
356
|
+
const [server, { StdioServerTransport }] = await Promise.all([
|
|
357
|
+
createMcpServer(opts),
|
|
358
|
+
import("@modelcontextprotocol/sdk/server/stdio.js")
|
|
359
|
+
]);
|
|
360
|
+
const transport = new StdioServerTransport();
|
|
361
|
+
await server.connect(transport);
|
|
362
|
+
}
|
|
363
|
+
export {
|
|
364
|
+
buildToolDefinitions,
|
|
365
|
+
handleToolCall,
|
|
366
|
+
runMcpServer
|
|
367
|
+
};
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeview",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "VibeView CLI -- run mobile tests from CI/CD pipelines",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"vibeview": "./dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist
|
|
10
|
+
"dist",
|
|
11
|
+
"skills"
|
|
11
12
|
],
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"directory": "cli"
|
|
13
|
+
"homepage": "https://vibeview.io/docs/cli",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"email": "support@vibeview.io"
|
|
16
16
|
},
|
|
17
17
|
"publishConfig": {
|
|
18
18
|
"access": "public"
|
|
@@ -26,11 +26,13 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@inquirer/prompts": "^7.0.0",
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
29
30
|
"chalk": "^5.0.0",
|
|
30
31
|
"commander": "^14.0.0",
|
|
31
32
|
"open": "^11.0.0",
|
|
32
33
|
"ora": "^9.0.0",
|
|
33
|
-
"ws": "^8.18.0"
|
|
34
|
+
"ws": "^8.18.0",
|
|
35
|
+
"zod": "^4.0.0"
|
|
34
36
|
},
|
|
35
37
|
"devDependencies": {
|
|
36
38
|
"@types/node": "^20.0.0",
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vibeview-agent
|
|
3
|
+
description: Verify a React Native change on a real running app — install/launch it on a live iOS or Android device via VibeView, drive the UI, and check the result. Use when asked to verify this change on a device, test my React Native app, or run it on a simulator.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# VibeView agent control
|
|
7
|
+
|
|
8
|
+
Drive a live iOS/Android VibeView session from the CLI to verify a React
|
|
9
|
+
Native change actually works, not just that it compiles. Requires the
|
|
10
|
+
`vibeview` CLI installed and authenticated (`vibeview login`, or
|
|
11
|
+
`VIBEVIEW_API_TOKEN` set).
|
|
12
|
+
|
|
13
|
+
## 1. Build requirements
|
|
14
|
+
|
|
15
|
+
Read before uploading anything — a build that violates these will install
|
|
16
|
+
and then crash or refuse to boot:
|
|
17
|
+
|
|
18
|
+
- **Android**: the debug APK must include an `x86_64` slice. Standard
|
|
19
|
+
`./gradlew assembleDebug` output includes it by default; only an issue if
|
|
20
|
+
the project filters ABIs.
|
|
21
|
+
- **iOS**: upload a **simulator** debug build (not a device `.ipa`), and it
|
|
22
|
+
must include the `arm64` slice.
|
|
23
|
+
|
|
24
|
+
Debug builds are auto-detected on upload — no separate flag needed. Upload
|
|
25
|
+
warnings are the safety net: if a build is missing the right architecture,
|
|
26
|
+
`upload-app` says so.
|
|
27
|
+
|
|
28
|
+
Full detail: https://vibeview.io/docs/live-development and
|
|
29
|
+
https://vibeview.io/docs/preparing-your-build.
|
|
30
|
+
|
|
31
|
+
## 2. The loop
|
|
32
|
+
|
|
33
|
+
1. Upload the debug build — **only after a native change** (new/updated
|
|
34
|
+
native module, Podfile/Gradle change). Everyday JS/asset edits never need
|
|
35
|
+
a re-upload:
|
|
36
|
+
```bash
|
|
37
|
+
vibeview upload-app ./path/to/app-debug.apk
|
|
38
|
+
```
|
|
39
|
+
2. Start Metro in the React Native project (`yarn start` or equivalent) and
|
|
40
|
+
leave it running.
|
|
41
|
+
3. Start a dev session in the background and get structured output:
|
|
42
|
+
```bash
|
|
43
|
+
vibeview dev --detach --json
|
|
44
|
+
```
|
|
45
|
+
4. Parse the `session_ready` line from stdout:
|
|
46
|
+
```json
|
|
47
|
+
{"event":"session_ready","session_id":"...","url":"...","pid":1234}
|
|
48
|
+
```
|
|
49
|
+
Use `session_id` as `--session <id>` on every verb below. If the app
|
|
50
|
+
ships no native changes since the last run, skip step 1 and just do
|
|
51
|
+
steps 2–4.
|
|
52
|
+
|
|
53
|
+
Running `dev` writes two things into the project root: a `.vibeview/`
|
|
54
|
+
directory (live session state — local machine state, never commit) and a
|
|
55
|
+
`vibeview.json` (your platform/app/build choices, which teammates DO want).
|
|
56
|
+
Make sure `.vibeview/` is in the project's `.gitignore`, adding it if
|
|
57
|
+
missing. Leave `vibeview.json` tracked, but tell the developer you created
|
|
58
|
+
it rather than letting it appear unexplained in their `git status`.
|
|
59
|
+
|
|
60
|
+
## 3. Verifying
|
|
61
|
+
|
|
62
|
+
- **Always run `ui-tree` first** — every ref (`@e5`) the UI exposes comes
|
|
63
|
+
from here. Never guess a ref.
|
|
64
|
+
```bash
|
|
65
|
+
vibeview ui-tree --session <id>
|
|
66
|
+
```
|
|
67
|
+
- Act on the screen using refs from the tree:
|
|
68
|
+
```bash
|
|
69
|
+
vibeview tap @e5 --session <id>
|
|
70
|
+
vibeview type "hello@example.com" --session <id>
|
|
71
|
+
vibeview scroll down --session <id>
|
|
72
|
+
vibeview press back --session <id>
|
|
73
|
+
```
|
|
74
|
+
- **Read the observation in every response before deciding the next
|
|
75
|
+
action.** Every mutating verb returns one of: screen unchanged, a
|
|
76
|
+
`+`/`~`/`-` diff with fresh `@e` refs, a full tree, or an "unavailable"
|
|
77
|
+
warning. Refs go stale the moment the screen changes — a stale ref is
|
|
78
|
+
refused. **The refusal does NOT include the current screen**: because the
|
|
79
|
+
action never ran, the observation comes back as "screen unchanged" with no
|
|
80
|
+
tree, so run `ui-tree` again to get live refs before retrying. Never reuse
|
|
81
|
+
a ref from a previous observation.
|
|
82
|
+
- **Take a screenshot whenever you changed how something LOOKS.** The UI
|
|
83
|
+
tree proves an element exists, has the right text, and occupies the right
|
|
84
|
+
rect — it says nothing about colour, contrast, or whether something is
|
|
85
|
+
hidden behind another view, so a screen can pass every check the tree can
|
|
86
|
+
express and still be visibly broken (text the same colour as its
|
|
87
|
+
background, a view drawn on top of another). Tree for structure and refs;
|
|
88
|
+
screenshot for appearance.
|
|
89
|
+
|
|
90
|
+
**Actually look at the image** — confirming the file was written proves
|
|
91
|
+
nothing about how it renders.
|
|
92
|
+
```bash
|
|
93
|
+
vibeview screenshot --out ./check.png --session <id> # then open it
|
|
94
|
+
```
|
|
95
|
+
Over MCP, `screenshot` only writes the file and returns its path unless
|
|
96
|
+
you pass `inline: true`, which returns the image itself. If you cannot
|
|
97
|
+
open files on the machine running the server, `inline: true` is the only
|
|
98
|
+
way you will ever see the screen. It costs a lot of context, so use it
|
|
99
|
+
when appearance matters — not to confirm a tap landed, which the
|
|
100
|
+
response's observation already tells you.
|
|
101
|
+
- **Off-screen elements**: use `scroll-to <text>` rather than repeating
|
|
102
|
+
`scroll down`. It scrolls until an element whose label, value, or
|
|
103
|
+
accessibility id contains that text is visible, and stops early when the
|
|
104
|
+
content stops changing (end of list). Omit `--direction` for vertical
|
|
105
|
+
lists (it auto-detects); pass it explicitly for horizontal strips.
|
|
106
|
+
```bash
|
|
107
|
+
vibeview scroll-to "Delete account" --session <id>
|
|
108
|
+
```
|
|
109
|
+
- **Precise two-point gestures** (sliders, drag-and-drop reorder, map
|
|
110
|
+
panning) need `drag` — `swipe`/`scroll` are direction-only and can't
|
|
111
|
+
express them. Each end is independently a ref or `x,y`:
|
|
112
|
+
```bash
|
|
113
|
+
vibeview drag @e5 @e9 --session <id>
|
|
114
|
+
vibeview drag 100,400 300,400 --session <id>
|
|
115
|
+
vibeview drag @e5 300,400 --press-ms 500 --session <id>
|
|
116
|
+
```
|
|
117
|
+
Use `--press-ms 500` or more for long-press-to-reorder lists, and
|
|
118
|
+
`--velocity 150` for slow, precise slider adjustments.
|
|
119
|
+
- **iOS system alerts** (permission prompts and similar) sit above the app
|
|
120
|
+
and block it. Call `alert get` first to read the message and the exact
|
|
121
|
+
button labels, then act on one:
|
|
122
|
+
```bash
|
|
123
|
+
vibeview alert get --session <id>
|
|
124
|
+
vibeview alert accept --button "Allow" --session <id>
|
|
125
|
+
```
|
|
126
|
+
- Use `open-url` to jump straight to a deep link instead of navigating by
|
|
127
|
+
hand:
|
|
128
|
+
```bash
|
|
129
|
+
vibeview open-url "myapp://settings" --session <id>
|
|
130
|
+
```
|
|
131
|
+
- **TV apps**: start the session with `--platform tvos` (Apple TV) or
|
|
132
|
+
`--platform androidtv` (Android TV). Navigate with the d-pad via `press`
|
|
133
|
+
(`dpad_up`/`dpad_down`/`dpad_left`/`dpad_right`/`dpad_center`), or move
|
|
134
|
+
focus directly with `tap-focused` (moves focus AND activates) / `focus`
|
|
135
|
+
(moves focus only, no activation).
|
|
136
|
+
|
|
137
|
+
## 4. Cleanup
|
|
138
|
+
|
|
139
|
+
**Always** stop the session when done verifying — a running session bills
|
|
140
|
+
streaming minutes even when idle:
|
|
141
|
+
```bash
|
|
142
|
+
vibeview dev-stop
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 5. Command reference
|
|
146
|
+
|
|
147
|
+
Every verb accepts `--session <id>` to target a specific session (falls
|
|
148
|
+
back to the current project's `vibeview dev --detach` session if omitted)
|
|
149
|
+
and `--json` to print the raw response envelope as one JSON line instead of
|
|
150
|
+
human-readable text.
|
|
151
|
+
|
|
152
|
+
| Verb | Args | Purpose |
|
|
153
|
+
|------|------|---------|
|
|
154
|
+
| `ui-tree` | — | Fetch the ref-annotated UI tree for the current screen. |
|
|
155
|
+
| `screenshot` | `[--out <path>]` | Capture a screenshot of the current screen and save it to disk. |
|
|
156
|
+
| `tap` | `<target>` | Tap an element by ref (e.g. `@e5`), or raw coordinates (e.g. `100,200`). |
|
|
157
|
+
| `long-press` | `<ref>` `[--ms <n>]` | Long-press (touch and hold) an element by its ref. |
|
|
158
|
+
| `swipe` | `<direction>` `[--distance <short\|medium\|long>]` | Perform a scrollbar-semantic swipe gesture in one of four directions. |
|
|
159
|
+
| `scroll` | `<direction>` | Scroll the current view up, down, left, or right using a gesture preset. |
|
|
160
|
+
| `scroll-to` | `<text>` `[--direction <up\|down\|left\|right>] [--max-scrolls <n>] [--element-type <type>]` | Scroll until an element matching the text is visible. Stops at end of list. |
|
|
161
|
+
| `drag` | `<from>` `<to>` `[--velocity <n>] [--press-ms <n>] [--hold-ms <n>]` | Drag between two points, each an `@ref` or `x,y`. The only precise two-point gesture. |
|
|
162
|
+
| `alert` | `<get\|accept\|dismiss>` `[--button <label>]` | Inspect or respond to a visible iOS system alert. |
|
|
163
|
+
| `type` | `<text>` | Type text into the currently focused input field. |
|
|
164
|
+
| `clear-text` | — | Clear the text in the currently focused input field. |
|
|
165
|
+
| `press` | `<button>` | Press a device button or perform a system gesture (home, back, d-pad, etc). |
|
|
166
|
+
| `open-url` | `<url>` | Open a deep link or URL in the app under test. |
|
|
167
|
+
| `wait` | `[--ms <n>]` | Wait for a specified number of seconds before continuing (default: 2s). |
|
|
168
|
+
| `find` | `<text>` `[--below <text>] [--above <text>] [--near <text>]` | Find an element by text with optional spatial constraints. |
|
|
169
|
+
| `tap-focused` | `<ref>` | TV: move focus to an element and press SELECT in one step. |
|
|
170
|
+
| `focus` | `<ref>` | TV: move focus to an element without activating it. |
|
|
171
|
+
|
|
172
|
+
Dev-loop lifecycle commands (not registry verbs, but needed for every run):
|
|
173
|
+
|
|
174
|
+
| Command | Purpose |
|
|
175
|
+
|---------|---------|
|
|
176
|
+
| `dev --detach --json` | Start a device + Metro-tunnel session in the background, emitting `session_ready`/`warning`/`error` JSON events on stdout. |
|
|
177
|
+
| `dev-status` | Check on the current project's detached dev session. |
|
|
178
|
+
| `dev-stop` | End the current project's detached dev session. |
|
|
179
|
+
|
|
180
|
+
## 6. If you're calling VibeView over MCP
|
|
181
|
+
|
|
182
|
+
Everything above is also exposed as MCP tools by `vibeview mcp`, so an agent
|
|
183
|
+
with no shell can run the whole loop. Tool names are the CLI verbs with
|
|
184
|
+
hyphens replaced by underscores (`ui_tree`, `tap`, `scroll_to`, `long_press`,
|
|
185
|
+
`clear_text`, `open_url`, `tap_focused`, ...), each taking its args by name
|
|
186
|
+
(`tap {ref: "e5"}` or `tap {target: "100,200"}`, `scroll_to {text: "..."}`,
|
|
187
|
+
`drag {from: "@e5", to: "300,400"}`, `alert {action: "get"}`) plus an
|
|
188
|
+
optional `session_id`.
|
|
189
|
+
|
|
190
|
+
Three tools exist **only** over MCP, with no registry verb — they cover the
|
|
191
|
+
steps the CLI does with plain shell commands:
|
|
192
|
+
|
|
193
|
+
| MCP tool | Args | Purpose |
|
|
194
|
+
|----------|------|---------|
|
|
195
|
+
| `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`. |
|
|
196
|
+
| `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`. |
|
|
197
|
+
| `dev_stop` | — | Stop the session `dev_start` started. The MCP equivalent of `vibeview dev-stop`. |
|
|
198
|
+
|
|
199
|
+
**`dev_start` is stateful — it changes the default session for every later
|
|
200
|
+
call.** Once it succeeds, any tool called without an explicit `session_id`
|
|
201
|
+
targets that session instead of whatever `vibeview dev --detach` state exists
|
|
202
|
+
in the project directory. Only one such session can be held at a time
|
|
203
|
+
(`dev_start` fails if one is already running), and it stays live — billing
|
|
204
|
+
streaming minutes — until `dev_stop` or the MCP connection ends. Always call
|
|
205
|
+
`dev_stop` when you finish verifying.
|