@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.
Files changed (47) hide show
  1. package/README.md +23 -4
  2. package/dist/bundlers/metro.d.ts.map +1 -1
  3. package/dist/bundlers/metro.js +8 -4
  4. package/dist/commands/test.d.ts +2 -1
  5. package/dist/commands/test.d.ts.map +1 -1
  6. package/dist/commands/test.js +19 -17
  7. package/dist/discovery/index.d.ts +3 -0
  8. package/dist/discovery/index.d.ts.map +1 -0
  9. package/dist/discovery/index.js +1 -0
  10. package/dist/discovery/testDiscovery.d.ts +11 -0
  11. package/dist/discovery/testDiscovery.d.ts.map +1 -0
  12. package/dist/discovery/testDiscovery.js +29 -0
  13. package/dist/errors/errorHandler.d.ts.map +1 -1
  14. package/dist/errors/errorHandler.js +12 -2
  15. package/dist/errors/errors.d.ts +2 -2
  16. package/dist/errors/errors.d.ts.map +1 -1
  17. package/dist/errors/errors.js +6 -1
  18. package/dist/index.js +20 -4
  19. package/dist/platforms/android/index.js +1 -1
  20. package/dist/platforms/platform-registry.d.ts.map +1 -1
  21. package/dist/platforms/platform-registry.js +2 -0
  22. package/dist/platforms/vega/build.d.ts +23 -0
  23. package/dist/platforms/vega/build.d.ts.map +1 -0
  24. package/dist/platforms/vega/build.js +55 -0
  25. package/dist/platforms/vega/device.d.ts +57 -0
  26. package/dist/platforms/vega/device.d.ts.map +1 -0
  27. package/dist/platforms/vega/device.js +206 -0
  28. package/dist/platforms/vega/index.d.ts +4 -0
  29. package/dist/platforms/vega/index.d.ts.map +1 -0
  30. package/dist/platforms/vega/index.js +75 -0
  31. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  32. package/dist/utils/status-formatter.d.ts +27 -0
  33. package/dist/utils/status-formatter.d.ts.map +1 -0
  34. package/dist/utils/status-formatter.js +54 -0
  35. package/package.json +4 -4
  36. package/src/bundlers/metro.ts +8 -3
  37. package/src/commands/test.ts +33 -23
  38. package/src/discovery/index.ts +2 -0
  39. package/src/discovery/testDiscovery.ts +50 -0
  40. package/src/errors/errorHandler.ts +16 -4
  41. package/src/errors/errors.ts +9 -4
  42. package/src/index.ts +33 -5
  43. package/src/platforms/android/index.ts +1 -1
  44. package/src/platforms/platform-registry.ts +2 -0
  45. package/src/platforms/vega/build.ts +85 -0
  46. package/src/platforms/vega/device.ts +258 -0
  47. package/src/platforms/vega/index.ts +107 -0
@@ -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 { Glob } from 'glob';
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
- pattern?: string
88
+ options: TestFilterOptions = {}
91
89
  ): Promise<void> => {
92
90
  const discoverSpinner = spinner();
93
91
  discoverSpinner.start('Discovering tests');
94
92
 
95
- const globPattern = pattern || context.config.include;
96
- const glob = new Glob(globPattern, {
97
- cwd: context.projectRoot,
98
- });
99
- context.testFiles = await glob.walk();
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 (context: TestRunContext): Promise<void> => {
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 = progress({ style: 'block' });
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 = progress({ style: 'block' });
117
- runSpinner.message(`Restarting environment for next test file`);
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
- const result = await client.runTests(testFile);
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.ws.close();
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
- pattern?: string
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, pattern);
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,2 @@
1
+ export { discoverTestFiles } from './testDiscovery.js';
2
+ export type { TestFilterOptions } from './testDiscovery.js';
@@ -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) {
@@ -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
- .action(async (runner, pattern) => {
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
- await testCommand(runner, pattern);
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(runner.deviceId, runner.bundleId, runner.activityName);
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
+ };