@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
package/src/utils/adb.ts CHANGED
@@ -8,6 +8,63 @@ 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
  */
@@ -115,8 +172,15 @@ export function isPortListening(port: number): Promise<boolean> {
115
172
  /**
116
173
  * Idempotently set up ADB port forwarding for a given port
117
174
  */
118
- 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> {
119
179
  try {
180
+ // Detect CDP socket and port for this browser
181
+ const cdpSocket = await detectCDPSocket(browser);
182
+ const cdpPort = getCDPPortForSocket(cdpSocket);
183
+
120
184
  // Check reverse forwarding (Quest -> Host for dev server)
121
185
  const reverseList = await execCommand('adb', ['reverse', '--list']);
122
186
  const reverseExists = reverseList.includes(`tcp:${port}`);
@@ -131,27 +195,27 @@ export async function ensurePortForwarding(port: number): Promise<void> {
131
195
  // Check forward forwarding (Host -> Quest for CDP)
132
196
  // First check if ADB already has this forwarding set up
133
197
  const forwardList = await execCommand('adb', ['forward', '--list']);
134
- const forwardExists = forwardList.includes(`tcp:${CDP_PORT}`) && forwardList.includes('chrome_devtools_remote');
198
+ const forwardExists = forwardList.includes(`tcp:${cdpPort}`) && forwardList.includes(cdpSocket);
135
199
 
136
200
  if (forwardExists) {
137
- console.log(`CDP port ${CDP_PORT} forwarding already set up`);
201
+ console.log(`CDP port ${cdpPort} forwarding already set up`);
138
202
  } else {
139
203
  // Check if something else is using the port
140
- const cdpPortListening = await isPortListening(CDP_PORT);
204
+ const cdpPortListening = await isPortListening(cdpPort);
141
205
  if (cdpPortListening) {
142
- 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`);
143
207
  console.error('');
144
- console.error('CDP port forwarding requires port 9223 to be free.');
208
+ console.error(`CDP port forwarding requires port ${cdpPort} to be free.`);
145
209
  console.error('Please stop the process using this port and try again.');
146
210
  console.error('');
147
211
  console.error('To find what is using the port:');
148
- console.error(` lsof -i :${CDP_PORT}`);
212
+ console.error(` lsof -i :${cdpPort}`);
149
213
  console.error('');
150
214
  process.exit(1);
151
215
  }
152
216
 
153
- await execCommand('adb', ['forward', `tcp:${CDP_PORT}`, 'localabstract:chrome_devtools_remote']);
154
- 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)`);
155
219
  }
156
220
  } catch (error) {
157
221
  console.error('Failed to set up port forwarding:', (error as Error).message);
@@ -160,22 +224,22 @@ export async function ensurePortForwarding(port: number): Promise<void> {
160
224
  }
161
225
 
162
226
  /**
163
- * Check if Quest browser is running
227
+ * Check if browser is running
164
228
  */
165
- export async function isBrowserRunning(): Promise<boolean> {
229
+ export async function isBrowserRunning(browser: string = 'com.oculus.browser'): Promise<boolean> {
166
230
  try {
167
- const result = await execCommandFull('adb', ['shell', 'ps | grep com.oculus.browser']);
168
- return result.stdout.includes('com.oculus.browser');
231
+ const result = await execCommandFull('adb', ['shell', `ps | grep ${browser}`]);
232
+ return result.stdout.includes(browser);
169
233
  } catch (error) {
170
234
  return false;
171
235
  }
172
236
  }
173
237
 
174
238
  /**
175
- * Launch Quest browser with a URL using am start
239
+ * Launch browser with a URL using am start
176
240
  */
177
- export async function launchBrowser(url: string): Promise<boolean> {
178
- console.log('Launching Quest browser...');
241
+ export async function launchBrowser(url: string, browser: string = 'com.oculus.browser'): Promise<boolean> {
242
+ console.log('Launching browser...');
179
243
  try {
180
244
  await execCommand('adb', [
181
245
  'shell',
@@ -185,12 +249,12 @@ export async function launchBrowser(url: string): Promise<boolean> {
185
249
  'android.intent.action.VIEW',
186
250
  '-d',
187
251
  url,
188
- 'com.oculus.browser'
252
+ browser
189
253
  ]);
190
- console.log(`Quest browser launched with URL: ${url}`);
254
+ console.log(`Browser launched with URL: ${url}`);
191
255
  return true;
192
256
  } catch (error) {
193
- console.error('Failed to launch Quest browser:', (error as Error).message);
257
+ console.error('Failed to launch browser:', (error as Error).message);
194
258
  return false;
195
259
  }
196
260
  }
@@ -198,38 +262,45 @@ export async function launchBrowser(url: string): Promise<boolean> {
198
262
  /**
199
263
  * Get CDP port
200
264
  */
201
- export function getCDPPort(): number {
202
- 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);
203
268
  }
204
269
 
205
270
  /**
206
271
  * Set up only CDP forwarding (for external URLs that don't need reverse forwarding)
207
272
  */
208
- export async function ensureCDPForwarding(): Promise<void> {
273
+ export async function ensureCDPForwarding(
274
+ browser: string = 'com.oculus.browser'
275
+ ): Promise<void> {
209
276
  try {
277
+ // Detect CDP socket and port for this browser
278
+ const cdpSocket = await detectCDPSocket(browser);
279
+ const cdpPort = getCDPPortForSocket(cdpSocket);
280
+
210
281
  // Check forward forwarding (Host -> Quest for CDP)
211
282
  const forwardList = await execCommand('adb', ['forward', '--list']);
212
- const forwardExists = forwardList.includes(`tcp:${CDP_PORT}`) && forwardList.includes('chrome_devtools_remote');
283
+ const forwardExists = forwardList.includes(`tcp:${cdpPort}`) && forwardList.includes(cdpSocket);
213
284
 
214
285
  if (forwardExists) {
215
- console.log(`CDP port ${CDP_PORT} forwarding already set up`);
286
+ console.log(`CDP port ${cdpPort} forwarding already set up`);
216
287
  } else {
217
288
  // Check if something else is using the port
218
- const cdpPortListening = await isPortListening(CDP_PORT);
289
+ const cdpPortListening = await isPortListening(cdpPort);
219
290
  if (cdpPortListening) {
220
- 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`);
221
292
  console.error('');
222
- console.error('CDP port forwarding requires port 9223 to be free.');
293
+ console.error(`CDP port forwarding requires port ${cdpPort} to be free.`);
223
294
  console.error('Please stop the process using this port and try again.');
224
295
  console.error('');
225
296
  console.error('To find what is using the port:');
226
- console.error(` lsof -i :${CDP_PORT}`);
297
+ console.error(` lsof -i :${cdpPort}`);
227
298
  console.error('');
228
299
  process.exit(1);
229
300
  }
230
301
 
231
- await execCommand('adb', ['forward', `tcp:${CDP_PORT}`, 'localabstract:chrome_devtools_remote']);
232
- 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)`);
233
304
  }
234
305
  } catch (error) {
235
306
  console.error('Failed to set up CDP forwarding:', (error as Error).message);
@@ -273,3 +344,50 @@ export async function checkQuestAwake(): Promise<void> {
273
344
  process.exit(1);
274
345
  }
275
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
+ }
package/src/utils/exec.ts CHANGED
@@ -17,7 +17,6 @@ export function execCommand(command: string, args: string[] = []): Promise<strin
17
17
  return new Promise((resolve, reject) => {
18
18
  const proc = spawn(command, args, {
19
19
  stdio: 'pipe',
20
- shell: true
21
20
  });
22
21
 
23
22
  let stdout = '';
@@ -54,7 +53,6 @@ export function execCommandFull(command: string, args: string[] = []): Promise<E
54
53
  return new Promise((resolve) => {
55
54
  const proc = spawn(command, args, {
56
55
  stdio: 'pipe',
57
- shell: true
58
56
  });
59
57
 
60
58
  let stdout = '';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Tests for filename generation utilities
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { generateScreenshotFilename } from './filename.js';
7
+
8
+ describe('generateScreenshotFilename', () => {
9
+ it('formats UTC timestamp correctly', () => {
10
+ const date = new Date('2026-01-15T14:30:45Z');
11
+ expect(generateScreenshotFilename(date))
12
+ .toBe('screenshot-2026-01-15-14-30-45-Z.jpg');
13
+ });
14
+
15
+ it('pads single digits with zeros', () => {
16
+ const date = new Date('2026-01-05T08:09:03Z');
17
+ expect(generateScreenshotFilename(date))
18
+ .toBe('screenshot-2026-01-05-08-09-03-Z.jpg');
19
+ });
20
+
21
+ it('handles midnight correctly', () => {
22
+ const date = new Date('2026-12-31T00:00:00Z');
23
+ expect(generateScreenshotFilename(date))
24
+ .toBe('screenshot-2026-12-31-00-00-00-Z.jpg');
25
+ });
26
+
27
+ it('generates filename with current time when no date provided', () => {
28
+ const filename = generateScreenshotFilename();
29
+
30
+ // Verify format is correct
31
+ expect(filename).toMatch(/^screenshot-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}-Z\.jpg$/);
32
+
33
+ // Extract timestamp from filename
34
+ const match = filename.match(/^screenshot-(\d{4})-(\d{2})-(\d{2})-(\d{2})-(\d{2})-(\d{2})-Z\.jpg$/);
35
+ expect(match).not.toBeNull();
36
+
37
+ if (match) {
38
+ const [, year, month, day, hours, minutes, seconds] = match;
39
+ const filenameDate = new Date(Date.UTC(
40
+ parseInt(year),
41
+ parseInt(month) - 1,
42
+ parseInt(day),
43
+ parseInt(hours),
44
+ parseInt(minutes),
45
+ parseInt(seconds)
46
+ ));
47
+
48
+ // Verify the timestamp is reasonable (within last minute)
49
+ const now = new Date();
50
+ const diff = now.getTime() - filenameDate.getTime();
51
+ expect(diff).toBeGreaterThanOrEqual(0);
52
+ expect(diff).toBeLessThan(60000); // Within last 60 seconds
53
+ }
54
+ });
55
+ });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Filename generation utilities for screenshots
3
+ */
4
+
5
+ /**
6
+ * Generate UTC timestamp filename
7
+ * Format: screenshot-YYYY-MM-DD-HH-MM-SS-Z.jpg
8
+ */
9
+ export function generateScreenshotFilename(date: Date = new Date()): string {
10
+ const year = date.getUTCFullYear();
11
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
12
+ const day = String(date.getUTCDate()).padStart(2, '0');
13
+ const hours = String(date.getUTCHours()).padStart(2, '0');
14
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0');
15
+ const seconds = String(date.getUTCSeconds()).padStart(2, '0');
16
+
17
+ return `screenshot-${year}-${month}-${day}-${hours}-${minutes}-${seconds}-Z.jpg`;
18
+ }
package/tests/adb.test.ts CHANGED
@@ -28,8 +28,8 @@ describe('isPortListening', () => {
28
28
  });
29
29
 
30
30
  describe('getCDPPort', () => {
31
- it('should return the default CDP port', () => {
32
- const port = getCDPPort();
31
+ it('should return the default CDP port', async () => {
32
+ const port = await getCDPPort();
33
33
  expect(port).toBe(9223);
34
34
  });
35
35
  });
@@ -8,7 +8,7 @@ describe('execCommand', () => {
8
8
  });
9
9
 
10
10
  it('should reject on non-zero exit code', async () => {
11
- await expect(execCommand('sh -c "exit 1"', [])).rejects.toThrow();
11
+ await expect(execCommand('sh', ['-c', 'exit 1'])).rejects.toThrow();
12
12
  });
13
13
 
14
14
  it('should handle shell commands', async () => {
@@ -25,12 +25,12 @@ describe('execCommandFull', () => {
25
25
  });
26
26
 
27
27
  it('should return non-zero exit code without throwing', async () => {
28
- const result = await execCommandFull('sh -c "exit 42"', []);
28
+ const result = await execCommandFull('sh', ['-c', 'exit 42']);
29
29
  expect(result.code).toBe(42);
30
30
  });
31
31
 
32
32
  it('should capture stderr', async () => {
33
- const result = await execCommandFull('sh -c "echo error >&2"', []);
33
+ const result = await execCommandFull('sh', ['-c', 'echo error >&2']);
34
34
  expect(result.stderr.trim()).toBe('error');
35
35
  });
36
36
  });