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/dist/cli.js CHANGED
@@ -1,4 +1,25 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ AGENT_COMMANDS,
4
+ buildCommand,
5
+ clearDevState,
6
+ dev,
7
+ killOwnedMetro,
8
+ readDevState,
9
+ readMetroState,
10
+ runAgentVerb
11
+ } from "./chunk-NZ4G3JKV.js";
12
+ import {
13
+ ApiClient,
14
+ EXIT_INFRA_ERROR,
15
+ EXIT_PASS,
16
+ EXIT_TEST_FAIL,
17
+ createSpinner,
18
+ loadConfig,
19
+ resolveAuth,
20
+ saveConfig,
21
+ uploadApp
22
+ } from "./chunk-YD2X6TQV.js";
2
23
 
3
24
  // src/cli.ts
4
25
  import { createRequire } from "module";
@@ -7,225 +28,6 @@ import { Command } from "commander";
7
28
  // src/commands/run-suite.ts
8
29
  import chalk3 from "chalk";
9
30
 
10
- // src/lib/config.ts
11
- import { readFileSync, writeFileSync, mkdirSync } from "fs";
12
- import { join } from "path";
13
- import { homedir } from "os";
14
- var CONFIG_DIR = join(homedir(), ".vibeview");
15
- var CONFIG_FILE = join(CONFIG_DIR, "config.json");
16
- function loadConfig() {
17
- try {
18
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
19
- } catch {
20
- return {};
21
- }
22
- }
23
- function saveConfig(config) {
24
- mkdirSync(CONFIG_DIR, { recursive: true });
25
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
26
- }
27
-
28
- // src/lib/auth.ts
29
- function resolveAuth(options) {
30
- const token = options.token || process.env.VIBEVIEW_API_TOKEN || loadConfig().token;
31
- if (!token) {
32
- console.error("Error: No API token found.");
33
- console.error(
34
- "Set VIBEVIEW_API_TOKEN environment variable, use --token flag, or run: vibeview login"
35
- );
36
- process.exit(2);
37
- }
38
- const baseUrl = options.baseUrl || process.env.VIBEVIEW_BASE_URL || loadConfig().baseUrl || "https://vibeview.io";
39
- return { token, baseUrl };
40
- }
41
-
42
- // src/lib/api-client.ts
43
- import { readFileSync as readFileSync2 } from "fs";
44
- import { basename } from "path";
45
- var ApiClient = class {
46
- constructor(baseUrl, token) {
47
- this.baseUrl = baseUrl;
48
- this.token = token;
49
- }
50
- async request(method, path, body) {
51
- const url = `${this.baseUrl}/api/v1/ci${path}`;
52
- const headers = {
53
- Authorization: `Bearer ${this.token}`,
54
- "Content-Type": "application/json"
55
- };
56
- const resp = await fetch(url, {
57
- method,
58
- headers,
59
- body: body ? JSON.stringify(body) : void 0
60
- });
61
- if (!resp.ok) {
62
- const text = await resp.text().catch(() => "");
63
- if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
64
- if (resp.status === 404) throw new Error(`Not found: ${path}`);
65
- throw new Error(`API error ${resp.status}: ${text}`);
66
- }
67
- return resp.json();
68
- }
69
- async listSuites(opts) {
70
- const params = new URLSearchParams();
71
- if (opts?.tags && opts.tags.length > 0) params.set("tags", opts.tags.join(","));
72
- if (opts?.match) params.set("match", opts.match);
73
- const qs = params.toString();
74
- return this.request("GET", `/list-suites${qs ? `?${qs}` : ""}`);
75
- }
76
- async runSuite(suiteId, options) {
77
- return this.request("POST", "/run-suite", {
78
- suite_id: suiteId,
79
- commit_sha: options?.commitSha,
80
- model: options?.model,
81
- provider: options?.provider,
82
- mode: options?.mode,
83
- // When pinning a specific device, omit device_category — the backend
84
- // rejects the combo.
85
- device_category: options?.device ? void 0 : options?.deviceCategory ?? "phone",
86
- device_id: options?.device,
87
- build_id: options?.build,
88
- metadata: options?.metadata
89
- });
90
- }
91
- async runTest(testId, options) {
92
- return this.request("POST", "/run-test", {
93
- test_id: testId,
94
- commit_sha: options?.commitSha,
95
- model: options?.model,
96
- provider: options?.provider,
97
- mode: options?.mode,
98
- device_category: options?.device ? void 0 : options?.deviceCategory ?? "phone",
99
- device_id: options?.device,
100
- build_id: options?.build,
101
- metadata: options?.metadata
102
- });
103
- }
104
- /** Cancel a still-queued CI run by its queue entry id. Idempotent best-effort. */
105
- async cancelQueueEntry(queueEntryId) {
106
- await this.request("DELETE", `/queue/${queueEntryId}`);
107
- }
108
- /**
109
- * Request cancellation of a still-running multi-suite run.
110
- * The aggregate transitions to CANCELLED on the runner's next
111
- * aggregate-status write; the active child suite stops at its
112
- * next case boundary.
113
- */
114
- async cancelMultiSuiteRun(aggregateId) {
115
- await this.request("DELETE", `/multi-suite-runs/${aggregateId}`);
116
- }
117
- async getRunStatus(runId) {
118
- const data = await this.request("GET", `/runs/${runId}`);
119
- return { ...data, url: data.run_url ?? `${this.baseUrl}/tests/runs/${runId}` };
120
- }
121
- /** Request without /ci prefix — for non-CI endpoints like /apps, /tests */
122
- async requestRaw(method, path, body) {
123
- const url = `${this.baseUrl}/api/v1${path}`;
124
- const headers = {
125
- Authorization: `Bearer ${this.token}`,
126
- "Content-Type": "application/json"
127
- };
128
- const resp = await fetch(url, {
129
- method,
130
- headers,
131
- body: body ? JSON.stringify(body) : void 0
132
- });
133
- if (!resp.ok) {
134
- const text = await resp.text().catch(() => "");
135
- if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
136
- if (resp.status === 404) throw new Error(`Not found: ${path}`);
137
- throw new Error(`API error ${resp.status}: ${text}`);
138
- }
139
- return resp.json();
140
- }
141
- /** FormData request without /ci prefix — for file uploads */
142
- async requestFormData(method, path, formData) {
143
- const url = `${this.baseUrl}/api/v1${path}`;
144
- const headers = {
145
- Authorization: `Bearer ${this.token}`
146
- // Do NOT set Content-Type — let fetch set multipart boundary automatically
147
- };
148
- const resp = await fetch(url, {
149
- method,
150
- headers,
151
- body: formData
152
- });
153
- if (!resp.ok) {
154
- const text = await resp.text().catch(() => "");
155
- if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
156
- if (resp.status === 404) throw new Error(`Not found: ${path}`);
157
- throw new Error(`API error ${resp.status}: ${text}`);
158
- }
159
- return resp.json();
160
- }
161
- async listApps() {
162
- return this.requestRaw("GET", "/apps/");
163
- }
164
- async uploadApp(filePath, name) {
165
- const fileBuffer = readFileSync2(filePath);
166
- const blob = new Blob([fileBuffer]);
167
- const formData = new FormData();
168
- formData.append("file", blob, basename(filePath));
169
- if (name) {
170
- formData.append("name", name);
171
- }
172
- return this.requestFormData("POST", "/apps/upload", formData);
173
- }
174
- async uploadBuild(appPublicId, filePath, version) {
175
- const fileBuffer = readFileSync2(filePath);
176
- const blob = new Blob([fileBuffer]);
177
- const formData = new FormData();
178
- formData.append("file", blob, basename(filePath));
179
- if (version) {
180
- formData.append("version", version);
181
- }
182
- return this.requestFormData("POST", `/apps/${appPublicId}/builds`, formData);
183
- }
184
- async createTest(body) {
185
- return this.requestRaw("POST", "/tests/cases/", body);
186
- }
187
- async runSuites(body) {
188
- return this.request("POST", "/run-suites", body);
189
- }
190
- async getMultiSuiteRun(aggregateId) {
191
- return this.request("GET", `/multi-suite-runs/${aggregateId}`);
192
- }
193
- async listDevices(opts) {
194
- const params = new URLSearchParams();
195
- if (opts?.type) params.set("device_type", opts.type);
196
- if (opts?.category) params.set("category", opts.category);
197
- const qs = params.toString();
198
- const url = qs ? `/devices?${qs}` : "/devices";
199
- const all = await this.requestRaw("GET", url);
200
- if (opts?.status) {
201
- return all.filter((d) => d.status === opts.status);
202
- }
203
- return all;
204
- }
205
- async getDevice(deviceId) {
206
- return this.requestRaw("GET", `/devices/${deviceId}`);
207
- }
208
- async createSession(body) {
209
- return this.requestRaw("POST", "/sessions/", body);
210
- }
211
- async startSession(sessionId, deviceId) {
212
- return this.requestRaw("POST", `/sessions/${sessionId}/start`, { device_id: deviceId });
213
- }
214
- async getSession(sessionId) {
215
- return this.requestRaw("GET", `/sessions/${sessionId}`);
216
- }
217
- async stopSession(sessionId) {
218
- await this.requestRaw("DELETE", `/sessions/${sessionId}`);
219
- }
220
- };
221
-
222
- // src/lib/spinner.ts
223
- import ora from "ora";
224
- function createSpinner(text) {
225
- const isCI = process.env.CI === "true" || process.env.CI === "1" || !process.stdout.isTTY;
226
- return ora({ text, isEnabled: !isCI, stream: process.stderr });
227
- }
228
-
229
31
  // src/lib/poll.ts
