vibeview 0.7.0 → 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 +105 -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
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
|
|
1129
|
-
import { existsSync
|
|
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 (!
|
|
900
|
+
if (!existsSync(options.stepsFile)) {
|
|
1157
901
|
throw new Error(`Steps file not found: ${options.stepsFile}`);
|
|
1158
902
|
}
|
|
1159
|
-
return JSON.parse(
|
|
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
|
-
${
|
|
963
|
+
${chalk8.cyan(result.public_id)} ${result.name}`);
|
|
1220
964
|
if (result.app_name) {
|
|
1221
|
-
console.log(` ${
|
|
965
|
+
console.log(` ${chalk8.dim(`App: ${result.app_name}`)}`);
|
|
1222
966
|
}
|
|
1223
967
|
if (result.platform) {
|
|
1224
|
-
console.log(` ${
|
|
968
|
+
console.log(` ${chalk8.dim(`Platform: ${result.platform}`)}`);
|
|
1225
969
|
}
|
|
1226
|
-
console.log(` ${
|
|
1227
|
-
console.log(
|
|
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(
|
|
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
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
-
|
|
1427
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1042
|
+
const info = await api.getSession(state.session_id);
|
|
1043
|
+
sessionStatus = info.status;
|
|
1475
1044
|
} catch {
|
|
1476
|
-
|
|
1477
|
-
}
|
|
1478
|
-
const
|
|
1479
|
-
if (
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
);
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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
|
-
|
|
1588
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1712
|
-
|
|
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
|
|
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 = [
|
|
1123
|
+
const lines = [chalk10.bold(fmt(HEADERS))];
|
|
1734
1124
|
for (const row of rows.sort(rowSort)) {
|
|
1735
|
-
const statusColored = row[5] === "available" ?
|
|
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
|
|
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 =
|
|
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("--
|
|
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("--
|
|
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("--
|
|
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("--
|
|
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("--
|
|
1934
|
-
program.command("list-devices").description("List devices registered with the platform (filterable by type and category)").option("--token <token>", "API token").option("--
|
|
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("--
|
|
1936
|
-
program.command("create-test [name]").description("Create a standalone AI test case").option("--token <token>", "API token").option("--
|
|
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("--
|
|
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").
|
|
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
|