From 7331ee626e38765dfcb86b64933f2d7952fc6c90 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Tue, 28 Jul 2026 23:34:06 +0300 Subject: [PATCH] feat: add one-command mobile simulator dev loop Adds `mobile:sim:dev` / `sim:dev` to build, install, launch, and stream the iOS simulator in one command Updates mobile docs and serve-sim guidance with the new workflow and correct preview URL behavior Bumps `serve-sim` to a newer version --- .agents/skills/serve-sim/SKILL.md | 5 +- bun.lock | 12 ++--- package.json | 1 + packages/mobile/README.md | 1 + packages/mobile/package.json | 3 +- packages/mobile/scripts/ios-sim-dev.mjs | 62 +++++++++++++++++++++++++ 6 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 packages/mobile/scripts/ios-sim-dev.mjs diff --git a/.agents/skills/serve-sim/SKILL.md b/.agents/skills/serve-sim/SKILL.md index cd18d986..356cc143 100644 --- a/.agents/skills/serve-sim/SKILL.md +++ b/.agents/skills/serve-sim/SKILL.md @@ -19,6 +19,7 @@ Use `serve-sim` to stream and control a booted Apple Simulator from the terminal - `bun run mobile:sim:serve` - `bun run mobile:sim:list` - `bun run mobile:sim:kill` + - `bun run mobile:sim:dev` — foreground build + run + stream in one command (`--no-build` to skip the build); intended for the user, agents should prefer the discrete scripts above ## Workflow @@ -36,7 +37,7 @@ Use `serve-sim` to stream and control a booted Apple Simulator from the terminal ```sh bun run mobile:sim:serve ``` - Surface the returned `url` to the user. It normally starts at `http://localhost:3200`. + Surface the returned `url` to the user. It normally starts at `http://127.0.0.1:3100`; always use the `url` from the JSON output rather than assuming the port. 4. Stop helpers when finished unless the user asks to keep them running: ```sh @@ -52,6 +53,8 @@ Use `serve-sim` to stream and control a booted Apple Simulator from the terminal - List streams: `bunx serve-sim --list -q` - Accessibility tree: `curl http://localhost:3100/ax` +Run direct CLI commands from `packages/mobile` (the binary lives in that package; plain `serve-sim` inside `with-mobile-env.mjs` from elsewhere fails with command not found). + Coordinates are normalized `0..1`, not pixels. Prefer `tap` for simple taps; do not emulate taps using separate `gesture` begin/end commands because that can register as long press. ## Preconditions diff --git a/bun.lock b/bun.lock index b9f71ea0..e6e40037 100644 --- a/bun.lock +++ b/bun.lock @@ -97,7 +97,7 @@ }, "packages/electron": { "name": "@openchamber/electron", - "version": "1.16.3", + "version": "1.17.0", "dependencies": { "@openchamber/web": "workspace:*", "better-sqlite3": "^12.10.0", @@ -128,13 +128,13 @@ "@capacitor/cli": "^8.4.1", "@capacitor/ios": "^8.4.1", "@types/node": "^24.3.1", - "serve-sim": "^0.1.34", + "serve-sim": "^0.1.45", "typescript": "~5.9.0", }, }, "packages/ui": { "name": "@openchamber/ui", - "version": "1.16.3", + "version": "1.17.0", "dependencies": { "@aparajita/capacitor-secure-storage": "^8.0.0", "@base-ui/react": "^1.4.0", @@ -239,7 +239,7 @@ }, "packages/vscode": { "name": "openchamber", - "version": "1.16.3", + "version": "1.17.0", "dependencies": { "@openchamber/ui": "workspace:*", "@opencode-ai/sdk": "1.18.8", @@ -262,7 +262,7 @@ }, "packages/web": { "name": "@openchamber/web", - "version": "1.16.3", + "version": "1.17.0", "bin": { "openchamber": "./bin/cli.js", }, @@ -2966,7 +2966,7 @@ "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], - "serve-sim": ["serve-sim@0.1.43", "", { "dependencies": { "inspect-webkit": "^0.0.5", "ws": "^8.21.0" }, "bin": { "serve-sim": "dist/serve-sim.js" } }, "sha512-kLcWWucVZxPD2+73EAhku6iThATYXUTYlt8M4+sw1ZHZYkfFNhqCuhy8g+Z5JHCNUTf93t2qwCHPOPqvbYKRrw=="], + "serve-sim": ["serve-sim@0.1.45", "", { "dependencies": { "inspect-webkit": "^0.0.5", "sonner": "^2.0.7", "ws": "^8.21.0" }, "bin": { "serve-sim": "dist/serve-sim.js" } }, "sha512-I9YBJRz7DETanzh6hRD1yaVcUPO+xe4egjGNArk22mO7SIWZVCunHVTwpAbriP7H0aZMIKfQyVpuqvYonuKxBw=="], "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], diff --git a/package.json b/package.json index 1d549c41..b36ae83b 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "mobile:sim:install": "bun run --cwd packages/mobile sim:install", "mobile:sim:launch": "bun run --cwd packages/mobile sim:launch", "mobile:sim:run": "bun run --cwd packages/mobile sim:run", + "mobile:sim:dev": "bun run --cwd packages/mobile sim:dev", "mobile:sim:serve": "bun run --cwd packages/mobile sim:serve", "mobile:sim:list": "bun run --cwd packages/mobile sim:list", "mobile:sim:kill": "bun run --cwd packages/mobile sim:kill", diff --git a/packages/mobile/README.md b/packages/mobile/README.md index d56d3a5b..eb5f98f4 100644 --- a/packages/mobile/README.md +++ b/packages/mobile/README.md @@ -25,6 +25,7 @@ Run these from `packages/mobile`, or use the root `mobile:*` aliases. - `bun run build:android:debug`: builds a debug Android APK without launching an emulator. - `bun run build:ios:simulator`: builds an iOS Simulator app without launching Xcode or Simulator. - `bun run sim:run`: boots a simulator if needed, installs the built iOS app, and launches it. +- `bun run sim:dev`: one-command dev loop — builds the simulator app, installs + launches it, starts the `serve-sim` stream, and prints the preview URL; Ctrl+C stops the stream. Pass `--no-build` to skip the build step. - `bun run sim:serve`: starts `serve-sim` in detached JSON mode and prints the browser preview URL. - `bun run sim:list`: lists running `serve-sim` streams. - `bun run sim:kill`: stops running `serve-sim` streams. diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 67890952..d67ee10e 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -20,6 +20,7 @@ "sim:install": "node scripts/with-mobile-env.mjs \"node scripts/ios-sim.mjs install\"", "sim:launch": "node scripts/with-mobile-env.mjs \"node scripts/ios-sim.mjs launch\"", "sim:run": "node scripts/with-mobile-env.mjs \"node scripts/ios-sim.mjs run\"", + "sim:dev": "node scripts/with-mobile-env.mjs \"node scripts/ios-sim-dev.mjs\"", "sim:serve": "node scripts/with-mobile-env.mjs \"serve-sim --detach -q\"", "sim:list": "node scripts/with-mobile-env.mjs \"serve-sim --list -q\"", "sim:kill": "node scripts/with-mobile-env.mjs \"serve-sim --kill\"", @@ -42,7 +43,7 @@ "@capacitor/cli": "^8.4.1", "@capacitor/ios": "^8.4.1", "@types/node": "^24.3.1", - "serve-sim": "^0.1.34", + "serve-sim": "^0.1.45", "typescript": "~5.9.0" } } diff --git a/packages/mobile/scripts/ios-sim-dev.mjs b/packages/mobile/scripts/ios-sim-dev.mjs new file mode 100644 index 00000000..38948ce4 --- /dev/null +++ b/packages/mobile/scripts/ios-sim-dev.mjs @@ -0,0 +1,62 @@ +// One-command simulator dev loop: build the simulator app, install + launch it, +// start the serve-sim browser stream, and print the preview URL. +// The process then stays in the foreground so Ctrl+C stops the stream helpers. +// +// Pass --no-build to skip the (slow) web + xcodebuild step and just relaunch the +// previously built app with a fresh stream. + +import { spawnSync } from 'node:child_process'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const mobileRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); +// serve-sim resolves from the package bin dir; plain `serve-sim` is not on PATH +// when this script is invoked outside `bun run`. +const env = { + ...process.env, + PATH: `${join(mobileRoot, 'node_modules', '.bin')}:${process.env.PATH || ''}`, +}; + +const run = (command, args, options = {}) => { + const result = spawnSync(command, args, { + cwd: mobileRoot, + env, + encoding: 'utf8', + stdio: options.capture ? ['ignore', 'pipe', 'inherit'] : 'inherit', + }); + if (result.status !== 0) { + console.error(`[ios-sim-dev] ${command} ${args.join(' ')} exited with ${result.status ?? result.signal}`); + process.exit(result.status ?? 1); + } + return result.stdout?.trim() ?? ''; +}; + +if (!process.argv.includes('--no-build')) { + run('node', ['scripts/ios-sim-build.mjs']); +} + +run('node', ['scripts/ios-sim.mjs', 'run']); + +const serveOutput = run('serve-sim', ['--detach', '-q'], { capture: true }); +let url; +try { + url = JSON.parse(serveOutput).url; +} catch { + // fall through to the guard below +} +if (!url) { + console.error(`[ios-sim-dev] Unexpected serve-sim output: ${serveOutput}`); + process.exit(1); +} + +console.log(`[ios-sim-dev] Stream ready at ${url}`); +console.log('[ios-sim-dev] Press Ctrl+C to stop the stream (the simulator stays running).'); + +const stop = () => { + spawnSync('serve-sim', ['--kill'], { cwd: mobileRoot, env, stdio: 'inherit' }); + process.exit(0); +}; +process.on('SIGINT', stop); +process.on('SIGTERM', stop); +// Keep the foreground process alive until a signal arrives. +setInterval(() => {}, 1 << 30);