vibeview 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/chunk-WG2XR45H.js +2011 -0
- package/dist/chunk-YD2X6TQV.js +304 -0
- package/dist/cli.js +147 -722
- package/dist/server-S3D724K3.js +367 -0
- package/dist/upload-app-GAZVQZYR.js +9 -0
- package/package.json +9 -7
- package/skills/vibeview-agent/SKILL.md +210 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/upload-app.ts
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
|
|
7
|
+
// src/lib/config.ts
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
var CONFIG_DIR = join(homedir(), ".vibeview");
|
|
12
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
13
|
+
function loadConfig() {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
16
|
+
} catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function saveConfig(config) {
|
|
21
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
22
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/lib/auth.ts
|
|
26
|
+
function resolveAuth(options) {
|
|
27
|
+
const token = options.token || process.env.VIBEVIEW_API_TOKEN || loadConfig().token;
|
|
28
|
+
if (!token) {
|
|
29
|
+
console.error("Error: No API token found.");
|
|
30
|
+
console.error(
|
|
31
|
+
"Set VIBEVIEW_API_TOKEN environment variable, use --token flag, or run: vibeview login"
|
|
32
|
+
);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
const baseUrl = options.baseUrl || process.env.VIBEVIEW_BASE_URL || loadConfig().baseUrl || "https://vibeview.io";
|
|
36
|
+
return { token, baseUrl };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/lib/api-client.ts
|
|
40
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
41
|
+
import { basename } from "path";
|
|
42
|
+
var ApiClient = class {
|
|
43
|
+
constructor(baseUrl, token) {
|
|
44
|
+
this.baseUrl = baseUrl;
|
|
45
|
+
this.token = token;
|
|
46
|
+
}
|
|
47
|
+
async request(method, path, body) {
|
|
48
|
+
const url = `${this.baseUrl}/api/v1/ci${path}`;
|
|
49
|
+
const headers = {
|
|
50
|
+
Authorization: `Bearer ${this.token}`,
|
|
51
|
+
"Content-Type": "application/json"
|
|
52
|
+
};
|
|
53
|
+
const resp = await fetch(url, {
|
|
54
|
+
method,
|
|
55
|
+
headers,
|
|
56
|
+
body: body ? JSON.stringify(body) : void 0
|
|
57
|
+
});
|
|
58
|
+
if (!resp.ok) {
|
|
59
|
+
const text = await resp.text().catch(() => "");
|
|
60
|
+
if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
|
|
61
|
+
if (resp.status === 404) throw new Error(`Not found: ${path}`);
|
|
62
|
+
throw new Error(`API error ${resp.status}: ${text}`);
|
|
63
|
+
}
|
|
64
|
+
return resp.json();
|
|
65
|
+
}
|
|
66
|
+
async listSuites(opts) {
|
|
67
|
+
const params = new URLSearchParams();
|
|
68
|
+
if (opts?.tags && opts.tags.length > 0) params.set("tags", opts.tags.join(","));
|
|
69
|
+
if (opts?.match) params.set("match", opts.match);
|
|
70
|
+
const qs = params.toString();
|
|
71
|
+
return this.request("GET", `/list-suites${qs ? `?${qs}` : ""}`);
|
|
72
|
+
}
|
|
73
|
+
async runSuite(suiteId, options) {
|
|
74
|
+
return this.request("POST", "/run-suite", {
|
|
75
|
+
suite_id: suiteId,
|
|
76
|
+
commit_sha: options?.commitSha,
|
|
77
|
+
model: options?.model,
|
|
78
|
+
provider: options?.provider,
|
|
79
|
+
mode: options?.mode,
|
|
80
|
+
// When pinning a specific device, omit device_category — the backend
|
|
81
|
+
// rejects the combo.
|
|
82
|
+
device_category: options?.device ? void 0 : options?.deviceCategory ?? "phone",
|
|
83
|
+
device_id: options?.device,
|
|
84
|
+
build_id: options?.build,
|
|
85
|
+
metadata: options?.metadata
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async runTest(testId, options) {
|
|
89
|
+
return this.request("POST", "/run-test", {
|
|
90
|
+
test_id: testId,
|
|
91
|
+
commit_sha: options?.commitSha,
|
|
92
|
+
model: options?.model,
|
|
93
|
+
provider: options?.provider,
|
|
94
|
+
mode: options?.mode,
|
|
95
|
+
device_category: options?.device ? void 0 : options?.deviceCategory ?? "phone",
|
|
96
|
+
device_id: options?.device,
|
|
97
|
+
build_id: options?.build,
|
|
98
|
+
metadata: options?.metadata
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** Cancel a still-queued CI run by its queue entry id. Idempotent best-effort. */
|
|
102
|
+
async cancelQueueEntry(queueEntryId) {
|
|
103
|
+
await this.request("DELETE", `/queue/${queueEntryId}`);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Request cancellation of a still-running multi-suite run.
|
|
107
|
+
* The aggregate transitions to CANCELLED on the runner's next
|
|
108
|
+
* aggregate-status write; the active child suite stops at its
|
|
109
|
+
* next case boundary.
|
|
110
|
+
*/
|
|
111
|
+
async cancelMultiSuiteRun(aggregateId) {
|
|
112
|
+
await this.request("DELETE", `/multi-suite-runs/${aggregateId}`);
|
|
113
|
+
}
|
|
114
|
+
async getRunStatus(runId) {
|
|
115
|
+
const data = await this.request("GET", `/runs/${runId}`);
|
|
116
|
+
return { ...data, url: data.run_url ?? `${this.baseUrl}/tests/runs/${runId}` };
|
|
117
|
+
}
|
|
118
|
+
/** Request without /ci prefix — for non-CI endpoints like /apps, /tests */
|
|
119
|
+
async requestRaw(method, path, body) {
|
|
120
|
+
const url = `${this.baseUrl}/api/v1${path}`;
|
|
121
|
+
const headers = {
|
|
122
|
+
Authorization: `Bearer ${this.token}`,
|
|
123
|
+
"Content-Type": "application/json"
|
|
124
|
+
};
|
|
125
|
+
const resp = await fetch(url, {
|
|
126
|
+
method,
|
|
127
|
+
headers,
|
|
128
|
+
body: body ? JSON.stringify(body) : void 0
|
|
129
|
+
});
|
|
130
|
+
if (!resp.ok) {
|
|
131
|
+
const text = await resp.text().catch(() => "");
|
|
132
|
+
if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
|
|
133
|
+
if (resp.status === 404) throw new Error(`Not found: ${path}`);
|
|
134
|
+
throw new Error(`API error ${resp.status}: ${text}`);
|
|
135
|
+
}
|
|
136
|
+
return resp.json();
|
|
137
|
+
}
|
|
138
|
+
/** FormData request without /ci prefix — for file uploads */
|
|
139
|
+
async requestFormData(method, path, formData) {
|
|
140
|
+
const url = `${this.baseUrl}/api/v1${path}`;
|
|
141
|
+
const headers = {
|
|
142
|
+
Authorization: `Bearer ${this.token}`
|
|
143
|
+
// Do NOT set Content-Type — let fetch set multipart boundary automatically
|
|
144
|
+
};
|
|
145
|
+
const resp = await fetch(url, {
|
|
146
|
+
method,
|
|
147
|
+
headers,
|
|
148
|
+
body: formData
|
|
149
|
+
});
|
|
150
|
+
if (!resp.ok) {
|
|
151
|
+
const text = await resp.text().catch(() => "");
|
|
152
|
+
if (resp.status === 401) throw new Error("Authentication failed. Check your API token.");
|
|
153
|
+
if (resp.status === 404) throw new Error(`Not found: ${path}`);
|
|
154
|
+
throw new Error(`API error ${resp.status}: ${text}`);
|
|
155
|
+
}
|
|
156
|
+
return resp.json();
|
|
157
|
+
}
|
|
158
|
+
async listApps() {
|
|
159
|
+
return this.requestRaw("GET", "/apps/");
|
|
160
|
+
}
|
|
161
|
+
async uploadApp(filePath, name) {
|
|
162
|
+
const fileBuffer = readFileSync2(filePath);
|
|
163
|
+
const blob = new Blob([fileBuffer]);
|
|
164
|
+
const formData = new FormData();
|
|
165
|
+
formData.append("file", blob, basename(filePath));
|
|
166
|
+
if (name) {
|
|
167
|
+
formData.append("name", name);
|
|
168
|
+
}
|
|
169
|
+
return this.requestFormData("POST", "/apps/upload", formData);
|
|
170
|
+
}
|
|
171
|
+
async uploadBuild(appPublicId, filePath, version) {
|
|
172
|
+
const fileBuffer = readFileSync2(filePath);
|
|
173
|
+
const blob = new Blob([fileBuffer]);
|
|
174
|
+
const formData = new FormData();
|
|
175
|
+
formData.append("file", blob, basename(filePath));
|
|
176
|
+
if (version) {
|
|
177
|
+
formData.append("version", version);
|
|
178
|
+
}
|
|
179
|
+
return this.requestFormData("POST", `/apps/${appPublicId}/builds`, formData);
|
|
180
|
+
}
|
|
181
|
+
async createTest(body) {
|
|
182
|
+
return this.requestRaw("POST", "/tests/cases/", body);
|
|
183
|
+
}
|
|
184
|
+
async runSuites(body) {
|
|
185
|
+
return this.request("POST", "/run-suites", body);
|
|
186
|
+
}
|
|
187
|
+
async getMultiSuiteRun(aggregateId) {
|
|
188
|
+
return this.request("GET", `/multi-suite-runs/${aggregateId}`);
|
|
189
|
+
}
|
|
190
|
+
async listDevices(opts) {
|
|
191
|
+
const params = new URLSearchParams();
|
|
192
|
+
if (opts?.type) params.set("device_type", opts.type);
|
|
193
|
+
if (opts?.category) params.set("category", opts.category);
|
|
194
|
+
const qs = params.toString();
|
|
195
|
+
const url = qs ? `/devices?${qs}` : "/devices";
|
|
196
|
+
const all = await this.requestRaw("GET", url);
|
|
197
|
+
if (opts?.status) {
|
|
198
|
+
return all.filter((d) => d.status === opts.status);
|
|
199
|
+
}
|
|
200
|
+
return all;
|
|
201
|
+
}
|
|
202
|
+
async getDevice(deviceId) {
|
|
203
|
+
return this.requestRaw("GET", `/devices/${deviceId}`);
|
|
204
|
+
}
|
|
205
|
+
async createSession(body) {
|
|
206
|
+
return this.requestRaw("POST", "/sessions/", body);
|
|
207
|
+
}
|
|
208
|
+
async startSession(sessionId, deviceId) {
|
|
209
|
+
return this.requestRaw("POST", `/sessions/${sessionId}/start`, { device_id: deviceId });
|
|
210
|
+
}
|
|
211
|
+
async getSession(sessionId) {
|
|
212
|
+
return this.requestRaw("GET", `/sessions/${sessionId}`);
|
|
213
|
+
}
|
|
214
|
+
async stopSession(sessionId) {
|
|
215
|
+
await this.requestRaw("DELETE", `/sessions/${sessionId}`);
|
|
216
|
+
}
|
|
217
|
+
async agentTool(sessionId, tool, args) {
|
|
218
|
+
return this.requestRaw("POST", `/sessions/${sessionId}/agent/tools`, { tool, args });
|
|
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
|
+
// src/lib/types.ts
|
|
230
|
+
var EXIT_PASS = 0;
|
|
231
|
+
var EXIT_TEST_FAIL = 1;
|
|
232
|
+
var EXIT_INFRA_ERROR = 2;
|
|
233
|
+
|
|
234
|
+
// src/commands/upload-app.ts
|
|
235
|
+
async function uploadAppBuild(filePath, options) {
|
|
236
|
+
if (!existsSync(filePath)) {
|
|
237
|
+
throw new Error(`File not found: ${filePath}`);
|
|
238
|
+
}
|
|
239
|
+
const { token, baseUrl } = resolveAuth(options);
|
|
240
|
+
const client = new ApiClient(baseUrl, token);
|
|
241
|
+
const result = await client.uploadApp(filePath, options.name);
|
|
242
|
+
return { result, baseUrl };
|
|
243
|
+
}
|
|
244
|
+
async function uploadApp(filePath, options) {
|
|
245
|
+
if (!existsSync(filePath)) {
|
|
246
|
+
console.error(chalk.red(`Error: File not found: ${filePath}`));
|
|
247
|
+
process.exit(EXIT_INFRA_ERROR);
|
|
248
|
+
}
|
|
249
|
+
const { baseUrl } = resolveAuth(options);
|
|
250
|
+
const spinner = createSpinner("Uploading app...");
|
|
251
|
+
spinner.start();
|
|
252
|
+
try {
|
|
253
|
+
const { result } = await uploadAppBuild(filePath, options);
|
|
254
|
+
const isNew = result.is_new !== false;
|
|
255
|
+
spinner.succeed(
|
|
256
|
+
isNew ? `New app created: ${result.name}` : `Build added to existing app: ${result.name}`
|
|
257
|
+
);
|
|
258
|
+
if (options.output === "json") {
|
|
259
|
+
console.log(
|
|
260
|
+
JSON.stringify(
|
|
261
|
+
{
|
|
262
|
+
app_id: result.public_id,
|
|
263
|
+
name: result.name,
|
|
264
|
+
package_name: result.package_name,
|
|
265
|
+
platform: result.platform,
|
|
266
|
+
is_new: isNew,
|
|
267
|
+
url: `${baseUrl}/apps/${result.public_id}`,
|
|
268
|
+
warnings: result.warnings ?? []
|
|
269
|
+
},
|
|
270
|
+
null,
|
|
271
|
+
2
|
|
272
|
+
)
|
|
273
|
+
);
|
|
274
|
+
} else {
|
|
275
|
+
console.log(
|
|
276
|
+
`
|
|
277
|
+
${chalk.cyan(result.public_id)} ${result.name} ${chalk.dim(`[${result.platform}]`)}`
|
|
278
|
+
);
|
|
279
|
+
console.log(` ${chalk.dim(result.package_name)}`);
|
|
280
|
+
console.log(chalk.dim(` ${baseUrl}/apps/${result.public_id}
|
|
281
|
+
`));
|
|
282
|
+
}
|
|
283
|
+
for (const w of result.warnings ?? []) {
|
|
284
|
+
console.error(chalk.yellow(`
|
|
285
|
+
\u26A0 ${w}`));
|
|
286
|
+
}
|
|
287
|
+
} catch (err) {
|
|
288
|
+
spinner.fail(err instanceof Error ? err.message : "Upload failed");
|
|
289
|
+
process.exit(EXIT_INFRA_ERROR);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export {
|
|
294
|
+
loadConfig,
|
|
295
|
+
saveConfig,
|
|
296
|
+
resolveAuth,
|
|
297
|
+
ApiClient,
|
|
298
|
+
createSpinner,
|
|
299
|
+
EXIT_PASS,
|
|
300
|
+
EXIT_TEST_FAIL,
|
|
301
|
+
EXIT_INFRA_ERROR,
|
|
302
|
+
uploadAppBuild,
|
|
303
|
+
uploadApp
|
|
304
|
+
};
|