Compare commits

...

3 Commits

5 changed files with 1033 additions and 96 deletions

View File

@@ -0,0 +1,798 @@
# Electrobun Desktop: Production Mode via Child Process Architecture
## TL;DR
> **Quick Summary**: Redesign the Electrobun desktop app to support production mode by spawning the TanStack Start server as a child process. Currently only dev mode works (hardcoded `localhost:3000`). The desktop will detect dev/prod mode, spawn the server with `PORT=0` in prod, parse the actual port from stdout, and open the BrowserWindow.
>
> **Deliverables**:
> - Rewritten `apps/desktop/src/bun/index.ts` with dev/prod mode support
> - Updated `apps/desktop/electrobun.config.ts` with `build.copy` and platform configs
> - Cross-workspace build dependency in turbo pipeline
> - Updated `apps/desktop/AGENTS.md` reflecting new architecture
>
> **Estimated Effort**: Medium
> **Parallel Execution**: YES - 2 waves
> **Critical Path**: Tasks 1,2,3 (parallel) → Task 4 → Task 5
---
## Context
### Original Request
Redesign the Electrobun desktop app to support production mode. The desktop app should spawn the TanStack Start server as a child process, detect dev vs prod mode at runtime, use system-assigned ports for security, and handle server lifecycle (crash, quit).
### Confirmed Decisions
- **Architecture**: Desktop spawns server as child process via `Bun.spawn`
- **Server artifact**: `.output/server/index.mjs` (not compiled binary) — Electrobun already bundles Bun
- **Port strategy**: `PORT=0` (system-assigned), `HOST=127.0.0.1`
- **Dev/Prod detection**: `process.env.ELECTROBUN_BUILD_ENV` (see Defaults Applied below)
- **Target platforms**: All (Linux, macOS, Windows)
- **DATABASE_URL**: Pass-through via env var, no special handling
- **Crash handling**: MVP — log error to stderr, exit process
### Research Findings
**Electrobun APIs (verified from source code):**
- `build.copy` supports paths outside the project directory (e.g., `../server/.output`). Source paths are resolved relative to the project root. Destinations map into `Resources/app/` in the bundle.
- `PATHS` exported from `electrobun/bun` provides `RESOURCES_FOLDER` (absolute path to `Resources/`) and `VIEWS_FOLDER` (`Resources/app/views/`).
- `process.execPath` in a bundled Electrobun app points to the bundled Bun binary.
- `Electrobun.events.on('before-quit', callback)` fires before app quit. Callback receives an event with `response({ allow: false })` to cancel quit.
- `ELECTROBUN_BUILD_ENV` is set by the Electrobun CLI: `"dev"` for `electrobun dev`, `"stable"` for `electrobun build --env=stable`.
**Server startup behavior (verified from built output):**
- `.output/server/index.mjs` uses `Bun.serve` via the `h3+srvx` adapter (Nitro bun preset).
- Startup log format: `➜ Listening on: http://<address>:<port>/`
- The log uses the actual assigned address/port, not the requested one. So `PORT=0` will log the real port.
- `DATABASE_URL` is validated at startup via Zod (`@t3-oss/env-core`). Missing = immediate crash.
- The `.output/server/` directory contains `index.mjs` plus `_libs/` with bundled dependencies.
**Turbo pipeline:**
- Root `turbo.json` has `build.dependsOn: ["^build"]` which only builds workspace *dependencies*.
- Desktop does NOT depend on server in `package.json`, so `^build` won't trigger server build.
- Need explicit cross-workspace dependency via desktop's `turbo.json`.
### Metis Review
**Identified Gaps** (addressed):
- Dev/prod detection mechanism: Switched from custom `ELECTROBUN_DEV` to built-in `ELECTROBUN_BUILD_ENV`
- Server startup timeout: Added explicit timeout with error reporting
- Port parsing failure: Plan includes fallback and error handling
- Server crash during runtime: Watching `subprocess.exited` promise
- `cwd` for spawned server: Must set to server directory for relative import resolution
- Cross-platform considerations: `ELECTROBUN_BUILD_ENV` works everywhere (no `cross-env` needed)
### Unknowns Resolved
| Unknown | Resolution |
|---------|------------|
| Does `build.copy` support paths outside project? | **YES** — uses `cpSync` with source resolved from project root. `../server/.output` works. |
| Runtime API for resolving bundled resource paths? | **`PATHS.RESOURCES_FOLDER`** from `electrobun/bun`. Copied files land in `Resources/app/{dest}/`. |
| Does Nitro log actual port with PORT=0? | **YES** — format: `➜ Listening on: http://<addr>:<port>/` via h3+srvx adapter. |
| How does Electrobun detect dev mode? | **`ELECTROBUN_BUILD_ENV`** env var set by CLI. Values: `dev`, `canary`, `stable`. |
---
## Work Objectives
### Core Objective
Enable the Electrobun desktop app to run in production mode by spawning the TanStack Start server as a managed child process, while preserving existing dev mode behavior.
### Concrete Deliverables
- `apps/desktop/src/bun/index.ts` — Complete rewrite with dual-mode support
- `apps/desktop/electrobun.config.ts``build.copy` + all-platform configs
- `apps/desktop/turbo.json` — Cross-workspace build dependency
- `apps/desktop/AGENTS.md` — Accurate documentation of new architecture
### Definition of Done
- [ ] `bun typecheck` passes from monorepo root (zero errors)
- [ ] `bun build` from root succeeds: server builds first, then desktop bundles server output
- [ ] `bun dev` from root still starts both apps (dev mode preserved)
- [ ] Desktop `index.ts` has zero hardcoded URLs (all dynamic)
### Must Have
- Dev mode: poll external `localhost:3000`, open window when ready (existing behavior, refactored)
- Prod mode: spawn server via `Bun.spawn`, parse port from stdout, open window
- Server bound to `127.0.0.1` only (no network exposure)
- `PORT=0` for system-assigned port (no conflicts)
- Server process killed on app quit (via `before-quit` event)
- Server crash detection (watch `exited` promise, log error, exit app)
- Startup timeout with clear error message
- Server `cwd` set to its own directory (for relative import resolution)
- `DATABASE_URL` passed through from parent environment
### Must NOT Have (Guardrails)
- No hardcoded port numbers (not even 3000 — use a named constant `DEV_SERVER_URL`)
- No `as any`, `@ts-ignore`, or `@ts-expect-error`
- No empty catch blocks — always handle or re-throw
- No `npm`, `npx`, `node` — Bun only
- No manual `useMemo`/`useCallback` (not relevant here, but per project rules)
- No suppressed type errors — fix them properly
- No custom env var for dev detection — use built-in `ELECTROBUN_BUILD_ENV`
- No compiled server binary — use `.output/server/index.mjs` with bundled Bun
- Do NOT edit `apps/server/` — only `apps/desktop/` files change
- Do NOT add `@furtherverse/server` as a package dependency of desktop (use turbo cross-workspace dependency instead)
---
## Verification Strategy
> **UNIVERSAL RULE: ZERO HUMAN INTERVENTION**
>
> ALL tasks in this plan MUST be verifiable WITHOUT any human action.
> Every criterion is verified by running a command or using a tool.
### Test Decision
- **Infrastructure exists**: NO (no test framework in this project)
- **Automated tests**: NO (per project state — `AGENTS.md` says "No test framework configured yet")
- **Framework**: None
- **Agent-Executed QA**: ALWAYS (mandatory for all tasks)
---
## Execution Strategy
### Parallel Execution Waves
```
Wave 1 (Start Immediately — all independent, different files):
├── Task 1: Update electrobun.config.ts (build.copy + platform configs)
├── Task 2: Update turbo.json (cross-workspace build dependency)
└── Task 3: Rewrite index.ts (complete dev/prod mode implementation)
Wave 2 (After Wave 1 — needs final state of all files):
├── Task 4: Rewrite AGENTS.md (documentation reflecting new architecture)
└── Task 5: End-to-end verification (typecheck + build pipeline)
```
### Dependency Matrix
| Task | Depends On | Blocks | Can Parallelize With |
|------|------------|--------|---------------------|
| 1 | None | 4, 5 | 2, 3 |
| 2 | None | 4, 5 | 1, 3 |
| 3 | None | 4, 5 | 1, 2 |
| 4 | 1, 2, 3 | 5 | None |
| 5 | 1, 2, 3, 4 | None | None (final) |
### Agent Dispatch Summary
| Wave | Tasks | Recommended Agents |
|------|-------|-------------------|
| 1 | 1, 2, 3 | Tasks 1,2: `delegate_task(category="quick")`. Task 3: `delegate_task(category="unspecified-high")` |
| 2 | 4, 5 | Task 4: `delegate_task(category="writing")`. Task 5: `delegate_task(category="quick")` |
---
## TODOs
- [ ] 1. Update `apps/desktop/electrobun.config.ts` — Add build.copy and platform configs
**What to do**:
- Add `build.copy` to include the server's Nitro build output in the desktop bundle:
```typescript
copy: {
'../server/.output': 'server-output',
}
```
This copies `apps/server/.output/` (the entire Nitro build output) into `Resources/app/server-output/` in the Electrobun bundle. The full server entry point will be at `Resources/app/server-output/server/index.mjs`.
- Add `macOS` platform config block (currently only `linux` exists):
```typescript
macOS: {
bundleCEF: true,
}
```
- Add `windows` platform config block:
```typescript
windows: {
bundleCEF: true,
}
```
- Verify the exact property names by checking Electrobun's `ElectrobunConfig` type definition. The `linux` block already uses `bundleCEF: true`, so follow the same pattern for other platforms. If the type doesn't support `macOS`/`windows` yet, skip those and leave a `// TODO:` comment explaining what's needed.
- Preserve existing config values exactly (app name, identifier, version, bun entrypoint, linux config).
**Must NOT do**:
- Do not change the app name, identifier, or version
- Do not change the bun entrypoint path
- Do not remove the existing `linux` config
- Do not add dependencies or scripts
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: Single file, small config change, clear specification
- **Skills**: `[]`
- No specialized skills needed — straightforward TypeScript config edit
- **Skills Evaluated but Omitted**:
- `frontend-ui-ux`: No UI work involved
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1 (with Tasks 2, 3)
- **Blocks**: Tasks 4, 5
- **Blocked By**: None (can start immediately)
**References**:
**Pattern References** (existing code to follow):
- `apps/desktop/electrobun.config.ts` — Current config structure. The `linux.bundleCEF: true` pattern should be replicated for other platforms. The `build.bun.entrypoint` key shows where build config lives.
**API/Type References** (contracts to implement against):
- The `ElectrobunConfig` type from `electrobun` — imported via `import type { ElectrobunConfig } from 'electrobun'`. Check its definition (likely in `node_modules/electrobun/`) to verify exact property names for `copy`, `macOS`, `windows`.
**External References**:
- Electrobun `build.copy` syntax: copies source (relative to project root) into `Resources/app/{dest}/` in the bundle. Uses `cpSync` with `dereference: true`.
**WHY Each Reference Matters**:
- `electrobun.config.ts`: You're editing this file — need to know its current shape to preserve existing values
- `ElectrobunConfig` type: Must match the type definition exactly — don't guess property names
**Acceptance Criteria**:
- [ ] `build.copy` key exists with `'../server/.output': 'server-output'` mapping
- [ ] Platform configs added for all three platforms (or TODO comments if types don't support them)
- [ ] Existing config values unchanged (app.name = 'Desktop', etc.)
- [ ] File passes `bun typecheck` (no type errors)
**Agent-Executed QA Scenarios:**
```
Scenario: Config file is valid TypeScript with correct types
Tool: Bash
Preconditions: None
Steps:
1. Run: bun typecheck (from apps/desktop/)
2. Assert: Exit code 0
3. Assert: No errors mentioning electrobun.config.ts
Expected Result: TypeScript compilation succeeds
Evidence: Terminal output captured
Scenario: build.copy key has correct structure
Tool: Bash (grep)
Preconditions: File has been edited
Steps:
1. Read apps/desktop/electrobun.config.ts
2. Assert: Contains '../server/.output'
3. Assert: Contains 'server-output'
4. Assert: File still contains 'satisfies ElectrobunConfig'
Expected Result: Config has copy mapping and type annotation
Evidence: File contents
```
**Commit**: YES (groups with 2)
- Message: `feat(desktop): add build.copy for server bundle and platform configs`
- Files: `apps/desktop/electrobun.config.ts`
- Pre-commit: `bun typecheck` (from `apps/desktop/`)
---
- [ ] 2. Update `apps/desktop/turbo.json` — Add cross-workspace build dependency
**What to do**:
- Add `dependsOn` to the existing `build` task to ensure the server builds before the desktop:
```json
{
"tasks": {
"build": {
"dependsOn": ["@furtherverse/server#build"],
"outputs": ["build/**", "artifacts/**"]
}
}
}
```
- This tells Turbo: "before running `build` for `@furtherverse/desktop`, first run `build` for `@furtherverse/server`."
- This ensures `apps/server/.output/` exists when `electrobun build` runs and tries to `build.copy` from `../server/.output`.
- Preserve the existing `outputs` array exactly.
**Must NOT do**:
- Do not modify the root `turbo.json` — only `apps/desktop/turbo.json`
- Do not remove existing `outputs`
- Do not add other tasks or change other config
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: Single file, one-line JSON change
- **Skills**: `[]`
- No specialized skills needed
- **Skills Evaluated but Omitted**:
- `git-master`: Commit will be grouped with Task 1
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1 (with Tasks 1, 3)
- **Blocks**: Tasks 4, 5
- **Blocked By**: None (can start immediately)
**References**:
**Pattern References** (existing code to follow):
- `apps/desktop/turbo.json` — Current file with `build.outputs` already defined. You're adding `dependsOn` alongside it.
- `turbo.json` (root) — Shows existing turbo patterns like `build.dependsOn: ["^build"]`. The root already uses `^build` for workspace dependencies, but since desktop doesn't list server as a package dependency, we need an explicit cross-workspace reference.
**API/Type References**:
- Turbo `dependsOn` syntax: `"@furtherverse/server#build"` means "run the `build` task in the `@furtherverse/server` workspace".
**Documentation References**:
- `apps/server/package.json` — The package name is `@furtherverse/server` (verify this is the exact name used in the `dependsOn` reference).
**WHY Each Reference Matters**:
- `apps/desktop/turbo.json`: You're editing this file — preserve existing outputs
- `apps/server/package.json`: Need exact package name for the cross-workspace reference
- Root `turbo.json`: Context for existing turbo patterns in this project
**Acceptance Criteria**:
- [ ] `apps/desktop/turbo.json` has `dependsOn: ["@furtherverse/server#build"]` in the build task
- [ ] Existing `outputs` array is preserved
- [ ] Valid JSON (no syntax errors)
**Agent-Executed QA Scenarios:**
```
Scenario: turbo.json is valid JSON with correct structure
Tool: Bash
Preconditions: File has been edited
Steps:
1. Run: bun -e "JSON.parse(require('fs').readFileSync('apps/desktop/turbo.json', 'utf8'))"
2. Assert: Exit code 0 (valid JSON)
3. Read the file and verify structure contains both dependsOn and outputs
Expected Result: Valid JSON with both keys present
Evidence: Terminal output captured
Scenario: Turbo resolves the cross-workspace dependency
Tool: Bash
Preconditions: turbo.json updated
Steps:
1. Run: bunx turbo build --dry-run --filter=@furtherverse/desktop (from monorepo root)
2. Assert: Output shows @furtherverse/server#build runs BEFORE @furtherverse/desktop#build
Expected Result: Server build is listed as a dependency in the dry-run output
Evidence: Terminal output showing task execution order
```
**Commit**: YES (groups with 1)
- Message: `feat(desktop): add build.copy for server bundle and platform configs`
- Files: `apps/desktop/turbo.json`
- Pre-commit: Valid JSON check
---
- [ ] 3. Rewrite `apps/desktop/src/bun/index.ts` — Complete dev/prod mode implementation
**What to do**:
This is the core task. Completely rewrite `index.ts` to support both dev and prod modes. The new file should have this structure:
**A. Imports and Constants**:
```typescript
import Electrobun, { BrowserWindow } from 'electrobun/bun'
// Import PATHS — verify exact import syntax from electrobun/bun type definitions
// It may be: import { PATHS } from 'electrobun/bun'
// Or it may be on the Electrobun default export: Electrobun.PATHS
// CHECK the type definitions in node_modules/electrobun/ before writing
import { join, dirname } from 'path'
const DEV_SERVER_URL = 'http://localhost:3000'
const SERVER_READY_TIMEOUT_MS = 30_000
const PORT_PATTERN = /Listening on:?\s*https?:\/\/[^\s:]+:(\d+)/
```
**B. `isDev()` function**:
- Check `process.env.ELECTROBUN_BUILD_ENV === 'dev'`
- If `ELECTROBUN_BUILD_ENV` is not set, default to `true` (dev mode) — safe fallback
- Return a boolean
**C. `getServerEntryPath()` function**:
- Use `PATHS.RESOURCES_FOLDER` (or equivalent) to resolve the bundled server entry
- Path: `join(PATHS.RESOURCES_FOLDER, 'app', 'server-output', 'server', 'index.mjs')`
- **IMPORTANT**: Verify `PATHS.RESOURCES_FOLDER` points to `Resources/` and that `build.copy` destinations land in `Resources/app/`. If the pathing is different, adjust accordingly. The executor MUST verify by checking Electrobun's source or type definitions.
**D. `waitForServer(url, timeoutMs)` function** (preserved from current code):
- Polls a URL with `fetch` HEAD requests
- Returns `true` when server responds with `response.ok`
- Returns `false` on timeout
- Uses `Bun.sleep(100)` between attempts
- Catches fetch errors silently (server not up yet)
**E. `spawnServer()` function** (NEW — the critical piece):
- Returns a `Promise<{ process: Subprocess; url: string }>`
- Implementation:
1. Resolve the server entry path via `getServerEntryPath()`
2. Resolve the server directory via `dirname(serverEntryPath)` — used as `cwd`
3. Spawn with `Bun.spawn`:
```typescript
const serverProc = Bun.spawn([process.execPath, serverEntryPath], {
cwd: serverDir,
env: {
...process.env,
PORT: '0',
HOST: '127.0.0.1',
},
stdout: 'pipe',
stderr: 'pipe',
})
```
4. Read stdout as a stream to find the port:
- Use `serverProc.stdout` (a `ReadableStream<Uint8Array>`)
- Create a reader, accumulate chunks into a text buffer
- Test buffer against `PORT_PATTERN` regex after each chunk
- When match found: extract port, resolve promise with `{ process: serverProc, url: 'http://127.0.0.1:${port}' }`
5. Implement a timeout:
- Use `setTimeout` to reject the promise after `SERVER_READY_TIMEOUT_MS`
- On timeout, kill the server process before rejecting
6. Handle early exit:
- If stdout ends (stream done) before port is found, reject with error
- Include any stderr output in the error message for debugging
**F. `main()` async function**:
- Log startup message
- Branch on `isDev()`:
- **Dev mode**:
1. Log: "Dev mode: waiting for external server..."
2. Call `waitForServer(DEV_SERVER_URL)`
3. If timeout: log error with instructions (`"Run: bun dev in apps/server"`), `process.exit(1)`
4. Set `serverUrl = DEV_SERVER_URL`
- **Prod mode**:
1. Log: "Production mode: starting embedded server..."
2. Call `spawnServer()`
3. If error: log error, `process.exit(1)`
4. Store returned `process` and `url`
- Create `BrowserWindow` with the resolved `serverUrl`:
```typescript
new BrowserWindow({
title: 'Furtherverse',
url: serverUrl,
frame: { x: 100, y: 100, width: 1200, height: 800 },
renderer: 'cef',
})
```
- Register lifecycle handlers:
- `Electrobun.events.on('before-quit', ...)`: Kill server process if it exists
- Watch `serverProcess.exited` (if in prod mode): When server exits unexpectedly, log the exit code and stderr, then `process.exit(1)`
**G. Top-level execution**:
```typescript
main().catch((error) => {
console.error('Failed to start:', error)
process.exit(1)
})
```
**Critical implementation details**:
- The `PORT_PATTERN` regex must handle multiple log formats:
- `➜ Listening on: http://localhost:54321/` (srvx format)
- `Listening on http://127.0.0.1:54321` (node-server format)
- `Listening on http://[::]:54321` (IPv6 format)
- The regex `/Listening on:?\s*https?:\/\/[^\s:]+:(\d+)/` captures the port from all these formats.
- `cwd` MUST be set to the server directory (`dirname(serverEntryPath)`), not the app root. Nitro resolves internal `_libs/` imports relative to its directory.
- `process.execPath` in an Electrobun bundle points to the bundled Bun binary — this is what runs the server.
- `stderr: 'pipe'` — capture stderr for crash diagnostics but don't block on it during startup.
**Must NOT do**:
- Do not hardcode port numbers anywhere (use `PORT=0` and parse result)
- Do not use `as any` or type assertions to work around issues
- Do not use `child_process` module — use `Bun.spawn` (native Bun API)
- Do not bind server to `0.0.0.0` — always use `127.0.0.1`
- Do not leave the `waitForServer` function unused in dev mode
- Do not use synchronous I/O for stdout reading
**Recommended Agent Profile**:
- **Category**: `unspecified-high`
- Reason: Complex async logic (stream parsing, subprocess lifecycle, timeout management), multiple code paths (dev/prod), error handling across process boundaries. This is the architectural centerpiece.
- **Skills**: `[]`
- No specialized skills needed — pure Bun/TypeScript with Electrobun APIs
- **Skills Evaluated but Omitted**:
- `frontend-ui-ux`: No UI work — this is backend/process management code
- `playwright`: No browser testing needed for this task
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1 (with Tasks 1, 2)
- **Blocks**: Tasks 4, 5
- **Blocked By**: None (can start immediately — edits a different file from Tasks 1, 2)
**References**:
**Pattern References** (existing code to follow):
- `apps/desktop/src/bun/index.ts` — Current implementation. Preserve the `waitForServer` polling pattern (slightly refactored). Keep the `BrowserWindow` config (title, frame dimensions, renderer). Keep the top-level `main().catch(...)` pattern.
- `apps/desktop/src/bun/index.ts:1` — Current import: `import Electrobun, { BrowserWindow } from 'electrobun/bun'`. Extend this to also import `PATHS` (verify exact export name from type definitions).
**API/Type References** (contracts to implement against):
- `electrobun/bun` module — Exports `Electrobun` (default), `BrowserWindow` (named), and `PATHS` (named — verify). Check `node_modules/electrobun/` for exact type definitions.
- `Bun.spawn` API — Returns `Subprocess` with `.stdout` (ReadableStream when piped), `.stderr`, `.exited` (Promise<number>), `.kill()`.
- `PATHS.RESOURCES_FOLDER` — Absolute path to `Resources/` directory in the bundle. Verify by reading the Paths.ts source in electrobun package.
**Documentation References**:
- `apps/desktop/AGENTS.md` — Mentions production mode architecture (aspirational, but gives intent)
**External References**:
- Electrobun lifecycle events: `Electrobun.events.on('before-quit', callback)` — callback can call `event.response({ allow: false })` to cancel. Source: `electrobun/src/bun/core/Utils.ts`.
- Electrobun `PATHS`: Source at `electrobun/src/bun/core/Paths.ts`. Contains `RESOURCES_FOLDER` and `VIEWS_FOLDER`.
- Bun `Subprocess` docs: `stdout` is `ReadableStream<Uint8Array>` when `stdout: 'pipe'`.
**WHY Each Reference Matters**:
- Current `index.ts`: Preserving the `waitForServer` pattern, `BrowserWindow` config, and error handling style. You're rewriting this file, so understand what to keep vs. replace.
- `electrobun/bun` types: MUST verify `PATHS` export name and shape before using it. Don't assume — check.
- `Bun.spawn` API: Core to the entire prod mode implementation. Understand `stdout` stream reading.
- Lifecycle events: `before-quit` is where server cleanup happens. Understand the event contract.
**Acceptance Criteria**:
- [ ] File compiles: `bun typecheck` passes (from `apps/desktop/`)
- [ ] No hardcoded port numbers (grep for `:3000` — should only appear in `DEV_SERVER_URL` constant)
- [ ] `isDev()` function uses `process.env.ELECTROBUN_BUILD_ENV`
- [ ] `spawnServer()` uses `PORT=0`, `HOST=127.0.0.1`, `process.execPath`
- [ ] `spawnServer()` sets `cwd` to `dirname(serverEntryPath)`
- [ ] `before-quit` handler kills server process
- [ ] Server crash watcher exists (watches `subprocess.exited`)
- [ ] Timeout handling exists in both dev and prod paths
- [ ] All Biome rules pass: `bun fix` produces no changes
**Agent-Executed QA Scenarios:**
```
Scenario: File compiles with zero type errors
Tool: Bash
Preconditions: File has been rewritten
Steps:
1. Run: bun typecheck (from apps/desktop/)
2. Assert: Exit code 0
3. Assert: No errors in output
Expected Result: Clean TypeScript compilation
Evidence: Terminal output captured
Scenario: No hardcoded ports outside DEV_SERVER_URL
Tool: Bash (grep)
Preconditions: File has been rewritten
Steps:
1. Search apps/desktop/src/bun/index.ts for literal ':3000'
2. Assert: Only occurrence is in the DEV_SERVER_URL constant definition
3. Search for literal '3000' — should only appear once
Expected Result: Port 3000 only in constant, nowhere else
Evidence: Grep output
Scenario: Code passes Biome lint/format
Tool: Bash
Preconditions: File has been rewritten
Steps:
1. Run: bun fix (from apps/desktop/)
2. Run: git diff apps/desktop/src/bun/index.ts
3. Assert: No diff (bun fix made no changes)
Expected Result: Code already conforms to Biome rules
Evidence: Empty git diff
Scenario: Required patterns present in source
Tool: Bash (grep)
Preconditions: File has been rewritten
Steps:
1. Grep for 'ELECTROBUN_BUILD_ENV' — Assert: found
2. Grep for 'Bun.spawn' — Assert: found
3. Grep for 'process.execPath' — Assert: found
4. Grep for 'PORT.*0' — Assert: found
5. Grep for '127.0.0.1' — Assert: found
6. Grep for 'before-quit' — Assert: found
7. Grep for '.exited' — Assert: found (crash watcher)
8. Grep for 'dirname' — Assert: found (cwd for server)
Expected Result: All required patterns present
Evidence: Grep results for each pattern
```
**Commit**: YES
- Message: `feat(desktop): implement production mode with child process server`
- Files: `apps/desktop/src/bun/index.ts`
- Pre-commit: `bun typecheck && bun fix`
---
- [ ] 4. Rewrite `apps/desktop/AGENTS.md` — Document new architecture
**What to do**:
- Completely rewrite `AGENTS.md` to reflect the actual implemented architecture
- Document:
- **Architecture overview**: Desktop spawns server as child process in prod, connects to external server in dev
- **Dev mode**: How it works (polls localhost:3000, requires server running separately)
- **Prod mode**: How it works (spawns server from bundle, PORT=0, parses port from stdout)
- **Environment detection**: `ELECTROBUN_BUILD_ENV` values (`dev`, `canary`, `stable`)
- **Build pipeline**: Server must build before desktop (turbo dependency), `build.copy` bundles output
- **Key files**: `src/bun/index.ts` (main process), `electrobun.config.ts` (build config)
- **Environment variables**: `DATABASE_URL` (required, passed to server), `ELECTROBUN_BUILD_ENV` (auto-set by CLI)
- **Server lifecycle**: Spawned on start, killed on quit, crash = exit
- **Commands**: `bun dev`, `bun build`, `bun typecheck`, `bun fix`
- Follow the style and conventions of the root `AGENTS.md` and `apps/server/AGENTS.md`
- Be factual — only document what actually exists, not aspirational features
**Must NOT do**:
- Do not document features that don't exist
- Do not copy content from the server's AGENTS.md verbatim
- Do not include implementation details that belong in code comments
**Recommended Agent Profile**:
- **Category**: `writing`
- Reason: Documentation task requiring clear technical writing
- **Skills**: `[]`
- No specialized skills needed
- **Skills Evaluated but Omitted**:
- `frontend-ui-ux`: Not a UI task
**Parallelization**:
- **Can Run In Parallel**: NO
- **Parallel Group**: Wave 2
- **Blocks**: Task 5
- **Blocked By**: Tasks 1, 2, 3 (needs to know final state of all files)
**References**:
**Pattern References** (existing code to follow):
- `AGENTS.md` (root) — Follow same structure: Overview, Build Commands, Code Style, Directory Structure sections
- `apps/server/AGENTS.md` — Follow same style for app-specific documentation. Use this as a template for tone and detail level.
**Content References** (what to document):
- `apps/desktop/src/bun/index.ts` — The rewritten file (Task 3 output). Document its behavior, not its code.
- `apps/desktop/electrobun.config.ts` — The updated config (Task 1 output). Document build.copy and platform configs.
- `apps/desktop/turbo.json` — The updated turbo config (Task 2 output). Document the build dependency.
**WHY Each Reference Matters**:
- Root `AGENTS.md`: Template for documentation style
- Server `AGENTS.md`: Template for app-specific docs
- All Task 1-3 outputs: The actual implemented behavior that must be accurately documented
**Acceptance Criteria**:
- [ ] File exists and is valid Markdown
- [ ] Documents dev mode behavior accurately
- [ ] Documents prod mode behavior accurately
- [ ] Documents `ELECTROBUN_BUILD_ENV` mechanism
- [ ] Documents build pipeline (server → desktop dependency)
- [ ] Documents `DATABASE_URL` requirement
- [ ] Does NOT mention features that don't exist
- [ ] Follows conventions from root `AGENTS.md`
**Agent-Executed QA Scenarios:**
```
Scenario: AGENTS.md contains all required sections
Tool: Bash (grep)
Preconditions: File has been rewritten
Steps:
1. Grep for 'dev' or 'Dev' — Assert: found (dev mode documented)
2. Grep for 'prod' or 'Prod' or 'production' — Assert: found (prod mode documented)
3. Grep for 'ELECTROBUN_BUILD_ENV' — Assert: found
4. Grep for 'DATABASE_URL' — Assert: found
5. Grep for 'child process' or 'spawn' — Assert: found (architecture documented)
6. Grep for 'bun dev' — Assert: found (commands documented)
7. Grep for 'bun build' — Assert: found (commands documented)
Expected Result: All key topics are covered
Evidence: Grep results
Scenario: No aspirational/unimplemented features documented
Tool: Bash (grep)
Preconditions: File has been rewritten
Steps:
1. Grep for 'TODO' or 'planned' or 'future' or 'coming soon' — Assert: not found (or minimal)
2. Grep for 'auto-update' — Assert: not found (not implemented)
3. Grep for 'tray' — Assert: not found (not implemented)
Expected Result: Only implemented features documented
Evidence: Grep results showing no aspirational content
```
**Commit**: YES
- Message: `docs(desktop): rewrite AGENTS.md to reflect production mode architecture`
- Files: `apps/desktop/AGENTS.md`
- Pre-commit: None (Markdown file)
---
- [ ] 5. End-to-end verification — Typecheck and build pipeline
**What to do**:
- Run full monorepo typecheck to ensure no type errors were introduced
- Run full monorepo build to verify:
1. Server builds first (produces `.output/`)
2. Desktop builds second (copies server output into bundle)
3. No build errors
- Run Biome formatting/linting check on all changed files
- Verify dev mode still works conceptually (no runtime test — just verify the code path exists)
**Must NOT do**:
- Do not fix issues in server code — only desktop code
- Do not modify any files unless fixing issues found during verification
- Do not skip any verification step
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: Running commands and checking output, no creative work
- **Skills**: `[]`
- No specialized skills needed
- **Skills Evaluated but Omitted**:
- `playwright`: No browser testing in this verification
**Parallelization**:
- **Can Run In Parallel**: NO
- **Parallel Group**: Wave 2 (sequential after Task 4)
- **Blocks**: None (final task)
- **Blocked By**: Tasks 1, 2, 3, 4
**References**:
**Documentation References**:
- `AGENTS.md` (root) — Build/Lint/Test commands: `bun typecheck`, `bun fix`, `bun build`
- `apps/desktop/package.json` — Desktop-specific scripts
**WHY Each Reference Matters**:
- Root `AGENTS.md`: Canonical list of verification commands
- Desktop `package.json`: Desktop-specific build/typecheck commands
**Acceptance Criteria**:
- [ ] `bun typecheck` (monorepo root) exits with code 0
- [ ] `bun build` (monorepo root) exits with code 0
- [ ] `bun fix` (monorepo root) produces no changes (all code formatted)
- [ ] Build output shows server building before desktop
- [ ] Desktop build output includes server bundle (verify in build artifacts)
**Agent-Executed QA Scenarios:**
```
Scenario: Monorepo typecheck passes
Tool: Bash
Preconditions: All tasks 1-4 completed
Steps:
1. Run: bun typecheck (from monorepo root)
2. Assert: Exit code 0
3. Assert: No error output
Expected Result: Zero type errors across entire monorepo
Evidence: Terminal output captured
Scenario: Monorepo build succeeds with correct order
Tool: Bash
Preconditions: All tasks 1-4 completed
Steps:
1. Run: bun build (from monorepo root)
2. Assert: Exit code 0
3. Assert: Output shows @furtherverse/server build task runs
4. Assert: Output shows @furtherverse/desktop build task runs AFTER server
Expected Result: Build pipeline executes in correct order
Evidence: Terminal output showing task order
Scenario: Biome finds no issues
Tool: Bash
Preconditions: All tasks 1-4 completed
Steps:
1. Run: bun fix (from monorepo root)
2. Run: git diff
3. Assert: No changes (all code already formatted)
Expected Result: All code passes Biome rules
Evidence: Empty git diff
Scenario: Desktop build artifacts include server bundle
Tool: Bash
Preconditions: Build succeeded
Steps:
1. Search desktop build output directory for server-output/ or index.mjs
2. Assert: Server files are present in the desktop bundle
Expected Result: Server bundle is included in desktop build output
Evidence: File listing of build artifacts
```
**Commit**: NO (verification only — no file changes unless fixing issues)
---
## Commit Strategy
| After Task(s) | Message | Files | Verification |
|---------------|---------|-------|--------------|
| 1, 2 | `feat(desktop): add build.copy for server bundle and cross-workspace build dependency` | `electrobun.config.ts`, `turbo.json` | `bun typecheck` |
| 3 | `feat(desktop): implement production mode with child process server` | `src/bun/index.ts` | `bun typecheck && bun fix` |
| 4 | `docs(desktop): rewrite AGENTS.md to reflect production mode architecture` | `AGENTS.md` | None |
| 5 | (no commit — verification only) | — | — |
---
## Success Criteria
### Verification Commands
```bash
bun typecheck # Expected: exit 0, no errors
bun build # Expected: exit 0, server builds before desktop
bun fix # Expected: no changes (already formatted)
```
### Final Checklist
- [ ] All "Must Have" features present in `index.ts`
- [ ] All "Must NOT Have" exclusions verified absent
- [ ] All 3 verification commands pass
- [ ] `AGENTS.md` accurately reflects implemented architecture
- [ ] Server output is bundled into desktop build via `build.copy`
- [ ] Turbo builds server before desktop

View File

@@ -1,124 +1,130 @@
# AGENTS.md - Desktop App Guidelines
Electrobun desktop shell — loads the server app in a native window.
Thin Electrobun shell hosting the fullstack server app.
## Tech Stack
> **⚠️ This project uses Bun — NOT Node.js / npm. All commands use `bun`. Never use `npm`, `npx`, or `node`.**
- **Type**: Electrobun desktop application
- **Design**: Server-driven desktop (thin native shell hosting web app)
- **Runtime**: Bun (Main process) + CEF (Chromium Embedded Framework)
- **Framework**: Electrobun
- **Build**: Electrobun CLI + Turborepo
## Architecture
- **Type**: Electrobun desktop application
- **Design**: Bun main process + CEF renderer (Chromium Embedded Framework)
- **Dev mode**: Connects to `localhost:3000` (requires server dev running)
- **Prod mode**: Embeds server bundle and starts local HTTP server
- **Config**: `electrobun.config.ts` — app metadata, build entrypoint, platform options
- **Server-driven design**: The desktop app is a "thin" native shell. It does not contain UI or business logic; it merely hosts the `apps/server` TanStack Start application in a native window.
- **Dev mode**: Connects to an external Vite dev server at `localhost:3000`. Requires `apps/server` to be running separately.
- **Prod mode**: Spawns an embedded TanStack Start server (Nitro) as a child process and loads the dynamically assigned local URL.
- **Config**: `electrobun.config.ts` manages app metadata (identifier, name), build entries, and asset bundling.
## Commands
```bash
# Development (from apps/desktop/)
bun dev # Start Electrobun dev mode
# Development
bun dev # Start Electrobun dev mode (requires server dev running)
# Build
bun build # Build canary release (current platform)
bun build:all # Build canary release (all platforms)
bun build:stable # Build stable release (current platform)
bun build:stable:all # Build stable release (all platforms)
bun build # Build stable release (all platforms)
# Code Quality
bun fix # Biome auto-fix (lint + format)
bun fix # Biome auto-fix
bun typecheck # TypeScript check
```
## Directory Structure
```
apps/desktop/
.
├── src/
│ └── bun/
│ └── index.ts # Main process entry (BrowserWindow + server wait)
├── electrobun.config.ts # App name, identifier, version, build config
├── package.json
├── tsconfig.json # Extends @furtherverse/tsconfig/bun.json
└── turbo.json # Build output config
│ └── index.ts # Main process entry (Window management + server lifecycle)
├── electrobun.config.ts # App metadata and build/copy configuration
├── package.json # Scripts and dependencies
├── turbo.json # Build pipeline dependencies (depends on server build)
└── AGENTS.md # Desktop guidelines (this file)
```
## Development Workflow
1. **Start server dev first**: `bun dev` from `apps/server/`
2. **Start Electrobun**: `bun dev` from `apps/desktop/`
3. Desktop app polls `localhost:3000` until server responds, then opens window
1. **Start server**: In `apps/server`, run `bun dev`.
2. **Start desktop**: In `apps/desktop`, run `bun dev`.
3. **Connection**: The desktop app polls `localhost:3000` until responsive, then opens the native window.
## Production Architecture
### Build Pipeline
The desktop build is orchestrated by Turbo. It depends on the server's production build:
- `turbo.json`: `@furtherverse/desktop#build` depends on `@furtherverse/server#build`.
- `electrobun.config.ts`: Copies `../server/.output` to `server-output` folder within the app bundle.
### Server Lifecycle
In production, the main process manages the embedded server:
- **Spawn**: Spawns server from `server-output/server/index.mjs` using `Bun.spawn`.
- **Port Allocation**: Server is started with `PORT=0` and `HOST=127.0.0.1`.
- **Port Detection**: The main process parses the server's `stdout` using a regex to find the dynamically assigned port.
- **Lifecycle**: The server process is tied to the app; it is killed on `SIGTERM`, `SIGINT`, or app exit. If the server process crashes, the app exits with an error.
## Environment Detection
The application determines its environment via the `ELECTROBUN_BUILD_ENV` variable, automatically set by the Electrobun CLI:
```typescript
const isDev = () => {
const env = process.env.ELECTROBUN_BUILD_ENV
return !env || env === 'dev'
}
```
- **dev**: Development mode (connects to external host).
- **canary / stable**: Production mode (starts embedded server).
## Environment Variables
- `ELECTROBUN_BUILD_ENV`: Auto-set by CLI. Determines whether to use local dev server or embedded server.
- `DATABASE_URL`: Required by the server process. Must be passed through from the parent environment to the spawned child process.
## Electrobun Patterns
### Config (`electrobun.config.ts`)
### BrowserWindow Configuration
The main window uses the CEF renderer for consistency across platforms.
```typescript
import type { ElectrobunConfig } from 'electrobun'
export default {
app: {
name: 'MyApp',
identifier: 'com.example.myapp',
version: '0.1.0',
},
build: {
bun: { entrypoint: 'src/bun/index.ts' },
linux: { bundleCEF: true },
},
} satisfies ElectrobunConfig
```
### BrowserWindow
```typescript
import { BrowserWindow } from 'electrobun/bun'
new BrowserWindow({
title: 'My App',
url: 'http://localhost:3000',
frame: { x: 100, y: 100, width: 1200, height: 800 },
title: 'Furtherverse',
url: serverUrl,
frame: {
x: 100,
y: 100,
width: 1200,
height: 800,
},
renderer: 'cef',
})
```
### Server Readiness Check
### Path Aliases
The main process uses `electrobun/bun` for native APIs and `PATHS` for locating bundled assets.
```typescript
// Poll server before opening window — don't block indefinitely
async function waitForServer(url: string, timeoutMs = 30000): Promise<boolean> {
const start = Date.now()
while (Date.now() - start < timeoutMs) {
try {
const response = await fetch(url, { method: 'HEAD' })
if (response.ok) return true
} catch {
await Bun.sleep(100)
}
}
return false
}
```
import { BrowserWindow, PATHS } from 'electrobun/bun'
### Application Events
```typescript
import Electrobun from 'electrobun/bun'
Electrobun.events.on('will-quit', () => {
console.log('App quitting...')
})
// Locate the embedded server bundle
const serverEntryPath = join(PATHS.VIEWS_FOLDER, '..', 'server-output', 'server', 'index.mjs')
```
## Critical Rules
**DO:**
- Run server dev before desktop dev
- Use `catalog:` for dependency versions
- Wait for server readiness before opening BrowserWindow
- Handle server timeout gracefully (exit with helpful error message)
- Use arrow functions for all components and utility functions.
- Ensure `apps/server` is built before building `apps/desktop` (handled by Turbo).
- Parse the server's stdout for the port instead of hardcoding ports in production.
- Handle server process termination to avoid orphan processes.
- Use `catalog:` for all dependency versions in `package.json`.
**DON'T:**
- Use `npm`, `npx`, `node`, `yarn`, `pnpm` — always use `bun` / `bunx`
- Hardcode dependency versions (use catalog)
- Block main thread with synchronous waits
- Use `unwrap()`-style patterns without error handling
- Use `npm`, `npx`, `node`, `yarn`, or `pnpm`. Always use `bun`.
- Hardcode `localhost:3000` for production builds.
- Include UI components or business logic in the desktop app (keep it thin).
- Use `as any` or `@ts-ignore`.

View File

@@ -2,14 +2,23 @@ import type { ElectrobunConfig } from 'electrobun'
export default {
app: {
name: 'Furtherverse',
identifier: 'com.imbytecat.furtherverse',
version: '0.1.0',
name: 'Desktop',
identifier: 'com.furtherverse.desktop',
version: '1.0.0',
},
build: {
bun: {
entrypoint: 'src/bun/index.ts',
},
copy: {
'../server/.output': 'server-output',
},
mac: {
bundleCEF: true,
},
win: {
bundleCEF: true,
},
linux: {
bundleCEF: true,
},

View File

@@ -1,37 +1,136 @@
import Electrobun, { BrowserWindow } from 'electrobun/bun'
import { dirname, join } from 'node:path'
import { BrowserWindow, PATHS } from 'electrobun/bun'
const DEV_SERVER_URL = 'http://localhost:3000'
const SERVER_READY_TIMEOUT_MS = 30_000
const PORT_PATTERN = /Listening on:?\s*https?:\/\/[^\s:]+:(\d+)/
async function waitForServer(url: string, timeoutMs = 30000): Promise<boolean> {
const isDev = (): boolean => {
const env = process.env.ELECTROBUN_BUILD_ENV
return !env || env === 'dev'
}
const getServerEntryPath = (): string => {
return join(PATHS.VIEWS_FOLDER, '..', 'server-output', 'server', 'index.mjs')
}
const waitForServer = async (
url: string,
timeoutMs = SERVER_READY_TIMEOUT_MS,
): Promise<boolean> => {
const start = Date.now()
while (Date.now() - start < timeoutMs) {
try {
const response = await fetch(url, { method: 'HEAD' })
if (response.ok) return true
} catch {
await Bun.sleep(100)
} catch (_) {
// Server not up yet, retry after sleep
}
await Bun.sleep(100)
}
return false
}
async function main() {
console.log('Starting Furtherverse Desktop...')
console.log('Waiting for dev server at', DEV_SERVER_URL)
const spawnServer = async (): Promise<{
process: ReturnType<typeof Bun.spawn>
url: string
}> => {
const serverEntryPath = getServerEntryPath()
const serverDir = dirname(serverEntryPath)
const serverProc = Bun.spawn([process.execPath, serverEntryPath], {
cwd: serverDir,
env: {
...process.env,
PORT: '0',
HOST: '127.0.0.1',
},
stdout: 'pipe',
stderr: 'pipe',
})
const port = await new Promise<string>((resolve, reject) => {
const timeout = setTimeout(() => {
serverProc.kill()
reject(
new Error(`Server did not start within ${SERVER_READY_TIMEOUT_MS}ms`),
)
}, SERVER_READY_TIMEOUT_MS)
const reader = serverProc.stdout.getReader()
let buffer = ''
const read = async () => {
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
clearTimeout(timeout)
reject(new Error('Server process exited before reporting port'))
return
}
const text = new TextDecoder().decode(value)
buffer += text
process.stdout.write(text)
const match = PORT_PATTERN.exec(buffer)
if (match?.[1]) {
clearTimeout(timeout)
resolve(match[1])
return
}
}
} catch (err) {
clearTimeout(timeout)
reject(err)
}
}
serverProc.exited.then((code) => {
clearTimeout(timeout)
reject(new Error(`Server exited with code ${code} before becoming ready`))
})
read()
})
return { process: serverProc, url: `http://127.0.0.1:${port}` }
}
const main = async () => {
console.log('Starting Furtherverse Desktop...')
let serverUrl: string
let serverProcess: ReturnType<typeof Bun.spawn> | null = null
if (isDev()) {
console.log('Dev mode: waiting for external server at', DEV_SERVER_URL)
const ready = await waitForServer(DEV_SERVER_URL)
if (!ready) {
console.error(
'Dev server not responding. Make sure to run: cd apps/server && bun dev',
'Dev server not responding. Make sure to run: bun dev in apps/server',
)
process.exit(1)
}
console.log('Dev server ready!')
serverUrl = DEV_SERVER_URL
} else {
console.log('Production mode: starting embedded server...')
try {
const server = await spawnServer()
serverProcess = server.process
serverUrl = server.url
console.log('Server ready at', serverUrl)
} catch (err) {
console.error('Failed to start embedded server:', err)
process.exit(1)
}
}
new BrowserWindow({
title: 'Furtherverse',
url: DEV_SERVER_URL,
url: serverUrl,
frame: {
x: 100,
y: 100,
@@ -41,7 +140,31 @@ async function main() {
renderer: 'cef',
})
Electrobun.events.on('will-quit', () => console.log('Quitting...'))
if (serverProcess) {
const cleanup = () => {
if (serverProcess) {
serverProcess.kill()
serverProcess = null
}
}
process.on('exit', cleanup)
process.on('SIGTERM', () => {
cleanup()
process.exit(0)
})
process.on('SIGINT', () => {
cleanup()
process.exit(0)
})
serverProcess.exited.then((code) => {
if (serverProcess) {
console.error(`Server exited unexpectedly with code ${code}`)
process.exit(1)
}
})
}
}
main().catch((error) => {

View File

@@ -3,6 +3,7 @@
"extends": ["//"],
"tasks": {
"build": {
"dependsOn": ["@furtherverse/server#build"],
"outputs": ["build/**", "artifacts/**"]
}
}