@react-native-harness/cli 1.0.0-alpha.10 → 1.0.0-alpha.12
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/README.md +23 -4
- package/dist/bundlers/metro.d.ts.map +1 -1
- package/dist/bundlers/metro.js +8 -4
- package/dist/commands/test.d.ts +2 -1
- package/dist/commands/test.d.ts.map +1 -1
- package/dist/commands/test.js +19 -17
- package/dist/discovery/index.d.ts +3 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +1 -0
- package/dist/discovery/testDiscovery.d.ts +11 -0
- package/dist/discovery/testDiscovery.d.ts.map +1 -0
- package/dist/discovery/testDiscovery.js +29 -0
- package/dist/errors/errorHandler.d.ts.map +1 -1
- package/dist/errors/errorHandler.js +12 -2
- package/dist/errors/errors.d.ts +2 -2
- package/dist/errors/errors.d.ts.map +1 -1
- package/dist/errors/errors.js +6 -1
- package/dist/index.js +20 -4
- package/dist/platforms/android/index.js +1 -1
- package/dist/platforms/platform-registry.d.ts.map +1 -1
- package/dist/platforms/platform-registry.js +2 -0
- package/dist/platforms/vega/build.d.ts +23 -0
- package/dist/platforms/vega/build.d.ts.map +1 -0
- package/dist/platforms/vega/build.js +55 -0
- package/dist/platforms/vega/device.d.ts +57 -0
- package/dist/platforms/vega/device.d.ts.map +1 -0
- package/dist/platforms/vega/device.js +206 -0
- package/dist/platforms/vega/index.d.ts +4 -0
- package/dist/platforms/vega/index.d.ts.map +1 -0
- package/dist/platforms/vega/index.js +75 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/utils/status-formatter.d.ts +27 -0
- package/dist/utils/status-formatter.d.ts.map +1 -0
- package/dist/utils/status-formatter.js +54 -0
- package/package.json +4 -4
- package/src/bundlers/metro.ts +8 -3
- package/src/commands/test.ts +33 -23
- package/src/discovery/index.ts +2 -0
- package/src/discovery/testDiscovery.ts +50 -0
- package/src/errors/errorHandler.ts +16 -4
- package/src/errors/errors.ts +9 -4
- package/src/index.ts +33 -5
- package/src/platforms/android/index.ts +1 -1
- package/src/platforms/platform-registry.ts +2 -0
- package/src/platforms/vega/build.ts +85 -0
- package/src/platforms/vega/device.ts +258 -0
- package/src/platforms/vega/index.ts +107 -0
package/src/commands/test.ts
CHANGED
|
@@ -2,20 +2,14 @@ import {
|
|
|
2
2
|
getBridgeServer,
|
|
3
3
|
type BridgeServer,
|
|
4
4
|
} from '@react-native-harness/bridge/server';
|
|
5
|
+
import { TestExecutionOptions } from '@react-native-harness/bridge';
|
|
5
6
|
import {
|
|
6
7
|
Config,
|
|
7
8
|
getConfig,
|
|
8
9
|
TestRunnerConfig,
|
|
9
10
|
} from '@react-native-harness/config';
|
|
10
11
|
import { getPlatformAdapter } from '../platforms/platform-registry.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
intro,
|
|
14
|
-
logger,
|
|
15
|
-
outro,
|
|
16
|
-
spinner,
|
|
17
|
-
progress,
|
|
18
|
-
} from '@react-native-harness/tools';
|
|
12
|
+
import { intro, logger, outro, spinner } from '@react-native-harness/tools';
|
|
19
13
|
import { type Environment } from '../platforms/platform-adapter.js';
|
|
20
14
|
import { BridgeTimeoutError } from '../errors/errors.js';
|
|
21
15
|
import { assert } from '../utils.js';
|
|
@@ -26,6 +20,10 @@ import {
|
|
|
26
20
|
RunnerNotFoundError,
|
|
27
21
|
} from '../errors/errors.js';
|
|
28
22
|
import { TestSuiteResult } from '@react-native-harness/bridge';
|
|
23
|
+
import {
|
|
24
|
+
discoverTestFiles,
|
|
25
|
+
type TestFilterOptions,
|
|
26
|
+
} from '../discovery/index.js';
|
|
29
27
|
|
|
30
28
|
type TestRunContext = {
|
|
31
29
|
config: Config;
|
|
@@ -87,34 +85,38 @@ const setupEnvironment = async (context: TestRunContext): Promise<void> => {
|
|
|
87
85
|
|
|
88
86
|
const findTestFiles = async (
|
|
89
87
|
context: TestRunContext,
|
|
90
|
-
|
|
88
|
+
options: TestFilterOptions = {}
|
|
91
89
|
): Promise<void> => {
|
|
92
90
|
const discoverSpinner = spinner();
|
|
93
91
|
discoverSpinner.start('Discovering tests');
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
context.testFiles = await discoverTestFiles(
|
|
94
|
+
context.projectRoot,
|
|
95
|
+
context.config.include,
|
|
96
|
+
options
|
|
97
|
+
);
|
|
98
|
+
|
|
100
99
|
discoverSpinner.stop(`Found ${context.testFiles.length} test files`);
|
|
101
100
|
};
|
|
102
101
|
|
|
103
|
-
const runTests = async (
|
|
102
|
+
const runTests = async (
|
|
103
|
+
context: TestRunContext,
|
|
104
|
+
options: TestFilterOptions = {}
|
|
105
|
+
): Promise<void> => {
|
|
104
106
|
const { bridge, environment, testFiles } = context;
|
|
105
107
|
assert(bridge != null, 'Bridge not initialized');
|
|
106
108
|
assert(environment != null, 'Environment not initialized');
|
|
107
109
|
assert(testFiles != null, 'Test files not initialized');
|
|
108
110
|
|
|
109
|
-
let runSpinner =
|
|
111
|
+
let runSpinner = spinner();
|
|
110
112
|
runSpinner.start('Running tests');
|
|
111
113
|
|
|
112
114
|
let shouldRestart = false;
|
|
113
115
|
|
|
114
116
|
for (const testFile of testFiles) {
|
|
115
117
|
if (shouldRestart) {
|
|
116
|
-
runSpinner =
|
|
117
|
-
runSpinner.
|
|
118
|
+
runSpinner = spinner();
|
|
119
|
+
runSpinner.start(`Restarting environment for next test file`);
|
|
118
120
|
|
|
119
121
|
await new Promise((resolve) => {
|
|
120
122
|
bridge.once('ready', resolve);
|
|
@@ -132,15 +134,23 @@ const runTests = async (context: TestRunContext): Promise<void> => {
|
|
|
132
134
|
);
|
|
133
135
|
}
|
|
134
136
|
|
|
135
|
-
|
|
137
|
+
// Pass only testNamePattern to runtime (file filtering already done)
|
|
138
|
+
const executionOptions: TestExecutionOptions = {
|
|
139
|
+
testNamePattern: options.testNamePattern,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const result = await client.runTests(testFile, executionOptions);
|
|
136
143
|
context.results = [...(context.results ?? []), ...result.suites];
|
|
137
144
|
shouldRestart = true;
|
|
145
|
+
runSpinner.stop(`Test file ${testFile} completed`);
|
|
138
146
|
}
|
|
147
|
+
|
|
148
|
+
runSpinner.stop('Tests completed');
|
|
139
149
|
};
|
|
140
150
|
|
|
141
151
|
const cleanUp = async (context: TestRunContext): Promise<void> => {
|
|
142
152
|
if (context.bridge) {
|
|
143
|
-
context.bridge.
|
|
153
|
+
context.bridge.dispose();
|
|
144
154
|
}
|
|
145
155
|
if (context.environment) {
|
|
146
156
|
await context.environment.dispose();
|
|
@@ -172,7 +182,7 @@ const hasFailedTests = (results: TestSuiteResult[]): boolean => {
|
|
|
172
182
|
|
|
173
183
|
export const testCommand = async (
|
|
174
184
|
runnerName?: string,
|
|
175
|
-
|
|
185
|
+
options: TestFilterOptions = {}
|
|
176
186
|
): Promise<void> => {
|
|
177
187
|
intro('React Native Test Harness');
|
|
178
188
|
|
|
@@ -199,8 +209,8 @@ export const testCommand = async (
|
|
|
199
209
|
|
|
200
210
|
try {
|
|
201
211
|
await setupEnvironment(context);
|
|
202
|
-
await findTestFiles(context,
|
|
203
|
-
await runTests(context);
|
|
212
|
+
await findTestFiles(context, options);
|
|
213
|
+
await runTests(context, options);
|
|
204
214
|
|
|
205
215
|
assert(context.results != null, 'Results not initialized');
|
|
206
216
|
config.reporter?.report(context.results);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Glob } from 'glob';
|
|
2
|
+
|
|
3
|
+
export type TestFilterOptions = {
|
|
4
|
+
testNamePattern?: string;
|
|
5
|
+
testPathPattern?: string;
|
|
6
|
+
testPathIgnorePatterns?: string[];
|
|
7
|
+
testMatch?: string[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Discovers test files based on patterns and filtering options
|
|
12
|
+
*/
|
|
13
|
+
export const discoverTestFiles = async (
|
|
14
|
+
projectRoot: string,
|
|
15
|
+
configInclude: string | string[],
|
|
16
|
+
options: TestFilterOptions = {}
|
|
17
|
+
): Promise<string[]> => {
|
|
18
|
+
// Priority: testMatch > configInclude
|
|
19
|
+
const patterns = options.testMatch || configInclude;
|
|
20
|
+
const patternArray = Array.isArray(patterns) ? patterns : [patterns];
|
|
21
|
+
|
|
22
|
+
// Glob discovery
|
|
23
|
+
const allFiles: string[] = [];
|
|
24
|
+
for (const pattern of patternArray) {
|
|
25
|
+
const glob = new Glob(pattern, { cwd: projectRoot, nodir: true });
|
|
26
|
+
const files = await glob.walk();
|
|
27
|
+
allFiles.push(...files);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Remove duplicates
|
|
31
|
+
let uniqueFiles = [...new Set(allFiles)];
|
|
32
|
+
|
|
33
|
+
// Apply testPathPattern filtering
|
|
34
|
+
if (options.testPathPattern) {
|
|
35
|
+
const regex = new RegExp(options.testPathPattern);
|
|
36
|
+
uniqueFiles = uniqueFiles.filter((file) => regex.test(file));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Apply testPathIgnorePatterns filtering
|
|
40
|
+
if (options.testPathIgnorePatterns?.length) {
|
|
41
|
+
const ignoreRegexes = options.testPathIgnorePatterns.map(
|
|
42
|
+
(p) => new RegExp(p)
|
|
43
|
+
);
|
|
44
|
+
uniqueFiles = uniqueFiles.filter(
|
|
45
|
+
(file) => !ignoreRegexes.some((regex) => regex.test(file))
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return uniqueFiles;
|
|
50
|
+
};
|
|
@@ -128,10 +128,14 @@ export const handleError = (error: unknown): void => {
|
|
|
128
128
|
console.error(`\nPlease check your bridge connection and try again.`);
|
|
129
129
|
} else if (error instanceof AppNotInstalledError) {
|
|
130
130
|
console.error(`\n❌ App Not Installed`);
|
|
131
|
+
const deviceType =
|
|
132
|
+
error.platform === 'ios'
|
|
133
|
+
? 'simulator'
|
|
134
|
+
: error.platform === 'android'
|
|
135
|
+
? 'emulator'
|
|
136
|
+
: 'virtual device';
|
|
131
137
|
console.error(
|
|
132
|
-
`\nThe app "${error.bundleId}" is not installed on ${
|
|
133
|
-
error.platform === 'ios' ? 'simulator' : 'emulator'
|
|
134
|
-
} "${error.deviceName}".`
|
|
138
|
+
`\nThe app "${error.bundleId}" is not installed on ${deviceType} "${error.deviceName}".`
|
|
135
139
|
);
|
|
136
140
|
console.error(`\nTo resolve this issue:`);
|
|
137
141
|
if (error.platform === 'ios') {
|
|
@@ -141,13 +145,21 @@ export const handleError = (error: unknown): void => {
|
|
|
141
145
|
console.error(
|
|
142
146
|
` • Or install from Xcode: Open ios/*.xcworkspace and run the project`
|
|
143
147
|
);
|
|
144
|
-
} else {
|
|
148
|
+
} else if (error.platform === 'android') {
|
|
145
149
|
console.error(
|
|
146
150
|
` • Build and install the app: npx react-native run-android`
|
|
147
151
|
);
|
|
148
152
|
console.error(
|
|
149
153
|
` • Or build manually: ./gradlew assembleDebug && adb install android/app/build/outputs/apk/debug/app-debug.apk`
|
|
150
154
|
);
|
|
155
|
+
} else if (error.platform === 'vega') {
|
|
156
|
+
console.error(` • Build the Vega app: npm run build:app`);
|
|
157
|
+
console.error(
|
|
158
|
+
` • Install the app: kepler device install-app -p <path-to-vpkg> --device "${error.deviceName}"`
|
|
159
|
+
);
|
|
160
|
+
console.error(
|
|
161
|
+
` • Or use the combined command: kepler run-kepler <path-to-vpkg> "${error.bundleId}" -d "${error.deviceName}"`
|
|
162
|
+
);
|
|
151
163
|
}
|
|
152
164
|
console.error(`\nPlease install the app and try running the tests again.`);
|
|
153
165
|
} else if (error instanceof BundlingFailedError) {
|
package/src/errors/errors.ts
CHANGED
|
@@ -97,12 +97,17 @@ export class AppNotInstalledError extends Error {
|
|
|
97
97
|
constructor(
|
|
98
98
|
public readonly deviceName: string,
|
|
99
99
|
public readonly bundleId: string,
|
|
100
|
-
public readonly platform: 'ios' | 'android'
|
|
100
|
+
public readonly platform: 'ios' | 'android' | 'vega'
|
|
101
101
|
) {
|
|
102
|
+
const deviceType =
|
|
103
|
+
platform === 'ios'
|
|
104
|
+
? 'simulator'
|
|
105
|
+
: platform === 'android'
|
|
106
|
+
? 'emulator'
|
|
107
|
+
: 'virtual device';
|
|
108
|
+
|
|
102
109
|
super(
|
|
103
|
-
`App "${bundleId}" is not installed on ${
|
|
104
|
-
platform === 'ios' ? 'simulator' : 'emulator'
|
|
105
|
-
} "${deviceName}"`
|
|
110
|
+
`App "${bundleId}" is not installed on ${deviceType} "${deviceName}"`
|
|
106
111
|
);
|
|
107
112
|
this.name = 'AppNotInstalledError';
|
|
108
113
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,14 +13,20 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
|
13
13
|
|
|
14
14
|
const program = new Command();
|
|
15
15
|
|
|
16
|
-
logger.setVerbose(true);
|
|
17
|
-
|
|
18
16
|
program
|
|
19
17
|
.name('react-native-harness')
|
|
20
18
|
.description(
|
|
21
19
|
'React Native Test Harness - A comprehensive testing framework for React Native applications'
|
|
22
20
|
)
|
|
23
|
-
.version(packageJson.version)
|
|
21
|
+
.version(packageJson.version)
|
|
22
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
23
|
+
.hook('preAction', (thisCommand) => {
|
|
24
|
+
// Handle global verbose option
|
|
25
|
+
const opts = thisCommand.optsWithGlobals();
|
|
26
|
+
if (opts.verbose) {
|
|
27
|
+
logger.setVerbose(true);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
24
30
|
|
|
25
31
|
program
|
|
26
32
|
.command('test')
|
|
@@ -33,9 +39,31 @@ program
|
|
|
33
39
|
'[pattern]',
|
|
34
40
|
'glob pattern to match test files (uses config.include if not specified)'
|
|
35
41
|
)
|
|
36
|
-
.
|
|
42
|
+
.option(
|
|
43
|
+
'-t, --testNamePattern <pattern>',
|
|
44
|
+
'Run only tests with names matching regex pattern'
|
|
45
|
+
)
|
|
46
|
+
.option(
|
|
47
|
+
'--testPathPattern <pattern>',
|
|
48
|
+
'Run only test files with paths matching regex pattern'
|
|
49
|
+
)
|
|
50
|
+
.option(
|
|
51
|
+
'--testPathIgnorePatterns <patterns...>',
|
|
52
|
+
'Ignore test files matching these patterns'
|
|
53
|
+
)
|
|
54
|
+
.option(
|
|
55
|
+
'--testMatch <patterns...>',
|
|
56
|
+
'Override config.include with these glob patterns'
|
|
57
|
+
)
|
|
58
|
+
.action(async (runner, pattern, options) => {
|
|
37
59
|
try {
|
|
38
|
-
|
|
60
|
+
// Convert CLI pattern argument to testMatch option
|
|
61
|
+
const mergedOptions = {
|
|
62
|
+
...options,
|
|
63
|
+
testMatch: pattern ? [pattern] : options.testMatch,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
await testCommand(runner, mergedOptions);
|
|
39
67
|
} catch (error) {
|
|
40
68
|
handleError(error);
|
|
41
69
|
process.exit(1);
|
|
@@ -69,7 +69,7 @@ const androidPlatformAdapter: PlatformAdapter = {
|
|
|
69
69
|
|
|
70
70
|
return {
|
|
71
71
|
restart: async () => {
|
|
72
|
-
await runApp(
|
|
72
|
+
await runApp(deviceId, runner.bundleId, runner.activityName);
|
|
73
73
|
},
|
|
74
74
|
dispose: async () => {
|
|
75
75
|
await killApp(deviceId, runner.bundleId);
|
|
@@ -2,11 +2,13 @@ import { PlatformAdapter } from './platform-adapter.js';
|
|
|
2
2
|
import androidPlatformAdapter from './android/index.js';
|
|
3
3
|
import iosPlatformAdapter from './ios/index.js';
|
|
4
4
|
import webPlatformAdapter from './web/index.js';
|
|
5
|
+
import vegaPlatformAdapter from './vega/index.js';
|
|
5
6
|
|
|
6
7
|
const platformAdapters = {
|
|
7
8
|
android: androidPlatformAdapter,
|
|
8
9
|
ios: iosPlatformAdapter,
|
|
9
10
|
web: webPlatformAdapter,
|
|
11
|
+
vega: vegaPlatformAdapter,
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
export const getPlatformAdapter = async (
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { spawn } from '@react-native-harness/tools';
|
|
3
|
+
|
|
4
|
+
export type VegaBuildTarget = 'sim_tv_x86_64' | 'sim_tv_aarch64';
|
|
5
|
+
export type VegaBuildType = 'Debug' | 'Release';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build Vega app and produce .vpkg file
|
|
9
|
+
*/
|
|
10
|
+
export const buildVegaApp = async (
|
|
11
|
+
buildType: VegaBuildType = 'Release',
|
|
12
|
+
target?: VegaBuildTarget
|
|
13
|
+
): Promise<void> => {
|
|
14
|
+
const args = ['run', 'build:app'];
|
|
15
|
+
|
|
16
|
+
if (buildType) {
|
|
17
|
+
args.push('-b', buildType);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (target) {
|
|
21
|
+
args.push('-t', target);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await spawn('npm', args);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Clean build artifacts
|
|
29
|
+
*/
|
|
30
|
+
export const cleanBuild = async (): Promise<void> => {
|
|
31
|
+
await spawn('kepler', ['clean']);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the expected .vpkg file path based on build configuration
|
|
36
|
+
*/
|
|
37
|
+
export const getVpkgPath = (
|
|
38
|
+
appName: string,
|
|
39
|
+
buildType: VegaBuildType = 'Release',
|
|
40
|
+
target: VegaBuildTarget = 'sim_tv_x86_64'
|
|
41
|
+
): string => {
|
|
42
|
+
const buildTypeStr = buildType.toLowerCase();
|
|
43
|
+
const vpkgFileName = `${appName}_${target}.vpkg`;
|
|
44
|
+
|
|
45
|
+
return path.join(
|
|
46
|
+
process.cwd(),
|
|
47
|
+
'build',
|
|
48
|
+
`${target}-${buildTypeStr}`,
|
|
49
|
+
vpkgFileName
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Launch an already installed app on specified Vega virtual device
|
|
55
|
+
*/
|
|
56
|
+
export const runApp = async (
|
|
57
|
+
deviceId: string,
|
|
58
|
+
bundleId: string
|
|
59
|
+
): Promise<void> => {
|
|
60
|
+
await spawn('kepler', [
|
|
61
|
+
'device',
|
|
62
|
+
'launch-app',
|
|
63
|
+
'--device',
|
|
64
|
+
deviceId,
|
|
65
|
+
'--appName',
|
|
66
|
+
bundleId,
|
|
67
|
+
]);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Kill/terminate app on specified Vega virtual device
|
|
72
|
+
*/
|
|
73
|
+
export const killApp = async (
|
|
74
|
+
deviceId: string,
|
|
75
|
+
bundleId: string
|
|
76
|
+
): Promise<void> => {
|
|
77
|
+
await spawn('kepler', [
|
|
78
|
+
'device',
|
|
79
|
+
'terminate-app',
|
|
80
|
+
'--device',
|
|
81
|
+
deviceId,
|
|
82
|
+
'--appName',
|
|
83
|
+
bundleId,
|
|
84
|
+
]);
|
|
85
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { spawn } from '@react-native-harness/tools';
|
|
2
|
+
|
|
3
|
+
export type VegaVirtualDeviceStatus = 'running' | 'stopped';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List all available Vega virtual devices
|
|
7
|
+
* Returns array of device identifiers that can be used with kepler commands
|
|
8
|
+
*/
|
|
9
|
+
export const listVegaDevices = async (): Promise<string[]> => {
|
|
10
|
+
try {
|
|
11
|
+
const { stdout } = await spawn('kepler', ['device', 'list']);
|
|
12
|
+
const lines = stdout.trim().split('\n');
|
|
13
|
+
const devices: string[] = [];
|
|
14
|
+
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
if (line.trim()) {
|
|
17
|
+
// Parse device line format: "VirtualDevice : tv - x86_64 - OS - hostname"
|
|
18
|
+
// or potentially "VegaTV_1 : tv - x86_64 - OS - hostname" for named instances
|
|
19
|
+
const deviceId = line.split(' : ')[0].trim();
|
|
20
|
+
if (
|
|
21
|
+
deviceId &&
|
|
22
|
+
(deviceId === 'VirtualDevice' || deviceId.startsWith('Vega'))
|
|
23
|
+
) {
|
|
24
|
+
devices.push(deviceId);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return devices;
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if a specific Vega virtual device is connected/available
|
|
37
|
+
*/
|
|
38
|
+
export const isVegaDeviceConnected = async (
|
|
39
|
+
deviceId: string
|
|
40
|
+
): Promise<boolean> => {
|
|
41
|
+
try {
|
|
42
|
+
const { stdout } = await spawn('kepler', [
|
|
43
|
+
'device',
|
|
44
|
+
'is-connected',
|
|
45
|
+
'--device',
|
|
46
|
+
deviceId,
|
|
47
|
+
]);
|
|
48
|
+
return stdout.includes('is connected');
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if an app is installed on the specified Vega virtual device
|
|
56
|
+
*/
|
|
57
|
+
export const isAppInstalled = async (
|
|
58
|
+
deviceId: string,
|
|
59
|
+
bundleId: string
|
|
60
|
+
): Promise<boolean> => {
|
|
61
|
+
try {
|
|
62
|
+
await spawn('kepler', [
|
|
63
|
+
'device',
|
|
64
|
+
'is-app-installed',
|
|
65
|
+
'--device',
|
|
66
|
+
deviceId,
|
|
67
|
+
'--appName',
|
|
68
|
+
bundleId,
|
|
69
|
+
]);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if an app is currently running on the specified Vega virtual device
|
|
78
|
+
*/
|
|
79
|
+
export const isAppRunning = async (
|
|
80
|
+
deviceId: string,
|
|
81
|
+
bundleId: string
|
|
82
|
+
): Promise<boolean> => {
|
|
83
|
+
try {
|
|
84
|
+
await spawn('kepler', [
|
|
85
|
+
'device',
|
|
86
|
+
'is-app-running',
|
|
87
|
+
'--device',
|
|
88
|
+
deviceId,
|
|
89
|
+
'--appName',
|
|
90
|
+
bundleId,
|
|
91
|
+
]);
|
|
92
|
+
return true;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Install app on specified Vega virtual device using .vpkg file
|
|
100
|
+
*/
|
|
101
|
+
export const installApp = async (
|
|
102
|
+
deviceId: string,
|
|
103
|
+
vpkgPath: string
|
|
104
|
+
): Promise<void> => {
|
|
105
|
+
await spawn('kepler', [
|
|
106
|
+
'device',
|
|
107
|
+
'install-app',
|
|
108
|
+
'-p',
|
|
109
|
+
vpkgPath,
|
|
110
|
+
'--device',
|
|
111
|
+
deviceId,
|
|
112
|
+
]);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Terminate app on specified Vega virtual device
|
|
117
|
+
*/
|
|
118
|
+
export const terminateApp = async (
|
|
119
|
+
deviceId: string,
|
|
120
|
+
bundleId: string
|
|
121
|
+
): Promise<void> => {
|
|
122
|
+
await spawn('kepler', [
|
|
123
|
+
'device',
|
|
124
|
+
'terminate-app',
|
|
125
|
+
'--device',
|
|
126
|
+
deviceId,
|
|
127
|
+
'--appName',
|
|
128
|
+
bundleId,
|
|
129
|
+
]);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Uninstall app from specified Vega virtual device
|
|
134
|
+
*/
|
|
135
|
+
export const uninstallApp = async (
|
|
136
|
+
deviceId: string,
|
|
137
|
+
bundleId: string
|
|
138
|
+
): Promise<void> => {
|
|
139
|
+
await spawn('kepler', [
|
|
140
|
+
'device',
|
|
141
|
+
'uninstall-app',
|
|
142
|
+
'--device',
|
|
143
|
+
deviceId,
|
|
144
|
+
'--appName',
|
|
145
|
+
bundleId,
|
|
146
|
+
]);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Start port forwarding for debugging on specified Vega virtual device
|
|
151
|
+
*/
|
|
152
|
+
export const startPortForwarding = async (
|
|
153
|
+
deviceId: string,
|
|
154
|
+
port: number,
|
|
155
|
+
forward: boolean = true
|
|
156
|
+
): Promise<void> => {
|
|
157
|
+
await spawn('kepler', [
|
|
158
|
+
'device',
|
|
159
|
+
'start-port-forwarding',
|
|
160
|
+
'--device',
|
|
161
|
+
deviceId,
|
|
162
|
+
'--port',
|
|
163
|
+
port.toString(),
|
|
164
|
+
'--forward',
|
|
165
|
+
forward.toString(),
|
|
166
|
+
]);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Stop port forwarding on specified Vega virtual device
|
|
171
|
+
*/
|
|
172
|
+
export const stopPortForwarding = async (
|
|
173
|
+
deviceId: string,
|
|
174
|
+
port: number,
|
|
175
|
+
forward: boolean = true
|
|
176
|
+
): Promise<void> => {
|
|
177
|
+
await spawn('kepler', [
|
|
178
|
+
'device',
|
|
179
|
+
'stop-port-forwarding',
|
|
180
|
+
'--device',
|
|
181
|
+
deviceId,
|
|
182
|
+
'--port',
|
|
183
|
+
port.toString(),
|
|
184
|
+
'--forward',
|
|
185
|
+
forward.toString(),
|
|
186
|
+
]);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get status of a specific Vega virtual device
|
|
191
|
+
* Note: Vega CLI might manage virtual devices globally, so this checks if the device is available
|
|
192
|
+
*/
|
|
193
|
+
export const getVegaDeviceStatus = async (
|
|
194
|
+
deviceId: string
|
|
195
|
+
): Promise<VegaVirtualDeviceStatus> => {
|
|
196
|
+
try {
|
|
197
|
+
// First check if the device is connected/available
|
|
198
|
+
const isConnected = await isVegaDeviceConnected(deviceId);
|
|
199
|
+
if (isConnected) {
|
|
200
|
+
return 'running';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check general virtual device status
|
|
204
|
+
const { stdout } = await spawn('kepler', ['virtual-device', 'status']);
|
|
205
|
+
// Parse the status output to determine if VVD is running
|
|
206
|
+
return stdout.toLowerCase().includes('running') ||
|
|
207
|
+
stdout.toLowerCase().includes('ready')
|
|
208
|
+
? 'running'
|
|
209
|
+
: 'stopped';
|
|
210
|
+
} catch {
|
|
211
|
+
return 'stopped';
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Start Vega Virtual Device
|
|
217
|
+
* Note: Vega might manage virtual devices globally, this starts the virtual device system
|
|
218
|
+
*/
|
|
219
|
+
export const startVirtualDevice = async (): Promise<void> => {
|
|
220
|
+
await spawn('kepler', ['virtual-device', 'start']);
|
|
221
|
+
|
|
222
|
+
// Poll for VVD status until it's running
|
|
223
|
+
let attempts = 0;
|
|
224
|
+
const maxAttempts = 30; // 30 seconds timeout
|
|
225
|
+
|
|
226
|
+
while (attempts < maxAttempts) {
|
|
227
|
+
const { stdout } = await spawn('kepler', ['virtual-device', 'status']);
|
|
228
|
+
if (
|
|
229
|
+
stdout.toLowerCase().includes('running') ||
|
|
230
|
+
stdout.toLowerCase().includes('ready')
|
|
231
|
+
) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
236
|
+
attempts++;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
throw new Error('Vega Virtual Device failed to start within timeout');
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Stop Vega Virtual Device
|
|
244
|
+
*/
|
|
245
|
+
export const stopVirtualDevice = async (): Promise<void> => {
|
|
246
|
+
await spawn('kepler', ['virtual-device', 'stop']);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Combined install and run command for specified Vega virtual device (Vega-specific convenience method)
|
|
251
|
+
*/
|
|
252
|
+
export const runKepler = async (
|
|
253
|
+
deviceId: string,
|
|
254
|
+
vpkgPath: string,
|
|
255
|
+
bundleId: string
|
|
256
|
+
): Promise<void> => {
|
|
257
|
+
await spawn('kepler', ['run-kepler', vpkgPath, bundleId, '-d', deviceId]);
|
|
258
|
+
};
|