# Toes Personal web appliance that auto-discovers and runs multiple web apps on your home network. "Plug it in, turn it on, and forget about the cloud." ## How It Works 1. Server scans `APPS_DIR` for directories with a `package.json` containing a `scripts.toes` entry 2. Each app is spawned as a child process with a unique port (3001-3100) 3. Dashboard UI shows all apps with status, logs, and links via SSE 4. CLI communicates with the server over HTTP ## Tech Stack - **Bun** runtime (not Node) - **Hype** (custom HTTP framework wrapping Hono) from `@because/hype` - **Forge** (typed CSS-in-JS) from `@because/forge` - **Commander** + **kleur** for CLI - TypeScript + Hono JSX - Client renders with `hono/jsx/dom` (no build step, served directly) ## Running ```bash bun run dev # Hot reload (deletes pub/client/index.js first) bun run start # Production bun run check # Type check bun run test # Tests ``` ## Project Structure ``` src/ server/ # HTTP server and process management ($) client/ # Browser-side dashboard shared/ # Types shared between server and client (@) lib/ # Code shared between CLI and server (%) cli/ # CLI tool tools/ # @because/toes package exports pages/ # Hype page routes ``` Path aliases: `$` = server, `@` = shared, `%` = lib (defined in tsconfig.json). ### Server (`src/server/`) - `apps.ts` -- **The heart**: app discovery, process spawning, health checks, auto-restart, port allocation, log management, graceful shutdown. Exports `APPS_DIR`, `TOES_DIR`, `TOES_URL`, and the `App` type (extends shared `App` with process/timer fields). - `api/apps.ts` -- REST API + SSE stream. Routes: `GET /` (list), `GET /stream` (SSE), `POST /:name/start|stop|restart`, `GET /:name/logs`, `POST /` (create), `DELETE /:name`, `PUT /:name/rename`, `PUT /:name/icon`. - `api/sync.ts` -- File sync protocol: manifest comparison, push/pull with hash-based diffing. - `index.tsx` -- Entry point. Mounts API routers, tool URL redirects (`/tool/:tool`), tool API proxy (`/api/tools/:tool/*`), initializes apps. - `shell.tsx` -- Minimal HTML shell for the SPA. - `tui.ts` -- Terminal UI for the server process (renders app status table when TTY). ### Client (`src/client/`) Client-side SPA rendered with `hono/jsx/dom`. No build step -- Bun serves `.tsx` files directly. - `index.tsx` -- Entry point. Initializes rendering, SSE connection, theme, tool iframes. - `state.ts` -- Mutable module-level state (`apps`, `selectedApp`, `sidebarCollapsed`, etc.) with localStorage persistence. Components import state directly. - `api.ts` -- Fetch wrappers for server API calls. - `tool-iframes.ts` -- Manages tool iframe lifecycle (caching, visibility, height communication). - `update.tsx` -- SSE connection to `/api/apps/stream` for real-time state updates. - `components/` -- Dashboard, Sidebar, AppDetail, Nav, AppSelector, LogsSection. - `modals/` -- NewApp, RenameApp, DeleteApp dialogs. - `styles/` -- Forge CSS-in-JS (themes, buttons, forms, layout). - `themes/` -- Light/dark theme token definitions. ### CLI (`src/cli/`) - `index.ts` -- Entry point (`#!/usr/bin/env bun`). - `setup.ts` -- Commander program definition with all commands. - `commands/` -- Command implementations. - `http.ts` -- HTTP client for talking to the toes server. - `name.ts` -- App name resolution (argument or current directory). - `prompts.ts` -- Interactive prompts. - `pager.ts` -- Pipe output through system pager. CLI commands: - **Apps**: `list`, `info`, `new`, `get`, `open`, `rename`, `rm` - **Lifecycle**: `start`, `stop`, `restart`, `logs`, `stats`, `cron` - **Sync**: `push`, `pull`, `status`, `diff`, `sync`, `clean`, `stash` - **Config**: `config`, `env`, `versions`, `history`, `rollback` ### Shared (`src/shared/`) Types shared between browser and server. **Cannot use Node/filesystem APIs** (runs in browser). - `types.ts` -- `App`, `AppState`, `LogLine`, `Manifest`, `FileInfo` - `gitignore.ts` -- `.toesignore` pattern matching ### Lib (`src/lib/`) Server-side code shared between CLI and server. Can use Node APIs. - `templates.ts` -- Template generation for `toes new` (bare, ssr, spa) - `sync.ts` -- Manifest generation, hash computation ### Tools Package (`src/tools/`) The `@because/toes` package that apps/tools import. Published exports: - `@because/toes` -- re-exports from server (`src/index.ts` -> `src/server/sync.ts`) - `@because/toes/tools` -- `baseStyles`, `ToolScript`, `theme`, `loadAppEnv` ### Pages (`src/pages/`) Hype page routes. `index.tsx` renders the Shell. ## Key Concepts ### App Lifecycle States: `invalid` -> `stopped` <-> `starting` -> `running` -> `stopping` -> `stopped` - Discovery: scan `APPS_DIR`, read each `package.json` for `scripts.toes` - Spawn: `Bun.spawn()` with `PORT`, `APPS_DIR`, `TOES_URL`, `TOES_DIR`, plus per-app env vars - Health checks: every 30s to `/ok`, 3 consecutive failures trigger restart - Auto-restart: exponential backoff (1s, 2s, 4s, 8s, 16s, 32s), resets after 60s stable run - Graceful shutdown: SIGTERM with 10s timeout before SIGKILL ### Tools vs Apps Tools are apps with `"toes": { "tool": true }` in package.json. From the server's perspective they're identical processes. The dashboard renders tools as iframe tabs instead of sidebar entries. Tool URLs redirect through the server: `/tool/:tool?app=foo` -> `http://host:toolPort/?app=foo`. ### Versioning Apps live at `APPS_DIR//` with timestamped version directories and a `current` symlink. Push creates a new version; rollback moves the symlink. ### Environment Variables Per-app env files in `TOES_DIR/env/`: - `_global.env` -- shared by all apps - `.env` -- per-app overrides The server sets these on each app process: `PORT`, `APPS_DIR`, `TOES_URL`, `TOES_DIR`, `DATA_DIR`. ### SSE Streaming `/api/apps/stream` pushes the full app list on every state change. Client reconnects automatically. The `onChange()` callback system in `apps.ts` notifies listeners. ## Coding Guidelines TS files should be organized in the following way: - imports - re-exports - const/lets - enums - interfaces - types - classes - functions - module init (top level function calls) In each section, put the `export`s first, in alphabetical order. Then, after the `export`s (if there were any), put everything else, also in alphabetical order. For single-line functions, use `const fn = () => {}` and put them in the "functions" section of the file. All other functions use the `function blah(){}` format. Example: ```ts import { code } from "coders" import { something } from "somewhere" export type { SomeType } const RETRY_TIMES = 5 const WIDTH = 480 enum State { Stopped, Starting, Running, } interface Config { name: string port: number } type Handler = (req: Request) => Response class App { config: Config constructor(config: Config) { this.config = config } } const isApp = (name: string) => apps.has(name) function createApp(name: string): App { const app = new App({ name, port: 3000 }) apps.set(name, app) return app } function start(app: App): void { console.log(`Starting ${app.config.name}`) } ``` ## Writing Apps and Tools See `docs/CLAUDE.md` for the guide to writing toes apps and tools.