@myerscarpenter/quest-dev 1.0.0 → 1.0.3

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.
@@ -9,7 +9,7 @@ jobs:
9
9
  test-and-publish:
10
10
  runs-on: ubuntu-latest
11
11
  permissions:
12
- contents: read
12
+ contents: write # Required for creating releases
13
13
  id-token: write # Required for OIDC trusted publishing
14
14
  steps:
15
15
  - uses: actions/checkout@v4
@@ -39,3 +39,8 @@ jobs:
39
39
 
40
40
  - name: Publish to npm
41
41
  run: npm publish --provenance --access public
42
+
43
+ - name: Create GitHub Release
44
+ env:
45
+ GH_TOKEN: ${{ github.token }}
46
+ run: gh release create ${{ github.ref_name }} --generate-notes
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # quest-dev
2
+
3
+ CLI tools for Meta Quest Browser development. Take screenshots and open URLs on your Quest device via ADB.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @myerscarpenter/quest-dev
9
+ ```
10
+
11
+ ## Prerequisites
12
+
13
+ - **ADB** - Android Debug Bridge must be installed and in your PATH
14
+ - macOS: `brew install android-platform-tools`
15
+ - Linux: `sudo apt install adb`
16
+ - Windows: [Download Platform Tools](https://developer.android.com/tools/releases/platform-tools)
17
+
18
+ - **Quest Device** - Connected via USB with [Developer Mode and USB Debugging enabled](https://developers.meta.com/horizon/documentation/native/android/mobile-device-setup/)
19
+
20
+ - **cdp-cli** (optional) - For smart tab reuse in the `open` command
21
+
22
+ ```bash
23
+ npm install -g @myerscarpenter/cdp-cli
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### Screenshot
29
+
30
+ Take a screenshot from Quest and save it locally:
31
+
32
+ ```bash
33
+ quest-dev screenshot ./screenshot.jpg
34
+ quest-dev screenshot ~/Pictures/quest-capture.jpg
35
+ ```
36
+
37
+ This uses Quest's native screenshot service which captures the full VR view, including immersive content that CDP screenshots can't capture.
38
+
39
+ ### Open URL
40
+
41
+ Open a URL in Quest Browser with automatic ADB port forwarding:
42
+
43
+ ```bash
44
+ quest-dev open http://localhost:3000/
45
+ quest-dev open http://localhost:9004/my-xr-app/
46
+ ```
47
+
48
+ This command:
49
+
50
+ 1. Sets up ADB reverse port forwarding (Quest → Host) so the Quest can reach your dev server
51
+ 2. Sets up ADB forward port forwarding (Host → Quest) for CDP communication
52
+ 3. If Quest Browser is already running with the URL, reloads the tab
53
+ 4. If a blank tab exists, navigates it to the URL
54
+ 5. Otherwise, launches Quest Browser with the URL
55
+
56
+ Port forwarding is idempotent - safe to run multiple times without issues.
57
+
58
+ ## How It Works
59
+
60
+ - **screenshot**: Triggers `com.oculus.metacam/.capture.CaptureService` via ADB, waits for the screenshot to save, then pulls the most recent file from `/sdcard/Oculus/Screenshots/`
61
+
62
+ - **open**: Uses ADB for port forwarding and browser launching. If `cdp-cli` is installed, it uses CDP to intelligently reuse existing tabs instead of opening new ones.
63
+
64
+ ## Development
65
+
66
+ ```bash
67
+ # Install dependencies
68
+ pnpm install
69
+
70
+ # Build
71
+ pnpm run build
72
+
73
+ # Run locally
74
+ node build/index.js screenshot ./test.jpg
75
+ node build/index.js open http://localhost:3000/
76
+ ```
77
+
78
+ ## License
79
+
80
+ MIT
package/build/index.js CHANGED
@@ -33,7 +33,7 @@ const cli = yargs(hideBin(process.argv))
33
33
  })
34
34
  .help()
35
35
  .alias('help', 'h')
36
- .epilog('Requires ADB to be installed and Quest connected via USB with debugging enabled.');
36
+ .epilog('Requires ADB and Quest connected via USB. Sets up CDP on port 9223 for cdp-cli.');
37
37
  // Screenshot command
