diff --git a/CLAUDE.md b/CLAUDE.md index 9124d2b..457808e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,4 @@ -# Toes - Claude Code Guide - -## What It Is +# Toes Personal web appliance that auto-discovers and runs multiple web apps on your home network. @@ -8,151 +6,139 @@ Personal web appliance that auto-discovers and runs multiple web apps on your ho ## How It Works -1. Host server scans `/apps` directory for valid apps -2. Valid app = has `package.json` with `scripts.toes` entry -3. Each app spawned as child process with unique port (3001+) -4. Dashboard UI shows all apps with current status, logs, and links - -## Key Files - -### Server (`src/server/`) -- `apps.ts` - **The heart**: app discovery, process management, health checks, auto-restart -- `api/apps.ts` - REST API for app lifecycle (start/stop/restart, logs, icons, rename) -- `api/sync.ts` - File sync protocol (manifest, push/pull, watch) -- `index.tsx` - Entry point (minimal, initializes Hype) -- `shell.tsx` - HTML shell for web UI - -### Client (`src/client/`) -- `components/` - Dashboard, Sidebar, AppDetail, Nav -- `modals/` - NewApp, RenameApp, DeleteApp dialogs -- `styles/` - Forge CSS-in-JS (themes, buttons, forms, layout) -- `state.ts` - Client state management -- `api.ts` - API client - -### CLI (`src/cli/`) -- `commands/manage.ts` - list, start, stop, restart, info, new, rename, delete, open -- `commands/sync.ts` - push, pull, sync -- `commands/logs.ts` - log viewing with tail support - -### Shared (`src/shared/`) -- Code shared between frontend (browser) and backend (server) -- `types.ts` - App, AppState, Manifest interfaces -- IMPORTANT: Cannot use filesystem or Node APIs (runs in browser) - -### Lib (`src/lib/`) -- Code shared between CLI and server (server-side only) -- `templates.ts` - Template generation for new apps -- Can use filesystem and Node APIs (never runs in browser) - -### Other -- `apps/*/package.json` - Must have `"toes": "bun run --watch index.tsx"` script -- `TODO.txt` - Task list - -## Tools - -Tools are special apps that appear as tabs in the dashboard rather than standalone entries in the sidebar. They integrate directly into the Toes UI and can interact with the currently selected app. - -### Creating a Tool - -Add `toes.tool` to your app's `package.json`: - -```json -{ - "toes": { - "icon": "🔧", - "tool": true - }, - "scripts": { - "toes": "bun run --watch index.tsx" - } -} -``` - -### How Tools Work - -- Tools run as regular apps (spawned process with unique port) -- Displayed as iframes overlaying the tab content area -- Receive `?app=` query parameter for the currently selected app -- Iframes are cached per tool+app combination (never recreated once loaded) -- Tool state persists across tab switches -- **App paths**: When accessing app files, tools must use `APPS_DIR//current` (not just `APPS_DIR/`) to resolve through the version symlink - -### CLI Flags - -```bash -toes list # Lists all apps including tools -toes list --tools # Lists tools only -toes list --apps # Lists regular apps only (excludes tools) -``` - -### Tool vs App - -| Aspect | Regular App | Tool | -|--------|-------------|------| -| `toes.tool` | absent/false | true | -| UI location | Sidebar | Tab bar | -| Rendering | New browser tab | Iframe in dashboard | -| Context | Standalone | Knows selected app via query param | +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 git+https://git.nose.space/defunkt/hype -- **Forge** (typed CSS-in-JS) from git+https://git.nose.space/defunkt/forge +- **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 --hot src/server/index.tsx # Dev mode with hot reload +bun run dev # Hot reload (deletes pub/client/index.js first) +bun run start # Production +bun run check # Type check +bun run test # Tests ``` -## App Structure - -```tsx -// apps/example/index.tsx -import { Hype } from "@because/hype" -const app = new Hype() -app.get("/", (c) => c.html(

