@myerscarpenter/quest-dev 1.0.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.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Shell command execution utilities
3
+ */
4
+ import { spawn } from 'child_process';
5
+ /**
6
+ * Execute a shell command and return a promise with stdout
7
+ */
8
+ export function execCommand(command, args = []) {
9
+ return new Promise((resolve, reject) => {
10
+ const proc = spawn(command, args, {
11
+ stdio: 'pipe',
12
+ shell: true
13
+ });
14
+ let stdout = '';
15
+ let stderr = '';
16
+ if (proc.stdout) {
17
+ proc.stdout.on('data', (data) => {
18
+ stdout += data.toString();
19
+ });
20
+ }
21
+ if (proc.stderr) {
22
+ proc.stderr.on('data', (data) => {
23
+ stderr += data.toString();
24
+ });
25
+ }
26
+ proc.on('close', (code) => {
27
+ if (code === 0) {
28
+ resolve(stdout);
29
+ }
30
+ else {
31
+ reject(new Error(`Command failed with code ${code}: ${stderr}`));
32
+ }
33
+ });
34
+ proc.on('error', reject);
35
+ });
36
+ }
37
+ /**
38
+ * Execute a shell command and return full result (doesn't throw on non-zero exit)
39
+ */
40
+ export function execCommandFull(command, args = []) {
41
+ return new Promise((resolve) => {
42
+ const proc = spawn(command, args, {
43
+ stdio: 'pipe',
44
+ shell: true
45
+ });
46
+ let stdout = '';
47
+ let stderr = '';
48
+ if (proc.stdout) {
49
+ proc.stdout.on('data', (data) => {
50
+ stdout += data.toString();
51
+ });
52
+ }
53
+ if (proc.stderr) {
54
+ proc.stderr.on('data', (data) => {
55
+ stderr += data.toString();
56
+ });
57
+ }
58
+ proc.on('close', (code) => {
59
+ resolve({ stdout, stderr, code: code ?? 1 });
60
+ });
61
+ proc.on('error', (err) => {
62
+ resolve({ stdout, stderr: err.message, code: 1 });
63
+ });
64
+ });
65
+ }
66
+ //# sourceMappingURL=exec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/utils/exec.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAQtC;;GAEG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,OAAiB,EAAE;IAC9D,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE;YAChC,KAAK,EAAE,MAAM;YACb,KAAK,EAAE,IAAI;SACZ,CAAC,CAAC;QAEH,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAEhB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;gBAC9B,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5B,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;gBAC9B,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5B,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YACxB,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;gBACf,OAAO,CAAC,MAAM,CAAC,CAAC;YAClB,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,IAAI,KAAK,CAAC,4BAA4B,IAAI,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC;YACnE,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,OAAiB,EAAE;IAClE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE;YAChC,KAAK,EAAE,MAAM;YACb,KAAK,EAAE,IAAI;SACZ,CAAC,CAAC;QAEH,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAEhB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;gBAC9B,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5B,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;gBAC9B,MAAM,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5B,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YACxB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACvB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@myerscarpenter/quest-dev",
3
+ "version": "1.0.0",
4
+ "description": "CLI for Meta Quest Browser development - screenshot and URL opening via ADB and cdp-cli",
5
+ "type": "module",
6
+ "bin": {
7
+ "quest-dev": "./build/index.js"
8
+ },
9
+ "main": "./build/index.js",
10
+ "keywords": [
11
+ "meta",
12
+ "quest",
13
+ "quest-browser",
14
+ "adb",
15
+ "cli",
16
+ "automation",
17
+ "vr",
18
+ "xr",
19
+ "screenshot"
20
+ ],
21
+ "author": "",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/myers/quest-dev"
26
+ },
27
+ "dependencies": {
28
+ "which": "^4.0.0",
29
+ "yargs": "^18.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^24.3.3",
33
+ "@types/which": "^3.0.4",
34
+ "@types/yargs": "^17.0.33",
35
+ "@vitest/ui": "^4.0.3",
36
+ "c8": "^10.1.3",
37
+ "typescript": "^5.9.2",
38
+ "vitest": "^4.0.3"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ },
43
+ "peerDependencies": {
44
+ "@myerscarpenter/cdp-cli": ">=2.0.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "@myerscarpenter/cdp-cli": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "scripts": {
52
+ "clean": "rm -rf build",
53
+ "build": "pnpm run clean && tsc && chmod +x build/index.js",
54
+ "start": "pnpm run build && node build/index.js",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest",
57
+ "test:ui": "vitest --ui",
58
+ "test:coverage": "vitest run --coverage",
59
+ "version:patch": "pnpm version patch",
60
+ "version:minor": "pnpm version minor",
61
+ "version:major": "pnpm version major"
62
+ }
63
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Quest open command
3
+ * Opens a URL in Quest browser with proper ADB port forwarding
4
+ */
5
+
6
+ import {
7
+ checkADBPath,
8
+ checkADBDevices,
9
+ ensurePortForwarding,
10
+ isBrowserRunning,
11
+ launchBrowser,
12
+ getCDPPort
13
+ } from '../utils/adb.js';
14
+ import { execCommand, execCommandFull } from '../utils/exec.js';
15
+
16
+ /**
17
+ * Try to navigate or reload existing tab via cdp-cli
18
+ */
19
+ async function tryNavigateExistingTab(targetUrl: string): Promise<boolean> {
20
+ const cdpPort = getCDPPort();
21
+
22
+ try {
23
+ // Get list of tabs using cdp-cli
24
+ const result = await execCommandFull('cdp-cli', ['--cdp-url', `http://localhost:${cdpPort}`, 'tabs']);
25
+
26
+ if (result.code !== 0) {
27
+ console.log('cdp-cli tabs command failed, will launch browser directly');
28
+ return false;
29
+ }
30
+
31
+ // Parse NDJSON output to find tabs
32
+ const lines = result.stdout.trim().split('\n').filter(line => line.trim());
33
+ const tabs: Array<{ id: string; url: string; title: string }> = [];
34
+
35
+ for (const line of lines) {
36
+ try {
37
+ const tab = JSON.parse(line);
38
+ if (tab.id && tab.url !== undefined) {
39
+ tabs.push(tab);
40
+ }
41
+ } catch {
42
+ // Skip non-JSON lines
43
+ }
44
+ }
45
+
46
+ // First, check if URL is already open
47
+ const existingTab = tabs.find(tab => tab.url === targetUrl);
48
+ if (existingTab) {
49
+ console.log(`Found existing tab with URL: ${targetUrl}`);
50
+
51
+ // Reload the tab
52
+ const reloadResult = await execCommandFull('cdp-cli', [
53
+ '--cdp-url', `http://localhost:${cdpPort}`,
54
+ 'go', existingTab.id, 'reload'
55
+ ]);
56
+
57
+ if (reloadResult.code === 0) {
58
+ console.log('Reloaded existing tab');
59
+ return true;
60
+ }
61
+ }
62
+
63
+ // Second, look for a blank tab to navigate
64
+ const blankTab = tabs.find(tab =>
65
+ tab.url === 'about:blank' ||
66
+ tab.url === 'chrome://newtab/' ||
67
+ tab.url === 'chrome://panel-app-nav/ntp' || // Quest New Tab page
68
+ tab.url === ''
69
+ );
70
+
71
+ if (blankTab) {
72
+ console.log('Found blank tab, navigating it...');
73
+
74
+ const navResult = await execCommandFull('cdp-cli', [
75
+ '--cdp-url', `http://localhost:${cdpPort}`,
76
+ 'go', blankTab.id, targetUrl
77
+ ]);
78
+
79
+ if (navResult.code === 0) {
80
+ console.log('Navigated blank tab to URL');
81
+ return true;
82
+ }
83
+ }
84
+
85
+ return false;
86
+ } catch (error) {
87
+ console.log('CDP operation failed:', (error as Error).message);
88
+ return false;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Main open command handler
94
+ */
95
+ export async function openCommand(url: string): Promise<void> {
96
+ // Parse port from URL
97
+ let port: number;
98
+ try {
99
+ const parsedUrl = new URL(url);
100
+ port = parseInt(parsedUrl.port, 10);
101
+ if (!port || isNaN(port)) {
102
+ console.error('Error: Could not parse port from URL:', url);
103
+ process.exit(1);
104
+ }
105
+ } catch (error) {
106
+ console.error('Error: Invalid URL:', url);
107
+ process.exit(1);
108
+ }
109
+
110
+ console.log(`\nOpening ${url} on Quest...\n`);
111
+
112
+ // Check prerequisites
113
+ checkADBPath();
114
+ await checkADBDevices();
115
+
116
+ // Set up port forwarding (idempotent)
117
+ await ensurePortForwarding(port);
118
+
119
+ // Check if browser is running
120
+ const browserRunning = await isBrowserRunning();
121
+
122
+ if (!browserRunning) {
123
+ console.log('Quest browser is not running');
124
+ await launchBrowser(url);
125
+ } else {
126
+ console.log('Quest browser is already running');
127
+
128
+ // Try to navigate existing or blank tab via cdp-cli first
129
+ const navigated = await tryNavigateExistingTab(url);
130
+
131
+ if (!navigated) {
132
+ console.log('No existing or blank tab found, opening URL...');
133
+ await launchBrowser(url);
134
+ }
135
+ }
136
+
137
+ console.log('\nDone!\n');
138
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Quest screenshot command
3
+ * Triggers Quest's native screenshot service and pulls the file
4
+ */
5
+
6
+ import { resolve } from 'path';
7
+ import { checkADBPath, checkADBDevices } from '../utils/adb.js';
8
+ import { execCommand } from '../utils/exec.js';
9
+
10
+ /**
11
+ * Trigger Quest screenshot service
12
+ */
13
+ async function triggerScreenshot(): Promise<boolean> {
14
+ try {
15
+ await execCommand('adb', [
16
+ 'shell',
17
+ 'am',
18
+ 'startservice',
19
+ '-n',
20
+ 'com.oculus.metacam/.capture.CaptureService',
21
+ '-a',
22
+ 'TAKE_SCREENSHOT'
23
+ ]);
24
+ console.log('Screenshot service triggered');
25
+ return true;
26
+ } catch (error) {
27
+ console.error('Failed to trigger screenshot:', (error as Error).message);
28
+ return false;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Get most recent screenshot filename from Quest
34
+ */
35
+ async function getMostRecentScreenshot(): Promise<string | null> {
36
+ try {
37
+ const output = await execCommand('adb', ['shell', 'ls', '-t', '/sdcard/Oculus/Screenshots/']);
38
+ const files = output.split('\n').filter(line => line.trim() && line.endsWith('.jpg'));
39
+
40
+ if (files.length === 0) {
41
+ console.error('No screenshots found in /sdcard/Oculus/Screenshots/');
42
+ return null;
43
+ }
44
+
45
+ const mostRecent = files[0].trim();
46
+ console.log(`Found most recent screenshot: ${mostRecent}`);
47
+ return mostRecent;
48
+ } catch (error) {
49
+ console.error('Failed to list screenshots:', (error as Error).message);
50
+ return null;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Pull screenshot from Quest to local path
56
+ */
57
+ async function pullScreenshot(filename: string, outputPath: string): Promise<boolean> {
58
+ try {
59
+ const remotePath = `/sdcard/Oculus/Screenshots/${filename}`;
60
+ await execCommand('adb', ['pull', remotePath, outputPath]);
61
+ console.log(`Screenshot saved to: ${outputPath}`);
62
+ return true;
63
+ } catch (error) {
64
+ console.error('Failed to pull screenshot:', (error as Error).message);
65
+ return false;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Main screenshot command handler
71
+ */
72
+ export async function screenshotCommand(outputPath: string): Promise<void> {
73
+ const resolvedPath = resolve(outputPath);
74
+
75
+ console.log('\nQuest Screenshot\n');
76
+
77
+ // Check prerequisites
78
+ checkADBPath();
79
+ await checkADBDevices();
80
+
81
+ // Trigger screenshot
82
+ if (!await triggerScreenshot()) {
83
+ process.exit(1);
84
+ }
85
+
86
+ // Wait for screenshot to save
87
+ console.log('Waiting for screenshot to save...');
88
+ await new Promise(resolve => setTimeout(resolve, 2000));
89
+
90
+ // Get most recent screenshot
91
+ const filename = await getMostRecentScreenshot();
92
+ if (!filename) {
93
+ process.exit(1);
94
+ }
95
+
96
+ // Pull screenshot
97
+ if (!await pullScreenshot(filename, resolvedPath)) {
98
+ process.exit(1);
99
+ }
100
+
101
+ console.log('\nDone!\n');
102
+ }
package/src/index.ts ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Quest Dev CLI
5
+ * Command-line tools for Meta Quest Browser development
6
+ */
7
+
8
+ import yargs from 'yargs';
9
+ import { hideBin } from 'yargs/helpers';
10
+ import { readFileSync } from 'fs';
11
+ import { fileURLToPath } from 'url';
12
+ import { dirname, join } from 'path';
13
+ import { screenshotCommand } from './commands/screenshot.js';
14
+ import { openCommand } from './commands/open.js';
15
+
16
+ // Read version from package.json
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const packageJson = JSON.parse(
19
+ readFileSync(join(__dirname, '../package.json'), 'utf-8')
20
+ );
21
+ const version = packageJson.version;
22
+
23
+ // Create CLI
24
+ const cli = yargs(hideBin(process.argv))
25
+ .scriptName('quest-dev')
26
+ .version(version)
27
+ .usage('Usage: $0 <command> [options]')
28
+ .demandCommand(1, 'You must provide a command')
29
+ .strict()
30
+ .fail((msg, err, yargs) => {
31
+ if (msg) {
32
+ console.error(`Error: ${msg}\n`);
33
+ }
34
+ if (err) {
35
+ console.error(err.message);
36
+ }
37
+ console.error('Run "quest-dev --help" for usage information.');
38
+ process.exit(1);
39
+ })
40
+ .help()
41
+ .alias('help', 'h')
42
+ .epilog('Requires ADB to be installed and Quest connected via USB with debugging enabled.');
43
+
44
+ // Screenshot command
45
+ cli.command(
46
+ 'screenshot <output>',
47
+ 'Take a screenshot from Quest and save to local file',
48
+ (yargs) => {
49
+ return yargs.positional('output', {
50
+ describe: 'Output file path (e.g., ~/screenshots/test.jpg)',
51
+ type: 'string',
52
+ demandOption: true
53
+ });
54
+ },
55
+ async (argv) => {
56
+ await screenshotCommand(argv.output as string);
57
+ }
58
+ );
59
+
60
+ // Open command
61
+ cli.command(
62
+ 'open <url>',
63
+ 'Open URL in Quest browser with ADB port forwarding',
64
+ (yargs) => {
65
+ return yargs.positional('url', {
66
+ describe: 'URL to open (e.g., http://localhost:9004/myapp/)',
67
+ type: 'string',
68
+ demandOption: true
69
+ });
70
+ },
71
+ async (argv) => {
72
+ await openCommand(argv.url as string);
73
+ }
74
+ );
75
+
76
+ // Parse and execute
77
+ cli.parse();
@@ -0,0 +1,197 @@
1
+ /**
2
+ * ADB utilities for Quest device communication
3
+ */
4
+
5
+ import which from 'which';
6
+ import net from 'net';
7
+ import { execCommand, execCommandFull } from './exec.js';
8
+
9
+ const CDP_PORT = 9223; // Chrome DevTools Protocol port (Quest browser default)
10
+
11
+ /**
12
+ * Check if ADB is available on PATH
13
+ */
14
+ export function checkADBPath(): string {
15
+ try {
16
+ const adbPath = which.sync('adb');
17
+ console.log(`Found ADB at: ${adbPath}`);
18
+ return adbPath;
19
+ } catch (error) {
20
+ console.error('Error: ADB not found in PATH');
21
+ console.error('');
22
+ console.error('Please install Android Platform Tools and add adb to your PATH:');
23
+ console.error('https://developer.android.com/tools/releases/platform-tools');
24
+ console.error('');
25
+ console.error('Installation instructions:');
26
+ console.error('- macOS: brew install android-platform-tools');
27
+ console.error('- Linux: sudo apt install adb (or equivalent)');
28
+ console.error('- Windows: Download from the link above and add to PATH');
29
+ console.error('');
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Restart ADB server if it's in a bad state
36
+ */
37
+ async function restartADBServer(): Promise<boolean> {
38
+ console.log('ADB server appears to be in a bad state, restarting...');
39
+ try {
40
+ // Kill server (ignore errors - it might already be dead)
41
+ await execCommandFull('adb', ['kill-server']);
42
+ // Start server
43
+ await execCommand('adb', ['start-server']);
44
+ console.log('ADB server restarted successfully');
45
+ return true;
46
+ } catch (error) {
47
+ console.error('Failed to restart ADB server:', (error as Error).message);
48
+ return false;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Check if ADB devices are connected (with auto-recovery for server issues)
54
+ */
55
+ export async function checkADBDevices(retryCount = 0): Promise<boolean> {
56
+ try {
57
+ const output = await execCommand('adb', ['devices']);
58
+ const lines = output.trim().split('\n').slice(1); // Skip header
59
+ const devices = lines.filter(line => line.trim() && !line.includes('List of devices'));
60
+
61
+ if (devices.length === 0) {
62
+ console.error('Error: No ADB devices connected');
63
+ console.error('');
64
+ console.error('Please connect your Quest device via USB and enable USB debugging');
65
+ console.error('');
66
+ process.exit(1);
67
+ }
68
+
69
+ console.log(`Found ${devices.length} ADB device(s)`);
70
+ return true;
71
+ } catch (error) {
72
+ const errorMsg = (error as Error).message;
73
+ // Check if it's a server issue
74
+ const isServerIssue = errorMsg.includes('protocol fault') ||
75
+ errorMsg.includes('Connection reset') ||
76
+ errorMsg.includes('server version') ||
77
+ errorMsg.includes('cannot connect to daemon');
78
+
79
+ if (isServerIssue && retryCount === 0) {
80
+ const restarted = await restartADBServer();
81
+ if (restarted) {
82
+ return await checkADBDevices(1);
83
+ }
84
+ }
85
+
86
+ console.error('Error: Failed to list ADB devices:', errorMsg);
87
+ console.error('');
88
+ console.error('Try running: adb kill-server && adb start-server');
89
+ console.error('');
90
+ process.exit(1);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Check if a port is already listening on localhost
96
+ */
97
+ export function isPortListening(port: number): Promise<boolean> {
98
+ return new Promise((resolve) => {
99
+ const socket = new net.Socket();
100
+ socket.setTimeout(500);
101
+ socket.on('connect', () => {
102
+ socket.destroy();
103
+ resolve(true);
104
+ });
105
+ socket.on('timeout', () => {
106
+ socket.destroy();
107
+ resolve(false);
108
+ });
109
+ socket.on('error', () => {
110
+ resolve(false);
111
+ });
112
+ socket.connect(port, '127.0.0.1');
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Idempotently set up ADB port forwarding for a given port
118
+ */
119
+ export async function ensurePortForwarding(port: number): Promise<void> {
120
+ try {
121
+ // Check reverse forwarding (Quest -> Host for dev server)
122
+ const reverseList = await execCommand('adb', ['reverse', '--list']);
123
+ const reverseExists = reverseList.includes(`tcp:${port}`);
124
+
125
+ if (reverseExists) {
126
+ console.log(`ADB reverse port forwarding already set up: Quest:${port} -> Host:${port}`);
127
+ } else {
128
+ await execCommand('adb', ['reverse', `tcp:${port}`, `tcp:${port}`]);
129
+ console.log(`ADB reverse port forwarding set up: Quest:${port} -> Host:${port}`);
130
+ }
131
+
132
+ // Check forward forwarding (Host -> Quest for CDP)
133
+ const cdpPortListening = await isPortListening(CDP_PORT);
134
+
135
+ if (cdpPortListening) {
136
+ console.log(`CDP port ${CDP_PORT} already listening (forwarding assumed active)`);
137
+ } else {
138
+ try {
139
+ await execCommand('adb', ['forward', `tcp:${CDP_PORT}`, 'localabstract:chrome_devtools_remote']);
140
+ console.log(`ADB forward port forwarding set up: Host:${CDP_PORT} -> Quest:chrome_devtools_remote (CDP)`);
141
+ } catch (forwardError) {
142
+ const errorMsg = (forwardError as Error).message;
143
+ if (errorMsg.includes('Address already in use')) {
144
+ console.log(`CDP port ${CDP_PORT} is now active (another process set it up)`);
145
+ } else {
146
+ throw forwardError;
147
+ }
148
+ }
149
+ }
150
+ } catch (error) {
151
+ console.error('Failed to set up port forwarding:', (error as Error).message);
152
+ process.exit(1);
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Check if Quest browser is running
158
+ */
159
+ export async function isBrowserRunning(): Promise<boolean> {
160
+ try {
161
+ const result = await execCommandFull('adb', ['shell', 'ps | grep com.oculus.browser']);
162
+ return result.stdout.includes('com.oculus.browser');
163
+ } catch (error) {
164
+ return false;
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Launch Quest browser with a URL using am start
170
+ */
171
+ export async function launchBrowser(url: string): Promise<boolean> {
172
+ console.log('Launching Quest browser...');
173
+ try {
174
+ await execCommand('adb', [
175
+ 'shell',
176
+ 'am',
177
+ 'start',
178
+ '-a',
179
+ 'android.intent.action.VIEW',
180
+ '-d',
181
+ url,
182
+ 'com.oculus.browser'
183
+ ]);
184
+ console.log(`Quest browser launched with URL: ${url}`);
185
+ return true;
186
+ } catch (error) {
187
+ console.error('Failed to launch Quest browser:', (error as Error).message);
188
+ return false;
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Get CDP port
194
+ */
195
+ export function getCDPPort(): number {
196
+ return CDP_PORT;
197
+ }