@astur-mobile/cli 0.1.0-beta.0
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/assets/brand/Astur_A_balck.png +0 -0
- package/assets/brand/Astur_A_white.png +0 -0
- package/assets/brand/astur-logo-dark.png +0 -0
- package/assets/brand/astur-logo-light.png +0 -0
- package/assets/brand/astur-mark-light.png +0 -0
- package/assets/brand/astur-mark-transparent.png +0 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +917 -0
- package/dist/index.js.map +1 -0
- package/dist/inspectorServer.d.ts +194 -0
- package/dist/inspectorServer.d.ts.map +1 -0
- package/dist/inspectorServer.js +3487 -0
- package/dist/inspectorServer.js.map +1 -0
- package/dist/inspectorUi.d.ts +17 -0
- package/dist/inspectorUi.d.ts.map +1 -0
- package/dist/inspectorUi.js +1181 -0
- package/dist/inspectorUi.js.map +1 -0
- package/dist/scaffold.d.ts +67 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +470 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { realpathSync } from 'node:fs';
|
|
5
|
+
import { access, mkdir, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { dirname } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { createAndroidDriver } from '@astur-mobile/android';
|
|
9
|
+
import { AsturError, AsturRuntime } from '@astur-mobile/core';
|
|
10
|
+
import { createIosDriver } from '@astur-mobile/ios';
|
|
11
|
+
import { __testing as inspectorServerTesting, startInspectorServer } from './inspectorServer.js';
|
|
12
|
+
import { buildInitFiles, defaultInitAnswers, promptInitAnswers } from './scaffold.js';
|
|
13
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
14
|
+
const [command = 'help', ...rest] = argv;
|
|
15
|
+
switch (command) {
|
|
16
|
+
case 'doctor':
|
|
17
|
+
await doctor(rest);
|
|
18
|
+
break;
|
|
19
|
+
case 'devices':
|
|
20
|
+
await devices(rest);
|
|
21
|
+
break;
|
|
22
|
+
case 'init':
|
|
23
|
+
await init(rest);
|
|
24
|
+
break;
|
|
25
|
+
case 'test':
|
|
26
|
+
await runPlaywright(rest);
|
|
27
|
+
break;
|
|
28
|
+
case 'codegen':
|
|
29
|
+
await codegen(rest);
|
|
30
|
+
break;
|
|
31
|
+
case 'inspect':
|
|
32
|
+
await codegen(rest);
|
|
33
|
+
break;
|
|
34
|
+
case '-h':
|
|
35
|
+
case '--help':
|
|
36
|
+
case 'help':
|
|
37
|
+
help();
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
console.error(`Unknown command: ${command}`);
|
|
41
|
+
help();
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function createRuntime() {
|
|
46
|
+
const runtime = new AsturRuntime().register(createAndroidDriver());
|
|
47
|
+
if (supportsLocalIos()) {
|
|
48
|
+
runtime.register(createIosDriver());
|
|
49
|
+
}
|
|
50
|
+
return runtime;
|
|
51
|
+
}
|
|
52
|
+
async function doctor(args) {
|
|
53
|
+
const checks = await createRuntime().doctor();
|
|
54
|
+
if (!supportsLocalIos()) {
|
|
55
|
+
checks.push(createIosSkipCheck());
|
|
56
|
+
}
|
|
57
|
+
printChecks(checks, { verbose: args.includes('--verbose') });
|
|
58
|
+
if (checks.some((check) => check.status === 'fail')) {
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function devices(args) {
|
|
63
|
+
const platform = args.includes('--android') ? 'android' : args.includes('--ios') ? 'ios' : undefined;
|
|
64
|
+
if (platform === 'ios' && !supportsLocalIos()) {
|
|
65
|
+
printHeader('devices', 'Connected devices and available simulators');
|
|
66
|
+
console.log(`\n${colors.yellow('iOS devices are not available on this host.')}`);
|
|
67
|
+
console.log(colors.dim('Local iOS automation requires macOS with Xcode.'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const found = await createRuntime().listDevices(platform);
|
|
71
|
+
printDevices(found);
|
|
72
|
+
}
|
|
73
|
+
async function init(args) {
|
|
74
|
+
printHeader('init', 'Create Astur Playwright config and starter test');
|
|
75
|
+
const useDefaults = args.includes('--yes') || args.includes('-y') || !process.stdin.isTTY;
|
|
76
|
+
const answers = useDefaults ? defaultInitAnswers() : await promptInitAnswers();
|
|
77
|
+
for (const file of buildInitFiles(answers)) {
|
|
78
|
+
await writeIfMissing(file.path, file.contents);
|
|
79
|
+
}
|
|
80
|
+
console.log('\nCreated Astur starter files.');
|
|
81
|
+
console.log(colors.dim('Next: run `npx astur-mobile doctor`, then `npx astur-mobile test`.'));
|
|
82
|
+
}
|
|
83
|
+
async function codegen(args) {
|
|
84
|
+
const parsed = parseCodegenArgs(args);
|
|
85
|
+
if (parsed.help) {
|
|
86
|
+
printCodegenHelp();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (parsed.platform === 'ios' && !supportsLocalIos()) {
|
|
90
|
+
printHeader('codegen', 'Playwright-style mobile inspector bootstrap');
|
|
91
|
+
console.log(`\n${colors.yellow('iOS codegen requires macOS with Xcode.')}`);
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const runtime = createRuntime();
|
|
96
|
+
const available = await runtime.listDevices(parsed.platform);
|
|
97
|
+
const selected = selectCodegenDevice(available, parsed.deviceId, parsed.deviceKind);
|
|
98
|
+
if (!selected) {
|
|
99
|
+
printHeader('codegen', 'Playwright-style mobile inspector bootstrap');
|
|
100
|
+
console.log(`\n${colors.yellow('No matching online/booted device was found.')}`);
|
|
101
|
+
console.log(colors.dim('Run `astur devices` to list available targets.'));
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// ── Live inspector path (default) ─────────────────────────────────────────
|
|
106
|
+
if (parsed.ui) {
|
|
107
|
+
printHeader('codegen', 'Astur Inspector UI');
|
|
108
|
+
// Create device without fetching tree — server streams everything
|
|
109
|
+
const config = buildCodegenConfig(selected, parsed);
|
|
110
|
+
printCodegenPreparation(selected, config);
|
|
111
|
+
let device;
|
|
112
|
+
let selectedDevice = selected;
|
|
113
|
+
try {
|
|
114
|
+
device = await createCodegenDevice(runtime, config, {
|
|
115
|
+
forceIosAppInstall: selectedDevice.platform === 'ios' && Boolean(parsed.appPath)
|
|
116
|
+
});
|
|
117
|
+
if (parsed.launch && config.app) {
|
|
118
|
+
await device.app.launch();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
console.error(`\n${colors.red('Failed to connect to device:')} ${formatCodegenConnectionError(err)}`);
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
let handle;
|
|
127
|
+
try {
|
|
128
|
+
handle = await startInspectorServer(device.inspector(codegenInspectorOptions(selectedDevice)), selectedDevice, {
|
|
129
|
+
captureScreenshot: () => device.screenshot().catch(() => undefined),
|
|
130
|
+
performDeviceAction: (action) => runInspectorDeviceAction(device, action),
|
|
131
|
+
performTap: (point) => device.tap(point),
|
|
132
|
+
performSwipe: (gesture) => device.swipe(gesture),
|
|
133
|
+
installApp: (path) => device.app.install(path),
|
|
134
|
+
performAppAction: async (action, options) => {
|
|
135
|
+
if (action === 'launch' && device.deviceInfo.platform === 'ios') {
|
|
136
|
+
const identifier = requireInspectorAppIdentifier(options.identifier);
|
|
137
|
+
const nextConfig = buildCodegenConfig(selectedDevice, {
|
|
138
|
+
...parsed,
|
|
139
|
+
platform: selectedDevice.platform,
|
|
140
|
+
deviceId: selectedDevice.id,
|
|
141
|
+
appId: identifier,
|
|
142
|
+
launch: false
|
|
143
|
+
});
|
|
144
|
+
const nextDevice = await createCodegenDevice(runtime, nextConfig, {
|
|
145
|
+
forceIosAppInstall: selectedDevice.platform === 'ios' && hasConfiguredAppPath(nextConfig)
|
|
146
|
+
});
|
|
147
|
+
await nextDevice.app.launch({ app: { bundleId: identifier } });
|
|
148
|
+
const previousDevice = device;
|
|
149
|
+
device = nextDevice;
|
|
150
|
+
await previousDevice.close().catch(() => undefined);
|
|
151
|
+
return {
|
|
152
|
+
device: selectedDevice,
|
|
153
|
+
inspector: nextDevice.inspector(codegenInspectorOptions(selectedDevice))
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
await runInspectorAppAction(device, action, options);
|
|
157
|
+
},
|
|
158
|
+
listDevices: () => runtime.listDevices(),
|
|
159
|
+
switchDevice: async (deviceId) => {
|
|
160
|
+
const devices = await runtime.listDevices();
|
|
161
|
+
const next = selectCodegenDevice(devices, deviceId, parsed.deviceKind);
|
|
162
|
+
if (!next) {
|
|
163
|
+
throw new Error(`No ready device found for ${deviceId}.`);
|
|
164
|
+
}
|
|
165
|
+
const nextConfig = buildCodegenConfig(next, {
|
|
166
|
+
...parsed,
|
|
167
|
+
platform: next.platform,
|
|
168
|
+
deviceId: next.id,
|
|
169
|
+
launch: false
|
|
170
|
+
});
|
|
171
|
+
const nextDevice = await createCodegenDevice(runtime, nextConfig, {
|
|
172
|
+
forceIosAppInstall: next.platform === 'ios' && hasConfiguredAppPath(nextConfig)
|
|
173
|
+
});
|
|
174
|
+
const previousDevice = device;
|
|
175
|
+
device = nextDevice;
|
|
176
|
+
selectedDevice = next;
|
|
177
|
+
await previousDevice.close().catch(() => undefined);
|
|
178
|
+
return {
|
|
179
|
+
device: next,
|
|
180
|
+
inspector: nextDevice.inspector(codegenInspectorOptions(next))
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
onListen: (port) => {
|
|
184
|
+
const url = `http://localhost:${port}`;
|
|
185
|
+
console.log(`\n${colors.bold('device')} ${selectedDevice.platform} ${selectedDevice.name} (${selectedDevice.id})`);
|
|
186
|
+
console.log(`${colors.bold('ui')} ${colors.green('live')} ${colors.dim(url)}`);
|
|
187
|
+
console.log(`${colors.bold('ready')} ${colors.yellow('pending')} ${colors.dim('the mirror becomes live on the first screen frame; the UI tree can finish loading separately')}`);
|
|
188
|
+
openBrowser(url);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.error(`\n${colors.red('Failed to start inspector server:')} ${String(err)}`);
|
|
194
|
+
await device.close().catch(() => undefined);
|
|
195
|
+
process.exitCode = 1;
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Guard against re-entrancy: an impatient user mashing Ctrl-C would
|
|
199
|
+
// otherwise fire cleanup() repeatedly, each call re-attaching exit/error
|
|
200
|
+
// listeners to the agent ChildProcess (the MaxListenersExceededWarning) and
|
|
201
|
+
// racing multiple teardown chains.
|
|
202
|
+
let cleaningUp = false;
|
|
203
|
+
const cleanup = async () => {
|
|
204
|
+
if (cleaningUp) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
cleaningUp = true;
|
|
208
|
+
handle.close();
|
|
209
|
+
await device.close().catch(() => undefined);
|
|
210
|
+
process.exit(0);
|
|
211
|
+
};
|
|
212
|
+
process.once('SIGINT', cleanup);
|
|
213
|
+
process.once('SIGTERM', cleanup);
|
|
214
|
+
await new Promise(() => undefined);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// ── CLI / JSON path ────────────────────────────────────────────────────────
|
|
218
|
+
const bootstrap = await bootstrapCodegen(runtime, selected, parsed);
|
|
219
|
+
if (parsed.json) {
|
|
220
|
+
console.log(JSON.stringify({
|
|
221
|
+
command: 'codegen',
|
|
222
|
+
selectedDevice: {
|
|
223
|
+
id: bootstrap.selected.id,
|
|
224
|
+
name: bootstrap.selected.name,
|
|
225
|
+
platform: bootstrap.selected.platform,
|
|
226
|
+
kind: bootstrap.selected.kind,
|
|
227
|
+
state: bootstrap.selected.state
|
|
228
|
+
},
|
|
229
|
+
app: {
|
|
230
|
+
launched: bootstrap.launched,
|
|
231
|
+
warning: bootstrap.launchWarning
|
|
232
|
+
},
|
|
233
|
+
tree: {
|
|
234
|
+
nodes: bootstrap.treeNodeCount,
|
|
235
|
+
visibleNodes: bootstrap.visibleNodeCount
|
|
236
|
+
},
|
|
237
|
+
suggestion: bootstrap.suggestion?.code,
|
|
238
|
+
suggestions: bootstrap.suggestions.map((candidate) => ({
|
|
239
|
+
code: candidate.code,
|
|
240
|
+
score: candidate.score
|
|
241
|
+
}))
|
|
242
|
+
}, null, 2));
|
|
243
|
+
await bootstrap.device.close().catch(() => undefined);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
await bootstrap.device.close().catch(() => undefined);
|
|
247
|
+
printHeader('codegen', 'Playwright-style mobile inspector bootstrap');
|
|
248
|
+
console.log(`\n${colors.bold('device')} ${bootstrap.selected.platform} ${bootstrap.selected.name} (${bootstrap.selected.id})`);
|
|
249
|
+
console.log(`${colors.bold('mirror')} ready for inspector frontend attachment`);
|
|
250
|
+
if (bootstrap.launched) {
|
|
251
|
+
console.log(`${colors.bold('app')} launched on selected device`);
|
|
252
|
+
}
|
|
253
|
+
else if (bootstrap.launchWarning) {
|
|
254
|
+
console.log(`${colors.bold('app')} ${colors.yellow('launch skipped')} ${colors.dim(`(${bootstrap.launchWarning})`)}`);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
console.log(`${colors.bold('app')} ${colors.dim('not launched (pass --app or --app-id for automatic launch)')}`);
|
|
258
|
+
}
|
|
259
|
+
console.log(`${colors.bold('tree')} ${bootstrap.treeNodeCount} nodes (${bootstrap.visibleNodeCount} visible)`);
|
|
260
|
+
if (bootstrap.suggestion) {
|
|
261
|
+
console.log(`${colors.bold('locator')} ${colors.green(bootstrap.suggestion.code)} ${colors.dim(`score=${bootstrap.suggestion.score}`)}`);
|
|
262
|
+
}
|
|
263
|
+
console.log(`\n${colors.bold('next')} start recording interactions in the inspector UI layer and convert actions into Astur codegen snippets.`);
|
|
264
|
+
}
|
|
265
|
+
function runPlaywright(args) {
|
|
266
|
+
const bin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
267
|
+
const child = spawn(bin, ['playwright', 'test', ...args], {
|
|
268
|
+
stdio: 'inherit',
|
|
269
|
+
shell: false
|
|
270
|
+
});
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
child.on('error', reject);
|
|
273
|
+
child.on('exit', (code, signal) => {
|
|
274
|
+
if (signal) {
|
|
275
|
+
reject(new Error(`Playwright was terminated by ${signal}.`));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
process.exitCode = code ?? 1;
|
|
279
|
+
resolve();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async function writeIfMissing(path, contents) {
|
|
284
|
+
try {
|
|
285
|
+
await access(path, constants.F_OK);
|
|
286
|
+
console.log(`Skipped existing ${path}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
await mkdir(dirname(path), { recursive: true });
|
|
291
|
+
await writeFile(path, contents, 'utf8');
|
|
292
|
+
console.log(`Created ${path}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function printChecks(checks, options = {}) {
|
|
296
|
+
printHeader('doctor', 'Environment diagnostics');
|
|
297
|
+
for (const [group, groupChecks] of groupChecksByPlatform(checks)) {
|
|
298
|
+
console.log(`\n${colors.dim('◦')} ${colors.bold(group)}`);
|
|
299
|
+
for (const check of groupChecks) {
|
|
300
|
+
const status = renderStatus(check.status);
|
|
301
|
+
const message = options.verbose ? check.message : compactMessage(check.message);
|
|
302
|
+
console.log(` ${status} ${colors.bold(check.label.padEnd(18))} ${message}`);
|
|
303
|
+
if (!options.verbose && check.message !== message) {
|
|
304
|
+
console.log(` ${colors.dim('details hidden; run astur doctor --verbose')}`);
|
|
305
|
+
}
|
|
306
|
+
if (check.fix) {
|
|
307
|
+
console.log(` ${colors.dim('fix')} ${colors.yellow(check.fix)}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const failures = checks.filter((check) => check.status === 'fail').length;
|
|
312
|
+
const warnings = checks.filter((check) => check.status === 'warn').length;
|
|
313
|
+
const skipped = checks.filter((check) => check.status === 'skip').length;
|
|
314
|
+
const skippedSuffix = skipped > 0 ? `, ${skipped} skipped` : '';
|
|
315
|
+
const summary = failures > 0
|
|
316
|
+
? colors.red(`${failures} failed, ${warnings} warning(s)${skippedSuffix}`)
|
|
317
|
+
: warnings > 0
|
|
318
|
+
? colors.yellow(`${warnings} warning(s)${skippedSuffix}, ready with limits`)
|
|
319
|
+
: skipped > 0
|
|
320
|
+
? colors.cyan(`${skipped} skipped, ready for supported platforms`)
|
|
321
|
+
: colors.green('all checks passed');
|
|
322
|
+
console.log(`\n${colors.dim('summary')} ${summary}`);
|
|
323
|
+
printNextSteps(checks);
|
|
324
|
+
}
|
|
325
|
+
function printDevices(devices) {
|
|
326
|
+
printHeader('devices', 'Connected devices and available simulators');
|
|
327
|
+
if (!devices.length) {
|
|
328
|
+
console.log(`\n${colors.yellow('No devices found.')}`);
|
|
329
|
+
console.log(colors.dim('Start an Android emulator, connect a device, or install an iOS simulator runtime.'));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
console.log('');
|
|
333
|
+
for (const device of devices) {
|
|
334
|
+
const version = device.osVersion ? ` ${device.osVersion}` : '';
|
|
335
|
+
const state = renderDeviceState(device.state);
|
|
336
|
+
const platform = device.platform === 'android' ? colors.green('android') : colors.cyan('ios');
|
|
337
|
+
console.log(` ${platform.padEnd(16)} ${colors.dim(device.kind.padEnd(9))} ${state} ${colors.dim(device.id)} ${device.name}${version}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function help() {
|
|
341
|
+
printHeader('cli', 'Native mobile automation');
|
|
342
|
+
console.log(`
|
|
343
|
+
${colors.bold('Usage')}
|
|
344
|
+
astur doctor Check Android, iOS, and agent prerequisites
|
|
345
|
+
astur doctor --verbose
|
|
346
|
+
astur devices List connected devices and available simulators
|
|
347
|
+
astur init Create starter Playwright config and test interactively
|
|
348
|
+
astur init --yes Create starter files with Android emulator defaults
|
|
349
|
+
astur codegen Bootstrap runtime-backed inspector/codegen session
|
|
350
|
+
astur test [args] Run Playwright Test
|
|
351
|
+
astur inspect Alias for astur codegen
|
|
352
|
+
`);
|
|
353
|
+
}
|
|
354
|
+
function printCodegenHelp() {
|
|
355
|
+
printHeader('codegen', 'Playwright-style mobile inspector bootstrap');
|
|
356
|
+
console.log(`
|
|
357
|
+
${colors.bold('Usage')}
|
|
358
|
+
astur codegen [options]
|
|
359
|
+
|
|
360
|
+
${colors.bold('Options')}
|
|
361
|
+
--android Target Android devices only
|
|
362
|
+
--ios Target iOS devices only
|
|
363
|
+
--emulator Target Android emulators only
|
|
364
|
+
--simulator Target iOS simulators only
|
|
365
|
+
--real Target real devices only
|
|
366
|
+
--platform <name> Explicit platform: android or ios
|
|
367
|
+
--device <id> Prefer a specific device id/UDID
|
|
368
|
+
--app <path> App path to install/use for launch (.apk, .app, .ipa)
|
|
369
|
+
--app-id <id> Installed package (Android) or bundle id (iOS)
|
|
370
|
+
--ui Open Astur Inspector UI (default)
|
|
371
|
+
--no-ui Print CLI summary only
|
|
372
|
+
--no-launch Skip app launch even if app metadata is available
|
|
373
|
+
--json Print machine-readable bootstrap output
|
|
374
|
+
-h, --help Show this help
|
|
375
|
+
`);
|
|
376
|
+
}
|
|
377
|
+
function printHeader(command, subtitle) {
|
|
378
|
+
console.log(`${colors.cyan('Astur')} ${colors.dim('›')} ${colors.bold(command)}`);
|
|
379
|
+
console.log(colors.dim(subtitle));
|
|
380
|
+
}
|
|
381
|
+
function groupChecksByPlatform(checks) {
|
|
382
|
+
const groups = new Map();
|
|
383
|
+
for (const check of checks) {
|
|
384
|
+
const key = check.id.startsWith('android.')
|
|
385
|
+
? 'Android'
|
|
386
|
+
: check.id.startsWith('ios.')
|
|
387
|
+
? 'iOS'
|
|
388
|
+
: 'General';
|
|
389
|
+
groups.set(key, [...(groups.get(key) ?? []), check]);
|
|
390
|
+
}
|
|
391
|
+
return [...groups.entries()];
|
|
392
|
+
}
|
|
393
|
+
function renderStatus(status) {
|
|
394
|
+
switch (status) {
|
|
395
|
+
case 'pass':
|
|
396
|
+
return colors.green('✓ PASS');
|
|
397
|
+
case 'warn':
|
|
398
|
+
return colors.yellow('⚠ WARN');
|
|
399
|
+
case 'fail':
|
|
400
|
+
return colors.red('✕ FAIL');
|
|
401
|
+
case 'skip':
|
|
402
|
+
return colors.cyan('• SKIP');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function renderDeviceState(state) {
|
|
406
|
+
switch (state) {
|
|
407
|
+
case 'online':
|
|
408
|
+
case 'booted':
|
|
409
|
+
return colors.green(state.padEnd(8));
|
|
410
|
+
case 'offline':
|
|
411
|
+
case 'unauthorized':
|
|
412
|
+
return colors.red(state.padEnd(8));
|
|
413
|
+
case 'shutdown':
|
|
414
|
+
return colors.yellow(state.padEnd(8));
|
|
415
|
+
case 'unknown':
|
|
416
|
+
return colors.dim(state.padEnd(8));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function compactMessage(message) {
|
|
420
|
+
const lines = message
|
|
421
|
+
.split(/\r?\n/)
|
|
422
|
+
.map((line) => line.trim())
|
|
423
|
+
.filter(Boolean);
|
|
424
|
+
return lines[0] ?? message;
|
|
425
|
+
}
|
|
426
|
+
function printNextSteps(checks) {
|
|
427
|
+
const actionable = checks.filter((check) => check.status !== 'pass' && check.fix);
|
|
428
|
+
if (!actionable.length) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
console.log(`\n${colors.bold('next steps')}`);
|
|
432
|
+
for (const [index, check] of actionable.entries()) {
|
|
433
|
+
console.log(` ${index + 1}. ${colors.bold(check.label)}: ${check.fix}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function supportsLocalIos() {
|
|
437
|
+
return process.platform === 'darwin';
|
|
438
|
+
}
|
|
439
|
+
function parseCodegenArgs(args) {
|
|
440
|
+
const parsed = {
|
|
441
|
+
help: false,
|
|
442
|
+
json: false,
|
|
443
|
+
ui: true,
|
|
444
|
+
launch: true
|
|
445
|
+
};
|
|
446
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
447
|
+
const token = args[index];
|
|
448
|
+
switch (token) {
|
|
449
|
+
case '-h':
|
|
450
|
+
case '--help':
|
|
451
|
+
parsed.help = true;
|
|
452
|
+
break;
|
|
453
|
+
case '--json':
|
|
454
|
+
parsed.json = true;
|
|
455
|
+
parsed.ui = false;
|
|
456
|
+
break;
|
|
457
|
+
case '--ui':
|
|
458
|
+
parsed.ui = true;
|
|
459
|
+
break;
|
|
460
|
+
case '--no-ui':
|
|
461
|
+
parsed.ui = false;
|
|
462
|
+
break;
|
|
463
|
+
case '--launch':
|
|
464
|
+
parsed.launch = true;
|
|
465
|
+
break;
|
|
466
|
+
case '--no-launch':
|
|
467
|
+
parsed.launch = false;
|
|
468
|
+
break;
|
|
469
|
+
case '--android':
|
|
470
|
+
parsed.platform = 'android';
|
|
471
|
+
break;
|
|
472
|
+
case '--ios':
|
|
473
|
+
parsed.platform = 'ios';
|
|
474
|
+
break;
|
|
475
|
+
case '--emulator':
|
|
476
|
+
parsed.platform = 'android';
|
|
477
|
+
parsed.deviceKind = 'emulator';
|
|
478
|
+
break;
|
|
479
|
+
case '--simulator':
|
|
480
|
+
parsed.platform = 'ios';
|
|
481
|
+
parsed.deviceKind = 'simulator';
|
|
482
|
+
break;
|
|
483
|
+
case '--real':
|
|
484
|
+
parsed.deviceKind = 'real';
|
|
485
|
+
break;
|
|
486
|
+
case '--platform': {
|
|
487
|
+
const value = args[index + 1];
|
|
488
|
+
if (!value) {
|
|
489
|
+
throw new Error('--platform requires a value: android or ios');
|
|
490
|
+
}
|
|
491
|
+
if (value !== 'android' && value !== 'ios') {
|
|
492
|
+
throw new Error(`Unsupported --platform value: ${value}`);
|
|
493
|
+
}
|
|
494
|
+
parsed.platform = value;
|
|
495
|
+
index += 1;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
case '--device': {
|
|
499
|
+
const value = args[index + 1];
|
|
500
|
+
if (!value) {
|
|
501
|
+
throw new Error('--device requires a value');
|
|
502
|
+
}
|
|
503
|
+
parsed.deviceId = value;
|
|
504
|
+
index += 1;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case '--app': {
|
|
508
|
+
const value = args[index + 1];
|
|
509
|
+
if (!value) {
|
|
510
|
+
throw new Error('--app requires a value');
|
|
511
|
+
}
|
|
512
|
+
parsed.appPath = value;
|
|
513
|
+
index += 1;
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
case '--app-id': {
|
|
517
|
+
const value = args[index + 1];
|
|
518
|
+
if (!value) {
|
|
519
|
+
throw new Error('--app-id requires a value');
|
|
520
|
+
}
|
|
521
|
+
parsed.appId = value;
|
|
522
|
+
index += 1;
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
default:
|
|
526
|
+
if (token.startsWith('-')) {
|
|
527
|
+
throw new Error(`Unknown codegen option: ${token}`);
|
|
528
|
+
}
|
|
529
|
+
throw new Error(`Unexpected positional argument for codegen: ${token}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return parsed;
|
|
533
|
+
}
|
|
534
|
+
function selectCodegenDevice(devices, deviceId, deviceKind) {
|
|
535
|
+
const isReady = (device) => device.state === 'online' || device.state === 'booted';
|
|
536
|
+
const isSelectable = (device) => isReady(device) || (deviceKind === 'simulator' && device.state === 'shutdown');
|
|
537
|
+
const matchesKind = (device) => !deviceKind || device.kind === deviceKind;
|
|
538
|
+
if (deviceId) {
|
|
539
|
+
return devices.find((device) => device.id === deviceId && matchesKind(device) && isSelectable(device));
|
|
540
|
+
}
|
|
541
|
+
return devices.find((device) => matchesKind(device) && isReady(device))
|
|
542
|
+
?? devices.find((device) => matchesKind(device) && isSelectable(device));
|
|
543
|
+
}
|
|
544
|
+
async function bootstrapCodegen(runtime, selected, parsed) {
|
|
545
|
+
const config = buildCodegenConfig(selected, parsed);
|
|
546
|
+
const device = await runtime.createDevice(config);
|
|
547
|
+
let launched = false;
|
|
548
|
+
let launchWarning;
|
|
549
|
+
try {
|
|
550
|
+
if (parsed.launch && config.app) {
|
|
551
|
+
await device.app.launch();
|
|
552
|
+
launched = true;
|
|
553
|
+
}
|
|
554
|
+
else if (parsed.launch && !config.app) {
|
|
555
|
+
launchWarning = 'no app was provided';
|
|
556
|
+
}
|
|
557
|
+
const inspector = device.inspector({ pollIntervalMs: 500 });
|
|
558
|
+
const update = await firstTreeUpdate(inspector);
|
|
559
|
+
const nodes = flattenTree(update.root);
|
|
560
|
+
const locatorContext = await bestLocatorContext(device, update.root);
|
|
561
|
+
return {
|
|
562
|
+
selected,
|
|
563
|
+
launched,
|
|
564
|
+
launchWarning,
|
|
565
|
+
tree: update.root,
|
|
566
|
+
treeNodeCount: nodes.length,
|
|
567
|
+
visibleNodeCount: nodes.filter((node) => node.visible).length,
|
|
568
|
+
suggestions: locatorContext?.suggestions ?? [],
|
|
569
|
+
suggestion: locatorContext?.suggestions[0],
|
|
570
|
+
device
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
catch (err) {
|
|
574
|
+
await device.close().catch(() => undefined);
|
|
575
|
+
throw err;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function buildCodegenConfig(selected, parsed) {
|
|
579
|
+
const appId = parsed.appId ?? defaultCodegenAppId(selected.platform);
|
|
580
|
+
const config = {
|
|
581
|
+
platform: selected.platform,
|
|
582
|
+
device: {
|
|
583
|
+
id: selected.id
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
if (!parsed.appPath && !appId) {
|
|
587
|
+
return config;
|
|
588
|
+
}
|
|
589
|
+
if (selected.platform === 'android') {
|
|
590
|
+
config.app = {
|
|
591
|
+
path: parsed.appPath,
|
|
592
|
+
packageName: appId
|
|
593
|
+
};
|
|
594
|
+
return config;
|
|
595
|
+
}
|
|
596
|
+
config.app = {
|
|
597
|
+
path: parsed.appPath,
|
|
598
|
+
bundleId: appId
|
|
599
|
+
};
|
|
600
|
+
config.timeout = 60_000;
|
|
601
|
+
config.agent = {
|
|
602
|
+
mode: 'required',
|
|
603
|
+
install: true,
|
|
604
|
+
legacyFallback: 'never',
|
|
605
|
+
launchTimeout: 60_000,
|
|
606
|
+
commandTimeout: 20_000
|
|
607
|
+
};
|
|
608
|
+
return config;
|
|
609
|
+
}
|
|
610
|
+
function printCodegenPreparation(selected, config) {
|
|
611
|
+
const details = codegenPreparationDetails(selected, config);
|
|
612
|
+
console.log(`\n${colors.bold('status')} ${colors.yellow('preparing')} ${details.title}`);
|
|
613
|
+
for (const detail of details.notes) {
|
|
614
|
+
console.log(` ${colors.dim(detail)}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function codegenPreparationDetails(selected, config) {
|
|
618
|
+
if (selected.platform === 'ios') {
|
|
619
|
+
const notes = [
|
|
620
|
+
'Astur is installing/starting the bundled XCUITest agent and preparing the app.',
|
|
621
|
+
'The mirror becomes usable after the first screen frame; the UI tree may continue loading separately.'
|
|
622
|
+
];
|
|
623
|
+
if (selected.kind === 'real') {
|
|
624
|
+
notes.unshift('First real-device runs can take a few minutes while Xcode builds and signs the agent.');
|
|
625
|
+
}
|
|
626
|
+
if (!config.app) {
|
|
627
|
+
notes.push('No app was provided, so Astur will inspect the currently available device state.');
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
title: selected.kind === 'real' ? 'iOS real device' : 'iOS simulator',
|
|
631
|
+
notes
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
title: selected.kind === 'emulator' ? 'Android emulator' : 'Android device',
|
|
636
|
+
notes: [
|
|
637
|
+
'Astur is connecting to the device, preparing automation services, and launching the app when configured.',
|
|
638
|
+
'The mirror becomes usable after the first screen frame; the UI tree may continue loading separately.'
|
|
639
|
+
]
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function codegenInspectorOptions(device) {
|
|
643
|
+
return {
|
|
644
|
+
pollIntervalMs: device.platform === 'ios' ? 2_000 : 500
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function formatCodegenConnectionError(error) {
|
|
648
|
+
if (!(error instanceof AsturError)) {
|
|
649
|
+
return error instanceof Error ? error.message : String(error);
|
|
650
|
+
}
|
|
651
|
+
const lines = [`${error.code}: ${error.message}`];
|
|
652
|
+
const details = isRecord(error.details) ? error.details : undefined;
|
|
653
|
+
const bundleId = typeof details?.bundleId === 'string' ? details.bundleId : undefined;
|
|
654
|
+
switch (error.code) {
|
|
655
|
+
case 'IOS_APP_INSTALL_SIGNATURE_INVALID':
|
|
656
|
+
lines.push('Fix: rebuild or re-sign the IPA with a non-expired provisioning profile that includes this iPhone UDID, then run codegen again.');
|
|
657
|
+
break;
|
|
658
|
+
case 'IOS_APP_SIGNATURE_NOT_TRUSTED':
|
|
659
|
+
lines.push('Fix: install an IPA signed for this iPhone UDID, trust the developer profile on the phone, and make sure --app-id matches the IPA bundle id.');
|
|
660
|
+
if (bundleId) {
|
|
661
|
+
lines.push(`Bundle id: ${bundleId}`);
|
|
662
|
+
}
|
|
663
|
+
break;
|
|
664
|
+
case 'IOS_DEVELOPMENT_TEAM_REQUIRED':
|
|
665
|
+
lines.push('Fix: set ASTUR_IOS_DEVELOPMENT_TEAM to your Apple development team id, then run codegen again.');
|
|
666
|
+
break;
|
|
667
|
+
case 'IOS_SIGNING_KEYCHAIN_LOCKED':
|
|
668
|
+
lines.push('Fix: unlock the macOS login keychain or allow codesign/Xcode access to the Apple Development certificate.');
|
|
669
|
+
break;
|
|
670
|
+
case 'IOS_AGENT_HOST_REQUIRED':
|
|
671
|
+
lines.push('Fix: set ASTUR_IOS_AGENT_HOST to a Mac IP address reachable by the iPhone.');
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
const commandOutput = typeof details?.commandOutput === 'string' ? details.commandOutput.trim() : '';
|
|
675
|
+
if (commandOutput) {
|
|
676
|
+
const tail = commandOutput.split(/\r?\n/).slice(-12).join('\n');
|
|
677
|
+
lines.push(`${colors.dim('install output:')}\n${tail}`);
|
|
678
|
+
}
|
|
679
|
+
const xcodebuildOutput = typeof details?.xcodebuildOutput === 'string' ? details.xcodebuildOutput.trim() : '';
|
|
680
|
+
if (xcodebuildOutput) {
|
|
681
|
+
const tail = xcodebuildOutput.split(/\r?\n/).slice(-12).join('\n');
|
|
682
|
+
lines.push(`${colors.dim('xcodebuild tail:')}\n${tail}`);
|
|
683
|
+
}
|
|
684
|
+
return lines.join('\n');
|
|
685
|
+
}
|
|
686
|
+
function isRecord(value) {
|
|
687
|
+
return typeof value === 'object' && value !== null;
|
|
688
|
+
}
|
|
689
|
+
async function createCodegenDevice(runtime, config, options = {}) {
|
|
690
|
+
if (!options.forceIosAppInstall || config.platform !== 'ios') {
|
|
691
|
+
return runtime.createDevice(config);
|
|
692
|
+
}
|
|
693
|
+
const previous = process.env.ASTUR_IOS_APP_FORCE_INSTALL;
|
|
694
|
+
process.env.ASTUR_IOS_APP_FORCE_INSTALL = '1';
|
|
695
|
+
try {
|
|
696
|
+
return await runtime.createDevice(config);
|
|
697
|
+
}
|
|
698
|
+
finally {
|
|
699
|
+
if (previous === undefined) {
|
|
700
|
+
delete process.env.ASTUR_IOS_APP_FORCE_INSTALL;
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
process.env.ASTUR_IOS_APP_FORCE_INSTALL = previous;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function hasConfiguredAppPath(config) {
|
|
708
|
+
return typeof config.app === 'string'
|
|
709
|
+
? config.app.length > 0
|
|
710
|
+
: Boolean(config.app?.path);
|
|
711
|
+
}
|
|
712
|
+
function defaultCodegenAppId(platform) {
|
|
713
|
+
if (process.env.ASTUR_CODEGEN_APP_ID) {
|
|
714
|
+
return process.env.ASTUR_CODEGEN_APP_ID;
|
|
715
|
+
}
|
|
716
|
+
if (platform === 'android') {
|
|
717
|
+
return process.env.ASTUR_ANDROID_PACKAGE_NAME ?? process.env.ASTUR_ANDROID_APP_ID;
|
|
718
|
+
}
|
|
719
|
+
return process.env.ASTUR_IOS_BUNDLE_ID
|
|
720
|
+
?? process.env.ASTUR_AUT_BUNDLE_ID
|
|
721
|
+
?? 'com.astur.demo';
|
|
722
|
+
}
|
|
723
|
+
async function runInspectorDeviceAction(device, action) {
|
|
724
|
+
switch (action) {
|
|
725
|
+
case 'refresh':
|
|
726
|
+
case 'tree.refresh':
|
|
727
|
+
return;
|
|
728
|
+
case 'orientation.portrait':
|
|
729
|
+
await device.orientation.portrait();
|
|
730
|
+
return;
|
|
731
|
+
case 'orientation.landscape':
|
|
732
|
+
await device.orientation.landscape();
|
|
733
|
+
return;
|
|
734
|
+
case 'keyboard.dismiss':
|
|
735
|
+
await device.keyboard.dismiss();
|
|
736
|
+
return;
|
|
737
|
+
case 'device.lock':
|
|
738
|
+
await device.system.lock();
|
|
739
|
+
return;
|
|
740
|
+
case 'device.unlock':
|
|
741
|
+
await device.system.unlock();
|
|
742
|
+
return;
|
|
743
|
+
case 'navigation.back':
|
|
744
|
+
await device.navigation.back();
|
|
745
|
+
return;
|
|
746
|
+
case 'navigation.home':
|
|
747
|
+
await device.navigation.home();
|
|
748
|
+
return;
|
|
749
|
+
case 'navigation.recents':
|
|
750
|
+
await device.navigation.recentApps();
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
async function runInspectorAppAction(device, action, options) {
|
|
755
|
+
switch (action) {
|
|
756
|
+
case 'launch': {
|
|
757
|
+
const identifier = requireInspectorAppIdentifier(options.identifier);
|
|
758
|
+
await device.app.launch({
|
|
759
|
+
app: device.deviceInfo.platform === 'android'
|
|
760
|
+
? { packageName: identifier }
|
|
761
|
+
: { bundleId: identifier }
|
|
762
|
+
});
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
case 'clearData':
|
|
766
|
+
await device.app.clearData(requireInspectorAppIdentifier(options.identifier));
|
|
767
|
+
return;
|
|
768
|
+
case 'clearCache':
|
|
769
|
+
await device.app.clearCache(requireInspectorAppIdentifier(options.identifier));
|
|
770
|
+
return;
|
|
771
|
+
case 'grantPermission':
|
|
772
|
+
await device.permissions.grant(requireInspectorPermission(options.permission), requireInspectorAppIdentifier(options.identifier));
|
|
773
|
+
return;
|
|
774
|
+
case 'revokePermission':
|
|
775
|
+
await device.permissions.revoke(requireInspectorPermission(options.permission), requireInspectorAppIdentifier(options.identifier));
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function requireInspectorAppIdentifier(identifier) {
|
|
780
|
+
const value = identifier?.trim();
|
|
781
|
+
if (!value) {
|
|
782
|
+
throw new Error('Package or bundle id is required.');
|
|
783
|
+
}
|
|
784
|
+
return value;
|
|
785
|
+
}
|
|
786
|
+
function requireInspectorPermission(permission) {
|
|
787
|
+
const value = permission?.trim();
|
|
788
|
+
if (!value) {
|
|
789
|
+
throw new Error('Permission is required.');
|
|
790
|
+
}
|
|
791
|
+
return value;
|
|
792
|
+
}
|
|
793
|
+
async function firstTreeUpdate(inspector) {
|
|
794
|
+
for await (const update of inspector.subscribeTree({ maxUpdates: 1 })) {
|
|
795
|
+
return update;
|
|
796
|
+
}
|
|
797
|
+
throw new Error('Failed to receive an initial UI tree update for codegen bootstrap.');
|
|
798
|
+
}
|
|
799
|
+
async function bestLocatorContext(device, root) {
|
|
800
|
+
const target = flattenTree(root).find((node) => {
|
|
801
|
+
return node.visible
|
|
802
|
+
&& node.enabled
|
|
803
|
+
&& node.type !== 'android.root'
|
|
804
|
+
&& node.type !== 'ios.root'
|
|
805
|
+
&& Boolean(node.id || node.label || node.text);
|
|
806
|
+
});
|
|
807
|
+
if (!target) {
|
|
808
|
+
return undefined;
|
|
809
|
+
}
|
|
810
|
+
const selector = selectorFromNode(target);
|
|
811
|
+
if (!selector) {
|
|
812
|
+
return undefined;
|
|
813
|
+
}
|
|
814
|
+
const suggestions = await device.suggestLocators(selector);
|
|
815
|
+
if (!suggestions.length) {
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
return { suggestions };
|
|
819
|
+
}
|
|
820
|
+
function openBrowser(url) {
|
|
821
|
+
try {
|
|
822
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
823
|
+
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
// ignore
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function selectorFromNode(node) {
|
|
830
|
+
if (node.id) {
|
|
831
|
+
return {
|
|
832
|
+
strategy: 'id',
|
|
833
|
+
value: node.id,
|
|
834
|
+
exact: true
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
if (node.label) {
|
|
838
|
+
return {
|
|
839
|
+
strategy: 'accessibility',
|
|
840
|
+
value: node.label,
|
|
841
|
+
exact: true
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
if (node.text) {
|
|
845
|
+
return {
|
|
846
|
+
strategy: 'text',
|
|
847
|
+
value: node.text,
|
|
848
|
+
exact: true
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
return undefined;
|
|
852
|
+
}
|
|
853
|
+
function flattenTree(root) {
|
|
854
|
+
return [root, ...root.children.flatMap((child) => flattenTree(child))];
|
|
855
|
+
}
|
|
856
|
+
function createIosSkipCheck() {
|
|
857
|
+
const host = hostName();
|
|
858
|
+
return {
|
|
859
|
+
id: 'ios.platform',
|
|
860
|
+
label: 'iOS platform',
|
|
861
|
+
status: 'skip',
|
|
862
|
+
message: `Local iOS automation requires macOS with Xcode. Current host is ${host}.`,
|
|
863
|
+
fix: 'Use macOS for iOS simulators/devices, or use Android-only checks on this host.'
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
function hostName() {
|
|
867
|
+
switch (process.platform) {
|
|
868
|
+
case 'win32':
|
|
869
|
+
return 'Windows';
|
|
870
|
+
case 'linux':
|
|
871
|
+
return 'Linux';
|
|
872
|
+
case 'darwin':
|
|
873
|
+
return 'macOS';
|
|
874
|
+
default:
|
|
875
|
+
return process.platform;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
const shouldColor = process.env.NO_COLOR === undefined && process.stdout.isTTY;
|
|
879
|
+
const colors = {
|
|
880
|
+
bold: (value) => color(value, '1'),
|
|
881
|
+
dim: (value) => color(value, '2'),
|
|
882
|
+
green: (value) => color(value, '32'),
|
|
883
|
+
yellow: (value) => color(value, '33'),
|
|
884
|
+
red: (value) => color(value, '31'),
|
|
885
|
+
cyan: (value) => color(value, '36')
|
|
886
|
+
};
|
|
887
|
+
function color(value, code) {
|
|
888
|
+
return shouldColor ? `\u001b[${code}m${value}\u001b[0m` : value;
|
|
889
|
+
}
|
|
890
|
+
function isDirectEntry() {
|
|
891
|
+
if (!process.argv[1]) {
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
try {
|
|
895
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
return fileURLToPath(import.meta.url) === process.argv[1];
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
export const __testing = {
|
|
902
|
+
printChecks,
|
|
903
|
+
buildInitFiles,
|
|
904
|
+
defaultInitAnswers,
|
|
905
|
+
parseCodegenArgs,
|
|
906
|
+
selectCodegenDevice,
|
|
907
|
+
buildCodegenConfig,
|
|
908
|
+
codegenPreparationDetails,
|
|
909
|
+
inspectorServer: inspectorServerTesting
|
|
910
|
+
};
|
|
911
|
+
if (isDirectEntry()) {
|
|
912
|
+
main().catch((error) => {
|
|
913
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
914
|
+
process.exitCode = 1;
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
//# sourceMappingURL=index.js.map
|