toes/CLAUDE.md
2026-02-15 08:36:58 -08:00

219 lines
7.4 KiB
Markdown

# 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`, `metrics`, `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/<name>/` 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
- `<appname>.env` -- per-app overrides
The server sets these on each app process: `PORT`, `APPS_DIR`, `TOES_URL`, `TOES_DIR`, `DATA_DIR`.
### SSE Streaming
Two SSE endpoints serve different consumers:
- `/api/apps/stream` -- Full app state snapshots on every change. Used by the dashboard UI. Driven by `onChange()` in `apps.ts`.
- `/api/events/stream` -- Discrete lifecycle events (`app:start`, `app:stop`, `app:activate`, `app:create`, `app:delete`). Used by app processes to react to other apps' lifecycle changes. Driven by `emit()`/`onEvent()` in `apps.ts`. Apps subscribe via `on()` from `@because/toes/tools`.
## 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.