Content

)) -export default app.defaults -``` - -## Conventions - -- Apps get `PORT` env var from host -- Each app is isolated process with own dependencies -- No path-based routing - apps run on separate ports -- `DATA_DIR` env controls where apps are discovered -- Path aliases: `$` → server, `@` → shared, `%` → lib - -## Environment Variables - -Env vars are stored per-app in `TOES_DIR/env/`: +## Project Structure ``` -${DATA_DIR}/toes/env/ - clock.env # env vars for clock app - todo.env # env vars for todo app +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 ``` -`TOES_DIR` defaults to `${DATA_DIR}/toes`. Apps cannot access this directory directly. +Path aliases: `$` = server, `@` = shared, `%` = lib (defined in tsconfig.json). -## Current State +### Server (`src/server/`) -### Infrastructure (Complete) -- App discovery, spawn, watch, auto-restart with exponential backoff -- Health checks every 30s (3 failures trigger restart) -- Port pool (3001-3100), sticky allocation per app -- SSE streams for real-time app state and log updates -- File sync protocol with hash-based manifests +- `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). -### CLI -- Full management: `toes list|start|stop|restart|info|new|rename|delete|open` -- File sync: `toes push|pull|sync` -- Logs: `toes logs [-f] ` +### Client (`src/client/`) -Check `TODO.txt` for planned features +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 @@ -224,338 +210,6 @@ function start(app: App): void { } ``` -## Guide to Writing Tools +## Writing Apps and Tools -This guide explains how to write a Toes tool. - -A tool is just an app that is displayed in a tab and an iframe on each app's page. When the iframe is loaded, it's passed ?app=. - -### Minimal Tool Structure - -A tool needs four files at minimum: - -**.npmrc** -``` -registry=https://npm.nose.space -``` - -**tsconfig.json** -```json -{ - "compilerOptions": { - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "jsxImportSource": "hono/jsx", - "allowJs": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false, - "baseUrl": ".", - "paths": { - "$*": ["src/server/*"], - "#*": ["src/client/*"], - "@*": ["src/shared/*"] - } - } -} -``` - -**package.json** -```json -{ - "name": "my-tool", - "private": true, - "module": "index.tsx", - "type": "module", - "scripts": { - "toes": "bun run --watch index.tsx" - }, - "toes": { - "tool": true, // can also be set to a string to control the tab's label - "icon": "🔧" - }, - "dependencies": { - "@because/forge": "*", - "@because/hype": "*", - "@because/toes": "*" - }, - "devDependencies": { - "@types/bun": "latest" - } -} -``` - -**index.tsx** -```tsx -import { Hype } from '@because/hype' -import { define, stylesToCSS } from '@because/forge' -import { baseStyles, ToolScript, theme } from '@because/toes/tools' -import type { Child } from 'hono/jsx' - -const APPS_DIR = process.env.APPS_DIR! - -const app = new Hype({ prettyHTML: false }) - -// Define styled components using Forge -const Container = define('Container', { - fontFamily: theme('fonts-sans'), - padding: '20px', - paddingTop: 0, - maxWidth: '800px', - margin: '0 auto', - color: theme('colors-text'), -}) - -// Layout wrapper (every tool needs this pattern) -function Layout({ title, children }: { title: string; children: Child }) { - return ( - - - - - {title} - - - - - - {children} - - - - ) -} - -// Serve styles (required) -app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, { - 'Content-Type': 'text/css; charset=utf-8', -})) - -// Main route -app.get('/', async c => { - const appName = c.req.query('app') - - if (!appName) { - return c.html( - -

No app selected

