visor-ai 0.2.5 → 0.2.7

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,21 @@
1
- import { DEFAULT_SERVER_URL, getAdapter } from './adapters.js';
2
- import { DEFAULT_STARTUP_TIMEOUT_SECONDS, isAppiumReachable, startManagedAppium, statusManagedAppium, stopManagedAppium } from './appiumLifecycle.js';
1
+ import fs from 'node:fs';
2
+ import { DEFAULT_SERVER_URL } from './adapters.js';
3
+ import { DaemonRequestTimeoutError, DaemonUnavailableError, runDaemonAction, runDaemonScenario, startVisorDaemon, statusVisorDaemon, stopVisorDaemon } from './daemon.js';
4
+ import { DeviceSelectionError, resolveRunningDevice } from './devices.js';
3
5
  import { makeError } from './errors.js';
4
6
  import { writeReports } from './report.js';
5
- import { determinismCheck, runScenario } from './runner.js';
7
+ import { determinismCheck } from './runner.js';
6
8
  import { errorMessage, makeId, utcNowIso } from './utils.js';
7
9
  import { parseAndValidate } from './validator.js';
10
+ function packageVersion() {
11
+ try {
12
+ const packageJson = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
13
+ return typeof packageJson.version === 'string' ? packageJson.version : 'unknown';
14
+ }
15
+ catch {
16
+ return 'unknown';
17
+ }
18
+ }
8
19
  const ACTION_COMMANDS = new Set([
9
20
  'tap',
10
21
  'navigate',
@@ -25,7 +36,6 @@ const ALL_COMMANDS = new Set([
25
36
  'stop'
26
37
  ]);
27
38
  const GLOBAL_SPEC = {
28
- platform: 'string',
29
39
  device: 'string',
30
40
  timeout: 'number',
31
41
  output: 'string',
@@ -33,11 +43,7 @@ const GLOBAL_SPEC = {
33
43
  seed: 'number',
34
44
  'server-url': 'string',
35
45
  'app-id': 'string',
36
- 'appium-cmd': 'string',
37
- 'startup-timeout': 'number',
38
- 'no-auto-start-appium': 'boolean',
39
46
  attach: 'boolean',
40
- mock: 'boolean',
41
47
  verbose: 'boolean'
42
48
  };
43
49
  const ACTION_SPEC = {
@@ -58,40 +64,29 @@ const ACTION_SPEC = {
58
64
  const COMMAND_SPECS = {
59
65
  validate: { format: 'string' },
60
66
  run: {
61
- platform: 'string',
62
67
  device: 'string',
63
68
  timeout: 'number',
64
69
  output: 'string',
65
70
  format: 'string',
66
71
  'server-url': 'string',
67
72
  'app-id': 'string',
68
- 'appium-cmd': 'string',
69
- 'startup-timeout': 'number',
70
- 'no-auto-start-appium': 'boolean',
71
- attach: 'boolean',
72
- mock: 'boolean'
73
+ attach: 'boolean'
73
74
  },
74
75
  benchmark: {
75
76
  runs: 'number',
76
77
  threshold: 'number',
77
- platform: 'string',
78
78
  device: 'string',
79
79
  timeout: 'number',
80
80
  output: 'string',
81
81
  format: 'string',
82
82
  'server-url': 'string',
83
83
  'app-id': 'string',
84
- 'appium-cmd': 'string',
85
- 'startup-timeout': 'number',
86
- 'no-auto-start-appium': 'boolean',
87
- attach: 'boolean',
88
- mock: 'boolean'
84
+ attach: 'boolean'
89
85
  },
90
86
  report: { format: 'string' },
91
87
  start: {
92
88
  'server-url': 'string',
93
89
  'appium-cmd': 'string',
94
- 'startup-timeout': 'number',
95
90
  format: 'string'
96
91
  },
97
92
  status: {
@@ -117,23 +112,25 @@ function helpText() {
117
112
  '',
118
113
  'Usage:',
119
114
  ' visor <command> [options]',
120
- ' visor --help',
115
+ ' visor --help | -h',
116
+ ' visor --version | -v',
121
117
  '',
122
118
  'Commands:',
123
119
  ' validate <scenario>',
124
- ' run <scenario> [--mock] [--output <dir>]',
120
+ ' run <scenario> [--output <dir>]',
125
121
  ' benchmark <scenario> [--runs <n>] [--threshold <percent>]',
126
122
  ' report [path]',
127
- ' start [--server-url <url>]',
123
+ ' start [--server-url <url>] [--appium-cmd <cmd>]',
128
124
  ' status [--server-url <url>]',
129
125
  ' stop [--server-url <url>] [--force]',
130
126
  ' tap|navigate|act|scroll|screenshot|wait|source',
131
127
  '',
132
128
  'Examples:',
133
129
  ' visor validate scenarios/checkout-smoke.json',
134
- ' visor run scenarios/checkout-smoke.json --mock --output artifacts-test',
135
- ' visor scroll --platform android --mock --direction down',
136
- ' node dist/main.js status'
130
+ ' visor start --server-url http://127.0.0.1:4723',
131
+ ' visor run scenarios/checkout-smoke.json --output artifacts-test',
132
+ ' visor scroll --device emulator-5554 --direction down',
133
+ ' visor status'
137
134
  ].join('\n');
138
135
  }
139
136
  function envelopeOk(commandId, startedAt, artifacts = [], nextAction = '') {
@@ -208,69 +205,31 @@ function parseCommand(argv) {
208
205
  function warningIssues(issues) {
209
206
  return issues.filter((issue) => issue.severity === 'warning');
210
207
  }
211
- function resolvedRuntime(options, scenario) {
212
- const platform = String(options.platform ?? scenario.meta.platform);
213
- const defaultDevice = platform === 'android' ? 'emulator-5554' : 'iPhone 17 Pro';
214
- scenario.meta.platform = platform;
208
+ async function resolvedRuntime(options) {
209
+ const selectedDevice = await resolveRunningDevice(typeof options.device === 'string' ? options.device : undefined);
215
210
  return {
216
- platform,
217
- device: String(options.device ?? defaultDevice),
211
+ platform: selectedDevice.platform,
212
+ device: selectedDevice.id,
218
213
  timeout: typeof options.timeout === 'number'
219
214
  ? options.timeout
220
- : typeof scenario.config.timeoutMs === 'number'
221
- ? scenario.config.timeoutMs
222
- : 2500,
223
- output_dir: String(options.output ?? scenario.config.artifactsDir ?? 'artifacts'),
215
+ : 2500,
216
+ output_dir: String(options.output ?? 'artifacts'),
224
217
  server_url: String(options['server-url'] ?? DEFAULT_SERVER_URL),
225
- use_mock: Boolean(options.mock),
226
218
  app_id: typeof options['app-id'] === 'string' ? options['app-id'] : undefined,
227
- attach_to_running: Boolean(options.attach),
228
- auto_start_appium: !Boolean(options['no-auto-start-appium']),
229
- appium_cmd: typeof options['appium-cmd'] === 'string' ? options['appium-cmd'] : undefined,
230
- startup_timeout: typeof options['startup-timeout'] === 'number'
231
- ? options['startup-timeout']
232
- : DEFAULT_STARTUP_TIMEOUT_SECONDS
219
+ attach_to_running: Boolean(options.attach)
233
220
  };
234
221
  }
235
- async function ensureNonMockRuntime(options) {
236
- if (await isAppiumReachable(options.server_url, 2000)) {
237
- return {
238
- serverUrl: options.server_url,
239
- started: false
240
- };
241
- }
242
- if (!options.auto_start_appium) {
243
- throw new Error(`Cannot reach Appium server at ${options.server_url}. Start Appium and ensure ${options.platform} target '${options.device}' is booted.`);
244
- }
245
- return startManagedAppium(options.server_url, options.appium_cmd, options.startup_timeout);
246
- }
247
- async function stopAutoStartedAppium(runtimeState) {
248
- if (!runtimeState || !runtimeState.started || typeof runtimeState.serverUrl !== 'string') {
249
- return;
250
- }
251
- try {
252
- await stopManagedAppium(runtimeState.serverUrl, false);
253
- }
254
- catch {
255
- await stopManagedAppium(runtimeState.serverUrl, true);
256
- }
257
- }
258
222
  function actionArgs(command, options) {
259
223
  const commonIgnored = new Set([
260
- 'platform',
261
224
  'device',
262
225
  'format',
263
226
  'output',
264
227
  'timeout',
265
228
  'verbose',
266
229
  'server-url',
267
- 'mock',
268
230
  'seed',
269
231
  'app-id',
270
- 'attach',
271
- 'appium-cmd',
272
- 'startup-timeout',
273
- 'no-auto-start-appium'
232
+ 'attach'
274
233
  ]);
275
234
  const args = Object.entries(options).reduce((acc, [key, value]) => {
276
235
  if (!commonIgnored.has(key) && value !== undefined) {
@@ -283,6 +242,25 @@ function actionArgs(command, options) {
283
242
  }
284
243
  return args;
285
244
  }
245
+ function isTargetInitializationError(error) {
246
+ const message = errorMessage(error);
247
+ return (message.includes('Failed to create WebdriverIO Appium session') ||
248
+ message.includes('CoreSimulatorService') ||
249
+ message.includes('Could not find a driver for automationName'));
250
+ }
251
+ function targetInitializationNextStep(error) {
252
+ const message = errorMessage(error);
253
+ if (message.includes('CoreSimulatorService') || message.includes('simctl')) {
254
+ return 'Run `xcrun simctl list` from the same shell. If it fails, restart Simulator/CoreSimulator and ensure Appium is not running from a sandboxed process.';
255
+ }
256
+ if (message.includes('Could not find a driver for automationName')) {
257
+ return 'Install the matching Appium driver, verify it with `appium driver list --installed`, then restart `visor start`.';
258
+ }
259
+ if (message.includes('bundle identifier') && message.includes('unknown')) {
260
+ return 'Verify the iOS bundle id is installed on the selected simulator/device, launch it first when using `--attach`, then retry.';
261
+ }
262
+ return 'Verify Appium driver setup, target device state, and app id, then retry.';
263
+ }
286
264
  function cmdHelp() {
287
265
  const commandId = makeId('cmd');
288
266
  const startedAt = utcNowIso();
@@ -292,13 +270,25 @@ function cmdHelp() {
292
270
  commands: Array.from(ALL_COMMANDS),
293
271
  examples: [
294
272
  'visor validate scenarios/checkout-smoke.json',
295
- 'visor run scenarios/checkout-smoke.json --mock --output artifacts-test',
296
- 'visor scroll --platform android --mock --direction down',
297
- 'node dist/main.js status'
273
+ 'visor start --server-url http://127.0.0.1:4723',
274
+ 'visor run scenarios/checkout-smoke.json --output artifacts-test',
275
+ 'visor scroll --device emulator-5554 --direction down',
276
+ 'visor status'
298
277
  ]
299
278
  };
300
279
  return { code: 0, response };
301
280
  }
281
+ function cmdVersion() {
282
+ const commandId = makeId('cmd');
283
+ const startedAt = utcNowIso();
284
+ const version = packageVersion();
285
+ const response = envelopeOk(commandId, startedAt, [], 'none');
286
+ response.data = {
287
+ version,
288
+ versionText: version
289
+ };
290
+ return { code: 0, response };
291
+ }
302
292
  export async function cmdValidate(parsed) {
303
293
  const commandId = makeId('cmd');
304
294
  const startedAt = utcNowIso();
@@ -333,17 +323,36 @@ export async function cmdRun(parsed) {
333
323
  response.data = { issues };
334
324
  return { code: 1, response };
335
325
  }
336
- const runtime = resolvedRuntime(parsed.options, scenario);
337
- let runtimeState;
338
- let cleanupError;
326
+ let runtime;
339
327
  try {
340
- if (!runtime.use_mock) {
341
- runtimeState = await ensureNonMockRuntime(runtime);
328
+ runtime = await resolvedRuntime({
329
+ ...parsed.options,
330
+ timeout: typeof parsed.options.timeout === 'number'
331
+ ? parsed.options.timeout
332
+ : typeof scenario.config.timeoutMs === 'number'
333
+ ? scenario.config.timeoutMs
334
+ : undefined,
335
+ output: parsed.options.output ??
336
+ (typeof scenario.config.artifactsDir === 'string' ? scenario.config.artifactsDir : undefined)
337
+ });
338
+ }
339
+ catch (error) {
340
+ if (!(error instanceof DeviceSelectionError)) {
341
+ throw error;
342
342
  }
343
- const adapter = await getAdapter(runtime.platform, runtime.server_url, runtime.device, runtime.use_mock, runtime.app_id, runtime.attach_to_running);
344
- const result = await runScenario(scenario, adapter, runtime.device, runtime.timeout, runtime.output_dir);
343
+ const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Device selection failed', error.message, 'Start one target device or rerun with --device <device-id>');
344
+ response.data = { devices: error.devices };
345
+ return { code: 1, response };
346
+ }
347
+ try {
348
+ const result = await runDaemonScenario({
349
+ platform: runtime.platform,
350
+ server_url: runtime.server_url,
351
+ device: runtime.device,
352
+ app_id: runtime.app_id,
353
+ attach_to_running: runtime.attach_to_running
354
+ }, scenario, runtime.device, runtime.timeout, runtime.output_dir);
345
355
  const outputs = writeReports(result, runtime.output_dir);
346
- await stopAutoStartedAppium(runtimeState);
347
356
  if (result.status === 'fail' && result.error) {
348
357
  const response = envelopeFail(commandId, startedAt, result.error.code, result.error.message, result.error.likely_cause, result.error.next_step);
349
358
  response.artifacts = Object.values(outputs);
@@ -361,17 +370,19 @@ export async function cmdRun(parsed) {
361
370
  return { code: 0, response };
362
371
  }
363
372
  catch (error) {
364
- const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Failed to initialize platform target', errorMessage(error), 'For local non-mock runs: install Node deps, run `visor start` (or remove --no-auto-start-appium), boot target emulator/simulator, and retry.');
373
+ const isDaemonUnavailable = error instanceof DaemonUnavailableError;
374
+ const isDaemonTimeout = error instanceof DaemonRequestTimeoutError;
375
+ const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', isDaemonUnavailable
376
+ ? 'Run requires visor start'
377
+ : isDaemonTimeout
378
+ ? 'Timed out waiting for Visor daemon'
379
+ : 'Failed to initialize platform target', errorMessage(error), isDaemonUnavailable
380
+ ? 'Run `visor start`, verify the target emulator/simulator is booted, and retry.'
381
+ : isDaemonTimeout
382
+ ? 'Inspect .visor/daemon/daemon.log and .visor/appium/*.log, then retry or run `visor stop --force` before restarting.'
383
+ : 'Verify the target emulator/simulator and Appium driver setup, then retry.');
365
384
  return { code: 1, response };
366
385
  }
367
- finally {
368
- try {
369
- await stopAutoStartedAppium(runtimeState);
370
- }
371
- catch (error) {
372
- cleanupError = error;
373
- }
374
- }
375
386
  }
376
387
  export async function cmdBenchmark(parsed) {
377
388
  const commandId = makeId('cmd');
@@ -383,62 +394,62 @@ export async function cmdBenchmark(parsed) {
383
394
  response.data = { issues };
384
395
  return { code: 1, response };
385
396
  }
386
- const runtime = resolvedRuntime(parsed.options, scenario);
397
+ let runtime;
398
+ try {
399
+ runtime = await resolvedRuntime({
400
+ ...parsed.options,
401
+ timeout: typeof parsed.options.timeout === 'number'
402
+ ? parsed.options.timeout
403
+ : typeof scenario.config.timeoutMs === 'number'
404
+ ? scenario.config.timeoutMs
405
+ : undefined,
406
+ output: parsed.options.output ??
407
+ (typeof scenario.config.artifactsDir === 'string' ? scenario.config.artifactsDir : undefined)
408
+ });
409
+ }
410
+ catch (error) {
411
+ if (!(error instanceof DeviceSelectionError)) {
412
+ throw error;
413
+ }
414
+ const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Device selection failed', error.message, 'Start one target device or rerun with --device <device-id>');
415
+ response.data = { devices: error.devices };
416
+ return { code: 1, response };
417
+ }
387
418
  const runs = typeof parsed.options.runs === 'number' ? parsed.options.runs : 20;
388
419
  const threshold = typeof parsed.options.threshold === 'number' ? parsed.options.threshold : 95;
389
420
  const signatures = [];
390
421
  const runIds = [];
391
422
  let failures = 0;
392
- let runtimeState;
393
- let cleanupError;
394
- if (!runtime.use_mock) {
423
+ for (let index = 0; index < runs; index += 1) {
395
424
  try {
396
- runtimeState = await ensureNonMockRuntime(runtime);
397
- }
398
- catch (error) {
399
- const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Failed benchmark preflight for non-mock runtime', errorMessage(error), 'Start Appium (or allow auto-start), verify local device target, then rerun benchmark');
400
- return { code: 1, response };
401
- }
402
- }
403
- try {
404
- for (let index = 0; index < runs; index += 1) {
405
- try {
406
- const adapter = await getAdapter(runtime.platform, runtime.server_url, runtime.device, runtime.use_mock, runtime.app_id, runtime.attach_to_running);
407
- const result = await runScenario(scenario, adapter, runtime.device, runtime.timeout, runtime.output_dir);
408
- writeReports(result, runtime.output_dir);
409
- signatures.push(result.determinism_signature);
410
- runIds.push(result.run_id);
411
- if (result.status !== 'ok') {
412
- failures += 1;
413
- }
414
- }
415
- catch {
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') {
416
436
  failures += 1;
417
437
  }
418
438
  }
419
- }
420
- finally {
421
- try {
422
- await stopAutoStartedAppium(runtimeState);
423
- }
424
439
  catch (error) {
425
- cleanupError = 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 };
443
+ }
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 };
447
+ }
448
+ failures += 1;
426
449
  }
427
450
  }
428
451
  const score = determinismCheck(signatures);
429
452
  const passGate = score >= threshold && failures === 0;
430
- if (cleanupError) {
431
- const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Benchmark completed but failed to stop auto-started Appium', errorMessage(cleanupError), 'Inspect .visor/appium logs and stop Appium manually');
432
- response.data = {
433
- runs,
434
- threshold,
435
- determinismScore: score,
436
- pass: false,
437
- failures,
438
- runIds
439
- };
440
- return { code: 1, response };
441
- }
442
453
  const response = envelopeOk(commandId, startedAt, [], 'report');
443
454
  response.data = {
444
455
  runs,
@@ -466,43 +477,29 @@ export async function cmdReport(parsed) {
466
477
  export async function cmdAction(command, parsed) {
467
478
  const commandId = makeId('cmd');
468
479
  const startedAt = utcNowIso();
469
- const options = {
470
- platform: String(parsed.options.platform ?? 'android'),
471
- device: typeof parsed.options.device === 'string' ? parsed.options.device : undefined,
472
- server_url: String(parsed.options['server-url'] ?? DEFAULT_SERVER_URL),
473
- use_mock: Boolean(parsed.options.mock),
474
- app_id: typeof parsed.options['app-id'] === 'string' ? parsed.options['app-id'] : undefined,
475
- attach_to_running: Boolean(parsed.options.attach),
476
- auto_start_appium: !Boolean(parsed.options['no-auto-start-appium']),
477
- appium_cmd: typeof parsed.options['appium-cmd'] === 'string' ? parsed.options['appium-cmd'] : undefined,
478
- startup_timeout: typeof parsed.options['startup-timeout'] === 'number'
479
- ? parsed.options['startup-timeout']
480
- : DEFAULT_STARTUP_TIMEOUT_SECONDS
481
- };
482
- let runtimeState;
483
- let cleanupError;
480
+ let options;
481
+ try {
482
+ options = await resolvedRuntime(parsed.options);
483
+ }
484
+ catch (error) {
485
+ if (!(error instanceof DeviceSelectionError)) {
486
+ throw error;
487
+ }
488
+ const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Device selection failed', error.message, 'Start one target device or rerun with --device <device-id>');
489
+ response.data = { devices: error.devices };
490
+ return { code: 1, response };
491
+ }
484
492
  let payload = {};
485
493
  let artifacts = [];
486
494
  let actionError;
487
- let adapter;
488
495
  try {
489
- if (!options.use_mock) {
490
- runtimeState = await ensureNonMockRuntime({
491
- platform: options.platform,
492
- device: options.device ?? (options.platform === 'android' ? 'emulator-5554' : 'iPhone 17 Pro'),
493
- timeout: undefined,
494
- output_dir: 'artifacts',
495
- server_url: options.server_url,
496
- use_mock: options.use_mock,
497
- app_id: options.app_id,
498
- attach_to_running: options.attach_to_running,
499
- auto_start_appium: options.auto_start_appium,
500
- appium_cmd: options.appium_cmd,
501
- startup_timeout: options.startup_timeout
502
- });
503
- }
504
- adapter = await getAdapter(options.platform, options.server_url, options.device, options.use_mock, options.app_id, options.attach_to_running);
505
- payload = await adapter[command](actionArgs(command, parsed.options));
496
+ payload = await runDaemonAction({
497
+ platform: options.platform,
498
+ server_url: options.server_url,
499
+ device: options.device,
500
+ app_id: options.app_id,
501
+ attach_to_running: options.attach_to_running
502
+ }, command, actionArgs(command, parsed.options));
506
503
  const actionPayload = payload.args;
507
504
  if (actionPayload && typeof actionPayload === 'object' && !Array.isArray(actionPayload)) {
508
505
  const maybePath = actionPayload.path;
@@ -514,27 +511,23 @@ export async function cmdAction(command, parsed) {
514
511
  catch (error) {
515
512
  actionError = error;
516
513
  }
517
- finally {
518
- if (adapter) {
519
- await adapter.close();
520
- }
521
- try {
522
- await stopAutoStartedAppium(runtimeState);
523
- }
524
- catch (error) {
525
- cleanupError = error;
526
- }
527
- }
528
514
  if (actionError) {
529
- const cause = cleanupError
530
- ? `${errorMessage(actionError)}; additionally failed to stop auto-started Appium: ${errorMessage(cleanupError)}`
531
- : errorMessage(actionError);
532
- const response = envelopeFail(commandId, startedAt, 'ACTION_ERROR', `${command} failed`, cause, 'Check command args and retry');
533
- response.data = payload;
534
- return { code: 1, response };
535
- }
536
- if (cleanupError) {
537
- const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', `${command} completed but failed to stop auto-started Appium`, errorMessage(cleanupError), 'Inspect .visor/appium logs and stop Appium manually');
515
+ const isDaemonUnavailable = actionError instanceof DaemonUnavailableError;
516
+ const isDaemonTimeout = actionError instanceof DaemonRequestTimeoutError;
517
+ const isTargetInitialization = isTargetInitializationError(actionError);
518
+ const response = envelopeFail(commandId, startedAt, isDaemonUnavailable || isDaemonTimeout || isTargetInitialization ? 'TARGET_ERROR' : 'ACTION_ERROR', isDaemonUnavailable
519
+ ? `${command} requires visor start`
520
+ : isDaemonTimeout
521
+ ? `${command} timed out waiting for Visor daemon`
522
+ : isTargetInitialization
523
+ ? `${command} failed to initialize platform target`
524
+ : `${command} failed`, errorMessage(actionError), isDaemonUnavailable
525
+ ? 'Run `visor start`, verify the target emulator/simulator is booted, and retry.'
526
+ : isDaemonTimeout
527
+ ? 'Inspect .visor/daemon/daemon.log and .visor/appium/*.log, then retry or run `visor stop --force` before restarting.'
528
+ : isTargetInitialization
529
+ ? targetInitializationNextStep(actionError)
530
+ : 'Check command args and retry');
538
531
  response.data = payload;
539
532
  return { code: 1, response };
540
533
  }
@@ -546,23 +539,22 @@ export async function cmdStart(parsed) {
546
539
  const commandId = makeId('cmd');
547
540
  const startedAt = utcNowIso();
548
541
  try {
549
- const status = await startManagedAppium(String(parsed.options['server-url'] ?? DEFAULT_SERVER_URL), typeof parsed.options['appium-cmd'] === 'string' ? parsed.options['appium-cmd'] : undefined, typeof parsed.options['startup-timeout'] === 'number'
550
- ? parsed.options['startup-timeout']
551
- : DEFAULT_STARTUP_TIMEOUT_SECONDS);
542
+ const status = await startVisorDaemon(String(parsed.options['server-url'] ?? DEFAULT_SERVER_URL), typeof parsed.options['appium-cmd'] === 'string' ? parsed.options['appium-cmd'] : undefined);
552
543
  const response = envelopeOk(commandId, startedAt, [], 'run');
553
544
  response.data = status;
554
545
  return { code: 0, response };
555
546
  }
556
547
  catch (error) {
557
- const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Failed to start Appium', errorMessage(error), 'Install Node deps, check --appium-cmd, and inspect .visor/appium/*.log');
548
+ const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Failed to start Visor daemon', errorMessage(error), 'Install Node deps, check --appium-cmd, and inspect .visor/daemon/*.log');
558
549
  return { code: 1, response };
559
550
  }
560
551
  }
561
552
  export async function cmdStatus(parsed) {
562
553
  const commandId = makeId('cmd');
563
554
  const startedAt = utcNowIso();
564
- const status = await statusManagedAppium(String(parsed.options['server-url'] ?? DEFAULT_SERVER_URL));
565
- const response = envelopeOk(commandId, startedAt, [], Boolean(status.reachable) ? 'run' : 'start');
555
+ const status = await statusVisorDaemon(String(parsed.options['server-url'] ?? DEFAULT_SERVER_URL));
556
+ const daemon = status.daemon;
557
+ const response = envelopeOk(commandId, startedAt, [], daemon?.running ? 'run' : daemon?.unresponsive ? 'stop' : 'start');
566
558
  response.data = status;
567
559
  return { code: 0, response };
568
560
  }
@@ -570,13 +562,13 @@ export async function cmdStop(parsed) {
570
562
  const commandId = makeId('cmd');
571
563
  const startedAt = utcNowIso();
572
564
  try {
573
- const result = await stopManagedAppium(String(parsed.options['server-url'] ?? DEFAULT_SERVER_URL), Boolean(parsed.options.force));
565
+ const result = await stopVisorDaemon(String(parsed.options['server-url'] ?? DEFAULT_SERVER_URL), Boolean(parsed.options.force));
574
566
  const response = envelopeOk(commandId, startedAt, [], 'none');
575
567
  response.data = result;
576
568
  return { code: 0, response };
577
569
  }
578
570
  catch (error) {
579
- const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Failed to stop managed Appium', errorMessage(error), 'Retry with --force or check process state manually');
571
+ const response = envelopeFail(commandId, startedAt, 'TARGET_ERROR', 'Failed to stop Visor daemon', errorMessage(error), 'Retry with --force or check process state manually');
580
572
  return { code: 1, response };
581
573
  }
582
574
  }
@@ -587,6 +579,9 @@ export async function executeCommand(argv) {
587
579
  argv.includes('-h')) {
588
580
  return cmdHelp();
589
581
  }
582
+ if (argv.includes('--version') || argv.includes('-v')) {
583
+ return cmdVersion();
584
+ }
590
585
  const parsed = parseCommand(argv);
591
586
  if (parsed.command === 'validate') {
592
587
  return cmdValidate(parsed);