@myerscarpenter/quest-dev 1.1.0 → 1.2.1

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 (49) 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.map +1 -1
  6. package/build/commands/logcat.js +8 -6
  7. package/build/commands/logcat.js.map +1 -1
  8. package/build/commands/open.d.ts +1 -1
  9. package/build/commands/open.d.ts.map +1 -1
  10. package/build/commands/open.js +14 -14
  11. package/build/commands/open.js.map +1 -1
  12. package/build/commands/screenshot.d.ts +1 -1
  13. package/build/commands/screenshot.d.ts.map +1 -1
  14. package/build/commands/screenshot.js +55 -9
  15. package/build/commands/screenshot.js.map +1 -1
  16. package/build/commands/stay-awake.d.ts +14 -0
  17. package/build/commands/stay-awake.d.ts.map +1 -0
  18. package/build/commands/stay-awake.js +234 -0
  19. package/build/commands/stay-awake.js.map +1 -0
  20. package/build/index.js +49 -5
  21. package/build/index.js.map +1 -1
  22. package/build/utils/adb.d.ts +12 -7
  23. package/build/utils/adb.d.ts.map +1 -1
  24. package/build/utils/adb.js +138 -30
  25. package/build/utils/adb.js.map +1 -1
  26. package/build/utils/exec.d.ts.map +1 -1
  27. package/build/utils/exec.js +0 -2
  28. package/build/utils/exec.js.map +1 -1
  29. package/build/utils/filename.d.ts +9 -0
  30. package/build/utils/filename.d.ts.map +1 -0
  31. package/build/utils/filename.js +17 -0
  32. package/build/utils/filename.js.map +1 -0
  33. package/build/utils/filename.test.d.ts +5 -0
  34. package/build/utils/filename.test.d.ts.map +1 -0
  35. package/build/utils/filename.test.js +40 -0
  36. package/build/utils/filename.test.js.map +1 -0
  37. package/package.json +2 -1
  38. package/src/commands/battery.ts +34 -0
  39. package/src/commands/logcat.ts +7 -5
  40. package/src/commands/open.ts +18 -14
  41. package/src/commands/screenshot.ts +61 -9
  42. package/src/commands/stay-awake.ts +254 -0
  43. package/src/index.ts +77 -9
  44. package/src/utils/adb.ts +148 -30
  45. package/src/utils/exec.ts +0 -2
  46. package/src/utils/filename.test.ts +55 -0
  47. package/src/utils/filename.ts +18 -0
  48. package/tests/adb.test.ts +2 -2
  49. package/tests/exec.test.ts +3 -3
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Quest battery command
3
+ * Shows battery percentage and charging status
4
+ */
5
+
6
+ import { checkADBPath, checkADBDevices, getBatteryStatus } from '../utils/adb.js';
7
+
8
+ /**
9
+ * Main battery command handler
10
+ */
11
+ export async function batteryCommand(): Promise<void> {
12
+ // Check prerequisites (silent)
13
+ checkADBPath();
14
+
15
+ // Check devices without verbose output
16
+ try {
17
+ const { execCommand } = await import('../utils/exec.js');
18
+ const output = await execCommand('adb', ['devices']);
19
+ const lines = output.trim().split('\n').slice(1);
20
+ const devices = lines.filter(line => line.trim() && !line.includes('List of devices'));
21
+
22
+ if (devices.length === 0) {
23
+ console.error('Error: No ADB devices connected');
24
+ process.exit(1);
25
+ }
26
+ } catch (error) {
27
+ console.error('Error: Failed to list ADB devices');
28
+ process.exit(1);
29
+ }
30
+
31
+ // Get and display battery status
32
+ const status = await getBatteryStatus();
33
+ console.log(status);
34
+ }
@@ -250,11 +250,13 @@ export async function statusCommand(): Promise<void> {
250
250
  console.log('');
251
251
  console.log('Recent logs:');
252
252
  try {
253
- const result = await execCommandFull('ls', ['-lht', join(LOG_DIR, '*.txt')]);
254
- if (result.code === 0) {
255
- const lines = result.stdout.trim().split('\n').slice(0, 5);
256
- lines.forEach(line => console.log(' ' + line));
257
- }
253
+ const { readdirSync } = await import('fs');
254
+ const files = readdirSync(LOG_DIR)
255
+ .filter(f => f.endsWith('.txt'))
256
+ .map(f => ({ name: f, mtime: statSync(join(LOG_DIR, f)).mtimeMs }))
257
+ .sort((a, b) => b.mtime - a.mtime)
258
+ .slice(0, 5);
259
+ files.forEach(f => console.log(' ' + f.name));
258
260
  } catch {
259
261
  // Ignore
260
262
  }
@@ -17,8 +17,8 @@ import { execCommand, execCommandFull } from '../utils/exec.js';
17
17
  /**
18
18
  * Close all tabs except the one with the target URL
19
19
  */
20
- async function closeOtherTabs(targetUrl: string): Promise<void> {
21
- const cdpPort = getCDPPort();
20
+ async function closeOtherTabs(targetUrl: string, browser: string): Promise<void> {
21
+ const cdpPort = await getCDPPort(browser);
22
22
 
23
23
  try {
24
24
  // Get list of tabs
@@ -66,8 +66,8 @@ async function closeOtherTabs(targetUrl: string): Promise<void> {
66
66
  /**
67
67
  * Try to navigate or reload existing tab via cdp-cli
68
68
  */
69
- async function tryNavigateExistingTab(targetUrl: string): Promise<boolean> {
70
- const cdpPort = getCDPPort();
69
+ async function tryNavigateExistingTab(targetUrl: string, browser: string): Promise<boolean> {
70
+ const cdpPort = await getCDPPort(browser);
71
71
 
72
72
  try {
73
73
  // Get list of tabs using cdp-cli
@@ -142,7 +142,11 @@ async function tryNavigateExistingTab(targetUrl: string): Promise<boolean> {
142
142
  /**
143
143
  * Main open command handler
144
144
  */
145
- export async function openCommand(url: string, closeOthers: boolean = false): Promise<void> {
145
+ export async function openCommand(
146
+ url: string,
147
+ closeOthers: boolean = false,
148
+ browser: string = 'com.oculus.browser'
149
+ ): Promise<void> {
146
150
  // Parse URL to determine if we need reverse port forwarding
147
151
  let parsedUrl: URL;
148
152
  try {
@@ -175,27 +179,27 @@ export async function openCommand(url: string, closeOthers: boolean = false): Pr
175
179
  // Set up port forwarding
176
180
  if (port !== null) {
177
181
  // Localhost URL: need reverse forwarding so Quest can reach the dev server
178
- await ensurePortForwarding(port);
182
+ await ensurePortForwarding(port, browser);
179
183
  } else {
180
184
  // External URL: only need CDP forwarding to control the browser
181
- await ensureCDPForwarding();
185
+ await ensureCDPForwarding(browser);
182
186
  }
183
187
 
184
188
  // Check if browser is running
185
- const browserRunning = await isBrowserRunning();
189
+ const browserRunning = await isBrowserRunning(browser);
186
190
 
187
191
  if (!browserRunning) {
188
- console.log('Quest browser is not running');
189
- await launchBrowser(url);
192
+ console.log('Browser is not running');
193
+ await launchBrowser(url, browser);
190
194
  } else {
191
- console.log('Quest browser is already running');
195
+ console.log('Browser is already running');
192
196
 
193
197
  // Try to navigate existing or blank tab via cdp-cli first
194
- const navigated = await tryNavigateExistingTab(url);
198
+ const navigated = await tryNavigateExistingTab(url, browser);
195
199
 
196
200
  if (!navigated) {
197
201
  console.log('No existing or blank tab found, opening URL...');
198
- await launchBrowser(url);
202
+ await launchBrowser(url, browser);
199
203
  }
200
204
  }
201
205
 
@@ -204,7 +208,7 @@ export async function openCommand(url: string, closeOthers: boolean = false): Pr
204
208
  // Wait for browser to stabilize after launch/navigation
205
209
  console.log('Waiting for browser to stabilize...');
206
210
  await new Promise(resolve => setTimeout(resolve, 2000));
207
- await closeOtherTabs(url);
211
+ await closeOtherTabs(url, browser);
208
212
  }
209
213
 
210
214
  console.log('\nDone!\n');
@@ -3,9 +3,33 @@
3
3
  * Triggers Quest's native screenshot service and pulls the file
4
4
  */
5
5
 
6
- import { resolve } from 'path';
6
+ import { resolve, join } from 'path';
7
+ import { existsSync, statSync } from 'fs';
7
8
  import { checkADBPath, checkADBDevices, checkUSBFileTransfer, checkQuestAwake } from '../utils/adb.js';
8
9
  import { execCommand, execCommandFull } from '../utils/exec.js';
10
+ import { generateScreenshotFilename } from '../utils/filename.js';
11
+
12
+ /**
13
+ * Validate directory exists and is writable
14
+ */
15
+ function validateDirectory(dirPath: string): void {
16
+ const resolvedPath = resolve(dirPath);
17
+
18
+ if (!existsSync(resolvedPath)) {
19
+ console.error(`Error: Directory does not exist: ${resolvedPath}`);
20
+ console.error('');
21
+ console.error('Please create the directory first.');
22
+ console.error('');
23
+ process.exit(1);
24
+ }
25
+
26
+ const stat = statSync(resolvedPath);
27
+ if (!stat.isDirectory()) {
28
+ console.error(`Error: Path is not a directory: ${resolvedPath}`);
29
+ console.error('');
30
+ process.exit(1);
31
+ }
32
+ }
9
33
 
10
34
  /**
11
35
  * Trigger Quest screenshot service
@@ -106,14 +130,37 @@ async function deleteRemoteScreenshot(filename: string): Promise<void> {
106
130
  }
107
131
  }
108
132
 
133
+ /**
134
+ * Add caption to JPEG COM metadata
135
+ */
136
+ async function addJpegMetadata(filePath: string, caption: string): Promise<boolean> {
137
+ try {
138
+ const { exiftool } = await import('exiftool-vendored');
139
+ await exiftool.write(filePath, { Comment: caption });
140
+ await exiftool.end();
141
+ console.log(`Caption added: "${caption}"`);
142
+ return true;
143
+ } catch (error) {
144
+ console.error('Warning: Failed to add caption metadata:', (error as Error).message);
145
+ return false;
146
+ }
147
+ }
148
+
109
149
  /**
110
150
  * Main screenshot command handler
111
151
  */
112
- export async function screenshotCommand(outputPath: string): Promise<void> {
113
- const resolvedPath = resolve(outputPath);
152
+ export async function screenshotCommand(directoryPath: string, caption: string | undefined): Promise<void> {
153
+ const resolvedDir = resolve(directoryPath);
114
154
 
115
155
  console.log('\nQuest Screenshot\n');
116
156
 
157
+ // Validate directory (fail-fast before expensive ADB ops)
158
+ validateDirectory(resolvedDir);
159
+
160
+ // Generate filename
161
+ const localFilename = generateScreenshotFilename();
162
+ const outputPath = join(resolvedDir, localFilename);
163
+
117
164
  // Check prerequisites
118
165
  checkADBPath();
119
166
  await checkADBDevices();
@@ -130,7 +177,7 @@ export async function screenshotCommand(outputPath: string): Promise<void> {
130
177
 
131
178
  // Wait for screenshot to save and verify it's complete (has JPEG EOI marker)
132
179
  console.log('Waiting for screenshot to save...');
133
- let filename: string | null = null;
180
+ let questFilename: string | null = null;
134
181
  const maxAttempts = 20;
135
182
 
136
183
  for (let i = 0; i < maxAttempts; i++) {
@@ -141,14 +188,14 @@ export async function screenshotCommand(outputPath: string): Promise<void> {
141
188
  // Check that the JPEG is fully written (has EOI marker)
142
189
  const complete = await isJpegComplete(newScreenshot);
143
190
  if (complete) {
144
- filename = newScreenshot;
145
- console.log(`Screenshot ready: ${filename}`);
191
+ questFilename = newScreenshot;
192
+ console.log(`Screenshot ready: ${questFilename}`);
146
193
  break;
147
194
  }
148
195
  }
149
196
  }
150
197
 
151
- if (!filename) {
198
+ if (!questFilename) {
152
199
  console.error('Error: Screenshot was not created or is incomplete');
153
200
  console.error('');
154
201
  console.error('The screenshot service was triggered but no valid screenshot appeared.');
@@ -162,12 +209,17 @@ export async function screenshotCommand(outputPath: string): Promise<void> {
162
209
  }
163
210
 
164
211
  // Pull screenshot
165
- if (!await pullScreenshot(filename, resolvedPath)) {
212
+ if (!await pullScreenshot(questFilename, outputPath)) {
166
213
  process.exit(1);
167
214
  }
168
215
 
216
+ // Add metadata (non-fatal, only if caption provided)
217
+ if (caption) {
218
+ await addJpegMetadata(outputPath, caption);
219
+ }
220
+
169
221
  // Delete from Quest after successful pull
170
- await deleteRemoteScreenshot(filename);
222
+ await deleteRemoteScreenshot(questFilename);
171
223
 
172
224
  console.log('\nDone!\n');
173
225
  }
@@ -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
@@ -13,6 +13,8 @@ import { dirname, join } from 'path';
13
13
  import { screenshotCommand } from './commands/screenshot.js';
14
14
  import { openCommand } from './commands/open.js';
15
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';
16
18
 
17
19
  // Read version from package.json
18
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -44,17 +46,26 @@ const cli = yargs(hideBin(process.argv))
44
46
 
45
47
  // Screenshot command
46
48
  cli.command(
47
- 'screenshot <output>',
48
- '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',
49
51
  (yargs) => {
50
- return yargs.positional('output', {
51
- describe: 'Output file path (e.g., ~/screenshots/test.jpg)',
52
- type: 'string',
53
- demandOption: true
54
- });
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
+ });
55
63
  },
56
64
  async (argv) => {
57
- await screenshotCommand(argv.output as string);
65
+ await screenshotCommand(
66
+ argv.directory as string,
67
+ argv.caption as string | undefined
68
+ );
58
69
  }
59
70
  );
60
71
 
@@ -73,10 +84,20 @@ cli.command(
73
84
  describe: 'Close all other tabs before opening',
74
85
  type: 'boolean',
75
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'
76
93
  });
77
94
  },
78
95
  async (argv) => {
79
- await openCommand(argv.url as string, argv.closeOthers as boolean);
96
+ await openCommand(
97
+ argv.url as string,
98
+ argv.closeOthers as boolean,
99
+ argv.browser as string
100
+ );
80
101
  }
81
102
  );
82
103
 
@@ -121,5 +142,52 @@ cli.command(
121
142
  }
122
143
  );
123
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
185
+ });
186
+ },
187
+ async (argv) => {
188
+ await stayAwakeWatchdog(argv.parentPid as number, argv.originalTimeout as number);
189
+ }
190
+ );
191
+
124
192
  // Parse and execute
125
193
  cli.parse();