diff --git a/AGENTS.md b/AGENTS.md index 63231a67..8ef52016 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -175,7 +175,7 @@ This project uses [Portless](https://github.com/vercel-labs/portless) for local To bypass Portless: `PORTLESS=0 yarn start` -Android phone over USB (Chrome on device → dev server via `adb reverse`): `yarn start:android-usb` +Android phone over USB (default browser opens via `adb`; `ANDROID_USB_OPEN_BROWSER=0` to skip): `yarn start:android-usb` ## Common Commands diff --git a/README.md b/README.md index ae110201..81d22bf1 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ The dev server runs at http://5chan.localhost:1355 via [Portless](https://port13 For device testing on a USB-connected Android phone (without relying on `5chan.localhost` DNS from the device): -- `yarn start:android-usb` starts Vite bound to `127.0.0.1` and runs `adb reverse`, so the phone can load the dev site at `http://localhost:1355` in Chrome (or another browser). Requires [Android platform-tools](https://developer.android.com/tools/releases/platform-tools) (`adb` on your `PATH`), USB debugging enabled, and the device showing as `device` in `adb devices`. +- `yarn start:android-usb` starts Vite bound to `127.0.0.1` and runs `adb reverse`, so the phone can load the dev site at `http://localhost:1355`. When the server is up, it opens that URL in each connected device’s default browser via `adb`. Set `ANDROID_USB_OPEN_BROWSER=0` to skip auto-open. Requires [Android platform-tools](https://developer.android.com/tools/releases/platform-tools) (`adb` on your `PATH`), USB debugging enabled, and the device showing as `device` in `adb devices`. ### Scripts diff --git a/scripts/AGENTS.md b/scripts/AGENTS.md index e63ee26b..b929d452 100644 --- a/scripts/AGENTS.md +++ b/scripts/AGENTS.md @@ -4,7 +4,7 @@ These rules apply to `scripts/**`. Follow the repo-root `AGENTS.md` first, then - Keep scripts non-interactive and idempotent. Print the command, URL, branch, or path being acted on so failures are diagnosable. - Use repo-relative paths and environment variables instead of user-specific absolute paths. -- For dev-server helpers, default to `http://5chan.localhost:1355` and respect the existing `PORTLESS=0` fallback instead of hard-coding alternate ports. For USB Android preview, `scripts/start-android-usb.mjs` mirrors bitsocial-web: `adb reverse` plus Vite on `127.0.0.1` so the device opens `http://localhost:`. +- For dev-server helpers, default to `http://5chan.localhost:1355` and respect the existing `PORTLESS=0` fallback instead of hard-coding alternate ports. For USB Android preview, `scripts/start-android-usb.mjs` mirrors bitsocial-web: `adb reverse` plus Vite on `127.0.0.1`, then `am start` VIEW to open the default browser when the port is listening (disable with `ANDROID_USB_OPEN_BROWSER=0`). - Keep shell helpers thin. When logic becomes stateful or cross-platform, prefer a Node script. - Git and worktree helpers must validate input and default to safe operations. - If a helper deletes local branches automatically, document the exact eligibility checks and keep the behavior conservative. diff --git a/scripts/dev-server-utils.mjs b/scripts/dev-server-utils.mjs index 4d822826..b1edc5cb 100644 --- a/scripts/dev-server-utils.mjs +++ b/scripts/dev-server-utils.mjs @@ -33,6 +33,30 @@ export async function resolvePort(requestedPort) { return port; } +export async function waitForPort(host, port, timeoutMs = 30_000) { + const startedAt = Date.now(); + + while (Date.now() - startedAt < timeoutMs) { + const ready = await new Promise((resolve) => { + const socket = net.connect({ host, port }); + + socket.once('connect', () => { + socket.destroy(); + resolve(true); + }); + socket.once('error', () => resolve(false)); + }); + + if (ready) { + return; + } + + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + throw new Error(`Timed out waiting for dev server on http://${host}:${port}`); +} + export function startVite(host, port) { const child = spawn('corepack', ['yarn', 'exec', 'vite', '--host', host, '--port', String(port), '--strictPort'], { cwd: repoRoot, diff --git a/scripts/start-android-usb.mjs b/scripts/start-android-usb.mjs index 472244f4..c62c8b82 100644 --- a/scripts/start-android-usb.mjs +++ b/scripts/start-android-usb.mjs @@ -2,11 +2,12 @@ import { spawnSync } from 'node:child_process'; import process from 'node:process'; -import { isWindows, repoRoot, resolvePort, startVite } from './dev-server-utils.mjs'; +import { isWindows, repoRoot, resolvePort, startVite, waitForPort } from './dev-server-utils.mjs'; const adbBin = isWindows ? 'adb.exe' : 'adb'; const host = '127.0.0.1'; const requestedPort = Number(process.env.ANDROID_USB_PORT || 1355); +const openBrowser = process.env.ANDROID_USB_OPEN_BROWSER !== '0' && process.env.ANDROID_USB_OPEN_BROWSER !== 'false'; function fail(message) { console.error(message); @@ -68,6 +69,20 @@ function reversePorts(devices, port) { } } +/** Opens the preview URL in each device's default browser via VIEW intent. */ +function openPreviewOnDevices(devices, url) { + for (const serial of devices) { + const open = spawnSync(adbBin, ['-s', serial, 'shell', 'am', 'start', '-a', 'android.intent.action.VIEW', '-d', url], { + cwd: repoRoot, + encoding: 'utf8', + }); + + if (open.status !== 0) { + console.warn(`Could not open URL on ${serial}: ${open.stderr.trim() || open.stdout.trim() || 'unknown error'}`); + } + } +} + runPrestart(); const devices = getReadyDevices(); @@ -82,7 +97,24 @@ if (port !== requestedPort) { console.log(`Preferred port ${requestedPort} is busy, so this run will use ${port}.`); } console.log(`ADB reverse is active for: ${devices.join(', ')}`); -console.log(`Open http://localhost:${port} in Chrome on the Android device.`); +const deviceUrl = `http://localhost:${port}`; +if (openBrowser) { + console.log(`Opening ${deviceUrl} in the default browser on the device(s) when the dev server is ready…`); +} else { + console.log(`Open ${deviceUrl} in a browser on the Android device (ANDROID_USB_OPEN_BROWSER=0).`); +} console.log(''); startVite(host, port); + +if (openBrowser) { + waitForPort(host, port) + .then(() => { + openPreviewOnDevices(devices, deviceUrl); + console.log(`Opened ${deviceUrl} on device(s): ${devices.join(', ')}`); + console.log(''); + }) + .catch((err) => { + console.warn(String(err.message || err)); + }); +}