@myerscarpenter/quest-dev 1.0.8 → 1.2.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.
Files changed (45) hide show
  1. package/build/commands/battery.d.ts +9 -0
  2. package/build/commands/battery.d.ts.map +1 -0
  3. package/build/commands/battery.js +31 -0
  4. package/build/commands/battery.js.map +1 -0
  5. package/build/commands/logcat.d.ts +24 -0
  6. package/build/commands/logcat.d.ts.map +1 -0
  7. package/build/commands/logcat.js +261 -0
  8. package/build/commands/logcat.js.map +1 -0
  9. package/build/commands/open.d.ts +1 -1
  10. package/build/commands/open.d.ts.map +1 -1
  11. package/build/commands/open.js +14 -14
  12. package/build/commands/open.js.map +1 -1
  13. package/build/commands/screenshot.d.ts +1 -1
  14. package/build/commands/screenshot.d.ts.map +1 -1
  15. package/build/commands/screenshot.js +55 -9
  16. package/build/commands/screenshot.js.map +1 -1
  17. package/build/commands/stay-awake.d.ts +14 -0
  18. package/build/commands/stay-awake.d.ts.map +1 -0
  19. package/build/commands/stay-awake.js +234 -0
  20. package/build/commands/stay-awake.js.map +1 -0
  21. package/build/index.js +84 -5
  22. package/build/index.js.map +1 -1
  23. package/build/utils/adb.d.ts +12 -7
  24. package/build/utils/adb.d.ts.map +1 -1
  25. package/build/utils/adb.js +138 -31
  26. package/build/utils/adb.js.map +1 -1
  27. package/build/utils/filename.d.ts +9 -0
  28. package/build/utils/filename.d.ts.map +1 -0
  29. package/build/utils/filename.js +17 -0
  30. package/build/utils/filename.js.map +1 -0
  31. package/build/utils/filename.test.d.ts +5 -0
  32. package/build/utils/filename.test.d.ts.map +1 -0
  33. package/build/utils/filename.test.js +40 -0
  34. package/build/utils/filename.test.js.map +1 -0
  35. package/package.json +2 -1
  36. package/src/commands/battery.ts +34 -0
  37. package/src/commands/logcat.ts +288 -0
  38. package/src/commands/open.ts +18 -14
  39. package/src/commands/screenshot.ts +61 -9
  40. package/src/commands/stay-awake.ts +254 -0
  41. package/src/index.ts +119 -9
  42. package/src/utils/adb.ts +148 -31
  43. package/src/utils/filename.test.ts +55 -0
  44. package/src/utils/filename.ts +18 -0
  45. package/tests/adb.test.ts +2 -2
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Quest stay-awake command
3
+ * Keeps Quest screen awake by setting screen timeout to 24 hours
4
+ * Restores original timeout on exit (Ctrl-C)
5
+ */
6
+
7
+ import { checkADBPath } from '../utils/adb.js';
8
+ import { execCommand } from '../utils/exec.js';
9
+ import { execSync, spawn, ChildProcess } from 'child_process';
10
+ import * as os from 'os';
11
+ import * as fs from 'fs';
12
+
13
+ /**
14
+ * Get current screen timeout setting
15
+ */
16
+ async function getScreenTimeout(): Promise<number> {
17
+ const output = await execCommand('adb', ['shell', 'settings', 'get', 'system', 'screen_off_timeout']);
18
+ return parseInt(output.trim(), 10);
19
+ }
20
+
21
+ /**
22
+ * Set screen timeout (in milliseconds)
23
+ */
24
+ async function setScreenTimeout(timeout: number): Promise<void> {
25
+ await execCommand('adb', ['shell', 'settings', 'put', 'system', 'screen_off_timeout', timeout.toString()]);
26
+ }
27
+
28
+ /**
29
+ * Disable Quest proximity sensor (keeps screen on even when not worn)
30
+ */
31
+ async function disableProximitySensor(): Promise<void> {
32
+ await execCommand('adb', ['shell', 'am', 'broadcast', '-a', 'com.oculus.vrpowermanager.prox_close']);
33
+ }
34
+
35
+ /**
36
+ * Enable Quest proximity sensor (re-enable normal behavior)
37
+ * Note: automation_disable actually RE-ENABLES normal proximity sensor automation
38
+ */
39
+ async function enableProximitySensor(): Promise<void> {
40
+ await execCommand('adb', ['shell', 'am', 'broadcast', '-a', 'com.oculus.vrpowermanager.automation_disable']);
41
+ }
42
+
43
+ /**
44
+ * Wake the Quest screen
45
+ */
46
+ async function wakeScreen(): Promise<void> {
47
+ await execCommand('adb', ['shell', 'input', 'keyevent', 'KEYCODE_WAKEUP']);
48
+ }
49
+
50
+ /**
51
+ * Child watchdog process - polls for parent death and cleans up
52
+ */
53
+ export async function stayAwakeWatchdog(parentPid: number, originalTimeout: number): Promise<void> {
54
+ const pollInterval = 5000; // Check every 5 seconds
55
+
56
+ const checkParent = setInterval(() => {
57
+ try {
58
+ // Check if parent process still exists
59
+ process.kill(parentPid, 0);
60
+ // Parent still alive, continue polling
61
+ } catch {
62
+ // Parent is dead - perform cleanup
63
+ console.log('Parent process died, restoring Quest settings...');
64
+ clearInterval(checkParent);
65
+
66
+ // Restore settings synchronously
67
+ try {
68
+ execSync(`adb shell settings put system screen_off_timeout ${originalTimeout}`, { stdio: 'ignore' });
69
+ execSync(`adb shell am broadcast -a com.oculus.vrpowermanager.automation_disable`, { stdio: 'ignore' });
70
+
71
+ // Cleanup PID file
72
+ const pidFile = `${os.homedir()}/.quest-dev-stay-awake.pid`;
73
+ try {
74
+ fs.unlinkSync(pidFile);
75
+ } catch {}
76
+
77
+ console.log(`Screen timeout restored to ${originalTimeout}ms (${Math.round(originalTimeout / 1000)}s)`);
78
+ console.log('Proximity sensor re-enabled');
79
+ } catch (err) {
80
+ console.error('Failed to restore settings:', (err as Error).message);
81
+ }
82
+
83
+ process.exit(0);
84
+ }
85
+ }, pollInterval);
86
+ }
87
+
88
+ /**
89
+ * Main stay-awake command handler
90
+ */
91
+ export async function stayAwakeCommand(idleTimeout: number = 300000): Promise<void> {
92
+ // Check prerequisites
93
+ checkADBPath();
94
+
95
+ // Check devices without verbose output
96
+ try {
97
+ const output = await execCommand('adb', ['devices']);
98
+ const lines = output.trim().split('\n').slice(1);
99
+ const devices = lines.filter(line => line.trim() && !line.includes('List of devices'));
100
+
101
+ if (devices.length === 0) {
102
+ console.error('Error: No ADB devices connected');
103
+ process.exit(1);
104
+ }
105
+ } catch (error) {
106
+ console.error('Error: Failed to list ADB devices');
107
+ process.exit(1);
108
+ }
109
+
110
+ // PID file management
111
+ const pidFilePath = `${os.homedir()}/.quest-dev-stay-awake.pid`;
112
+
113
+ // Check for existing process
114
+ if (fs.existsSync(pidFilePath)) {
115
+ const existingPid = parseInt(fs.readFileSync(pidFilePath, 'utf-8'));
116
+ try {
117
+ process.kill(existingPid, 0); // Test if process exists
118
+ console.error(`Error: stay-awake is already running (PID: ${existingPid})`);
119
+ process.exit(1);
120
+ } catch {
121
+ // Process dead, cleanup stale PID file
122
+ fs.unlinkSync(pidFilePath);
123
+ }
124
+ }
125
+
126
+ // Get original timeout
127
+ let originalTimeout: number;
128
+
129
+ try {
130
+ originalTimeout = await getScreenTimeout();
131
+ console.log(`Original screen timeout: ${originalTimeout}ms (${Math.round(originalTimeout / 1000)}s)`);
132
+ } catch (error) {
133
+ console.error('Failed to get current screen timeout');
134
+ process.exit(1);
135
+ }
136
+
137
+ // Write PID file
138
+ try {
139
+ fs.writeFileSync(pidFilePath, process.pid.toString());
140
+ } catch (error) {
141
+ console.warn('Failed to write PID file, hook will not work');
142
+ }
143
+
144
+ // Spawn child watchdog process
145
+ let childProcess: ChildProcess | null = null;
146
+ try {
147
+ childProcess = spawn(process.execPath, [
148
+ process.argv[1], // quest-dev script path
149
+ 'stay-awake-watchdog',
150
+ '--parent-pid', process.pid.toString(),
151
+ '--original-timeout', originalTimeout.toString()
152
+ ], {
153
+ detached: true,
154
+ stdio: 'ignore'
155
+ });
156
+
157
+ childProcess.unref(); // Allow parent to exit without waiting for child
158
+ } catch (error) {
159
+ console.warn('Failed to spawn watchdog child process');
160
+ }
161
+
162
+ // Wake screen and disable proximity sensor
163
+ try {
164
+ await wakeScreen();
165
+ console.log('Quest screen woken up');
166
+
167
+ await disableProximitySensor();
168
+ console.log('Proximity sensor disabled');
169
+ } catch (error) {
170
+ console.error('Failed to wake screen or disable proximity sensor:', (error as Error).message);
171
+ }
172
+
173
+ // Set timeout to 24 hours (86400000ms)
174
+ const longTimeout = 86400000;
175
+ try {
176
+ await setScreenTimeout(longTimeout);
177
+ console.log(`Screen timeout set to 24 hours`);
178
+ console.log(`Quest will stay awake (idle timeout: ${Math.round(idleTimeout / 1000)}s). Press Ctrl-C to restore original settings.`);
179
+ } catch (error) {
180
+ console.error('Failed to set screen timeout');
181
+ process.exit(1);
182
+ }
183
+
184
+ // Idle timer mechanism
185
+ let idleTimerHandle: NodeJS.Timeout | null = null;
186
+ let cleanupInProgress = false;
187
+
188
+ const resetIdleTimer = () => {
189
+ if (idleTimerHandle) clearTimeout(idleTimerHandle);
190
+ idleTimerHandle = setTimeout(() => {
191
+ console.log('\nIdle timeout reached, exiting...');
192
+ cleanup();
193
+ }, idleTimeout);
194
+ };
195
+
196
+ // Set up cleanup on exit (must be synchronous for signal handlers)
197
+ const cleanup = () => {
198
+ if (cleanupInProgress) return; // Guard against double-cleanup
199
+ cleanupInProgress = true;
200
+
201
+ // Clear idle timer
202
+ if (idleTimerHandle) clearTimeout(idleTimerHandle);
203
+
204
+ // Kill child watchdog
205
+ if (childProcess) {
206
+ try {
207
+ childProcess.kill();
208
+ } catch {}
209
+ }
210
+
211
+ console.log('\nRestoring original settings...');
212
+ try {
213
+ // Remove PID file
214
+ try {
215
+ fs.unlinkSync(pidFilePath);
216
+ } catch {}
217
+
218
+ // Restore Quest settings
219
+ execSync(`adb shell settings put system screen_off_timeout ${originalTimeout}`, { stdio: 'ignore' });
220
+ execSync(`adb shell am broadcast -a com.oculus.vrpowermanager.automation_disable`, { stdio: 'ignore' });
221
+ console.log(`Screen timeout restored to ${originalTimeout}ms (${Math.round(originalTimeout / 1000)}s)`);
222
+ console.log(`Proximity sensor re-enabled`);
223
+ } catch (error) {
224
+ console.error('Failed to restore settings:', (error as Error).message);
225
+ }
226
+ process.exit(0);
227
+ };
228
+
229
+ // Handle Ctrl-C and termination
230
+ process.on('SIGINT', cleanup);
231
+ process.on('SIGTERM', cleanup);
232
+ process.on('SIGHUP', cleanup);
233
+
234
+ // Handle SIGUSR1 for activity reset
235
+ process.on('SIGUSR1', () => {
236
+ console.log('Activity detected, resetting idle timer');
237
+ resetIdleTimer();
238
+ });
239
+
240
+ // Start idle timer
241
+ resetIdleTimer();
242
+
243
+ // Keep process running with an interval that does nothing
244
+ console.log('Keeping Quest awake...');
245
+ setInterval(() => {
246
+ // Do nothing, just keep process alive
247
+ }, 60000); // Check every minute
248
+
249
+ // Prevent process from exiting
250
+ await new Promise<void>((resolve) => {
251
+ // This will only resolve when cleanup is called
252
+ process.on('exit', () => resolve());
253
+ });
254
+ }
package/src/index.ts CHANGED
@@ -12,6 +12,9 @@ import { fileURLToPath } from 'url';
12
12
  import { dirname, join } from 'path';
