new claudes

This commit is contained in:
Chris Wanstrath 2026-02-11 20:50:18 -08:00
parent 2d7ec7d53a
commit 28e8d0db2c
2 changed files with 411 additions and 462 deletions

578
CLAUDE.md
View File

@ -1,6 +1,4 @@
# Toes - Claude Code Guide # Toes
## What It Is
Personal web appliance that auto-discovers and runs multiple web apps on your home network. 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 ## How It Works
1. Host server scans `/apps` directory for valid apps 1. Server scans `APPS_DIR` for directories with a `package.json` containing a `scripts.toes` entry
2. Valid app = has `package.json` with `scripts.toes` entry 2. Each app is spawned as a child process with a unique port (3001-3100)
3. Each app spawned as child process with unique port (3001+) 3. Dashboard UI shows all apps with status, logs, and links via SSE
4. Dashboard UI shows all apps with current status, logs, and links 4. CLI communicates with the server over HTTP
## 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=<name>` 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/<app>/current` (not just `APPS_DIR/<app>`) 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 |
## Tech Stack ## Tech Stack
- **Bun** runtime (not Node) - **Bun** runtime (not Node)
- **Hype** (custom HTTP framework wrapping Hono) from git+https://git.nose.space/defunkt/hype - **Hype** (custom HTTP framework wrapping Hono) from `@because/hype`
- **Forge** (typed CSS-in-JS) from git+https://git.nose.space/defunkt/forge - **Forge** (typed CSS-in-JS) from `@because/forge`
- **Commander** + **kleur** for CLI - **Commander** + **kleur** for CLI
- TypeScript + Hono JSX - TypeScript + Hono JSX
- Client renders with `hono/jsx/dom` (no build step, served directly)
## Running ## Running
```bash ```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 ## Project Structure
```tsx
// apps/example/index.tsx
import { Hype } from "@because/hype"
const app = new Hype()
app.get("/", (c) => c.html(<h1>Content</h1>))
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/`:
``` ```
${DATA_DIR}/toes/env/ src/
clock.env # env vars for clock app server/ # HTTP server and process management ($)
todo.env # env vars for todo app 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) - `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).
- App discovery, spawn, watch, auto-restart with exponential backoff - `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`.
- Health checks every 30s (3 failures trigger restart) - `api/sync.ts` -- File sync protocol: manifest comparison, push/pull with hash-based diffing.
- Port pool (3001-3100), sticky allocation per app - `index.tsx` -- Entry point. Mounts API routers, tool URL redirects (`/tool/:tool`), tool API proxy (`/api/tools/:tool/*`), initializes apps.
- SSE streams for real-time app state and log updates - `shell.tsx` -- Minimal HTML shell for the SPA.
- File sync protocol with hash-based manifests - `tui.ts` -- Terminal UI for the server process (renders app status table when TTY).
### CLI ### Client (`src/client/`)
- Full management: `toes list|start|stop|restart|info|new|rename|delete|open`
- File sync: `toes push|pull|sync`
- Logs: `toes logs [-f] <app>`
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/<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
`/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 ## 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. See `docs/CLAUDE.md` for the guide to writing toes apps and tools.
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=<app-name>.
### 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 (
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<ToolScript />
<Container>
{children}
</Container>
</body>
</html>
)
}
// 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(
<Layout title="My Tool">
<p>No app selected</p>
</Layout>
)
}
return c.html(
<Layout title="My Tool">
<h1>Tool for {appName}</h1>
</Layout>
)
})
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=<name>` 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 `<ToolScript />` 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
<a href={`${TOES_URL}/tool/code?app=${appName}&version=${version}`}>
View Code
</a>
```
### 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(
<Layout title="My Tool">
<ErrorBox>Please specify an app with ?app=&lt;name&gt;</ErrorBox>
</Layout>
)
}
```
**Empty states:**
```tsx
const EmptyState = define('EmptyState', {
padding: '40px 20px',
textAlign: 'center',
color: theme('colors-textMuted'),
})
{items.length === 0 ? (
<EmptyState>No items found</EmptyState>
) : (
<List>...</List>
)}
```
**Form handling:**
```tsx
app.get('/new', async c => {
return c.html(<Layout><form method="post" action="/new">...</form></Layout>)
})
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 `<ToolScript />`
8. [ ] Styles served at `/styles.css` with `baseStyles + stylesToCSS()`
9. [ ] Main route handles missing `?app` parameter gracefully
10. [ ] Uses `APPS_DIR/<app>/current` for file paths
11. [ ] Exports `app.defaults` as default export

295
docs/CLAUDE.md Normal file
View File

@ -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=<name>` 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(<h1>Hello</h1>))
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',
})
// <Box>content</Box> renders <div class="Box">content</div>
```
**`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' },
},
},
})
// <Button variant="primary">Save</Button>
```
**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. `<ToolScript />` in `<body>` (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 (
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<ToolScript />
<Container>{children}</Container>
</body>
</html>
)
}
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(<Layout title="My Tool"><p>No app selected</p></Layout>)
// ... tool logic using join(APPS_DIR, appName, 'current') for file paths
return c.html(<Layout title="My Tool">...</Layout>)
})
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:** `<a href={\`${TOES_URL}/tool/code?app=${appName}\`}>View Code</a>`.
## Patterns
**Fire-and-forget with polling** -- for long-running ops, don't await in POST. Use `<meta http-equiv="refresh" content="2" />` to poll while running.
**Inline client JS** -- use `<script dangerouslySetInnerHTML={{ __html: script }} />`.
**Data persistence** -- use filesystem. `DATA_DIR` env var points to a per-app data directory.
## Cron Jobs
Place `.ts` files in an app's `cron/` directory:
```ts
export const schedule = "day"
export default async function() {
console.log("Running at", new Date().toISOString())
}
```
Valid schedules: `"1 minute"`, `"5 minutes"`, `"15 minutes"`, `"30 minutes"`, `"hour"`, `"noon"`, `"midnight"`, `"day"`, `"week"`, `"sunday"` through `"saturday"`.
## Coding Guidelines
TS file organization order: imports, re-exports, const/lets, enums, interfaces, types, classes, functions, module init. Within each section, exports first (alphabetical), then non-exports (alphabetical).
Single-line functions: `const fn = () => {}`. Multi-line: `function name() {}`.