vibeview 0.33.0 → 0.34.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.
|
@@ -384,6 +384,80 @@ function buildLabel(b) {
|
|
|
384
384
|
if (b.note) parts.push(JSON.stringify(b.note.length > 60 ? `${b.note.slice(0, 57)}...` : b.note));
|
|
385
385
|
return `${b.id} (${parts.join(", ")})`;
|
|
386
386
|
}
|
|
387
|
+
function startedBuild(b) {
|
|
388
|
+
return {
|
|
389
|
+
id: b.id,
|
|
390
|
+
version: b.version ?? null,
|
|
391
|
+
build_number: b.build_number ?? null,
|
|
392
|
+
kind: buildKind(b),
|
|
393
|
+
uploaded_at: b.uploaded_at ?? null,
|
|
394
|
+
note: b.note ?? null
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function otherDebugBuilds(app, installedId) {
|
|
398
|
+
const builds = debugBuildsNewestFirst(app);
|
|
399
|
+
const installed = app.builds.find((b) => b.id === installedId);
|
|
400
|
+
const seen = /* @__PURE__ */ new Set();
|
|
401
|
+
if (installed) seen.add(installed.version ?? "");
|
|
402
|
+
const out = [];
|
|
403
|
+
for (const b of builds) {
|
|
404
|
+
if (b.id === installedId) continue;
|
|
405
|
+
const key = b.version ?? "";
|
|
406
|
+
if (seen.has(key)) continue;
|
|
407
|
+
seen.add(key);
|
|
408
|
+
out.push(b);
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
function preferDevBuild(app, pinned, project) {
|
|
413
|
+
if (pinned !== void 0 || !app) return { buildId: pinned, reason: null };
|
|
414
|
+
const picked = pickDevBuild(app, project?.version ?? null);
|
|
415
|
+
return picked?.reason ? { buildId: picked.build.id, reason: picked.reason } : { buildId: void 0, reason: null };
|
|
416
|
+
}
|
|
417
|
+
function describeDevBuild(app, session, pick, project) {
|
|
418
|
+
const installedId = session?.app_build_id ?? pick.buildId ?? null;
|
|
419
|
+
const installed = app?.builds.find((b) => b.id === installedId);
|
|
420
|
+
const build = installed ? startedBuild(installed) : typeof installedId === "number" ? {
|
|
421
|
+
id: installedId,
|
|
422
|
+
version: session?.app_version ?? null,
|
|
423
|
+
build_number: session?.app_build_number ?? null,
|
|
424
|
+
kind: "unknown",
|
|
425
|
+
uploaded_at: null,
|
|
426
|
+
note: null
|
|
427
|
+
} : null;
|
|
428
|
+
return {
|
|
429
|
+
build,
|
|
430
|
+
reason: pick.reason,
|
|
431
|
+
pinned: pick.pinned,
|
|
432
|
+
other_debug_builds: app ? otherDebugBuilds(app, installedId).map(startedBuild) : [],
|
|
433
|
+
project
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function buildLines(choice, opts) {
|
|
437
|
+
const lines = [];
|
|
438
|
+
if (!choice) return lines;
|
|
439
|
+
if (choice.build) lines.push(`build: ${buildLabel(choice.build)}`);
|
|
440
|
+
if (choice.reason) lines.push(`picked that build because ${choice.reason}`);
|
|
441
|
+
const project = choice.project;
|
|
442
|
+
if (!opts.standalone && !choice.pinned && !choice.reason && project && !project.version) {
|
|
443
|
+
const why = project.versionUnreadable ? ` (${project.versionUnreadable})` : "";
|
|
444
|
+
lines.push(
|
|
445
|
+
`couldn't read this project's app version${why}, so this is the newest debug build, not necessarily one of this version`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
if (choice.other_debug_builds.length > 0) {
|
|
449
|
+
lines.push(
|
|
450
|
+
`other debug builds: ${choice.other_debug_builds.map((b) => buildLabel(b)).join("; ")} \u2014 pass ${opts.flag} to run one`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
const rn = project?.reactNative;
|
|
454
|
+
if (!opts.standalone && rn) {
|
|
455
|
+
lines.push(
|
|
456
|
+
`this project: react-native ${rn}${project?.version ? `, version ${project.version}` : ""}. If the app shows a red error screen right after it starts (for example "React Native version mismatch", or "undefined is not a function" inside a native module), the build may have been made from other native code than this project \u2014 try a build of this project (${opts.flag}, see ${opts.listBuilds}).`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
return lines;
|
|
460
|
+
}
|
|
387
461
|
|
|
388
462
|
// src/commands/list-builds.ts
|
|
389
463
|
import chalk from "chalk";
|
|
@@ -628,7 +702,18 @@ function describeLocalChanges(before, after) {
|
|
|
628
702
|
return changes;
|
|
629
703
|
}
|
|
630
704
|
|
|
705
|
+
// src/commands/build.ts
|
|
706
|
+
import { spawn as spawn2 } from "child_process";
|
|
707
|
+
import { createInterface } from "readline";
|
|
708
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
709
|
+
import chalk3 from "chalk";
|
|
710
|
+
|
|
711
|
+
// src/lib/dev-setup.ts
|
|
712
|
+
import chalk2 from "chalk";
|
|
713
|
+
import { input, select } from "@inquirer/prompts";
|
|
714
|
+
|
|
631
715
|
// src/lib/project-detect.ts
|
|
716
|
+
import { execFile } from "child_process";
|
|
632
717
|
import { existsSync as existsSync2, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
|
|
633
718
|
import { join as join4 } from "path";
|
|
634
719
|
import ignore from "ignore";
|
|
@@ -828,16 +913,64 @@ function pbxprojText(cwd) {
|
|
|
828
913
|
return null;
|
|
829
914
|
}
|
|
830
915
|
}
|
|
831
|
-
|
|
916
|
+
var readExpoConfig = (cwd) => {
|
|
917
|
+
const cli = join4(cwd, "node_modules", "expo", "bin", "cli");
|
|
918
|
+
if (!existsSync2(cli)) {
|
|
919
|
+
return Promise.reject(new Error("Expo is not installed in this project (node_modules/expo)"));
|
|
920
|
+
}
|
|
921
|
+
return new Promise((resolve, reject) => {
|
|
922
|
+
execFile(
|
|
923
|
+
process.execPath,
|
|
924
|
+
[cli, "config", "--json", "--type", "public"],
|
|
925
|
+
{
|
|
926
|
+
cwd,
|
|
927
|
+
timeout: 3e4,
|
|
928
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
929
|
+
env: { ...process.env, EXPO_NO_TELEMETRY: "1", CI: "1" }
|
|
930
|
+
},
|
|
931
|
+
(err, stdout, stderr) => {
|
|
932
|
+
if (err) {
|
|
933
|
+
const why = String(stderr || err.message).split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
934
|
+
reject(new Error(`expo config failed${why ? `: ${why}` : ""}`));
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
try {
|
|
938
|
+
const parsed = JSON.parse(stdout);
|
|
939
|
+
if (parsed && typeof parsed === "object") resolve(parsed);
|
|
940
|
+
else reject(new Error("expo config printed no config"));
|
|
941
|
+
} catch {
|
|
942
|
+
reject(new Error("expo config printed something that is not JSON"));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
);
|
|
946
|
+
});
|
|
947
|
+
};
|
|
948
|
+
async function localAppIdentity(cwd, platform, readConfig = readExpoConfig) {
|
|
832
949
|
const rn = readJson(cwd, "node_modules/react-native/package.json");
|
|
833
950
|
const reactNative = typeof rn?.version === "string" ? rn.version : null;
|
|
834
951
|
const android = platform === "android" || platform === "androidtv";
|
|
835
952
|
const ios = platform === "ios" || platform === "tvos";
|
|
836
953
|
let version = null;
|
|
837
954
|
let bundleId = null;
|
|
838
|
-
|
|
839
|
-
const
|
|
840
|
-
|
|
955
|
+
let versionUnreadable = null;
|
|
956
|
+
const dynamicName = ["app.config.ts", "app.config.js", "app.config.mjs", "app.config.cjs"].find(
|
|
957
|
+
(f) => existsSync2(join4(cwd, f))
|
|
958
|
+
);
|
|
959
|
+
const staticRaw = readJson(cwd, "app.config.json") ?? readJson(cwd, "app.json");
|
|
960
|
+
const isExpo = Boolean(dynamicName) || staticRaw !== null && "expo" in staticRaw || existsSync2(join4(cwd, "node_modules", "expo", "package.json"));
|
|
961
|
+
let expo = null;
|
|
962
|
+
if (isExpo && (dynamicName || existsSync2(join4(cwd, "node_modules", "expo", "bin", "cli")))) {
|
|
963
|
+
try {
|
|
964
|
+
expo = await readConfig(cwd);
|
|
965
|
+
} catch (err) {
|
|
966
|
+
if (dynamicName) {
|
|
967
|
+
versionUnreadable = `${dynamicName} is dynamic and could not be evaluated: ${err instanceof Error ? err.message : String(err)}`;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (expo === null && !dynamicName && staticRaw) {
|
|
972
|
+
expo = "expo" in staticRaw ? staticRaw.expo : staticRaw;
|
|
973
|
+
}
|
|
841
974
|
if (expo && typeof expo === "object") {
|
|
842
975
|
const cfg = expo;
|
|
843
976
|
if (typeof cfg.version === "string") version = cfg.version;
|
|
@@ -859,18 +992,10 @@ function localAppIdentity(cwd, platform) {
|
|
|
859
992
|
bundleId ??= uniqueMatch(pbx, /PRODUCT_BUNDLE_IDENTIFIER = ([^;]+);/g);
|
|
860
993
|
}
|
|
861
994
|
}
|
|
862
|
-
return { reactNative, version, bundleId };
|
|
995
|
+
return { reactNative, version, bundleId, versionUnreadable: version ? null : versionUnreadable };
|
|
863
996
|
}
|
|
864
997
|
|
|
865
|
-
// src/commands/build.ts
|
|
866
|
-
import { spawn as spawn2 } from "child_process";
|
|
867
|
-
import { createInterface } from "readline";
|
|
868
|
-
import { select as select2 } from "@inquirer/prompts";
|
|
869
|
-
import chalk3 from "chalk";
|
|
870
|
-
|
|
871
998
|
// src/lib/dev-setup.ts
|
|
872
|
-
import chalk2 from "chalk";
|
|
873
|
-
import { input, select } from "@inquirer/prompts";
|
|
874
999
|
function platformChoices(cwd) {
|
|
875
1000
|
const choices = [
|
|
876
1001
|
{ value: "ios", name: "iOS simulator" },
|
|
@@ -968,7 +1093,7 @@ async function promptMetroCommand(cwd, platform) {
|
|
|
968
1093
|
}
|
|
969
1094
|
|
|
970
1095
|
// src/lib/source-pack.ts
|
|
971
|
-
import { execFile, spawn } from "child_process";
|
|
1096
|
+
import { execFile as execFile2, spawn } from "child_process";
|
|
972
1097
|
import { lstatSync, mkdtempSync, readFileSync as readFileSync5, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
973
1098
|
import { tmpdir } from "os";
|
|
974
1099
|
import { join as join5 } from "path";
|
|
@@ -1107,7 +1232,7 @@ function probeTarNullSupport() {
|
|
|
1107
1232
|
const listPath = join5(tmp, "empty.list");
|
|
1108
1233
|
writeFileSync4(listPath, "");
|
|
1109
1234
|
const out = join5(tmp, "probe.tgz");
|
|
1110
|
-
|
|
1235
|
+
execFile2("tar", ["-czf", out, "-C", tmp, "--null", "-T", listPath], (err) => {
|
|
1111
1236
|
rmSync4(tmp, { recursive: true, force: true });
|
|
1112
1237
|
resolve(!err);
|
|
1113
1238
|
});
|
|
@@ -2448,6 +2573,13 @@ function queuedLine(position, reasonMessage) {
|
|
|
2448
2573
|
const why = reasonMessage ?? "Every device is busy.";
|
|
2449
2574
|
return `\u23F3 ${why} Waiting in the queue${where}, Ctrl-C to give up.`;
|
|
2450
2575
|
}
|
|
2576
|
+
function devBuildLines(choice, standalone) {
|
|
2577
|
+
return buildLines(choice, {
|
|
2578
|
+
standalone,
|
|
2579
|
+
flag: "--build <id>",
|
|
2580
|
+
listBuilds: "vibeview list-builds"
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2451
2583
|
function emit(json, evt) {
|
|
2452
2584
|
if (json) console.log(JSON.stringify(evt));
|
|
2453
2585
|
}
|
|
@@ -2456,8 +2588,30 @@ async function startDevSession(cfg, onEvent = () => {
|
|
|
2456
2588
|
const api = new ApiClient(cfg.baseUrl, cfg.token);
|
|
2457
2589
|
const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform] && !cfg.standalone;
|
|
2458
2590
|
const pool = cfg.model ? resolveDeviceModel(await api.listDeviceModels(), cfg.platform, cfg.model) : null;
|
|
2591
|
+
let app = null;
|
|
2592
|
+
let project = null;
|
|
2593
|
+
let pick = {
|
|
2594
|
+
buildId: cfg.buildId,
|
|
2595
|
+
reason: null
|
|
2596
|
+
};
|
|
2597
|
+
if (usesTunnel && cfg.projectDir) {
|
|
2598
|
+
try {
|
|
2599
|
+
app = cfg.app ?? await api.getApp(cfg.appId);
|
|
2600
|
+
} catch {
|
|
2601
|
+
app = null;
|
|
2602
|
+
}
|
|
2603
|
+
try {
|
|
2604
|
+
project = await localAppIdentity(cfg.projectDir, cfg.platform);
|
|
2605
|
+
} catch {
|
|
2606
|
+
project = null;
|
|
2607
|
+
}
|
|
2608
|
+
pick = preferDevBuild(app, cfg.buildId, project);
|
|
2609
|
+
}
|
|
2610
|
+
const body = { ...cfg, buildId: pick.buildId };
|
|
2459
2611
|
const created = await api.createSession(
|
|
2460
|
-
devSessionCreateBody(
|
|
2612
|
+
devSessionCreateBody(
|
|
2613
|
+
pool ? { ...body, deviceCategory: pool.category, model: pool.model } : body
|
|
2614
|
+
)
|
|
2461
2615
|
);
|
|
2462
2616
|
const sessionId = created.session_id;
|
|
2463
2617
|
let stopping = false;
|
|
@@ -2573,7 +2727,8 @@ async function startDevSession(cfg, onEvent = () => {
|
|
|
2573
2727
|
stop: () => stopSession(0),
|
|
2574
2728
|
reportedWarnings,
|
|
2575
2729
|
session: active,
|
|
2576
|
-
lapsedReason: () => leaseLapsedReason(Date.now() - lastLeaseAt)
|
|
2730
|
+
lapsedReason: () => leaseLapsedReason(Date.now() - lastLeaseAt),
|
|
2731
|
+
build: usesTunnel && cfg.projectDir ? describeDevBuild(app, active, { ...pick, pinned: cfg.buildId !== void 0 }, project) : null
|
|
2577
2732
|
};
|
|
2578
2733
|
} catch (err) {
|
|
2579
2734
|
await stopSession(1);
|
|
@@ -3192,6 +3347,7 @@ async function runDevInProcess(opts) {
|
|
|
3192
3347
|
deviceCategory: cfg.deviceCategory,
|
|
3193
3348
|
model: cfg.model,
|
|
3194
3349
|
buildId: pinnedBuildId,
|
|
3350
|
+
projectDir: process.cwd(),
|
|
3195
3351
|
...standalone ? { standalone } : {},
|
|
3196
3352
|
metroPort: cfg.metroPort ?? 8081,
|
|
3197
3353
|
// startDevSession's own browser-open happens before we've written the
|
|
@@ -3249,6 +3405,8 @@ async function runDevInProcess(opts) {
|
|
|
3249
3405
|
}
|
|
3250
3406
|
);
|
|
3251
3407
|
const keysLine = rokuBuild ? `Press ${chalk4.bold("r")} to reload the channel, ${chalk4.bold("q")} or Ctrl-C to end the session.` : "Ctrl-C to end the session.";
|
|
3408
|
+
for (const line of devBuildLines(handle.build, standalone))
|
|
3409
|
+
console.error(chalk4.dim(` ${line}`));
|
|
3252
3410
|
console.error(`
|
|
3253
3411
|
Watch and interact: ${chalk4.cyan(handle.pageUrl)}
|
|
3254
3412
|
${keysLine}
|
|
@@ -3266,7 +3424,8 @@ async function runDevInProcess(opts) {
|
|
|
3266
3424
|
event: "session_ready",
|
|
3267
3425
|
session_id: handle.sessionId,
|
|
3268
3426
|
url: handle.pageUrl,
|
|
3269
|
-
pid: process.pid
|
|
3427
|
+
pid: process.pid,
|
|
3428
|
+
build: handle.build ?? null
|
|
3270
3429
|
});
|
|
3271
3430
|
if (opts.open !== false) await openBrowser(handle.pageUrl).catch(() => {
|
|
3272
3431
|
});
|
|
@@ -3398,6 +3557,8 @@ async function dev(opts) {
|
|
|
3398
3557
|
} else {
|
|
3399
3558
|
console.error(chalk4.dim(`Using ${resolveAuth(opts).baseUrl}`));
|
|
3400
3559
|
console.error(chalk4.green(`\u2713 dev session started in the background (pid ${parsed.pid})`));
|
|
3560
|
+
for (const line2 of devBuildLines(parsed.build, false))
|
|
3561
|
+
console.error(chalk4.dim(` ${line2}`));
|
|
3401
3562
|
console.error(`
|
|
3402
3563
|
Watch and interact: ${chalk4.cyan(parsed.url)}`);
|
|
3403
3564
|
console.error(chalk4.dim(` vibeview dev-status \u2014 check on it`));
|
|
@@ -4056,12 +4217,12 @@ var AGENT_COMMANDS = [
|
|
|
4056
4217
|
|
|
4057
4218
|
// src/agent/render.ts
|
|
4058
4219
|
import chalk6 from "chalk";
|
|
4059
|
-
function renderEnvelope(env) {
|
|
4220
|
+
function renderEnvelope(env, label = env.tool) {
|
|
4060
4221
|
const lines = [];
|
|
4061
4222
|
if (env.ok) {
|
|
4062
|
-
lines.push(chalk6.green(`ok`) + chalk6.dim(` (${
|
|
4223
|
+
lines.push(chalk6.green(`ok`) + chalk6.dim(` (${label})`));
|
|
4063
4224
|
} else {
|
|
4064
|
-
lines.push(chalk6.red(`error`) + chalk6.dim(` (${
|
|
4225
|
+
lines.push(chalk6.red(`error`) + chalk6.dim(` (${label})`));
|
|
4065
4226
|
}
|
|
4066
4227
|
lines.push(...resultLines(env.result));
|
|
4067
4228
|
if (env.observation) {
|
|
@@ -4367,7 +4528,7 @@ async function runAgentVerb(cmd, positionals, opts) {
|
|
|
4367
4528
|
JSON.stringify({ ...envelope, result: { written_to: outPath, bytes: imageBytes.length } })
|
|
4368
4529
|
);
|
|
4369
4530
|
} else {
|
|
4370
|
-
process.stderr.write(renderEnvelope(envelope) + "\n");
|
|
4531
|
+
process.stderr.write(renderEnvelope(envelope, cmd.verb) + "\n");
|
|
4371
4532
|
console.log(outPath);
|
|
4372
4533
|
}
|
|
4373
4534
|
process.exit(agentExitCode(envelope));
|
|
@@ -4376,7 +4537,7 @@ async function runAgentVerb(cmd, positionals, opts) {
|
|
|
4376
4537
|
if (opts.json) {
|
|
4377
4538
|
console.log(JSON.stringify(envelope));
|
|
4378
4539
|
} else {
|
|
4379
|
-
console.log(renderEnvelope(envelope));
|
|
4540
|
+
console.log(renderEnvelope(envelope, cmd.verb));
|
|
4380
4541
|
if (cmd.verb === "press" && !envelope.ok) process.stderr.write(pressButtonsHelp() + "\n");
|
|
4381
4542
|
}
|
|
4382
4543
|
process.exit(agentExitCode(envelope));
|
|
@@ -4391,11 +4552,9 @@ export {
|
|
|
4391
4552
|
loadDevConfig,
|
|
4392
4553
|
resolveMetro,
|
|
4393
4554
|
parseBuildId,
|
|
4394
|
-
buildKind,
|
|
4395
4555
|
checkDevBuild,
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
buildLabel,
|
|
4556
|
+
describeDevBuild,
|
|
4557
|
+
buildLines,
|
|
4399
4558
|
buildRows,
|
|
4400
4559
|
formatBuildRowsHuman,
|
|
4401
4560
|
listBuilds,
|
|
@@ -4407,7 +4566,6 @@ export {
|
|
|
4407
4566
|
checkMetro,
|
|
4408
4567
|
loadLocalDevConfig,
|
|
4409
4568
|
effectiveDevConfig,
|
|
4410
|
-
localAppIdentity,
|
|
4411
4569
|
buildCommand,
|
|
4412
4570
|
formatDuration,
|
|
4413
4571
|
BUILD_TERMINAL_STATUSES,
|
package/dist/cli.js
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
readDevStateFile,
|
|
25
25
|
readMetroState,
|
|
26
26
|
runAgentVerb
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-TPZG7NCU.js";
|
|
28
28
|
import {
|
|
29
29
|
ApiClient,
|
|
30
30
|
ApiStatusError,
|
|
@@ -60,10 +60,25 @@ import chalk2 from "chalk";
|
|
|
60
60
|
// src/lib/formatters/human.ts
|
|
61
61
|
import chalk from "chalk";
|
|
62
62
|
|
|
63
|
+
// src/lib/formatters/not-checked.ts
|
|
64
|
+
function notCheckedSteps(steps) {
|
|
65
|
+
return (steps ?? []).filter((s) => s.status === "not_checked");
|
|
66
|
+
}
|
|
67
|
+
function runStepsNotChecked(result) {
|
|
68
|
+
if (typeof result.steps_not_checked === "number") return result.steps_not_checked;
|
|
69
|
+
return notCheckedSteps(result.steps).length;
|
|
70
|
+
}
|
|
71
|
+
function stepsNotCheckedLabel(n) {
|
|
72
|
+
return `${n} step${n === 1 ? "" : "s"} not checked`;
|
|
73
|
+
}
|
|
74
|
+
function notCheckedSuffix(status, n) {
|
|
75
|
+
return status === "passed" && n && n > 0 ? ` (${stepsNotCheckedLabel(n)})` : "";
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
// src/lib/formatters/cases.ts
|
|
64
79
|
function resultCases(result) {
|
|
65
80
|
if (Array.isArray(result.test_runs)) return result.test_runs;
|
|
66
|
-
const failedStep = result.steps?.find((s) => s.status
|
|
81
|
+
const failedStep = result.steps?.find((s) => s.status === "failed" || s.status === "error");
|
|
67
82
|
return [
|
|
68
83
|
{
|
|
69
84
|
case_name: result.case_name || result.run_id,
|
|
@@ -73,7 +88,12 @@ function resultCases(result) {
|
|
|
73
88
|
error_kind: failedStep?.error_kind ?? null,
|
|
74
89
|
error_kind_description: failedStep?.error_kind_description ?? null,
|
|
75
90
|
screenshot_signed_url: failedStep?.screenshot_signed_url ?? null,
|
|
76
|
-
url: result.url
|
|
91
|
+
url: result.url,
|
|
92
|
+
steps_not_checked: runStepsNotChecked(result),
|
|
93
|
+
not_checked_steps: notCheckedSteps(result.steps).map((s) => ({
|
|
94
|
+
step: s.step,
|
|
95
|
+
note: s.note ?? null
|
|
96
|
+
}))
|
|
77
97
|
}
|
|
78
98
|
];
|
|
79
99
|
}
|
|
@@ -100,11 +120,16 @@ function formatHuman(result, verbose) {
|
|
|
100
120
|
const duration = result.duration_ms ? `${(result.duration_ms / 1e3).toFixed(1)}s` : "-";
|
|
101
121
|
lines.push("");
|
|
102
122
|
const title = result.suite_name || result.case_name || result.run_id;
|
|
103
|
-
lines.push(` ${icon} ${chalk.bold(title)} ${chalk.dim(duration)}`);
|
|
104
123
|
const cases = resultCases(result);
|
|
124
|
+
const notChecked = cases.reduce((n, t) => n + (t.steps_not_checked ?? 0), 0);
|
|
125
|
+
const runSuffix = notCheckedSuffix(result.status, notChecked);
|
|
126
|
+
lines.push(
|
|
127
|
+
` ${icon}${runSuffix ? chalk.yellow(runSuffix) : ""} ${chalk.bold(title)} ${chalk.dim(duration)}`
|
|
128
|
+
);
|
|
105
129
|
const passed = cases.filter((t) => t.status === "passed").length;
|
|
106
130
|
const total = cases.length;
|
|
107
|
-
|
|
131
|
+
const notCheckedNote = notChecked > 0 ? `, ${stepsNotCheckedLabel(notChecked)}` : "";
|
|
132
|
+
lines.push(` ${chalk.dim(`${passed}/${total} cases passed${notCheckedNote}`)}`);
|
|
108
133
|
if (result.url) {
|
|
109
134
|
lines.push(` ${chalk.dim(result.url)}`);
|
|
110
135
|
}
|
|
@@ -114,7 +139,10 @@ function formatHuman(result, verbose) {
|
|
|
114
139
|
for (const tr of suiteCases) {
|
|
115
140
|
const caseIcon = tr.status === "passed" ? chalk.green(" PASS") : tr.status === "cancelled" || tr.status === "pending" || tr.status === "running" ? chalk.yellow(" SKIP") : chalk.red(" FAIL");
|
|
116
141
|
const caseDuration = tr.duration_ms ? `${(tr.duration_ms / 1e3).toFixed(1)}s` : "-";
|
|
117
|
-
|
|
142
|
+
const caseSuffix = notCheckedSuffix(tr.status, tr.steps_not_checked);
|
|
143
|
+
lines.push(
|
|
144
|
+
` ${caseIcon}${caseSuffix ? chalk.yellow(caseSuffix) : ""} ${tr.case_name} ${chalk.dim(caseDuration)}`
|
|
145
|
+
);
|
|
118
146
|
if (tr.url) {
|
|
119
147
|
lines.push(` ${chalk.dim(tr.url)}`);
|
|
120
148
|
}
|
|
@@ -129,7 +157,7 @@ function formatHuman(result, verbose) {
|
|
|
129
157
|
lines.push(` ${chalk.bold(result.case_name)}`);
|
|
130
158
|
}
|
|
131
159
|
for (const step of result.steps) {
|
|
132
|
-
const stepIcon = step.status === "passed" ? chalk.green(" PASS") : chalk.red(" FAIL");
|
|
160
|
+
const stepIcon = step.status === "passed" ? chalk.green(" PASS") : step.status === "not_checked" ? chalk.yellow(" NOT CHECKED") : chalk.red(" FAIL");
|
|
133
161
|
const stepDuration = step.duration_ms ? `${(step.duration_ms / 1e3).toFixed(1)}s` : "-";
|
|
134
162
|
lines.push(` ${stepIcon} ${step.step} ${chalk.dim(stepDuration)}`);
|
|
135
163
|
if (verbose && step.actions && step.actions.length > 0) {
|
|
@@ -137,7 +165,9 @@ function formatHuman(result, verbose) {
|
|
|
137
165
|
lines.push(` ${chalk.dim(formatAction(action))}`);
|
|
138
166
|
}
|
|
139
167
|
}
|
|
140
|
-
if (step.status
|
|
168
|
+
if (step.status === "not_checked") {
|
|
169
|
+
if (step.note) lines.push(` ${chalk.dim(step.note)}`);
|
|
170
|
+
} else if (step.status !== "passed") {
|
|
141
171
|
lines.push(...failureLines(step));
|
|
142
172
|
}
|
|
143
173
|
}
|
|
@@ -156,13 +186,33 @@ function formatHuman(result, verbose) {
|
|
|
156
186
|
|
|
157
187
|
// src/lib/formatters/json.ts
|
|
158
188
|
function formatJson(result) {
|
|
159
|
-
|
|
189
|
+
const doc = Array.isArray(result.test_runs) ? result : { ...result, steps_not_checked: runStepsNotChecked(result) };
|
|
190
|
+
return JSON.stringify(doc, null, 2);
|
|
160
191
|
}
|
|
161
192
|
|
|
162
193
|
// src/lib/formatters/junit.ts
|
|
163
194
|
function escapeXml(s) {
|
|
164
195
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
165
196
|
}
|
|
197
|
+
function caseDetailLines(c) {
|
|
198
|
+
const n = c.status === "passed" ? c.steps_not_checked ?? 0 : 0;
|
|
199
|
+
const lines = [];
|
|
200
|
+
if (n > 0) {
|
|
201
|
+
lines.push(" <properties>");
|
|
202
|
+
lines.push(` <property name="steps_not_checked" value="${n}" />`);
|
|
203
|
+
lines.push(" </properties>");
|
|
204
|
+
}
|
|
205
|
+
const out = [];
|
|
206
|
+
if (c.url) out.push(c.url);
|
|
207
|
+
if (n > 0) {
|
|
208
|
+
out.push(`${stepsNotCheckedLabel(n)}:`);
|
|
209
|
+
for (const s of c.not_checked_steps ?? []) {
|
|
210
|
+
out.push(`- ${s.step}${s.note ? ` (${s.note})` : ""}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (out.length) lines.push(` <system-out>${escapeXml(out.join("\n"))}</system-out>`);
|
|
214
|
+
return lines;
|
|
215
|
+
}
|
|
166
216
|
function formatJunit(result) {
|
|
167
217
|
const suiteName = escapeXml(result.suite_name || result.case_name || result.run_id);
|
|
168
218
|
const cases = resultCases(result);
|
|
@@ -194,8 +244,8 @@ function formatJunit(result) {
|
|
|
194
244
|
for (const tr of cases) {
|
|
195
245
|
const caseName = escapeXml(tr.case_name);
|
|
196
246
|
const caseTime = tr.duration_ms ? (tr.duration_ms / 1e3).toFixed(1) : "0";
|
|
197
|
-
const
|
|
198
|
-
if (tr.status === "passed" &&
|
|
247
|
+
const detail = caseDetailLines(tr);
|
|
248
|
+
if (tr.status === "passed" && detail.length === 0) {
|
|
199
249
|
lines.push(` <testcase name="${caseName}" classname="${suiteName}" time="${caseTime}" />`);
|
|
200
250
|
continue;
|
|
201
251
|
}
|
|
@@ -207,7 +257,7 @@ function formatJunit(result) {
|
|
|
207
257
|
const msg = escapeXml(tr.error_message || "Infrastructure error");
|
|
208
258
|
lines.push(` <error message="${msg}" type="InfraError">${msg}</error>`);
|
|
209
259
|
}
|
|
210
|
-
|
|
260
|
+
lines.push(...detail);
|
|
211
261
|
lines.push(" </testcase>");
|
|
212
262
|
}
|
|
213
263
|
lines.push(" </testsuite>");
|
|
@@ -240,8 +290,12 @@ var MULTI_TERMINAL = /* @__PURE__ */ new Set(["passed", "failed", "error", "canc
|
|
|
240
290
|
function caseLines(c, classname) {
|
|
241
291
|
const name = escapeXml(c.name);
|
|
242
292
|
const head = ` <testcase name="${name}" classname="${classname}" time="${c.time}"`;
|
|
243
|
-
const
|
|
244
|
-
|
|
293
|
+
const detail = caseDetailLines({
|
|
294
|
+
status: c.verdict,
|
|
295
|
+
url: c.url,
|
|
296
|
+
steps_not_checked: c.steps_not_checked
|
|
297
|
+
});
|
|
298
|
+
if (c.verdict === "passed" && detail.length === 0) return [`${head} />`];
|
|
245
299
|
const lines = [`${head}>`];
|
|
246
300
|
const msg = escapeXml(c.message ?? "");
|
|
247
301
|
if (c.verdict === "failure") {
|
|
@@ -251,7 +305,7 @@ function caseLines(c, classname) {
|
|
|
251
305
|
} else if (c.verdict === "skipped") {
|
|
252
306
|
lines.push(` <skipped message="${msg}" />`);
|
|
253
307
|
}
|
|
254
|
-
|
|
308
|
+
lines.push(...detail);
|
|
255
309
|
lines.push(" </testcase>");
|
|
256
310
|
return lines;
|
|
257
311
|
}
|
|
@@ -263,7 +317,12 @@ function formatMultiSuiteJunit(result, opts = {}) {
|
|
|
263
317
|
const cases = (sr.case_runs ?? []).map((c) => {
|
|
264
318
|
const name = c.case_name || c.run_id;
|
|
265
319
|
const time = c.duration_ms ? (c.duration_ms / 1e3).toFixed(1) : "0";
|
|
266
|
-
const base = {
|
|
320
|
+
const base = {
|
|
321
|
+
name,
|
|
322
|
+
time,
|
|
323
|
+
url: c.url || void 0,
|
|
324
|
+
steps_not_checked: c.steps_not_checked
|
|
325
|
+
};
|
|
267
326
|
switch (c.status) {
|
|
268
327
|
case "passed":
|
|
269
328
|
return { ...base, verdict: "passed" };
|
|
@@ -1039,8 +1098,13 @@ ${chalk5.dim("Matched:")} ${totalMatched} suite(s)${matchedNames.length ? " \u20
|
|
|
1039
1098
|
} else if (opts.output === "junit") {
|
|
1040
1099
|
process.stdout.write(formatMultiSuiteJunit(last) + "\n");
|
|
1041
1100
|
} else {
|
|
1101
|
+
const totalNotChecked = last.suite_runs.reduce(
|
|
1102
|
+
(n, sr) => n + (sr.case_runs ?? []).reduce((m, cr) => m + (cr.steps_not_checked ?? 0), 0),
|
|
1103
|
+
0
|
|
1104
|
+
);
|
|
1105
|
+
const aggregateNote = totalNotChecked > 0 ? chalk5.yellow(` (${stepsNotCheckedLabel(totalNotChecked)})`) : "";
|
|
1042
1106
|
process.stdout.write(`
|
|
1043
|
-
Aggregate: ${last.status}
|
|
1107
|
+
Aggregate: ${last.status}${aggregateNote}
|
|
1044
1108
|
`);
|
|
1045
1109
|
for (const sr of last.suite_runs) {
|
|
1046
1110
|
const mark = sr.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
@@ -1056,8 +1120,9 @@ Aggregate: ${last.status}
|
|
|
1056
1120
|
for (const cr of sr.case_runs ?? []) {
|
|
1057
1121
|
const caseMark = cr.status === "passed" ? chalk5.green("\u2713") : cr.status === "cancelled" || cr.status === "pending" || cr.status === "running" ? chalk5.yellow("-") : chalk5.red("\u2717");
|
|
1058
1122
|
const dur = cr.duration_ms ? chalk5.dim(` (${(cr.duration_ms / 1e3).toFixed(1)}s)`) : "";
|
|
1123
|
+
const unchecked = notCheckedSuffix(cr.status, cr.steps_not_checked);
|
|
1059
1124
|
process.stdout.write(
|
|
1060
|
-
` ${caseMark} ${cr.case_name ?? "Case"} ${cr.status ?? "?"}${dur} ${chalk5.dim(cr.url)}
|
|
1125
|
+
` ${caseMark} ${cr.case_name ?? "Case"} ${cr.status ?? "?"}${unchecked ? chalk5.yellow(unchecked) : ""}${dur} ${chalk5.dim(cr.url)}
|
|
1061
1126
|
`
|
|
1062
1127
|
);
|
|
1063
1128
|
if (cr.error_message && cr.status !== "passed") {
|
|
@@ -4079,7 +4144,7 @@ program.command("dev-reload").description(
|
|
|
4079
4144
|
program.command("mcp").description(
|
|
4080
4145
|
"Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
|
|
4081
4146
|
).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
|
|
4082
|
-
const { runMcpServer } = await import("./server-
|
|
4147
|
+
const { runMcpServer } = await import("./server-RDGEVJD4.js");
|
|
4083
4148
|
await runMcpServer({ ...opts, version: pkg.version });
|
|
4084
4149
|
});
|
|
4085
4150
|
program.command("agent-setup").description(
|
|
@@ -4,15 +4,14 @@ import {
|
|
|
4
4
|
DEV_PLATFORMS,
|
|
5
5
|
PLATFORM_DEVICE,
|
|
6
6
|
PLATFORM_NEEDS_METRO,
|
|
7
|
-
|
|
8
|
-
buildLabel,
|
|
7
|
+
buildLines,
|
|
9
8
|
buildRows,
|
|
10
9
|
buildToolCall,
|
|
11
10
|
checkDevBuild,
|
|
12
11
|
checkMetro,
|
|
13
12
|
createRokuPush,
|
|
14
|
-
debugBuildsNewestFirst,
|
|
15
13
|
decodeScreenshotResult,
|
|
14
|
+
describeDevBuild,
|
|
16
15
|
effectiveDevConfig,
|
|
17
16
|
filterDeviceModels,
|
|
18
17
|
formatBuildRowsHuman,
|
|
@@ -20,15 +19,13 @@ import {
|
|
|
20
19
|
isDevPlatform,
|
|
21
20
|
loadDevConfig,
|
|
22
21
|
loadLocalDevConfig,
|
|
23
|
-
localAppIdentity,
|
|
24
22
|
parseBuildId,
|
|
25
|
-
pickDevBuild,
|
|
26
23
|
platformWithArticle,
|
|
27
24
|
readDevState,
|
|
28
25
|
renderEnvelope,
|
|
29
26
|
resolveMetro,
|
|
30
27
|
startDevSession
|
|
31
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-TPZG7NCU.js";
|
|
32
29
|
import {
|
|
33
30
|
ApiClient,
|
|
34
31
|
NotFoundError,
|
|
@@ -117,7 +114,7 @@ var DEV_START_DEF = {
|
|
|
117
114
|
},
|
|
118
115
|
build_id: {
|
|
119
116
|
type: "number",
|
|
120
|
-
description:
|
|
117
|
+
description: `Run this uploaded build of the app (an id from list_builds). Without standalone it must be a debug build (kind "debug"); with standalone any build of the app works. Omitted: the newest debug build (preferring one of the local project's own app version over a newer one of another version), or with standalone the newest release build. Not for roku.`
|
|
121
118
|
},
|
|
122
119
|
standalone: {
|
|
123
120
|
type: "boolean",
|
|
@@ -240,6 +237,9 @@ async function readSession(deps, sessionId) {
|
|
|
240
237
|
function isEndedStatus(status) {
|
|
241
238
|
return status === "stopped" || status === "failed";
|
|
242
239
|
}
|
|
240
|
+
function capitalize(text) {
|
|
241
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
242
|
+
}
|
|
243
243
|
function endedLine(ended) {
|
|
244
244
|
const why = ended.reason ? `: ${ended.reason}` : "";
|
|
245
245
|
return `session ${ended.sessionId} has ended (${ended.status}${why})`;
|
|
@@ -329,7 +329,7 @@ async function handleToolCall(toolName, args, deps, extra = {}) {
|
|
|
329
329
|
if (cmd.verb === "screenshot" && envelope.ok) {
|
|
330
330
|
return screenshotToolResult(envelope, args);
|
|
331
331
|
}
|
|
332
|
-
return textResult(renderEnvelope(envelope), envelope, !envelope.ok);
|
|
332
|
+
return textResult(renderEnvelope(envelope, toolNameFor(cmd.verb)), envelope, !envelope.ok);
|
|
333
333
|
}
|
|
334
334
|
function screenshotToolResult(envelope, args) {
|
|
335
335
|
const { image, preview } = splitScreenshotResult(envelope.result);
|
|
@@ -351,7 +351,7 @@ function screenshotToolResult(envelope, args) {
|
|
|
351
351
|
...envelope,
|
|
352
352
|
result: { written_to: outPath, bytes: imageBytes.length }
|
|
353
353
|
};
|
|
354
|
-
const lines = [`${renderEnvelope(sanitized)}
|
|
354
|
+
const lines = [`${renderEnvelope(sanitized, "screenshot")}
|
|
355
355
|
screenshot written to ${outPath}`];
|
|
356
356
|
let inlineBlock = null;
|
|
357
357
|
if (args.inline === true) {
|
|
@@ -457,7 +457,7 @@ async function handleDevStart(args, deps, extra = {}) {
|
|
|
457
457
|
const lines = [`${standalone ? "session" : "dev session"} started: ${payload.session_id}`];
|
|
458
458
|
const device = result.device ? deviceLine(result.device) : null;
|
|
459
459
|
if (device) lines.push(device);
|
|
460
|
-
lines.push(...
|
|
460
|
+
lines.push(...buildLines2(result, standalone));
|
|
461
461
|
if (typeof result.queuedSeconds === "number") {
|
|
462
462
|
lines.push(`waited ${Math.round(result.queuedSeconds)} s in the queue for a device`);
|
|
463
463
|
}
|
|
@@ -467,35 +467,17 @@ async function handleDevStart(args, deps, extra = {}) {
|
|
|
467
467
|
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
468
468
|
}
|
|
469
469
|
}
|
|
470
|
-
function
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
const rn = result.project?.reactNative;
|
|
483
|
-
if (!standalone && rn) {
|
|
484
|
-
lines.push(
|
|
485
|
-
`this project: react-native ${rn}. If the app shows "React Native version mismatch", the build was made with another React Native: run a build made from this project (build_id, see list_builds).`
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
return lines;
|
|
489
|
-
}
|
|
490
|
-
function startedBuild(b) {
|
|
491
|
-
return {
|
|
492
|
-
id: b.id,
|
|
493
|
-
version: b.version ?? null,
|
|
494
|
-
build_number: b.build_number ?? null,
|
|
495
|
-
kind: buildKind(b),
|
|
496
|
-
uploaded_at: b.uploaded_at ?? null,
|
|
497
|
-
note: b.note ?? null
|
|
498
|
-
};
|
|
470
|
+
function buildLines2(result, standalone) {
|
|
471
|
+
return buildLines(
|
|
472
|
+
{
|
|
473
|
+
build: result.build ?? null,
|
|
474
|
+
reason: result.buildReason ?? null,
|
|
475
|
+
pinned: result.pinned === true,
|
|
476
|
+
other_debug_builds: result.otherBuilds ?? [],
|
|
477
|
+
project: result.project ?? null
|
|
478
|
+
},
|
|
479
|
+
{ standalone, flag: "build_id", listBuilds: "list_builds" }
|
|
480
|
+
);
|
|
499
481
|
}
|
|
500
482
|
function deviceLine(d) {
|
|
501
483
|
const name = d.model || d.name;
|
|
@@ -631,7 +613,7 @@ async function handleDevStop(deps) {
|
|
|
631
613
|
}
|
|
632
614
|
if (!current) {
|
|
633
615
|
return textResult(
|
|
634
|
-
ended ? `no dev session was running \u2014 ${endedLine(ended)}
|
|
616
|
+
ended ? `no dev session was running \u2014 it had already ended. ${capitalize(endedLine(ended))}` : "no dev session was running",
|
|
635
617
|
{ session_id: null, stopped: false }
|
|
636
618
|
);
|
|
637
619
|
}
|
|
@@ -823,22 +805,8 @@ function createDevSessionController(auth, webUrl) {
|
|
|
823
805
|
if (buildId !== void 0) {
|
|
824
806
|
checkDevBuild(app, buildId, cfg.platform, { standalone });
|
|
825
807
|
}
|
|
826
|
-
const project = usesMetro ? localAppIdentity(process.cwd(), cfg.platform) : null;
|
|
827
|
-
let buildReason = null;
|
|
828
|
-
if (buildId === void 0 && usesMetro) {
|
|
829
|
-
const picked = pickDevBuild(app, project?.version ?? null);
|
|
830
|
-
if (picked?.reason) {
|
|
831
|
-
buildId = picked.build.id;
|
|
832
|
-
buildReason = picked.reason;
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
808
|
const isTv = PLATFORM_DEVICE[cfg.platform].tv;
|
|
836
809
|
const warnings = [];
|
|
837
|
-
if (project?.bundleId && app.package_name && project.bundleId !== app.package_name) {
|
|
838
|
-
warnings.push(
|
|
839
|
-
`this project's ${isTv || cfg.platform.startsWith("android") ? "package name" : "bundle id"} is ${project.bundleId}, but app ${app.public_id} is ${app.package_name} \u2014 check that dev_start runs the app this project builds (list_apps)`
|
|
840
|
-
);
|
|
841
|
-
}
|
|
842
810
|
const progress = startProgress(cfg.onProgress);
|
|
843
811
|
let started = null;
|
|
844
812
|
try {
|
|
@@ -853,6 +821,11 @@ function createDevSessionController(auth, webUrl) {
|
|
|
853
821
|
// A named model decides the category itself (startDevSession).
|
|
854
822
|
model: cfg.model,
|
|
855
823
|
buildId,
|
|
824
|
+
// The build choice (a debug build of this project's version over
|
|
825
|
+
// a newer one of another) and its report live in startDevSession,
|
|
826
|
+
// shared with `vibeview dev` and `--detach`.
|
|
827
|
+
app,
|
|
828
|
+
...usesMetro ? { projectDir: process.cwd() } : {},
|
|
856
829
|
...standalone ? { standalone } : {},
|
|
857
830
|
metroPort,
|
|
858
831
|
open: false,
|
|
@@ -880,23 +853,26 @@ function createDevSessionController(auth, webUrl) {
|
|
|
880
853
|
handle = started;
|
|
881
854
|
platform = cfg.platform;
|
|
882
855
|
ended = null;
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
856
|
+
const choice = handle.build ?? describeDevBuild(
|
|
857
|
+
app,
|
|
858
|
+
handle.session,
|
|
859
|
+
{ buildId, reason: null, pinned: buildId !== void 0 },
|
|
860
|
+
null
|
|
861
|
+
);
|
|
862
|
+
const project = choice.project;
|
|
863
|
+
if (project?.bundleId && app.package_name && project.bundleId !== app.package_name) {
|
|
864
|
+
warnings.push(
|
|
865
|
+
`this project's ${isTv || cfg.platform.startsWith("android") ? "package name" : "bundle id"} is ${project.bundleId}, but app ${app.public_id} is ${app.package_name} \u2014 check that dev_start runs the app this project builds (list_apps)`
|
|
866
|
+
);
|
|
867
|
+
}
|
|
886
868
|
return {
|
|
887
869
|
sessionId: handle.sessionId,
|
|
888
870
|
pageUrl: handle.pageUrl,
|
|
889
871
|
device: startedDevice(handle.session),
|
|
890
|
-
build:
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
kind: "unknown",
|
|
895
|
-
uploaded_at: null,
|
|
896
|
-
note: null
|
|
897
|
-
} : null,
|
|
898
|
-
buildReason,
|
|
899
|
-
otherBuilds: others.slice(0, 3).map(startedBuild),
|
|
872
|
+
build: choice.build,
|
|
873
|
+
buildReason: choice.reason,
|
|
874
|
+
pinned: choice.pinned,
|
|
875
|
+
otherBuilds: usesMetro ? choice.other_debug_builds : [],
|
|
900
876
|
project,
|
|
901
877
|
queuedSeconds: progress.queuedSeconds(),
|
|
902
878
|
warnings
|
|
@@ -1117,7 +1093,7 @@ export {
|
|
|
1117
1093
|
MCP_INSTRUCTIONS,
|
|
1118
1094
|
SHUTDOWN_TIMEOUT_MS,
|
|
1119
1095
|
START_PROGRESS_INTERVAL_MS,
|
|
1120
|
-
buildLines,
|
|
1096
|
+
buildLines2 as buildLines,
|
|
1121
1097
|
buildToolDefinitions,
|
|
1122
1098
|
createDevSessionController,
|
|
1123
1099
|
createShutdown,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeview",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "VibeView CLI -- run your app on cloud iOS simulators, Android emulators, Apple TV, Android TV and Roku (beta) devices: a React Native dev loop, device control for you or your coding agent (CLI or MCP), and tests from CI",
|
|
5
5
|
"mcpName": "io.github.vibeview/vibeview",
|
|
6
6
|
"type": "module",
|
|
@@ -51,11 +51,18 @@ https://vibeview.io/docs/preparing-your-build.
|
|
|
51
51
|
still missing (usually `--platform` or `--app`) — pass it and retry.
|
|
52
52
|
4. Parse the `session_ready` line from stdout:
|
|
53
53
|
```json
|
|
54
|
-
{"event":"session_ready","session_id":"...","url":"...","pid":1234}
|
|
54
|
+
{"event":"session_ready","session_id":"...","url":"...","pid":1234,"build":{...}}
|
|
55
55
|
```
|
|
56
56
|
Use `session_id` as `--session <id>` on every verb below. The event
|
|
57
57
|
fires once the session accepts commands (device up, app installed) —
|
|
58
|
-
no need to poll `dev-status` first.
|
|
58
|
+
no need to poll `dev-status` first. `build` says which build was
|
|
59
|
+
installed (`build.build`), why (`build.reason`: it prefers a debug build
|
|
60
|
+
of the project's own app version over a newer one of another version),
|
|
61
|
+
and the newest debug build of each other version (`build.other_debug_builds`).
|
|
62
|
+
If the app shows a red error screen right after start ("React Native
|
|
63
|
+
version mismatch", or "undefined is not a function" in a native module),
|
|
64
|
+
the build may be from other native code — rerun with `--build <id>` of
|
|
65
|
+
one of those. If the app ships no native changes
|
|
59
66
|
since the last run, skip step 1 and just do steps 2–4.
|
|
60
67
|
5. **Immediately tell the developer the session `url`** — "watch or take
|
|
61
68
|
over at any time: `<url>`". The page is a live, fully interactive view
|
|
@@ -244,6 +251,9 @@ streaming minutes even when idle:
|
|
|
244
251
|
```bash
|
|
245
252
|
vibeview dev-stop
|
|
246
253
|
```
|
|
254
|
+
If the `dev --detach` process is killed or the computer sleeps, its session
|
|
255
|
+
ends on its own within about 90 seconds (unless a browser tab is watching
|
|
256
|
+
it) — but don't rely on that; stop it.
|
|
247
257
|
A session you did not start with `dev` (one created through the API, the
|
|
248
258
|
sandbox, or a collaboration link) is stopped by id:
|
|
249
259
|
```bash
|
|
@@ -312,7 +322,7 @@ steps the CLI does with plain shell commands:
|
|
|
312
322
|
|----------|------|---------|
|
|
313
323
|
| `upload_app` | `file` (required path), `name` | Upload a build (zipped iOS/tvOS simulator `.app`, Android/Android TV `.apk`, Roku channel `.zip`). The MCP equivalent of `vibeview upload-app`, so step 1 of the loop needs no shell. Returns the `app_id` to pass to `dev_start`. |
|
|
314
324
|
| `list_apps` | `platform` | List the org's apps (app id, name, platform, bundle id). The MCP equivalent of `vibeview list-apps` — use it to find an app someone else uploaded. |
|
|
315
|
-
| `dev_start` | `platform` (required: `ios`/`android`/`tvos`/`androidtv`/`roku`), `app`, `metro_port`, `model`, `build_id`, `standalone` | Start a session held open by the MCP server. The MCP equivalent of `vibeview dev --detach`; `model` picks an exact device model (e.g. `"iPhone Duo"`); `build_id` runs that build instead of the newest debug one (see `list_builds`). `standalone: true` runs any build as it is with no Metro — a release, CI or cloud build (without `build_id`: the newest release build). An app for another platform is refused before a device is taken. Returns `page_url` — relay it to the developer immediately, same as step 5 of the loop — the device it got (model, OS, category; posture and lit screen on a foldable), and the build it installed (id, version, upload time) with the app's other debug builds. A red "React Native version mismatch"
|
|
325
|
+
| `dev_start` | `platform` (required: `ios`/`android`/`tvos`/`androidtv`/`roku`), `app`, `metro_port`, `model`, `build_id`, `standalone` | Start a session held open by the MCP server. The MCP equivalent of `vibeview dev --detach`; `model` picks an exact device model (e.g. `"iPhone Duo"`); `build_id` runs that build instead of the newest debug one (see `list_builds`). `standalone: true` runs any build as it is with no Metro — a release, CI or cloud build (without `build_id`: the newest release build). An app for another platform is refused before a device is taken. Returns `page_url` — relay it to the developer immediately, same as step 5 of the loop — the device it got (model, OS, category; posture and lit screen on a foldable), and the build it installed (id, version, upload time) with the app's other debug builds. It chooses the build the way `vibeview dev` does (a debug build of the project's own version over a newer one of another version) and says when it can't read the project's version. A red error screen right after start ("React Native version mismatch", or "undefined is not a function" in a native module) may mean the build was made from other native code: try one of the listed other builds with `build_id`. While every device is busy it waits in the queue, sending progress. |
|
|
316
326
|
| `list_device_models` | `device_type` | List the device models you can start (model, OS, platform, category, `foldable` marker, and how many are free/busy). The MCP equivalent of `vibeview list-devices --models`. |
|
|
317
327
|
| `list_builds` | `app` (required) | List an app's uploaded builds with their kind (`debug`/`release`/`unknown`). The MCP equivalent of `vibeview list-builds`. |
|
|
318
328
|
| `dev_stop` | — | Stop the session `dev_start` started. The MCP equivalent of `vibeview dev-stop`. Says "no dev session was running" when there was none. |
|
|
@@ -329,5 +339,7 @@ targets that session instead of whatever `vibeview dev --detach` state exists
|
|
|
329
339
|
in the project directory. Only one such session can be held at a time
|
|
330
340
|
(`dev_start` fails if one is already running), and it stays live — billing
|
|
331
341
|
streaming minutes — until `dev_stop` or the MCP connection ends (the server
|
|
332
|
-
stops it when the client quits or closes the connection;
|
|
333
|
-
|
|
342
|
+
stops it when the client quits or closes the connection; if the server is
|
|
343
|
+
killed or the computer sleeps, the session ends within about 90 seconds
|
|
344
|
+
unless a browser tab is watching it). Always call `dev_stop` when you
|
|
345
|
+
finish verifying.
|