13
13
  import { screenshotCommand } from './commands/screenshot.js';
14
14
  import { openCommand } from './commands/open.js';
15
+ import { startCommand, stopCommand, statusCommand, tailCommand } from './commands/logcat.js';
16
+ import { batteryCommand } from './commands/battery.js';
17
+ import { stayAwakeCommand, stayAwakeWatchdog } from './commands/stay-awake.js';
15
18
 
16
19
  // Read version from package.json
17
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -43,17 +46,26 @@ const cli = yargs(hideBin(process.argv))
43
46
 
44
47
  // Screenshot command
45
48
  cli.command(
46
- 'screenshot <output>',
47
- 'Take a screenshot from Quest and save to local file',
49
+ 'screenshot <directory>',
50
+ 'Take a screenshot from Quest and save to directory with auto-generated filename',
48
51
  (yargs) => {
49
- return yargs.positional('output', {
50
- describe: 'Output file path (e.g., ~/screenshots/test.jpg)',
51
- type: 'string',
52
- demandOption: true
53
- });
52
+ return yargs
53
+ .positional('directory', {
54
+ describe: 'Output directory path (e.g., ~/screenshots)',
55
+ type: 'string',
56
+ demandOption: true
57
+ })
58
+ .option('caption', {
59
+ describe: 'Caption to embed in JPEG COM metadata',
60
+ type: 'string',
61
+ alias: 'c'
62
+ });
54
63
  },
55
64
  async (argv) => {
56
- await screenshotCommand(argv.output as string);
65
+ await screenshotCommand(
66
+ argv.directory as string,
67
+ argv.caption as string | undefined
68
+ );
57
69
  }
58
70
  );
