visor-ai 0.2.4 → 0.2.6

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