vibeview 0.32.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.
- package/dist/{chunk-ZZK7YQSP.js → chunk-4D2E2PE4.js} +13 -3
- package/dist/{chunk-T6H5XTBX.js → chunk-TPZG7NCU.js} +352 -21
- package/dist/cli.js +85 -20
- package/dist/{server-FJSBWEHZ.js → server-RDGEVJD4.js} +314 -76
- package/dist/{upload-app-2LVG4LMB.js → upload-app-AKD73742.js} +1 -1
- package/package.json +1 -1
- package/skills/vibeview-agent/SKILL.md +34 -12
|
@@ -377,9 +377,19 @@ var ApiClient = class {
|
|
|
377
377
|
/** Report presence on a session and read it back in one call. A Roku dev
|
|
378
378
|
* loop has no tunnel and no browser, so this is what tells the backend
|
|
379
379
|
* someone is still using the session between device commands. Same
|
|
380
|
-
* response shape as `getSession`.
|
|
381
|
-
|
|
382
|
-
|
|
380
|
+
* response shape as `getSession`.
|
|
381
|
+
*
|
|
382
|
+
* `lease: true` also renews the caller's hold on the session: a dev loop
|
|
383
|
+
* (`vibeview dev`, `--detach`, MCP `dev_start`) renews it every
|
|
384
|
+
* `AGENT_LEASE_RENEW_MS`, and once renewals stop — the process was killed,
|
|
385
|
+
* crashed, or the computer went to sleep — the backend ends the session
|
|
386
|
+
* about a minute later instead of after the org idle timeout. */
|
|
387
|
+
async touchSession(sessionId, opts = {}) {
|
|
388
|
+
return this.requestRaw(
|
|
389
|
+
"POST",
|
|
390
|
+
`/sessions/${sessionId}/presence`,
|
|
391
|
+
opts.lease ? { lease: true } : void 0
|
|
392
|
+
);
|
|
383
393
|
}
|
|
384
394
|
async stopSession(sessionId) {
|
|
385
395
|
await this.requestRaw("DELETE", `/sessions/${sessionId}`);
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
resolveArtifact,
|
|
14
14
|
resolveAuth,
|
|
15
15
|
uploadAppBuild
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-4D2E2PE4.js";
|
|
17
17
|
|
|
18
18
|
// src/lib/dev-config.ts
|
|
19
19
|
import { readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
@@ -360,6 +360,104 @@ function checkDevBuild(app, buildId, platform, opts = {}) {
|
|
|
360
360
|
}
|
|
361
361
|
return build;
|
|
362
362
|
}
|
|
363
|
+
function debugBuildsNewestFirst(app) {
|
|
364
|
+
const time = (b) => b.uploaded_at ? Date.parse(b.uploaded_at) || 0 : 0;
|
|
365
|
+
return app.builds.filter((b) => canUseMetro(b) === true).sort((a, b) => time(b) - time(a) || b.id - a.id);
|
|
366
|
+
}
|
|
367
|
+
function pickDevBuild(app, localVersion) {
|
|
368
|
+
const builds = debugBuildsNewestFirst(app);
|
|
369
|
+
if (builds.length === 0) return null;
|
|
370
|
+
const newest = builds[0];
|
|
371
|
+
if (!localVersion || newest.version === localVersion) return { build: newest, reason: null };
|
|
372
|
+
const match = builds.find((b) => b.version === localVersion);
|
|
373
|
+
if (!match) return { build: newest, reason: null };
|
|
374
|
+
return {
|
|
375
|
+
build: match,
|
|
376
|
+
reason: `the newest debug build (${newest.id}) is version ${newest.version ?? "unknown"}, this project is version ${localVersion}`
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function buildLabel(b) {
|
|
380
|
+
const parts = [];
|
|
381
|
+
if (b.version) parts.push(`version ${b.version}${b.build_number ? ` (${b.build_number})` : ""}`);
|
|
382
|
+
parts.push(b.kind ?? buildKind(b));
|
|
383
|
+
if (b.uploaded_at) parts.push(`uploaded ${formatUtcTimestamp(b.uploaded_at)}`);
|
|
384
|
+
if (b.note) parts.push(JSON.stringify(b.note.length > 60 ? `${b.note.slice(0, 57)}...` : b.note));
|
|
385
|
+
return `${b.id} (${parts.join(", ")})`;
|
|
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
|
+
}
|
|
363
461
|
|
|
364
462
|
// src/commands/list-builds.ts
|
|
365
463
|
import chalk from "chalk";
|
|
@@ -615,6 +713,7 @@ import chalk2 from "chalk";
|
|
|
615
713
|
import { input, select } from "@inquirer/prompts";
|
|
616
714
|
|
|
617
715
|
// src/lib/project-detect.ts
|
|
716
|
+
import { execFile } from "child_process";
|
|
618
717
|
import { existsSync as existsSync2, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
|
|
619
718
|
import { join as join4 } from "path";
|
|
620
719
|
import ignore from "ignore";
|
|
@@ -791,6 +890,110 @@ function productionDefaultsFor(cwd, platform) {
|
|
|
791
890
|
}
|
|
792
891
|
return {};
|
|
793
892
|
}
|
|
893
|
+
function uniqueMatch(text, re) {
|
|
894
|
+
const found = /* @__PURE__ */ new Set();
|
|
895
|
+
for (const m of text.matchAll(re)) {
|
|
896
|
+
const v = m[1].trim().replace(/^"|"$/g, "");
|
|
897
|
+
if (v && !v.includes("$")) found.add(v);
|
|
898
|
+
}
|
|
899
|
+
return found.size === 1 ? [...found][0] : null;
|
|
900
|
+
}
|
|
901
|
+
function readText(path) {
|
|
902
|
+
try {
|
|
903
|
+
return readFileSync4(path, "utf-8");
|
|
904
|
+
} catch {
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
function pbxprojText(cwd) {
|
|
909
|
+
try {
|
|
910
|
+
const proj = readdirSync(join4(cwd, "ios")).find((f) => f.endsWith(".xcodeproj"));
|
|
911
|
+
return proj ? readText(join4(cwd, "ios", proj, "project.pbxproj")) : null;
|
|
912
|
+
} catch {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
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) {
|
|
949
|
+
const rn = readJson(cwd, "node_modules/react-native/package.json");
|
|
950
|
+
const reactNative = typeof rn?.version === "string" ? rn.version : null;
|
|
951
|
+
const android = platform === "android" || platform === "androidtv";
|
|
952
|
+
const ios = platform === "ios" || platform === "tvos";
|
|
953
|
+
let version = null;
|
|
954
|
+
let bundleId = null;
|
|
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
|
+
}
|
|
974
|
+
if (expo && typeof expo === "object") {
|
|
975
|
+
const cfg = expo;
|
|
976
|
+
if (typeof cfg.version === "string") version = cfg.version;
|
|
977
|
+
const section = android ? cfg.android : ios ? cfg.ios : null;
|
|
978
|
+
const id = android ? section?.package : section?.bundleIdentifier;
|
|
979
|
+
if (typeof id === "string") bundleId = id;
|
|
980
|
+
}
|
|
981
|
+
if (android && (!version || !bundleId)) {
|
|
982
|
+
const gradle = readText(join4(cwd, "android", "app", "build.gradle")) ?? readText(join4(cwd, "android", "app", "build.gradle.kts"));
|
|
983
|
+
if (gradle) {
|
|
984
|
+
version ??= uniqueMatch(gradle, /versionName\s*=?\s*["']([^"']+)["']/g);
|
|
985
|
+
bundleId ??= uniqueMatch(gradle, /applicationId\s*=?\s*["']([^"']+)["']/g);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (ios && (!version || !bundleId)) {
|
|
989
|
+
const pbx = pbxprojText(cwd);
|
|
990
|
+
if (pbx) {
|
|
991
|
+
version ??= uniqueMatch(pbx, /MARKETING_VERSION = ([^;]+);/g);
|
|
992
|
+
bundleId ??= uniqueMatch(pbx, /PRODUCT_BUNDLE_IDENTIFIER = ([^;]+);/g);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return { reactNative, version, bundleId, versionUnreadable: version ? null : versionUnreadable };
|
|
996
|
+
}
|
|
794
997
|
|
|
795
998
|
// src/lib/dev-setup.ts
|
|
796
999
|
function platformChoices(cwd) {
|
|
@@ -890,7 +1093,7 @@ async function promptMetroCommand(cwd, platform) {
|
|
|
890
1093
|
}
|
|
891
1094
|
|
|
892
1095
|
// src/lib/source-pack.ts
|
|
893
|
-
import { execFile, spawn } from "child_process";
|
|
1096
|
+
import { execFile as execFile2, spawn } from "child_process";
|
|
894
1097
|
import { lstatSync, mkdtempSync, readFileSync as readFileSync5, rmSync as rmSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
895
1098
|
import { tmpdir } from "os";
|
|
896
1099
|
import { join as join5 } from "path";
|
|
@@ -1029,7 +1232,7 @@ function probeTarNullSupport() {
|
|
|
1029
1232
|
const listPath = join5(tmp, "empty.list");
|
|
1030
1233
|
writeFileSync4(listPath, "");
|
|
1031
1234
|
const out = join5(tmp, "probe.tgz");
|
|
1032
|
-
|
|
1235
|
+
execFile2("tar", ["-czf", out, "-C", tmp, "--null", "-T", listPath], (err) => {
|
|
1033
1236
|
rmSync4(tmp, { recursive: true, force: true });
|
|
1034
1237
|
resolve(!err);
|
|
1035
1238
|
});
|
|
@@ -2359,11 +2562,24 @@ function parseWatchSeconds(flag) {
|
|
|
2359
2562
|
}
|
|
2360
2563
|
return seconds;
|
|
2361
2564
|
}
|
|
2565
|
+
var AGENT_LEASE_RENEW_MS = 15e3;
|
|
2566
|
+
var AGENT_LEASE_TTL_MS = 6e4;
|
|
2567
|
+
function leaseLapsedReason(gapMs) {
|
|
2568
|
+
if (gapMs <= AGENT_LEASE_TTL_MS) return void 0;
|
|
2569
|
+
return `the session ended because this computer did not check in with VibeView for ${Math.round(gapMs / 1e3)}s (asleep, offline, or the process was paused) \u2014 start a new session to continue`;
|
|
2570
|
+
}
|
|
2362
2571
|
function queuedLine(position, reasonMessage) {
|
|
2363
2572
|
const where = position ? ` (position ${position})` : "";
|
|
2364
2573
|
const why = reasonMessage ?? "Every device is busy.";
|
|
2365
2574
|
return `\u23F3 ${why} Waiting in the queue${where}, Ctrl-C to give up.`;
|
|
2366
2575
|
}
|
|
2576
|
+
function devBuildLines(choice, standalone) {
|
|
2577
|
+
return buildLines(choice, {
|
|
2578
|
+
standalone,
|
|
2579
|
+
flag: "--build <id>",
|
|
2580
|
+
listBuilds: "vibeview list-builds"
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2367
2583
|
function emit(json, evt) {
|
|
2368
2584
|
if (json) console.log(JSON.stringify(evt));
|
|
2369
2585
|
}
|
|
@@ -2372,16 +2588,43 @@ async function startDevSession(cfg, onEvent = () => {
|
|
|
2372
2588
|
const api = new ApiClient(cfg.baseUrl, cfg.token);
|
|
2373
2589
|
const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform] && !cfg.standalone;
|
|
2374
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 };
|
|
2375
2611
|
const created = await api.createSession(
|
|
2376
|
-
devSessionCreateBody(
|
|
2612
|
+
devSessionCreateBody(
|
|
2613
|
+
pool ? { ...body, deviceCategory: pool.category, model: pool.model } : body
|
|
2614
|
+
)
|
|
2377
2615
|
);
|
|
2378
2616
|
const sessionId = created.session_id;
|
|
2379
2617
|
let stopping = false;
|
|
2380
2618
|
let tunnel = null;
|
|
2381
2619
|
let queueEntryId = null;
|
|
2620
|
+
let lastLeaseAt = Date.now();
|
|
2621
|
+
let leaseTimer = null;
|
|
2382
2622
|
const stopSession = async (code) => {
|
|
2383
2623
|
if (stopping) return;
|
|
2384
2624
|
stopping = true;
|
|
2625
|
+
if (leaseTimer) clearInterval(leaseTimer);
|
|
2626
|
+
leaseTimer = null;
|
|
2627
|
+
const reason = code !== 0 ? leaseLapsedReason(Date.now() - lastLeaseAt) : void 0;
|
|
2385
2628
|
tunnel?.stop();
|
|
2386
2629
|
if (queueEntryId !== null) {
|
|
2387
2630
|
await api.cancelQueueEntry(queueEntryId).catch(() => {
|
|
@@ -2390,8 +2633,27 @@ async function startDevSession(cfg, onEvent = () => {
|
|
|
2390
2633
|
}
|
|
2391
2634
|
await api.stopSession(sessionId).catch(() => {
|
|
2392
2635
|
});
|
|
2393
|
-
onEvent({ event: "session_ended", session_id: sessionId, code });
|
|
2636
|
+
onEvent({ event: "session_ended", session_id: sessionId, code, ...reason ? { reason } : {} });
|
|
2637
|
+
};
|
|
2638
|
+
const renewLease = async () => {
|
|
2639
|
+
if (stopping) return;
|
|
2640
|
+
let info;
|
|
2641
|
+
try {
|
|
2642
|
+
info = await api.touchSession(sessionId, { lease: true });
|
|
2643
|
+
} catch {
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
if (stopping) return;
|
|
2647
|
+
if (info.status === "stopped" || info.status === "failed") {
|
|
2648
|
+
void stopSession(1);
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
lastLeaseAt = Date.now();
|
|
2394
2652
|
};
|
|
2653
|
+
void renewLease();
|
|
2654
|
+
leaseTimer = setInterval(() => void renewLease(), AGENT_LEASE_RENEW_MS);
|
|
2655
|
+
leaseTimer.unref?.();
|
|
2656
|
+
cfg.onCreated?.({ sessionId, stop: () => stopSession(1) });
|
|
2395
2657
|
try {
|
|
2396
2658
|
const started = await api.startSession(sessionId);
|
|
2397
2659
|
if (started.status === "queued") {
|
|
@@ -2406,6 +2668,7 @@ async function startDevSession(cfg, onEvent = () => {
|
|
|
2406
2668
|
});
|
|
2407
2669
|
await pollWhileQueued(api, sessionId, QUEUE_WAIT_MS);
|
|
2408
2670
|
queueEntryId = null;
|
|
2671
|
+
cfg.onDequeued?.();
|
|
2409
2672
|
}
|
|
2410
2673
|
const pageUrl = `${cfg.webUrl || cfg.baseUrl}/sandbox/${sessionId}`;
|
|
2411
2674
|
const reportedWarnings = /* @__PURE__ */ new Set();
|
|
@@ -2458,7 +2721,15 @@ async function startDevSession(cfg, onEvent = () => {
|
|
|
2458
2721
|
reportedWarnings.add(w);
|
|
2459
2722
|
onEvent({ event: "warning", message: w });
|
|
2460
2723
|
}
|
|
2461
|
-
return {
|
|
2724
|
+
return {
|
|
2725
|
+
sessionId,
|
|
2726
|
+
pageUrl,
|
|
2727
|
+
stop: () => stopSession(0),
|
|
2728
|
+
reportedWarnings,
|
|
2729
|
+
session: active,
|
|
2730
|
+
lapsedReason: () => leaseLapsedReason(Date.now() - lastLeaseAt),
|
|
2731
|
+
build: usesTunnel && cfg.projectDir ? describeDevBuild(app, active, { ...pick, pinned: cfg.buildId !== void 0 }, project) : null
|
|
2732
|
+
};
|
|
2462
2733
|
} catch (err) {
|
|
2463
2734
|
await stopSession(1);
|
|
2464
2735
|
throw err;
|
|
@@ -3076,6 +3347,7 @@ async function runDevInProcess(opts) {
|
|
|
3076
3347
|
deviceCategory: cfg.deviceCategory,
|
|
3077
3348
|
model: cfg.model,
|
|
3078
3349
|
buildId: pinnedBuildId,
|
|
3350
|
+
projectDir: process.cwd(),
|
|
3079
3351
|
...standalone ? { standalone } : {},
|
|
3080
3352
|
metroPort: cfg.metroPort ?? 8081,
|
|
3081
3353
|
// startDevSession's own browser-open happens before we've written the
|
|
@@ -3102,7 +3374,7 @@ async function runDevInProcess(opts) {
|
|
|
3102
3374
|
chalk4.dim(" Putting the channel on the Roku \u2014 the link appears once it is up.")
|
|
3103
3375
|
);
|
|
3104
3376
|
} else if (evt.event === "session_ended" && !stopping && handle) {
|
|
3105
|
-
console.error(chalk4.red("
|
|
3377
|
+
console.error(chalk4.red(evt.reason ?? "session ended by the server"));
|
|
3106
3378
|
stopping = true;
|
|
3107
3379
|
endedEmitted = true;
|
|
3108
3380
|
emit(opts.json, evt);
|
|
@@ -3133,6 +3405,8 @@ async function runDevInProcess(opts) {
|
|
|
3133
3405
|
}
|
|
3134
3406
|
);
|
|
3135
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}`));
|
|
3136
3410
|
console.error(`
|
|
3137
3411
|
Watch and interact: ${chalk4.cyan(handle.pageUrl)}
|
|
3138
3412
|
${keysLine}
|
|
@@ -3150,7 +3424,8 @@ async function runDevInProcess(opts) {
|
|
|
3150
3424
|
event: "session_ready",
|
|
3151
3425
|
session_id: handle.sessionId,
|
|
3152
3426
|
url: handle.pageUrl,
|
|
3153
|
-
pid: process.pid
|
|
3427
|
+
pid: process.pid,
|
|
3428
|
+
build: handle.build ?? null
|
|
3154
3429
|
});
|
|
3155
3430
|
if (opts.open !== false) await openBrowser(handle.pageUrl).catch(() => {
|
|
3156
3431
|
});
|
|
@@ -3217,8 +3492,14 @@ async function runDevInProcess(opts) {
|
|
|
3217
3492
|
}
|
|
3218
3493
|
const verdict = sessionLivenessVerdict(info);
|
|
3219
3494
|
if (verdict.end && !stopping) {
|
|
3220
|
-
|
|
3221
|
-
|
|
3495
|
+
const reason = handle?.lapsedReason?.();
|
|
3496
|
+
console.error(chalk4.red(reason ?? verdict.message));
|
|
3497
|
+
emit(opts.json, {
|
|
3498
|
+
event: "session_ended",
|
|
3499
|
+
session_id: sessionId,
|
|
3500
|
+
code: 1,
|
|
3501
|
+
...reason ? { reason } : {}
|
|
3502
|
+
});
|
|
3222
3503
|
endedEmitted = true;
|
|
3223
3504
|
await stopSession(1);
|
|
3224
3505
|
return;
|
|
@@ -3276,6 +3557,8 @@ async function dev(opts) {
|
|
|
3276
3557
|
} else {
|
|
3277
3558
|
console.error(chalk4.dim(`Using ${resolveAuth(opts).baseUrl}`));
|
|
3278
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}`));
|
|
3279
3562
|
console.error(`
|
|
3280
3563
|
Watch and interact: ${chalk4.cyan(parsed.url)}`);
|
|
3281
3564
|
console.error(chalk4.dim(` vibeview dev-status \u2014 check on it`));
|
|
@@ -3664,7 +3947,7 @@ var AGENT_COMMANDS = [
|
|
|
3664
3947
|
// iOS system alerts only.
|
|
3665
3948
|
verb: "alert",
|
|
3666
3949
|
tool: "alert",
|
|
3667
|
-
summary: "Inspect or respond to a visible
|
|
3950
|
+
summary: "Inspect or respond to a visible system alert on iOS or Apple TV (get, accept, or dismiss). `get` returns the message and the button labels.",
|
|
3668
3951
|
mutating: true,
|
|
3669
3952
|
params: [
|
|
3670
3953
|
{
|
|
@@ -3747,7 +4030,7 @@ var AGENT_COMMANDS = [
|
|
|
3747
4030
|
// tools.py open_url: { url: string }.
|
|
3748
4031
|
verb: "open-url",
|
|
3749
4032
|
tool: "open_url",
|
|
3750
|
-
summary: "Open a deep link
|
|
4033
|
+
summary: "Open a deep link in the app under test. The link goes to that app only, never a browser: on Android a link the app has no screen for fails; on iOS only the app's own URL scheme (e.g. myapp://settings) is accepted, and a web or system link is refused.",
|
|
3751
4034
|
mutating: true,
|
|
3752
4035
|
params: [
|
|
3753
4036
|
{
|
|
@@ -3759,6 +4042,16 @@ var AGENT_COMMANDS = [
|
|
|
3759
4042
|
}
|
|
3760
4043
|
]
|
|
3761
4044
|
},
|
|
4045
|
+
{
|
|
4046
|
+
// relaunch_app: {} — terminate the session's app and launch it again the
|
|
4047
|
+
// way the session launched it (same launch params); its data is kept.
|
|
4048
|
+
// Refused on embed sessions (their watchdog owns launching) and on Roku.
|
|
4049
|
+
verb: "relaunch-app",
|
|
4050
|
+
tool: "relaunch_app",
|
|
4051
|
+
summary: "Close the app under test and start it again: a cold start that keeps the app's data, e.g. to check that something persists. Its log keeps streaming across the restart. Not available on Roku or embed sessions.",
|
|
4052
|
+
mutating: true,
|
|
4053
|
+
params: []
|
|
4054
|
+
},
|
|
3762
4055
|
{
|
|
3763
4056
|
// set_posture: { posture: 'closed' | 'partial' | 'open' } OR { angle: 0..180 }
|
|
3764
4057
|
// — exactly one (buildToolCall enforces it CLI-side, agent_control.py's
|
|
@@ -3805,6 +4098,33 @@ var AGENT_COMMANDS = [
|
|
|
3805
4098
|
}
|
|
3806
4099
|
]
|
|
3807
4100
|
},
|
|
4101
|
+
{
|
|
4102
|
+
// set_location: { latitude: -90..90, longitude: -180..180 } — both
|
|
4103
|
+
// required (agent_control.py validate_tool_args refuses anything else).
|
|
4104
|
+
// The same control as the dashboard's Set location. Flags rather than
|
|
4105
|
+
// positionals: a western/southern coordinate starts with `-`, which a
|
|
4106
|
+
// positional would read as an option.
|
|
4107
|
+
verb: "set-location",
|
|
4108
|
+
tool: "set_location",
|
|
4109
|
+
summary: "Set the device's simulated GPS location, for location-based flows. Phones, tablets and foldables (iOS and Android); not available on TV devices.",
|
|
4110
|
+
mutating: true,
|
|
4111
|
+
params: [
|
|
4112
|
+
{
|
|
4113
|
+
name: "latitude",
|
|
4114
|
+
cli: "--lat",
|
|
4115
|
+
type: "number",
|
|
4116
|
+
required: true,
|
|
4117
|
+
description: "Latitude in decimal degrees, -90 to 90."
|
|
4118
|
+
},
|
|
4119
|
+
{
|
|
4120
|
+
name: "longitude",
|
|
4121
|
+
cli: "--lon",
|
|
4122
|
+
type: "number",
|
|
4123
|
+
required: true,
|
|
4124
|
+
description: "Longitude in decimal degrees, -180 to 180 (west is negative)."
|
|
4125
|
+
}
|
|
4126
|
+
]
|
|
4127
|
+
},
|
|
3808
4128
|
{
|
|
3809
4129
|
// tools.py wait: { seconds?: number } (default: 2). Note the wire arg is
|
|
3810
4130
|
// "seconds", NOT "ms" — tool_dispatch.py reads args.get("seconds", 2)
|
|
@@ -3897,12 +4217,12 @@ var AGENT_COMMANDS = [
|
|
|
3897
4217
|
|
|
3898
4218
|
// src/agent/render.ts
|
|
3899
4219
|
import chalk6 from "chalk";
|
|
3900
|
-
function renderEnvelope(env) {
|
|
4220
|
+
function renderEnvelope(env, label = env.tool) {
|
|
3901
4221
|
const lines = [];
|
|
3902
4222
|
if (env.ok) {
|
|
3903
|
-
lines.push(chalk6.green(`ok`) + chalk6.dim(` (${
|
|
4223
|
+
lines.push(chalk6.green(`ok`) + chalk6.dim(` (${label})`));
|
|
3904
4224
|
} else {
|
|
3905
|
-
lines.push(chalk6.red(`error`) + chalk6.dim(` (${
|
|
4225
|
+
lines.push(chalk6.red(`error`) + chalk6.dim(` (${label})`));
|
|
3906
4226
|
}
|
|
3907
4227
|
lines.push(...resultLines(env.result));
|
|
3908
4228
|
if (env.observation) {
|
|
@@ -3949,6 +4269,14 @@ function resultLines(result) {
|
|
|
3949
4269
|
const state = deviceStateLine(r);
|
|
3950
4270
|
if (state) lines.push(state);
|
|
3951
4271
|
}
|
|
4272
|
+
if (Array.isArray(r.buttons) && r.buttons.length > 0) {
|
|
4273
|
+
const labels = r.buttons.filter((b) => typeof b === "string");
|
|
4274
|
+
if (labels.length > 0)
|
|
4275
|
+
lines.push(`buttons: ${labels.map((b) => JSON.stringify(b)).join(", ")}`);
|
|
4276
|
+
}
|
|
4277
|
+
if (r.action === "location_set" && typeof r.latitude === "number" && typeof r.longitude === "number") {
|
|
4278
|
+
lines.push(`location: ${r.latitude}, ${r.longitude}`);
|
|
4279
|
+
}
|
|
3952
4280
|
if (typeof r.note === "string" && r.note) lines.push(chalk6.dim(r.note));
|
|
3953
4281
|
if (typeof r.message === "string" && r.message) lines.push(r.message);
|
|
3954
4282
|
if (typeof r.warning === "string" && r.warning) lines.push(r.warning);
|
|
@@ -3959,8 +4287,8 @@ function deviceStateLine(r) {
|
|
|
3959
4287
|
if (r.action === "rotated") {
|
|
3960
4288
|
if (typeof r.device_orientation === "string") {
|
|
3961
4289
|
let o = `device held ${r.device_orientation}`;
|
|
3962
|
-
if (typeof r.screen_orientation === "string" && r.
|
|
3963
|
-
o += ` (picture on the lit screen: ${r.screen_orientation})`;
|
|
4290
|
+
if (typeof r.screen_orientation === "string" && !r.device_orientation.startsWith(r.screen_orientation)) {
|
|
4291
|
+
o += typeof r.posture === "string" ? ` (picture on the lit screen: ${r.screen_orientation})` : ` (screen stayed ${r.screen_orientation})`;
|
|
3964
4292
|
}
|
|
3965
4293
|
parts.push(o);
|
|
3966
4294
|
}
|
|
@@ -4196,9 +4524,11 @@ async function runAgentVerb(cmd, positionals, opts) {
|
|
|
4196
4524
|
}
|
|
4197
4525
|
writeFileSync6(outPath, imageBytes);
|
|
4198
4526
|
if (opts.json) {
|
|
4199
|
-
console.log(
|
|
4527
|
+
console.log(
|
|
4528
|
+
JSON.stringify({ ...envelope, result: { written_to: outPath, bytes: imageBytes.length } })
|
|
4529
|
+
);
|
|
4200
4530
|
} else {
|
|
4201
|
-
process.stderr.write(renderEnvelope(envelope) + "\n");
|
|
4531
|
+
process.stderr.write(renderEnvelope(envelope, cmd.verb) + "\n");
|
|
4202
4532
|
console.log(outPath);
|
|
4203
4533
|
}
|
|
4204
4534
|
process.exit(agentExitCode(envelope));
|
|
@@ -4207,7 +4537,7 @@ async function runAgentVerb(cmd, positionals, opts) {
|
|
|
4207
4537
|
if (opts.json) {
|
|
4208
4538
|
console.log(JSON.stringify(envelope));
|
|
4209
4539
|
} else {
|
|
4210
|
-
console.log(renderEnvelope(envelope));
|
|
4540
|
+
console.log(renderEnvelope(envelope, cmd.verb));
|
|
4211
4541
|
if (cmd.verb === "press" && !envelope.ok) process.stderr.write(pressButtonsHelp() + "\n");
|
|
4212
4542
|
}
|
|
4213
4543
|
process.exit(agentExitCode(envelope));
|
|
@@ -4223,6 +4553,8 @@ export {
|
|
|
4223
4553
|
resolveMetro,
|
|
4224
4554
|
parseBuildId,
|
|
4225
4555
|
checkDevBuild,
|
|
4556
|
+
describeDevBuild,
|
|
4557
|
+
buildLines,
|
|
4226
4558
|
buildRows,
|
|
4227
4559
|
formatBuildRowsHuman,
|
|
4228
4560
|
listBuilds,
|
|
@@ -4243,7 +4575,6 @@ export {
|
|
|
4243
4575
|
printOutcomeLine,
|
|
4244
4576
|
createRokuPush,
|
|
4245
4577
|
startDevSession,
|
|
4246
|
-
ROKU_LIVENESS_INTERVAL_MS,
|
|
4247
4578
|
PLATFORM_DEVICE,
|
|
4248
4579
|
dev,
|
|
4249
4580
|
formatDevicesHuman,
|