visor-ai 0.2.7 → 0.2.9

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/cli.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import fs from 'node:fs';
2
2
  import { DEFAULT_SERVER_URL } from './adapters.js';
3
- import { DaemonRequestTimeoutError, DaemonUnavailableError, runDaemonAction, runDaemonScenario, startVisorDaemon, statusVisorDaemon, stopVisorDaemon } from './daemon.js';
3
+ import { DaemonOperationError, DaemonRequestTimeoutError, DaemonUnavailableError, runDaemonAction, runDaemonDiscover, runDaemonScenario, startVisorDaemon, statusVisorDaemon, stopVisorDaemon } from './daemon.js';
4
4
  import { DeviceSelectionError, resolveRunningDevice } from './devices.js';
5
5
  import { makeError } from './errors.js';
6
+ import { LocalRuntimeAdapter } from './localRuntime.js';
6
7
  import { writeReports } from './report.js';
7
- import { determinismCheck } from './runner.js';
8
+ import { determinismCheck, runScenario } from './runner.js';
8
9
  import { errorMessage, makeId, utcNowIso } from './utils.js';
9
10
  import { parseAndValidate } from './validator.js';
10
11
  function packageVersion() {
@@ -29,6 +30,7 @@ const ALL_COMMANDS = new Set([
29
30
  ...ACTION_COMMANDS,
30
31
  'validate',
31
32
  'run',
33
+ 'discover',
32
34
  'benchmark',
33
35
  'report',
34
36
  'start',
@@ -44,6 +46,8 @@ const GLOBAL_SPEC = {
44
46
  'server-url': 'string',
45
47
  'app-id': 'string',
46
48
  attach: 'boolean',
49
+ 'no-map': 'boolean',
50
+ repair: 'boolean',
47
51
  verbose: 'boolean'
48
52
  };
49
53
  const ACTION_SPEC = {
@@ -51,12 +55,17 @@ const ACTION_SPEC = {
51
55
  target: 'string',
52
56
  x: 'number',
53
57
  y: 'number',
58
+ 'start-x': 'number',
59
+ 'start-y': 'number',
60
+ 'end-x': 'number',
61
+ 'end-y': 'number',
54
62
  direction: 'string',
55
63
  percent: 'number',
56
64
  normalized: 'boolean',
57
65
  to: 'string',
58
66
  name: 'string',
59
67
  value: 'string',
68
+ 'start-value': 'number',
60
69
  label: 'string',
61
70
  ms: 'number',
62
71
  path: 'string'
@@ -68,9 +77,28 @@ const COMMAND_SPECS = {
68
77
  timeout: 'number',
69
78
  output: 'string',
70
79
  format: 'string',
80
+ runtime: 'string',
71
81
  'server-url': 'string',
72
82
  'app-id': 'string',
73
- attach: 'boolean'
83
+ attach: 'boolean',
84
+ 'no-map': 'boolean',
85
+ repair: 'boolean',
86
+ crawl: 'boolean',
87
+ 'crawl-depth': 'number',
88
+ 'crawl-limit': 'number'
89
+ },
90
+ discover: {
91
+ device: 'string',
92
+ timeout: 'number',
93
+ format: 'string',
94
+ 'server-url': 'string',
95
+ 'app-id': 'string',
96
+ attach: 'boolean',
97
+ 'no-map': 'boolean',
98
+ repair: 'boolean',
99
+ crawl: 'boolean',
100
+ 'crawl-depth': 'number',
101
+ 'crawl-limit': 'number'
74
102
  },
75
103
  benchmark: {
76
104
  runs: 'number',
@@ -81,7 +109,10 @@ const COMMAND_SPECS = {
81
109
  format: 'string',
82
110
  'server-url': 'string',
83
111
  'app-id': 'string',
84
- attach: 'boolean'
112
+ attach: 'boolean',
113
+ 'no-map': 'boolean',
114
+ repair: 'boolean',
115
+ 'compare-map': 'boolean'
85
116
  },
86
117
  report: { format: 'string' },
87
118
  start: {
@@ -106,6 +137,13 @@ const COMMAND_SPECS = {
106
137
  wait: ACTION_SPEC,
107
138
  source: ACTION_SPEC
108
139
  };
140
+ const ACTION_ARG_OPTION_NAMES = {
141
+ 'start-x': 'startX',
142
+ 'start-y': 'startY',
143
+ 'end-x': 'endX',
144
+ 'end-y': 'endY',
145
+ 'start-value': 'startValue'
146
+ };
109
147
  function helpText() {
110
148
  return [
111
149
  'Visor TypeScript CLI',
@@ -117,8 +155,9 @@ function helpText() {
117
155
  '',
118
156
  'Commands:',
119
157
  ' validate <scenario>',
120
- ' run <scenario> [--output <dir>]',
158
+ ' run <scenario> [--output <dir>] [--runtime appium|local]',
121
159
  ' benchmark <scenario> [--runs <n>] [--threshold <percent>]',
160
+ ' discover [--app-id <id>]',
122
161
  ' report [path]',
123
162
  ' start [--server-url <url>] [--appium-cmd <cmd>]',
124
163
  ' status [--server-url <url>]',
@@ -127,12 +166,125 @@ function helpText() {
127
166
  '',
128
167
  'Examples:',
129
168
  ' visor validate scenarios/checkout-smoke.json',
169
+ ' visor run path/to/scenario.json --runtime local --output artifacts-local',
130
170
  ' visor start --server-url http://127.0.0.1:4723',
131
171
  ' visor run scenarios/checkout-smoke.json --output artifacts-test',
172
+ ' visor run scenarios/checkout-smoke.json --no-map',
173
+ ' visor discover --app-id com.example.app',
132
174
  ' visor scroll --device emulator-5554 --direction down',
133
175
  ' visor status'
134
176
  ].join('\n');
135
177
  }
178
+ function commandHelpText(command) {
179
+ if (command === 'tap') {
180
+ const examples = [
181
+ 'visor tap --target accessibility=Continue',
182
+ 'visor tap --target text=Continue',
183
+ 'visor tap --target text~=Settings',
184
+ 'visor tap --target "first-in-section=Featured products"',
185
+ 'visor tap --x 120 --y 640',
186
+ 'visor tap --x 0.5 --y 0.92 --normalized'
187
+ ];
188
+ return {
189
+ usageText: [
190
+ 'Visor tap',
191
+ '',
192
+ 'Usage:',
193
+ ' visor tap --target <selector> [runtime options]',
194
+ ' visor tap --x <points> --y <points> [--normalized] [runtime options]',
195
+ '',
196
+ 'Options:',
197
+ ' --target <selector> Selector to tap',
198
+ ' --x <points> X coordinate in screen points, or fraction with --normalized',
199
+ ' --y <points> Y coordinate in screen points, or fraction with --normalized',
200
+ ' --normalized Treat x/y as fractions of current screen size',
201
+ ' --no-map Disable app-map reads and writes',
202
+ ' --repair Allow opt-in exploratory app-map repair'
203
+ ].join('\n'),
204
+ examples
205
+ };
206
+ }
207
+ if (command === 'discover') {
208
+ const examples = [
209
+ 'visor discover --app-id com.example.app',
210
+ 'visor discover --app-id com.example.app --crawl --crawl-depth 2 --crawl-limit 24'
211
+ ];
212
+ return {
213
+ usageText: [
214
+ 'Visor discover',
215
+ '',
216
+ 'Usage:',
217
+ ' visor discover [--app-id <id>] [--crawl] [runtime options]',
218
+ '',
219
+ 'Options:',
220
+ ' --crawl Explore safe controls and record app-map edges',
221
+ ' --crawl-depth <n> Maximum crawl depth, default 2',
222
+ ' --crawl-limit <n> Maximum crawl actions, default 24',
223
+ ' --no-map Disable app-map reads and writes'
224
+ ].join('\n'),
225
+ examples
226
+ };
227
+ }
228
+ if (command === 'act') {
229
+ const examples = [
230
+ 'visor act --name back',
231
+ 'visor act --name reset --app-id com.example.app',
232
+ 'visor act --name drag --start-x 120 --start-y 640 --end-x 320 --end-y 640',
233
+ 'visor act --name slider --target accessibility=Amount --value 0.5'
234
+ ];
235
+ return {
236
+ usageText: [
237
+ 'Visor act',
238
+ '',
239
+ 'Usage:',
240
+ ' visor act --name <type|back|home|reset|drag|slider> [options]',
241
+ '',
242
+ 'Options:',
243
+ ' --name <operation> Helper action name',
244
+ ' --target <selector> Target selector for type or slider',
245
+ ' --value <value> Text value for type, or 0..1 slider value',
246
+ ' --start-x <points> Drag start x coordinate',
247
+ ' --start-y <points> Drag start y coordinate',
248
+ ' --end-x <points> Drag end x coordinate',
249
+ ' --end-y <points> Drag end y coordinate',
250
+ ' --start-value <0..1> Slider starting value, default 0.5',
251
+ ' --normalized Treat drag coordinates as viewport fractions'
252
+ ].join('\n'),
253
+ examples
254
+ };
255
+ }
256
+ if (command === 'benchmark') {
257
+ const examples = [
258
+ 'visor benchmark scenarios/checkout-smoke.json --runs 20',
259
+ 'visor benchmark scenarios/checkout-smoke.json --runs 5 --compare-map'
260
+ ];
261
+ return {
262
+ usageText: [
263
+ 'Visor benchmark',
264
+ '',
265
+ 'Usage:',
266
+ ' visor benchmark <scenario> [--runs <n>] [--threshold <percent>] [--compare-map]',
267
+ '',
268
+ 'Options:',
269
+ ' --runs <n> Number of runs, default 20',
270
+ ' --threshold <percent> Required determinism score, default 95',
271
+ ' --compare-map Run no-map and app-map variants for A/B comparison'
272
+ ].join('\n'),
273
+ examples
274
+ };
275
+ }
276
+ return {
277
+ usageText: [
278
+ `Visor ${command}`,
279
+ '',
280
+ 'Usage:',
281
+ ` visor ${command} [options]`,
282
+ '',
283
+ 'Run visor --help for the full command list.'
284
+ ].join('\n'),
285
+ examples: []
286
+ };
287
+ }
136
288
  function envelopeOk(commandId, startedAt, artifacts = [], nextAction = '') {
137
289
  return {
138
290
  status: 'ok',
@@ -216,7 +368,14 @@ async function resolvedRuntime(options) {
216
368
  output_dir: String(options.output ?? 'artifacts'),
217
369
  server_url: String(options['server-url'] ?? DEFAULT_SERVER_URL),
218
370
  app_id: typeof options['app-id'] === 'string' ? options['app-id'] : undefined,
219
- attach_to_running: Boolean(options.attach)
371
+ attach_to_running: Boolean(options.attach),
372
+ map: {
373
+ enabled: !Boolean(options['no-map']),
374
+ ...(options.repair === true ? { repair: true } : {}),
375
+ ...(options.crawl === true ? { crawl: true } : {}),
376
+ ...(typeof options['crawl-depth'] === 'number' ? { crawlDepth: options['crawl-depth'] } : {}),
377
+ ...(typeof options['crawl-limit'] === 'number' ? { crawlLimit: options['crawl-limit'] } : {})
378
+ }
220
379
  };
221
380
  }
222
381
  function actionArgs(command, options) {
@@ -228,12 +387,15 @@ function actionArgs(command, options) {
228
387
  'verbose',
229
388
  'server-url',
230
389
  'seed',
390
+ 'runtime',
231
391
  'app-id',
232
- 'attach'
392
+ 'attach',
393
+ 'no-map',
394
+ 'repair'
233
395
  ]);
234
396
  const args = Object.entries(options).reduce((acc, [key, value]) => {
235
397
  if (!commonIgnored.has(key) && value !== undefined) {
236
- acc[key] = value;
398
+ acc[ACTION_ARG_OPTION_NAMES[key] ?? key] = value;
237
399
  }
238
400
  return acc;
239
401
  }, {});
@@ -242,6 +404,10 @@ function actionArgs(command, options) {
242
404
  }
243
405
  return args;
244
406
  }
407
+ function unsupportedRuntimeResult(commandId, startedAt, runtime) {
408
+ const response = envelopeFail(commandId, startedAt, 'INPUT_ERROR', 'Unsupported runtime', `Unsupported runtime '${String(runtime)}'`, 'Use --runtime appium or --runtime local');
409
+ return { code: 1, response };
410
+ }
245
411
  function isTargetInitializationError(error) {
246
412
  const message = errorMessage(error);
247
413
  return (message.includes('Failed to create WebdriverIO Appium session') ||
@@ -261,15 +427,80 @@ function targetInitializationNextStep(error) {
261
427
  }
262
428
  return 'Verify Appium driver setup, target device state, and app id, then retry.';
263
429
  }
264
- function cmdHelp() {
430
+ function isRecord(value) {
431
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
432
+ }
433
+ function optionalString(value) {
434
+ return typeof value === 'string' ? value : undefined;
435
+ }
436
+ function mapArray(value) {
437
+ return Array.isArray(value) ? value : [];
438
+ }
439
+ function mapActionCount(variants) {
440
+ let total = 0;
441
+ for (const variant of variants) {
442
+ if (!isRecord(variant)) {
443
+ continue;
444
+ }
445
+ total += mapArray(variant.actions).length;
446
+ }
447
+ return total;
448
+ }
449
+ function appMapSummaryFromPath(mapPath) {
450
+ try {
451
+ const parsed = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
452
+ if (!isRecord(parsed)) {
453
+ return undefined;
454
+ }
455
+ const variants = mapArray(parsed.variants);
456
+ return {
457
+ schema_version: typeof parsed.schema_version === 'number' ? parsed.schema_version : undefined,
458
+ identity: optionalString(parsed.identity),
459
+ app_id: optionalString(parsed.app_id),
460
+ platform: optionalString(parsed.platform),
461
+ screens: mapArray(parsed.screens).length,
462
+ variants: variants.length,
463
+ edges: mapArray(parsed.edges).length,
464
+ actions: mapActionCount(variants),
465
+ auth_required_variants: variants.filter((variant) => isRecord(variant) && variant.auth_required === true).length,
466
+ updated_at: optionalString(parsed.updated_at)
467
+ };
468
+ }
469
+ catch {
470
+ return undefined;
471
+ }
472
+ }
473
+ function addDiscoverMapSummary(result) {
474
+ if (!isRecord(result.map)) {
475
+ return result;
476
+ }
477
+ const map = result.map;
478
+ if (map.summary !== undefined || typeof map.path !== 'string') {
479
+ return result;
480
+ }
481
+ const summary = appMapSummaryFromPath(map.path);
482
+ if (!summary) {
483
+ return result;
484
+ }
485
+ return {
486
+ ...result,
487
+ map: {
488
+ ...map,
489
+ summary
490
+ }
491
+ };
492
+ }
493
+ function cmdHelp(command) {
265
494
  const commandId = makeId('cmd');
266
495
  const startedAt = utcNowIso();
267
496
  const response = envelopeOk(commandId, startedAt, [], 'validate');
497
+ const commandHelp = command ? commandHelpText(command) : undefined;
268
498
  response.data = {
269
- usageText: helpText(),
270
- commands: Array.from(ALL_COMMANDS),
271
- examples: [
499
+ usageText: commandHelp?.usageText ?? helpText(),
500
+ commands: command ? [command] : Array.from(ALL_COMMANDS),
501
+ examples: commandHelp?.examples ?? [
272
502
  'visor validate scenarios/checkout-smoke.json',
503
+ 'visor run path/to/scenario.json --runtime local --output artifacts-local',
273
504
  'visor start --server-url http://127.0.0.1:4723',
274
505
  'visor run scenarios/checkout-smoke.json --output artifacts-test',
275
506
  'visor scroll --device emulator-5554 --direction down',
@@ -313,6 +544,19 @@ export async function cmdValidate(parsed) {
313
544
  return { code: 1, response };
314
545
  }
315
546
  }
547
+ async function cmdRunLocal(commandId, startedAt, scenario, warnings, runtime) {
548
+ const result = await runScenario(scenario, new LocalRuntimeAdapter(), runtime.device, runtime.timeout, runtime.output_dir);
549
+ const outputs = writeReports(result, runtime.output_dir);
550
+ const response = result.status === 'ok'
551
+ ? envelopeOk(commandId, startedAt, Object.values(outputs), 'report')
552
+ : envelopeFail(commandId, startedAt, result.error?.code ?? 'ACTION_ERROR', result.error?.message ?? 'Local runtime scenario failed', result.error?.likely_cause ?? 'The deterministic local runtime did not satisfy the scenario', result.error?.next_step ?? 'Inspect local runtime scenario artifacts and retry');
553
+ response.artifacts = Object.values(outputs);
554
+ response.data = {
555
+ run: result,
556
+ warnings
557
+ };
558
+ return { code: result.status === 'ok' ? 0 : 2, response };
559
+ }
316
560
  export async function cmdRun(parsed) {
317
561
  const commandId = makeId('cmd');
318
562
  const startedAt = utcNowIso();
@@ -323,6 +567,24 @@ export async function cmdRun(parsed) {
323
567
  response.data = { issues };
324
568
  return { code: 1, response };
325
569
  }
570
+ if (parsed.options.runtime !== undefined &&
571
+ parsed.options.runtime !== 'appium' &&
572
+ parsed.options.runtime !== 'local') {
573
+ return unsupportedRuntimeResult(commandId, startedAt, parsed.options.runtime);
574
+ }
575
+ if (parsed.options.runtime === 'local') {
576
+ const runtime = {
577
+ device: 'local',
578
+ timeout: typeof parsed.options.timeout === 'number'
579
+ ? parsed.options.timeout
580
+ : typeof scenario.config.timeoutMs === 'number'
581
+ ? scenario.config.timeoutMs
582
+ : undefined,
583
+ output_dir: String(parsed.options.output ??
584
+ (typeof scenario.config.artifactsDir === 'string' ? scenario.config.artifactsDir : 'artifacts'))
585
+ };
586
+ return cmdRunLocal(commandId, startedAt, scenario, warningIssues(issues), runtime);
587
+ }
326
588
  let runtime;
327
589
  try {
328
590
  runtime = await resolvedRuntime({
@@ -351,7 +613,10 @@ export async function cmdRun(parsed) {
351
613
  device: runtime.device,
352
614
  app_id: runtime.app_id,
353
615
  attach_to_running: runtime.attach_to_running
354
- }, scenario, runtime.device, runtime.timeout, runtime.output_dir);
616
+ }, scenario, runtime.device, runtime.timeout, runtime.output_dir, {
617
+ ...runtime.map,
618
+ appId: runtime.app_id
619
+ });
355
620
  const outputs = writeReports(result, runtime.output_dir);
356
621
  if (result.status === 'fail' && result.error) {
357
622
  const response = envelopeFail(commandId, startedAt, result.error.code, result.error.message, result.error.likely_cause, result.error.next_step);
@@ -384,6 +649,51 @@ export async function cmdRun(parsed) {
384
649
  return { code: 1, response };
385
650
  }
386
651
  }
652
+ export async function cmdDiscover(parsed) {
653
+ const commandId = makeId('cmd');
654
+ const startedAt = utcNowIso();
655
+ let runtime;
656
+ try {
657
+ runtime = await resolvedRuntime(parsed.options);
658
+ }
659
+ catch (error) {
660
+ if (!(error instanceof DeviceSelectionError)) {
661
+ throw error;
662
+ }
663
+ const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Device selection failed', error.message, 'Start one target device or rerun with --device <device-id>');
664
+ response.data = { devices: error.devices };
665
+ return { code: 1, response };
666
+ }
667
+ try {
668
+ const result = await runDaemonDiscover({
669
+ platform: runtime.platform,
670
+ server_url: runtime.server_url,
671
+ device: runtime.device,
672
+ app_id: runtime.app_id,
673
+ attach_to_running: runtime.attach_to_running
674
+ }, {
675
+ ...runtime.map,
676
+ appId: runtime.app_id
677
+ });
678
+ const response = envelopeOk(commandId, startedAt, [], 'run');
679
+ response.data = addDiscoverMapSummary(result);
680
+ return { code: 0, response };
681
+ }
682
+ catch (error) {
683
+ const isDaemonUnavailable = error instanceof DaemonUnavailableError;
684
+ const isDaemonTimeout = error instanceof DaemonRequestTimeoutError;
685
+ const response = envelopeFail(commandId, startedAt, isDaemonUnavailable || isDaemonTimeout ? 'TARGET_ERROR' : 'ACTION_ERROR', isDaemonUnavailable
686
+ ? 'discover requires visor start'
687
+ : isDaemonTimeout
688
+ ? 'discover timed out waiting for Visor daemon'
689
+ : 'discover failed', errorMessage(error), isDaemonUnavailable
690
+ ? 'Run `visor start`, verify the target emulator/simulator is booted, and retry.'
691
+ : isDaemonTimeout
692
+ ? 'Inspect .visor/daemon/daemon.log and .visor/appium/*.log, then retry or run `visor stop --force` before restarting.'
693
+ : 'Check target app state and retry');
694
+ return { code: 1, response };
695
+ }
696
+ }
387
697
  export async function cmdBenchmark(parsed) {
388
698
  const commandId = makeId('cmd');
389
699
  const startedAt = utcNowIso();
@@ -417,39 +727,97 @@ export async function cmdBenchmark(parsed) {
417
727
  }
418
728
  const runs = typeof parsed.options.runs === 'number' ? parsed.options.runs : 20;
419
729
  const threshold = typeof parsed.options.threshold === 'number' ? parsed.options.threshold : 95;
420
- const signatures = [];
421
- const runIds = [];
422
- let failures = 0;
423
- for (let index = 0; index < runs; index += 1) {
424
- try {
425
- const result = await runDaemonScenario({
426
- platform: runtime.platform,
427
- server_url: runtime.server_url,
428
- device: runtime.device,
429
- app_id: runtime.app_id,
430
- attach_to_running: runtime.attach_to_running
431
- }, scenario, runtime.device, runtime.timeout, runtime.output_dir);
432
- writeReports(result, runtime.output_dir);
433
- signatures.push(result.determinism_signature);
434
- runIds.push(result.run_id);
435
- if (result.status !== 'ok') {
730
+ const runtimeRequest = {
731
+ platform: runtime.platform,
732
+ server_url: runtime.server_url,
733
+ device: runtime.device,
734
+ app_id: runtime.app_id,
735
+ attach_to_running: runtime.attach_to_running
736
+ };
737
+ const runVariant = async (name, mapEnabled, mapOptions) => {
738
+ const signatures = [];
739
+ const runIds = [];
740
+ let failures = 0;
741
+ for (let index = 0; index < runs; index += 1) {
742
+ try {
743
+ const result = await runDaemonScenario(runtimeRequest, scenario, runtime.device, runtime.timeout, runtime.output_dir, mapOptions);
744
+ writeReports(result, runtime.output_dir);
745
+ signatures.push(result.determinism_signature);
746
+ runIds.push(result.run_id);
747
+ if (result.status !== 'ok') {
748
+ failures += 1;
749
+ }
750
+ }
751
+ catch (error) {
752
+ if (error instanceof DaemonUnavailableError) {
753
+ const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Benchmark requires visor start', errorMessage(error), 'Run `visor start`, verify the target emulator/simulator is booted, and retry.');
754
+ return { error: { code: 1, response } };
755
+ }
756
+ if (error instanceof DaemonRequestTimeoutError) {
757
+ const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Timed out waiting for Visor daemon', errorMessage(error), 'Inspect .visor/daemon/daemon.log and .visor/appium/*.log, then retry or run `visor stop --force` before restarting.');
758
+ return { error: { code: 1, response } };
759
+ }
436
760
  failures += 1;
437
761
  }
438
762
  }
439
- catch (error) {
440
- if (error instanceof DaemonUnavailableError) {
441
- const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Benchmark requires visor start', errorMessage(error), 'Run `visor start`, verify the target emulator/simulator is booted, and retry.');
442
- return { code: 1, response };
763
+ const score = determinismCheck(signatures);
764
+ return {
765
+ variant: {
766
+ name,
767
+ mapEnabled,
768
+ runs,
769
+ threshold,
770
+ determinismScore: score,
771
+ pass: score >= threshold && failures === 0,
772
+ failures,
773
+ runIds
443
774
  }
444
- if (error instanceof DaemonRequestTimeoutError) {
445
- const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Timed out waiting for Visor daemon', errorMessage(error), 'Inspect .visor/daemon/daemon.log and .visor/appium/*.log, then retry or run `visor stop --force` before restarting.');
446
- return { code: 1, response };
775
+ };
776
+ };
777
+ if (parsed.options['compare-map'] === true) {
778
+ const variants = [];
779
+ for (const variantConfig of [
780
+ { name: 'no-map', mapEnabled: false },
781
+ { name: 'map', mapEnabled: true }
782
+ ]) {
783
+ const outcome = await runVariant(variantConfig.name, variantConfig.mapEnabled, {
784
+ ...runtime.map,
785
+ enabled: variantConfig.mapEnabled,
786
+ appId: runtime.app_id
787
+ });
788
+ if ('error' in outcome) {
789
+ return outcome.error;
447
790
  }
448
- failures += 1;
791
+ variants.push(outcome.variant);
449
792
  }
450
- }
451
- const score = determinismCheck(signatures);
452
- const passGate = score >= threshold && failures === 0;
793
+ const failures = variants.reduce((total, variant) => total + Number(variant.failures ?? 0), 0);
794
+ const score = Math.min(...variants.map((variant) => Number(variant.determinismScore ?? 0)));
795
+ const passGate = variants.every((variant) => variant.pass === true);
796
+ const runIds = variants.flatMap((variant) => Array.isArray(variant.runIds) ? variant.runIds : []);
797
+ const response = envelopeOk(commandId, startedAt, [], 'report');
798
+ response.data = {
799
+ runs,
800
+ threshold,
801
+ determinismScore: score,
802
+ pass: passGate,
803
+ failures,
804
+ runIds,
805
+ variants,
806
+ warnings: warningIssues(issues)
807
+ };
808
+ return { code: passGate ? 0 : 3, response };
809
+ }
810
+ const outcome = await runVariant('benchmark', runtime.map.enabled !== false, {
811
+ ...runtime.map,
812
+ appId: runtime.app_id
813
+ });
814
+ if ('error' in outcome) {
815
+ return outcome.error;
816
+ }
817
+ const score = Number(outcome.variant.determinismScore ?? 0);
818
+ const failures = Number(outcome.variant.failures ?? 0);
819
+ const passGate = outcome.variant.pass === true;
820
+ const runIds = Array.isArray(outcome.variant.runIds) ? outcome.variant.runIds : [];
453
821
  const response = envelopeOk(commandId, startedAt, [], 'report');
454
822
  response.data = {
455
823
  runs,
@@ -499,7 +867,10 @@ export async function cmdAction(command, parsed) {
499
867
  device: options.device,
500
868
  app_id: options.app_id,
501
869
  attach_to_running: options.attach_to_running
502
- }, command, actionArgs(command, parsed.options));
870
+ }, command, actionArgs(command, parsed.options), {
871
+ ...options.map,
872
+ appId: options.app_id
873
+ });
503
874
  const actionPayload = payload.args;
504
875
  if (actionPayload && typeof actionPayload === 'object' && !Array.isArray(actionPayload)) {
505
876
  const maybePath = actionPayload.path;
@@ -509,6 +880,9 @@ export async function cmdAction(command, parsed) {
509
880
  }
510
881
  }
511
882
  catch (error) {
883
+ if (error instanceof DaemonOperationError && error.data) {
884
+ payload = error.data;
885
+ }
512
886
  actionError = error;
513
887
  }
514
888
  if (actionError) {
@@ -573,10 +947,12 @@ export async function cmdStop(parsed) {
573
947
  }
574
948
  }
575
949
  export async function executeCommand(argv) {
576
- if (argv.length === 0 ||
577
- argv[0] === 'help' ||
578
- argv.includes('--help') ||
579
- argv.includes('-h')) {
950
+ const requestedHelp = argv[0] === 'help' || argv.includes('--help') || argv.includes('-h');
951
+ if (requestedHelp) {
952
+ const helpCommand = argv.find((token) => ALL_COMMANDS.has(token));
953
+ return cmdHelp(helpCommand);
954
+ }
955
+ if (argv.length === 0) {
580
956
  return cmdHelp();
581
957
  }
582
958
  if (argv.includes('--version') || argv.includes('-v')) {
@@ -589,6 +965,9 @@ export async function executeCommand(argv) {
589
965
  if (parsed.command === 'run') {
590
966
  return cmdRun(parsed);
591
967
  }
968
+ if (parsed.command === 'discover') {
969
+ return cmdDiscover(parsed);
970
+ }
592
971
  if (parsed.command === 'benchmark') {
593
972
  return cmdBenchmark(parsed);
594
973
  }