visor-ai 0.2.1
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/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/adapters.js +425 -0
- package/dist/appiumLifecycle.js +267 -0
- package/dist/cli.js +610 -0
- package/dist/errors.js +8 -0
- package/dist/index.js +9 -0
- package/dist/main.js +43 -0
- package/dist/report.js +90 -0
- package/dist/runner.js +177 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +118 -0
- package/dist/validator.js +183 -0
- package/package.json +46 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import { DEFAULT_SERVER_URL, getAdapter } from './adapters.js';
|
|
2
|
+
import { DEFAULT_STARTUP_TIMEOUT_SECONDS, isAppiumReachable, startManagedAppium, statusManagedAppium, stopManagedAppium } from './appiumLifecycle.js';
|
|
3
|
+
import { makeError } from './errors.js';
|
|
4
|
+
import { writeReports } from './report.js';
|
|
5
|
+
import { determinismCheck, runScenario } from './runner.js';
|
|
6
|
+
import { errorMessage, makeId, utcNowIso } from './utils.js';
|
|
7
|
+
import { parseAndValidate } from './validator.js';
|
|
8
|
+
const ACTION_COMMANDS = new Set([
|
|
9
|
+
'tap',
|
|
10
|
+
'navigate',
|
|
11
|
+
'act',
|
|
12
|
+
'screenshot',
|
|
13
|
+
'wait',
|
|
14
|
+
'source'
|
|
15
|
+
]);
|
|
16
|
+
const ALL_COMMANDS = new Set([
|
|
17
|
+
...ACTION_COMMANDS,
|
|
18
|
+
'validate',
|
|
19
|
+
'run',
|
|
20
|
+
'benchmark',
|
|
21
|
+
'report',
|
|
22
|
+
'start',
|
|
23
|
+
'status',
|
|
24
|
+
'stop'
|
|
25
|
+
]);
|
|
26
|
+
const GLOBAL_SPEC = {
|
|
27
|
+
platform: 'string',
|
|
28
|
+
device: 'string',
|
|
29
|
+
timeout: 'number',
|
|
30
|
+
output: 'string',
|
|
31
|
+
format: 'string',
|
|
32
|
+
seed: 'number',
|
|
33
|
+
'server-url': 'string',
|
|
34
|
+
'app-id': 'string',
|
|
35
|
+
'appium-cmd': 'string',
|
|
36
|
+
'startup-timeout': 'number',
|
|
37
|
+
'no-auto-start-appium': 'boolean',
|
|
38
|
+
attach: 'boolean',
|
|
39
|
+
mock: 'boolean',
|
|
40
|
+
verbose: 'boolean'
|
|
41
|
+
};
|
|
42
|
+
const ACTION_SPEC = {
|
|
43
|
+
...GLOBAL_SPEC,
|
|
44
|
+
target: 'string',
|
|
45
|
+
x: 'number',
|
|
46
|
+
y: 'number',
|
|
47
|
+
normalized: 'boolean',
|
|
48
|
+
to: 'string',
|
|
49
|
+
name: 'string',
|
|
50
|
+
value: 'string',
|
|
51
|
+
label: 'string',
|
|
52
|
+
ms: 'number',
|
|
53
|
+
path: 'string'
|
|
54
|
+
};
|
|
55
|
+
const COMMAND_SPECS = {
|
|
56
|
+
validate: { format: 'string' },
|
|
57
|
+
run: {
|
|
58
|
+
platform: 'string',
|
|
59
|
+
device: 'string',
|
|
60
|
+
timeout: 'number',
|
|
61
|
+
output: 'string',
|
|
62
|
+
format: 'string',
|
|
63
|
+
'server-url': 'string',
|
|
64
|
+
'app-id': 'string',
|
|
65
|
+
'appium-cmd': 'string',
|
|
66
|
+
'startup-timeout': 'number',
|
|
67
|
+
'no-auto-start-appium': 'boolean',
|
|
68
|
+
attach: 'boolean',
|
|
69
|
+
mock: 'boolean'
|
|
70
|
+
},
|
|
71
|
+
benchmark: {
|
|
72
|
+
runs: 'number',
|
|
73
|
+
threshold: 'number',
|
|
74
|
+
platform: 'string',
|
|
75
|
+
device: 'string',
|
|
76
|
+
timeout: 'number',
|
|
77
|
+
output: 'string',
|
|
78
|
+
format: 'string',
|
|
79
|
+
'server-url': 'string',
|
|
80
|
+
'app-id': 'string',
|
|
81
|
+
'appium-cmd': 'string',
|
|
82
|
+
'startup-timeout': 'number',
|
|
83
|
+
'no-auto-start-appium': 'boolean',
|
|
84
|
+
attach: 'boolean',
|
|
85
|
+
mock: 'boolean'
|
|
86
|
+
},
|
|
87
|
+
report: { format: 'string' },
|
|
88
|
+
start: {
|
|
89
|
+
'server-url': 'string',
|
|
90
|
+
'appium-cmd': 'string',
|
|
91
|
+
'startup-timeout': 'number',
|
|
92
|
+
format: 'string'
|
|
93
|
+
},
|
|
94
|
+
status: {
|
|
95
|
+
'server-url': 'string',
|
|
96
|
+
format: 'string'
|
|
97
|
+
},
|
|
98
|
+
stop: {
|
|
99
|
+
'server-url': 'string',
|
|
100
|
+
force: 'boolean',
|
|
101
|
+
format: 'string'
|
|
102
|
+
},
|
|
103
|
+
tap: ACTION_SPEC,
|
|
104
|
+
navigate: ACTION_SPEC,
|
|
105
|
+
act: ACTION_SPEC,
|
|
106
|
+
screenshot: ACTION_SPEC,
|
|
107
|
+
wait: ACTION_SPEC,
|
|
108
|
+
source: ACTION_SPEC
|
|
109
|
+
};
|
|
110
|
+
function helpText() {
|
|
111
|
+
return [
|
|
112
|
+
'Visor TypeScript CLI',
|
|
113
|
+
'',
|
|
114
|
+
'Usage:',
|
|
115
|
+
' visor <command> [options]',
|
|
116
|
+
' visor --help',
|
|
117
|
+
'',
|
|
118
|
+
'Commands:',
|
|
119
|
+
' validate <scenario>',
|
|
120
|
+
' run <scenario> [--mock] [--output <dir>]',
|
|
121
|
+
' benchmark <scenario> [--runs <n>] [--threshold <percent>]',
|
|
122
|
+
' report [path]',
|
|
123
|
+
' start [--server-url <url>]',
|
|
124
|
+
' status [--server-url <url>]',
|
|
125
|
+
' stop [--server-url <url>] [--force]',
|
|
126
|
+
' tap|navigate|act|screenshot|wait|source',
|
|
127
|
+
'',
|
|
128
|
+
'Examples:',
|
|
129
|
+
' visor validate scenarios/checkout-smoke.json',
|
|
130
|
+
' visor run scenarios/checkout-smoke.json --mock --output artifacts-test',
|
|
131
|
+
' node dist/main.js status'
|
|
132
|
+
].join('\n');
|
|
133
|
+
}
|
|
134
|
+
function envelopeOk(commandId, startedAt, artifacts = [], nextAction = '') {
|
|
135
|
+
return {
|
|
136
|
+
status: 'ok',
|
|
137
|
+
command_id: commandId,
|
|
138
|
+
started_at: startedAt,
|
|
139
|
+
ended_at: utcNowIso(),
|
|
140
|
+
artifacts,
|
|
141
|
+
next_action: nextAction,
|
|
142
|
+
data: {}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function envelopeFail(commandId, startedAt, code, message, cause, nextStep) {
|
|
146
|
+
return {
|
|
147
|
+
status: 'fail',
|
|
148
|
+
command_id: commandId,
|
|
149
|
+
started_at: startedAt,
|
|
150
|
+
ended_at: utcNowIso(),
|
|
151
|
+
artifacts: [],
|
|
152
|
+
next_action: nextStep,
|
|
153
|
+
error: makeError(code, message, cause, nextStep),
|
|
154
|
+
data: {}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function parseOptions(tokens, spec) {
|
|
158
|
+
const options = {};
|
|
159
|
+
const positionals = [];
|
|
160
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
161
|
+
const token = tokens[index];
|
|
162
|
+
if (!token.startsWith('--')) {
|
|
163
|
+
positionals.push(token);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const optionName = token.slice(2);
|
|
167
|
+
const optionType = spec[optionName];
|
|
168
|
+
if (!optionType) {
|
|
169
|
+
throw new Error(`Unknown option '--${optionName}'`);
|
|
170
|
+
}
|
|
171
|
+
if (optionType === 'boolean') {
|
|
172
|
+
options[optionName] = true;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const rawValue = tokens[index + 1];
|
|
176
|
+
if (rawValue === undefined || rawValue.startsWith('--')) {
|
|
177
|
+
throw new Error(`Option '--${optionName}' requires a value`);
|
|
178
|
+
}
|
|
179
|
+
options[optionName] = optionType === 'number' ? Number(rawValue) : rawValue;
|
|
180
|
+
index += 1;
|
|
181
|
+
}
|
|
182
|
+
return { options, positionals };
|
|
183
|
+
}
|
|
184
|
+
function parseCommand(argv) {
|
|
185
|
+
const commandIndex = argv.findIndex((token) => ALL_COMMANDS.has(token));
|
|
186
|
+
if (commandIndex === -1) {
|
|
187
|
+
throw new Error('Missing command');
|
|
188
|
+
}
|
|
189
|
+
const globalTokens = argv.slice(0, commandIndex);
|
|
190
|
+
const command = argv[commandIndex];
|
|
191
|
+
const commandTokens = argv.slice(commandIndex + 1);
|
|
192
|
+
const globalParsed = parseOptions(globalTokens, GLOBAL_SPEC);
|
|
193
|
+
const commandParsed = parseOptions(commandTokens, COMMAND_SPECS[command] ?? {});
|
|
194
|
+
return {
|
|
195
|
+
command,
|
|
196
|
+
options: {
|
|
197
|
+
...globalParsed.options,
|
|
198
|
+
...commandParsed.options
|
|
199
|
+
},
|
|
200
|
+
positionals: [...globalParsed.positionals, ...commandParsed.positionals]
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function warningIssues(issues) {
|
|
204
|
+
return issues.filter((issue) => issue.severity === 'warning');
|
|
205
|
+
}
|
|
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;
|
|
210
|
+
return {
|
|
211
|
+
platform,
|
|
212
|
+
device: String(options.device ?? defaultDevice),
|
|
213
|
+
timeout: typeof options.timeout === 'number'
|
|
214
|
+
? options.timeout
|
|
215
|
+
: typeof scenario.config.timeoutMs === 'number'
|
|
216
|
+
? scenario.config.timeoutMs
|
|
217
|
+
: 2500,
|
|
218
|
+
output_dir: String(options.output ?? scenario.config.artifactsDir ?? 'artifacts'),
|
|
219
|
+
server_url: String(options['server-url'] ?? DEFAULT_SERVER_URL),
|
|
220
|
+
use_mock: Boolean(options.mock),
|
|
221
|
+
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
|
|
228
|
+
};
|
|
229
|
+
}
|
|
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
|
+
function actionArgs(command, options) {
|
|
254
|
+
const commonIgnored = new Set([
|
|
255
|
+
'platform',
|
|
256
|
+
'device',
|
|
257
|
+
'format',
|
|
258
|
+
'output',
|
|
259
|
+
'timeout',
|
|
260
|
+
'verbose',
|
|
261
|
+
'server-url',
|
|
262
|
+
'mock',
|
|
263
|
+
'seed',
|
|
264
|
+
'app-id',
|
|
265
|
+
'attach',
|
|
266
|
+
'appium-cmd',
|
|
267
|
+
'startup-timeout',
|
|
268
|
+
'no-auto-start-appium'
|
|
269
|
+
]);
|
|
270
|
+
const args = Object.entries(options).reduce((acc, [key, value]) => {
|
|
271
|
+
if (!commonIgnored.has(key) && value !== undefined) {
|
|
272
|
+
acc[key] = value;
|
|
273
|
+
}
|
|
274
|
+
return acc;
|
|
275
|
+
}, {});
|
|
276
|
+
if (command === 'source' && args.label === undefined) {
|
|
277
|
+
args.label = 'source';
|
|
278
|
+
}
|
|
279
|
+
return args;
|
|
280
|
+
}
|
|
281
|
+
function cmdHelp() {
|
|
282
|
+
const commandId = makeId('cmd');
|
|
283
|
+
const startedAt = utcNowIso();
|
|
284
|
+
const response = envelopeOk(commandId, startedAt, [], 'validate');
|
|
285
|
+
response.data = {
|
|
286
|
+
usageText: helpText(),
|
|
287
|
+
commands: Array.from(ALL_COMMANDS),
|
|
288
|
+
examples: [
|
|
289
|
+
'visor validate scenarios/checkout-smoke.json',
|
|
290
|
+
'visor run scenarios/checkout-smoke.json --mock --output artifacts-test',
|
|
291
|
+
'node dist/main.js status'
|
|
292
|
+
]
|
|
293
|
+
};
|
|
294
|
+
return { code: 0, response };
|
|
295
|
+
}
|
|
296
|
+
export async function cmdValidate(parsed) {
|
|
297
|
+
const commandId = makeId('cmd');
|
|
298
|
+
const startedAt = utcNowIso();
|
|
299
|
+
const scenarioPath = parsed.positionals[0];
|
|
300
|
+
try {
|
|
301
|
+
if (!scenarioPath) {
|
|
302
|
+
throw new Error('validate requires a scenario path');
|
|
303
|
+
}
|
|
304
|
+
const { scenario, issues } = parseAndValidate(scenarioPath);
|
|
305
|
+
const response = envelopeOk(commandId, startedAt, [], 'run');
|
|
306
|
+
response.data = {
|
|
307
|
+
valid: scenario !== null,
|
|
308
|
+
issues
|
|
309
|
+
};
|
|
310
|
+
return {
|
|
311
|
+
code: scenario ? 0 : 1,
|
|
312
|
+
response
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
const response = envelopeFail(commandId, startedAt, 'INPUT_ERROR', 'Validation failed', errorMessage(error), 'Fix scenario JSON and rerun validate');
|
|
317
|
+
return { code: 1, response };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
export async function cmdRun(parsed) {
|
|
321
|
+
const commandId = makeId('cmd');
|
|
322
|
+
const startedAt = utcNowIso();
|
|
323
|
+
const scenarioPath = parsed.positionals[0];
|
|
324
|
+
const { scenario, issues } = parseAndValidate(String(scenarioPath ?? ''));
|
|
325
|
+
if (!scenario) {
|
|
326
|
+
const response = envelopeFail(commandId, startedAt, 'INPUT_ERROR', 'Scenario validation failed', 'One or more schema violations', 'Run `visor validate <file>` and resolve errors');
|
|
327
|
+
response.data = { issues };
|
|
328
|
+
return { code: 1, response };
|
|
329
|
+
}
|
|
330
|
+
const runtime = resolvedRuntime(parsed.options, scenario);
|
|
331
|
+
let runtimeState;
|
|
332
|
+
let cleanupError;
|
|
333
|
+
try {
|
|
334
|
+
if (!runtime.use_mock) {
|
|
335
|
+
runtimeState = await ensureNonMockRuntime(runtime);
|
|
336
|
+
}
|
|
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);
|
|
339
|
+
const outputs = writeReports(result, runtime.output_dir);
|
|
340
|
+
await stopAutoStartedAppium(runtimeState);
|
|
341
|
+
if (result.status === 'fail' && result.error) {
|
|
342
|
+
const response = envelopeFail(commandId, startedAt, result.error.code, result.error.message, result.error.likely_cause, result.error.next_step);
|
|
343
|
+
response.artifacts = Object.values(outputs);
|
|
344
|
+
response.data = {
|
|
345
|
+
run: result,
|
|
346
|
+
warnings: warningIssues(issues)
|
|
347
|
+
};
|
|
348
|
+
return { code: 2, response };
|
|
349
|
+
}
|
|
350
|
+
const response = envelopeOk(commandId, startedAt, Object.values(outputs), 'report');
|
|
351
|
+
response.data = {
|
|
352
|
+
run: result,
|
|
353
|
+
warnings: warningIssues(issues)
|
|
354
|
+
};
|
|
355
|
+
return { code: 0, response };
|
|
356
|
+
}
|
|
357
|
+
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.');
|
|
359
|
+
return { code: 1, response };
|
|
360
|
+
}
|
|
361
|
+
finally {
|
|
362
|
+
try {
|
|
363
|
+
await stopAutoStartedAppium(runtimeState);
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
cleanupError = error;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
export async function cmdBenchmark(parsed) {
|
|
371
|
+
const commandId = makeId('cmd');
|
|
372
|
+
const startedAt = utcNowIso();
|
|
373
|
+
const scenarioPath = parsed.positionals[0];
|
|
374
|
+
const { scenario, issues } = parseAndValidate(String(scenarioPath ?? ''));
|
|
375
|
+
if (!scenario) {
|
|
376
|
+
const response = envelopeFail(commandId, startedAt, 'INPUT_ERROR', 'Scenario validation failed', 'Invalid scenario', 'Fix schema errors before benchmark');
|
|
377
|
+
response.data = { issues };
|
|
378
|
+
return { code: 1, response };
|
|
379
|
+
}
|
|
380
|
+
const runtime = resolvedRuntime(parsed.options, scenario);
|
|
381
|
+
const runs = typeof parsed.options.runs === 'number' ? parsed.options.runs : 20;
|
|
382
|
+
const threshold = typeof parsed.options.threshold === 'number' ? parsed.options.threshold : 95;
|
|
383
|
+
const signatures = [];
|
|
384
|
+
const runIds = [];
|
|
385
|
+
let failures = 0;
|
|
386
|
+
let runtimeState;
|
|
387
|
+
let cleanupError;
|
|
388
|
+
if (!runtime.use_mock) {
|
|
389
|
+
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 {
|
|
410
|
+
failures += 1;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
try {
|
|
416
|
+
await stopAutoStartedAppium(runtimeState);
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
cleanupError = error;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const score = determinismCheck(signatures);
|
|
423
|
+
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
|
+
const response = envelopeOk(commandId, startedAt, [], 'report');
|
|
437
|
+
response.data = {
|
|
438
|
+
runs,
|
|
439
|
+
threshold,
|
|
440
|
+
determinismScore: score,
|
|
441
|
+
pass: passGate,
|
|
442
|
+
failures,
|
|
443
|
+
runIds,
|
|
444
|
+
warnings: warningIssues(issues)
|
|
445
|
+
};
|
|
446
|
+
return { code: passGate ? 0 : 3, response };
|
|
447
|
+
}
|
|
448
|
+
export async function cmdReport(parsed) {
|
|
449
|
+
const commandId = makeId('cmd');
|
|
450
|
+
const startedAt = utcNowIso();
|
|
451
|
+
const reportPath = parsed.positionals[0] ?? 'artifacts';
|
|
452
|
+
const response = envelopeOk(commandId, startedAt, [], 'none');
|
|
453
|
+
response.data = {
|
|
454
|
+
message: `Use output under ${reportPath}/<run-id>/summary.txt|summary.json|junit.xml|report.html`,
|
|
455
|
+
path: reportPath,
|
|
456
|
+
format: parsed.options.format ?? 'json'
|
|
457
|
+
};
|
|
458
|
+
return { code: 0, response };
|
|
459
|
+
}
|
|
460
|
+
export async function cmdAction(command, parsed) {
|
|
461
|
+
const commandId = makeId('cmd');
|
|
462
|
+
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;
|
|
478
|
+
let payload = {};
|
|
479
|
+
let artifacts = [];
|
|
480
|
+
let actionError;
|
|
481
|
+
let adapter;
|
|
482
|
+
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));
|
|
500
|
+
const actionPayload = payload.args;
|
|
501
|
+
if (actionPayload && typeof actionPayload === 'object' && !Array.isArray(actionPayload)) {
|
|
502
|
+
const maybePath = actionPayload.path;
|
|
503
|
+
if (typeof maybePath === 'string') {
|
|
504
|
+
artifacts = [maybePath];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
actionError = error;
|
|
510
|
+
}
|
|
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
|
+
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');
|
|
532
|
+
response.data = payload;
|
|
533
|
+
return { code: 1, response };
|
|
534
|
+
}
|
|
535
|
+
const response = envelopeOk(commandId, startedAt, artifacts, 'run');
|
|
536
|
+
response.data = payload;
|
|
537
|
+
return { code: 0, response };
|
|
538
|
+
}
|
|
539
|
+
export async function cmdStart(parsed) {
|
|
540
|
+
const commandId = makeId('cmd');
|
|
541
|
+
const startedAt = utcNowIso();
|
|
542
|
+
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);
|
|
546
|
+
const response = envelopeOk(commandId, startedAt, [], 'run');
|
|
547
|
+
response.data = status;
|
|
548
|
+
return { code: 0, response };
|
|
549
|
+
}
|
|
550
|
+
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');
|
|
552
|
+
return { code: 1, response };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
export async function cmdStatus(parsed) {
|
|
556
|
+
const commandId = makeId('cmd');
|
|
557
|
+
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');
|
|
560
|
+
response.data = status;
|
|
561
|
+
return { code: 0, response };
|
|
562
|
+
}
|
|
563
|
+
export async function cmdStop(parsed) {
|
|
564
|
+
const commandId = makeId('cmd');
|
|
565
|
+
const startedAt = utcNowIso();
|
|
566
|
+
try {
|
|
567
|
+
const result = await stopManagedAppium(String(parsed.options['server-url'] ?? DEFAULT_SERVER_URL), Boolean(parsed.options.force));
|
|
568
|
+
const response = envelopeOk(commandId, startedAt, [], 'none');
|
|
569
|
+
response.data = result;
|
|
570
|
+
return { code: 0, response };
|
|
571
|
+
}
|
|
572
|
+
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');
|
|
574
|
+
return { code: 1, response };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
export async function executeCommand(argv) {
|
|
578
|
+
if (argv.length === 0 ||
|
|
579
|
+
argv[0] === 'help' ||
|
|
580
|
+
argv.includes('--help') ||
|
|
581
|
+
argv.includes('-h')) {
|
|
582
|
+
return cmdHelp();
|
|
583
|
+
}
|
|
584
|
+
const parsed = parseCommand(argv);
|
|
585
|
+
if (parsed.command === 'validate') {
|
|
586
|
+
return cmdValidate(parsed);
|
|
587
|
+
}
|
|
588
|
+
if (parsed.command === 'run') {
|
|
589
|
+
return cmdRun(parsed);
|
|
590
|
+
}
|
|
591
|
+
if (parsed.command === 'benchmark') {
|
|
592
|
+
return cmdBenchmark(parsed);
|
|
593
|
+
}
|
|
594
|
+
if (parsed.command === 'report') {
|
|
595
|
+
return cmdReport(parsed);
|
|
596
|
+
}
|
|
597
|
+
if (parsed.command === 'start') {
|
|
598
|
+
return cmdStart(parsed);
|
|
599
|
+
}
|
|
600
|
+
if (parsed.command === 'status') {
|
|
601
|
+
return cmdStatus(parsed);
|
|
602
|
+
}
|
|
603
|
+
if (parsed.command === 'stop') {
|
|
604
|
+
return cmdStop(parsed);
|
|
605
|
+
}
|
|
606
|
+
if (ACTION_COMMANDS.has(parsed.command)) {
|
|
607
|
+
return cmdAction(parsed.command, parsed);
|
|
608
|
+
}
|
|
609
|
+
throw new Error(`Unsupported command '${parsed.command}'`);
|
|
610
|
+
}
|
package/dist/errors.js
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './adapters.js';
|
|
2
|
+
export * from './appiumLifecycle.js';
|
|
3
|
+
export * from './cli.js';
|
|
4
|
+
export * from './errors.js';
|
|
5
|
+
export * from './report.js';
|
|
6
|
+
export * from './runner.js';
|
|
7
|
+
export * from './types.js';
|
|
8
|
+
export * from './utils.js';
|
|
9
|
+
export * from './validator.js';
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { executeCommand } from './cli.js';
|
|
3
|
+
import { makeError } from './errors.js';
|
|
4
|
+
import { makeId, utcNowIso } from './utils.js';
|
|
5
|
+
function isHelpData(value) {
|
|
6
|
+
return Boolean(value &&
|
|
7
|
+
typeof value === 'object' &&
|
|
8
|
+
'usageText' in value &&
|
|
9
|
+
typeof value.usageText === 'string');
|
|
10
|
+
}
|
|
11
|
+
async function main(argv = process.argv.slice(2)) {
|
|
12
|
+
try {
|
|
13
|
+
const result = await executeCommand(argv);
|
|
14
|
+
if (isHelpData(result.response.data)) {
|
|
15
|
+
console.log(result.response.data.usageText);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
console.log(JSON.stringify(result.response, null, 2));
|
|
19
|
+
}
|
|
20
|
+
return result.code;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const startedAt = utcNowIso();
|
|
24
|
+
const response = {
|
|
25
|
+
status: 'fail',
|
|
26
|
+
command_id: makeId('cmd'),
|
|
27
|
+
started_at: startedAt,
|
|
28
|
+
ended_at: utcNowIso(),
|
|
29
|
+
artifacts: [],
|
|
30
|
+
next_action: 'Inspect CLI arguments and retry',
|
|
31
|
+
error: makeError('SYSTEM_ERROR', 'Unhandled CLI failure', error instanceof Error ? error.message : String(error), 'Inspect CLI arguments and retry'),
|
|
32
|
+
data: {}
|
|
33
|
+
};
|
|
34
|
+
console.log(JSON.stringify(response, null, 2));
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
39
|
+
void main().then((code) => {
|
|
40
|
+
process.exitCode = code;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export { main };
|