vibeview 0.22.0 → 0.24.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-UGCGJ6IN.js → chunk-2WNJYNDW.js} +43 -6
- package/dist/{chunk-SHS5PCSS.js → chunk-MVNMGB6I.js} +199 -14
- package/dist/cli.js +57 -59
- package/dist/{server-6IJ44HAX.js → server-KIJCEN6T.js} +49 -5
- package/dist/{upload-app-LJH3PETS.js → upload-app-BXXKH65S.js} +3 -1
- package/package.json +1 -1
- package/skills/vibeview-agent/SKILL.md +25 -3
|
@@ -197,7 +197,7 @@ var ApiClient = class {
|
|
|
197
197
|
async listApps() {
|
|
198
198
|
return this.requestRaw("GET", "/apps/");
|
|
199
199
|
}
|
|
200
|
-
async uploadApp(filePath, name) {
|
|
200
|
+
async uploadApp(filePath, name, note) {
|
|
201
201
|
const fileBuffer = readFileSync2(filePath);
|
|
202
202
|
const blob = new Blob([fileBuffer]);
|
|
203
203
|
const formData = new FormData();
|
|
@@ -205,8 +205,15 @@ var ApiClient = class {
|
|
|
205
205
|
if (name) {
|
|
206
206
|
formData.append("name", name);
|
|
207
207
|
}
|
|
208
|
+
if (note) {
|
|
209
|
+
formData.append("note", note);
|
|
210
|
+
}
|
|
208
211
|
return this.requestFormData("POST", "/apps/upload", formData);
|
|
209
212
|
}
|
|
213
|
+
/** PATCH a build's note (null clears it). */
|
|
214
|
+
async updateBuildNote(appPublicId, buildId, note) {
|
|
215
|
+
return this.requestRaw("PATCH", `/apps/${appPublicId}/builds/${buildId}`, { note });
|
|
216
|
+
}
|
|
210
217
|
async uploadBuild(appPublicId, filePath, version) {
|
|
211
218
|
const fileBuffer = readFileSync2(filePath);
|
|
212
219
|
const blob = new Blob([fileBuffer]);
|
|
@@ -238,6 +245,10 @@ var ApiClient = class {
|
|
|
238
245
|
}
|
|
239
246
|
return all;
|
|
240
247
|
}
|
|
248
|
+
/** The startable device models (see `DeviceModelPool`). */
|
|
249
|
+
async listDeviceModels() {
|
|
250
|
+
return this.requestRaw("GET", "/devices/pools");
|
|
251
|
+
}
|
|
241
252
|
async getDevice(deviceId) {
|
|
242
253
|
return this.requestRaw("GET", `/devices/${deviceId}`);
|
|
243
254
|
}
|
|
@@ -609,9 +620,15 @@ function sanitizeDownloadFilename(raw) {
|
|
|
609
620
|
|
|
610
621
|
// src/lib/spinner.ts
|
|
611
622
|
import ora from "ora";
|
|
612
|
-
function createSpinner(text) {
|
|
623
|
+
function createSpinner(text, opts = {}) {
|
|
613
624
|
const isCI = process.env.CI === "true" || process.env.CI === "1" || !process.stdout.isTTY;
|
|
614
|
-
return ora({
|
|
625
|
+
return ora({
|
|
626
|
+
text,
|
|
627
|
+
isEnabled: !isCI,
|
|
628
|
+
stream: process.stderr,
|
|
629
|
+
discardStdin: false,
|
|
630
|
+
isSilent: opts.silent === true
|
|
631
|
+
});
|
|
615
632
|
}
|
|
616
633
|
|
|
617
634
|
// src/lib/types.ts
|
|
@@ -632,16 +649,26 @@ async function uploadAppBuild(filePath, options) {
|
|
|
632
649
|
}
|
|
633
650
|
const { token, baseUrl } = resolveAuth(options);
|
|
634
651
|
const client = new ApiClient(baseUrl, token);
|
|
635
|
-
const
|
|
652
|
+
const note = options.note?.trim() || void 0;
|
|
653
|
+
const result = await client.uploadApp(filePath, options.name, note);
|
|
654
|
+
if (note && result.deduplicated && result.build_id != null) {
|
|
655
|
+
await client.updateBuildNote(result.public_id, result.build_id, note);
|
|
656
|
+
}
|
|
636
657
|
return { result, baseUrl };
|
|
637
658
|
}
|
|
659
|
+
function describeBuild(result) {
|
|
660
|
+
if (result.build_id == null) return null;
|
|
661
|
+
const version = result.version ? `v${result.version}${result.build_number ? ` (${result.build_number})` : ""}` : "";
|
|
662
|
+
const dedup = result.deduplicated ? chalk.dim(" identical build already uploaded") : "";
|
|
663
|
+
return `Build ${chalk.cyan(String(result.build_id))}${version ? ` ${version}` : ""}${dedup}`;
|
|
664
|
+
}
|
|
638
665
|
async function uploadApp(filePath, options) {
|
|
639
666
|
if (!existsSync(filePath)) {
|
|
640
667
|
console.error(chalk.red(`Error: File not found: ${filePath}`));
|
|
641
668
|
process.exit(EXIT_INFRA_ERROR);
|
|
642
669
|
}
|
|
643
670
|
const { baseUrl } = resolveAuth(options);
|
|
644
|
-
const spinner = createSpinner("Uploading app...");
|
|
671
|
+
const spinner = createSpinner("Uploading app...", { silent: options.output === "json" });
|
|
645
672
|
spinner.start();
|
|
646
673
|
try {
|
|
647
674
|
const { result } = await uploadAppBuild(filePath, options);
|
|
@@ -658,6 +685,11 @@ async function uploadApp(filePath, options) {
|
|
|
658
685
|
package_name: result.package_name,
|
|
659
686
|
platform: result.platform,
|
|
660
687
|
is_new: isNew,
|
|
688
|
+
build_id: result.build_id ?? null,
|
|
689
|
+
version: result.version ?? null,
|
|
690
|
+
build_number: result.build_number ?? null,
|
|
691
|
+
deduplicated: result.deduplicated === true,
|
|
692
|
+
note: options.note?.trim() || null,
|
|
661
693
|
url: `${baseUrl}/apps/${result.public_id}`,
|
|
662
694
|
warnings: result.warnings ?? []
|
|
663
695
|
},
|
|
@@ -671,6 +703,8 @@ async function uploadApp(filePath, options) {
|
|
|
671
703
|
${chalk.cyan(result.public_id)} ${result.name} ${chalk.dim(`[${result.platform}]`)}`
|
|
672
704
|
);
|
|
673
705
|
console.log(` ${chalk.dim(result.package_name)}`);
|
|
706
|
+
const build = describeBuild(result);
|
|
707
|
+
if (build) console.log(` ${build}`);
|
|
674
708
|
console.log(chalk.dim(` ${baseUrl}/apps/${result.public_id}
|
|
675
709
|
`));
|
|
676
710
|
}
|
|
@@ -679,7 +713,9 @@ async function uploadApp(filePath, options) {
|
|
|
679
713
|
\u26A0 ${w}`));
|
|
680
714
|
}
|
|
681
715
|
} catch (err) {
|
|
682
|
-
|
|
716
|
+
const message = err instanceof Error ? err.message : "Upload failed";
|
|
717
|
+
spinner.fail(message);
|
|
718
|
+
if (options.output === "json") console.error(chalk.red(`Error: ${message}`));
|
|
683
719
|
process.exit(EXIT_INFRA_ERROR);
|
|
684
720
|
}
|
|
685
721
|
}
|
|
@@ -696,5 +732,6 @@ export {
|
|
|
696
732
|
EXIT_INFRA_ERROR,
|
|
697
733
|
UsageError,
|
|
698
734
|
uploadAppBuild,
|
|
735
|
+
describeBuild,
|
|
699
736
|
uploadApp
|
|
700
737
|
};
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
loadConfig,
|
|
7
7
|
resolveAuth,
|
|
8
8
|
uploadAppBuild
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-2WNJYNDW.js";
|
|
10
10
|
|
|
11
11
|
// src/lib/dev-state.ts
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
@@ -1646,6 +1646,26 @@ function decodeFrame(data) {
|
|
|
1646
1646
|
return { type, streamId: data.readUInt32BE(1), payload: data.subarray(HEADER_LEN) };
|
|
1647
1647
|
}
|
|
1648
1648
|
|
|
1649
|
+
// src/lib/redact.ts
|
|
1650
|
+
var SECRET_PARAMS = /* @__PURE__ */ new Set([
|
|
1651
|
+
"token",
|
|
1652
|
+
"access_token",
|
|
1653
|
+
"refresh_token",
|
|
1654
|
+
"api_key",
|
|
1655
|
+
"apikey",
|
|
1656
|
+
"key",
|
|
1657
|
+
"secret",
|
|
1658
|
+
"signature",
|
|
1659
|
+
"sig",
|
|
1660
|
+
"jwt"
|
|
1661
|
+
]);
|
|
1662
|
+
function redactSecrets(text) {
|
|
1663
|
+
return text.replace(
|
|
1664
|
+
/\b([A-Za-z_]+)=([^&\s"'#]+)/g,
|
|
1665
|
+
(match, name) => SECRET_PARAMS.has(name.toLowerCase()) ? `${name}=[redacted]` : match
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1649
1669
|
// src/lib/tunnel-client.ts
|
|
1650
1670
|
var FATAL_CLOSE_CODES = /* @__PURE__ */ new Set([4008, 4009]);
|
|
1651
1671
|
var MAX_BACKOFF_MS = 8e3;
|
|
@@ -1665,7 +1685,7 @@ var TunnelClient = class {
|
|
|
1665
1685
|
url;
|
|
1666
1686
|
connCount = 0;
|
|
1667
1687
|
debug(msg) {
|
|
1668
|
-
this.opts.onDebug?.(msg);
|
|
1688
|
+
this.opts.onDebug?.(redactSecrets(msg));
|
|
1669
1689
|
}
|
|
1670
1690
|
start() {
|
|
1671
1691
|
this.connect();
|
|
@@ -2060,7 +2080,10 @@ async function startDevSession(cfg, onEvent = () => {
|
|
|
2060
2080
|
}, onTunnelState) {
|
|
2061
2081
|
const api = new ApiClient(cfg.baseUrl, cfg.token);
|
|
2062
2082
|
const usesTunnel = PLATFORM_NEEDS_METRO[cfg.platform];
|
|
2063
|
-
const
|
|
2083
|
+
const pool = cfg.model ? resolveDeviceModel(await api.listDeviceModels(), cfg.platform, cfg.model) : null;
|
|
2084
|
+
const created = await api.createSession(
|
|
2085
|
+
devSessionCreateBody(pool ? { ...cfg, deviceCategory: pool.category, model: pool.model } : cfg)
|
|
2086
|
+
);
|
|
2064
2087
|
const sessionId = created.session_id;
|
|
2065
2088
|
let stopping = false;
|
|
2066
2089
|
let tunnel = null;
|
|
@@ -2154,9 +2177,23 @@ function devSessionCreateBody(cfg) {
|
|
|
2154
2177
|
device_category: cfg.deviceCategory,
|
|
2155
2178
|
app_id: cfg.appId,
|
|
2156
2179
|
// Roku has no JS bundle to serve, so there is nothing to tunnel.
|
|
2157
|
-
tunnel: PLATFORM_NEEDS_METRO[cfg.platform]
|
|
2180
|
+
tunnel: PLATFORM_NEEDS_METRO[cfg.platform],
|
|
2181
|
+
...cfg.model ? { model: cfg.model } : {}
|
|
2158
2182
|
};
|
|
2159
2183
|
}
|
|
2184
|
+
function resolveDeviceModel(pools, platform, model) {
|
|
2185
|
+
const { deviceType, tv } = PLATFORM_DEVICE[platform];
|
|
2186
|
+
const candidates = pools.filter(
|
|
2187
|
+
(p) => p.device_type === deviceType && p.category === "tv" === tv
|
|
2188
|
+
);
|
|
2189
|
+
const wanted = model.trim();
|
|
2190
|
+
const match = candidates.find((p) => p.model === wanted) ?? candidates.find((p) => p.model.toLowerCase() === wanted.toLowerCase());
|
|
2191
|
+
if (match) return match;
|
|
2192
|
+
const names = [...new Set(candidates.map((p) => p.model))].sort();
|
|
2193
|
+
throw new Error(
|
|
2194
|
+
names.length ? `no ${platform} device model named "${model}" \u2014 available: ${names.join(", ")} (see \`vibeview list-devices --models\`)` : `no ${platform} devices are available right now`
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2160
2197
|
var BLOCKING_WARNING_PREFIXES = ["No debug build", "App install failed", "App not found"];
|
|
2161
2198
|
function findBlockingWarning(info) {
|
|
2162
2199
|
return (info.warnings ?? []).find((w) => BLOCKING_WARNING_PREFIXES.some((p) => w.includes(p)));
|
|
@@ -2296,6 +2333,16 @@ async function ensureConfig(api, opts) {
|
|
|
2296
2333
|
console.error(chalk3.red(`error: use --platform tvos or --platform androidtv for TV devices`));
|
|
2297
2334
|
process.exit(2);
|
|
2298
2335
|
}
|
|
2336
|
+
if (opts.model !== void 0 && opts.deviceCategory) {
|
|
2337
|
+
console.error(chalk3.red("error: pass --model or --device-category, not both"));
|
|
2338
|
+
process.exit(2);
|
|
2339
|
+
}
|
|
2340
|
+
if (opts.model !== void 0 && !opts.model.trim()) {
|
|
2341
|
+
console.error(
|
|
2342
|
+
chalk3.red("error: --model needs a model name (see `vibeview list-devices --models`)")
|
|
2343
|
+
);
|
|
2344
|
+
process.exit(2);
|
|
2345
|
+
}
|
|
2299
2346
|
let firstRunBuild = null;
|
|
2300
2347
|
let firstRunMetro = null;
|
|
2301
2348
|
let isFirstRun = false;
|
|
@@ -2430,6 +2477,7 @@ Run \`vibeview dev\` without --app to pick from the list.`
|
|
|
2430
2477
|
platform,
|
|
2431
2478
|
appId,
|
|
2432
2479
|
deviceCategory,
|
|
2480
|
+
model: opts.model,
|
|
2433
2481
|
metroPort: finalMetro.port,
|
|
2434
2482
|
metroCommand: finalMetro.command,
|
|
2435
2483
|
platformHasMetroOverride: finalMetro.platformHasOverride,
|
|
@@ -2573,6 +2621,7 @@ async function runDevInProcess(opts) {
|
|
|
2573
2621
|
platform: cfg.platform,
|
|
2574
2622
|
appId: cfg.appId,
|
|
2575
2623
|
deviceCategory: cfg.deviceCategory,
|
|
2624
|
+
model: cfg.model,
|
|
2576
2625
|
metroPort: cfg.metroPort ?? 8081,
|
|
2577
2626
|
// startDevSession's own browser-open happens before we've written the
|
|
2578
2627
|
// state file / emitted session_ready — open it ourselves afterward
|
|
@@ -2820,6 +2869,55 @@ async function dev(opts) {
|
|
|
2820
2869
|
if (process.exitCode === 1) return;
|
|
2821
2870
|
}
|
|
2822
2871
|
|
|
2872
|
+
// src/lib/formatters/devices.ts
|
|
2873
|
+
import chalk4 from "chalk";
|
|
2874
|
+
var HEADERS = ["DEVICE_ID", "NAME", "TYPE", "CATEGORY", "OS", "STATUS"];
|
|
2875
|
+
function formatDevicesHuman(devices) {
|
|
2876
|
+
if (devices.length === 0) {
|
|
2877
|
+
return "No devices registered.\n";
|
|
2878
|
+
}
|
|
2879
|
+
const rows = devices.map((d) => [
|
|
2880
|
+
d.device_id,
|
|
2881
|
+
d.name,
|
|
2882
|
+
d.type,
|
|
2883
|
+
d.category,
|
|
2884
|
+
d.os_version,
|
|
2885
|
+
d.status
|
|
2886
|
+
]);
|
|
2887
|
+
const widths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2888
|
+
const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
|
|
2889
|
+
const lines = [chalk4.bold(fmt(HEADERS))];
|
|
2890
|
+
for (const row of rows.sort(rowSort)) {
|
|
2891
|
+
const statusColored = row[5] === "available" ? chalk4.green(row[5]) : row[5] === "busy" ? chalk4.yellow(row[5]) : chalk4.dim(row[5]);
|
|
2892
|
+
lines.push(fmt([row[0], row[1], row[2], row[3], row[4], statusColored]));
|
|
2893
|
+
}
|
|
2894
|
+
return lines.join("\n") + "\n";
|
|
2895
|
+
}
|
|
2896
|
+
function rowSort(a, b) {
|
|
2897
|
+
return a[2].localeCompare(b[2]) || a[3].localeCompare(b[3]) || a[0].localeCompare(b[0]);
|
|
2898
|
+
}
|
|
2899
|
+
function filterDeviceModels(models, opts) {
|
|
2900
|
+
return models.filter((m) => !opts.type || m.device_type === opts.type).filter((m) => !opts.category || m.category === opts.category).map((m) => ({
|
|
2901
|
+
model: m.model,
|
|
2902
|
+
os_version: m.os_version,
|
|
2903
|
+
device_type: m.device_type,
|
|
2904
|
+
category: m.category,
|
|
2905
|
+
form_factor: m.form_factor ?? null
|
|
2906
|
+
}));
|
|
2907
|
+
}
|
|
2908
|
+
var MODEL_HEADERS = ["MODEL", "OS", "TYPE", "CATEGORY", "FORM FACTOR"];
|
|
2909
|
+
function formatDeviceModelsHuman(models) {
|
|
2910
|
+
if (models.length === 0) {
|
|
2911
|
+
return "No device models available right now.\n";
|
|
2912
|
+
}
|
|
2913
|
+
const rows = models.map((m) => [m.model, m.os_version, m.device_type, m.category, m.form_factor ?? "-"]).sort(
|
|
2914
|
+
(a, b) => a[2].localeCompare(b[2]) || a[3].localeCompare(b[3]) || a[0].localeCompare(b[0]) || a[1].localeCompare(b[1])
|
|
2915
|
+
);
|
|
2916
|
+
const widths = MODEL_HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
2917
|
+
const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
|
|
2918
|
+
return [chalk4.bold(fmt(MODEL_HEADERS)), ...rows.map(fmt)].join("\n") + "\n";
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2823
2921
|
// src/agent/registry.ts
|
|
2824
2922
|
var AGENT_COMMANDS = [
|
|
2825
2923
|
{
|
|
@@ -3135,6 +3233,52 @@ var AGENT_COMMANDS = [
|
|
|
3135
3233
|
}
|
|
3136
3234
|
]
|
|
3137
3235
|
},
|
|
3236
|
+
{
|
|
3237
|
+
// set_posture: { posture: 'closed' | 'partial' | 'open' } OR { angle: 0..180 }
|
|
3238
|
+
// — exactly one (buildToolCall enforces it CLI-side, agent_control.py's
|
|
3239
|
+
// validate_tool_args at the gate). The angle is a settle-once fold, the
|
|
3240
|
+
// same as the dashboard slider's release, not its live drag stream.
|
|
3241
|
+
// Foldable devices only.
|
|
3242
|
+
verb: "set-posture",
|
|
3243
|
+
tool: "set_posture",
|
|
3244
|
+
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.",
|
|
3245
|
+
mutating: true,
|
|
3246
|
+
params: [
|
|
3247
|
+
{
|
|
3248
|
+
name: "posture",
|
|
3249
|
+
cli: "positional",
|
|
3250
|
+
type: "string",
|
|
3251
|
+
required: false,
|
|
3252
|
+
description: "One of 'closed', 'partial', 'open'. Omit when passing an angle."
|
|
3253
|
+
},
|
|
3254
|
+
{
|
|
3255
|
+
name: "angle",
|
|
3256
|
+
cli: "--angle",
|
|
3257
|
+
type: "number",
|
|
3258
|
+
required: false,
|
|
3259
|
+
description: "Exact hinge angle in whole degrees, 0 (shut) to 180 (flat open). Omit when passing a posture."
|
|
3260
|
+
}
|
|
3261
|
+
]
|
|
3262
|
+
},
|
|
3263
|
+
{
|
|
3264
|
+
// rotate: { degrees?: 90 | 180 | 270 } (default 90). The same control as
|
|
3265
|
+
// the dashboard's Rotate button. A foldable turns its body by quarter
|
|
3266
|
+
// turns (90 clockwise, 270 back, 180 a half turn); a rigid phone toggles
|
|
3267
|
+
// portrait <-> landscape and accepts only 90. Refused on TV devices.
|
|
3268
|
+
verb: "rotate",
|
|
3269
|
+
tool: "rotate",
|
|
3270
|
+
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.",
|
|
3271
|
+
mutating: true,
|
|
3272
|
+
params: [
|
|
3273
|
+
{
|
|
3274
|
+
name: "degrees",
|
|
3275
|
+
cli: "--degrees",
|
|
3276
|
+
type: "number",
|
|
3277
|
+
required: false,
|
|
3278
|
+
description: "Foldable only: 90 (default, a quarter turn clockwise), 180, or 270 (a quarter turn back). A phone accepts only 90."
|
|
3279
|
+
}
|
|
3280
|
+
]
|
|
3281
|
+
},
|
|
3138
3282
|
{
|
|
3139
3283
|
// tools.py wait: { seconds?: number } (default: 2). Note the wire arg is
|
|
3140
3284
|
// "seconds", NOT "ms" — tool_dispatch.py reads args.get("seconds", 2)
|
|
@@ -3226,18 +3370,18 @@ var AGENT_COMMANDS = [
|
|
|
3226
3370
|
];
|
|
3227
3371
|
|
|
3228
3372
|
// src/agent/render.ts
|
|
3229
|
-
import
|
|
3373
|
+
import chalk5 from "chalk";
|
|
3230
3374
|
function renderEnvelope(env) {
|
|
3231
3375
|
const lines = [];
|
|
3232
3376
|
if (env.ok) {
|
|
3233
|
-
lines.push(
|
|
3377
|
+
lines.push(chalk5.green(`ok`) + chalk5.dim(` (${env.tool})`));
|
|
3234
3378
|
} else {
|
|
3235
|
-
lines.push(
|
|
3379
|
+
lines.push(chalk5.red(`error`) + chalk5.dim(` (${env.tool})`));
|
|
3236
3380
|
}
|
|
3237
3381
|
lines.push(...resultLines(env.result));
|
|
3238
3382
|
if (env.observation) {
|
|
3239
3383
|
if (env.observation.kind === "unavailable") {
|
|
3240
|
-
lines.push(
|
|
3384
|
+
lines.push(chalk5.yellow(`warning: ${env.observation.text}`));
|
|
3241
3385
|
} else {
|
|
3242
3386
|
lines.push(env.observation.text);
|
|
3243
3387
|
}
|
|
@@ -3250,17 +3394,17 @@ function resultLines(result) {
|
|
|
3250
3394
|
const lines = [];
|
|
3251
3395
|
if (Array.isArray(r.entries) && typeof r.cursor === "number") {
|
|
3252
3396
|
if (r.entries.length === 0) {
|
|
3253
|
-
lines.push(
|
|
3397
|
+
lines.push(chalk5.dim("(no log lines)"));
|
|
3254
3398
|
}
|
|
3255
3399
|
for (const e of r.entries) {
|
|
3256
3400
|
if (!e || typeof e !== "object") continue;
|
|
3257
3401
|
const entry = e;
|
|
3258
3402
|
const ts = typeof entry.timestamp === "number" ? new Date(entry.timestamp).toISOString().slice(11, 23) : "??:??:??.???";
|
|
3259
3403
|
const level = typeof entry.level === "string" ? entry.level : "info";
|
|
3260
|
-
const levelTag = level === "error" ?
|
|
3261
|
-
lines.push(`${
|
|
3404
|
+
const levelTag = level === "error" ? chalk5.red(level) : level === "warn" ? chalk5.yellow(level) : chalk5.dim(level);
|
|
3405
|
+
lines.push(`${chalk5.dim(ts)} ${levelTag} ${String(entry.message ?? "")}`);
|
|
3262
3406
|
}
|
|
3263
|
-
lines.push(
|
|
3407
|
+
lines.push(chalk5.dim(`cursor: ${r.cursor}`));
|
|
3264
3408
|
}
|
|
3265
3409
|
if (r.found === true && typeof r.ref === "string" && r.ref) {
|
|
3266
3410
|
let found = `found: @${r.ref}`;
|
|
@@ -3272,14 +3416,43 @@ function resultLines(result) {
|
|
|
3272
3416
|
const text = typeof a.text === "string" && a.text ? ` ${JSON.stringify(a.text)}` : "";
|
|
3273
3417
|
return `${ref}${text}`;
|
|
3274
3418
|
});
|
|
3275
|
-
if (alts.length > 0) lines.push(
|
|
3419
|
+
if (alts.length > 0) lines.push(chalk5.dim(`alternatives: ${alts.join(", ")}`));
|
|
3276
3420
|
}
|
|
3277
3421
|
}
|
|
3278
|
-
if (
|
|
3422
|
+
if (r.action === "posture_set" || r.action === "rotated") {
|
|
3423
|
+
const state = deviceStateLine(r);
|
|
3424
|
+
if (state) lines.push(state);
|
|
3425
|
+
}
|
|
3426
|
+
if (typeof r.note === "string" && r.note) lines.push(chalk5.dim(r.note));
|
|
3279
3427
|
if (typeof r.message === "string" && r.message) lines.push(r.message);
|
|
3280
3428
|
if (typeof r.warning === "string" && r.warning) lines.push(r.warning);
|
|
3281
3429
|
return lines;
|
|
3282
3430
|
}
|
|
3431
|
+
function deviceStateLine(r) {
|
|
3432
|
+
const parts = [];
|
|
3433
|
+
if (r.action === "rotated") {
|
|
3434
|
+
if (typeof r.device_orientation === "string") {
|
|
3435
|
+
let o = `device held ${r.device_orientation}`;
|
|
3436
|
+
if (typeof r.screen_orientation === "string" && r.screen_orientation !== r.device_orientation) {
|
|
3437
|
+
o += ` (picture on the lit screen: ${r.screen_orientation})`;
|
|
3438
|
+
}
|
|
3439
|
+
parts.push(o);
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
if (typeof r.posture === "string") {
|
|
3443
|
+
let p = `posture: ${r.posture}`;
|
|
3444
|
+
if (typeof r.angle === "number") p += ` at ${r.angle}\xB0`;
|
|
3445
|
+
const extras = [];
|
|
3446
|
+
if (typeof r.screen === "string") extras.push(`${r.screen} screen`);
|
|
3447
|
+
const d = r.display;
|
|
3448
|
+
if (d && typeof d.width === "number" && typeof d.height === "number") {
|
|
3449
|
+
extras.push(`${d.width}x${d.height}`);
|
|
3450
|
+
}
|
|
3451
|
+
if (extras.length) p += ` (${extras.join(", ")})`;
|
|
3452
|
+
parts.push(p);
|
|
3453
|
+
}
|
|
3454
|
+
return parts.length ? parts.join("; ") : null;
|
|
3455
|
+
}
|
|
3283
3456
|
function agentExitCode(env) {
|
|
3284
3457
|
return env.ok ? 0 : 1;
|
|
3285
3458
|
}
|
|
@@ -3401,6 +3574,15 @@ function buildToolCall(cmd, positionals, opts) {
|
|
|
3401
3574
|
}
|
|
3402
3575
|
return { tool: cmd.tool, args: args2 };
|
|
3403
3576
|
}
|
|
3577
|
+
if (cmd.verb === "set-posture") {
|
|
3578
|
+
const hasPosture = positionals[0] !== void 0 && positionals[0] !== "";
|
|
3579
|
+
const hasAngle = opts.angle !== void 0 && opts.angle !== "";
|
|
3580
|
+
if (hasPosture === hasAngle) {
|
|
3581
|
+
throw new FlagError(
|
|
3582
|
+
hasPosture ? "set-posture takes a posture or --angle, not both" : "set-posture requires a posture (closed, partial, open) or --angle <0-180>"
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3404
3586
|
const args = {};
|
|
3405
3587
|
let positionalIndex = 0;
|
|
3406
3588
|
for (const param of cmd.params) {
|
|
@@ -3526,6 +3708,9 @@ export {
|
|
|
3526
3708
|
ROKU_LIVENESS_INTERVAL_MS,
|
|
3527
3709
|
PLATFORM_DEVICE,
|
|
3528
3710
|
dev,
|
|
3711
|
+
formatDevicesHuman,
|
|
3712
|
+
filterDeviceModels,
|
|
3713
|
+
formatDeviceModelsHuman,
|
|
3529
3714
|
AGENT_COMMANDS,
|
|
3530
3715
|
renderEnvelope,
|
|
3531
3716
|
buildToolCall,
|
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-MVNMGB6I.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-2WNJYNDW.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,10 +3222,14 @@ 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
|
-
).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));
|
|
3232
|
+
).option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--name <name>", "App name (auto-detected if not provided)").option("--note <text>", "Note stored on the uploaded build (shown in the dashboard)").action((file, opts) => uploadApp(file, opts));
|
|
3238
3233
|
program.command("create-test [name]").description("Create a standalone AI test case").option("--token <token>", "API token").option("--output <format>", "Output format: human, json", "human").option("--app <app>", "App ID (public_id) or package name (requires --platform)").option("--platform <platform>", "Platform (ios, android) when using package name for --app").option("--steps <json>", "Test steps as inline JSON array").option("--steps-file <path>", "Path to JSON file containing test steps").option("--context <text>", "Additional context for AI test execution").option("--test-name <name>", "Test case name (alternative to positional arg)").action((name, opts) => createTest({ ...opts, testName: name || opts.testName || "CLI Test" }));
|
|
3239
3234
|
program.command("login").description("Log in to VibeView (interactive)").option("--base-url <url>", "VibeView server URL (defaults to https://vibeview.io)").action((opts) => login(opts));
|
|
3240
3235
|
program.command("logout").description("Revoke the saved login token and remove it from ~/.vibeview/config.json").action(() => logout());
|
|
@@ -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-KIJCEN6T.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-MVNMGB6I.js";
|
|
18
20
|
import {
|
|
19
21
|
ApiClient,
|
|
20
22
|
resolveAuth
|
|
21
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-2WNJYNDW.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-BXXKH65S.js");
|
|
269
305
|
const { result: result2 } = await uploadAppBuild(f, { ...deps.auth, name: n });
|
|
270
306
|
return result2;
|
|
271
307
|
});
|
|
@@ -283,11 +319,17 @@ async function handleUploadApp(args, deps) {
|
|
|
283
319
|
package_name: result.package_name,
|
|
284
320
|
platform: result.platform,
|
|
285
321
|
is_new: isNew,
|
|
322
|
+
build_id: result.build_id ?? null,
|
|
323
|
+
version: result.version ?? null,
|
|
324
|
+
build_number: result.build_number ?? null,
|
|
286
325
|
warnings
|
|
287
326
|
};
|
|
288
327
|
const lines = [
|
|
289
328
|
`${isNew ? "New app created" : "Build added to existing app"}: ${result.name}`,
|
|
290
329
|
`app_id: ${result.public_id} [${result.platform}] ${result.package_name}`,
|
|
330
|
+
...result.build_id != null ? [
|
|
331
|
+
`build_id: ${result.build_id}${result.version ? ` version ${result.version}${result.build_number ? ` (${result.build_number})` : ""}` : ""}`
|
|
332
|
+
] : [],
|
|
291
333
|
...warnings.map((w) => `warning: ${w}`)
|
|
292
334
|
];
|
|
293
335
|
return textResult(lines.join("\n"), payload);
|
|
@@ -348,6 +390,8 @@ function createDevSessionController(auth, webUrl) {
|
|
|
348
390
|
platform: cfg.platform,
|
|
349
391
|
appId,
|
|
350
392
|
deviceCategory: isTv ? "tv" : entry?.deviceCategory ?? "phone",
|
|
393
|
+
// A named model decides the category itself (startDevSession).
|
|
394
|
+
model: cfg.model,
|
|
351
395
|
metroPort,
|
|
352
396
|
open: false
|
|
353
397
|
});
|
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
|