59
71
 
@@ -72,10 +84,108 @@ cli.command(
72
84
  describe: 'Close all other tabs before opening',
73
85
  type: 'boolean',
74
86
  default: false
87
+ })
88
+ .option('browser', {
89
+ describe: 'Browser package name (e.g., com.oculus.browser, org.chromium.chrome)',
90
+ type: 'string',
91
+ default: 'com.oculus.browser',
92
+ alias: 'b'
93
+ });
94
+ },
95
+ async (argv) => {
96
+ await openCommand(
97
+ argv.url as string,
98
+ argv.closeOthers as boolean,
99
+ argv.browser as string
100
+ );
101
+ }
102
+ );
103
+
104
+ // Logcat command
105
+ cli.command(
106
+ 'logcat <action>',
107
+ 'Capture Android logcat to files (CRITICAL: always start before testing to avoid losing crash logs)',
108
+ (yargs) => {
109
+ return yargs
110
+ .positional('action', {
111
+ describe: 'Action to perform',
112
+ type: 'string',
113
+ choices: ['start', 'stop', 'status', 'tail'],
114
+ demandOption: true
115
+ })
116
+ .option('filter', {
117
+ describe: 'Logcat filter expression (e.g., "*:W" for warnings+, "chromium:V *:S" for chromium only)',
118
+ type: 'string'
119
+ });
120
+ },
121
+ async (argv) => {
122
+ const action = argv.action as string;
123
+ const filter = argv.filter as string | undefined;
124
+
125
+ switch (action) {
126
+ case 'start':
127
+ await startCommand(filter);
128
+ break;
129
+ case 'stop':
130
+ await stopCommand();
131
+ break;
132
+ case 'status':
133
+ await statusCommand();
134
+ break;
135
+ case 'tail':
136
+ await tailCommand();
137
+ break;
138
+ default:
139
+ console.error(`Unknown action: ${action}`);
140
+ process.exit(1);
141
+ }
142
+ }
143
+ );
144
+
145
+ // Battery command
146
+ cli.command(
147
+ 'battery',
148
+ 'Show Quest battery percentage and charging status',
149
+ () => {},
150
+ async () => {
151
+ await batteryCommand();
152
+ }
153
+ );
154
+
155
+ // Stay-awake command
156
+ cli.command(
157
+ 'stay-awake',
158
+ 'Keep Quest screen awake (sets 24hr timeout, restores on Ctrl-C)',
159
+ (yargs) => {
160
+ return yargs.option('idle-timeout', {
161
+ describe: 'Idle timeout in milliseconds (default: 300000 = 5 minutes)',
162
+ type: 'number',
163
+ default: 300000,
164
+ alias: 'i'
165
+ });
166
+ },
167
+ async (argv) => {
168
+ await stayAwakeCommand(argv.idleTimeout as number);
169
+ }
170
+ );
171
+
172
+ // Stay-awake watchdog (internal subcommand, spawned by stay-awake parent)
173
+ cli.command(
174
+ 'stay-awake-watchdog',
175
+ false as any, // Hide from help
176
+ (yargs) => {
177
+ return yargs
178
+ .option('parent-pid', {
179
+ type: 'number',
180
+ demandOption: true
181
+ })
182
+ .option('original-timeout', {
183
+ type: 'number',
184
+ demandOption: true
75
185
  });
76
186
  },
77
187
  async (argv) => {
78
- await openCommand(argv.url as string, argv.closeOthers as boolean);
188
+ await stayAwakeWatchdog(argv.parentPid as number, argv.originalTimeout as number);
79
189
  }
80
190
  );
