vibeview 0.22.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-SHS5PCSS.js → chunk-E2LZSLRF.js} +178 -13
- package/dist/{chunk-UGCGJ6IN.js → chunk-PSFXVUHG.js} +4 -0
- package/dist/cli.js +56 -58
- package/dist/{server-6IJ44HAX.js → server-XMSBLIWY.js} +43 -5
- package/dist/{upload-app-LJH3PETS.js → upload-app-ZAKAOJKE.js} +1 -1
- package/package.json +1 -1
- package/skills/vibeview-agent/SKILL.md +25 -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";
|
|
@@ -2060,7 +2060,10 @@ async function startDevSession(cfg, onEvent = () => {
|
|
|
2060
2060
|
}, onTunnelState) {
|
|
2061
2061
|
const api = new ApiClient(cfg.baseUrl, cfg.token);
|
|
2062
2062
|
const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform];
|
|
2063
|
-
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
|
+
);
|
|
2064
2067
|
const sessionId = created.session_id;
|
|
2065
2068
|
let stopping = false;
|
|
2066
2069
|
let tunnel = null;
|
|
@@ -2154,9 +2157,23 @@ function devSessionCreateBody(cfg) {
|
|
|
2154
2157
|
device_category: cfg.deviceCategory,
|
|
2155
2158
|
app_id: cfg.appId,
|
|
2156
2159
|
// Roku has no JS bundle to serve, so there is nothing to tunnel.
|
|
2157
|
-
tunnel: PLATFORM_NEEDS_METRO[cfg.platform]
|
|
2160
|
+
tunnel: PLATFORM_NEEDS_METRO[cfg.platform],
|
|
2161
|
+
...cfg.model ? { model: cfg.model } : {}
|
|
2158
2162
|
};
|
|
2159
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
|
+
}
|
|
2160
2177
|
var BLOCKING_WARNING_PREFIXES = ["No debug build", "App install failed", "App not found"];
|
|
2161
2178
|
function findBlockingWarning(info) {
|
|
2162
2179
|
return (info.warnings ?? []).find((w) => BLOCKING_WARNING_PREFIXES.some((p) => w.includes(p)));
|
|
@@ -2296,6 +2313,16 @@ async function ensureConfig(api, opts) {
|
|
|
2296
2313
|
console.error(chalk3.red(`error: use --platform tvos or --platform androidtv for TV devices`));
|
|
2297
2314
|
process.exit(2);
|
|
2298
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
|
+
}
|
|
2299
2326
|
let firstRunBuild = null;
|
|
2300
2327
|
let firstRunMetro = null;
|
|
2301
2328
|
let isFirstRun = false;
|
|
@@ -2430,6 +2457,7 @@ Run \`vibeview dev\` without --app to pick from the list.`
|
|
|
2430
2457
|
platform,
|
|
2431
2458
|
appId,
|
|
2432
2459
|
deviceCategory,
|
|
2460
|
+
model: opts.model,
|
|
2433
2461
|
metroPort: finalMetro.port,
|
|
2434
2462
|
metroCommand: finalMetro.command,
|
|
2435
2463
|
platformHasMetroOverride: finalMetro.platformHasOverride,
|
|
@@ -2573,6 +2601,7 @@ async function runDevInProcess(opts) {
|
|
|
2573
2601
|
platform: cfg.platform,
|
|
2574
2602
|
appId: cfg.appId,
|
|
2575
2603
|
deviceCategory: cfg.deviceCategory,
|
|
2604
|
+
model: cfg.model,
|
|
2576
2605
|
metroPort: cfg.metroPort ?? 8081,
|
|
2577
2606
|
// startDevSession's own browser-open happens before we've written the
|
|
2578
2607
|
// state file / emitted session_ready — open it ourselves afterward
|
|
@@ -2820,6 +2849,55 @@ async function dev(opts) {
|
|
|
2820
2849
|
if (process.exitCode === 1) return;
|
|
2821
2850
|
}
|
|
2822
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
|
+
|
|
2823
2901
|
// src/agent/registry.ts
|
|
2824
2902
|
var AGENT_COMMANDS = [
|
|
2825
2903
|
{
|
|
@@ -3135,6 +3213,52 @@ var AGENT_COMMANDS = [
|
|
|
3135
3213
|
}
|
|
3136
3214
|
]
|
|
3137
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
|
+
},
|
|
3138
3262
|
{
|
|
3139
3263
|
// tools.py wait: { seconds?: number } (default: 2). Note the wire arg is
|
|
3140
3264
|
// "seconds", NOT "ms" — tool_dispatch.py reads args.get("seconds", 2)
|
|
@@ -3226,18 +3350,18 @@ var AGENT_COMMANDS = [
|
|
|
3226
3350
|
];
|
|
3227
3351
|
|
|
3228
3352
|
// src/agent/render.ts
|
|
3229
|
-
import
|
|
3353
|
+
import chalk5 from "chalk";
|
|
3230
3354
|
function renderEnvelope(env) {
|
|
3231
3355
|
const lines = [];
|
|
3232
3356
|
if (env.ok) {
|
|
3233
|
-
lines.push(
|
|
3357
|
+
lines.push(chalk5.green(`ok`) + chalk5.dim(` (${env.tool})`));
|
|
3234
3358
|
} else {
|
|
3235
|
-
lines.push(
|
|
3359
|
+
lines.push(chalk5.red(`error`) + chalk5.dim(` (${env.tool})`));
|
|
3236
3360
|
}
|
|
3237
3361
|
lines.push(...resultLines(env.result));
|
|
3238
3362
|
if (env.observation) {
|
|
3239
3363
|
if (env.observation.kind === "unavailable") {
|
|
3240
|
-
lines.push(
|
|
3364
|
+
lines.push(chalk5.yellow(`warning: ${env.observation.text}`));
|
|
3241
3365
|
} else {
|
|
3242
3366
|
lines.push(env.observation.text);
|
|
3243
3367
|
}
|
|
@@ -3250,17 +3374,17 @@ function resultLines(result) {
|
|
|
3250
3374
|
const lines = [];
|
|
3251
3375
|
if (Array.isArray(r.entries) && typeof r.cursor === "number") {
|
|
3252
3376
|
if (r.entries.length === 0) {
|
|
3253
|
-
lines.push(
|
|
3377
|
+
lines.push(chalk5.dim("(no log lines)"));
|
|
3254
3378
|
}
|
|
3255
3379
|
for (const e of r.entries) {
|
|
3256
3380
|
if (!e || typeof e !== "object") continue;
|
|
3257
3381
|
const entry = e;
|
|
3258
3382
|
const ts = typeof entry.timestamp === "number" ? new Date(entry.timestamp).toISOString().slice(11, 23) : "??:??:??.???";
|
|
3259
3383
|
const level = typeof entry.level === "string" ? entry.level : "info";
|
|
3260
|
-
const levelTag = level === "error" ?
|
|
3261
|
-
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 ?? "")}`);
|
|
3262
3386
|
}
|
|
3263
|
-
lines.push(
|
|
3387
|
+
lines.push(chalk5.dim(`cursor: ${r.cursor}`));
|
|
3264
3388
|
}
|
|
3265
3389
|
if (r.found === true && typeof r.ref === "string" && r.ref) {
|
|
3266
3390
|
let found = `found: @${r.ref}`;
|
|
@@ -3272,14 +3396,43 @@ function resultLines(result) {
|
|
|
3272
3396
|
const text = typeof a.text === "string" && a.text ? ` ${JSON.stringify(a.text)}` : "";
|
|
3273
3397
|
return `${ref}${text}`;
|
|
3274
3398
|
});
|
|
3275
|
-
if (alts.length > 0) lines.push(
|
|
3399
|
+
if (alts.length > 0) lines.push(chalk5.dim(`alternatives: ${alts.join(", ")}`));
|
|
3276
3400
|
}
|
|
3277
3401
|
}
|
|
3278
|
-
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));
|
|
3279
3407
|
if (typeof r.message === "string" && r.message) lines.push(r.message);
|
|
3280
3408
|
if (typeof r.warning === "string" && r.warning) lines.push(r.warning);
|
|
3281
3409
|
return lines;
|
|
3282
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
|
+
}
|
|
3283
3436
|
function agentExitCode(env) {
|
|
3284
3437
|
return env.ok ? 0 : 1;
|
|
3285
3438
|
}
|
|
@@ -3401,6 +3554,15 @@ function buildToolCall(cmd, positionals, opts) {
|
|
|
3401
3554
|
}
|
|
3402
3555
|
return { tool: cmd.tool, args: args2 };
|
|
3403
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
|
+
}
|
|
3404
3566
|
const args = {};
|
|
3405
3567
|
let positionalIndex = 0;
|
|
3406
3568
|
for (const param of cmd.params) {
|
|
@@ -3526,6 +3688,9 @@ export {
|
|
|
3526
3688
|
ROKU_LIVENESS_INTERVAL_MS,
|
|
3527
3689
|
PLATFORM_DEVICE,
|
|
3528
3690
|
dev,
|
|
3691
|
+
formatDevicesHuman,
|
|
3692
|
+
filterDeviceModels,
|
|
3693
|
+
formatDeviceModelsHuman,
|
|
3529
3694
|
AGENT_COMMANDS,
|
|
3530
3695
|
renderEnvelope,
|
|
3531
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
|
}
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
createRokuPush,
|
|
8
8
|
dev,
|
|
9
9
|
fetchFinalSummary,
|
|
10
|
+
filterDeviceModels,
|
|
11
|
+
formatDeviceModelsHuman,
|
|
12
|
+
formatDevicesHuman,
|
|
10
13
|
formatDuration,
|
|
11
14
|
killOwnedMetro,
|
|
12
15
|
loadDevConfig,
|
|
@@ -16,7 +19,7 @@ import {
|
|
|
16
19
|
readDevState,
|
|
17
20
|
readMetroState,
|
|
18
21
|
runAgentVerb
|
|
19
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-E2LZSLRF.js";
|
|
20
23
|
import {
|
|
21
24
|
ApiClient,
|
|
22
25
|
EXIT_INFRA_ERROR,
|
|
@@ -29,7 +32,7 @@ import {
|
|
|
29
32
|
resolveAuth,
|
|
30
33
|
saveConfig,
|
|
31
34
|
uploadApp
|
|
32
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-PSFXVUHG.js";
|
|
33
36
|
|
|
34
37
|
// src/cli.ts
|
|
35
38
|
import { createRequire } from "module";
|
|
@@ -1998,17 +2001,17 @@ function formatEnvVarsHuman(variables) {
|
|
|
1998
2001
|
if (variables.length === 0) {
|
|
1999
2002
|
return "No build variables found.\n";
|
|
2000
2003
|
}
|
|
2001
|
-
const
|
|
2004
|
+
const HEADERS3 = ["NAME", "VISIBILITY", "SCOPE", "HINT"];
|
|
2002
2005
|
const rows = variables.map((v) => [
|
|
2003
2006
|
v.name,
|
|
2004
2007
|
v.is_file ? `${v.visibility} (file)` : v.visibility,
|
|
2005
2008
|
v.app_public_id ?? "org-wide",
|
|
2006
2009
|
v.value_hint ?? ""
|
|
2007
2010
|
]);
|
|
2008
|
-
const widths =
|
|
2011
|
+
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2009
2012
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2010
2013
|
const join3 = (cells) => cells.join(" ").trimEnd();
|
|
2011
|
-
const lines = [chalk16.bold(join3(padCells(
|
|
2014
|
+
const lines = [chalk16.bold(join3(padCells(HEADERS3)))];
|
|
2012
2015
|
for (const row of rows) lines.push(join3(padCells(row)));
|
|
2013
2016
|
return lines.join("\n") + "\n";
|
|
2014
2017
|
}
|
|
@@ -2545,12 +2548,12 @@ function formatAscKeysHuman(keys) {
|
|
|
2545
2548
|
if (keys.length === 0) {
|
|
2546
2549
|
return "No App Store Connect keys found.\n";
|
|
2547
2550
|
}
|
|
2548
|
-
const
|
|
2551
|
+
const HEADERS3 = ["NAME", "KEY ID", "ISSUER ID", "CREATED"];
|
|
2549
2552
|
const rows = keys.map((k) => [k.name, k.key_id, k.issuer_id, k.created_at ?? ""]);
|
|
2550
|
-
const widths =
|
|
2553
|
+
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2551
2554
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2552
2555
|
const join3 = (cells) => cells.join(" ").trimEnd();
|
|
2553
|
-
const lines = [chalk17.bold(join3(padCells(
|
|
2556
|
+
const lines = [chalk17.bold(join3(padCells(HEADERS3)))];
|
|
2554
2557
|
for (const row of rows) lines.push(join3(padCells(row)));
|
|
2555
2558
|
return lines.join("\n") + "\n";
|
|
2556
2559
|
}
|
|
@@ -2621,12 +2624,12 @@ function formatPlayKeysHuman(keys) {
|
|
|
2621
2624
|
if (keys.length === 0) {
|
|
2622
2625
|
return "No Google Play keys found.\n";
|
|
2623
2626
|
}
|
|
2624
|
-
const
|
|
2627
|
+
const HEADERS3 = ["NAME", "CLIENT EMAIL", "CREATED"];
|
|
2625
2628
|
const rows = keys.map((k) => [k.name, k.client_email, k.created_at ?? ""]);
|
|
2626
|
-
const widths =
|
|
2629
|
+
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2627
2630
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2628
2631
|
const join3 = (cells) => cells.join(" ").trimEnd();
|
|
2629
|
-
const lines = [chalk17.bold(join3(padCells(
|
|
2632
|
+
const lines = [chalk17.bold(join3(padCells(HEADERS3)))];
|
|
2630
2633
|
for (const row of rows) lines.push(join3(padCells(row)));
|
|
2631
2634
|
return lines.join("\n") + "\n";
|
|
2632
2635
|
}
|
|
@@ -2695,7 +2698,7 @@ function formatCredentialsHuman(credentials2) {
|
|
|
2695
2698
|
if (credentials2.length === 0) {
|
|
2696
2699
|
return "No signing credentials found.\n";
|
|
2697
2700
|
}
|
|
2698
|
-
const
|
|
2701
|
+
const HEADERS3 = ["NAME", "PLATFORM", "SCOPE", "IDENTITY", "EXPIRES"];
|
|
2699
2702
|
const rows = credentials2.map((c) => [
|
|
2700
2703
|
c.name,
|
|
2701
2704
|
c.platform,
|
|
@@ -2703,10 +2706,10 @@ function formatCredentialsHuman(credentials2) {
|
|
|
2703
2706
|
formatIdentity(c),
|
|
2704
2707
|
formatExpires(c)
|
|
2705
2708
|
]);
|
|
2706
|
-
const widths =
|
|
2709
|
+
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2707
2710
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2708
2711
|
const join3 = (cells) => cells.join(" ").trimEnd();
|
|
2709
|
-
const lines = [chalk17.bold(join3(padCells(
|
|
2712
|
+
const lines = [chalk17.bold(join3(padCells(HEADERS3)))];
|
|
2710
2713
|
for (const row of rows) lines.push(join3(padCells(row)));
|
|
2711
2714
|
return lines.join("\n") + "\n";
|
|
2712
2715
|
}
|
|
@@ -2814,7 +2817,7 @@ function formatWebhooksHuman(webhooks2) {
|
|
|
2814
2817
|
if (webhooks2.length === 0) {
|
|
2815
2818
|
return "No webhooks found.\n";
|
|
2816
2819
|
}
|
|
2817
|
-
const
|
|
2820
|
+
const HEADERS3 = ["ID", "URL", "ACTIVE", "LAST DELIVERY", "CREATED"];
|
|
2818
2821
|
const rows = webhooks2.map((w) => [
|
|
2819
2822
|
w.id,
|
|
2820
2823
|
w.url,
|
|
@@ -2822,10 +2825,10 @@ function formatWebhooksHuman(webhooks2) {
|
|
|
2822
2825
|
formatLastDelivery(w.last_delivery),
|
|
2823
2826
|
w.created_at
|
|
2824
2827
|
]);
|
|
2825
|
-
const widths =
|
|
2828
|
+
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2826
2829
|
const padCells = (cells) => cells.map((c, i) => c.padEnd(widths[i]));
|
|
2827
2830
|
const join3 = (cells) => cells.join(" ").trimEnd();
|
|
2828
|
-
const lines = [chalk18.bold(join3(padCells(
|
|
2831
|
+
const lines = [chalk18.bold(join3(padCells(HEADERS3)))];
|
|
2829
2832
|
for (const row of rows) lines.push(join3(padCells(row)));
|
|
2830
2833
|
return lines.join("\n") + "\n";
|
|
2831
2834
|
}
|
|
@@ -2915,38 +2918,26 @@ function printError6(opts, err) {
|
|
|
2915
2918
|
console.error(chalk18.red(`error: ${message}`));
|
|
2916
2919
|
}
|
|
2917
2920
|
|
|
2918
|
-
// src/lib/formatters/devices.ts
|
|
2919
|
-
import chalk19 from "chalk";
|
|
2920
|
-
var HEADERS3 = ["DEVICE_ID", "NAME", "TYPE", "CATEGORY", "OS", "STATUS"];
|
|
2921
|
-
function formatDevicesHuman(devices) {
|
|
2922
|
-
if (devices.length === 0) {
|
|
2923
|
-
return "No devices registered.\n";
|
|
2924
|
-
}
|
|
2925
|
-
const rows = devices.map((d) => [
|
|
2926
|
-
d.device_id,
|
|
2927
|
-
d.name,
|
|
2928
|
-
d.type,
|
|
2929
|
-
d.category,
|
|
2930
|
-
d.os_version,
|
|
2931
|
-
d.status
|
|
2932
|
-
]);
|
|
2933
|
-
const widths = HEADERS3.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2934
|
-
const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
|
|
2935
|
-
const lines = [chalk19.bold(fmt(HEADERS3))];
|
|
2936
|
-
for (const row of rows.sort(rowSort)) {
|
|
2937
|
-
const statusColored = row[5] === "available" ? chalk19.green(row[5]) : row[5] === "busy" ? chalk19.yellow(row[5]) : chalk19.dim(row[5]);
|
|
2938
|
-
lines.push(fmt([row[0], row[1], row[2], row[3], row[4], statusColored]));
|
|
2939
|
-
}
|
|
2940
|
-
return lines.join("\n") + "\n";
|
|
2941
|
-
}
|
|
2942
|
-
function rowSort(a, b) {
|
|
2943
|
-
return a[2].localeCompare(b[2]) || a[3].localeCompare(b[3]) || a[0].localeCompare(b[0]);
|
|
2944
|
-
}
|
|
2945
|
-
|
|
2946
2921
|
// src/commands/list-devices.ts
|
|
2947
2922
|
async function listDevicesCommand(options) {
|
|
2948
2923
|
const { token, baseUrl } = resolveAuth(options);
|
|
2949
2924
|
const client = new ApiClient(baseUrl, token);
|
|
2925
|
+
if (options.models) {
|
|
2926
|
+
try {
|
|
2927
|
+
const models = filterDeviceModels(await client.listDeviceModels(), options);
|
|
2928
|
+
if (options.output === "json") {
|
|
2929
|
+
process.stdout.write(JSON.stringify(models, null, 2) + "\n");
|
|
2930
|
+
} else {
|
|
2931
|
+
process.stdout.write(formatDeviceModelsHuman(models));
|
|
2932
|
+
}
|
|
2933
|
+
process.exit(EXIT_PASS);
|
|
2934
|
+
} catch (err) {
|
|
2935
|
+
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
|
|
2936
|
+
`);
|
|
2937
|
+
process.exit(EXIT_INFRA_ERROR);
|
|
2938
|
+
}
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2950
2941
|
try {
|
|
2951
2942
|
const devices = await client.listDevices({
|
|
2952
2943
|
type: options.type,
|
|
@@ -3024,7 +3015,7 @@ import { dirname, join as join2 } from "path";
|
|
|
3024
3015
|
import { fileURLToPath } from "url";
|
|
3025
3016
|
import { spawnSync } from "child_process";
|
|
3026
3017
|
import { confirm } from "@inquirer/prompts";
|
|
3027
|
-
import
|
|
3018
|
+
import chalk19 from "chalk";
|
|
3028
3019
|
function resolveShippedSkillPath(fromDir) {
|
|
3029
3020
|
for (const rel of ["..", join2("..", "..")]) {
|
|
3030
3021
|
const candidate = join2(fromDir, rel, "skills", "vibeview-agent", "SKILL.md");
|
|
@@ -3052,23 +3043,23 @@ function registerMcp() {
|
|
|
3052
3043
|
encoding: "utf8"
|
|
3053
3044
|
});
|
|
3054
3045
|
if (res.status === 0) {
|
|
3055
|
-
console.log(
|
|
3046
|
+
console.log(chalk19.green("\u2713 MCP server registered with Claude Code (claude mcp add vibeview)"));
|
|
3056
3047
|
return;
|
|
3057
3048
|
}
|
|
3058
3049
|
const out = `${res.stderr ?? ""}${res.stdout ?? ""}`;
|
|
3059
3050
|
if (out.includes("already exists")) {
|
|
3060
|
-
console.log(
|
|
3051
|
+
console.log(chalk19.dim("MCP server already registered with Claude Code \u2014 nothing to do"));
|
|
3061
3052
|
return;
|
|
3062
3053
|
}
|
|
3063
|
-
console.error(
|
|
3064
|
-
console.error(
|
|
3054
|
+
console.error(chalk19.yellow(`\u26A0 could not register the MCP server: ${out.trim() || res.error}`));
|
|
3055
|
+
console.error(chalk19.yellow(" register it manually: claude mcp add vibeview -- vibeview mcp"));
|
|
3065
3056
|
}
|
|
3066
3057
|
async function agentSetup(opts) {
|
|
3067
3058
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
3068
3059
|
const shipped = resolveShippedSkillPath(moduleDir);
|
|
3069
3060
|
if (!shipped) {
|
|
3070
3061
|
console.error(
|
|
3071
|
-
|
|
3062
|
+
chalk19.red(
|
|
3072
3063
|
"error: could not find the packaged skill (skills/vibeview-agent/SKILL.md) \u2014 is this a complete install of the vibeview package?"
|
|
3073
3064
|
)
|
|
3074
3065
|
);
|
|
@@ -3077,9 +3068,9 @@ async function agentSetup(opts) {
|
|
|
3077
3068
|
}
|
|
3078
3069
|
const targetDir = skillInstallDir(opts.project ?? false, process.cwd(), homedir());
|
|
3079
3070
|
const result = installSkill(shipped, targetDir);
|
|
3080
|
-
console.log(
|
|
3071
|
+
console.log(chalk19.green(`\u2713 vibeview-agent skill ${result}: ${join2(targetDir, "SKILL.md")}`));
|
|
3081
3072
|
console.log(
|
|
3082
|
-
|
|
3073
|
+
chalk19.dim(
|
|
3083
3074
|
opts.project ? " Claude Code discovers it automatically for anyone working in this project." : " Claude Code discovers it automatically in every project on this machine."
|
|
3084
3075
|
)
|
|
3085
3076
|
);
|
|
@@ -3087,7 +3078,7 @@ async function agentSetup(opts) {
|
|
|
3087
3078
|
if (!claudeCliAvailable()) {
|
|
3088
3079
|
if (opts.mcp === true) {
|
|
3089
3080
|
console.error(
|
|
3090
|
-
|
|
3081
|
+
chalk19.yellow(
|
|
3091
3082
|
"\u26A0 claude CLI not found \u2014 install Claude Code, then run: claude mcp add vibeview -- vibeview mcp"
|
|
3092
3083
|
)
|
|
3093
3084
|
);
|
|
@@ -3101,7 +3092,7 @@ async function agentSetup(opts) {
|
|
|
3101
3092
|
}
|
|
3102
3093
|
console.log(
|
|
3103
3094
|
`
|
|
3104
|
-
Suggested line for your project's ${
|
|
3095
|
+
Suggested line for your project's ${chalk19.bold("CLAUDE.md")} (or ${chalk19.bold(
|
|
3105
3096
|
"AGENTS.md"
|
|
3106
3097
|
)} for other agents):
|
|
3107
3098
|
`
|
|
@@ -3109,7 +3100,7 @@ Suggested line for your project's ${chalk20.bold("CLAUDE.md")} (or ${chalk20.bol
|
|
|
3109
3100
|
console.log(` ${CLAUDE_MD_SUGGESTION}
|
|
3110
3101
|
`);
|
|
3111
3102
|
console.log(
|
|
3112
|
-
|
|
3103
|
+
chalk19.dim(
|
|
3113
3104
|
"Other agents (Cursor, Copilot, Codex, ...): install the same skill from GitHub with\n npx skills add vibeview/skills\nor point them at the skill file above, or add the MCP server per client \u2014\nsee https://vibeview.io/docs/agent-control"
|
|
3114
3105
|
)
|
|
3115
3106
|
);
|
|
@@ -3231,7 +3222,11 @@ program.command("list-suites").description("List available test suites").option(
|
|
|
3231
3222
|
[]
|
|
3232
3223
|
).option("--match <mode>", "When multiple --tag flags: any (default) or all", "any").action((opts) => listSuites(opts));
|
|
3233
3224
|
program.command("list-apps").description("List apps in your organization").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").action((opts) => listApps(opts));
|
|
3234
|
-
program.command("list-devices").description("List devices registered with the platform (filterable by type and category)").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--type <type>", "Filter by device type: ios, android, roku").option("--category <category>", "Filter by category: phone, tablet, tv").option("--status <status>", "Filter by status: available, busy, offline").
|
|
3225
|
+
program.command("list-devices").description("List devices registered with the platform (filterable by type and category)").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--type <type>", "Filter by device type: ios, android, roku").option("--category <category>", "Filter by category: phone, tablet, tv").option("--status <status>", "Filter by status: available, busy, offline").option(
|
|
3226
|
+
"--models",
|
|
3227
|
+
"List the device models you can start instead (the names `dev --model` takes)",
|
|
3228
|
+
false
|
|
3229
|
+
).action((opts) => listDevicesCommand(opts));
|
|
3235
3230
|
program.command("upload-app <file>").description(
|
|
3236
3231
|
"Upload an app build (zipped simulator .app for iOS/tvOS, .apk for Android/Android TV, or a Roku channel .zip)"
|
|
3237
3232
|
).option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--name <name>", "App name (auto-detected if not provided)").action((file, opts) => uploadApp(file, opts));
|
|
@@ -3247,6 +3242,9 @@ program.command("dev").description(
|
|
|
3247
3242
|
"--device-category <category>",
|
|
3248
3243
|
"phone or tablet (default: phone; TV platforms always use tv)",
|
|
3249
3244
|
parseDeviceCategory
|
|
3245
|
+
).option(
|
|
3246
|
+
"--model <name>",
|
|
3247
|
+
'Run on this exact device model, e.g. "iPhone Duo" (see `vibeview list-devices --models`)'
|
|
3250
3248
|
).option("--no-open", "Do not open the browser automatically").option("--debug", "Log raw tunnel lifecycle events (close codes, reconnects) for diagnostics").option("--json", "Emit line-delimited JSON events on stdout instead of human output", false).option(
|
|
3251
3249
|
"--detach",
|
|
3252
3250
|
"Run the dev session in the background; prints session info and exits (see `dev-status`, `dev-stop`, and `dev-reload` for Roku)",
|
|
@@ -3395,7 +3393,7 @@ program.command("dev-reload").description(
|
|
|
3395
3393
|
program.command("mcp").description(
|
|
3396
3394
|
"Run an MCP stdio server exposing the agent verbs (tap, swipe, type, ...) plus dev_start/dev_stop/dev_reload as tools"
|
|
3397
3395
|
).option("--token <token>", "API token (or set VIBEVIEW_API_TOKEN)").action(async (opts) => {
|
|
3398
|
-
const { runMcpServer } = await import("./server-
|
|
3396
|
+
const { runMcpServer } = await import("./server-XMSBLIWY.js");
|
|
3399
3397
|
await runMcpServer(opts);
|
|
3400
3398
|
});
|
|
3401
3399
|
program.command("agent-setup").description(
|
|
@@ -9,16 +9,18 @@ import {
|
|
|
9
9
|
checkMetro,
|
|
10
10
|
createRokuPush,
|
|
11
11
|
decodeScreenshotResult,
|
|
12
|
+
filterDeviceModels,
|
|
13
|
+
formatDeviceModelsHuman,
|
|
12
14
|
isDevPlatform,
|
|
13
15
|
loadDevConfig,
|
|
14
16
|
readDevState,
|
|
15
17
|
renderEnvelope,
|
|
16
18
|
startDevSession
|
|
17
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-E2LZSLRF.js";
|
|
18
20
|
import {
|
|
19
21
|
ApiClient,
|
|
20
22
|
resolveAuth
|
|
21
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-PSFXVUHG.js";
|
|
22
24
|
|
|
23
25
|
// src/mcp/server.ts
|
|
24
26
|
import { writeFileSync } from "fs";
|
|
@@ -78,10 +80,27 @@ var DEV_START_DEF = {
|
|
|
78
80
|
metro_port: {
|
|
79
81
|
type: "number",
|
|
80
82
|
description: "Local Metro port (default: 8081, or vibeview.json)."
|
|
83
|
+
},
|
|
84
|
+
model: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: 'Run on this exact device model, e.g. "iPhone Duo" for the foldable. Names come from list_device_models. Omit for a default phone (or TV on TV platforms).'
|
|
81
87
|
}
|
|
82
88
|
},
|
|
83
89
|
required: ["platform"]
|
|
84
90
|
};
|
|
91
|
+
var LIST_DEVICE_MODELS_DEF = {
|
|
92
|
+
name: "list_device_models",
|
|
93
|
+
description: 'List the device models you can start right now (model, OS version, platform, category, and form_factor "foldable" for a device that folds, like the iPhone Duo). Pass a model name to dev_start to run on it.',
|
|
94
|
+
mutating: false,
|
|
95
|
+
properties: {
|
|
96
|
+
device_type: {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: "Only models of this device type.",
|
|
99
|
+
enum: ["ios", "android", "roku"]
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
required: []
|
|
103
|
+
};
|
|
85
104
|
var DEV_STOP_DEF = {
|
|
86
105
|
name: "dev_stop",
|
|
87
106
|
description: "Stop the dev-loop session started by dev_start.",
|
|
@@ -118,7 +137,8 @@ function buildToolDefinitions() {
|
|
|
118
137
|
DEV_START_DEF,
|
|
119
138
|
DEV_STOP_DEF,
|
|
120
139
|
DEV_RELOAD_DEF,
|
|
121
|
-
UPLOAD_APP_DEF
|
|
140
|
+
UPLOAD_APP_DEF,
|
|
141
|
+
LIST_DEVICE_MODELS_DEF
|
|
122
142
|
];
|
|
123
143
|
}
|
|
124
144
|
function textResult(text, structuredContent, isError = false) {
|
|
@@ -143,6 +163,9 @@ async function handleToolCall(toolName, args, deps) {
|
|
|
143
163
|
if (toolName === "upload_app") {
|
|
144
164
|
return handleUploadApp(args, deps);
|
|
145
165
|
}
|
|
166
|
+
if (toolName === "list_device_models") {
|
|
167
|
+
return handleListDeviceModels(args, deps);
|
|
168
|
+
}
|
|
146
169
|
const cmd = findCommand(toolName);
|
|
147
170
|
if (!cmd) {
|
|
148
171
|
return errorResult(`Unknown tool: ${toolName}`);
|
|
@@ -226,7 +249,8 @@ async function handleDevStart(args, deps) {
|
|
|
226
249
|
const result = await deps.devSession.start({
|
|
227
250
|
platform,
|
|
228
251
|
app: typeof args.app === "string" ? args.app : void 0,
|
|
229
|
-
metroPort: typeof args.metro_port === "number" ? args.metro_port : void 0
|
|
252
|
+
metroPort: typeof args.metro_port === "number" ? args.metro_port : void 0,
|
|
253
|
+
model: typeof args.model === "string" && args.model.trim() ? args.model : void 0
|
|
230
254
|
});
|
|
231
255
|
const payload = { session_id: result.sessionId, page_url: result.pageUrl };
|
|
232
256
|
return textResult(`dev session started: ${payload.session_id}
|
|
@@ -235,6 +259,18 @@ ${payload.page_url}`, payload);
|
|
|
235
259
|
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
236
260
|
}
|
|
237
261
|
}
|
|
262
|
+
async function handleListDeviceModels(args, deps) {
|
|
263
|
+
const list = deps.listDeviceModels ?? (() => new ApiClient(deps.auth.baseUrl, deps.auth.token).listDeviceModels());
|
|
264
|
+
let models;
|
|
265
|
+
try {
|
|
266
|
+
models = filterDeviceModels(await list(), {
|
|
267
|
+
type: typeof args.device_type === "string" ? args.device_type : void 0
|
|
268
|
+
});
|
|
269
|
+
} catch (err) {
|
|
270
|
+
return errorResult(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
271
|
+
}
|
|
272
|
+
return textResult(formatDeviceModelsHuman(models).trimEnd(), { models });
|
|
273
|
+
}
|
|
238
274
|
async function handleDevReload(args, deps) {
|
|
239
275
|
const requested = typeof args.session_id === "string" ? args.session_id : void 0;
|
|
240
276
|
try {
|
|
@@ -265,7 +301,7 @@ async function handleUploadApp(args, deps) {
|
|
|
265
301
|
}
|
|
266
302
|
const name = typeof args.name === "string" && args.name ? args.name : void 0;
|
|
267
303
|
const upload = deps.uploadApp ?? (async (f, n) => {
|
|
268
|
-
const { uploadAppBuild } = await import("./upload-app-
|
|
304
|
+
const { uploadAppBuild } = await import("./upload-app-ZAKAOJKE.js");
|
|
269
305
|
const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
|
|
270
306
|
return result2;
|
|
271
307
|
});
|
|
@@ -348,6 +384,8 @@ function createDevSessionController(auth, webUrl) {
|
|
|
348
384
|
platform: cfg.platform,
|
|
349
385
|
appId,
|
|
350
386
|
deviceCategory: isTv ? "tv" : entry?.deviceCategory ?? "phone",
|
|
387
|
+
// A named model decides the category itself (startDevSession).
|
|
388
|
+
model: cfg.model,
|
|
351
389
|
metroPort,
|
|
352
390
|
open: false
|
|
353
391
|
});
|
package/package.json
CHANGED
|
@@ -156,6 +156,24 @@ it rather than letting it appear unexplained in their `git status`.
|
|
|
156
156
|
(`dpad_up`/`dpad_down`/`dpad_left`/`dpad_right`/`dpad_center`), or move
|
|
157
157
|
focus directly with `tap-focused` (moves focus AND activates) / `focus`
|
|
158
158
|
(moves focus only, no activation).
|
|
159
|
+
- **Foldable (iPhone Duo)**: start on it by model name —
|
|
160
|
+
`vibeview dev --detach --json --model "iPhone Duo"` (MCP: `dev_start` with
|
|
161
|
+
`model`). Exact names come from `vibeview list-devices --models` (MCP:
|
|
162
|
+
`list_device_models`); a foldable is marked `foldable`. It starts closed, on
|
|
163
|
+
the cover screen. Fold with a preset or an exact hinge angle, and rotate:
|
|
164
|
+
```bash
|
|
165
|
+
vibeview set-posture open --session <id> # closed | partial | open
|
|
166
|
+
vibeview set-posture --angle 75 --session <id> # 0 (shut) to 180 (flat)
|
|
167
|
+
vibeview rotate --session <id> # quarter turn clockwise
|
|
168
|
+
vibeview rotate --degrees 270 --session <id> # quarter turn back (180: half)
|
|
169
|
+
```
|
|
170
|
+
Each response says where the device ended up — posture, angle and which
|
|
171
|
+
screen is lit (`cover` or `inner`; the device decides — a small angle can
|
|
172
|
+
keep the inner screen lit on the way down), or the new orientation. Folding
|
|
173
|
+
switches screens of different sizes and rotating turns the picture, so run
|
|
174
|
+
`ui-tree` afterwards — old refs are stale. On other phones and tablets
|
|
175
|
+
`rotate` toggles portrait/landscape (only 90 is accepted); TV devices don't
|
|
176
|
+
rotate.
|
|
159
177
|
- **Roku channels (beta)**: start the session with
|
|
160
178
|
`vibeview dev --platform roku --detach` (or `dev_start` with platform
|
|
161
179
|
`roku` over MCP); after an edit run `vibeview dev-reload` (MCP:
|
|
@@ -220,6 +238,8 @@ human-readable text.
|
|
|
220
238
|
| `clear-text` | — | Clear the text in the currently focused input field. |
|
|
221
239
|
| `press` | `<button>` | Press a device button or perform a system gesture (home, back, d-pad, etc). |
|
|
222
240
|
| `open-url` | `<url>` | Open a deep link or URL in the app under test. |
|
|
241
|
+
| `set-posture` | `<closed\|partial\|open>` or `--angle <0-180>` | Fold or unfold a foldable device (iPhone Duo) to a preset or an exact hinge angle — exactly one. Reports posture, angle and lit screen. Errors on a device without a hinge. |
|
|
242
|
+
| `rotate` | `[--degrees <90\|180\|270>]` | Rotate the device and report the new orientation. Phones/tablets toggle portrait/landscape (90 only); a foldable turns a quarter clockwise by default. Not on TV. |
|
|
223
243
|
| `wait` | `[--ms <n>]` | Wait for a specified number of seconds before continuing (default: 2s). |
|
|
224
244
|
| `find` | `<text>` `[--below <text>] [--above <text>] [--near <text>]` | Find an element by text with optional spatial constraints. |
|
|
225
245
|
| `tap-focused` | `<ref>` | TV: move focus to an element and press SELECT in one step. |
|
|
@@ -229,7 +249,8 @@ Dev-loop lifecycle commands (not registry verbs, but needed for every run):
|
|
|
229
249
|
|
|
230
250
|
| Command | Purpose |
|
|
231
251
|
|---------|---------|
|
|
232
|
-
| `dev --detach --json` | Start a device + Metro-tunnel session in the background, emitting `session_ready`/`warning`/`error` JSON events on stdout. |
|
|
252
|
+
| `dev --detach --json` | Start a device + Metro-tunnel session in the background, emitting `session_ready`/`warning`/`error` JSON events on stdout. Add `--model "<name>"` to pick an exact device model. |
|
|
253
|
+
| `list-devices --models` | List the device models you can start (the names `--model` takes); foldables are marked `foldable`. |
|
|
233
254
|
| `dev-status` | Check on the current project's detached dev session. |
|
|
234
255
|
| `dev-stop` | End the current project's detached dev session. |
|
|
235
256
|
| `dev-reload` | Roku only: re-package, upload and restart the channel in the detached session (the `r` key of the foreground loop). |
|
|
@@ -244,13 +265,14 @@ hyphens replaced by underscores (`ui_tree`, `tap`, `scroll_to`, `long_press`,
|
|
|
244
265
|
`drag {from: "@e5", to: "300,400"}`, `alert {action: "get"}`) plus an
|
|
245
266
|
optional `session_id`.
|
|
246
267
|
|
|
247
|
-
|
|
268
|
+
These tools exist **only** over MCP, with no registry verb — they cover the
|
|
248
269
|
steps the CLI does with plain shell commands:
|
|
249
270
|
|
|
250
271
|
| MCP tool | Args | Purpose |
|
|
251
272
|
|----------|------|---------|
|
|
252
273
|
| `upload_app` | `file` (required path), `name` | Upload a debug build. 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`. |
|
|
253
|
-
| `dev_start` | `platform` (required: `ios`/`android`/`tvos`/`androidtv`), `app`, `metro_port` | Start a dev-loop session held open by the MCP server. The MCP equivalent of `vibeview dev --detach
|
|
274
|
+
| `dev_start` | `platform` (required: `ios`/`android`/`tvos`/`androidtv`/`roku`), `app`, `metro_port`, `model` | Start a dev-loop session held open by the MCP server. The MCP equivalent of `vibeview dev --detach`; `model` picks an exact device model (e.g. `"iPhone Duo"`). Returns `page_url` — relay it to the developer immediately, same as step 5 of the loop. |
|
|
275
|
+
| `list_device_models` | `device_type` | List the device models you can start (model, OS, platform, category, `foldable` marker). The MCP equivalent of `vibeview list-devices --models`. |
|
|
254
276
|
| `dev_stop` | — | Stop the session `dev_start` started. The MCP equivalent of `vibeview dev-stop`. |
|
|
255
277
|
|
|
256
278
|
**`dev_start` is stateful — it changes the default session for every later
|