vibeview 0.25.0 → 0.27.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-GEKYHEWU.js → chunk-46X7UN6Y.js} +272 -199
- package/dist/{chunk-RFFCCPGZ.js → chunk-U7FCGMWG.js} +24 -4
- package/dist/cli.js +83 -60
- package/dist/{server-6EUBLXKE.js → server-XJREB7KU.js} +8 -7
- package/dist/{upload-app-E32EZXYQ.js → upload-app-QUUXKNR2.js} +1 -1
- package/package.json +1 -1
- package/skills/vibeview-agent/SKILL.md +36 -17
|
@@ -1,170 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
ApiClient,
|
|
4
|
+
AuthError,
|
|
5
|
+
NotFoundError,
|
|
4
6
|
UsageError,
|
|
5
7
|
createSpinner,
|
|
8
|
+
errorExitCode,
|
|
6
9
|
loadConfig,
|
|
7
10
|
resolveAuth,
|
|
8
11
|
uploadAppBuild
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
|
|
11
|
-
// src/lib/dev-build.ts
|
|
12
|
-
function parseBuildId(raw) {
|
|
13
|
-
const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw.trim() : "";
|
|
14
|
-
if (!/^\d+$/.test(text) || Number(text) < 1) {
|
|
15
|
-
throw new Error(
|
|
16
|
-
`--build expects a build id (a positive number, see \`vibeview list-builds <app>\`), got "${String(raw)}"`
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
return Number(text);
|
|
20
|
-
}
|
|
21
|
-
function buildKind(isDebug) {
|
|
22
|
-
if (isDebug === true) return "debug";
|
|
23
|
-
if (isDebug === false) return "release";
|
|
24
|
-
return "unknown";
|
|
25
|
-
}
|
|
26
|
-
function checkDevBuild(app, buildId, platform) {
|
|
27
|
-
if (platform === "roku") {
|
|
28
|
-
throw new Error(
|
|
29
|
-
"a build id cannot be pinned on roku \u2014 `vibeview dev` packages and sideloads the channel itself"
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
if (app.platform !== platform) {
|
|
33
|
-
throw new Error(`app ${app.public_id} is a ${app.platform} app, not ${platform}`);
|
|
34
|
-
}
|
|
35
|
-
const build = app.builds.find((b) => b.id === buildId);
|
|
36
|
-
if (!build) {
|
|
37
|
-
throw new Error(
|
|
38
|
-
`build ${buildId} is not a build of app ${app.public_id} \u2014 see \`vibeview list-builds ${app.public_id}\``
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
if (build.is_debug !== true) {
|
|
42
|
-
const why = build.is_debug === false ? "it runs standalone (a release build, or one with an embedded JS bundle)" : "it could not be identified as a Metro debug build";
|
|
43
|
-
throw new Error(
|
|
44
|
-
`build ${buildId} is not a debug build \u2014 ${why}. \`vibeview dev\` needs a ${platform} debug simulator/emulator build without an embedded JS bundle (see \`vibeview list-builds ${app.public_id}\`)`
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
return build;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// src/commands/list-builds.ts
|
|
51
|
-
import chalk from "chalk";
|
|
52
|
-
function buildRows(app) {
|
|
53
|
-
return app.builds.map((b) => ({
|
|
54
|
-
id: b.id,
|
|
55
|
-
version: b.version ?? null,
|
|
56
|
-
build_number: b.build_number ?? null,
|
|
57
|
-
platform: app.platform,
|
|
58
|
-
kind: buildKind(b.is_debug),
|
|
59
|
-
is_debug: b.is_debug ?? null,
|
|
60
|
-
uploaded_at: b.uploaded_at ?? null,
|
|
61
|
-
note: b.note ?? null
|
|
62
|
-
}));
|
|
63
|
-
}
|
|
64
|
-
var HEADERS = ["ID", "VERSION", "PLATFORM", "KIND", "UPLOADED", "NOTE"];
|
|
65
|
-
function formatBuildRowsHuman(app, rows) {
|
|
66
|
-
if (rows.length === 0) {
|
|
67
|
-
return `No builds uploaded for ${app.name} (${app.public_id}) \u2014 upload one with \`vibeview upload-app\`.
|
|
68
|
-
`;
|
|
69
|
-
}
|
|
70
|
-
const cells = rows.map((r) => [
|
|
71
|
-
String(r.id),
|
|
72
|
-
r.version ? `${r.version}${r.build_number ? ` (${r.build_number})` : ""}` : r.build_number ?? "",
|
|
73
|
-
r.platform,
|
|
74
|
-
r.kind,
|
|
75
|
-
r.uploaded_at ?? "",
|
|
76
|
-
(r.note ?? "").split("\n")[0]
|
|
77
|
-
]);
|
|
78
|
-
const widths = HEADERS.map((h, i) => Math.max(h.length, ...cells.map((c) => c[i].length)));
|
|
79
|
-
const pad = (row) => row.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
|
|
80
|
-
const lines = [chalk.bold(pad(HEADERS)), ...cells.map(pad)];
|
|
81
|
-
return lines.join("\n") + "\n";
|
|
82
|
-
}
|
|
83
|
-
async function listBuilds(appId, opts, deps) {
|
|
84
|
-
const { token, baseUrl } = resolveAuth(opts);
|
|
85
|
-
const d = deps ?? { getApp: (id) => new ApiClient(baseUrl, token).getApp(id) };
|
|
86
|
-
try {
|
|
87
|
-
const app = await d.getApp(appId);
|
|
88
|
-
const rows = buildRows(app);
|
|
89
|
-
if (opts.json) {
|
|
90
|
-
console.log(JSON.stringify(rows, null, 2));
|
|
91
|
-
} else {
|
|
92
|
-
process.stdout.write(formatBuildRowsHuman(app, rows));
|
|
93
|
-
}
|
|
94
|
-
process.exitCode = 0;
|
|
95
|
-
} catch (err) {
|
|
96
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
-
if (opts.json) console.log(JSON.stringify({ event: "error", message }));
|
|
98
|
-
console.error(chalk.red(`error: ${message}`));
|
|
99
|
-
process.exitCode = 1;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// src/lib/dev-state.ts
|
|
104
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
105
|
-
import { dirname, join } from "path";
|
|
106
|
-
var STATE_DIR = ".vibeview";
|
|
107
|
-
var STATE_FILE = "dev-state.json";
|
|
108
|
-
function statePath(cwd) {
|
|
109
|
-
return join(cwd, STATE_DIR, STATE_FILE);
|
|
110
|
-
}
|
|
111
|
-
function isPidAlive(pid) {
|
|
112
|
-
try {
|
|
113
|
-
process.kill(pid, 0);
|
|
114
|
-
return true;
|
|
115
|
-
} catch (err) {
|
|
116
|
-
return err.code === "EPERM";
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
function ensureGitignored(cwd) {
|
|
120
|
-
try {
|
|
121
|
-
const gitignore = join(cwd, ".gitignore");
|
|
122
|
-
if (!existsSync(gitignore)) return;
|
|
123
|
-
const content = readFileSync(gitignore, "utf-8");
|
|
124
|
-
const covered = content.split("\n").map((l) => l.trim()).some((l) => l === ".vibeview" || l === ".vibeview/" || l === "/.vibeview");
|
|
125
|
-
if (covered) return;
|
|
126
|
-
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
127
|
-
writeFileSync(gitignore, `${content}${prefix}
|
|
128
|
-
# VibeView local session state
|
|
129
|
-
.vibeview/
|
|
130
|
-
`);
|
|
131
|
-
} catch {
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
function writeDevState(cwd, state) {
|
|
135
|
-
const path = statePath(cwd);
|
|
136
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
137
|
-
writeFileSync(path, JSON.stringify(state, null, 2) + "\n");
|
|
138
|
-
ensureGitignored(cwd);
|
|
139
|
-
}
|
|
140
|
-
function readDevState(cwd) {
|
|
141
|
-
let raw;
|
|
142
|
-
try {
|
|
143
|
-
raw = readFileSync(statePath(cwd), "utf-8");
|
|
144
|
-
} catch {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
let parsed;
|
|
148
|
-
try {
|
|
149
|
-
parsed = JSON.parse(raw);
|
|
150
|
-
} catch {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
if (typeof parsed.pid !== "number" || !isPidAlive(parsed.pid)) {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
return parsed;
|
|
157
|
-
}
|
|
158
|
-
function clearDevState(cwd) {
|
|
159
|
-
const path = statePath(cwd);
|
|
160
|
-
if (existsSync(path)) {
|
|
161
|
-
rmSync(path, { force: true });
|
|
162
|
-
}
|
|
163
|
-
}
|
|
12
|
+
} from "./chunk-U7FCGMWG.js";
|
|
164
13
|
|
|
165
14
|
// src/lib/dev-config.ts
|
|
166
|
-
import { readFileSync
|
|
167
|
-
import { join
|
|
15
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
168
17
|
var DEV_PLATFORMS = ["ios", "android", "tvos", "androidtv", "roku"];
|
|
169
18
|
var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
170
19
|
var PLATFORM_NEEDS_METRO = {
|
|
@@ -178,10 +27,25 @@ var FILE = "vibeview.json";
|
|
|
178
27
|
function isDevPlatform(p) {
|
|
179
28
|
return typeof p === "string" && DEV_PLATFORMS.includes(p);
|
|
180
29
|
}
|
|
30
|
+
var PLATFORM_LABELS = {
|
|
31
|
+
ios: "iOS",
|
|
32
|
+
android: "Android",
|
|
33
|
+
tvos: "tvOS",
|
|
34
|
+
androidtv: "Android TV",
|
|
35
|
+
roku: "Roku"
|
|
36
|
+
};
|
|
37
|
+
function platformLabel(platform) {
|
|
38
|
+
return isDevPlatform(platform) ? PLATFORM_LABELS[platform] : platform;
|
|
39
|
+
}
|
|
40
|
+
function platformWithArticle(platform) {
|
|
41
|
+
const label = platformLabel(platform);
|
|
42
|
+
const article = /^[aeiou]/i.test(label) ? "an" : "a";
|
|
43
|
+
return `${article} ${label}`;
|
|
44
|
+
}
|
|
181
45
|
function loadDevConfig(cwd) {
|
|
182
46
|
let raw;
|
|
183
47
|
try {
|
|
184
|
-
raw =
|
|
48
|
+
raw = readFileSync(join(cwd, FILE), "utf-8");
|
|
185
49
|
} catch {
|
|
186
50
|
return null;
|
|
187
51
|
}
|
|
@@ -298,7 +162,7 @@ function validateBuildEnv(env, platformKey) {
|
|
|
298
162
|
}
|
|
299
163
|
}
|
|
300
164
|
function saveDevConfig(cwd, cfg) {
|
|
301
|
-
|
|
165
|
+
writeFileSync(join(cwd, FILE), JSON.stringify(cfg, null, 2) + "\n");
|
|
302
166
|
}
|
|
303
167
|
function resolveMetro(cfg, platform, portFlag) {
|
|
304
168
|
const entry = cfg?.platforms?.[platform];
|
|
@@ -342,6 +206,173 @@ function buildSavedConfig(input2) {
|
|
|
342
206
|
};
|
|
343
207
|
}
|
|
344
208
|
|
|
209
|
+
// src/lib/dev-build.ts
|
|
210
|
+
function parseBuildId(raw) {
|
|
211
|
+
const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw.trim() : "";
|
|
212
|
+
if (!/^\d+$/.test(text) || Number(text) < 1) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`--build expects a build id (a positive number, see \`vibeview list-builds <app>\`), got "${String(raw)}"`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
return Number(text);
|
|
218
|
+
}
|
|
219
|
+
function buildKind(isDebug) {
|
|
220
|
+
if (isDebug === true) return "debug";
|
|
221
|
+
if (isDebug === false) return "release";
|
|
222
|
+
return "unknown";
|
|
223
|
+
}
|
|
224
|
+
function checkDevBuild(app, buildId, platform) {
|
|
225
|
+
if (platform === "roku") {
|
|
226
|
+
throw new Error(
|
|
227
|
+
"a build id cannot be pinned on roku \u2014 `vibeview dev` packages and sideloads the channel itself"
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (app.platform !== platform) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`app ${app.public_id} is ${platformWithArticle(app.platform)} app, not ${platformLabel(platform)}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const build = app.builds.find((b) => b.id === buildId);
|
|
236
|
+
if (!build) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`build ${buildId} is not a build of app ${app.public_id} \u2014 see \`vibeview list-builds ${app.public_id}\``
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (build.is_debug !== true) {
|
|
242
|
+
const why = build.is_debug === false ? "it runs standalone (a release build, or one with an embedded JS bundle)" : "it could not be identified as a Metro debug build";
|
|
243
|
+
throw new Error(
|
|
244
|
+
`build ${buildId} is not a debug build \u2014 ${why}. \`vibeview dev\` needs ${platformWithArticle(platform)} debug simulator/emulator build without an embedded JS bundle (see \`vibeview list-builds ${app.public_id}\`)`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return build;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/commands/list-builds.ts
|
|
251
|
+
import chalk from "chalk";
|
|
252
|
+
|
|
253
|
+
// src/lib/formatters/time.ts
|
|
254
|
+
function formatUtcTimestamp(iso) {
|
|
255
|
+
if (!iso) return "";
|
|
256
|
+
const hasZone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(iso);
|
|
257
|
+
const d = new Date(hasZone ? iso : `${iso}Z`);
|
|
258
|
+
if (Number.isNaN(d.getTime())) return iso;
|
|
259
|
+
return `${d.toISOString().slice(0, 19).replace("T", " ")} UTC`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/commands/list-builds.ts
|
|
263
|
+
function buildRows(app) {
|
|
264
|
+
return app.builds.map((b) => ({
|
|
265
|
+
id: b.id,
|
|
266
|
+
version: b.version ?? null,
|
|
267
|
+
build_number: b.build_number ?? null,
|
|
268
|
+
platform: app.platform,
|
|
269
|
+
kind: buildKind(b.is_debug),
|
|
270
|
+
is_debug: b.is_debug ?? null,
|
|
271
|
+
uploaded_at: b.uploaded_at ?? null,
|
|
272
|
+
note: b.note ?? null
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
var HEADERS = ["ID", "VERSION", "PLATFORM", "KIND", "UPLOADED", "NOTE"];
|
|
276
|
+
function formatBuildRowsHuman(app, rows) {
|
|
277
|
+
if (rows.length === 0) {
|
|
278
|
+
return `No builds uploaded for ${app.name} (${app.public_id}) \u2014 upload one with \`vibeview upload-app\`.
|
|
279
|
+
`;
|
|
280
|
+
}
|
|
281
|
+
const cells = rows.map((r) => [
|
|
282
|
+
String(r.id),
|
|
283
|
+
r.version ? `${r.version}${r.build_number ? ` (${r.build_number})` : ""}` : r.build_number ?? "",
|
|
284
|
+
r.platform,
|
|
285
|
+
r.kind,
|
|
286
|
+
formatUtcTimestamp(r.uploaded_at),
|
|
287
|
+
(r.note ?? "").split("\n")[0]
|
|
288
|
+
]);
|
|
289
|
+
const widths = HEADERS.map((h, i) => Math.max(h.length, ...cells.map((c) => c[i].length)));
|
|
290
|
+
const pad = (row) => row.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
|
|
291
|
+
const lines = [chalk.bold(pad(HEADERS)), ...cells.map(pad)];
|
|
292
|
+
return lines.join("\n") + "\n";
|
|
293
|
+
}
|
|
294
|
+
async function listBuilds(appId, opts, deps) {
|
|
295
|
+
const { token, baseUrl } = resolveAuth(opts);
|
|
296
|
+
const d = deps ?? { getApp: (id) => new ApiClient(baseUrl, token).getApp(id) };
|
|
297
|
+
try {
|
|
298
|
+
const app = await d.getApp(appId);
|
|
299
|
+
const rows = buildRows(app);
|
|
300
|
+
if (opts.json) {
|
|
301
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
302
|
+
} else {
|
|
303
|
+
process.stdout.write(formatBuildRowsHuman(app, rows));
|
|
304
|
+
}
|
|
305
|
+
process.exitCode = 0;
|
|
306
|
+
} catch (err) {
|
|
307
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
308
|
+
if (opts.json) console.log(JSON.stringify({ event: "error", message }));
|
|
309
|
+
console.error(chalk.red(`error: ${message}`));
|
|
310
|
+
process.exitCode = errorExitCode(err);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/lib/dev-state.ts
|
|
315
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
316
|
+
import { dirname, join as join2 } from "path";
|
|
317
|
+
var STATE_DIR = ".vibeview";
|
|
318
|
+
var STATE_FILE = "dev-state.json";
|
|
319
|
+
function statePath(cwd) {
|
|
320
|
+
return join2(cwd, STATE_DIR, STATE_FILE);
|
|
321
|
+
}
|
|
322
|
+
function isPidAlive(pid) {
|
|
323
|
+
try {
|
|
324
|
+
process.kill(pid, 0);
|
|
325
|
+
return true;
|
|
326
|
+
} catch (err) {
|
|
327
|
+
return err.code === "EPERM";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function ensureGitignored(cwd) {
|
|
331
|
+
try {
|
|
332
|
+
const gitignore = join2(cwd, ".gitignore");
|
|
333
|
+
if (!existsSync(gitignore)) return;
|
|
334
|
+
const content = readFileSync2(gitignore, "utf-8");
|
|
335
|
+
const covered = content.split("\n").map((l) => l.trim()).some((l) => l === ".vibeview" || l === ".vibeview/" || l === "/.vibeview");
|
|
336
|
+
if (covered) return;
|
|
337
|
+
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
338
|
+
writeFileSync2(gitignore, `${content}${prefix}
|
|
339
|
+
# VibeView local session state
|
|
340
|
+
.vibeview/
|
|
341
|
+
`);
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function writeDevState(cwd, state) {
|
|
346
|
+
const path = statePath(cwd);
|
|
347
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
348
|
+
writeFileSync2(path, JSON.stringify(state, null, 2) + "\n");
|
|
349
|
+
ensureGitignored(cwd);
|
|
350
|
+
}
|
|
351
|
+
function readDevState(cwd) {
|
|
352
|
+
let raw;
|
|
353
|
+
try {
|
|
354
|
+
raw = readFileSync2(statePath(cwd), "utf-8");
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
let parsed;
|
|
359
|
+
try {
|
|
360
|
+
parsed = JSON.parse(raw);
|
|
361
|
+
} catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
if (typeof parsed.pid !== "number" || !isPidAlive(parsed.pid)) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
return parsed;
|
|
368
|
+
}
|
|
369
|
+
function clearDevState(cwd) {
|
|
370
|
+
const path = statePath(cwd);
|
|
371
|
+
if (existsSync(path)) {
|
|
372
|
+
rmSync(path, { force: true });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
345
376
|
// src/lib/metro-check.ts
|
|
346
377
|
async function checkMetro(port, timeoutMs = 1500) {
|
|
347
378
|
try {
|
|
@@ -871,7 +902,7 @@ function runTar(args) {
|
|
|
871
902
|
var CLOUD_PLATFORMS = ["ios", "android", "tvos", "androidtv"];
|
|
872
903
|
var PRODUCTION_SIGNING_PLATFORMS = ["ios", "android", "androidtv", "tvos"];
|
|
873
904
|
function productionUnsupportedReason(platform) {
|
|
874
|
-
return `--production doesn't support ${platform} \u2014 production signing supports ${PRODUCTION_SIGNING_PLATFORMS.join(", ")}`;
|
|
905
|
+
return `--production doesn't support ${platformLabel(platform)} \u2014 production signing supports ${PRODUCTION_SIGNING_PLATFORMS.join(", ")}`;
|
|
875
906
|
}
|
|
876
907
|
function isCloudPlatform(p) {
|
|
877
908
|
return CLOUD_PLATFORMS.includes(p);
|
|
@@ -898,7 +929,7 @@ async function runBuildForPlatform(cwd, platform, build, opts, log, deps = { upl
|
|
|
898
929
|
const { result } = await deps.upload(uploadPath, opts);
|
|
899
930
|
if (result.platform !== platform) {
|
|
900
931
|
throw new Error(
|
|
901
|
-
`you built
|
|
932
|
+
`you built ${platformWithArticle(result.platform)} app (${result.name}) but the target platform is ${platformLabel(platform)} \u2014 check platforms.${platform}.build in vibeview.json`
|
|
902
933
|
);
|
|
903
934
|
}
|
|
904
935
|
return result;
|
|
@@ -1056,7 +1087,7 @@ async function buildCommand(opts, cloudDeps) {
|
|
|
1056
1087
|
if (!(err instanceof BuildFailError)) {
|
|
1057
1088
|
printFailure(opts, err instanceof Error ? err.message : String(err));
|
|
1058
1089
|
}
|
|
1059
|
-
process.exitCode =
|
|
1090
|
+
process.exitCode = errorExitCode(err);
|
|
1060
1091
|
}
|
|
1061
1092
|
}
|
|
1062
1093
|
function printFailure(opts, message, code) {
|
|
@@ -1135,7 +1166,7 @@ function requireCloudCapable(opts, platform) {
|
|
|
1135
1166
|
if (!isCloudPlatform(platform)) {
|
|
1136
1167
|
fail(
|
|
1137
1168
|
opts,
|
|
1138
|
-
`cloud builds don't support ${platform} \u2014 pass one of ${CLOUD_PLATFORMS.join(", ")}`
|
|
1169
|
+
`cloud builds don't support ${platformLabel(platform)} \u2014 pass one of ${CLOUD_PLATFORMS.join(", ")}`
|
|
1139
1170
|
);
|
|
1140
1171
|
}
|
|
1141
1172
|
}
|
|
@@ -2393,13 +2424,13 @@ function buildAppStrategy(apps, buildFlag, json, isTTY) {
|
|
|
2393
2424
|
if (json || !isTTY) return "pick";
|
|
2394
2425
|
return "ask";
|
|
2395
2426
|
}
|
|
2396
|
-
function
|
|
2427
|
+
function noBuildConfigMessage(platform, interactive) {
|
|
2428
|
+
const addKey = `add platforms.${platform}.build {command, artifact} to vibeview.json`;
|
|
2429
|
+
return interactive ? `no build config for ${platform}: ${addKey}, or run \`vibeview build --platform ${platform}\` once to set it up` : `no build config for ${platform}: ${addKey}`;
|
|
2430
|
+
}
|
|
2431
|
+
function requireNonInteractive(json, message) {
|
|
2397
2432
|
if (!json) return;
|
|
2398
|
-
emit(json, {
|
|
2399
|
-
event: "error",
|
|
2400
|
-
code: "config_required",
|
|
2401
|
-
message: "pass --platform and run once interactively, or commit vibeview.json"
|
|
2402
|
-
});
|
|
2433
|
+
emit(json, { event: "error", code: "config_required", message });
|
|
2403
2434
|
process.exit(1);
|
|
2404
2435
|
}
|
|
2405
2436
|
async function ensureConfig(api, opts) {
|
|
@@ -2409,7 +2440,10 @@ async function ensureConfig(api, opts) {
|
|
|
2409
2440
|
process.exit(2);
|
|
2410
2441
|
}
|
|
2411
2442
|
if (!opts.platform && !existing?.defaultPlatform) {
|
|
2412
|
-
requireNonInteractive(
|
|
2443
|
+
requireNonInteractive(
|
|
2444
|
+
opts.json,
|
|
2445
|
+
`--platform is required (no vibeview.json default yet) \u2014 pass --platform <${DEV_PLATFORMS.join("|")}>, or run once interactively to set a default.`
|
|
2446
|
+
);
|
|
2413
2447
|
}
|
|
2414
2448
|
const platform = opts.platform || existing?.defaultPlatform || await select3({
|
|
2415
2449
|
message: "Platform to run on",
|
|
@@ -2441,9 +2475,7 @@ async function ensureConfig(api, opts) {
|
|
|
2441
2475
|
let isFirstRun = false;
|
|
2442
2476
|
if (!existing) {
|
|
2443
2477
|
isFirstRun = true;
|
|
2444
|
-
if (opts.json) {
|
|
2445
|
-
requireNonInteractive(opts.json);
|
|
2446
|
-
} else if (process.stdin.isTTY) {
|
|
2478
|
+
if (!opts.json && process.stdin.isTTY) {
|
|
2447
2479
|
firstRunBuild = await promptBuildConfig(process.cwd(), platform);
|
|
2448
2480
|
if (PLATFORM_NEEDS_METRO[platform]) {
|
|
2449
2481
|
firstRunMetro = await promptMetroCommand(process.cwd(), platform);
|
|
@@ -2524,7 +2556,10 @@ Run \`vibeview dev\` without --app to pick from the list.`
|
|
|
2524
2556
|
emit(opts.json, { event: "error", code: "app_deleted", message });
|
|
2525
2557
|
process.exit(1);
|
|
2526
2558
|
}
|
|
2527
|
-
requireNonInteractive(
|
|
2559
|
+
requireNonInteractive(
|
|
2560
|
+
opts.json,
|
|
2561
|
+
`--app is required \u2014 no app configured for ${platform} yet; pass --app <id> (see \`vibeview list-apps\`), or run interactively to pick one.`
|
|
2562
|
+
);
|
|
2528
2563
|
if (!process.stdin.isTTY) {
|
|
2529
2564
|
console.error(
|
|
2530
2565
|
chalk4.red(
|
|
@@ -2557,14 +2592,17 @@ Run \`vibeview dev\` without --app to pick from the list.`
|
|
|
2557
2592
|
firstRunMetro,
|
|
2558
2593
|
firstRunBuild
|
|
2559
2594
|
});
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2595
|
+
const commitConfig = () => {
|
|
2596
|
+
saveDevConfig(process.cwd(), cfg);
|
|
2597
|
+
if (isFirstRun) {
|
|
2598
|
+
console.error(chalk4.dim(`Created vibeview.json (platform: ${platform}, app: ${appId}).`));
|
|
2599
|
+
const hint = fillLaterHint(
|
|
2600
|
+
{ build: !firstRunBuild, metro: PLATFORM_NEEDS_METRO[platform] && !firstRunMetro },
|
|
2601
|
+
platform
|
|
2602
|
+
);
|
|
2603
|
+
if (hint) console.error(chalk4.dim(hint));
|
|
2604
|
+
}
|
|
2605
|
+
};
|
|
2568
2606
|
const finalMetro = isFirstRun ? resolveMetro(cfg, platform, opts.metroPort ? parseInt(opts.metroPort, 10) : void 0) : metroResolved;
|
|
2569
2607
|
return {
|
|
2570
2608
|
platform,
|
|
@@ -2574,7 +2612,9 @@ Run \`vibeview dev\` without --app to pick from the list.`
|
|
|
2574
2612
|
metroPort: finalMetro.port,
|
|
2575
2613
|
metroCommand: finalMetro.command,
|
|
2576
2614
|
platformHasMetroOverride: finalMetro.platformHasOverride,
|
|
2577
|
-
builtThisRun
|
|
2615
|
+
builtThisRun,
|
|
2616
|
+
savedConfig: cfg,
|
|
2617
|
+
commitConfig
|
|
2578
2618
|
};
|
|
2579
2619
|
}
|
|
2580
2620
|
async function runDevInProcess(opts) {
|
|
@@ -2623,11 +2663,20 @@ async function runDevInProcess(opts) {
|
|
|
2623
2663
|
process.exit(1);
|
|
2624
2664
|
}
|
|
2625
2665
|
}
|
|
2666
|
+
const interactive = !opts.json && Boolean(process.stdin.isTTY);
|
|
2667
|
+
const plannedBuild = cfg.savedConfig.platforms?.[cfg.platform]?.build;
|
|
2668
|
+
if (!plannedBuild && (opts.build === true && !cfg.builtThisRun || !PLATFORM_NEEDS_METRO[cfg.platform])) {
|
|
2669
|
+
const message = noBuildConfigMessage(cfg.platform, interactive);
|
|
2670
|
+
console.error(chalk4.red(`error: ${message}`));
|
|
2671
|
+
emit(opts.json, { event: "error", code: "config_required", message });
|
|
2672
|
+
process.exit(1);
|
|
2673
|
+
}
|
|
2674
|
+
cfg.commitConfig();
|
|
2626
2675
|
if (opts.build === true && !cfg.builtThisRun) {
|
|
2627
2676
|
const reloaded = loadDevConfig(process.cwd());
|
|
2628
2677
|
const build = reloaded?.platforms?.[cfg.platform]?.build;
|
|
2629
2678
|
if (!build) {
|
|
2630
|
-
const message =
|
|
2679
|
+
const message = noBuildConfigMessage(cfg.platform, interactive);
|
|
2631
2680
|
console.error(chalk4.red(`error: ${message}`));
|
|
2632
2681
|
emit(opts.json, { event: "error", code: "config_required", message });
|
|
2633
2682
|
process.exit(1);
|
|
@@ -2700,7 +2749,7 @@ async function runDevInProcess(opts) {
|
|
|
2700
2749
|
rokuBuild = rokuEntry?.build;
|
|
2701
2750
|
watchIgnore = rokuEntry?.watchIgnore ?? [];
|
|
2702
2751
|
if (!rokuBuild) {
|
|
2703
|
-
const message =
|
|
2752
|
+
const message = noBuildConfigMessage(cfg.platform, interactive);
|
|
2704
2753
|
console.error(chalk4.red(`error: ${message}`));
|
|
2705
2754
|
emit(opts.json, { event: "error", code: "config_required", message });
|
|
2706
2755
|
process.exit(1);
|
|
@@ -2708,7 +2757,7 @@ async function runDevInProcess(opts) {
|
|
|
2708
2757
|
}
|
|
2709
2758
|
console.error(
|
|
2710
2759
|
chalk4.dim(
|
|
2711
|
-
`Starting
|
|
2760
|
+
`Starting ${platformWithArticle(cfg.platform)} dev session for ${cfg.appId}${pinnedBuildId !== void 0 ? ` (build ${pinnedBuildId})` : ""}...`
|
|
2712
2761
|
)
|
|
2713
2762
|
);
|
|
2714
2763
|
let stopping = false;
|
|
@@ -3038,6 +3087,26 @@ function formatDeviceModelsHuman(models) {
|
|
|
3038
3087
|
}
|
|
3039
3088
|
|
|
3040
3089
|
// src/agent/registry.ts
|
|
3090
|
+
var PRESS_BUTTONS_BY_PLATFORM = [
|
|
3091
|
+
["iPhone / iPad", ["home", "back", "lock", "siri"]],
|
|
3092
|
+
["Android phone / tablet", ["home", "back", "lock"]],
|
|
3093
|
+
[
|
|
3094
|
+
"Apple TV / Android TV",
|
|
3095
|
+
["dpad_up", "dpad_down", "dpad_left", "dpad_right", "dpad_center", "back", "home"]
|
|
3096
|
+
],
|
|
3097
|
+
[
|
|
3098
|
+
"Roku (beta)",
|
|
3099
|
+
["up", "down", "left", "right", "select", "back", "(dpad_* also work; home is not forwarded)"]
|
|
3100
|
+
]
|
|
3101
|
+
];
|
|
3102
|
+
function pressButtonsHelp() {
|
|
3103
|
+
return [
|
|
3104
|
+
"Valid buttons:",
|
|
3105
|
+
...PRESS_BUTTONS_BY_PLATFORM.map(
|
|
3106
|
+
([platform, buttons]) => ` ${platform}: ${buttons.join(", ")}`
|
|
3107
|
+
)
|
|
3108
|
+
].join("\n");
|
|
3109
|
+
}
|
|
3041
3110
|
var AGENT_COMMANDS = [
|
|
3042
3111
|
{
|
|
3043
3112
|
verb: "ui-tree",
|
|
@@ -3071,7 +3140,7 @@ var AGENT_COMMANDS = [
|
|
|
3071
3140
|
{
|
|
3072
3141
|
verb: "screenshot",
|
|
3073
3142
|
tool: "screenshot",
|
|
3074
|
-
summary: "Capture a screenshot of the current screen. Use it to check how something LOOKS \u2014 the UI tree carries no colour, contrast, or stacking information, so a screen can pass every check the tree can express and still be visibly broken. Saved to disk; over MCP, pass inline: true to receive the image itself.",
|
|
3143
|
+
summary: "Capture a screenshot of the current screen. Use it to check how something LOOKS \u2014 the UI tree carries no colour, contrast, or stacking information, so a screen can pass every check the tree can express and still be visibly broken. Saved to disk; over MCP, pass inline: true to receive the image itself. On a phone or tablet its pixels are the coordinates `tap`/`drag` take, so a point read off it can be tapped as is.",
|
|
3075
3144
|
mutating: false,
|
|
3076
3145
|
params: [
|
|
3077
3146
|
{
|
|
@@ -3306,7 +3375,7 @@ var AGENT_COMMANDS = [
|
|
|
3306
3375
|
cli: "positional",
|
|
3307
3376
|
type: "string",
|
|
3308
3377
|
required: true,
|
|
3309
|
-
description: "Text to type into the focused field."
|
|
3378
|
+
description: "Text to type into the focused field. On a phone or tablet a line break presses Enter, which submits the field (bash: $'query\\n'); `press enter` does the same."
|
|
3310
3379
|
}
|
|
3311
3380
|
]
|
|
3312
3381
|
},
|
|
@@ -3332,7 +3401,7 @@ var AGENT_COMMANDS = [
|
|
|
3332
3401
|
cli: "positional",
|
|
3333
3402
|
type: "string",
|
|
3334
3403
|
required: true,
|
|
3335
|
-
description: "Button to press: home, back, lock, siri, dpad_up, dpad_down, dpad_left, dpad_right,
|
|
3404
|
+
description: "Button to press. iPhone/iPad: home, back, lock, siri. Android: home, back, lock. Apple TV / Android TV: dpad_up, dpad_down, dpad_left, dpad_right, dpad_center, back, home. Roku: up, down, left, right, select, back (dpad_* also work; home is not forwarded)."
|
|
3336
3405
|
}
|
|
3337
3406
|
]
|
|
3338
3407
|
},
|
|
@@ -3340,7 +3409,7 @@ var AGENT_COMMANDS = [
|
|
|
3340
3409
|
// tools.py open_url: { url: string }.
|
|
3341
3410
|
verb: "open-url",
|
|
3342
3411
|
tool: "open_url",
|
|
3343
|
-
summary: "Open a deep link or URL in the app under test.",
|
|
3412
|
+
summary: "Open a deep link or URL in the app under test. The link goes to that app only; one it has no screen for fails rather than opening a browser.",
|
|
3344
3413
|
mutating: true,
|
|
3345
3414
|
params: [
|
|
3346
3415
|
{
|
|
@@ -3360,7 +3429,7 @@ var AGENT_COMMANDS = [
|
|
|
3360
3429
|
// Foldable devices only.
|
|
3361
3430
|
verb: "set-posture",
|
|
3362
3431
|
tool: "set_posture",
|
|
3363
|
-
summary: "Fold or unfold a foldable device (iPhone Duo): a preset (closed, partial, open) or an exact hinge angle in degrees (--angle 0-180) \u2014 pass exactly one. Reports the posture it settled in, the angle, and which screen is lit. Errors on a device without a hinge.",
|
|
3432
|
+
summary: "Fold or unfold a foldable device (form factor `foldable` in `list-devices --models` \u2014 the iPhone Duo, Android foldables such as the Pixel 9 Pro Fold): a preset (closed, partial, open) or an exact hinge angle in degrees (--angle 0-180) \u2014 pass exactly one. Reports the posture it settled in, the angle, and which screen is lit. Errors on a device without a hinge.",
|
|
3364
3433
|
mutating: true,
|
|
3365
3434
|
params: [
|
|
3366
3435
|
{
|
|
@@ -3386,7 +3455,7 @@ var AGENT_COMMANDS = [
|
|
|
3386
3455
|
// portrait <-> landscape and accepts only 90. Refused on TV devices.
|
|
3387
3456
|
verb: "rotate",
|
|
3388
3457
|
tool: "rotate",
|
|
3389
|
-
summary: "Rotate the device. A phone toggles between portrait and landscape;
|
|
3458
|
+
summary: "Rotate the device. A phone (and an Android foldable) toggles between portrait and landscape; the iPhone Duo turns a quarter clockwise, or by --degrees 180/270. Reports the new orientation. Not available on TV devices.",
|
|
3390
3459
|
mutating: true,
|
|
3391
3460
|
params: [
|
|
3392
3461
|
{
|
|
@@ -3394,7 +3463,7 @@ var AGENT_COMMANDS = [
|
|
|
3394
3463
|
cli: "--degrees",
|
|
3395
3464
|
type: "number",
|
|
3396
3465
|
required: false,
|
|
3397
|
-
description: "
|
|
3466
|
+
description: "iPhone Duo only: 90 (default, a quarter turn clockwise), 180, or 270 (a quarter turn back). Other devices accept only 90."
|
|
3398
3467
|
}
|
|
3399
3468
|
]
|
|
3400
3469
|
},
|
|
@@ -3769,13 +3838,13 @@ async function runAgentVerb(cmd, positionals, opts) {
|
|
|
3769
3838
|
} catch (err) {
|
|
3770
3839
|
const message = err instanceof Error ? err.message : String(err);
|
|
3771
3840
|
if (opts.json) {
|
|
3772
|
-
const code =
|
|
3841
|
+
const code = err instanceof AuthError ? "auth_failed" : err instanceof NotFoundError ? "not_found" : void 0;
|
|
3773
3842
|
console.log(JSON.stringify({ event: "error", code, message }));
|
|
3774
3843
|
} else {
|
|
3775
3844
|
process.stderr.write(`error: ${message}
|
|
3776
3845
|
`);
|
|
3777
3846
|
}
|
|
3778
|
-
process.exit(
|
|
3847
|
+
process.exit(errorExitCode(err));
|
|
3779
3848
|
}
|
|
3780
3849
|
if (cmd.verb === "screenshot" && envelope.ok && envelope.result) {
|
|
3781
3850
|
const outPath = opts.out || defaultScreenshotPath();
|
|
@@ -3801,13 +3870,20 @@ async function runAgentVerb(cmd, positionals, opts) {
|
|
|
3801
3870
|
console.log(JSON.stringify(envelope));
|
|
3802
3871
|
} else {
|
|
3803
3872
|
console.log(renderEnvelope(envelope));
|
|
3873
|
+
if (cmd.verb === "press" && !envelope.ok) process.stderr.write(pressButtonsHelp() + "\n");
|
|
3804
3874
|
}
|
|
3805
3875
|
process.exit(agentExitCode(envelope));
|
|
3806
3876
|
}
|
|
3807
3877
|
|
|
3808
3878
|
export {
|
|
3879
|
+
DEV_PLATFORMS,
|
|
3880
|
+
PLATFORM_NEEDS_METRO,
|
|
3881
|
+
isDevPlatform,
|
|
3882
|
+
platformWithArticle,
|
|
3883
|
+
loadDevConfig,
|
|
3809
3884
|
parseBuildId,
|
|
3810
3885
|
checkDevBuild,
|
|
3886
|
+
formatUtcTimestamp,
|
|
3811
3887
|
buildRows,
|
|
3812
3888
|
formatBuildRowsHuman,
|
|
3813
3889
|
listBuilds,
|
|
@@ -3815,10 +3891,6 @@ export {
|
|
|
3815
3891
|
clearDevState,
|
|
3816
3892
|
readMetroState,
|
|
3817
3893
|
killOwnedMetro,
|
|
3818
|
-
DEV_PLATFORMS,
|
|
3819
|
-
PLATFORM_NEEDS_METRO,
|
|
3820
|
-
isDevPlatform,
|
|
3821
|
-
loadDevConfig,
|
|
3822
3894
|
checkMetro,
|
|
3823
3895
|
buildCommand,
|
|
3824
3896
|
formatDuration,
|
|
@@ -3835,6 +3907,7 @@ export {
|
|
|
3835
3907
|
formatDevicesHuman,
|
|
3836
3908
|
filterDeviceModels,
|
|
3837
3909
|
formatDeviceModelsHuman,
|
|
3910
|
+
pressButtonsHelp,
|
|
3838
3911
|
AGENT_COMMANDS,
|
|
3839
3912
|
renderEnvelope,
|
|
3840
3913
|
buildToolCall,
|