vibeview 0.10.1 → 0.12.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/{chunk-NA3JOV5Z.js → chunk-NJU4AKHN.js} +1045 -147
- package/dist/{chunk-YD2X6TQV.js → chunk-OHRTLUBD.js} +154 -12
- package/dist/cli.js +294 -24
- package/dist/{server-B2PDQJLR.js → server-LZRNKGVA.js} +23 -12
- package/dist/{upload-app-GAZVQZYR.js → upload-app-G5SEYBTU.js} +1 -1
- package/package.json +2 -1
|
@@ -37,20 +37,55 @@ function resolveAuth(options) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// src/lib/api-client.ts
|
|
40
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
41
|
-
import {
|
|
40
|
+
import { openAsBlob, readFileSync as readFileSync2 } from "fs";
|
|
41
|
+
import { createWriteStream, renameSync, rmSync } from "fs";
|
|
42
|
+
import { basename, join as join2, resolve, sep } from "path";
|
|
43
|
+
import { pipeline } from "stream/promises";
|
|
44
|
+
import { Readable } from "stream";
|
|
45
|
+
var NotFoundError = class extends Error {
|
|
46
|
+
constructor(message) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "NotFoundError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function extractDetail(text) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(text);
|
|
54
|
+
if (parsed && typeof parsed.detail === "string") return parsed.detail;
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
42
59
|
var ApiClient = class {
|
|
43
60
|
constructor(baseUrl, token) {
|
|
44
61
|
this.baseUrl = baseUrl;
|
|
45
62
|
this.token = token;
|
|
46
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* fetch() rejects with undici's bare "TypeError: fetch failed" on any
|
|
66
|
+
* network-level failure (connection refused, DNS, TLS). Translate that
|
|
67
|
+
* into a message that names the base URL — a stray VIBEVIEW_BASE_URL
|
|
68
|
+
* pointing at a dead dev backend is indistinguishable from an outage
|
|
69
|
+
* otherwise.
|
|
70
|
+
*/
|
|
71
|
+
async fetchOrExplain(url, init) {
|
|
72
|
+
try {
|
|
73
|
+
return await fetch(url, init);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
const cause = e.cause;
|
|
76
|
+
const detail = cause?.code ? ` (${cause.code})` : "";
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Cannot reach ${this.baseUrl}${detail}. Check the server URL (--base-url flag, VIBEVIEW_BASE_URL, or ~/.vibeview/config.json) and your network.`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
47
82
|
async request(method, path, body) {
|
|
48
83
|
const url = `${this.baseUrl}/api/v1/ci${path}`;
|
|
49
84
|
const headers = {
|
|
50
85
|
Authorization: `Bearer ${this.token}`,
|
|
51
86
|
"Content-Type": "application/json"
|
|
52
87
|
};
|
|
53
|
-
const resp = await
|
|
88
|
+
const resp = await this.fetchOrExplain(url, {
|
|
54
89
|
method,
|
|
55
90
|
headers,
|
|
56
91
|
body: body ? JSON.stringify(body) : void 0
|
|
@@ -58,8 +93,8 @@ var ApiClient = class {
|
|
|
58
93
|
if (!resp.ok) {
|
|
59
94
|
const text = await resp.text().catch(() => "");
|
|
60
95
|
if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
|
|
61
|
-
if (resp.status === 404) throw new
|
|
62
|
-
throw new Error(`API error ${resp.status}: ${text}`);
|
|
96
|
+
if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
|
|
97
|
+
throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
|
|
63
98
|
}
|
|
64
99
|
return resp.json();
|
|
65
100
|
}
|
|
@@ -122,7 +157,7 @@ var ApiClient = class {
|
|
|
122
157
|
Authorization: `Bearer ${this.token}`,
|
|
123
158
|
"Content-Type": "application/json"
|
|
124
159
|
};
|
|
125
|
-
const resp = await
|
|
160
|
+
const resp = await this.fetchOrExplain(url, {
|
|
126
161
|
method,
|
|
127
162
|
headers,
|
|
128
163
|
body: body ? JSON.stringify(body) : void 0
|
|
@@ -130,8 +165,8 @@ var ApiClient = class {
|
|
|
130
165
|
if (!resp.ok) {
|
|
131
166
|
const text = await resp.text().catch(() => "");
|
|
132
167
|
if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
|
|
133
|
-
if (resp.status === 404) throw new
|
|
134
|
-
throw new Error(`API error ${resp.status}: ${text}`);
|
|
168
|
+
if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
|
|
169
|
+
throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
|
|
135
170
|
}
|
|
136
171
|
return resp.json();
|
|
137
172
|
}
|
|
@@ -142,7 +177,7 @@ var ApiClient = class {
|
|
|
142
177
|
Authorization: `Bearer ${this.token}`
|
|
143
178
|
// Do NOT set Content-Type — let fetch set multipart boundary automatically
|
|
144
179
|
};
|
|
145
|
-
const resp = await
|
|
180
|
+
const resp = await this.fetchOrExplain(url, {
|
|
146
181
|
method,
|
|
147
182
|
headers,
|
|
148
183
|
body: formData
|
|
@@ -150,8 +185,8 @@ var ApiClient = class {
|
|
|
150
185
|
if (!resp.ok) {
|
|
151
186
|
const text = await resp.text().catch(() => "");
|
|
152
187
|
if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
|
|
153
|
-
if (resp.status === 404) throw new
|
|
154
|
-
throw new Error(`API error ${resp.status}: ${text}`);
|
|
188
|
+
if (resp.status === 404) throw new NotFoundError(`Not found: ${extractDetail(text) ?? path}`);
|
|
189
|
+
throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
|
|
155
190
|
}
|
|
156
191
|
return resp.json();
|
|
157
192
|
}
|
|
@@ -214,16 +249,122 @@ var ApiClient = class {
|
|
|
214
249
|
async stopSession(sessionId) {
|
|
215
250
|
await this.requestRaw("DELETE", `/sessions/${sessionId}`);
|
|
216
251
|
}
|
|
252
|
+
/** Install an already-uploaded build into a running session. Used by the
|
|
253
|
+
* Roku dev reload loop; the worker sideloads and (with launchAfterInstall)
|
|
254
|
+
* relaunches the channel in place. */
|
|
255
|
+
async installBuild(sessionId, buildId, launchAfterInstall = true) {
|
|
256
|
+
return this.requestRaw("POST", `/sessions/${sessionId}/install-build`, {
|
|
257
|
+
build_id: buildId,
|
|
258
|
+
launch_after_install: launchAfterInstall
|
|
259
|
+
});
|
|
260
|
+
}
|
|
217
261
|
async agentTool(sessionId, tool, args) {
|
|
218
262
|
return this.requestRaw("POST", `/sessions/${sessionId}/agent/tools`, { tool, args });
|
|
219
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Create up to 4 cloud builds from a single manifest + source tarball
|
|
266
|
+
* (POST /api/v1/builds/). Multipart: "manifest" is the JSON-encoded
|
|
267
|
+
* manifest, "source" is the tarball attached via fs.openAsBlob (Node >=19.8;
|
|
268
|
+
* the CLI's engines floor is >=20, see package.json) — mirrors uploadBuild's
|
|
269
|
+
* read-then-attach pattern but avoids buffering the (up to 2 GB) tarball
|
|
270
|
+
* into memory.
|
|
271
|
+
*/
|
|
272
|
+
async createCloudBuilds(manifest, tarPath) {
|
|
273
|
+
const blob = await openAsBlob(tarPath);
|
|
274
|
+
const formData = new FormData();
|
|
275
|
+
formData.append("manifest", JSON.stringify(manifest));
|
|
276
|
+
formData.append("source", blob, "source.tgz");
|
|
277
|
+
return this.requestFormData("POST", "/builds/", formData);
|
|
278
|
+
}
|
|
279
|
+
async getCloudBuild(id) {
|
|
280
|
+
return this.requestRaw("GET", `/builds/${id}`);
|
|
281
|
+
}
|
|
282
|
+
async getCloudBuildLogs(id, offset) {
|
|
283
|
+
const qs = offset !== void 0 ? `?offset=${offset}` : "";
|
|
284
|
+
return this.requestRaw("GET", `/builds/${id}/logs${qs}`);
|
|
285
|
+
}
|
|
286
|
+
async listCloudBuilds(limit, offset) {
|
|
287
|
+
const params = new URLSearchParams();
|
|
288
|
+
if (limit !== void 0) params.set("limit", String(limit));
|
|
289
|
+
if (offset !== void 0) params.set("offset", String(offset));
|
|
290
|
+
const qs = params.toString();
|
|
291
|
+
return this.requestRaw("GET", `/builds/${qs ? `?${qs}` : ""}`);
|
|
292
|
+
}
|
|
293
|
+
async cancelCloudBuild(id) {
|
|
294
|
+
return this.requestRaw("POST", `/builds/${id}/cancel`);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Download a finished build's artifact (GET /api/v1/builds/{id}/artifact),
|
|
298
|
+
* streaming the response body straight to a file in destDir. The filename
|
|
299
|
+
* is taken from Content-Disposition — RFC5987 filename* preferred over the
|
|
300
|
+
* plain filename= form — falling back to "<id>.bin" if the header is
|
|
301
|
+
* missing or unparseable.
|
|
302
|
+
*/
|
|
303
|
+
async downloadCloudBuildArtifact(id, destDir) {
|
|
304
|
+
const url = `${this.baseUrl}/api/v1/builds/${id}/artifact`;
|
|
305
|
+
const headers = {
|
|
306
|
+
Authorization: `Bearer ${this.token}`
|
|
307
|
+
};
|
|
308
|
+
const resp = await this.fetchOrExplain(url, { method: "GET", headers });
|
|
309
|
+
if (!resp.ok) {
|
|
310
|
+
const text = await resp.text().catch(() => "");
|
|
311
|
+
if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
|
|
312
|
+
if (resp.status === 404)
|
|
313
|
+
throw new NotFoundError(`Not found: ${extractDetail(text) ?? `/builds/${id}/artifact`}`);
|
|
314
|
+
throw new Error(extractDetail(text) ?? `API error ${resp.status}: ${text}`);
|
|
315
|
+
}
|
|
316
|
+
const rawFilename = parseContentDispositionFilename(resp.headers.get("content-disposition"));
|
|
317
|
+
const filename = sanitizeDownloadFilename(rawFilename) ?? `${id}.bin`;
|
|
318
|
+
const path = join2(destDir, filename);
|
|
319
|
+
const resolvedDestDir = resolve(destDir);
|
|
320
|
+
const resolvedPath = resolve(path);
|
|
321
|
+
if (resolvedPath !== join2(resolvedDestDir, filename) || !resolvedPath.startsWith(resolvedDestDir + sep)) {
|
|
322
|
+
throw new Error(`Refusing to write artifact outside destination directory: ${filename}`);
|
|
323
|
+
}
|
|
324
|
+
if (!resp.body) {
|
|
325
|
+
createWriteStream(path).end();
|
|
326
|
+
return { filename, path };
|
|
327
|
+
}
|
|
328
|
+
const partialPath = `${path}.partial`;
|
|
329
|
+
try {
|
|
330
|
+
await pipeline(Readable.fromWeb(resp.body), createWriteStream(partialPath));
|
|
331
|
+
renameSync(partialPath, path);
|
|
332
|
+
} catch (err) {
|
|
333
|
+
rmSync(partialPath, { force: true });
|
|
334
|
+
throw err;
|
|
335
|
+
}
|
|
336
|
+
return { filename, path };
|
|
337
|
+
}
|
|
220
338
|
};
|
|
339
|
+
function parseContentDispositionFilename(header) {
|
|
340
|
+
if (!header) return void 0;
|
|
341
|
+
const extMatch = header.match(/filename\*\s*=\s*[^']*''([^;]+)/i);
|
|
342
|
+
if (extMatch) {
|
|
343
|
+
try {
|
|
344
|
+
return decodeURIComponent(extMatch[1].trim());
|
|
345
|
+
} catch {
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const plainMatch = header.match(/filename\s*=\s*"?([^";]+)"?/i);
|
|
349
|
+
if (plainMatch) {
|
|
350
|
+
return plainMatch[1].trim();
|
|
351
|
+
}
|
|
352
|
+
return void 0;
|
|
353
|
+
}
|
|
354
|
+
function sanitizeDownloadFilename(raw) {
|
|
355
|
+
if (!raw) return void 0;
|
|
356
|
+
if (raw.includes("\0")) return void 0;
|
|
357
|
+
const base = basename(raw);
|
|
358
|
+
if (!base || base === "." || base === "..") return void 0;
|
|
359
|
+
if (base.includes("/") || base.includes("\\")) return void 0;
|
|
360
|
+
return base;
|
|
361
|
+
}
|
|
221
362
|
|
|
222
363
|
// src/lib/spinner.ts
|
|
223
364
|
import ora from "ora";
|
|
224
365
|
function createSpinner(text) {
|
|
225
366
|
const isCI = process.env.CI === "true" || process.env.CI === "1" || !process.stdout.isTTY;
|
|
226
|
-
return ora({ text, isEnabled: !isCI, stream: process.stderr });
|
|
367
|
+
return ora({ text, isEnabled: !isCI, stream: process.stderr, discardStdin: false });
|
|
227
368
|
}
|
|
228
369
|
|
|
229
370
|
// src/lib/types.ts
|
|
@@ -294,6 +435,7 @@ export {
|
|
|
294
435
|
loadConfig,
|
|
295
436
|
saveConfig,
|
|
296
437
|
resolveAuth,
|
|
438
|
+
NotFoundError,
|
|
297
439
|
ApiClient,
|
|
298
440
|
createSpinner,
|
|
299
441
|
EXIT_PASS,
|
package/dist/cli.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
AGENT_COMMANDS,
|
|
4
|
+
BUILD_TERMINAL_STATUSES,
|
|
4
5
|
buildCommand,
|
|
5
6
|
clearDevState,
|
|
6
7
|
dev,
|
|
8
|
+
fetchFinalSummary,
|
|
7
9
|
killOwnedMetro,
|
|
10
|
+
printOutcomeLine,
|
|
8
11
|
readDevState,
|
|
9
12
|
readMetroState,
|
|
10
13
|
runAgentVerb
|
|
11
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-NJU4AKHN.js";
|
|
12
15
|
import {
|
|
13
16
|
ApiClient,
|
|
14
17
|
EXIT_INFRA_ERROR,
|
|
15
18
|
EXIT_PASS,
|
|
16
19
|
EXIT_TEST_FAIL,
|
|
20
|
+
NotFoundError,
|
|
17
21
|
createSpinner,
|
|
18
22
|
loadConfig,
|
|
19
23
|
resolveAuth,
|
|
20
24
|
saveConfig,
|
|
21
25
|
uploadApp
|
|
22
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-OHRTLUBD.js";
|
|
23
27
|
|
|
24
28
|
// src/cli.ts
|
|
25
29
|
import { createRequire } from "module";
|
|
@@ -1022,6 +1026,37 @@ Opening browser to log in...
|
|
|
1022
1026
|
process.exit(EXIT_INFRA_ERROR);
|
|
1023
1027
|
}
|
|
1024
1028
|
|
|
1029
|
+
// src/commands/logout.ts
|
|
1030
|
+
async function logout() {
|
|
1031
|
+
const config = loadConfig();
|
|
1032
|
+
if (!config.token) {
|
|
1033
|
+
console.log("Not logged in (no token saved in ~/.vibeview/config.json).");
|
|
1034
|
+
if (process.env.VIBEVIEW_API_TOKEN) {
|
|
1035
|
+
console.log("Note: VIBEVIEW_API_TOKEN is set; logout does not manage env tokens.");
|
|
1036
|
+
}
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const baseUrl = config.baseUrl || "https://vibeview.io";
|
|
1040
|
+
let revoked = false;
|
|
1041
|
+
try {
|
|
1042
|
+
const resp = await fetch(`${baseUrl}/api/v1/auth/cli-token/revoke`, {
|
|
1043
|
+
method: "POST",
|
|
1044
|
+
headers: { Authorization: `Bearer ${config.token}` }
|
|
1045
|
+
});
|
|
1046
|
+
revoked = resp.ok || resp.status === 401;
|
|
1047
|
+
} catch {
|
|
1048
|
+
revoked = false;
|
|
1049
|
+
}
|
|
1050
|
+
saveConfig({ ...config, token: void 0 });
|
|
1051
|
+
if (revoked) {
|
|
1052
|
+
console.log("Logged out. Token revoked and removed from ~/.vibeview/config.json.");
|
|
1053
|
+
} else {
|
|
1054
|
+
console.log("Logged out. Removed token from ~/.vibeview/config.json.");
|
|
1055
|
+
console.log(` Warning: could not reach ${baseUrl} to revoke the token server-side.`);
|
|
1056
|
+
console.log(" It stays active until revoked in the dashboard (Settings -> API Tokens).");
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1025
1060
|
// src/commands/dev-status.ts
|
|
1026
1061
|
import chalk9 from "chalk";
|
|
1027
1062
|
async function devStatus(opts) {
|
|
@@ -1103,9 +1138,217 @@ async function devStop(opts) {
|
|
|
1103
1138
|
process.exit(0);
|
|
1104
1139
|
}
|
|
1105
1140
|
|
|
1106
|
-
// src/
|
|
1141
|
+
// src/commands/builds.ts
|
|
1142
|
+
import chalk11 from "chalk";
|
|
1143
|
+
|
|
1144
|
+
// src/lib/formatters/builds.ts
|
|
1107
1145
|
import chalk10 from "chalk";
|
|
1108
|
-
var HEADERS = ["
|
|
1146
|
+
var HEADERS = ["ID", "PLATFORM", "STATUS", "CREATED", "COST", "APP"];
|
|
1147
|
+
function formatCostCell(costMicro) {
|
|
1148
|
+
return costMicro > 0 ? `$${(costMicro / 1e6).toFixed(2)}` : "";
|
|
1149
|
+
}
|
|
1150
|
+
var STATUS_COLOR = {
|
|
1151
|
+
succeeded: chalk10.green,
|
|
1152
|
+
failed: chalk10.red,
|
|
1153
|
+
timed_out: chalk10.red,
|
|
1154
|
+
infra_failed: chalk10.red,
|
|
1155
|
+
canceled: chalk10.dim,
|
|
1156
|
+
running: chalk10.yellow,
|
|
1157
|
+
queued: chalk10.dim
|
|
1158
|
+
};
|
|
1159
|
+
function formatBuildsHuman(builds2) {
|
|
1160
|
+
if (builds2.length === 0) {
|
|
1161
|
+
return "No cloud builds found.\n";
|
|
1162
|
+
}
|
|
1163
|
+
const rows = builds2.map((b) => [
|
|
1164
|
+
b.id,
|
|
1165
|
+
b.platform,
|
|
1166
|
+
b.status,
|
|
1167
|
+
b.created_at ?? "",
|
|
1168
|
+
formatCostCell(b.cost_micro),
|
|
1169
|
+
b.app_public_id ?? ""
|
|
1170
|
+
]);
|
|
1171
|
+
const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
1172
|
+
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
1173
|
+
const join2 = (cells) => cells.join(" ").trimEnd();
|
|
1174
|
+
const lines = [chalk10.bold(join2(padCells(HEADERS)))];
|
|
1175
|
+
for (const row of rows) {
|
|
1176
|
+
const padded = padCells(row);
|
|
1177
|
+
const colorFn = STATUS_COLOR[row[2]] ?? ((s) => s);
|
|
1178
|
+
padded[2] = colorFn(row[2]) + padded[2].slice(row[2].length);
|
|
1179
|
+
lines.push(join2(padded));
|
|
1180
|
+
}
|
|
1181
|
+
return lines.join("\n") + "\n";
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// src/commands/builds.ts
|
|
1185
|
+
function defaultDeps(client) {
|
|
1186
|
+
return {
|
|
1187
|
+
listCloudBuilds: client.listCloudBuilds.bind(client),
|
|
1188
|
+
getCloudBuild: client.getCloudBuild.bind(client),
|
|
1189
|
+
getCloudBuildLogs: client.getCloudBuildLogs.bind(client),
|
|
1190
|
+
cancelCloudBuild: client.cancelCloudBuild.bind(client),
|
|
1191
|
+
downloadCloudBuildArtifact: client.downloadCloudBuildArtifact.bind(client)
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
1195
|
+
async function buildsList(opts, deps) {
|
|
1196
|
+
const { token, baseUrl } = resolveAuth(opts);
|
|
1197
|
+
const client = new ApiClient(baseUrl, token);
|
|
1198
|
+
const d = deps ?? defaultDeps(client);
|
|
1199
|
+
let limit;
|
|
1200
|
+
if (opts.limit !== void 0) {
|
|
1201
|
+
limit = parseInt(opts.limit, 10);
|
|
1202
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
1203
|
+
printError(opts, new Error(`invalid --limit "${opts.limit}": must be a positive integer`));
|
|
1204
|
+
process.exitCode = 1;
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
try {
|
|
1209
|
+
const { builds: builds2 } = await d.listCloudBuilds(limit);
|
|
1210
|
+
if (opts.json) {
|
|
1211
|
+
console.log(JSON.stringify(builds2, null, 2));
|
|
1212
|
+
} else {
|
|
1213
|
+
process.stdout.write(formatBuildsHuman(builds2));
|
|
1214
|
+
}
|
|
1215
|
+
process.exitCode = 0;
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
printError(opts, err);
|
|
1218
|
+
process.exitCode = 1;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
async function buildsLogs(id, opts, deps) {
|
|
1222
|
+
const { token, baseUrl } = resolveAuth(opts);
|
|
1223
|
+
const client = new ApiClient(baseUrl, token);
|
|
1224
|
+
const d = deps ?? defaultDeps(client);
|
|
1225
|
+
try {
|
|
1226
|
+
if (!opts.follow) {
|
|
1227
|
+
const logs = await d.getCloudBuildLogs(id, 0);
|
|
1228
|
+
if (opts.json) {
|
|
1229
|
+
console.log(JSON.stringify(logs));
|
|
1230
|
+
} else {
|
|
1231
|
+
if (logs.content) process.stdout.write(logs.content);
|
|
1232
|
+
console.error(chalk11.dim(`status: ${logs.status}`));
|
|
1233
|
+
}
|
|
1234
|
+
process.exitCode = 0;
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const final = await followLogs(id, opts, d);
|
|
1238
|
+
process.exitCode = final.status === "succeeded" ? 0 : 1;
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
printError(opts, err);
|
|
1241
|
+
process.exitCode = 1;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
var MAX_CONSECUTIVE_POLL_FAILURES = 5;
|
|
1245
|
+
async function followLogs(id, opts, deps) {
|
|
1246
|
+
const pollIntervalMs = deps.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
1247
|
+
let offset = 0;
|
|
1248
|
+
let status = "queued";
|
|
1249
|
+
let consecutiveFailures = 0;
|
|
1250
|
+
while (!BUILD_TERMINAL_STATUSES.has(status)) {
|
|
1251
|
+
try {
|
|
1252
|
+
const logs = await deps.getCloudBuildLogs(id, offset);
|
|
1253
|
+
if (logs.content) {
|
|
1254
|
+
if (opts.json) {
|
|
1255
|
+
console.log(JSON.stringify({ event: "build_log", id, content: logs.content }));
|
|
1256
|
+
} else {
|
|
1257
|
+
process.stdout.write(logs.content);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
offset = logs.offset;
|
|
1261
|
+
status = logs.status;
|
|
1262
|
+
consecutiveFailures = 0;
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
if (err instanceof NotFoundError) throw err;
|
|
1265
|
+
consecutiveFailures++;
|
|
1266
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
|
|
1267
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1268
|
+
throw new Error(
|
|
1269
|
+
`cloud build poll failed ${consecutiveFailures} times in a row, giving up: ${message}
|
|
1270
|
+
The build may still be running server-side \u2014 resume watching it with \`vibeview builds logs ${id} --follow\`.`
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
if (!BUILD_TERMINAL_STATUSES.has(status)) {
|
|
1275
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const fallback = {
|
|
1279
|
+
id,
|
|
1280
|
+
platform: "",
|
|
1281
|
+
status,
|
|
1282
|
+
app_public_id: null,
|
|
1283
|
+
created_at: null,
|
|
1284
|
+
started_at: null,
|
|
1285
|
+
finished_at: null,
|
|
1286
|
+
cost_micro: 0,
|
|
1287
|
+
used_free_quota: false,
|
|
1288
|
+
error_summary: null,
|
|
1289
|
+
app_build_id: null,
|
|
1290
|
+
timeout_s: 0
|
|
1291
|
+
};
|
|
1292
|
+
const final = await fetchFinalSummary(id, fallback, deps);
|
|
1293
|
+
if (opts.json) {
|
|
1294
|
+
console.log(
|
|
1295
|
+
JSON.stringify({
|
|
1296
|
+
event: "build_finished",
|
|
1297
|
+
id: final.id,
|
|
1298
|
+
status: final.status,
|
|
1299
|
+
cost_micro: final.cost_micro,
|
|
1300
|
+
error_summary: final.error_summary
|
|
1301
|
+
})
|
|
1302
|
+
);
|
|
1303
|
+
} else {
|
|
1304
|
+
printOutcomeLine(final, false);
|
|
1305
|
+
}
|
|
1306
|
+
return final;
|
|
1307
|
+
}
|
|
1308
|
+
async function buildsDownload(id, opts, deps) {
|
|
1309
|
+
const { token, baseUrl } = resolveAuth(opts);
|
|
1310
|
+
const client = new ApiClient(baseUrl, token);
|
|
1311
|
+
const d = deps ?? defaultDeps(client);
|
|
1312
|
+
const destDir = opts.out ?? process.cwd();
|
|
1313
|
+
try {
|
|
1314
|
+
const result = await d.downloadCloudBuildArtifact(id, destDir);
|
|
1315
|
+
if (opts.json) {
|
|
1316
|
+
console.log(JSON.stringify(result));
|
|
1317
|
+
} else {
|
|
1318
|
+
console.log(chalk11.green(`\u2713 saved ${result.path}`));
|
|
1319
|
+
}
|
|
1320
|
+
process.exitCode = 0;
|
|
1321
|
+
} catch (err) {
|
|
1322
|
+
printError(opts, err);
|
|
1323
|
+
process.exitCode = 1;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
async function buildsCancel(id, opts, deps) {
|
|
1327
|
+
const { token, baseUrl } = resolveAuth(opts);
|
|
1328
|
+
const client = new ApiClient(baseUrl, token);
|
|
1329
|
+
const d = deps ?? defaultDeps(client);
|
|
1330
|
+
try {
|
|
1331
|
+
const build = await d.cancelCloudBuild(id);
|
|
1332
|
+
if (opts.json) {
|
|
1333
|
+
console.log(JSON.stringify(build));
|
|
1334
|
+
} else {
|
|
1335
|
+
console.log(`${chalk11.cyan(build.id)} ${build.status}`);
|
|
1336
|
+
}
|
|
1337
|
+
process.exitCode = 0;
|
|
1338
|
+
} catch (err) {
|
|
1339
|
+
printError(opts, err);
|
|
1340
|
+
process.exitCode = 1;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
function printError(opts, err) {
|
|
1344
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1345
|
+
if (opts.json) console.log(JSON.stringify({ event: "error", message }));
|
|
1346
|
+
console.error(chalk11.red(`error: ${message}`));
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/lib/formatters/devices.ts
|
|
1350
|
+
import chalk12 from "chalk";
|
|
1351
|
+
var HEADERS2 = ["DEVICE_ID", "NAME", "TYPE", "CATEGORY", "OS", "STATUS"];
|
|
1109
1352
|
function formatDevicesHuman(devices) {
|
|
1110
1353
|
if (devices.length === 0) {
|
|
1111
1354
|
return "No devices registered.\n";
|
|
@@ -1118,11 +1361,11 @@ function formatDevicesHuman(devices) {
|
|
|
1118
1361
|
d.os_version,
|
|
1119
1362
|
d.status
|
|
1120
1363
|
]);
|
|
1121
|
-
const widths =
|
|
1364
|
+
const widths = HEADERS2.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
1122
1365
|
const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
|
|
1123
|
-
const lines = [
|
|
1366
|
+
const lines = [chalk12.bold(fmt(HEADERS2))];
|
|
1124
1367
|
for (const row of rows.sort(rowSort)) {
|
|
1125
|
-
const statusColored = row[5] === "available" ?
|
|
1368
|
+
const statusColored = row[5] === "available" ? chalk12.green(row[5]) : row[5] === "busy" ? chalk12.yellow(row[5]) : chalk12.dim(row[5]);
|
|
1126
1369
|
lines.push(fmt([row[0], row[1], row[2], row[3], row[4], statusColored]));
|
|
1127
1370
|
}
|
|
1128
1371
|
return lines.join("\n") + "\n";
|
|
@@ -1212,7 +1455,7 @@ import { dirname, join } from "path";
|
|
|
1212
1455
|
import { fileURLToPath } from "url";
|
|
1213
1456
|
import { spawnSync } from "child_process";
|
|
1214
1457
|
import { confirm } from "@inquirer/prompts";
|
|
1215
|
-
import
|
|
1458
|
+
import chalk13 from "chalk";
|
|
1216
1459
|
function resolveShippedSkillPath(fromDir) {
|
|
1217
1460
|
for (const rel of ["..", join("..", "..")]) {
|
|
1218
1461
|
const candidate = join(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
|
|
@@ -1240,23 +1483,23 @@ function registerMcp() {
|
|
|
1240
1483
|
encoding: "utf8"
|
|
1241
1484
|
});
|
|
1242
1485
|
if (res.status === 0) {
|
|
1243
|
-
console.log(
|
|
1486
|
+
console.log(chalk13.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
|
|
1244
1487
|
return;
|
|
1245
1488
|
}
|
|
1246
1489
|
const out = `${res.stderr ?? ""}${res.stdout ?? ""}`;
|
|
1247
1490
|
if (out.includes("already exists")) {
|
|
1248
|
-
console.log(
|
|
1491
|
+
console.log(chalk13.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
|
|
1249
1492
|
return;
|
|
1250
1493
|
}
|
|
1251
|
-
console.error(
|
|
1252
|
-
console.error(
|
|
1494
|
+
console.error(chalk13.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
|
|
1495
|
+
console.error(chalk13.yellow(" register it manually: claude mcp add vibeview -- vibeview mcp"));
|
|
1253
1496
|
}
|
|
1254
1497
|
async function agentSetup(opts) {
|
|
1255
1498
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
1256
1499
|
const shipped = resolveShippedSkillPath(moduleDir);
|
|
1257
1500
|
if (!shipped) {
|
|
1258
1501
|
console.error(
|
|
1259
|
-
|
|
1502
|
+
chalk13.red(
|
|
1260
1503
|
"error: could not find the packaged skill (skills/vibeview-agent/SKILL.md) \u2014 is this a complete install of the vibeview package?"
|
|
1261
1504
|
)
|
|
1262
1505
|
);
|
|
@@ -1265,9 +1508,9 @@ async function agentSetup(opts) {
|
|
|
1265
1508
|
}
|
|
1266
1509
|
const targetDir = skillInstallDir(opts.project ?? false, process.cwd(), homedir());
|
|
1267
1510
|
const result = installSkill(shipped, targetDir);
|
|
1268
|
-
console.log(
|
|
1511
|
+
console.log(chalk13.green(`\u2713 vibeview-agent skill ${result}: ${join(targetDir, "SKILL.md")}`));
|
|
1269
1512
|
console.log(
|
|
1270
|
-
|
|
1513
|
+
chalk13.dim(
|
|
1271
1514
|
opts.project ? " Claude Code discovers it automatically for anyone working in this project." : " Claude Code discovers it automatically in every project on this machine."
|
|
1272
1515
|
)
|
|
1273
1516
|
);
|
|
@@ -1275,7 +1518,7 @@ async function agentSetup(opts) {
|
|
|
1275
1518
|
if (!claudeCliAvailable()) {
|
|
1276
1519
|
if (opts.mcp === true) {
|
|
1277
1520
|
console.error(
|
|
1278
|
-
|
|
1521
|
+
chalk13.yellow(
|
|
1279
1522
|
"\u26A0 claude CLI not found \u2014 install Claude Code, then run: claude mcp add vibeview -- vibeview mcp"
|
|
1280
1523
|
)
|
|
1281
1524
|
);
|
|
@@ -1289,7 +1532,7 @@ async function agentSetup(opts) {
|
|
|
1289
1532
|
}
|
|
1290
1533
|
console.log(
|
|
1291
1534
|
`
|
|
1292
|
-
Suggested line for your project's ${
|
|
1535
|
+
Suggested line for your project's ${chalk13.bold("CLAUDE.md")} (or ${chalk13.bold(
|
|
1293
1536
|
"AGENTS.md"
|
|
1294
1537
|
)} for other agents):
|
|
1295
1538
|
`
|
|
@@ -1297,7 +1540,7 @@ Suggested line for your project's ${chalk11.bold("CLAUDE.md")} (or ${chalk11.bol
|
|
|
1297
1540
|
console.log(` ${CLAUDE_MD_SUGGESTION}
|
|
1298
1541
|
`);
|
|
1299
1542
|
console.log(
|
|
1300
|
-
|
|
1543
|
+
chalk13.dim(
|
|
1301
1544
|
"Other agents (Cursor, Copilot, Codex, ...): point them at the skill file above, or\nadd the MCP server per client \u2014 see https://vibeview.io/docs/agent-control"
|
|
1302
1545
|
)
|
|
1303
1546
|
);
|
|
@@ -1423,9 +1666,12 @@ program.command("list-devices").description("List devices registered with the pl
|
|
|
1423
1666
|
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));
|
|
1424
1667
|
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" }));
|
|
1425
1668
|
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));
|
|
1426
|
-
program.command("
|
|
1669
|
+
program.command("logout").description("Revoke the saved login token and remove it from ~/.vibeview/config.json").action(() => logout());
|
|
1670
|
+
program.command("dev").description(
|
|
1671
|
+
"Stream your app from the fleet with live Metro hot-reload (RN debug builds), or press `r` to re-package and reload a Roku channel in place"
|
|
1672
|
+
).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(
|
|
1427
1673
|
"--platform <platform>",
|
|
1428
|
-
"ios, android, tvos, or
|
|
1674
|
+
"ios, android, tvos, androidtv, or roku (first run is interactive otherwise)"
|
|
1429
1675
|
).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(
|
|
1430
1676
|
"--device-category <category>",
|
|
1431
1677
|
"phone or tablet (default: phone; TV platforms always use tv)",
|
|
@@ -1438,17 +1684,33 @@ program.command("dev").description("Stream your app from the fleet with live Met
|
|
|
1438
1684
|
"--build",
|
|
1439
1685
|
"Build and upload first (uses platforms.<platform>.build from vibeview.json)",
|
|
1440
1686
|
false
|
|
1687
|
+
).option(
|
|
1688
|
+
"--watch [seconds]",
|
|
1689
|
+
"Roku only: auto-reload after file changes settle (default: 2 seconds of quiet)"
|
|
1441
1690
|
).action((opts) => dev(opts));
|
|
1442
1691
|
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(
|
|
1443
1692
|
"--platform <platform>",
|
|
1444
|
-
|
|
1445
|
-
).option(
|
|
1693
|
+
'ios, android, tvos, androidtv, or roku (default: vibeview.json defaultPlatform); with --cloud, "all" targets every configured platform with a build block'
|
|
1694
|
+
).option(
|
|
1695
|
+
"--cloud",
|
|
1696
|
+
"Build in the cloud instead of locally (uploads your project source, honoring .gitignore)",
|
|
1697
|
+
false
|
|
1698
|
+
).option("--no-wait", "--cloud only: print build ids and exit without waiting for completion").option(
|
|
1699
|
+
"--download",
|
|
1700
|
+
"--cloud only: download the artifact of each succeeded build (implies wait)",
|
|
1701
|
+
false
|
|
1702
|
+
).option("--json", "Print the result as one JSON line (NDJSON events with --cloud)", false).action((opts) => buildCommand(opts));
|
|
1703
|
+
var builds = program.command("builds").description("Manage cloud builds (see `vibeview build --cloud`)");
|
|
1704
|
+
builds.command("list").description("List cloud builds in your organization").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the raw build summaries as a JSON array", false).option("--limit <n>", "Max builds to return (default 50, max 200)").action((opts) => buildsList(opts));
|
|
1705
|
+
builds.command("logs <id>").description("Print a cloud build's log output").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print NDJSON log/outcome events instead of raw text", false).option("--follow", "Keep polling until the build reaches a terminal status", false).action((id, opts) => buildsLogs(id, opts));
|
|
1706
|
+
builds.command("download <id>").description("Download a succeeded cloud build's artifact").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the saved filename/path as one JSON line", false).option("--out <dir>", "Destination directory (default: current directory)").action((id, opts) => buildsDownload(id, opts));
|
|
1707
|
+
builds.command("cancel <id>").description("Cancel a queued or running cloud build").option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").option("--json", "Print the resulting build summary as one JSON line", false).action((id, opts) => buildsCancel(id, opts));
|
|
1446
1708
|
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));
|
|
1447
1709
|
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));
|
|
1448
1710
|
program.command("mcp").description(
|
|
1449
1711
|
"Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop as tools"
|
|
1450
1712
|
).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
|
|
1451
|
-
const { runMcpServer } = await import("./server-
|
|
1713
|
+
const { runMcpServer } = await import("./server-LZRNKGVA.js");
|
|
1452
1714
|
await runMcpServer(opts);
|
|
1453
1715
|
});
|
|
1454
1716
|
program.command("agent-setup").description(
|
|
@@ -1470,4 +1732,12 @@ for (const cmd of AGENT_COMMANDS) {
|
|
|
1470
1732
|
return runAgentVerb(cmd, positionals, opts);
|
|
1471
1733
|
});
|
|
1472
1734
|
}
|
|
1473
|
-
program.
|
|
1735
|
+
program.parseAsync().catch((e) => {
|
|
1736
|
+
if (e instanceof Error && e.name === "ExitPromptError") {
|
|
1737
|
+
process.exit(130);
|
|
1738
|
+
}
|
|
1739
|
+
if (process.env.VIBEVIEW_DEBUG) throw e;
|
|
1740
|
+
process.stderr.write(`error: ${e instanceof Error ? e.message : String(e)}
|
|
1741
|
+
`);
|
|
1742
|
+
process.exit(EXIT_INFRA_ERROR);
|
|
1743
|
+
});
|