verikun 0.22.1 → 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/.claude/skills/verikun/SKILL.md +28 -6
- package/CHANGELOG.md +34 -16
- package/README.md +11 -1
- package/dist/agent/remote.js +44 -0
- package/dist/args.js +5 -0
- package/dist/cli.js +327 -26
- package/dist/drivers/adb.js +275 -26
- package/dist/drivers/index.js +4 -1
- package/dist/drivers/ios.js +132 -25
- package/dist/drivers/lifecycle.js +188 -0
- package/dist/exec.js +48 -0
- package/dist/server.js +306 -18
- package/dist/version.js +1 -1
- package/dist/wait.js +61 -0
- package/package.json +4 -1
package/dist/cli.js
CHANGED
|
@@ -50,6 +50,8 @@ exports.chooseLogOpts = chooseLogOpts;
|
|
|
50
50
|
exports.evalAssert = evalAssert;
|
|
51
51
|
exports.tokenizeLine = tokenizeLine;
|
|
52
52
|
exports.withBatchGlobals = withBatchGlobals;
|
|
53
|
+
exports.serverFromFlags = serverFromFlags;
|
|
54
|
+
exports.ensureDeviceTarget = ensureDeviceTarget;
|
|
53
55
|
exports.terminalFailure = terminalFailure;
|
|
54
56
|
exports.executeForServer = executeForServer;
|
|
55
57
|
exports.run = run;
|
|
@@ -60,6 +62,7 @@ const errors_1 = require("./errors");
|
|
|
60
62
|
const exec_1 = require("./exec");
|
|
61
63
|
const drivers_1 = require("./drivers");
|
|
62
64
|
const adb_1 = require("./drivers/adb");
|
|
65
|
+
const lifecycle_1 = require("./drivers/lifecycle");
|
|
63
66
|
const settings_1 = require("./device/settings");
|
|
64
67
|
const claims_1 = require("./device/claims");
|
|
65
68
|
const selector_1 = require("./ui/selector");
|
|
@@ -79,9 +82,9 @@ const cache_1 = require("./agent/cache");
|
|
|
79
82
|
const cost_1 = require("./agent/cost");
|
|
80
83
|
const remote_1 = require("./agent/remote");
|
|
81
84
|
const suite_1 = require("./suite");
|
|
85
|
+
const wait_1 = require("./wait");
|
|
82
86
|
const version_1 = require("./version");
|
|
83
87
|
const update_check_1 = require("./update-check");
|
|
84
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
85
88
|
// Exported for src/server.ts (which resolves its own platform/device at startup).
|
|
86
89
|
function platformFromFlags(flags) {
|
|
87
90
|
if ((0, args_1.flagBool)(flags, 'ios'))
|
|
@@ -217,7 +220,7 @@ async function matchWaiting(ctx, sel, opts = {}) {
|
|
|
217
220
|
const res = (0, selector_1.matchElements)(readForPoll(ctx, opts), sel);
|
|
218
221
|
if (res.matches.length > 0 || Date.now() >= deadline)
|
|
219
222
|
return res;
|
|
220
|
-
await sleep(pollStep(ctx.flags, deadline));
|
|
223
|
+
await (0, wait_1.sleep)(pollStep(ctx.flags, deadline));
|
|
221
224
|
}
|
|
222
225
|
}
|
|
223
226
|
/**
|
|
@@ -242,7 +245,7 @@ async function resolveOneWaiting(ctx, sel, opts = {}) {
|
|
|
242
245
|
const waited = windowMs > 0 ? ` after ${(windowMs / 1000).toFixed(1)}s` : '';
|
|
243
246
|
throw new errors_1.SelectorNotFoundError(`No element matched selector '${sel.raw}'${waited}. Run \`verikun ui\` to inspect the current screen.`);
|
|
244
247
|
}
|
|
245
|
-
await sleep(pollStep(ctx.flags, deadline));
|
|
248
|
+
await (0, wait_1.sleep)(pollStep(ctx.flags, deadline));
|
|
246
249
|
}
|
|
247
250
|
}
|
|
248
251
|
// --- Auto scroll-into-view --------------------------------------------------
|
|
@@ -299,7 +302,7 @@ async function scrollIntoView(ctx, sel, target, elements, screen, opts = {}) {
|
|
|
299
302
|
break; // in view, or no swipe big enough to be worth making
|
|
300
303
|
ctx.driver.swipe(plan.from.x, plan.from.y, plan.to.x, plan.to.y, (0, viewport_1.swipeDurationMs)(plan.distance));
|
|
301
304
|
swipes++;
|
|
302
|
-
await sleep(SCROLL_SETTLE_MS);
|
|
305
|
+
await (0, wait_1.sleep)(SCROLL_SETTLE_MS);
|
|
303
306
|
const before = current.bounds;
|
|
304
307
|
snapshot = ctx.driver.getElements(opts);
|
|
305
308
|
// An empty tree is a bad read, not a screen (the device returns partial dumps
|
|
@@ -312,7 +315,7 @@ async function scrollIntoView(ctx, sel, target, elements, screen, opts = {}) {
|
|
|
312
315
|
// (half of it, to land between the two) and look once more before giving up.
|
|
313
316
|
const mid = { x: Math.round((plan.from.x + plan.to.x) / 2), y: Math.round((plan.from.y + plan.to.y) / 2) };
|
|
314
317
|
ctx.driver.swipe(plan.to.x, plan.to.y, mid.x, mid.y, (0, viewport_1.swipeDurationMs)(plan.distance / 2));
|
|
315
|
-
await sleep(SCROLL_SETTLE_MS);
|
|
318
|
+
await (0, wait_1.sleep)(SCROLL_SETTLE_MS);
|
|
316
319
|
snapshot = ctx.driver.getElements(opts);
|
|
317
320
|
if ((0, selector_1.matchElements)(snapshot, sel).matches.length === 0) {
|
|
318
321
|
throw new errors_1.SelectorNotFoundError(`'${sel.raw}' left the hierarchy while being scrolled into view (after ${swipes} swipe(s)) — ` +
|
|
@@ -388,10 +391,28 @@ async function resolveTappable(ctx, sel, opts = {}) {
|
|
|
388
391
|
// ---------------------------------------------------------------------------
|
|
389
392
|
// Commands
|
|
390
393
|
// ---------------------------------------------------------------------------
|
|
391
|
-
function cmdDevices(
|
|
394
|
+
function cmdDevices(flags) {
|
|
395
|
+
const all = (0, args_1.flagBool)(flags, 'all');
|
|
392
396
|
const allDevices = [];
|
|
393
397
|
try {
|
|
394
|
-
|
|
398
|
+
const android = new drivers_1.AdbDriver().listDevices();
|
|
399
|
+
// Name each running emulator by its AVD, so the listing shows the token you'd
|
|
400
|
+
// pass to `vk devices stop|restart`. Best-effort: the console may not answer.
|
|
401
|
+
for (const d of android) {
|
|
402
|
+
if (d.kind === 'emulator')
|
|
403
|
+
d.name = (0, adb_1.avdNameOf)(d.serial) || undefined;
|
|
404
|
+
}
|
|
405
|
+
allDevices.push(...android);
|
|
406
|
+
if (all) {
|
|
407
|
+
// Startable-but-not-running AVDs have no adb address yet — they are addressed
|
|
408
|
+
// by name, so `serial` stays empty rather than being invented.
|
|
409
|
+
const running = new Set(android.map((d) => (d.name ?? '').toLowerCase()).filter(Boolean));
|
|
410
|
+
for (const name of (0, adb_1.listAvds)()) {
|
|
411
|
+
if (running.has(name.toLowerCase()))
|
|
412
|
+
continue;
|
|
413
|
+
allDevices.push({ serial: '', state: 'shutdown', platform: 'android', kind: 'emulator', name });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
395
416
|
}
|
|
396
417
|
catch (e) {
|
|
397
418
|
// adb not on PATH is the common (expected) case, but surface anything else so a real
|
|
@@ -399,8 +420,10 @@ function cmdDevices(ctx) {
|
|
|
399
420
|
(0, output_1.err)(`devices: adb backend unavailable (${e.message})`);
|
|
400
421
|
}
|
|
401
422
|
try {
|
|
402
|
-
//
|
|
403
|
-
|
|
423
|
+
// By default only booted simulators (a shutdown one isn't drivable); physical
|
|
424
|
+
// devices always show (they carry a note). --all lists everything startable.
|
|
425
|
+
const ios = new drivers_1.IdbDriver().listDevices();
|
|
426
|
+
allDevices.push(...(all ? ios : ios.filter((d) => d.state === 'booted' || d.note)));
|
|
404
427
|
}
|
|
405
428
|
catch (e) {
|
|
406
429
|
(0, output_1.err)(`devices: iOS backend unavailable (${e.message})`);
|
|
@@ -414,22 +437,140 @@ function cmdDevices(ctx) {
|
|
|
414
437
|
d.claim = claim;
|
|
415
438
|
}
|
|
416
439
|
}
|
|
417
|
-
if ((0, args_1.flagBool)(
|
|
440
|
+
if ((0, args_1.flagBool)(flags, 'json')) {
|
|
418
441
|
(0, output_1.json)(allDevices);
|
|
419
442
|
return 0;
|
|
420
443
|
}
|
|
421
444
|
if (!allDevices.length) {
|
|
422
|
-
(0, output_1.err)('No devices found.');
|
|
445
|
+
(0, output_1.err)(all ? 'No devices or startable AVDs/simulators found.' : 'No devices found.');
|
|
423
446
|
return 0;
|
|
424
447
|
}
|
|
425
448
|
for (const line of formatDeviceTable(allDevices))
|
|
426
449
|
(0, output_1.out)(line);
|
|
427
450
|
return 0;
|
|
428
451
|
}
|
|
452
|
+
/** Aliases for the lifecycle verbs, mirroring the top-level command aliases. */
|
|
453
|
+
const DEVICE_VERBS = {
|
|
454
|
+
start: 'start',
|
|
455
|
+
boot: 'start',
|
|
456
|
+
stop: 'stop',
|
|
457
|
+
shutdown: 'stop',
|
|
458
|
+
kill: 'stop',
|
|
459
|
+
restart: 'restart',
|
|
460
|
+
reboot: 'restart',
|
|
461
|
+
};
|
|
462
|
+
/**
|
|
463
|
+
* `vk devices [list|start|stop|restart]`. Dispatched as a META-command (before the
|
|
464
|
+
* recording machinery) whenever a subcommand is present: lifecycle needs no local
|
|
465
|
+
* driver — it is what you run when there ISN'T one — and a `--server` invocation
|
|
466
|
+
* must not build one either. Bare `vk devices` keeps the ordinary path.
|
|
467
|
+
*/
|
|
468
|
+
async function cmdDevicesEntry(positionals, flags) {
|
|
469
|
+
const sub = String(positionals[0] ?? 'list').toLowerCase();
|
|
470
|
+
const server = serverFromFlags(flags);
|
|
471
|
+
if (sub === 'list')
|
|
472
|
+
return server ? cmdDevicesRemoteList(server, flags) : cmdDevices(flags);
|
|
473
|
+
const verb = DEVICE_VERBS[sub];
|
|
474
|
+
if (!verb) {
|
|
475
|
+
throw new errors_1.CliError(`Unknown 'devices' subcommand '${sub}'. Use: list | start | stop | restart.`, 2);
|
|
476
|
+
}
|
|
477
|
+
return server
|
|
478
|
+
? cmdDevicesRemote(verb, positionals[1], server, flags)
|
|
479
|
+
: cmdDeviceLifecycle(verb, positionals[1], flags);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Refuse to power-cycle a device another job is actively driving.
|
|
483
|
+
*
|
|
484
|
+
* The local counterpart of the server's `409`: you may restart YOUR OWN device, never
|
|
485
|
+
* someone else's. Without this, device claims and device lifecycle would each work and
|
|
486
|
+
* together be a hole — one job could reboot the phone another job is mid-suite on,
|
|
487
|
+
* which is precisely the collision claims exist to prevent.
|
|
488
|
+
*
|
|
489
|
+
* `start` is exempt: it only ever brings up something that is not running, so there is
|
|
490
|
+
* nothing to pull out from under anyone.
|
|
491
|
+
*/
|
|
492
|
+
function assertNotClaimedByOthers(target, verb) {
|
|
493
|
+
if (verb === 'start' || !target.serial || !(0, claims_1.claimsEnabled)())
|
|
494
|
+
return;
|
|
495
|
+
const claim = (0, claims_1.summarize)(target.serial);
|
|
496
|
+
if (!claim || claim.mine)
|
|
497
|
+
return;
|
|
498
|
+
throw new errors_1.CliError(`${(0, lifecycle_1.targetLabel)(target)} (${target.serial}) is in use by ${claim.by} — ` +
|
|
499
|
+
`${verb} would pull the device out from under it.\n` +
|
|
500
|
+
`If that job is gone, hand the device back first: vk device release ${target.serial}`, 2);
|
|
501
|
+
}
|
|
502
|
+
async function cmdDeviceLifecycle(verb, rawTarget, flags) {
|
|
503
|
+
const wipe = (0, args_1.flagBool)(flags, 'wipe');
|
|
504
|
+
const wait = !(0, args_1.flagBool)(flags, 'no-wait');
|
|
505
|
+
const timeoutFlag = (0, args_1.flagStr)(flags, 'timeout');
|
|
506
|
+
const timeoutMs = timeoutFlag
|
|
507
|
+
? parseDuration(timeoutFlag, 'timeout')
|
|
508
|
+
: verb === 'stop'
|
|
509
|
+
? wait_1.DEFAULT_STOP_TIMEOUT_MS
|
|
510
|
+
: wait_1.DEFAULT_BOOT_TIMEOUT_MS;
|
|
511
|
+
// An explicit --ios/--android/--platform narrows the search; otherwise probe both
|
|
512
|
+
// backends, exactly as bare `vk devices` does, so `devices start Pixel_6_API_34`
|
|
513
|
+
// works without also naming the platform. A name that hits BOTH is still exit 2.
|
|
514
|
+
const explicitPlatform = (0, args_1.flagBool)(flags, 'ios') || (0, args_1.flagBool)(flags, 'android') || (0, args_1.flagStr)(flags, 'platform') !== undefined;
|
|
515
|
+
const lifecycles = explicitPlatform ? [(0, lifecycle_1.lifecycleFor)(platformFromFlags(flags))] : (0, lifecycle_1.allLifecycles)();
|
|
516
|
+
const targets = lifecycles.flatMap((l) => l.targets());
|
|
517
|
+
if (!rawTarget) {
|
|
518
|
+
const startable = targets.filter((t) => t.kind !== 'physical').map(lifecycle_1.targetLabel).filter(Boolean);
|
|
519
|
+
const hint = startable.length
|
|
520
|
+
? `\nStartable: ${[...new Set(startable)].join(', ')}`
|
|
521
|
+
: '\nRun `vk devices --all` to list startable devices.';
|
|
522
|
+
throw new errors_1.CliError(`Usage: verikun devices ${verb} <name|serial> [--timeout <dur>]${hint}`, 2);
|
|
523
|
+
}
|
|
524
|
+
// `start` wants the one that ISN'T up; `stop`/`restart` want the live one.
|
|
525
|
+
const chosen = (0, lifecycle_1.chooseTarget)(targets, rawTarget, { prefer: verb === 'start' ? 'startable' : 'running' });
|
|
526
|
+
(0, lifecycle_1.assertActionable)(chosen, verb, { wipe });
|
|
527
|
+
assertNotClaimedByOthers(chosen, verb);
|
|
528
|
+
const lc = (0, lifecycle_1.lifecycleFor)(chosen.platform);
|
|
529
|
+
const onProgress = (m) => (0, output_1.err)(`[verikun] ${m}`);
|
|
530
|
+
const asJson = (0, args_1.flagBool)(flags, 'json');
|
|
531
|
+
if (verb === 'stop') {
|
|
532
|
+
const r = await lc.shutdown(chosen, { timeoutMs, onProgress });
|
|
533
|
+
if (asJson) {
|
|
534
|
+
(0, output_1.json)({ action: 'stop', requested: rawTarget, platform: chosen.platform, kind: chosen.kind, serial: r.serial, name: chosen.name, stopped: r.stopped });
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
(0, output_1.err)(r.stopped ? `stopped ${(0, lifecycle_1.targetLabel)(chosen)}` : `${(0, lifecycle_1.targetLabel)(chosen)} was not running`);
|
|
538
|
+
(0, output_1.out)(r.serial);
|
|
539
|
+
}
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
const opts = { timeoutMs, wait, wipe, onProgress };
|
|
543
|
+
const r = verb === 'restart'
|
|
544
|
+
? await (0, lifecycle_1.restartTarget)(lc, chosen, opts)
|
|
545
|
+
: { ...(await lc.boot(chosen, opts)), stopped: false };
|
|
546
|
+
if (asJson) {
|
|
547
|
+
(0, output_1.json)({
|
|
548
|
+
action: verb,
|
|
549
|
+
requested: rawTarget,
|
|
550
|
+
platform: chosen.platform,
|
|
551
|
+
kind: chosen.kind,
|
|
552
|
+
serial: r.serial,
|
|
553
|
+
name: chosen.name,
|
|
554
|
+
started: r.started,
|
|
555
|
+
ready: r.ready,
|
|
556
|
+
waitedMs: r.waitedMs,
|
|
557
|
+
...(r.logPath ? { logPath: r.logPath } : {}),
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
if (verb === 'start' && !r.started)
|
|
562
|
+
(0, output_1.err)(`${(0, lifecycle_1.targetLabel)(chosen)} is already running`);
|
|
563
|
+
// stdout is the one machine-usable datum: the serial, so
|
|
564
|
+
// `vk -d "$(vk devices start X)" ui` composes. Under --no-wait on Android there
|
|
565
|
+
// is genuinely no serial yet, so print nothing rather than invent one.
|
|
566
|
+
(0, output_1.out)(r.serial ?? '');
|
|
567
|
+
}
|
|
568
|
+
return 0;
|
|
569
|
+
}
|
|
429
570
|
/**
|
|
430
571
|
* Render the device list as an aligned, headed table (header line first, then one
|
|
431
|
-
* line per device). Optional columns (MODEL/PRODUCT/NOTE) are dropped when no
|
|
432
|
-
* populates them; every shown cell is padded to its column width so columns line up
|
|
572
|
+
* line per device). Optional columns (KIND/NAME/MODEL/PRODUCT/NOTE) are dropped when no
|
|
573
|
+
* device populates them; every shown cell is padded to its column width so columns line up
|
|
433
574
|
* regardless of which cells are empty — the previous `.filter(Boolean).join('\t')`
|
|
434
575
|
* dropped empty cells, sliding later cells into earlier tab stops. Exported for unit
|
|
435
576
|
* testing.
|
|
@@ -437,8 +578,10 @@ function cmdDevices(ctx) {
|
|
|
437
578
|
function formatDeviceTable(devices) {
|
|
438
579
|
const columns = [
|
|
439
580
|
{ header: 'PLATFORM', get: (d) => d.platform },
|
|
581
|
+
{ header: 'KIND', get: (d) => d.kind ?? '', optional: true },
|
|
440
582
|
{ header: 'SERIAL', get: (d) => d.serial },
|
|
441
583
|
{ header: 'STATE', get: (d) => d.state },
|
|
584
|
+
{ header: 'NAME', get: (d) => d.name ?? '', optional: true },
|
|
442
585
|
{ header: 'MODEL', get: (d) => d.model ?? '', optional: true },
|
|
443
586
|
{ header: 'PRODUCT', get: (d) => d.product ?? '', optional: true },
|
|
444
587
|
// Optional like the rest, which is load-bearing twice over: nothing changes for a
|
|
@@ -650,13 +793,13 @@ async function cmdText(ctx) {
|
|
|
650
793
|
});
|
|
651
794
|
ctx.driver.tap(point.x, point.y);
|
|
652
795
|
// Wait for field to be focused after tap
|
|
653
|
-
await sleep(100);
|
|
796
|
+
await (0, wait_1.sleep)(100);
|
|
654
797
|
if ((0, args_1.flagBool)(ctx.flags, 'clear') && target.text) {
|
|
655
798
|
ctx.driver.pressKey('move_end');
|
|
656
799
|
for (let i = 0; i < target.text.length + 2; i++)
|
|
657
800
|
ctx.driver.pressKey('del');
|
|
658
801
|
// Wait for field to settle after clearing before typing
|
|
659
|
-
await sleep(200);
|
|
802
|
+
await (0, wait_1.sleep)(200);
|
|
660
803
|
}
|
|
661
804
|
// Prime the input method with a space, then delete it, to avoid losing first character
|
|
662
805
|
// (workaround for adb input text behavior where first char is sometimes lost)
|
|
@@ -887,7 +1030,7 @@ async function cmdWait(ctx) {
|
|
|
887
1030
|
(0, output_1.out)((0, format_1.formatCompact)(matches));
|
|
888
1031
|
return 0;
|
|
889
1032
|
}
|
|
890
|
-
await sleep(interval);
|
|
1033
|
+
await (0, wait_1.sleep)(interval);
|
|
891
1034
|
}
|
|
892
1035
|
ctx.record?.note({ selector: sel, message: `timeout after ${timeout}ms${gone ? ' (still present)' : ' (never appeared)'}` });
|
|
893
1036
|
(0, output_1.err)(`timeout after ${timeout}ms waiting for '${sel.raw}'${gone ? ' to disappear' : ''}`);
|
|
@@ -938,7 +1081,7 @@ async function cmdAssert(ctx) {
|
|
|
938
1081
|
const deadline = Date.now() + waitWindowMs(ctx.flags);
|
|
939
1082
|
let result = evalAssert(readForPoll(ctx), sel, ctx.flags);
|
|
940
1083
|
while (!result.pass && Date.now() < deadline) {
|
|
941
|
-
await sleep(pollStep(ctx.flags, deadline));
|
|
1084
|
+
await (0, wait_1.sleep)(pollStep(ctx.flags, deadline));
|
|
942
1085
|
result = evalAssert(readForPoll(ctx), sel, ctx.flags);
|
|
943
1086
|
}
|
|
944
1087
|
const { pass, reason, matches } = result;
|
|
@@ -1624,9 +1767,136 @@ async function obtainPlan(key, file, opts, cost, provider) {
|
|
|
1624
1767
|
}
|
|
1625
1768
|
return { plan: compiled.plan, cached: false };
|
|
1626
1769
|
}
|
|
1770
|
+
/** The `--server` URL, or VERIKUN_SERVER. Exported-shape helper so `resolveBackend`
|
|
1771
|
+
* and `vk devices --server` can never disagree about what "remote" means. */
|
|
1772
|
+
function serverFromFlags(flags) {
|
|
1773
|
+
return (0, args_1.flagStr)(flags, 'server') || process.env.VERIKUN_SERVER || undefined;
|
|
1774
|
+
}
|
|
1775
|
+
function remoteOptsFrom(url, flags) {
|
|
1776
|
+
return { url, authKey: (0, args_1.flagStr)(flags, 'auth-key') || process.env.VERIKUN_SERVER_AUTH_KEY || undefined };
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* `--ensure-device[=<target>]` — the requested target, or `undefined` for the bare
|
|
1780
|
+
* form, or `null` when the flag is absent. Exported for unit tests.
|
|
1781
|
+
*/
|
|
1782
|
+
function ensureDeviceTarget(flags) {
|
|
1783
|
+
const raw = flags['ensure-device'];
|
|
1784
|
+
if (raw === undefined || raw === false)
|
|
1785
|
+
return null;
|
|
1786
|
+
if (raw === true || raw === 'true')
|
|
1787
|
+
return undefined;
|
|
1788
|
+
return String(raw);
|
|
1789
|
+
}
|
|
1790
|
+
/** The one target that is startable without guessing, else exit 2 with the options. */
|
|
1791
|
+
function soleStartable(targets) {
|
|
1792
|
+
const startable = targets.filter((t) => t.kind !== 'physical' && !(0, lifecycle_1.isRunning)(t));
|
|
1793
|
+
if (startable.length === 1)
|
|
1794
|
+
return startable[0];
|
|
1795
|
+
const names = [...new Set(startable.map(lifecycle_1.targetLabel).filter(Boolean))];
|
|
1796
|
+
throw new errors_1.CliError(startable.length === 0
|
|
1797
|
+
? '--ensure-device: no startable device found. Run `vk devices --all` to check the toolchain.'
|
|
1798
|
+
: `--ensure-device: ${startable.length} devices could be started — name one, ` +
|
|
1799
|
+
`e.g. --ensure-device="${names[0]}"\n ${names.join('\n ')}`, 2);
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Boot a device before the first step, when `--ensure-device` asks for it. Runs here
|
|
1803
|
+
* — inside resolveBackend, before runCtx is fixed — rather than inside `vk suite`,
|
|
1804
|
+
* so the booted serial is the one every recorded step is attributed to. It is
|
|
1805
|
+
* structurally unable to turn a red test green: it happens before any step executes,
|
|
1806
|
+
* never between tests and never mid-run.
|
|
1807
|
+
*/
|
|
1808
|
+
async function ensureLocalDevice(platform, device, flags) {
|
|
1809
|
+
const want = ensureDeviceTarget(flags);
|
|
1810
|
+
if (want === null)
|
|
1811
|
+
return;
|
|
1812
|
+
try {
|
|
1813
|
+
const serial = (0, drivers_1.getDriver)(platform, device).resolvedSerial();
|
|
1814
|
+
// `--ensure-device=X` means "boot X if nothing is usable", not "make X the
|
|
1815
|
+
// device" — so say so plainly when a different device is already serving,
|
|
1816
|
+
// rather than reporting a serial the caller didn't ask for.
|
|
1817
|
+
(0, output_1.err)(`[verikun] --ensure-device: ${serial} is already available${want ? ` — not booting '${want}'` : ''}`);
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
catch (e) {
|
|
1821
|
+
// Ambiguity (exit 2) is an operator error — booting another device makes it worse.
|
|
1822
|
+
if (!(e instanceof errors_1.CliError) || e.exitCode !== 3)
|
|
1823
|
+
throw e;
|
|
1824
|
+
}
|
|
1825
|
+
const lc = (0, lifecycle_1.lifecycleFor)(platform);
|
|
1826
|
+
const targets = lc.targets();
|
|
1827
|
+
const chosen = want ? (0, lifecycle_1.chooseTarget)(targets, want, { prefer: 'startable' }) : soleStartable(targets);
|
|
1828
|
+
(0, lifecycle_1.assertActionable)(chosen, 'start');
|
|
1829
|
+
(0, output_1.err)(`[verikun] --ensure-device: booting ${(0, lifecycle_1.targetLabel)(chosen)}…`);
|
|
1830
|
+
await lc.boot(chosen, {
|
|
1831
|
+
timeoutMs: wait_1.DEFAULT_BOOT_TIMEOUT_MS,
|
|
1832
|
+
wait: true,
|
|
1833
|
+
wipe: false,
|
|
1834
|
+
onProgress: (m) => (0, output_1.err)(`[verikun] ${m}`),
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
/** The remote half of `--ensure-device`: ask the server to boot, then re-ping so the
|
|
1838
|
+
* run context carries the serial the device actually came up as. */
|
|
1839
|
+
async function ensureRemoteDevice(health, opts, url, flags) {
|
|
1840
|
+
const want = ensureDeviceTarget(flags);
|
|
1841
|
+
if (want === null)
|
|
1842
|
+
return health;
|
|
1843
|
+
if (health.serial) {
|
|
1844
|
+
(0, output_1.err)(`[verikun] --ensure-device: ${health.serial} is already bound on ${url}${want ? ` — not booting '${want}'` : ''}`);
|
|
1845
|
+
return health;
|
|
1846
|
+
}
|
|
1847
|
+
assertDeviceControl(health, url, want);
|
|
1848
|
+
(0, output_1.err)(`[verikun] --ensure-device: asking ${url} to boot a device…`);
|
|
1849
|
+
await (0, remote_1.remoteDeviceOp)(opts, 'start', want ? { target: want } : {});
|
|
1850
|
+
return (0, remote_1.pingServer)(opts);
|
|
1851
|
+
}
|
|
1852
|
+
/** Feature-detect device control from /v1/health. Never compare versions: a client
|
|
1853
|
+
* cannot tell "old server" from "new server, flag off", and both need this answer. */
|
|
1854
|
+
function assertDeviceControl(health, url, target) {
|
|
1855
|
+
if (!health.deviceControlEnabled) {
|
|
1856
|
+
throw new errors_1.CliError(`device control is not available on the verikun server at ${url} (verikun ${health.version}). ` +
|
|
1857
|
+
'Restart it with --allow-device-control.', 3);
|
|
1858
|
+
}
|
|
1859
|
+
if (target && !health.deviceNamingEnabled) {
|
|
1860
|
+
throw new errors_1.CliError(`the server at ${url} was started with a bare --allow-device-control — it can only act on its own ` +
|
|
1861
|
+
`device, not '${target}'. Restart it with --allow-device-control=<names>.`, 2);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
async function cmdDevicesRemote(verb, target, url, flags) {
|
|
1865
|
+
const opts = remoteOptsFrom(url, flags);
|
|
1866
|
+
const health = await (0, remote_1.pingServer)(opts); // fails fast (exit 3) on a bad URL or key
|
|
1867
|
+
assertDeviceControl(health, url, target);
|
|
1868
|
+
const wipe = (0, args_1.flagBool)(flags, 'wipe');
|
|
1869
|
+
(0, output_1.err)(`[verikun] ${verb} device on ${url}${target ? ` (${target})` : ''}${wipe ? ' with WIPE' : ''}…`);
|
|
1870
|
+
const r = await (0, remote_1.remoteDeviceOp)(opts, verb, { ...(target ? { target } : {}), ...(wipe ? { wipe } : {}) });
|
|
1871
|
+
(0, output_1.err)(`[verikun] ${r.changed ? `${verb} ok` : 'already running'} · ${r.serial ?? '(none)'} · ` +
|
|
1872
|
+
`${(r.durationMs / 1000).toFixed(1)}s`);
|
|
1873
|
+
if ((0, args_1.flagBool)(flags, 'json'))
|
|
1874
|
+
(0, output_1.json)({ action: verb, server: url, ...r });
|
|
1875
|
+
else
|
|
1876
|
+
(0, output_1.out)(r.serial ?? '');
|
|
1877
|
+
return 0;
|
|
1878
|
+
}
|
|
1879
|
+
async function cmdDevicesRemoteList(url, flags) {
|
|
1880
|
+
const opts = remoteOptsFrom(url, flags);
|
|
1881
|
+
const health = await (0, remote_1.pingServer)(opts);
|
|
1882
|
+
assertDeviceControl(health, url);
|
|
1883
|
+
const r = await (0, remote_1.remoteDeviceList)(opts);
|
|
1884
|
+
if ((0, args_1.flagBool)(flags, 'json')) {
|
|
1885
|
+
(0, output_1.json)({ server: url, ...r });
|
|
1886
|
+
return 0;
|
|
1887
|
+
}
|
|
1888
|
+
if (r.devices.length)
|
|
1889
|
+
for (const line of formatDeviceTable(r.devices))
|
|
1890
|
+
(0, output_1.out)(line);
|
|
1891
|
+
else
|
|
1892
|
+
(0, output_1.err)('No devices visible on the server.');
|
|
1893
|
+
(0, output_1.err)(`[verikun] bound: ${r.bound ?? '(none)'}${r.startable.length ? ` · startable: ${r.startable.join(', ')}` : ''}`);
|
|
1894
|
+
return 0;
|
|
1895
|
+
}
|
|
1627
1896
|
async function resolveBackend(platform, device, flags) {
|
|
1628
|
-
const server = (
|
|
1897
|
+
const server = serverFromFlags(flags);
|
|
1629
1898
|
if (!server) {
|
|
1899
|
+
await ensureLocalDevice(platform, device, flags);
|
|
1630
1900
|
const driver = (0, drivers_1.getDriver)(platform, device);
|
|
1631
1901
|
// Fail fast on a broken toolchain BEFORE any model spend — the mirror of the
|
|
1632
1902
|
// remote branch's pingServer below. Without this, a missing `idb` isn't noticed
|
|
@@ -1680,9 +1950,13 @@ async function resolveBackend(platform, device, flags) {
|
|
|
1680
1950
|
// so archive-time / vk log scoping works without a local driver.
|
|
1681
1951
|
onStep: (step, artifacts, logStart) => run_1.Recorder.appendForeignStep(step, artifacts, { ...runCtx, logStart }),
|
|
1682
1952
|
};
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1953
|
+
let health = await (0, remote_1.pingServer)(opts); // fails fast (exit 3) on a bad URL or key
|
|
1954
|
+
// `--ensure-device` boots BEFORE runCtx is fixed: resolveBackend bakes the serial
|
|
1955
|
+
// into the run context and the returned device, so booting afterwards would
|
|
1956
|
+
// attribute every spliced step to a device that didn't exist yet.
|
|
1957
|
+
health = await ensureRemoteDevice(health, opts, server, flags);
|
|
1958
|
+
runCtx = { platform: health.platform, device: health.serial ?? undefined };
|
|
1959
|
+
(0, output_1.err)(`[verikun] server ${server}: ${health.platform} · device ${health.serial ?? '(none)'} · verikun ${health.version}`);
|
|
1686
1960
|
// Say the read path once, here. Reads execute server-side, so this is the only end of the
|
|
1687
1961
|
// connection that knows it — and without it a companion that had silently stood down was
|
|
1688
1962
|
// indistinguishable from one that never engaged, for a whole suite (issue #77). An older
|
|
@@ -1715,7 +1989,7 @@ async function resolveBackend(platform, device, flags) {
|
|
|
1715
1989
|
},
|
|
1716
1990
|
},
|
|
1717
1991
|
platform: health.platform,
|
|
1718
|
-
device: health.serial,
|
|
1992
|
+
device: health.serial ?? undefined,
|
|
1719
1993
|
remote: { url: server, version: health.version, reads: health.reads },
|
|
1720
1994
|
};
|
|
1721
1995
|
}
|
|
@@ -2094,8 +2368,6 @@ function cmdCompanion(ctx) {
|
|
|
2094
2368
|
// ---------------------------------------------------------------------------
|
|
2095
2369
|
async function executeCommand(command, ctx) {
|
|
2096
2370
|
switch (command) {
|
|
2097
|
-
case 'devices':
|
|
2098
|
-
return cmdDevices(ctx);
|
|
2099
2371
|
case 'doctor':
|
|
2100
2372
|
return cmdDoctor(ctx);
|
|
2101
2373
|
case 'companion':
|
|
@@ -2280,6 +2552,19 @@ async function executeParsed(command, positionals, flags) {
|
|
|
2280
2552
|
return cmdRun(positionals, flags, platform, device);
|
|
2281
2553
|
if (command === 'batch')
|
|
2282
2554
|
return cmdBatch(positionals, flags);
|
|
2555
|
+
// `devices` is wholly a meta-command: it must not build a local driver. Lifecycle is
|
|
2556
|
+
// what you run when there ISN'T one, a `--server` call needs no local device at all,
|
|
2557
|
+
// and even the bare listing probes both backends itself rather than using ctx.driver.
|
|
2558
|
+
// Dispatched in exactly one place on purpose — a command split across the switch AND
|
|
2559
|
+
// this chain reads as two commands to anything parsing the dispatch (tests/docs-coverage).
|
|
2560
|
+
if (command === 'devices') {
|
|
2561
|
+
try {
|
|
2562
|
+
return await cmdDevicesEntry(positionals, flags);
|
|
2563
|
+
}
|
|
2564
|
+
catch (e) {
|
|
2565
|
+
return mapError(e, flags);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2283
2568
|
// `ai`/`suite`/`install`/`server` orchestrate their own steps; map their thrown
|
|
2284
2569
|
// CliErrors to exit codes here (usage 2 / env 3 / …) so they honor the exit-code
|
|
2285
2570
|
// contract instead of escaping to the top-level "Fatal" handler (exit 3).
|
|
@@ -2435,6 +2720,7 @@ SUITE (run a directory of natural-language tests as one gated suite)
|
|
|
2435
2720
|
|
|
2436
2721
|
SERVER (expose a locally-connected device to remote verikun clients)
|
|
2437
2722
|
server [--bind addr] [--port n] [--auth-key k] [--allow-install]
|
|
2723
|
+
[--allow-device-control[=names]]
|
|
2438
2724
|
[--allow-unsafe-anonymous] Serve THIS machine's device over HTTP+JSON for
|
|
2439
2725
|
\`vk ai/suite/install --server <url>\`. Only
|
|
2440
2726
|
verikun's validated action grammar is runnable
|
|
@@ -2446,16 +2732,31 @@ SERVER (expose a locally-connected device to remote verikun clients)
|
|
|
2446
2732
|
Env: VERIKUN_SERVER_AUTH_KEY (keeps it off argv).
|
|
2447
2733
|
Clients: pass --server <url> (or VERIKUN_SERVER) + --auth-key (or
|
|
2448
2734
|
VERIKUN_SERVER_AUTH_KEY) to ai/suite/install. The server's device+platform apply.
|
|
2735
|
+
--allow-device-control lets a client restart/stop the server's OWN device (for a
|
|
2736
|
+
device gone flaky mid-suite); --allow-device-control=<avd,sim> additionally lets it
|
|
2737
|
+
START one of those named targets — and, with it, ERASE the device (--wipe). With
|
|
2738
|
+
the flag the server also starts even when no device is attached, so a client can
|
|
2739
|
+
boot one: \`vk devices start|stop|restart [name] --server <url>\`, or add
|
|
2740
|
+
--ensure-device[=name] to ai/suite/install to boot once before the first step.
|
|
2449
2741
|
|
|
2450
2742
|
ENVIRONMENT
|
|
2451
|
-
devices [--json]
|
|
2452
|
-
already driving each (USED BY)
|
|
2743
|
+
devices [--all] [--json] List attached devices/simulators, and which job is
|
|
2744
|
+
already driving each (USED BY); --all also lists
|
|
2745
|
+
startable (not-yet-booted) AVDs/simulators
|
|
2746
|
+
devices start <name> [--wipe] Boot an AVD/simulator; prints its serial. Already
|
|
2747
|
+
[--timeout dur] [--no-wait] running = a no-op. --wipe erases its data first
|
|
2748
|
+
devices stop <name|serial> Shut a running emulator/simulator down
|
|
2749
|
+
devices restart <name> [--wipe] Stop then boot (use for a wedged/flaky device)
|
|
2453
2750
|
doctor [--fix] Diagnose adb/device, and warn if this CLI or the
|
|
2454
2751
|
Claude Code plugin is out of date (a warning only —
|
|
2455
2752
|
it never changes the exit code). --fix disables
|
|
2456
2753
|
animations. VERIKUN_NO_UPDATE_CHECK skips the check
|
|
2457
2754
|
companion <status|stop> [--json] On-device hierarchy reader (Android, on by default;
|
|
2458
2755
|
VERIKUN_COMPANION=0 opts out)
|
|
2756
|
+
Note: \`devices stop\` powers a DEVICE off; \`stop <appId>\` force-stops an APP, and
|
|
2757
|
+
\`device set\` (singular) changes settings on the device you are driving.
|
|
2758
|
+
Physical devices are never power-cycled. Env: VERIKUN_EMULATOR (path to the SDK's
|
|
2759
|
+
\`emulator\` binary, if it isn't on PATH or under \$ANDROID_HOME).
|
|
2459
2760
|
|
|
2460
2761
|
TEST RUNS (actions are recorded; a run auto-starts on first action)
|
|
2461
2762
|
run start [name] [--force] Begin a named run (else one starts implicitly)
|