-
- ) - } - - return c.html( - -

Tool for {appName}

-
- ) -}) - -export default app.defaults -``` - -### Required Imports - -Every tool imports from three packages: - -```tsx -// HTTP helpers -import { Hype } from '@because/hype' - -// Structured CSS -import { define, stylesToCSS } from '@because/forge' - -// Tool helpers -import { baseStyles, ToolScript, theme } from '@because/toes/tools' -``` - -### Environment Variables - -Tools have access to these environment variables: - -| Variable | Description | -|----------|-------------| -| `APPS_DIR` | Path to the apps directory (e.g., `/home/toes/apps`) | -| `TOES_URL` | Base URL of the Toes server (e.g., `http://192.168.1.100:3000`) | -| `PORT` | Port assigned to this tool | - -### Query Parameters - -Tools receive `?app=` when an app is selected. Add custom params as needed: - -```tsx -const appName = c.req.query('app') // Always present when app selected -const version = c.req.query('version') // Custom param (code tool) -const file = c.req.query('file') // Custom param (code tool) -``` - -### Styling with Forge - -Use `define()` to create styled components and `theme()` for design tokens: - -```tsx -const Button = define('Button', { - base: 'button', // HTML element (optional, defaults to 'div') - padding: '8px 16px', - backgroundColor: theme('colors-primary'), - color: 'white', - border: 'none', - borderRadius: theme('radius-md'), - cursor: 'pointer', - states: { - ':hover': { opacity: 0.9 }, - ':disabled': { opacity: 0.5, cursor: 'not-allowed' }, - }, -}) - -const List = define('List', { - listStyle: 'none', - padding: 0, - border: `1px solid ${theme('colors-border')}`, - borderRadius: theme('radius-md'), - selectors: { - '& > li:last-child': { borderBottom: 'none' }, - }, -}) -``` - -Common theme values: -- `colors-text`, `colors-textMuted`, `colors-bg`, `colors-bgHover`, `colors-bgElement`, `colors-bgSubtle` -- `colors-border`, `colors-link`, `colors-primary`, `colors-error` -- `colors-statusRunning`, `colors-statusStopped` -- `fonts-sans`, `fonts-mono` -- `radius-md` - -### ToolScript - -Always include `` in your layout's body. It handles: -1. Theme detection (light/dark mode based on system preference) -2. Iframe height communication with parent dashboard - -### Accessing App Files - -When accessing files in an app, always use the `current` symlink: - -```tsx -import { join } from 'path' -import { readdir } from 'fs/promises' - -const appPath = join(APPS_DIR, appName, 'current') // ✓ Correct -// NOT: join(APPS_DIR, appName) // ✗ Wrong -``` - -### Linking Between Tools - -Use `TOES_URL` to create links to other tools: - -```tsx -const TOES_URL = process.env.TOES_URL! - -// Link from versions tool to code tool - - View Code - -``` - -### Tool Complexity Levels - -**Simple (single file)** - `versions` tool -- Everything in `index.tsx` -- Good for straightforward display tools - -**Medium (re-export)** - `code` tool -- `index.tsx` re-exports: `export { default } from './src/server'` -- Actual implementation in `src/server/index.tsx` -- Good when you want cleaner organization - -**Complex (multiple modules)** - `cron` tool -- Main server in `index.tsx` -- Business logic in `lib/` directory -- Good for tools with significant logic (discovery, scheduling, state management) - -### Common Patterns - -**Error states:** -```tsx -const ErrorBox = define('ErrorBox', { - color: theme('colors-error'), - padding: '20px', - backgroundColor: theme('colors-bgElement'), - borderRadius: theme('radius-md'), -}) - -if (!appName) { - return c.html( - - Please specify an app with ?app=<name> - - ) -} -``` - -**Empty states:** -```tsx -const EmptyState = define('EmptyState', { - padding: '40px 20px', - textAlign: 'center', - color: theme('colors-textMuted'), -}) - -{items.length === 0 ? ( - No items found -) : ( - ... -)} -``` - -**Form handling:** -```tsx -app.get('/new', async c => { - return c.html(
...
) -}) - -app.post('/new', async c => { - const body = await c.req.parseBody() - const name = body.name as string - // Process form... - return c.redirect('/') -}) -``` - -**File watching (for live updates):** -```tsx -import { watch } from 'fs' - -let debounceTimer: Timer | null = null - -watch(APPS_DIR, { recursive: true }, (_event, filename) => { - if (!filename?.includes('/target/')) return - if (debounceTimer) clearTimeout(debounceTimer) - debounceTimer = setTimeout(handleChange, 100) -}) -``` - -### Checklist for New Tools - -1. [ ] `.npmrc` contains `registry=https://npm.nose.space` -2. [ ] `tsconfig.json` matches the exact config shown above (do not improvise) -3. [ ] `package.json` has `toes.tool: true` and `toes.icon` -4. [ ] `scripts.toes` uses `bun run --watch index.tsx` -5. [ ] Dependencies include `@because/forge`, `@because/hype`, `@because/toes` -6. [ ] Import `baseStyles`, `ToolScript`, `theme` from `@because/toes/tools` -7. [ ] Layout body includes `` -8. [ ] Styles served at `/styles.css` with `baseStyles + stylesToCSS()` -9. [ ] Main route handles missing `?app` parameter gracefully -10. [ ] Uses `APPS_DIR//current` for file paths -11. [ ] Exports `app.defaults` as default export +See `docs/CLAUDE.md` for the guide to writing toes apps and tools. diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md new file mode 100644 index 0000000..cbc6d09 --- /dev/null +++ b/docs/CLAUDE.md @@ -0,0 +1,295 @@ +# Toes - Guide to Writing Apps + +Toes discovers and runs web apps from an `apps/` directory, each on its own port. Apps are server-rendered TypeScript using **Hype** (wraps Hono) and **Forge** (CSS-in-JS). Runtime is **Bun**. + +## App vs Tool + +An **app** shows in the sidebar and opens in its own browser tab. + +A **tool** renders as a tab inside the dashboard (in an iframe). It receives `?app=` to know the selected app. The only code difference is `"toes": { "tool": true }` in package.json and some extra imports from `@because/toes`. + +## Required Files + +Every app needs `.npmrc`, `tsconfig.json`, `package.json`, and `index.tsx`. + +**.npmrc** -- always this exact content: +``` +registry=https://npm.nose.space +``` + +**tsconfig.json** -- use exactly, do not improvise: +```json +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx", + "allowJs": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "baseUrl": ".", + "paths": { + "$*": ["src/server/*"], + "#*": ["src/client/*"], + "@*": ["src/shared/*"] + } + } +} +``` + +**package.json** for an app: +```json +{ + "name": "my-app", + "private": true, + "module": "index.tsx", + "type": "module", + "scripts": { "toes": "bun run --watch index.tsx" }, + "toes": { "icon": "🖥️" }, + "dependencies": { + "@because/forge": "*", + "@because/hype": "*" + }, + "devDependencies": { "@types/bun": "latest" } +} +``` + +For a **tool**, add `@because/toes` to dependencies and set `"tool": true` (or a string for a custom tab label like `"tool": ".env"`): +```json +{ + "toes": { "icon": "🔧", "tool": true }, + "dependencies": { + "@because/forge": "*", + "@because/hype": "*", + "@because/toes": "*" + } +} +``` + +## Hype + +Hype wraps Hono. It adds `app.defaults` (the Bun server export), `app.sse()` for server-sent events, and `Hype.router()` for sub-routers. Everything else is standard Hono. + +```tsx +import { Hype } from '@because/hype' +const app = new Hype() + +app.get('/', c => c.html(

Hello

)) +app.get('/ok', c => c.text('ok')) // Health check -- required + +export default app.defaults +``` + +Constructor options: `prettyHTML` (default true, tools should set false), `layout` (default true), `logging` (default true). + +**SSE** -- the one non-Hono addition: +```tsx +app.sse('/stream', (send, c) => { + send({ hello: 'world' }) + const interval = setInterval(() => send({ time: Date.now() }), 1000) + return () => clearInterval(interval) // cleanup on disconnect +}) +``` + +**Sub-routers:** +```tsx +const api = Hype.router() +api.get('/items', c => c.json([])) +app.route('/api', api) // mounts at /api/items +``` + +## Forge + +Forge creates styled JSX components via `define()`. Properties use camelCase CSS. Numbers auto-convert to `px` (except `flex`, `opacity`, `zIndex`, `fontWeight`). + +```tsx +import { define, stylesToCSS } from '@because/forge' + +const Box = define('Box', { + padding: 20, + borderRadius: '6px', +}) +// content renders
content
+``` + +**`base`** -- set the HTML element (default `div`): +```tsx +const Button = define('Button', { base: 'button', padding: '8px 16px' }) +const Link = define('Link', { base: 'a', textDecoration: 'none' }) +``` + +**`states`** -- pseudo-classes: +```tsx +const Item = define('Item', { + padding: 12, + states: { + ':hover': { backgroundColor: '#eee' }, + ':last-child': { borderBottom: 'none' }, + }, +}) +``` + +**`selectors`** -- nested CSS (`&` = the component): +```tsx +const List = define('List', { + selectors: { + '& > li:last-child': { borderBottom: 'none' }, + }, +}) +``` + +**`variants`** -- conditional styles via props: +```tsx +const Button = define('Button', { + base: 'button', + variants: { + variant: { + primary: { backgroundColor: '#2563eb', color: 'white' }, + danger: { backgroundColor: '#dc2626', color: 'white' }, + }, + }, +}) +// +``` + +**Serving CSS** -- apps serve `stylesToCSS()` from a route. Tools prepend `baseStyles`: +```tsx +app.get('/styles.css', c => + c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8' }) +) +``` + +## Theme Tokens + +Tools import `theme` from `@because/toes/tools`. It returns CSS variables that resolve per light/dark mode. + +```tsx +import { baseStyles, ToolScript, theme } from '@because/toes/tools' + +const Container = define('Container', { + color: theme('colors-text'), + border: `1px solid ${theme('colors-border')}`, +}) +``` + +Available tokens: + +| Token | Use for | +|-------|---------| +| `colors-bg`, `colors-bgSubtle`, `colors-bgElement`, `colors-bgHover` | Backgrounds | +| `colors-text`, `colors-textMuted`, `colors-textFaint` | Text | +| `colors-border` | Borders | +| `colors-link` | Links | +| `colors-primary`, `colors-primaryText` | Primary actions | +| `colors-error`, `colors-dangerBorder`, `colors-dangerText` | Errors/danger | +| `colors-success`, `colors-successBg` | Success states | +| `colors-statusRunning`, `colors-statusStopped` | Status indicators | +| `fonts-sans`, `fonts-mono` | Font stacks | +| `spacing-xs` (4), `spacing-sm` (8), `spacing-md` (12), `spacing-lg` (16), `spacing-xl` (24) | Spacing (px) | +| `radius-md` (6px) | Border radius | + +## Writing a Tool + +Tools need three extra things vs apps: + +1. `` in `` (handles dark mode + iframe height communication) +2. `baseStyles` prepended to CSS output +3. Handle the `?app=` query param + +```tsx +import { Hype } from '@because/hype' +import { define, stylesToCSS } from '@because/forge' +import { baseStyles, ToolScript, theme } from '@because/toes/tools' +import type { Child } from 'hono/jsx' + +const APPS_DIR = process.env.APPS_DIR! +const app = new Hype({ prettyHTML: false }) + +const Container = define('Container', { + fontFamily: theme('fonts-sans'), + padding: '20px', + paddingTop: 0, + maxWidth: '800px', + margin: '0 auto', + color: theme('colors-text'), +}) + +function Layout({ title, children }: { title: string; children: Child }) { + return ( + + + + + {title} + + + + + {children} + + + ) +} + +app.get('/ok', c => c.text('ok')) +app.get('/styles.css', c => + c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8' }) +) + +app.get('/', async c => { + const appName = c.req.query('app') + if (!appName) return c.html(

No app selected

) + // ... tool logic using join(APPS_DIR, appName, 'current') for file paths + return c.html(...) +}) + +export default app.defaults +``` + +**Environment variables** available to tools: `APPS_DIR`, `TOES_URL` (base URL of Toes server), `PORT`, `TOES_DIR`. + +**Accessing app files:** always use `join(APPS_DIR, appName, 'current')`. + +**Calling the Toes API:** `fetch(\`${TOES_URL}/api/apps\`)`, `fetch(\`${TOES_URL}/api/apps/${name}\`)`. + +**Linking between tools:** `View Code`. + +## Patterns + +**Fire-and-forget with polling** -- for long-running ops, don't await in POST. Use `` to poll while running. + +**Inline client JS** -- use `