vibeview 0.21.0 → 0.23.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-VVKF7GGL.js → chunk-E2LZSLRF.js} +245 -58
- package/dist/{chunk-KON2FOEC.js → chunk-PSFXVUHG.js} +11 -0
- package/dist/cli.js +224 -141
- package/dist/{server-WWNWYD4Q.js → server-XMSBLIWY.js} +165 -12
- package/dist/{upload-app-W6NOSJPA.js → upload-app-ZAKAOJKE.js} +1 -1
- package/package.json +1 -1
- package/skills/vibeview-agent/SKILL.md +39 -3
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
loadConfig,
|
|
7
7
|
resolveAuth,
|
|
8
8
|
uploadAppBuild
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-PSFXVUHG.js";
|
|
10
10
|
|
|
11
11
|
// src/lib/dev-state.ts
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
@@ -528,10 +528,6 @@ function noAppsMessage(platform, hasBuildConfig) {
|
|
|
528
528
|
}
|
|
529
529
|
return `No ${platform} apps found. Run \`vibeview build --platform ${platform}\` to build and upload one (it will ask for your build command), or upload a build with \`vibeview upload-app\`.`;
|
|
530
530
|
}
|
|
531
|
-
function firstRunAppStrategy(apps, buildConfigured) {
|
|
532
|
-
if (!buildConfigured) return "picker";
|
|
533
|
-
return apps.length === 0 ? "build_now" : "ask";
|
|
534
|
-
}
|
|
535
531
|
function fillLaterHint(skipped, platform) {
|
|
536
532
|
if (!skipped.build && !skipped.metro) return "";
|
|
537
533
|
const lines = ["You can enable one-command builds later \u2014 add to vibeview.json:"];
|
|
@@ -1586,6 +1582,38 @@ function printOutcomeLine(build, multi) {
|
|
|
1586
1582
|
console.error(line);
|
|
1587
1583
|
}
|
|
1588
1584
|
|
|
1585
|
+
// src/lib/roku-push.ts
|
|
1586
|
+
function createRokuPush(opts) {
|
|
1587
|
+
const runBuild = opts.runBuild ?? runBuildForPlatform;
|
|
1588
|
+
return async () => {
|
|
1589
|
+
const started = Date.now();
|
|
1590
|
+
let result;
|
|
1591
|
+
try {
|
|
1592
|
+
result = await runBuild(
|
|
1593
|
+
opts.cwd,
|
|
1594
|
+
"roku",
|
|
1595
|
+
opts.build,
|
|
1596
|
+
{ token: opts.token, baseUrl: opts.baseUrl },
|
|
1597
|
+
opts.log
|
|
1598
|
+
);
|
|
1599
|
+
} catch (err) {
|
|
1600
|
+
const message = err.message;
|
|
1601
|
+
if (/artifact not found/i.test(message)) {
|
|
1602
|
+
throw new Error(
|
|
1603
|
+
`${message}
|
|
1604
|
+
your build command must produce a zip \u2014 use the package/zip step, not deploy`
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
throw err;
|
|
1608
|
+
}
|
|
1609
|
+
if (!result.build_id) {
|
|
1610
|
+
throw new Error(`upload returned no build id for ${result.name ?? result.public_id}`);
|
|
1611
|
+
}
|
|
1612
|
+
await opts.api.installBuild(opts.sessionId, result.build_id, true);
|
|
1613
|
+
opts.log(`reloaded in ${((Date.now() - started) / 1e3).toFixed(1)}s`);
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1589
1617
|
// src/commands/dev.ts
|
|
1590
1618
|
import { spawn as spawn4 } from "child_process";
|
|
1591
1619
|
import { createWriteStream, mkdirSync as mkdirSync3 } from "fs";
|
|
@@ -2015,38 +2043,6 @@ function watchProjectFiles(cwd, artifact, userIgnore = []) {
|
|
|
2015
2043
|
};
|
|
2016
2044
|
}
|
|
2017
2045
|
|
|
2018
|
-
// src/lib/roku-push.ts
|
|
2019
|
-
function createRokuPush(opts) {
|
|
2020
|
-
const runBuild = opts.runBuild ?? runBuildForPlatform;
|
|
2021
|
-
return async () => {
|
|
2022
|
-
const started = Date.now();
|
|
2023
|
-
let result;
|
|
2024
|
-
try {
|
|
2025
|
-
result = await runBuild(
|
|
2026
|
-
opts.cwd,
|
|
2027
|
-
"roku",
|
|
2028
|
-
opts.build,
|
|
2029
|
-
{ token: opts.token, baseUrl: opts.baseUrl },
|
|
2030
|
-
opts.log
|
|
2031
|
-
);
|
|
2032
|
-
} catch (err) {
|
|
2033
|
-
const message = err.message;
|
|
2034
|
-
if (/artifact not found/i.test(message)) {
|
|
2035
|
-
throw new Error(
|
|
2036
|
-
`${message}
|
|
2037
|
-
your build command must produce a zip \u2014 use the package/zip step, not deploy`
|
|
2038
|
-
);
|
|
2039
|
-
}
|
|
2040
|
-
throw err;
|
|
2041
|
-
}
|
|
2042
|
-
if (!result.build_id) {
|
|
2043
|
-
throw new Error(`upload returned no build id for ${result.name ?? result.public_id}`);
|
|
2044
|
-
}
|
|
2045
|
-
await opts.api.installBuild(opts.sessionId, result.build_id, true);
|
|
2046
|
-
opts.log(`reloaded in ${((Date.now() - started) / 1e3).toFixed(1)}s`);
|
|
2047
|
-
};
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
2046
|
// src/commands/dev.ts
|
|
2051
2047
|
function parseWatchSeconds(flag) {
|
|
2052
2048
|
if (flag === void 0 || flag === false) return void 0;
|
|
@@ -2064,7 +2060,10 @@ async function startDevSession(cfg, onEvent = () => {
|
|
|
2064
2060
|
}, onTunnelState) {
|
|
2065
2061
|
const api = new ApiClient(cfg.baseUrl, cfg.token);
|
|
2066
2062
|
const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform];
|
|
2067
|
-
const
|
|
2063
|
+
const pool = cfg.model ? resolveDeviceModel(await api.listDeviceModels(), cfg.platform, cfg.model) : null;
|
|
2064
|
+
const created = await api.createSession(
|
|
2065
|
+
devSessionCreateBody(pool ? { ...cfg, deviceCategory: pool.category, model: pool.model } : cfg)
|
|
2066
|
+
);
|
|
2068
2067
|
const sessionId = created.session_id;
|
|
2069
2068
|
let stopping = false;
|
|
2070
2069
|
let tunnel = null;
|
|
@@ -2134,7 +2133,13 @@ async function startDevSession(cfg, onEvent = () => {
|
|
|
2134
2133
|
}
|
|
2135
2134
|
if (cfg.open !== false) await openBrowser(pageUrl).catch(() => {
|
|
2136
2135
|
});
|
|
2137
|
-
const active = await pollUntilActive(
|
|
2136
|
+
const active = await pollUntilActive(
|
|
2137
|
+
api,
|
|
2138
|
+
sessionId,
|
|
2139
|
+
18e4,
|
|
2140
|
+
2e3,
|
|
2141
|
+
() => onEvent({ event: "installing", session_id: sessionId })
|
|
2142
|
+
);
|
|
2138
2143
|
for (const w of active.warnings ?? []) {
|
|
2139
2144
|
if (reportedWarnings.has(w)) continue;
|
|
2140
2145
|
reportedWarnings.add(w);
|
|
@@ -2152,9 +2157,23 @@ function devSessionCreateBody(cfg) {
|
|
|
2152
2157
|
device_category: cfg.deviceCategory,
|
|
2153
2158
|
app_id: cfg.appId,
|
|
2154
2159
|
// Roku has no JS bundle to serve, so there is nothing to tunnel.
|
|
2155
|
-
tunnel: PLATFORM_NEEDS_METRO[cfg.platform]
|
|
2160
|
+
tunnel: PLATFORM_NEEDS_METRO[cfg.platform],
|
|
2161
|
+
...cfg.model ? { model: cfg.model } : {}
|
|
2156
2162
|
};
|
|
2157
2163
|
}
|
|
2164
|
+
function resolveDeviceModel(pools, platform, model) {
|
|
2165
|
+
const { deviceType, tv } = PLATFORM_DEVICE[platform];
|
|
2166
|
+
const candidates = pools.filter(
|
|
2167
|
+
(p) => p.device_type === deviceType && p.category === "tv" === tv
|
|
2168
|
+
);
|
|
2169
|
+
const wanted = model.trim();
|
|
2170
|
+
const match = candidates.find((p) => p.model === wanted) ?? candidates.find((p) => p.model.toLowerCase() === wanted.toLowerCase());
|
|
2171
|
+
if (match) return match;
|
|
2172
|
+
const names = [...new Set(candidates.map((p) => p.model))].sort();
|
|
2173
|
+
throw new Error(
|
|
2174
|
+
names.length ? `no ${platform} device model named "${model}" \u2014 available: ${names.join(", ")} (see \`vibeview list-devices --models\`)` : `no ${platform} devices are available right now`
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2158
2177
|
var BLOCKING_WARNING_PREFIXES = ["No debug build", "App install failed", "App not found"];
|
|
2159
2178
|
function findBlockingWarning(info) {
|
|
2160
2179
|
return (info.warnings ?? []).find((w) => BLOCKING_WARNING_PREFIXES.some((p) => w.includes(p)));
|
|
@@ -2192,13 +2211,18 @@ async function pollUntilTunnelReady(api, sessionId, timeoutMs, intervalMs = 2e3)
|
|
|
2192
2211
|
await new Promise((r) => setTimeout(r, intervalMs));
|
|
2193
2212
|
}
|
|
2194
2213
|
}
|
|
2195
|
-
async function pollUntilActive(api, sessionId, timeoutMs, intervalMs = 2e3) {
|
|
2214
|
+
async function pollUntilActive(api, sessionId, timeoutMs, intervalMs = 2e3, onInstallPending) {
|
|
2196
2215
|
const deadline = Date.now() + timeoutMs;
|
|
2216
|
+
let pendingReported = false;
|
|
2197
2217
|
for (; ; ) {
|
|
2198
|
-
const info = await api.
|
|
2218
|
+
const info = await api.touchSession(sessionId);
|
|
2199
2219
|
const blocking = findBlockingWarning(info);
|
|
2200
2220
|
if (blocking) throw new Error(blocking);
|
|
2201
|
-
if (info.status === "active"
|
|
2221
|
+
if (info.status === "active" && info.app_install_pending && !pendingReported) {
|
|
2222
|
+
pendingReported = true;
|
|
2223
|
+
onInstallPending?.();
|
|
2224
|
+
}
|
|
2225
|
+
if (info.status === "active" && !info.app_install_pending) return info;
|
|
2202
2226
|
if (info.status === "stopped" || info.status === "failed") {
|
|
2203
2227
|
throw new Error(`session ${info.status} before it became active`);
|
|
2204
2228
|
}
|
|
@@ -2250,6 +2274,12 @@ function validateAppChoice(requested, fromFlag, apps) {
|
|
|
2250
2274
|
if (apps.some((a) => a.public_id === requested)) return { appId: requested };
|
|
2251
2275
|
return fromFlag ? { staleFlag: requested } : { staleCached: requested };
|
|
2252
2276
|
}
|
|
2277
|
+
function buildAppStrategy(apps, buildFlag, json, isTTY) {
|
|
2278
|
+
if (buildFlag) return "build";
|
|
2279
|
+
if (apps.length === 0) return "build";
|
|
2280
|
+
if (json || !isTTY) return "pick";
|
|
2281
|
+
return "ask";
|
|
2282
|
+
}
|
|
2253
2283
|
function requireNonInteractive(json) {
|
|
2254
2284
|
if (!json) return;
|
|
2255
2285
|
emit(json, {
|
|
@@ -2283,6 +2313,16 @@ async function ensureConfig(api, opts) {
|
|
|
2283
2313
|
console.error(chalk3.red(`error: use --platform tvos or --platform androidtv for TV devices`));
|
|
2284
2314
|
process.exit(2);
|
|
2285
2315
|
}
|
|
2316
|
+
if (opts.model !== void 0 && opts.deviceCategory) {
|
|
2317
|
+
console.error(chalk3.red("error: pass --model or --device-category, not both"));
|
|
2318
|
+
process.exit(2);
|
|
2319
|
+
}
|
|
2320
|
+
if (opts.model !== void 0 && !opts.model.trim()) {
|
|
2321
|
+
console.error(
|
|
2322
|
+
chalk3.red("error: --model needs a model name (see `vibeview list-devices --models`)")
|
|
2323
|
+
);
|
|
2324
|
+
process.exit(2);
|
|
2325
|
+
}
|
|
2286
2326
|
let firstRunBuild = null;
|
|
2287
2327
|
let firstRunMetro = null;
|
|
2288
2328
|
let isFirstRun = false;
|
|
@@ -2325,9 +2365,10 @@ Run \`vibeview dev\` without --app to pick from the list.`
|
|
|
2325
2365
|
chalk3.yellow(`\u26A0 app "${choice.staleCached}" no longer exists \u2014 pick your debug build again`)
|
|
2326
2366
|
);
|
|
2327
2367
|
}
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2368
|
+
const buildConfig = firstRunBuild ?? entry?.build ?? null;
|
|
2369
|
+
if (!appId && buildConfig) {
|
|
2370
|
+
const strategy = buildAppStrategy(apps, Boolean(opts.build), opts.json, process.stdin.isTTY);
|
|
2371
|
+
let action = strategy === "ask" ? "pick" : strategy;
|
|
2331
2372
|
if (strategy === "ask") {
|
|
2332
2373
|
action = await select3({
|
|
2333
2374
|
message: "Run against which build?",
|
|
@@ -2343,7 +2384,7 @@ Run \`vibeview dev\` without --app to pick from the list.`
|
|
|
2343
2384
|
const result = await runBuildForPlatform(
|
|
2344
2385
|
process.cwd(),
|
|
2345
2386
|
platform,
|
|
2346
|
-
|
|
2387
|
+
buildConfig,
|
|
2347
2388
|
opts,
|
|
2348
2389
|
(l) => console.error(chalk3.dim(l))
|
|
2349
2390
|
);
|
|
@@ -2352,6 +2393,7 @@ Run \`vibeview dev\` without --app to pick from the list.`
|
|
|
2352
2393
|
} catch (err) {
|
|
2353
2394
|
const message = err.message;
|
|
2354
2395
|
console.error(chalk3.red(`error: ${message}`));
|
|
2396
|
+
emit(opts.json, { event: "error", message });
|
|
2355
2397
|
process.exit(1);
|
|
2356
2398
|
}
|
|
2357
2399
|
}
|
|
@@ -2415,6 +2457,7 @@ Run \`vibeview dev\` without --app to pick from the list.`
|
|
|
2415
2457
|
platform,
|
|
2416
2458
|
appId,
|
|
2417
2459
|
deviceCategory,
|
|
2460
|
+
model: opts.model,
|
|
2418
2461
|
metroPort: finalMetro.port,
|
|
2419
2462
|
metroCommand: finalMetro.command,
|
|
2420
2463
|
platformHasMetroOverride: finalMetro.platformHasOverride,
|
|
@@ -2558,6 +2601,7 @@ async function runDevInProcess(opts) {
|
|
|
2558
2601
|
platform: cfg.platform,
|
|
2559
2602
|
appId: cfg.appId,
|
|
2560
2603
|
deviceCategory: cfg.deviceCategory,
|
|
2604
|
+
model: cfg.model,
|
|
2561
2605
|
metroPort: cfg.metroPort ?? 8081,
|
|
2562
2606
|
// startDevSession's own browser-open happens before we've written the
|
|
2563
2607
|
// state file / emitted session_ready — open it ourselves afterward
|
|
@@ -2583,6 +2627,10 @@ async function runDevInProcess(opts) {
|
|
|
2583
2627
|
} else if (evt.event === "tunnel_connected") {
|
|
2584
2628
|
everConnected = true;
|
|
2585
2629
|
console.error(chalk3.green("\u2713 tunnel connected \u2014 edit away, Fast Refresh is live"));
|
|
2630
|
+
} else if (evt.event === "installing") {
|
|
2631
|
+
console.error(
|
|
2632
|
+
chalk3.dim(" Putting the channel on the Roku \u2014 the link appears once it is up.")
|
|
2633
|
+
);
|
|
2586
2634
|
} else if (evt.event === "session_ended" && !stopping && handle) {
|
|
2587
2635
|
console.error(chalk3.red("tunnel closed by server \u2014 session ended"));
|
|
2588
2636
|
stopping = true;
|
|
@@ -2692,7 +2740,7 @@ async function runDevInProcess(opts) {
|
|
|
2692
2740
|
if (stopping) return;
|
|
2693
2741
|
let info = null;
|
|
2694
2742
|
try {
|
|
2695
|
-
info = await api.
|
|
2743
|
+
info = await api.touchSession(sessionId);
|
|
2696
2744
|
} catch {
|
|
2697
2745
|
}
|
|
2698
2746
|
const verdict = sessionLivenessVerdict(info);
|
|
@@ -2801,6 +2849,55 @@ async function dev(opts) {
|
|
|
2801
2849
|
if (process.exitCode === 1) return;
|
|
2802
2850
|
}
|
|
2803
2851
|
|
|
2852
|
+
// src/lib/formatters/devices.ts
|
|
2853
|
+
import chalk4 from "chalk";
|
|
2854
|
+
var HEADERS = ["DEVICE_ID", "NAME", "TYPE", "CATEGORY", "OS", "STATUS"];
|
|
2855
|
+
function formatDevicesHuman(devices) {
|
|
2856
|
+
if (devices.length === 0) {
|
|
2857
|
+
return "No devices registered.\n";
|
|
2858
|
+
}
|
|
2859
|
+
const rows = devices.map((d) => [
|
|
2860
|
+
d.device_id,
|
|
2861
|
+
d.name,
|
|
2862
|
+
d.type,
|
|
2863
|
+
d.category,
|
|
2864
|
+
d.os_version,
|
|
2865
|
+
d.status
|
|
2866
|
+
]);
|
|
2867
|
+
const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2868
|
+
const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
|
|
2869
|
+
const lines = [chalk4.bold(fmt(HEADERS))];
|
|
2870
|
+
for (const row of rows.sort(rowSort)) {
|
|
2871
|
+
const statusColored = row[5] === "available" ? chalk4.green(row[5]) : row[5] === "busy" ? chalk4.yellow(row[5]) : chalk4.dim(row[5]);
|
|
2872
|
+
lines.push(fmt([row[0], row[1], row[2], row[3], row[4], statusColored]));
|
|
2873
|
+
}
|
|
2874
|
+
return lines.join("\n") + "\n";
|
|
2875
|
+
}
|
|
2876
|
+
function rowSort(a, b) {
|
|
2877
|
+
return a[2].localeCompare(b[2]) || a[3].localeCompare(b[3]) || a[0].localeCompare(b[0]);
|
|
2878
|
+
}
|
|
2879
|
+
function filterDeviceModels(models, opts) {
|
|
2880
|
+
return models.filter((m) => !opts.type || m.device_type === opts.type).filter((m) => !opts.category || m.category === opts.category).map((m) => ({
|
|
2881
|
+
model: m.model,
|
|
2882
|
+
os_version: m.os_version,
|
|
2883
|
+
device_type: m.device_type,
|
|
2884
|
+
category: m.category,
|
|
2885
|
+
form_factor: m.form_factor ?? null
|
|
2886
|
+
}));
|
|
2887
|
+
}
|
|
2888
|
+
var MODEL_HEADERS = ["MODEL", "OS", "TYPE", "CATEGORY", "FORM FACTOR"];
|
|
2889
|
+
function formatDeviceModelsHuman(models) {
|
|
2890
|
+
if (models.length === 0) {
|
|
2891
|
+
return "No device models available right now.\n";
|
|
2892
|
+
}
|
|
2893
|
+
const rows = models.map((m) => [m.model, m.os_version, m.device_type, m.category, m.form_factor ?? "-"]).sort(
|
|
2894
|
+
(a, b) => a[2].localeCompare(b[2]) || a[3].localeCompare(b[3]) || a[0].localeCompare(b[0]) || a[1].localeCompare(b[1])
|
|
2895
|
+
);
|
|
2896
|
+
const widths = MODEL_HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2897
|
+
const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
|
|
2898
|
+
return [chalk4.bold(fmt(MODEL_HEADERS)), ...rows.map(fmt)].join("\n") + "\n";
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2804
2901
|
// src/agent/registry.ts
|
|
2805
2902
|
var AGENT_COMMANDS = [
|
|
2806
2903
|
{
|
|
@@ -3116,6 +3213,52 @@ var AGENT_COMMANDS = [
|
|
|
3116
3213
|
}
|
|
3117
3214
|
]
|
|
3118
3215
|
},
|
|
3216
|
+
{
|
|
3217
|
+
// set_posture: { posture: 'closed' | 'partial' | 'open' } OR { angle: 0..180 }
|
|
3218
|
+
// — exactly one (buildToolCall enforces it CLI-side, agent_control.py's
|
|
3219
|
+
// validate_tool_args at the gate). The angle is a settle-once fold, the
|
|
3220
|
+
// same as the dashboard slider's release, not its live drag stream.
|
|
3221
|
+
// Foldable devices only.
|
|
3222
|
+
verb: "set-posture",
|
|
3223
|
+
tool: "set_posture",
|
|
3224
|
+
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.",
|
|
3225
|
+
mutating: true,
|
|
3226
|
+
params: [
|
|
3227
|
+
{
|
|
3228
|
+
name: "posture",
|
|
3229
|
+
cli: "positional",
|
|
3230
|
+
type: "string",
|
|
3231
|
+
required: false,
|
|
3232
|
+
description: "One of 'closed', 'partial', 'open'. Omit when passing an angle."
|
|
3233
|
+
},
|
|
3234
|
+
{
|
|
3235
|
+
name: "angle",
|
|
3236
|
+
cli: "--angle",
|
|
3237
|
+
type: "number",
|
|
3238
|
+
required: false,
|
|
3239
|
+
description: "Exact hinge angle in whole degrees, 0 (shut) to 180 (flat open). Omit when passing a posture."
|
|
3240
|
+
}
|
|
3241
|
+
]
|
|
3242
|
+
},
|
|
3243
|
+
{
|
|
3244
|
+
// rotate: { degrees?: 90 | 180 | 270 } (default 90). The same control as
|
|
3245
|
+
// the dashboard's Rotate button. A foldable turns its body by quarter
|
|
3246
|
+
// turns (90 clockwise, 270 back, 180 a half turn); a rigid phone toggles
|
|
3247
|
+
// portrait <-> landscape and accepts only 90. Refused on TV devices.
|
|
3248
|
+
verb: "rotate",
|
|
3249
|
+
tool: "rotate",
|
|
3250
|
+
summary: "Rotate the device. A phone toggles between portrait and landscape; a foldable (iPhone Duo) turns a quarter clockwise, or by --degrees 180/270. Reports the new orientation. Not available on TV devices.",
|
|
3251
|
+
mutating: true,
|
|
3252
|
+
params: [
|
|
3253
|
+
{
|
|
3254
|
+
name: "degrees",
|
|
3255
|
+
cli: "--degrees",
|
|
3256
|
+
type: "number",
|
|
3257
|
+
required: false,
|
|
3258
|
+
description: "Foldable only: 90 (default, a quarter turn clockwise), 180, or 270 (a quarter turn back). A phone accepts only 90."
|
|
3259
|
+
}
|
|
3260
|
+
]
|
|
3261
|
+
},
|
|
3119
3262
|
{
|
|
3120
3263
|
// tools.py wait: { seconds?: number } (default: 2). Note the wire arg is
|
|
3121
3264
|
// "seconds", NOT "ms" — tool_dispatch.py reads args.get("seconds", 2)
|
|
@@ -3207,18 +3350,18 @@ var AGENT_COMMANDS = [
|
|
|
3207
3350
|
];
|
|
3208
3351
|
|
|
3209
3352
|
// src/agent/render.ts
|
|
3210
|
-
import
|
|
3353
|
+
import chalk5 from "chalk";
|
|
3211
3354
|
function renderEnvelope(env) {
|
|
3212
3355
|
const lines = [];
|
|
3213
3356
|
if (env.ok) {
|
|
3214
|
-
lines.push(
|
|
3357
|
+
lines.push(chalk5.green(`ok`) + chalk5.dim(` (${env.tool})`));
|
|
3215
3358
|
} else {
|
|
3216
|
-
lines.push(
|
|
3359
|
+
lines.push(chalk5.red(`error`) + chalk5.dim(` (${env.tool})`));
|
|
3217
3360
|
}
|
|
3218
3361
|
lines.push(...resultLines(env.result));
|
|
3219
3362
|
if (env.observation) {
|
|
3220
3363
|
if (env.observation.kind === "unavailable") {
|
|
3221
|
-
lines.push(
|
|
3364
|
+
lines.push(chalk5.yellow(`warning: ${env.observation.text}`));
|
|
3222
3365
|
} else {
|
|
3223
3366
|
lines.push(env.observation.text);
|
|
3224
3367
|
}
|
|
@@ -3231,17 +3374,17 @@ function resultLines(result) {
|
|
|
3231
3374
|
const lines = [];
|
|
3232
3375
|
if (Array.isArray(r.entries) && typeof r.cursor === "number") {
|
|
3233
3376
|
if (r.entries.length === 0) {
|
|
3234
|
-
lines.push(
|
|
3377
|
+
lines.push(chalk5.dim("(no log lines)"));
|
|
3235
3378
|
}
|
|
3236
3379
|
for (const e of r.entries) {
|
|
3237
3380
|
if (!e || typeof e !== "object") continue;
|
|
3238
3381
|
const entry = e;
|
|
3239
3382
|
const ts = typeof entry.timestamp === "number" ? new Date(entry.timestamp).toISOString().slice(11, 23) : "??:??:??.???";
|
|
3240
3383
|
const level = typeof entry.level === "string" ? entry.level : "info";
|
|
3241
|
-
const levelTag = level === "error" ?
|
|
3242
|
-
lines.push(`${
|
|
3384
|
+
const levelTag = level === "error" ? chalk5.red(level) : level === "warn" ? chalk5.yellow(level) : chalk5.dim(level);
|
|
3385
|
+
lines.push(`${chalk5.dim(ts)} ${levelTag} ${String(entry.message ?? "")}`);
|
|
3243
3386
|
}
|
|
3244
|
-
lines.push(
|
|
3387
|
+
lines.push(chalk5.dim(`cursor: ${r.cursor}`));
|
|
3245
3388
|
}
|
|
3246
3389
|
if (r.found === true && typeof r.ref === "string" && r.ref) {
|
|
3247
3390
|
let found = `found: @${r.ref}`;
|
|
@@ -3253,14 +3396,43 @@ function resultLines(result) {
|
|
|
3253
3396
|
const text = typeof a.text === "string" && a.text ? ` ${JSON.stringify(a.text)}` : "";
|
|
3254
3397
|
return `${ref}${text}`;
|
|
3255
3398
|
});
|
|
3256
|
-
if (alts.length > 0) lines.push(
|
|
3399
|
+
if (alts.length > 0) lines.push(chalk5.dim(`alternatives: ${alts.join(", ")}`));
|
|
3257
3400
|
}
|
|
3258
3401
|
}
|
|
3259
|
-
if (
|
|
3402
|
+
if (r.action === "posture_set" || r.action === "rotated") {
|
|
3403
|
+
const state = deviceStateLine(r);
|
|
3404
|
+
if (state) lines.push(state);
|
|
3405
|
+
}
|
|
3406
|
+
if (typeof r.note === "string" && r.note) lines.push(chalk5.dim(r.note));
|
|
3260
3407
|
if (typeof r.message === "string" && r.message) lines.push(r.message);
|
|
3261
3408
|
if (typeof r.warning === "string" && r.warning) lines.push(r.warning);
|
|
3262
3409
|
return lines;
|
|
3263
3410
|
}
|
|
3411
|
+
function deviceStateLine(r) {
|
|
3412
|
+
const parts = [];
|
|
3413
|
+
if (r.action === "rotated") {
|
|
3414
|
+
if (typeof r.device_orientation === "string") {
|
|
3415
|
+
let o = `orientation: ${r.device_orientation}`;
|
|
3416
|
+
if (typeof r.screen_orientation === "string" && r.screen_orientation !== r.device_orientation) {
|
|
3417
|
+
o += ` (screen shows ${r.screen_orientation})`;
|
|
3418
|
+
}
|
|
3419
|
+
parts.push(o);
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
if (typeof r.posture === "string") {
|
|
3423
|
+
let p = `posture: ${r.posture}`;
|
|
3424
|
+
if (typeof r.angle === "number") p += ` at ${r.angle}\xB0`;
|
|
3425
|
+
const extras = [];
|
|
3426
|
+
if (typeof r.screen === "string") extras.push(`${r.screen} screen`);
|
|
3427
|
+
const d = r.display;
|
|
3428
|
+
if (d && typeof d.width === "number" && typeof d.height === "number") {
|
|
3429
|
+
extras.push(`${d.width}x${d.height}`);
|
|
3430
|
+
}
|
|
3431
|
+
if (extras.length) p += ` (${extras.join(", ")})`;
|
|
3432
|
+
parts.push(p);
|
|
3433
|
+
}
|
|
3434
|
+
return parts.length ? parts.join("; ") : null;
|
|
3435
|
+
}
|
|
3264
3436
|
function agentExitCode(env) {
|
|
3265
3437
|
return env.ok ? 0 : 1;
|
|
3266
3438
|
}
|
|
@@ -3382,6 +3554,15 @@ function buildToolCall(cmd, positionals, opts) {
|
|
|
3382
3554
|
}
|
|
3383
3555
|
return { tool: cmd.tool, args: args2 };
|
|
3384
3556
|
}
|
|
3557
|
+
if (cmd.verb === "set-posture") {
|
|
3558
|
+
const hasPosture = positionals[0] !== void 0 && positionals[0] !== "";
|
|
3559
|
+
const hasAngle = opts.angle !== void 0 && opts.angle !== "";
|
|
3560
|
+
if (hasPosture === hasAngle) {
|
|
3561
|
+
throw new FlagError(
|
|
3562
|
+
hasPosture ? "set-posture takes a posture or --angle, not both" : "set-posture requires a posture (closed, partial, open) or --angle <0-180>"
|
|
3563
|
+
);
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3385
3566
|
const args = {};
|
|
3386
3567
|
let positionalIndex = 0;
|
|
3387
3568
|
for (const param of cmd.params) {
|
|
@@ -3488,6 +3669,7 @@ async function runAgentVerb(cmd, positionals, opts) {
|
|
|
3488
3669
|
export {
|
|
3489
3670
|
readDevState,
|
|
3490
3671
|
clearDevState,
|
|
3672
|
+
DEV_PLATFORMS,
|
|
3491
3673
|
PLATFORM_NEEDS_METRO,
|
|
3492
3674
|
isDevPlatform,
|
|
3493
3675
|
loadDevConfig,
|
|
@@ -3501,9 +3683,14 @@ export {
|
|
|
3501
3683
|
newTransitionState,
|
|
3502
3684
|
printTransition,
|
|
3503
3685
|
printOutcomeLine,
|
|
3686
|
+
createRokuPush,
|
|
3504
3687
|
startDevSession,
|
|
3688
|
+
ROKU_LIVENESS_INTERVAL_MS,
|
|
3505
3689
|
PLATFORM_DEVICE,
|
|
3506
3690
|
dev,
|
|
3691
|
+
formatDevicesHuman,
|
|
3692
|
+
filterDeviceModels,
|
|
3693
|
+
formatDeviceModelsHuman,
|
|
3507
3694
|
AGENT_COMMANDS,
|
|
3508
3695
|
renderEnvelope,
|
|
3509
3696
|
buildToolCall,
|
|
@@ -238,6 +238,10 @@ var ApiClient = class {
|
|
|
238
238
|
}
|
|
239
239
|
return all;
|
|
240
240
|
}
|
|
241
|
+
/** The startable device models (see `DeviceModelPool`). */
|
|
242
|
+
async listDeviceModels() {
|
|
243
|
+
return this.requestRaw("GET", "/devices/pools");
|
|
244
|
+
}
|
|
241
245
|
async getDevice(deviceId) {
|
|
242
246
|
return this.requestRaw("GET", `/devices/${deviceId}`);
|
|
243
247
|
}
|
|
@@ -250,6 +254,13 @@ var ApiClient = class {
|
|
|
250
254
|
async getSession(sessionId) {
|
|
251
255
|
return this.requestRaw("GET", `/sessions/${sessionId}`);
|
|
252
256
|
}
|
|
257
|
+
/** Report presence on a session and read it back in one call. A Roku dev
|
|
258
|
+
* loop has no tunnel and no browser, so this is what tells the backend
|
|
259
|
+
* someone is still using the session between device commands. Same
|
|
260
|
+
* response shape as `getSession`. */
|
|
261
|
+
async touchSession(sessionId) {
|
|
262
|
+
return this.requestRaw("POST", `/sessions/${sessionId}/presence`);
|
|
263
|
+
}
|
|
253
264
|
async stopSession(sessionId) {
|
|
254
265
|
await this.requestRaw("DELETE", `/sessions/${sessionId}`);
|
|
255
266
|
}
|