230
32
  import chalk2 from "chalk";
231
33
 
@@ -375,11 +177,6 @@ function formatMultiSuiteJunit(result) {
375
177
  return lines.join("\n");
376
178
  }
377
179
 
378
- // src/lib/types.ts
379
- var EXIT_PASS = 0;
380
- var EXIT_TEST_FAIL = 1;
381
- var EXIT_INFRA_ERROR = 2;
382
-
383
180
  // src/lib/poll.ts
384
181
  function printBelowSpinner(spinner, line) {
385
182
  const text = spinner.text;
@@ -1071,62 +868,9 @@ async function listApps(options) {
1071
868
  console.log();
1072
869
  }
1073
870
 
1074
- // src/commands/upload-app.ts
1075
- import chalk8 from "chalk";
1076
- import { existsSync } from "fs";
1077
- async function uploadApp(filePath, options) {
1078
- if (!existsSync(filePath)) {
1079
- console.error(chalk8.red(`Error: File not found: ${filePath}`));
1080
- process.exit(EXIT_INFRA_ERROR);
1081
- }
1082
- const { token, baseUrl } = resolveAuth(options);
1083
- const client = new ApiClient(baseUrl, token);
1084
- const spinner = createSpinner("Uploading app...");
1085
- spinner.start();
1086
- try {
1087
- const result = await client.uploadApp(filePath, options.name);
1088
- const isNew = result.is_new !== false;
1089
- spinner.succeed(
1090
- isNew ? `New app created: ${result.name}` : `Build added to existing app: ${result.name}`
1091
- );
1092
- if (options.output === "json") {
1093
- console.log(
1094
- JSON.stringify(
1095
- {
1096
- app_id: result.public_id,
1097
- name: result.name,
1098
- package_name: result.package_name,
1099
- platform: result.platform,
1100
- is_new: isNew,
1101
- url: `${baseUrl}/apps/${result.public_id}`,
1102
- warnings: result.warnings ?? []
1103
- },
1104
- null,
1105
- 2
1106
- )
1107
- );
1108
- } else {
1109
- console.log(
1110
- `
1111
- ${chalk8.cyan(result.public_id)} ${result.name} ${chalk8.dim(`[${result.platform}]`)}`
1112
- );
1113
- console.log(` ${chalk8.dim(result.package_name)}`);
1114
- console.log(chalk8.dim(` ${baseUrl}/apps/${result.public_id}
1115
- `));
1116
- }
1117
- for (const w of result.warnings ?? []) {
1118
- console.error(chalk8.yellow(`
1119
- \u26A0 ${w}`));
1120
- }
1121
- } catch (err) {
1122
- spinner.fail(err instanceof Error ? err.message : "Upload failed");
1123
- process.exit(EXIT_INFRA_ERROR);
1124
- }
1125
- }
1126
-
1127
871
  // src/commands/create-test.ts
1128
- import chalk9 from "chalk";
1129
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
872
+ import chalk8 from "chalk";
873
+ import { existsSync, readFileSync } from "fs";
1130
874
  async function readStdin() {
1131
875
  return new Promise((resolve, reject) => {
1132
876
  let data = "";
@@ -1153,10 +897,10 @@ async function resolveSteps(options) {
1153
897
  return JSON.parse(options.steps);
1154
898
  }
1155
899
  if (options.stepsFile) {
1156
- if (!existsSync2(options.stepsFile)) {
900
+ if (!existsSync(options.stepsFile)) {
1157
901
  throw new Error(`Steps file not found: ${options.stepsFile}`);
1158
902
  }
1159
- return JSON.parse(readFileSync3(options.stepsFile, "utf-8"));
903
+ return JSON.parse(readFileSync(options.stepsFile, "utf-8"));
1160
904
  }
1161
905
  if (!process.stdin.isTTY) {
1162
906
  const input = await readStdin();
@@ -1216,19 +960,19 @@ async function createTest(options) {
1216
960
  );
1217
961
  } else {
1218
962
  console.log(`
1219
- ${chalk9.cyan(result.public_id)} ${result.name}`);
963
+ ${chalk8.cyan(result.public_id)} ${result.name}`);
1220
964
  if (result.app_name) {
1221
- console.log(` ${chalk9.dim(`App: ${result.app_name}`)}`);
965
+ console.log(` ${chalk8.dim(`App: ${result.app_name}`)}`);
1222
966
  }
1223
967
  if (result.platform) {
1224
- console.log(` ${chalk9.dim(`Platform: ${result.platform}`)}`);
968
+ console.log(` ${chalk8.dim(`Platform: ${result.platform}`)}`);
1225
969
  }
1226
- console.log(` ${chalk9.dim(`Steps: ${steps.length}`)}`);
1227
- console.log(chalk9.dim(` ${baseUrl}/tests?tab=standalone
970
+ console.log(` ${chalk8.dim(`Steps: ${steps.length}`)}`);
971
+ console.log(chalk8.dim(` ${baseUrl}/tests?tab=standalone
1228
972
  `));
1229
973
  }
1230
974
  } catch (err) {
1231
- console.error(chalk9.red(err instanceof Error ? err.message : "Failed to create test"));
975
+ console.error(chalk8.red(err instanceof Error ? err.message : "Failed to create test"));
1232
976
  process.exit(EXIT_INFRA_ERROR);
1233
977
  }
1234
978
  }
@@ -1278,443 +1022,89 @@ Opening browser to log in...
1278
1022
  process.exit(EXIT_INFRA_ERROR);
1279
1023
  }
1280
1024
 
1281
- // src/commands/dev.ts
1282
- import { select } from "@inquirer/prompts";
1283
- import chalk10 from "chalk";
1284
- import openBrowser from "open";
1285
-
1286
- // src/lib/tunnel-client.ts
1287
- import net from "net";
1288
- import WebSocket from "ws";
1289
-
1290
- // src/lib/mux.ts
1291
- var FRAME_OPEN = 1;
1292
- var FRAME_DATA = 2;
1293
- var FRAME_CLOSE = 3;
1294
- var HEADER_LEN = 5;
1295
- var KNOWN = /* @__PURE__ */ new Set([FRAME_OPEN, FRAME_DATA, FRAME_CLOSE]);
1296
- function encodeFrame(type, streamId, payload = Buffer.alloc(0)) {
1297
- const header = Buffer.alloc(HEADER_LEN);
1298
- header.writeUInt8(type, 0);
1299
- header.writeUInt32BE(streamId, 1);
1300
- return Buffer.concat([header, payload]);
1301
- }
1302
- function decodeFrame(data) {
1303
- if (data.length < HEADER_LEN) throw new Error(`tunnel frame too short: ${data.length}`);
1304
- const type = data.readUInt8(0);
1305
- if (!KNOWN.has(type)) throw new Error(`unknown tunnel frame type: ${type}`);
1306
- return { type, streamId: data.readUInt32BE(1), payload: data.subarray(HEADER_LEN) };
1307
- }
1308
-
1309
- // src/lib/tunnel-client.ts
1310
- var FATAL_CLOSE_CODES = /* @__PURE__ */ new Set([4008, 4009]);
1311
- var MAX_BACKOFF_MS = 8e3;
1312
- var STABLE_MS = 2e3;
1313
- var TunnelClient = class {
1314
- constructor(opts) {
1315
- this.opts = opts;
1316
- this.url = opts.url;
1317
- }
1318
- ws = null;
1319
- sockets = /* @__PURE__ */ new Map();
1320
- stopped = false;
1321
- backoffMs = 1e3;
1322
- metroWarned = false;
1323
- reconnectTimer = null;
1324
- stableTimer = null;
1325
- url;
1326
- connCount = 0;
1327
- debug(msg) {
1328
- this.opts.onDebug?.(msg);
1329
- }
1330
- start() {
1331
- this.connect();
1332
- }
1333
- stop() {
1334
- if (this.reconnectTimer) {
1335
- clearTimeout(this.reconnectTimer);
1336
- this.reconnectTimer = null;
1337
- }
1338
- if (this.stableTimer) {
1339
- clearTimeout(this.stableTimer);
1340
- this.stableTimer = null;
1341
- }
1342
- this.stopped = true;
1343
- this.setState("closed");
1344
- this.ws?.close();
1345
- for (const s of this.sockets.values()) s.destroy();
1346
- this.sockets.clear();
1347
- }
1348
- setState(s) {
1349
- this.opts.onState?.(s);
1350
- }
1351
- connect() {
1352
- if (this.stopped) return;
1353
- this.setState("connecting");
1354
- const conn = ++this.connCount;
1355
- const openedAt = Date.now();
1356
- this.debug(`ws#${conn} connecting url=${this.url}`);
1357
- const ws = new WebSocket(this.url);
1358
- this.ws = ws;
1359
- ws.on("open", () => {
1360
- this.backoffMs = 1e3;
1361
- this.debug(`ws#${conn} open`);
1362
- this.stableTimer = setTimeout(() => {
1363
- this.stableTimer = null;
1364
- if (this.ws === ws && !this.stopped) this.setState("connected");
1365
- }, STABLE_MS);
1366
- });
1367
- ws.on("message", (data) => this.onFrame(data));
1368
- ws.on("error", (err) => {
1369
- this.debug(`ws#${conn} error: ${err.message}`);
1370
- });
1371
- ws.on("close", (code, reason) => {
1372
- if (this.ws !== ws) this.debug(`ws#${conn} close code=${code} \u2014 SUPERSEDED socket`);
1373
- const heldMs = Date.now() - openedAt;
1374
- const why = reason?.length ? ` reason="${reason.toString()}"` : "";
1375
- this.debug(`ws#${conn} close code=${code}${why} heldMs=${heldMs}`);
1376
- if (this.stableTimer) {
1377
- clearTimeout(this.stableTimer);
1378
- this.stableTimer = null;
1379
- }
1380
- for (const s of this.sockets.values()) s.destroy();
1381
- this.sockets.clear();
1382
- if (this.stopped || FATAL_CLOSE_CODES.has(code)) {
1383
- this.stopped = true;
1384
- this.setState("closed");
1385
- return;
1386
- }
1387
- this.setState("reconnecting");
1388
- const delay = this.backoffMs;
1389
- this.backoffMs = Math.min(this.backoffMs * 2, MAX_BACKOFF_MS);
1390
- this.debug(`ws#${conn} scheduling reconnect in ${delay}ms`);
1391
- this.reconnectTimer = setTimeout(() => {
1392
- this.reconnectTimer = null;
1393
- void this.reconnect();
1394
- }, delay);
1395
- });
1396
- }
1397
- async reconnect() {
1398
- if (this.stopped) return;
1399
- if (this.opts.refreshUrl) {
1400
- let fresh;
1401
- try {
1402
- fresh = await this.opts.refreshUrl();
1403
- } catch (err) {
1404
- this.debug(`refreshUrl threw: ${err.message}`);
1405
- fresh = null;
1406
- }
1407
- if (this.stopped) return;
1408
- if (!fresh) {
1409
- this.debug("refreshUrl returned null \u2014 session gone, closing");
1410
- this.stopped = true;
1411
- this.setState("closed");
1412
- return;
1413
- }
1414
- this.debug("refreshUrl ok \u2014 reconnecting with fresh url");
1415
- this.url = fresh;
1416
- }
1417
- this.connect();
1418
- }
1419
- onFrame(data) {
1420
- let frame;
1421
- try {
1422
- frame = decodeFrame(Buffer.isBuffer(data) ? data : Buffer.from(data));
1423
- } catch {
1424
- return;
1025
+ // src/commands/dev-status.ts
1026
+ import chalk9 from "chalk";
1027
+ async function devStatus(opts) {
1028
+ const state = readDevState(process.cwd());
1029
+ if (!state) {
1030
+ if (opts.json) {
1031
+ console.log(JSON.stringify({ running: false }));
1032
+ } else {
1033
+ console.error("No dev session running (or it was not started with --detach).");
1425
1034
  }
1426
- if (frame.type === FRAME_OPEN) return this.openStream(frame.streamId);
1427
- const sock = this.sockets.get(frame.streamId);
1428
- if (!sock) return;
1429
- if (frame.type === FRAME_DATA) sock.write(frame.payload);
1430
- else if (frame.type === FRAME_CLOSE) sock.destroy();
1431
- }
1432
- openStream(id) {
1433
- const sock = net.createConnection({ port: this.opts.metroPort, host: "127.0.0.1" });
1434
- this.sockets.set(id, sock);
1435
- sock.on("data", (chunk) => this.ws?.send(encodeFrame(FRAME_DATA, id, chunk)));
1436
- sock.on("error", () => {
1437
- if (!this.metroWarned) {
1438
- this.metroWarned = true;
1439
- this.setState("metro-unreachable");
1440
- }
1441
- });
1442
- sock.on("close", () => {
1443
- if (this.sockets.delete(id) && this.ws?.readyState === WebSocket.OPEN) {
1444
- this.ws.send(encodeFrame(FRAME_CLOSE, id));
1445
- }
1446
- });
1447
- }
1448
- };
1449
-
1450
- // src/lib/metro-check.ts
1451
- async function checkMetro(port, timeoutMs = 1500) {
1452
- try {
1453
- const res = await fetch(`http://127.0.0.1:${port}/status`, {
1454
- signal: AbortSignal.timeout(timeoutMs)
1455
- });
1456
- const body = await res.text().catch(() => "");
1457
- return body.includes("packager-status:running") ? "running" : "listening";
1458
- } catch {
1459
- return "down";
1035
+ process.exit(1);
1036
+ return;
1460
1037
  }
1461
- }
1462
-
1463
- // src/lib/dev-config.ts
1464
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1465
- import { join as join2 } from "path";
1466
- var DEV_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
1467
- var FILE = "vibeview.json";
1468
- function isDevPlatform(p) {
1469
- return typeof p === "string" && DEV_PLATFORMS.includes(p);
1470
- }
1471
- function loadDevConfig(cwd) {
1472
- let raw;
1038
+ const { token, baseUrl } = resolveAuth(opts);
1039
+ const api = new ApiClient(baseUrl, token);
1040
+ let sessionStatus;
1473
1041
  try {
1474
- raw = readFileSync4(join2(cwd, FILE), "utf-8");
1042
+ const info = await api.getSession(state.session_id);
1043
+ sessionStatus = info.status;
1475
1044
  } catch {
1476
- return null;
1477
- }
1478
- const parsed = JSON.parse(raw);
1479
- if (!("platforms" in parsed) && typeof parsed.platform === "string") {
1480
- const legacy = parsed;
1481
- if (!isDevPlatform(legacy.platform)) {
1482
- throw new Error(
1483
- `vibeview.json: "platform" must be one of ${DEV_PLATFORMS.join(", ")}, got "${legacy.platform}"`
1484
- );
1485
- }
1486
- if (!legacy.appId || typeof legacy.appId !== "string") {
1487
- throw new Error('vibeview.json: "appId" is required \u2014 run `vibeview dev` to set it up');
1488
- }
1489
- return {
1490
- defaultPlatform: legacy.platform,
1491
- metroPort: legacy.metroPort,
1492
- platforms: {
1493
- [legacy.platform]: { appId: legacy.appId, deviceCategory: legacy.deviceCategory }
1494
- }
1495
- };
1496
- }
1497
- const cfg = parsed;
1498
- if (!isDevPlatform(cfg.defaultPlatform)) {
1499
- throw new Error(
1500
- `vibeview.json: "defaultPlatform" must be one of ${DEV_PLATFORMS.join(", ")}, got "${String(cfg.defaultPlatform)}"`
1045
+ sessionStatus = void 0;
1046
+ }
1047
+ const metro = readMetroState(process.cwd());
1048
+ if (opts.json) {
1049
+ console.log(
1050
+ JSON.stringify({
1051
+ running: true,
1052
+ pid: state.pid,
1053
+ session_id: state.session_id,
1054
+ page_url: state.page_url,
1055
+ platform: state.platform,
1056
+ app_id: state.app_id,
1057
+ metro_port: state.metro_port,
1058
+ started_at: state.started_at,
1059
+ session_status: sessionStatus ?? null,
1060
+ ...metro ? { owned_metro_pid: metro.pid, owned_metro_port: metro.port } : {}
1061
+ })
1501
1062
  );
1502
- }
1503
- if (!cfg.platforms || typeof cfg.platforms !== "object") {
1504
- throw new Error(
1505
- 'vibeview.json: "platforms" object is required \u2014 run `vibeview dev` to set it up'
1506
- );
1507
- }
1508
- for (const [key, entry] of Object.entries(cfg.platforms)) {
1509
- if (!isDevPlatform(key)) {
1510
- throw new Error(
1511
- `vibeview.json: unknown platform "${key}" (expected one of ${DEV_PLATFORMS.join(", ")})`
1512
- );
1513
- }
1514
- if (!entry || typeof entry.appId !== "string" || !entry.appId) {
1515
- throw new Error(`vibeview.json: platforms.${key}.appId is required`);
1516
- }
1517
- }
1518
- return cfg;
1519
- }
1520
- function saveDevConfig(cwd, cfg) {
1521
- writeFileSync2(join2(cwd, FILE), JSON.stringify(cfg, null, 2) + "\n");
1063
+ } else {
1064
+ console.error(`dev session running (pid ${state.pid})`);
1065
+ console.error(` session: ${state.session_id}${sessionStatus ? ` (${sessionStatus})` : ""}`);
1066
+ console.error(` platform: ${state.platform}`);
1067
+ console.error(` app: ${state.app_id}`);
1068
+ console.error(` url: ${state.page_url}`);
1069
+ console.error(` started: ${state.started_at}`);
1070
+ }
1071
+ if (metro) console.error(chalk9.dim(`Metro: pid ${metro.pid} on :${metro.port} (CLI-owned)`));
1072
+ process.exit(0);
1522
1073
  }
1523
1074
 
1524
- // src/commands/dev.ts
1525
- function findBlockingWarning(info) {
1526
- return (info.warnings ?? []).find((w) => w.includes("No debug build"));
1527
- }
1528
- async function pollUntilTunnelReady(api, sessionId, timeoutMs, intervalMs = 2e3) {
1529
- const deadline = Date.now() + timeoutMs;
1530
- for (; ; ) {
1531
- const info = await api.getSession(sessionId);
1532
- const blocking = findBlockingWarning(info);
1533
- if (blocking) throw new Error(blocking);
1534
- if (info.tunnel_url) return info;
1535
- if (info.status === "stopped" || info.status === "failed") {
1536
- throw new Error(`session ${info.status} before the tunnel came up`);
1537
- }
1538
- if (Date.now() > deadline) throw new Error("timed out waiting for the device to start");
1539
- await new Promise((r) => setTimeout(r, intervalMs));
1540
- }
1541
- }
1542
- var PLATFORM_DEVICE = {
1543
- ios: { deviceType: "ios", tv: false },
1544
- android: { deviceType: "android", tv: false },
1545
- tvos: { deviceType: "ios", tv: true },
1546
- androidtv: { deviceType: "android", tv: true }
1547
- };
1548
- function appsForPlatform(apps, platform) {
1549
- return apps.filter((a) => a.platform === platform);
1550
- }
1551
- async function ensureConfig(api, opts) {
1552
- const existing = loadDevConfig(process.cwd());
1553
- if (opts.platform && !isDevPlatform(opts.platform)) {
1554
- console.error(chalk10.red(`error: --platform must be one of ios, android, tvos, androidtv`));
1555
- process.exit(2);
1556
- }
1557
- const platform = opts.platform || existing?.defaultPlatform || await select({
1558
- message: "Platform to run on",
1559
- choices: [
1560
- { value: "ios", name: "iOS simulator" },
1561
- { value: "android", name: "Android emulator" },
1562
- { value: "tvos", name: "Apple TV simulator" },
1563
- { value: "androidtv", name: "Android TV emulator" }
1564
- ]
1565
- });
1566
- const isTv = PLATFORM_DEVICE[platform].tv;
1567
- if (isTv && opts.deviceCategory) {
1568
- console.error(
1569
- chalk10.red(`error: --device-category does not apply to ${platform} (always a tv device)`)
1570
- );
1571
- process.exit(2);
1572
- }
1573
- if (!isTv && opts.deviceCategory === "tv") {
1574
- console.error(chalk10.red(`error: use --platform tvos or --platform androidtv for TV devices`));
1575
- process.exit(2);
1576
- }
1577
- const entry = existing?.platforms?.[platform];
1578
- let appId = opts.app || entry?.appId;
1579
- if (!appId) {
1580
- const apps = appsForPlatform(await api.listApps(), platform);
1581
- if (apps.length === 0) {
1582
- console.error(
1583
- chalk10.red(`No ${platform} apps found \u2014 upload a debug build with \`vibeview upload-app\``)
1584
- );
1585
- process.exit(1);
1075
+ // src/commands/dev-stop.ts
1076
+ async function devStop(opts) {
1077
+ const state = readDevState(process.cwd());
1078
+ if (!state) {
1079
+ if (opts.json) {
1080
+ console.log(JSON.stringify({ stopped: false, reason: "no_dev_session" }));
1081
+ } else {
1082
+ console.error("No dev session running (or it was not started with --detach).");
1586
1083
  }
1587
- appId = await select({
1588
- message: "Which uploaded app is your debug build? (upload one with `vibeview upload-app`)",
1589
- // Platform suffix dropped: the picker is already filtered to one platform
1590
- // (appsForPlatform), so `(ios)` on every row is redundant noise.
1591
- choices: apps.map((a) => ({ value: a.public_id, name: a.name }))
1592
- });
1084
+ process.exit(1);
1085
+ return;
1593
1086
  }
1594
- const deviceCategory = isTv ? "tv" : opts.deviceCategory || entry?.deviceCategory || "phone";
1595
- const metroPort = opts.metroPort ? parseInt(opts.metroPort, 10) : existing?.metroPort ?? 8081;
1596
- const cfg = {
1597
- defaultPlatform: platform,
1598
- metroPort,
1599
- platforms: {
1600
- ...existing?.platforms,
1601
- [platform]: { appId, deviceCategory: isTv ? void 0 : deviceCategory }
1602
- }
1603
- };
1604
- saveDevConfig(process.cwd(), cfg);
1605
- return { platform, appId, deviceCategory, metroPort };
1606
- }
1607
- async function dev(opts) {
1608
1087
  const { token, baseUrl } = resolveAuth(opts);
1609
- console.error(chalk10.dim(`Using ${baseUrl}`));
1610
1088
  const api = new ApiClient(baseUrl, token);
1611
- const cfg = await ensureConfig(api, opts);
1612
- const metroPort = cfg.metroPort ?? 8081;
1613
- const metro = await checkMetro(metroPort);
1614
- if (metro === "down") {
1615
- console.error(
1616
- chalk10.red(`error: Metro isn't running on localhost:${metroPort}.`) + `
1617
- Start it in your project first (yarn start), then re-run vibeview dev.
1618
- If Metro uses another port, pass --metro-port <port>.`
1619
- );
1620
- process.exit(1);
1621
- }
1622
- if (metro === "listening") {
1623
- console.error(
1624
- chalk10.yellow(
1625
- `\u26A0 something is listening on localhost:${metroPort} but it doesn't look like Metro \u2014 continuing anyway`
1626
- )
1627
- );
1628
- }
1629
- console.error(chalk10.dim(`Starting a ${cfg.platform} dev session for ${cfg.appId}...`));
1630
- const created = await api.createSession({
1631
- device_type: PLATFORM_DEVICE[cfg.platform].deviceType,
1632
- device_category: cfg.deviceCategory,
1633
- app_id: cfg.appId,
1634
- tunnel: true
1089
+ await api.stopSession(state.session_id).catch(() => {
1635
1090
  });
1636
- const sessionId = created.session_id;
1637
- let stopping = false;
1638
- const stopSession = async (code) => {
1639
- if (stopping) return;
1640
- stopping = true;
1641
- tunnel?.stop();
1642
- console.error(chalk10.dim("\nStopping session..."));
1643
- await api.stopSession(sessionId).catch(() => {
1644
- });
1645
- process.exit(code);
1646
- };
1647
- process.on("SIGINT", () => void stopSession(0));
1648
- process.on("SIGTERM", () => void stopSession(0));
1649
- let tunnel = null;
1650
1091
  try {
1651
- await api.startSession(sessionId);
1652
- const ready = await pollUntilTunnelReady(api, sessionId, 18e4);
1653
- for (const w of ready.warnings ?? []) {
1654
- console.error(chalk10.yellow(`\u26A0 ${w}`));
1655
- }
1656
- let everConnected = false;
1657
- let waitingPrinted = false;
1658
- tunnel = new TunnelClient({
1659
- url: ready.tunnel_url,
1660
- metroPort: cfg.metroPort ?? 8081,
1661
- // Re-mint the URL on each reconnect: the tunnel token has a ~1h TTL and
1662
- // a worker restart drops the proxy registration, so the original URL
1663
- // eventually goes stale. Every authorized GET /sessions/{id} re-signs a
1664
- // fresh tunnel token. Null (session stopped/failed) ends the tunnel.
1665
- refreshUrl: async () => {
1666
- const info = await api.getSession(sessionId);
1667
- if (info.status === "stopped" || info.status === "failed") return null;
1668
- return info.tunnel_url ?? null;
1669
- },
1670
- // --debug: dump raw tunnel lifecycle (close codes/reasons, reconnect
1671
- // scheduling, url refresh) to diagnose flapping. Timestamped so the
1672
- // drop→reconnect cadence is visible.
1673
- onDebug: opts.debug ? (msg) => console.error(chalk10.dim(`[tunnel ${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}`)) : void 0,
1674
- onState: (s) => {
1675
- if (s === "connected") {
1676
- everConnected = true;
1677
- console.error(chalk10.green("\u2713 tunnel connected \u2014 edit away, Fast Refresh is live"));
1678
- } else if (s === "reconnecting") {
1679
- if (!everConnected) {
1680
- if (!waitingPrinted) {
1681
- waitingPrinted = true;
1682
- console.error(
1683
- chalk10.dim("\u2026 waiting for the device to come up (tunnel not ready yet)")
1684
- );
1685
- }
1686
- return;
1687
- }
1688
- console.error(chalk10.yellow("\u2026 tunnel dropped, reconnecting"));
1689
- } else if (s === "metro-unreachable")
1690
- console.error(
1691
- chalk10.red(
1692
- `\u2717 can't reach Metro on localhost:${cfg.metroPort ?? 8081} \u2014 is it running? (yarn start)`
1693
- )
1694
- );
1695
- else if (s === "closed" && !stopping) {
1696
- console.error(chalk10.red("tunnel closed by server \u2014 session ended"));
1697
- void stopSession(1);
1698
- }
1699
- }
1700
- });
1701
- tunnel.start();
1702
- const webBase = opts.webUrl || process.env.VIBEVIEW_WEB_URL || baseUrl;
1703
- const pageUrl = `${webBase}/sandbox/${sessionId}`;
1704
- console.error(`
1705
- Watch and interact: ${chalk10.cyan(pageUrl)}
1706
- Ctrl-C to end the session.
1707
- `);
1708
- if (opts.open !== false) await openBrowser(pageUrl).catch(() => {
1709
- });
1092
+ process.kill(state.pid, "SIGTERM");
1710
1093
  } catch (err) {
1711
- console.error(chalk10.red(`error: ${err.message}`));
1712
- await stopSession(1);
1094
+ if (err.code !== "ESRCH") throw err;
1095
+ }
1096
+ clearDevState(process.cwd());
1097
+ killOwnedMetro(process.cwd());
1098
+ if (opts.json) {
1099
+ console.log(JSON.stringify({ stopped: true, session_id: state.session_id, pid: state.pid }));
1100
+ } else {
1101
+ console.error(`Stopped dev session ${state.session_id} (pid ${state.pid}).`);
1713
1102
  }
1103
+ process.exit(0);
1714
1104
  }
1715
1105
 
1716
1106
  // src/lib/formatters/devices.ts
1717
- import chalk11 from "chalk";
1107
+ import chalk10 from "chalk";
1718
1108
  var HEADERS = ["DEVICE_ID", "NAME", "TYPE", "CATEGORY", "OS", "STATUS"];
1719
1109
  function formatDevicesHuman(devices) {
1720
1110
  if (devices.length === 0) {
@@ -1730,9 +1120,9 @@ function formatDevicesHuman(devices) {
1730
1120
  ]);
1731
1121
  const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
1732
1122
  const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
1733
- const lines = [chalk11.bold(fmt(HEADERS))];
1123
+ const lines = [chalk10.bold(fmt(HEADERS))];
1734
1124
  for (const row of rows.sort(rowSort)) {
1735
- const statusColored = row[5] === "available" ? chalk11.green(row[5]) : row[5] === "busy" ? chalk11.yellow(row[5]) : chalk11.dim(row[5]);
1125
+ const statusColored = row[5] === "available" ? chalk10.green(row[5]) : row[5] === "busy" ? chalk10.yellow(row[5]) : chalk10.dim(row[5]);
1736
1126
  lines.push(fmt([row[0], row[1], row[2], row[3], row[4], statusColored]));
1737
1127
  }
1738
1128
  return lines.join("\n") + "\n";
@@ -1765,7 +1155,7 @@ async function listDevicesCommand(options) {
1765
1155
  }
1766
1156
 
1767
1157
  // src/lib/metadata.ts
1768
- import { readFileSync as readFileSync5 } from "fs";
1158
+ import { readFileSync as readFileSync2 } from "fs";
1769
1159
  var MAX_METADATA_BYTES = 16384;
1770
1160
  var MAX_METADATA_DEPTH = 4;
1771
1161
  function measureDepth(value, current = 1) {
@@ -1804,7 +1194,7 @@ function parseMetadata(raw) {
1804
1194
  function readMetadataFile(path) {
1805
1195
  let raw;
1806
1196
  try {
1807
- raw = readFileSync5(path, "utf8");
1197
+ raw = readFileSync2(path, "utf8");
1808
1198
  } catch (e) {
1809
1199
  throw new Error(`failed to read --metadata-file ${path}: ${e.message}`);
1810
1200
  }
@@ -1856,7 +1246,7 @@ function resolveMetadata(opts) {
1856
1246
  }
1857
1247
  var program = new Command();
1858
1248
  program.name("vibeview").description("VibeView CLI -- run mobile tests from CI/CD pipelines").version(pkg.version);
1859
- program.command("run-suite <suite-id>").description("Run all test cases in a suite").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--base-url <url>", "VibeView server URL (or set VIBEVIEW_BASE_URL)").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show per-case breakdown", false).option("--timeout <seconds>", "Max wait time in seconds", "300").option("--commit-sha <sha>", "Git commit SHA for GitHub status checks").option("--model <model>", "AI model to use").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
1249
+ program.command("run-suite <suite-id>").description("Run all test cases in a suite").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show per-case breakdown", false).option("--timeout <seconds>", "Max wait time in seconds", "300").option("--commit-sha <sha>", "Git commit SHA for GitHub status checks").option("--model <model>", "AI model to use").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
1860
1250
  "--device-category <category>",
1861
1251
  "Device category: phone, tablet, tv (default: phone when --device is not set)",
1862
1252
  parseDeviceCategory
@@ -1884,7 +1274,7 @@ program.command("run-suites").description("Run all suites matching a tag filter"
1884
1274
  ).option("--match <mode>", "any (default) or all", "any").option(
1885
1275
  "--platform <platform>",
1886
1276
  "Filter matched suites to one platform: ios, android, tvos, androidtv (default: no filter)"
1887
- ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--base-url <url>", "VibeView server URL (or set VIBEVIEW_BASE_URL)").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show per-suite progress", false).option("--timeout <seconds>", "Per-suite timeout, capped at 2 hours total", "300").option("--commit-sha <sha>", "Git commit SHA propagated to every SuiteRun").option("--model <model>", "AI model").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
1277
+ ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show per-suite progress", false).option("--timeout <seconds>", "Per-suite timeout, capped at 2 hours total", "300").option("--commit-sha <sha>", "Git commit SHA propagated to every SuiteRun").option("--model <model>", "AI model").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
1888
1278
  "--device-category <category>",
1889
1279
  "Device category: phone, tablet, tv (default: phone when --device is not set)",
1890
1280
  parseDeviceCategory
@@ -1904,7 +1294,7 @@ program.command("run-suites").description("Run all suites matching a tag filter"
1904
1294
  build: opts.build ? parseInt(opts.build, 10) : void 0
1905
1295
  });
1906
1296
  });
1907
- program.command("run-test <test-id>").description("Run a single test case").option("--token <token>", "API token").option("--base-url <url>", "VibeView server URL").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show step details", false).option("--timeout <seconds>", "Max wait time in seconds", "300").option("--commit-sha <sha>", "Git commit SHA for GitHub status checks").option("--model <model>", "AI model to use").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
1297
+ program.command("run-test <test-id>").description("Run a single test case").option("--token <token>", "API token").option("--output <format>", "Output format: human, json, junit", "human").option("--verbose", "Show step details", false).option("--timeout <seconds>", "Max wait time in seconds", "300").option("--commit-sha <sha>", "Git commit SHA for GitHub status checks").option("--model <model>", "AI model to use").option("--provider <provider>", "AI provider").option("--mode <mode>", "Execution mode: hybrid, ai, replay").option(
1908
1298
  "--device-category <category>",
1909
1299
  "Device category: phone, tablet, tv (default: phone when --device is not set)",
1910
1300
  parseDeviceCategory
@@ -1924,24 +1314,59 @@ program.command("run-test <test-id>").description("Run a single test case").opti
1924
1314
  build: opts.build ? parseInt(opts.build, 10) : void 0
1925
1315
  });
1926
1316
  });
1927
- program.command("list-suites").description("List available test suites").option("--token <token>", "API token").option("--base-url <url>", "VibeView server URL").option("--output <format>", "Output format: human, json", "human").option(
1317
+ program.command("list-suites").description("List available test suites").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option(
1928
1318
  "--tag <tag>",
1929
1319
  "Filter suites by tag (repeatable)",
1930
1320
  (val, prev = []) => [...prev, val],
1931
1321
  []
1932
1322
  ).option("--match <mode>", "When multiple --tag flags: any (default) or all", "any").action((opts) => listSuites(opts));
1933
- program.command("list-apps").description("List apps in your organization").option("--token <token>", "API token").option("--base-url <url>", "VibeView server URL").option("--output <format>", "Output format: human, json", "human").action((opts) => listApps(opts));
1934
- program.command("list-devices").description("List devices registered with the platform (filterable by type and category)").option("--token <token>", "API token").option("--base-url <url>", "VibeView server URL").option("--output <format>", "Output format: human, json", "human").option("--type <type>", "Filter by device type: ios, android").option("--category <category>", "Filter by category: phone, tablet, tv").option("--status <status>", "Filter by status: available, busy, offline").action((opts) => listDevicesCommand(opts));
1935
- program.command("upload-app <file>").description("Upload an app build (zipped simulator .app for iOS, .apk for Android)").option("--token <token>", "API token").option("--base-url <url>", "VibeView server URL").option("--output <format>", "Output format: human, json", "human").option("--name <name>", "App name (auto-detected if not provided)").action((file, opts) => uploadApp(file, opts));
1936
- program.command("create-test [name]").description("Create a standalone AI test case").option("--token <token>", "API token").option("--base-url <url>", "VibeView server URL").option("--output <format>", "Output format: human, json", "human").option("--app <app>", "App ID (public_id) or package name (requires --platform)").option("--platform <platform>", "Platform (ios, android) when using package name for --app").option("--steps <json>", "Test steps as inline JSON array").option("--steps-file <path>", "Path to JSON file containing test steps").option("--context <text>", "Additional context for AI test execution").option("--test-name <name>", "Test case name (alternative to positional arg)").action((name, opts) => createTest({ ...opts, testName: name || opts.testName || "CLI Test" }));
1937
- program.command("login").description("Log in to VibeView (interactive)").option("--base-url <url>", "VibeView server URL").action((opts) => login(opts));
1938
- program.command("dev").description("Stream your app from the fleet with live Metro hot-reload (RN debug builds)").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--base-url <url>", "VibeView server URL (or set VIBEVIEW_BASE_URL)").option("--web-url <url>", "Dashboard URL if different from --base-url (local dev)").option(
1323
+ program.command("list-apps").description("List apps in your organization").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").action((opts) => listApps(opts));
1324
+ program.command("list-devices").description("List devices registered with the platform (filterable by type and category)").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--type <type>", "Filter by device type: ios, android").option("--category <category>", "Filter by category: phone, tablet, tv").option("--status <status>", "Filter by status: available, busy, offline").action((opts) => listDevicesCommand(opts));
1325
+ program.command("upload-app <file>").description("Upload an app build (zipped simulator .app for iOS, .apk for Android)").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--name <name>", "App name (auto-detected if not provided)").action((file, opts) => uploadApp(file, opts));
1326
+ program.command("create-test [name]").description("Create a standalone AI test case").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--app <app>", "App ID (public_id) or package name (requires --platform)").option("--platform <platform>", "Platform (ios, android) when using package name for --app").option("--steps <json>", "Test steps as inline JSON array").option("--steps-file <path>", "Path to JSON file containing test steps").option("--context <text>", "Additional context for AI test execution").option("--test-name <name>", "Test case name (alternative to positional arg)").action((name, opts) => createTest({ ...opts, testName: name || opts.testName || "CLI Test" }));
1327
+ program.command("login").description("Log in to VibeView (interactive)").option("--base-url <url>", "VibeView server URL (defaults to https://vibeview.io)").action((opts) => login(opts));
1328
+ program.command("dev").description("Stream your app from the fleet with live Metro hot-reload (RN debug builds)").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--web-url <url>", "Dashboard URL if different from the API URL (local dev)").option(
1939
1329
  "--platform <platform>",
1940
1330
  "ios, android, tvos, or androidtv (first run is interactive otherwise)"
1941
1331
  ).option("--app <app_id>", "Uploaded debug build to run (see `vibeview list-apps`)").option("--metro-port <port>", "Local Metro port (default: 8081, remembered in vibeview.json)").option(
1942
1332
  "--device-category <category>",
1943
1333
  "phone or tablet (default: phone; TV platforms always use tv)",
1944
1334
  parseDeviceCategory
1945
- ).option("--no-open", "Do not open the browser automatically").option("--debug", "Log raw tunnel lifecycle events (close codes, reconnects) for diagnostics").action((opts) => dev(opts));
1335
+ ).option("--no-open", "Do not open the browser automatically").option("--debug", "Log raw tunnel lifecycle events (close codes, reconnects) for diagnostics").option("--json", "Emit line-delimited JSON events on stdout instead of human output", false).option(
1336
+ "--detach",
1337
+ "Run the dev session in the background; prints session info and exits (see `dev-status`, `dev-stop`)",
1338
+ false
1339
+ ).option(
1340
+ "--build",
1341
+ "Build and upload first (uses platforms.<platform>.build from vibeview.json)",
1342
+ false
1343
+ ).action((opts) => dev(opts));
1344
+ program.command("build").description("Run your configured build command and upload the result (see vibeview.json)").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option(
1345
+ "--platform <platform>",
1346
+ "ios, android, tvos, or androidtv (default: vibeview.json defaultPlatform)"
1347
+ ).option("--json", "Print the result as one JSON line", false).action((opts) => buildCommand(opts));
1348
+ program.command("dev-status").description("Show the status of a `vibeview dev --detach` session in this project").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print as one JSON line", false).action((opts) => devStatus(opts));
1349
+ program.command("dev-stop").description("Stop a `vibeview dev --detach` session in this project").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print as one JSON line", false).action((opts) => devStop(opts));
1350
+ program.command("mcp").description(
1351
+ "Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop as tools"
1352
+ ).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
1353
+ const { runMcpServer } = await import("./server-JZAEEHZT.js");
1354
+ await runMcpServer(opts);
1355
+ });
1356
+ for (const cmd of AGENT_COMMANDS) {
1357
+ const positionalParams = cmd.params.filter((p) => p.cli === "positional");
1358
+ const flagParams = cmd.params.filter((p) => p.cli !== "positional");
1359
+ const argSignature = positionalParams.map((p) => p.required ? `<${p.name}>` : `[${p.name}]`).join(" ");
1360
+ let command = program.command(argSignature ? `${cmd.verb} ${argSignature}` : cmd.verb).description(cmd.summary);
1361
+ for (const param of flagParams) {
1362
+ const flagArg = param.type === "number" ? `${param.cli} <n>` : `${param.cli} <value>`;
1363
+ command = command.option(flagArg, param.description);
1364
+ }
1365
+ command = command.option("--session <id>", "Session ID to target (or use dev-loop session state)").option("--json", "Print the raw response envelope as one JSON line", false).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)");
1366
+ command.action((...args) => {
1367
+ const opts = args[args.length - 2];
1368
+ const positionals = args.slice(0, positionalParams.length);
1369
+ return runAgentVerb(cmd, positionals, opts);
1370
+ });
1371
+ }
1946
1372
  program.parse();
1947
- //# sourceMappingURL=cli.js.map