visor-ai 0.2.8 → 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
@@ -47,6 +47,7 @@ const GLOBAL_SPEC = {
47
47
  'app-id': 'string',
48
48
  attach: 'boolean',
49
49
  'no-map': 'boolean',
50
+ repair: 'boolean',
50
51
  verbose: 'boolean'
51
52
  };
52
53
  const ACTION_SPEC = {
@@ -54,12 +55,17 @@ const ACTION_SPEC = {
54
55
  target: 'string',
55
56
  x: 'number',
56
57
  y: 'number',
58
+ 'start-x': 'number',
59
+ 'start-y': 'number',
60
+ 'end-x': 'number',
61
+ 'end-y': 'number',
57
62
  direction: 'string',
58
63
  percent: 'number',
59
64
  normalized: 'boolean',
60
65
  to: 'string',
61
66
  name: 'string',
62
67
  value: 'string',
68
+ 'start-value': 'number',
63
69
  label: 'string',
64
70
  ms: 'number',
65
71
  path: 'string'
@@ -75,7 +81,11 @@ const COMMAND_SPECS = {
75
81
  'server-url': 'string',
76
82
  'app-id': 'string',
77
83
  attach: 'boolean',
78
- 'no-map': 'boolean'
84
+ 'no-map': 'boolean',
85
+ repair: 'boolean',
86
+ crawl: 'boolean',
87
+ 'crawl-depth': 'number',
88
+ 'crawl-limit': 'number'
79
89
  },
80
90
  discover: {
81
91
  device: 'string',
@@ -84,7 +94,11 @@ const COMMAND_SPECS = {
84
94
  'server-url': 'string',
85
95
  'app-id': 'string',
86
96
  attach: 'boolean',
87
- 'no-map': 'boolean'
97
+ 'no-map': 'boolean',
98
+ repair: 'boolean',
99
+ crawl: 'boolean',
100
+ 'crawl-depth': 'number',
101
+ 'crawl-limit': 'number'
88
102
  },
89
103
  benchmark: {
90
104
  runs: 'number',
@@ -96,7 +110,9 @@ const COMMAND_SPECS = {
96
110
  'server-url': 'string',
97
111
  'app-id': 'string',
98
112
  attach: 'boolean',
99
- 'no-map': 'boolean'
113
+ 'no-map': 'boolean',
114
+ repair: 'boolean',
115
+ 'compare-map': 'boolean'
100
116
  },
101
117
  report: { format: 'string' },
102
118
  start: {
@@ -121,6 +137,13 @@ const COMMAND_SPECS = {
121
137
  wait: ACTION_SPEC,
122
138
  source: ACTION_SPEC
123
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
+ };
124
147
  function helpText() {
125
148
  return [
126
149
  'Visor TypeScript CLI',
@@ -152,6 +175,116 @@ function helpText() {
152
175
  ' visor status'
153
176
  ].join('\n');
154
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
+ }
155
288
  function envelopeOk(commandId, startedAt, artifacts = [], nextAction = '') {
156
289
  return {
157
290
  status: 'ok',
@@ -237,7 +370,11 @@ async function resolvedRuntime(options) {
237
370
  app_id: typeof options['app-id'] === 'string' ? options['app-id'] : undefined,
238
371
  attach_to_running: Boolean(options.attach),
239
372
  map: {
240
- enabled: !Boolean(options['no-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'] } : {})
241
378
  }
242
379
  };
243
380
  }
@@ -253,11 +390,12 @@ function actionArgs(command, options) {
253
390
  'runtime',
254
391
  'app-id',
255
392
  'attach',
256
- 'no-map'
393
+ 'no-map',
394
+ 'repair'
257
395
  ]);
258
396
  const args = Object.entries(options).reduce((acc, [key, value]) => {
259
397
  if (!commonIgnored.has(key) && value !== undefined) {
260
- acc[key] = value;
398
+ acc[ACTION_ARG_OPTION_NAMES[key] ?? key] = value;
261
399
  }
262
400
  return acc;
263
401
  }, {});
@@ -289,14 +427,78 @@ function targetInitializationNextStep(error) {
289
427
  }
290
428
  return 'Verify Appium driver setup, target device state, and app id, then retry.';
291
429
  }
292
- 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) {
293
494
  const commandId = makeId('cmd');
294
495
  const startedAt = utcNowIso();
295
496
  const response = envelopeOk(commandId, startedAt, [], 'validate');
497
+ const commandHelp = command ? commandHelpText(command) : undefined;
296
498
  response.data = {
297
- usageText: helpText(),
298
- commands: Array.from(ALL_COMMANDS),
299
- examples: [
499
+ usageText: commandHelp?.usageText ?? helpText(),
500
+ commands: command ? [command] : Array.from(ALL_COMMANDS),
501
+ examples: commandHelp?.examples ?? [
300
502
  'visor validate scenarios/checkout-smoke.json',
301
503
  'visor run path/to/scenario.json --runtime local --output artifacts-local',
302
504
  'visor start --server-url http://127.0.0.1:4723',
@@ -474,7 +676,7 @@ export async function cmdDiscover(parsed) {
474
676
  appId: runtime.app_id
475
677
  });
476
678
  const response = envelopeOk(commandId, startedAt, [], 'run');
477
- response.data = result;
679
+ response.data = addDiscoverMapSummary(result);
478
680
  return { code: 0, response };
479
681
  }
480
682
  catch (error) {
@@ -525,42 +727,97 @@ export async function cmdBenchmark(parsed) {
525
727
  }
526
728
  const runs = typeof parsed.options.runs === 'number' ? parsed.options.runs : 20;
527
729
  const threshold = typeof parsed.options.threshold === 'number' ? parsed.options.threshold : 95;
528
- const signatures = [];
529
- const runIds = [];
530
- let failures = 0;
531
- for (let index = 0; index < runs; index += 1) {
532
- try {
533
- const result = await runDaemonScenario({
534
- platform: runtime.platform,
535
- server_url: runtime.server_url,
536
- device: runtime.device,
537
- app_id: runtime.app_id,
538
- attach_to_running: runtime.attach_to_running
539
- }, scenario, runtime.device, runtime.timeout, runtime.output_dir, {
540
- ...runtime.map,
541
- appId: runtime.app_id
542
- });
543
- writeReports(result, runtime.output_dir);
544
- signatures.push(result.determinism_signature);
545
- runIds.push(result.run_id);
546
- 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
+ }
547
760
  failures += 1;
548
761
  }
549
762
  }
550
- catch (error) {
551
- if (error instanceof DaemonUnavailableError) {
552
- 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.');
553
- 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
554
774
  }
555
- if (error instanceof DaemonRequestTimeoutError) {
556
- 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.');
557
- 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;
558
790
  }
559
- failures += 1;
791
+ variants.push(outcome.variant);
560
792
  }
561
- }
562
- const score = determinismCheck(signatures);
563
- 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 : [];
564
821
  const response = envelopeOk(commandId, startedAt, [], 'report');
565
822
  response.data = {
566
823
  runs,
@@ -690,10 +947,12 @@ export async function cmdStop(parsed) {
690
947
  }
691
948
  }
692
949
  export async function executeCommand(argv) {
693
- if (argv.length === 0 ||
694
- argv[0] === 'help' ||
695
- argv.includes('--help') ||
696
- 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) {
697
956
  return cmdHelp();
698
957
  }
699
958
  if (argv.includes('--version') || argv.includes('-v')) {
@@ -38,6 +38,14 @@ export class LocalRuntimeAdapter {
38
38
  };
39
39
  }
40
40
  async act(args) {
41
+ const name = String(args.name ?? '');
42
+ if (name === 'reset') {
43
+ this.counter = 0;
44
+ this.route = 'app://home';
45
+ }
46
+ if (name === 'home') {
47
+ this.route = 'app://home';
48
+ }
41
49
  return {
42
50
  action: 'act',
43
51
  platform: 'android',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "visor-ai",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Visor CLI for LLM-driven mobile app interaction and artifact capture",
5
5
  "type": "module",
6
6
  "license": "MIT",