81
191
 
package/src/utils/adb.ts CHANGED
@@ -8,13 +8,69 @@ import { execCommand, execCommandFull } from './exec.js';
8
8
 
9
9
  const CDP_PORT = 9223; // Chrome DevTools Protocol port (Quest browser default)
10
10
 
11
+ /**
12
+ * Get browser process PID
13
+ */
14
+ async function getBrowserPID(packageName: string): Promise<number | null> {
15
+ try {
16
+ const result = await execCommandFull('adb', ['shell', `ps | grep ${packageName}`]);
17
+ if (!result.stdout) return null;
18
+
19
+ // Parse ps output: USER PID PPID ... NAME
20
+ const lines = result.stdout.trim().split('\n');
21
+ for (const line of lines) {
22
+ if (line.includes('grep')) continue; // Skip grep itself
23
+ const parts = line.trim().split(/\s+/);
24
+ if (parts.length >= 2) {
25
+ return parseInt(parts[1], 10); // PID is second column
26
+ }
27
+ }
28
+ } catch {
29
+ return null;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * Detect CDP socket for a browser
36
+ * Returns socket name (e.g., "chrome_devtools_remote_12345")
37
+ */
38
+ async function detectCDPSocket(packageName: string): Promise<string> {
39
+ const pid = await getBrowserPID(packageName);
40
+
41
+ if (pid) {
42
+ // Try PID-based socket first
43
+ try {
44
+ const result = await execCommandFull('adb', [
45
+ 'shell',
46
+ `cat /proc/net/unix | grep chrome_devtools_remote_${pid}`
47
+ ]);
48
+ if (result.stdout.includes(`chrome_devtools_remote_${pid}`)) {
49
+ return `chrome_devtools_remote_${pid}`;
50
+ }
51
+ } catch {
52
+ // Fall through to default
53
+ }
54
+ }
55
+
56
+ // Default: generic socket (Quest Browser)
57
+ return 'chrome_devtools_remote';
58
+ }
59
+
60
+ /**
61
+ * Get CDP port for a socket
62
+ * Generic socket uses 9223, PID-based uses 9222
63
+ */
64
+ function getCDPPortForSocket(socket: string): number {
65
+ return socket === 'chrome_devtools_remote' ? 9223 : 9222;
66
+ }
67
+
11
68
  /**
12
69
  * Check if ADB is available on PATH
13
70
  */
14
71
  export function checkADBPath(): string {
15
72
  try {
16
73
  const adbPath = which.sync('adb');
17
- console.log(`Found ADB at: ${adbPath}`);
18
74
  return adbPath;
19
75
  } catch (error) {
20
76
  console.error('Error: ADB not found in PATH');
@@ -116,8 +172,15 @@ export function isPortListening(port: number): Promise<boolean> {
116
172
  /**
117
173
  * Idempotently set up ADB port forwarding for a given port
118
174
  */
119
- export async function ensurePortForwarding(port: number): Promise<void> {
175
+ export async function ensurePortForwarding(
176
+ port: number,
177
+ browser: string = 'com.oculus.browser'
178
+ ): Promise<void> {
120
179
  try {
180
+ // Detect CDP socket and port for this browser
181
+ const cdpSocket = await detectCDPSocket(browser);
182
+ const cdpPort = getCDPPortForSocket(cdpSocket);
183
+
121
184
  // Check reverse forwarding (Quest -> Host for dev server)
122
185
  const reverseList = await execCommand('adb', ['reverse', '--list']);
123
186
  const reverseExists = reverseList.includes(`tcp:${port}`);
@@ -132,27 +195,27 @@ export async function ensurePortForwarding(port: number): Promise<void> {
132
195
  // Check forward forwarding (Host -> Quest for CDP)
133
196
  // First check if ADB already has this forwarding set up
134
197
  const forwardList = await execCommand('adb', ['forward', '--list']);
135
- const forwardExists = forwardList.includes(`tcp:${CDP_PORT}`) && forwardList.includes('chrome_devtools_remote');
198
+ const forwardExists = forwardList.includes(`tcp:${cdpPort}`) && forwardList.includes(cdpSocket);
136
199
 
137
200
  if (forwardExists) {
138
- console.log(`CDP port ${CDP_PORT} forwarding already set up`);
201
+ console.log(`CDP port ${cdpPort} forwarding already set up`);
139
202
  } else {
140
203
  // Check if something else is using the port
141
- const cdpPortListening = await isPortListening(CDP_PORT);
204
+ const cdpPortListening = await isPortListening(cdpPort);
142
205
  if (cdpPortListening) {
143
- console.error(`Error: Port ${CDP_PORT} is already in use by another process`);
206
+ console.error(`Error: Port ${cdpPort} is already in use by another process`);
144
207
  console.error('');
145
- console.error('CDP port forwarding requires port 9223 to be free.');
208
+ console.error(`CDP port forwarding requires port ${cdpPort} to be free.`);
146
209
  console.error('Please stop the process using this port and try again.');
147
210
  console.error('');
148
211
  console.error('To find what is using the port:');
149
- console.error(` lsof -i :${CDP_PORT}`);
212
+ console.error(` lsof -i :${cdpPort}`);
150
213
  console.error('');
151
214
  process.exit(1);
152
215
  }
153
216
 
154
- await execCommand('adb', ['forward', `tcp:${CDP_PORT}`, 'localabstract:chrome_devtools_remote']);
155
- console.log(`ADB forward port forwarding set up: Host:${CDP_PORT} -> Quest:chrome_devtools_remote (CDP)`);
217
+ await execCommand('adb', ['forward', `tcp:${cdpPort}`, `localabstract:${cdpSocket}`]);
218
+ console.log(`ADB forward port forwarding set up: Host:${cdpPort} -> Quest:${cdpSocket} (CDP)`);
156
219
  }
157
220
  } catch (error) {
158
221
  console.error('Failed to set up port forwarding:', (error as Error).message);
@@ -161,22 +224,22 @@ export async function ensurePortForwarding(port: number): Promise<void> {
161
224
  }
162
225
 
163
226
  /**
164
- * Check if Quest browser is running
227
+ * Check if browser is running
165
228
  */
166
- export async function isBrowserRunning(): Promise<boolean> {
229
+ export async function isBrowserRunning(browser: string = 'com.oculus.browser'): Promise<boolean> {
167
230
  try {
168
- const result = await execCommandFull('adb', ['shell', 'ps | grep com.oculus.browser']);
169
- return result.stdout.includes('com.oculus.browser');
231
+ const result = await execCommandFull('adb', ['shell', `ps | grep ${browser}`]);
232
+ return result.stdout.includes(browser);
170
233
  } catch (error) {
171
234
  return false;
172
235
  }
173
236
  }
174
237
 
175
238
  /**
176
- * Launch Quest browser with a URL using am start
239
+ * Launch browser with a URL using am start
177
240
  */
178
- export async function launchBrowser(url: string): Promise<boolean> {
179
- console.log('Launching Quest browser...');
241
+ export async function launchBrowser(url: string, browser: string = 'com.oculus.browser'): Promise<boolean> {
242
+ console.log('Launching browser...');
180
243
  try {
181
244
  await execCommand('adb', [
182
245
  'shell',
@@ -186,12 +249,12 @@ export async function launchBrowser(url: string): Promise<boolean> {
186
249
  'android.intent.action.VIEW',
187
250
  '-d',
188
251
  url,
189
- 'com.oculus.browser'
252
+ browser
190
253
  ]);
191
- console.log(`Quest browser launched with URL: ${url}`);
254
+ console.log(`Browser launched with URL: ${url}`);
192
255
  return true;
193
256
  } catch (error) {
194
- console.error('Failed to launch Quest browser:', (error as Error).message);
257
+ console.error('Failed to launch browser:', (error as Error).message);
195
258
  return false;
196
259
  }
197
260
  }
@@ -199,38 +262,45 @@ export async function launchBrowser(url: string): Promise<boolean> {
199
262
  /**
200
263
  * Get CDP port
201
264
  */
202
- export function getCDPPort(): number {
203
- return CDP_PORT;
265
+ export async function getCDPPort(browser: string = 'com.oculus.browser'): Promise<number> {
266
+ const cdpSocket = await detectCDPSocket(browser);
267
+ return getCDPPortForSocket(cdpSocket);
204
268
  }
205
269
 
206
270
  /**
207
271
  * Set up only CDP forwarding (for external URLs that don't need reverse forwarding)
208
272
  */
209
- export async function ensureCDPForwarding(): Promise<void> {
273
+ export async function ensureCDPForwarding(
274
+ browser: string = 'com.oculus.browser'
275
+ ): Promise<void> {
210
276
  try {
277
+ // Detect CDP socket and port for this browser
278
+ const cdpSocket = await detectCDPSocket(browser);
279
+ const cdpPort = getCDPPortForSocket(cdpSocket);
280
+
211
281
  // Check forward forwarding (Host -> Quest for CDP)
212
282
  const forwardList = await execCommand('adb', ['forward', '--list']);
213
- const forwardExists = forwardList.includes(`tcp:${CDP_PORT}`) && forwardList.includes('chrome_devtools_remote');
283
+ const forwardExists = forwardList.includes(`tcp:${cdpPort}`) && forwardList.includes(cdpSocket);
214
284
 
215
285
  if (forwardExists) {
216
- console.log(`CDP port ${CDP_PORT} forwarding already set up`);
286
+ console.log(`CDP port ${cdpPort} forwarding already set up`);
217
287
  } else {
218
288
  // Check if something else is using the port
219
- const cdpPortListening = await isPortListening(CDP_PORT);
289
+ const cdpPortListening = await isPortListening(cdpPort);
220
290
  if (cdpPortListening) {
221
- console.error(`Error: Port ${CDP_PORT} is already in use by another process`);
291
+ console.error(`Error: Port ${cdpPort} is already in use by another process`);
222
292
  console.error('');
223
- console.error('CDP port forwarding requires port 9223 to be free.');
293
+ console.error(`CDP port forwarding requires port ${cdpPort} to be free.`);
224
294
  console.error('Please stop the process using this port and try again.');
225
295
  console.error('');
226
296
  console.error('To find what is using the port:');
227
- console.error(` lsof -i :${CDP_PORT}`);
297
+ console.error(` lsof -i :${cdpPort}`);
228
298
  console.error('');
229
299
  process.exit(1);
230
300
  }
231
301
 
232
- await execCommand('adb', ['forward', `tcp:${CDP_PORT}`, 'localabstract:chrome_devtools_remote']);
233
- console.log(`ADB forward port forwarding set up: Host:${CDP_PORT} -> Quest:chrome_devtools_remote (CDP)`);
302
+ await execCommand('adb', ['forward', `tcp:${cdpPort}`, `localabstract:${cdpSocket}`]);
303
+ console.log(`ADB forward port forwarding set up: Host:${cdpPort} -> Quest:${cdpSocket} (CDP)`);
234
304
  }
235
305
  } catch (error) {
236
306
  console.error('Failed to set up CDP forwarding:', (error as Error).message);
@@ -274,3 +344,50 @@ export async function checkQuestAwake(): Promise<void> {
274
344
  process.exit(1);
275
345
  }
276
346
  }
347
+
348
+ /**
349
+ * Get Quest battery status
350
+ * Returns percentage and charging state in one line
351
+ */
352
+ export async function getBatteryStatus(): Promise<string> {
353
+ const result = await execCommandFull('adb', ['shell', 'dumpsys', 'battery']);
354
+
355
+ if (result.code !== 0) {
356
+ throw new Error('Failed to get battery status');
357
+ }
358
+
359
+ // Parse battery info
360
+ let level = 0;
361
+ let acPowered = false;
362
+ let usbPowered = false;
363
+ let maxChargingCurrent = 0;
364
+
365
+ const lines = result.stdout.split('\n');
366
+ for (const line of lines) {
367
+ const trimmed = line.trim();
368
+ if (trimmed.startsWith('level: ')) {
369
+ level = parseInt(trimmed.substring(7), 10);
370
+ } else if (trimmed.startsWith('AC powered: ')) {
371
+ acPowered = trimmed.substring(12) === 'true';
372
+ } else if (trimmed.startsWith('USB powered: ')) {
373
+ usbPowered = trimmed.substring(13) === 'true';
374
+ } else if (trimmed.startsWith('Max charging current: ')) {
375
+ maxChargingCurrent = parseInt(trimmed.substring(22), 10);
376
+ }
377
+ }
378
+
379
+ // Determine charging state
380
+ let state: string;
381
+ if (acPowered || usbPowered) {
382
+ // Fast charging is typically > 2A (2000000 microamps)
383
+ if (maxChargingCurrent > 2000000) {
384
+ state = 'fast charging';
385
+ } else {
386
+ state = 'charging';
387
+ }
388
+ } else {
389
+ state = 'not charging';
390
+ }
391
+
392
+ return `${level}% ${state}`;
393
+ }