diff --git a/README.md b/README.md index 284121b..ea83896 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Build a production-style voice agent with a Next.js web client and Python FastAP - [Bun](https://bun.sh/) - [Agora CLI](https://github.com/AgoraIO/cli) +On Windows, install Python on `PATH` as `python` or use the Python launcher (`py -3`). The root `bun run ...` scripts create and use `server/venv` without requiring `bash`, `python3`, or POSIX `source`. + ## Run It Install the CLI (skip if already installed), scaffold the Python quickstart, install dependencies, and run. diff --git a/docs/ai/L0_repo_card.md b/docs/ai/L0_repo_card.md index d4c6ba6..abf8aef 100644 --- a/docs/ai/L0_repo_card.md +++ b/docs/ai/L0_repo_card.md @@ -11,7 +11,7 @@ | Language | Python 3.10+ (FastAPI + uvicorn) backend + Next.js 16 / React 19 web | | Deploy Target | `web/` as Next.js app, `server/` as a reachable FastAPI service | | Owner | Agora Conversational AI DevEx | -| Last Reviewed | 2026-06-11 | +| Last Reviewed | 2026-08-12 | | Recipe Role | `base` | | Recipe Version | `1.0.0` | | Recipe Status | `experimental` | diff --git a/docs/ai/L1/01_setup.md b/docs/ai/L1/01_setup.md index abf5e1a..1518967 100644 --- a/docs/ai/L1/01_setup.md +++ b/docs/ai/L1/01_setup.md @@ -6,7 +6,7 @@ - **Python** ≥ 3.10 (README + `server/README.md`). - **bun** as the JS toolchain (root `package.json` scripts and root `bun.lock`). -- **pip** + `venv` for Python dependencies. No `pyproject.toml` is present. +- **pip** + `venv` for Python dependencies. No `pyproject.toml` is present. Root scripts use `python3`/`python` on Unix and `python`/`py -3` on Windows. - Agora project with App ID + App Certificate. ## Install @@ -14,7 +14,7 @@ ```bash bun install # JS deps for the workspace, including web/ cd server -python3 -m venv venv # canonical name; matches package.json scripts +python3 -m venv venv # Unix; use python -m venv venv on Windows source venv/bin/activate pip install -r requirements.txt ``` @@ -28,6 +28,8 @@ bun run setup `setup:env` copies `server/.env.example` → `server/.env.local` if missing. `setup:backend` recreates `server/venv`, upgrades pip, and installs `requirements.txt`. `setup:frontend` runs `bun install`. `setup:deps` exists for `bun run dev:check`, not for `bun run setup`. +Root `package.json` delegates filesystem, venv, Python, env-var injection, and cleanup steps to `scripts/run.mjs` so the same commands work in POSIX shells and Windows PowerShell/cmd. + > The package.json scripts use `server/venv/` (no leading dot). `bun run dev:backend` activates `server/venv` and runs `python src/server.py` from inside `server/`. If you create the venv under a different name you'll need to adjust the scripts or symlink. ## Environment Variables @@ -75,10 +77,10 @@ The SDK is lower-bounded at v2 — add an upper bound or exact pin if you need r ```bash bun run dev # setup:env → setup:deps → concurrently {backend, frontend} -bun run dev:backend # python3 server/src/server.py -bun run dev:frontend # cd web && AGENT_BACKEND_URL=http://localhost:8000 bun run dev +bun run dev:backend # creates/reuses server/venv and runs server/src/server.py +bun run dev:frontend # starts web with AGENT_BACKEND_URL=http://localhost:8000 bun run doctor # bun + node_modules sanity -bun run doctor:local # adds python3 + .env.local + AGORA_* presence +bun run doctor:local # adds Python + .env.local + AGORA_* presence bun run build # bun --filter web build bun run verify # doctor + verify:web:api + verify:web:build bun run verify:local # doctor:local + verify:backend + verify:local:fastapi + verify:web:proxy + verify:web:build @@ -96,7 +98,7 @@ bun run clean # remove backend venv, node_modules, .next, web/d | Command | Live Agora? | Notes | | ----------------------------- | ----------- | ---------------------------------------------------- | | `bun run doctor` | No | bun + node_modules sanity | -| `bun run doctor:local` | No | Adds python3 + env presence | +| `bun run doctor:local` | No | Adds Python + env presence | | `bun run verify:web:api` | No | Contract harness with mocked SDK | | `bun run verify:web:proxy` | No | Static fake-server smoke | | `bun run verify:local:fastapi`| No | Boots `server/scripts/run_fake_server.py` | @@ -106,7 +108,7 @@ bun run clean # remove backend venv, node_modules, .next, web/d ## Common Setup Failures -- `bun run doctor:local` fails on **"python3 not found"** → install Python ≥ 3.10. +- `bun run doctor:local` fails on **"Python 3.10+ was not found"** → install Python ≥ 3.10 or set `PYTHON` to its executable path. - Doctor fails on missing `server/.env.local` → run `bun run setup:env` or copy from `server/.env.example`. - `cd web && bun run doctor` rejects empty/invalid `AGENT_BACKEND_URL` → ensure the URL is `http://` or `https://`. - `verify:web:api` fails on a new route → extend `web/scripts/verify-api-contracts.ts` to cover it. diff --git a/docs/ai/L1/03_code_map.md b/docs/ai/L1/03_code_map.md index 201012d..4925906 100644 --- a/docs/ai/L1/03_code_map.md +++ b/docs/ai/L1/03_code_map.md @@ -12,6 +12,7 @@ ARCHITECTURE.md # Top-level environment model AGENTS.md # Contributor entry point CLAUDE.md # Pointer to AGENTS.md LICENSE +scripts/run.mjs # Cross-platform root command runner for setup/dev/doctor/clean web/ # Next.js 16 app (workspace member) app/ @@ -70,7 +71,7 @@ server/ # Python FastAPI backend | File | Purpose | | --------------------------------------------------- | ------------------------------------------------------------------------ | -| `package.json` (root) | `concurrently`-driven dev orchestration; every workflow script. | +| `package.json` (root) | `concurrently`-driven dev orchestration; delegates cross-platform workflow steps to `scripts/run.mjs`. | | `web/next.config.ts` | Rewrites `/api/*` to `${AGENT_BACKEND_URL}/...` when env is set. | | `web/src/services/api.ts` | Browser API client: `getConfig`, `startAgent`, `stopAgent`. | | `web/src/components/LandingPage.tsx` | Session bootstrap, RTM login, renewal handler, provider wiring. | diff --git a/docs/ai/L1/04_conventions.md b/docs/ai/L1/04_conventions.md index c598d0a..0c0184a 100644 --- a/docs/ai/L1/04_conventions.md +++ b/docs/ai/L1/04_conventions.md @@ -8,11 +8,11 @@ | -------------- | -------------------------------------------------------------------------------------- | | Python format | None enforced in-repo. Match existing style; `ruff`/`black` are not configured. | | Python verify | `py_compile` over `server/src/*.py` (`bun run verify:backend`). | -| Python deps | `pip install -r server/requirements.txt` inside `server/venv` (created by `bun run setup:backend`). | +| Python deps | `pip install -r server/requirements.txt` inside `server/venv` (created by `bun run setup:backend`; Windows uses `python` / `py -3`). | | TypeScript | `strict: true` in `web/tsconfig.json`; path alias `@/* → ./src/*`. | | Linter | Biome (`web/biome.json`); `noExplicitAny` off, `useExhaustiveDependencies` off. | | Format | Biome (`bun run lint:fix` writes). | -| JS orchestration | bun (root `package.json` `concurrently`, `bun --filter web …`). | +| JS orchestration | bun (root `package.json` `concurrently`, `scripts/run.mjs`, `bun --filter web …`). | There is **no ESLint config file** in `web/` — Biome is the only TS/JS linter. @@ -68,6 +68,7 @@ There is **no ESLint config file** in `web/` — Biome is the only TS/JS linter. - Components: PascalCase `.tsx` (e.g. `ConversationComponent.tsx`). - UI primitives: lowercase under `ui/` (e.g. `ui/button.tsx`). - Scripts: kebab-case (`verify-api-contracts.ts`, `verify-local-fastapi.ts`). +- Root orchestration scripts stay in `scripts/run.mjs` when they need filesystem, env-var, venv, or process-spawn behavior that must work on Windows and POSIX. - Python modules: snake_case (`server.py`, `agent.py`, `run_fake_server.py`). ## Module Discipline diff --git a/docs/ai/L1/05_workflows.md b/docs/ai/L1/05_workflows.md index ba535b0..a1e59ce 100644 --- a/docs/ai/L1/05_workflows.md +++ b/docs/ai/L1/05_workflows.md @@ -55,8 +55,8 @@ bun run verify:local # full chain including backend + fastapi + proxy + b ```bash bun run dev # concurrently { -# dev:backend → python3 server/src/server.py -# dev:frontend → cd web && AGENT_BACKEND_URL=http://localhost:8000 bun run dev +# dev:backend → scripts/run.mjs creates/reuses server/venv and runs server/src/server.py +# dev:frontend → scripts/run.mjs starts web with AGENT_BACKEND_URL=http://localhost:8000 # } ``` diff --git a/docs/ai/L1/07_gotchas.md b/docs/ai/L1/07_gotchas.md index 74f998a..dea262d 100644 --- a/docs/ai/L1/07_gotchas.md +++ b/docs/ai/L1/07_gotchas.md @@ -12,6 +12,10 @@ `bun run dev` always exports `AGENT_BACKEND_URL=http://localhost:8000`. Deploy hosts must set it manually. +## Root Scripts Are Cross-Platform + +Root `package.json` scripts must not rely on POSIX-only commands such as `bash`, `test`, `source`, `rm -rf`, or `python3`. Put filesystem, venv, environment-variable, and process-spawn logic in `scripts/run.mjs` so `bun run dev`, setup, doctor, verify, and clean keep working on Windows PowerShell/cmd as well as POSIX shells. + ## No `web/app/api/**/route.ts` `web/scripts/verify-api-contracts.ts` asserts that no `app/api` route handlers exist. The web client must be rewrite-only. Adding a Next route handler would: diff --git a/docs/ai/L1/L2/verification_scripts.md b/docs/ai/L1/L2/verification_scripts.md index 4c73105..d035818 100644 --- a/docs/ai/L1/L2/verification_scripts.md +++ b/docs/ai/L1/L2/verification_scripts.md @@ -51,7 +51,7 @@ Purpose: exercise the **real FastAPI app** locally, with a `FakeAgent` substitut What it does: -1. Spawns `python3 server/scripts/run_fake_server.py`, which: +1. Spawns `server/scripts/run_fake_server.py` with the `server/venv` Python (`bin/python` on Unix, `Scripts/python.exe` on Windows), which: - Imports `server.src.server` so the `app` and `agent` module-level singleton exist. - Replaces `server_module.agent` with a `FakeAgent` instance. - Runs `uvicorn.run(app, ...)` on a known port. @@ -62,7 +62,7 @@ This is the closest CI gets to a full integration test. It never makes outbound ## `py_compile` Verification -`bun run verify:backend` runs `python3 -m py_compile server/src/server.py server/src/agent.py`. It catches: +`bun run verify:backend` runs `scripts/run.mjs verify:backend`, which uses the venv Python when present or a system Python fallback to compile `server/src/server.py` and `server/src/agent.py`. It catches: - Syntax errors. @@ -90,7 +90,7 @@ It does **not** execute module imports, load env, or catch logic regressions. Pa | `verify-api-contracts` fails on "app/api should not exist" | Someone added a Next route handler — remove it. | | `verify-local-proxy` hangs on `fetch` | Fake server failed to start; check the script's stderr. | | `verify-local-fastapi` errors on import | `Agent.__init__` failed (env missing or SDK import error). | -| `verify-backend` reports a syntax error | Run `python3 -m py_compile` directly on the offending file. | +| `verify-backend` reports a syntax error | Run `bun run verify:backend` or `python -m py_compile` directly on the offending file. | ## See Also diff --git a/package.json b/package.json index 5f5a086..f49a105 100644 --- a/package.json +++ b/package.json @@ -8,30 +8,30 @@ "scripts": { "dev": "bun run dev:check && concurrently -n backend,frontend -c blue,green \"bun run dev:backend\" \"bun run dev:frontend\"", "dev:check": "bun run setup:env && bun run setup:deps", - "dev:backend": "cd server && bash -c '(venv/bin/python -m pip --version >/dev/null 2>&1 || (rm -rf venv && python3 -m venv venv)) && source venv/bin/activate && python -m pip install -q -r requirements.txt && python src/server.py'", - "dev:frontend": "cd web && AGENT_BACKEND_URL=http://localhost:8000 bun run dev", - "backend": "cd server && source venv/bin/activate && python src/server.py", - "frontend": "cd web && AGENT_BACKEND_URL=http://localhost:8000 bun run dev", + "dev:backend": "bun scripts/run.mjs dev:backend", + "dev:frontend": "bun scripts/run.mjs dev:frontend", + "backend": "bun scripts/run.mjs dev:backend", + "frontend": "bun scripts/run.mjs dev:frontend", "setup": "bun run setup:env && bun run setup:backend && bun run setup:frontend && bun run setup:done", - "setup:env": "test -f server/.env.local || (cp server/.env.example server/.env.local && echo '\nCreated server/.env.local. Add Agora credentials before running the app.')", - "setup:deps": "test -d node_modules || (echo 'Installing workspace dependencies...' && bun install)", - "setup:backend": "cd server && rm -rf venv && python3 -m venv venv && source venv/bin/activate && python -m pip install --upgrade pip && PIP_INDEX_URL=https://pypi.org/simple python -m pip install -r requirements.txt", + "setup:env": "bun scripts/run.mjs setup:env", + "setup:deps": "bun scripts/run.mjs setup:deps", + "setup:backend": "bun scripts/run.mjs setup:backend", "setup:frontend": "bun install", - "setup:done": "echo '\n✅ Setup complete! Next steps:' && echo ' 1. Run: agora project env write server/.env.local' && echo ' 2. Run: bun run dev\n'", - "doctor": "bash -c 'set -e; echo \"Checking shared repo prerequisites...\"; command -v bun >/dev/null && echo \"- bun available\" || { echo \"- bun not found\"; exit 1; }; test -d node_modules && echo \"- workspace dependencies installed\" || { echo \"- root node_modules missing; run bun install\"; exit 1; }'", - "doctor:local": "bash -c 'set -e; bun run doctor; command -v python3 >/dev/null && echo \"- python3 available\" || { echo \"- python3 not found\"; exit 1; }; test -f server/.env.local && echo \"- server/.env.local present\" || { echo \"- missing server/.env.local\"; exit 1; }; grep -Eq \"^AGORA_APP_ID=.+$\" server/.env.local && echo \"- AGORA_APP_ID configured\" || { echo \"- AGORA_APP_ID missing in server/.env.local\"; exit 1; }; grep -Eq \"^AGORA_APP_CERTIFICATE=.+$\" server/.env.local && echo \"- AGORA_APP_CERTIFICATE configured\" || { echo \"- AGORA_APP_CERTIFICATE missing in server/.env.local\"; exit 1; }'", + "setup:done": "bun scripts/run.mjs setup:done", + "doctor": "bun scripts/run.mjs doctor", + "doctor:local": "bun scripts/run.mjs doctor:local", "build": "cd web && bun run build", "verify": "bun run verify:web", "verify:local": "bun run doctor:local && bun run verify:backend && bun run verify:local:fastapi && bun run verify:web:proxy && bun run verify:web:build", "verify:local:fastapi": "cd web && bun run scripts/verify-local-fastapi.ts", - "verify:backend": "cd server && python3 -m py_compile src/server.py src/agent.py", + "verify:backend": "bun scripts/run.mjs verify:backend", "verify:web": "bun run doctor && bun run verify:web:api && bun run verify:web:build", "verify:web:api": "cd web && bun run scripts/verify-api-contracts.ts", "verify:web:proxy": "cd web && bun run scripts/verify-local-proxy.ts", "verify:web:build": "cd web && bun run build", "clean": "bun run clean:backend && bun run clean:frontend", - "clean:backend": "rm -rf server/venv server/__pycache__ server/src/__pycache__", - "clean:frontend": "rm -rf node_modules web/node_modules web/.next web/dist" + "clean:backend": "bun scripts/run.mjs clean:backend", + "clean:frontend": "bun scripts/run.mjs clean:frontend" }, "devDependencies": { "concurrently": "^8.2.2" diff --git a/scripts/run.mjs b/scripts/run.mjs new file mode 100644 index 0000000..4abcd2d --- /dev/null +++ b/scripts/run.mjs @@ -0,0 +1,224 @@ +import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; +import { spawn, spawnSync } from "node:child_process"; +import path from "node:path"; + +const isWindows = process.platform === "win32"; +const rootDir = process.cwd(); +const serverDir = path.join(rootDir, "server"); +const webDir = path.join(rootDir, "web"); +const venvDir = path.join(serverDir, "venv"); +const venvPython = isWindows + ? path.join(venvDir, "Scripts", "python.exe") + : path.join(venvDir, "bin", "python"); + +function fail(message) { + console.error(message); + process.exit(1); +} + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: rootDir, + stdio: "inherit", + shell: isWindows && command === "bun", + ...options, + }); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function start(command, args, options = {}) { + const child = spawn(command, args, { + cwd: rootDir, + stdio: "inherit", + shell: isWindows && command === "bun", + ...options, + }); + child.on("exit", (code) => process.exit(code ?? 0)); +} + +function findSystemPython() { + const candidates = []; + if (process.env.PYTHON) { + candidates.push({ command: process.env.PYTHON, args: [] }); + } + if (isWindows) { + candidates.push({ command: "python", args: [] }, { command: "py", args: ["-3"] }); + } else { + candidates.push({ command: "python3", args: [] }, { command: "python", args: [] }); + } + + for (const candidate of candidates) { + const result = spawnSync(candidate.command, [...candidate.args, "--version"], { + stdio: "ignore", + shell: false, + }); + if (result.status === 0) { + return candidate; + } + } + + fail("Python 3.10+ was not found. Install Python or set PYTHON to its executable path."); +} + +function runSystemPython(args, options = {}) { + const python = findSystemPython(); + run(python.command, [...python.args, ...args], { shell: false, ...options }); +} + +function setupEnv() { + const target = path.join(serverDir, ".env.local"); + if (existsSync(target)) { + return; + } + + copyFileSync(path.join(serverDir, ".env.example"), target); + console.log("Created server/.env.local. Add Agora credentials before running the app."); +} + +function setupDeps() { + if (existsSync(path.join(rootDir, "node_modules"))) { + return; + } + + console.log("Installing workspace dependencies..."); + run("bun", ["install"]); +} + +function setupBackend({ reset = false, quiet = false } = {}) { + if (reset && existsSync(venvDir)) { + rmSync(venvDir, { recursive: true, force: true }); + } + if (!existsSync(venvPython)) { + mkdirSync(serverDir, { recursive: true }); + runSystemPython(["-m", "venv", venvDir]); + } + + run(venvPython, ["-m", "pip", "install", "--upgrade", "pip"], { shell: false }); + const pipArgs = ["-m", "pip", "install"]; + if (quiet) { + pipArgs.push("-q"); + } + pipArgs.push("-r", path.join(serverDir, "requirements.txt")); + run(venvPython, pipArgs, { + shell: false, + env: { ...process.env, PIP_INDEX_URL: process.env.PIP_INDEX_URL || "https://pypi.org/simple" }, + }); +} + +function setupDone() { + console.log(""); + console.log("Setup complete! Next steps:"); + console.log(" 1. Run: agora project env write server/.env.local"); + console.log(" 2. Run: bun run dev"); + console.log(""); +} + +function devBackend() { + setupBackend({ quiet: true }); + start(venvPython, [path.join(serverDir, "src", "server.py")], { shell: false }); +} + +function devFrontend() { + start("bun", ["run", "dev"], { + cwd: webDir, + env: { ...process.env, AGENT_BACKEND_URL: "http://localhost:8000" }, + }); +} + +function envFileHasValue(filePath, key) { + if (!existsSync(filePath)) { + return false; + } + const pattern = new RegExp(`^${key}=.+$`, "m"); + return pattern.test(readFileSync(filePath, "utf8")); +} + +function doctor() { + console.log("Checking shared repo prerequisites..."); + run("bun", ["--version"]); + if (!existsSync(path.join(rootDir, "node_modules"))) { + fail("- root node_modules missing; run bun install"); + } + console.log("- bun available"); + console.log("- workspace dependencies installed"); +} + +function doctorLocal() { + doctor(); + findSystemPython(); + console.log("- python available"); + + const envPath = path.join(serverDir, ".env.local"); + if (!existsSync(envPath)) { + fail("- missing server/.env.local"); + } + console.log("- server/.env.local present"); + + for (const key of ["AGORA_APP_ID", "AGORA_APP_CERTIFICATE"]) { + if (!envFileHasValue(envPath, key)) { + fail(`- ${key} missing in server/.env.local`); + } + console.log(`- ${key} configured`); + } +} + +function verifyBackend() { + const python = existsSync(venvPython) + ? { command: venvPython, args: [] } + : findSystemPython(); + run(python.command, [ + ...python.args, + "-m", + "py_compile", + path.join(serverDir, "src", "server.py"), + path.join(serverDir, "src", "agent.py"), + ], { shell: false }); +} + +function clean(paths) { + for (const target of paths) { + rmSync(path.join(rootDir, target), { recursive: true, force: true }); + } +} + +const command = process.argv[2]; + +switch (command) { + case "setup:env": + setupEnv(); + break; + case "setup:deps": + setupDeps(); + break; + case "setup:backend": + setupBackend({ reset: true }); + break; + case "setup:done": + setupDone(); + break; + case "dev:backend": + devBackend(); + break; + case "dev:frontend": + devFrontend(); + break; + case "doctor": + doctor(); + break; + case "doctor:local": + doctorLocal(); + break; + case "verify:backend": + verifyBackend(); + break; + case "clean:backend": + clean(["server/venv", "server/__pycache__", "server/src/__pycache__"]); + break; + case "clean:frontend": + clean(["node_modules", "web/node_modules", "web/.next", "web/dist"]); + break; + default: + fail(`Unknown script command: ${command || "(missing)"}`); +} diff --git a/web/scripts/verify-local-fastapi.ts b/web/scripts/verify-local-fastapi.ts index 7de7c35..46f2f85 100644 --- a/web/scripts/verify-local-fastapi.ts +++ b/web/scripts/verify-local-fastapi.ts @@ -95,13 +95,19 @@ async function waitForHealthyBackend(baseUrl: string, timeoutMs: number) { throw new Error(`Timed out waiting for FastAPI backend: ${lastError}`) } +function resolveVenvPython(serverRoot: string) { + return process.platform === 'win32' + ? path.join(serverRoot, 'venv', 'Scripts', 'python.exe') + : path.join(serverRoot, 'venv', 'bin', 'python') +} + async function main() { const projectRoot = process.cwd() const serverRoot = path.resolve(projectRoot, '..', 'server') - const venvPython = path.join(serverRoot, 'venv', 'bin', 'python') + const venvPython = resolveVenvPython(serverRoot) if (!existsSync(venvPython)) { - throw new Error('Missing server/venv/bin/python. Run bun run setup:backend before verify:local.') + throw new Error('Missing server/venv Python. Run bun run setup:backend before verify:local.') } const dependencyCheck = bunRuntime.Bun.spawnSync({