38
38
  cli.command('screenshot <output>', 'Take a screenshot from Quest and save to local file', (yargs) => {
39
39
  return yargs.positional('output', {
@@ -45,7 +45,7 @@ cli.command('screenshot <output>', 'Take a screenshot from Quest and save to loc
45
45
  await screenshotCommand(argv.output);
46
46
  });
47
47
  // Open command
48
- cli.command('open <url>', 'Open URL in Quest browser with ADB port forwarding', (yargs) => {
48
+ cli.command('open <url>', 'Open URL in Quest browser (sets up dev server + CDP debugging port forwarding)', (yargs) => {
49
49
  return yargs.positional('url', {
50
50
  describe: 'URL to open (e.g., http://localhost:9004/myapp/)',
51
51
  type: 'string',
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;GAGG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,iCAAiC;AACjC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAC5B,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,CAAC,EAAE,OAAO,CAAC,CAC1D,CAAC;AACF,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC;AAEpC,aAAa;AACb,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;KACrC,UAAU,CAAC,WAAW,CAAC;KACvB,OAAO,CAAC,OAAO,CAAC;KAChB,KAAK,CAAC,+BAA+B,CAAC;KACtC,aAAa,CAAC,CAAC,EAAE,4BAA4B,CAAC;KAC9C,MAAM,EAAE;KACR,IAAI,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;IACxB,IAAI,GAAG,EAAE,CAAC;QACR,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnC,CAAC;IACD,IAAI,GAAG,EAAE,CAAC;QACR,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;IAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;KACD,IAAI,EAAE;KACN,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC;KAClB,MAAM,CAAC,kFAAkF,CAAC,CAAC;AAE9F,qBAAqB;AACrB,GAAG,CAAC,OAAO,CACT,qBAAqB,EACrB,qDAAqD,EACrD,CAAC,KAAK,EAAE,EAAE;IACR,OAAO,KAAK,CAAC,UAAU,CAAC,QAAQ,EAAE;QAChC,QAAQ,EAAE,iDAAiD;QAC3D,IAAI,EAAE,QAAQ;QACd,YAAY,EAAE,IAAI;KACnB,CAAC,CAAC;AACL,CAAC,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,MAAM,iBAAiB,CAAC,IAAI,CAAC,MAAgB,CAAC,CAAC;AACjD,CAAC,CACF,CAAC;AAEF,eAAe;AACf,GAAG,CAAC,OAAO,CACT,YAAY,EACZ,oDAAoD,EACpD,CAAC,KAAK,EAAE,EAAE;IACR,OAAO,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE;QAC7B,QAAQ,EAAE,kDAAkD;QAC5D,IAAI,EAAE,QAAQ;QACd,YAAY,EAAE,IAAI;KACnB,CAAC,CAAC;AACL,CAAC,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,MAAM,WAAW,CAAC,IAAI,CAAC,GAAa,CAAC,CAAC;AACxC,CAAC,CACF,CAAC;AAEF,oBAAoB;AACpB,GAAG,CAAC,KAAK,EAAE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;GAGG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,iCAAiC;AACjC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAC5B,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,CAAC,EAAE,OAAO,CAAC,CAC1D,CAAC;AACF,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC;AAEpC,aAAa;AACb,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;KACrC,UAAU,CAAC,WAAW,CAAC;KACvB,OAAO,CAAC,OAAO,CAAC;KAChB,KAAK,CAAC,+BAA+B,CAAC;KACtC,aAAa,CAAC,CAAC,EAAE,4BAA4B,CAAC;KAC9C,MAAM,EAAE;KACR,IAAI,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;IACxB,IAAI,GAAG,EAAE,CAAC;QACR,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnC,CAAC;IACD,IAAI,GAAG,EAAE,CAAC;QACR,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAC;IAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;KACD,IAAI,EAAE;KACN,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC;KAClB,MAAM,CAAC,iFAAiF,CAAC,CAAC;AAE7F,qBAAqB;AACrB,GAAG,CAAC,OAAO,CACT,qBAAqB,EACrB,qDAAqD,EACrD,CAAC,KAAK,EAAE,EAAE;IACR,OAAO,KAAK,CAAC,UAAU,CAAC,QAAQ,EAAE;QAChC,QAAQ,EAAE,iDAAiD;QAC3D,IAAI,EAAE,QAAQ;QACd,YAAY,EAAE,IAAI;KACnB,CAAC,CAAC;AACL,CAAC,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,MAAM,iBAAiB,CAAC,IAAI,CAAC,MAAgB,CAAC,CAAC;AACjD,CAAC,CACF,CAAC;AAEF,eAAe;AACf,GAAG,CAAC,OAAO,CACT,YAAY,EACZ,gFAAgF,EAChF,CAAC,KAAK,EAAE,EAAE;IACR,OAAO,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE;QAC7B,QAAQ,EAAE,kDAAkD;QAC5D,IAAI,EAAE,QAAQ;QACd,YAAY,EAAE,IAAI;KACnB,CAAC,CAAC;AACL,CAAC,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,MAAM,WAAW,CAAC,IAAI,CAAC,GAAa,CAAC,CAAC;AACxC,CAAC,CACF,CAAC;AAEF,oBAAoB;AACpB,GAAG,CAAC,KAAK,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,12 +1,24 @@
1
1
  {
2
2
  "name": "@myerscarpenter/quest-dev",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "CLI for Meta Quest Browser development - screenshot and URL opening via ADB and cdp-cli",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "quest-dev": "./build/index.js"
8
8
  },
9
9
  "main": "./build/index.js",
10
+ "scripts": {
11
+ "clean": "rm -rf build",
12
+ "build": "pnpm run clean && tsc && chmod +x build/index.js",
13
+ "start": "pnpm run build && node build/index.js",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "test:ui": "vitest --ui",
17
+ "test:coverage": "vitest run --coverage",
18
+ "version:patch": "pnpm version patch",
19
+ "version:minor": "pnpm version minor",
20
+ "version:major": "pnpm version major"
21
+ },
10
22
  "keywords": [
11
23
  "meta",
12
24
  "quest",
@@ -47,17 +59,5 @@
47
59
  "@myerscarpenter/cdp-cli": {
48
60
  "optional": true
49
61
  }
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
62
  }
63
- }
63
+ }
package/src/index.ts CHANGED
@@ -39,7 +39,7 @@ const cli = yargs(hideBin(process.argv))
39
39
  })
40
40
  .help()
41
41
  .alias('help', 'h')
42
- .epilog('Requires ADB to be installed and Quest connected via USB with debugging enabled.');
42
+ .epilog('Requires ADB and Quest connected via USB. Sets up CDP on port 9223 for cdp-cli.');
43
43
 
44
44
  // Screenshot command
45
45
  cli.command(
@@ -60,7 +60,7 @@ cli.command(
60
60
  // Open command
61
61
  cli.command(
62
62
  'open <url>',
63
- 'Open URL in Quest browser with ADB port forwarding',
63
+ 'Open URL in Quest browser (sets up dev server + CDP debugging port forwarding)',
64
64
  (yargs) => {
65
65
  return yargs.positional('url', {
66
66
  describe: 'URL to open (e.g., http://localhost:9004/myapp/)',
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { isPortListening, getCDPPort } from '../src/utils/adb.js';
3
+ import net from 'net';
4
+
5
+ describe('isPortListening', () => {
6
+ it('should return false for a port that is not listening', async () => {
7
+ // Use a high port number unlikely to be in use
8
+ const result = await isPortListening(59999);
9
+ expect(result).toBe(false);
10
+ });
11
+
12
+ it('should return true for a port that is listening', async () => {
13
+ // Create a temporary server
14
+ const server = net.createServer();
15
+ await new Promise<void>((resolve) => {
16
+ server.listen(0, '127.0.0.1', () => resolve());
17
+ });
18
+
19
+ const address = server.address() as net.AddressInfo;
20
+ const port = address.port;
21
+
22
+ const result = await isPortListening(port);
23
+ expect(result).toBe(true);
24
+
25
+ // Clean up
26
+ server.close();
27
+ });
28
+ });
29
+
30
+ describe('getCDPPort', () => {
31
+ it('should return the default CDP port', () => {
32
+ const port = getCDPPort();
33
+ expect(port).toBe(9223);
34
+ });
35
+ });
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { execCommand, execCommandFull } from '../src/utils/exec.js';
3
+
4
+ describe('execCommand', () => {
5
+ it('should execute a simple command and return stdout', async () => {
6
+ const result = await execCommand('echo', ['hello']);
7
+ expect(result.trim()).toBe('hello');
8
+ });
9
+
10
+ it('should reject on non-zero exit code', async () => {
11
+ await expect(execCommand('sh -c "exit 1"', [])).rejects.toThrow();
12
+ });
13
+
14
+ it('should handle shell commands', async () => {
15
+ const result = await execCommand('echo', ['foo bar']);
16
+ expect(result.trim()).toBe('foo bar');
17
+ });
18
+ });
19
+
20
+ describe('execCommandFull', () => {
21
+ it('should return full result with exit code 0 on success', async () => {
22
+ const result = await execCommandFull('echo', ['test']);
23
+ expect(result.code).toBe(0);
24
+ expect(result.stdout.trim()).toBe('test');
25
+ });
26
+
27
+ it('should return non-zero exit code without throwing', async () => {
28
+ const result = await execCommandFull('sh -c "exit 42"', []);
29
+ expect(result.code).toBe(42);
30
+ });
31
+
32
+ it('should capture stderr', async () => {
33
+ const result = await execCommandFull('sh -c "echo error >&2"', []);
34
+ expect(result.stderr.trim()).toBe('error');
35
+ });
36
+ });