@myerscarpenter/quest-dev 1.0.0 → 1.0.2
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.
- package/.github/workflows/publish.yml +6 -1
- package/README.md +78 -0
- package/package.json +14 -14
- package/tests/adb.test.ts +35 -0
- package/tests/exec.test.ts +36 -0
|
@@ -9,7 +9,7 @@ jobs:
|
|
|
9
9
|
test-and-publish:
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
11
|
permissions:
|
|
12
|
-
contents:
|
|
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,78 @@
|
|
|
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
|
|
19
|
+
|
|
20
|
+
- **cdp-cli** (optional) - For smart tab reuse in the `open` command
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g @myerscarpenter/cdp-cli
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Screenshot
|
|
28
|
+
|
|
29
|
+
Take a screenshot from Quest and save it locally:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
quest-dev screenshot ./screenshot.jpg
|
|
33
|
+
quest-dev screenshot ~/Pictures/quest-capture.jpg
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This uses Quest's native screenshot service which captures the full VR view, including immersive content that CDP screenshots can't capture.
|
|
37
|
+
|
|
38
|
+
### Open URL
|
|
39
|
+
|
|
40
|
+
Open a URL in Quest Browser with automatic ADB port forwarding:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
quest-dev open http://localhost:3000/
|
|
44
|
+
quest-dev open http://localhost:9004/my-xr-app/
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This command:
|
|
48
|
+
1. Sets up ADB reverse port forwarding (Quest → Host) so the Quest can reach your dev server
|
|
49
|
+
2. Sets up ADB forward port forwarding (Host → Quest) for CDP communication
|
|
50
|
+
3. If Quest Browser is already running with the URL, reloads the tab
|
|
51
|
+
4. If a blank tab exists, navigates it to the URL
|
|
52
|
+
5. Otherwise, launches Quest Browser with the URL
|
|
53
|
+
|
|
54
|
+
Port forwarding is idempotent - safe to run multiple times without issues.
|
|
55
|
+
|
|
56
|
+
## How It Works
|
|
57
|
+
|
|
58
|
+
- **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/`
|
|
59
|
+
|
|
60
|
+
- **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.
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Install dependencies
|
|
66
|
+
pnpm install
|
|
67
|
+
|
|
68
|
+
# Build
|
|
69
|
+
pnpm run build
|
|
70
|
+
|
|
71
|
+
# Run locally
|
|
72
|
+
node build/index.js screenshot ./test.jpg
|
|
73
|
+
node build/index.js open http://localhost:3000/
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myerscarpenter/quest-dev",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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
|
+
}
|
|
@@ -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
|
+
});
|