From 547747055132a6551bbc5ecf8c929dc21b160ea0 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Thu, 19 Feb 2026 10:04:56 -0800 Subject: [PATCH] Add comprehensive user guide for Toes platform --- GUIDE.md | 846 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 846 insertions(+) create mode 100644 GUIDE.md diff --git a/GUIDE.md b/GUIDE.md new file mode 100644 index 0000000..4757771 --- /dev/null +++ b/GUIDE.md @@ -0,0 +1,846 @@ +# Toes User Guide + +Toes is a personal web appliance that runs multiple web apps on your home network. Plug it in, turn it on, and forget about the cloud. + +## Table of Contents + +- [Quick Start](#quick-start) +- [Creating an App](#creating-an-app) + - [App Templates](#app-templates) + - [App Structure](#app-structure) + - [The Bare Minimum](#the-bare-minimum) + - [Using Hype](#using-hype) + - [Using Forge](#using-forge) +- [Creating a Tool](#creating-a-tool) + - [What's a Tool?](#whats-a-tool) + - [Tool Setup](#tool-setup) + - [Theme Tokens](#theme-tokens) + - [Accessing App Data](#accessing-app-data) +- [CLI Reference](#cli-reference) + - [App Management](#app-management) + - [Lifecycle](#lifecycle) + - [Syncing Code](#syncing-code) + - [Environment Variables](#environment-variables) + - [Versioning](#versioning) + - [Cron Jobs](#cron-jobs-1) + - [Metrics](#metrics) + - [Sharing](#sharing) + - [Configuration](#configuration) +- [Environment Variables](#environment-variables-1) +- [Health Checks](#health-checks) +- [App Lifecycle](#app-lifecycle) +- [Cron Jobs](#cron-jobs) +- [Data Persistence](#data-persistence) + +--- + +## Quick Start + +```bash +# Install the CLI +curl -fsSL http://toes.local/install | bash + +# Create a new app +toes new my-app + +# Enter the directory, install deps, and develop locally +cd my-app +bun install +bun dev + +# Push to the server +toes push + +# Open in browser +toes open +``` + +Your app is now running at `http://my-app.toes.local`. + +--- + +## Creating an App + +### App Templates + +Toes ships with three templates. Pick one when creating an app: + +```bash +toes new my-app # SSR (default) +toes new my-app --bare # Minimal +toes new my-app --spa # Single-page app +``` + +**SSR** — Server-side rendered with a pages directory. Best for most apps. Uses Hype's built-in layout and page routing. + +**Bare** — Just an `index.tsx` with a single route. Good when you want to start from scratch. + +**SPA** — Client-side rendering with `hono/jsx/dom`. Hype serves the HTML shell and static files; the browser handles routing and rendering. + +### App Structure + +A generated SSR app looks like this: + +``` +my-app/ + .npmrc # Points to the private registry + .toesignore # Files to exclude from sync (like .gitignore) + package.json # Must have scripts.toes + tsconfig.json # TypeScript config + index.tsx # Entry point (re-exports from src/server) + src/ + server/ + index.tsx # Hype app with routes + pages/ + index.tsx # Page components +``` + +### The Bare Minimum + +Every app needs three things: + +1. **`package.json`** with a `scripts.toes` entry +2. **`index.tsx`** that exports `app.defaults` +3. **A `GET /ok` route** that returns 200 (health check) + +**package.json:** + +```json +{ + "name": "my-app", + "module": "index.tsx", + "type": "module", + "private": true, + "scripts": { + "toes": "bun run --watch index.tsx" + }, + "toes": { + "icon": "🎨" + }, + "dependencies": { + "@because/hype": "*", + "@because/forge": "*" + } +} +``` + +The `scripts.toes` field is how Toes discovers your app. The `toes.icon` field sets the emoji shown in the dashboard. + +**.npmrc:** + +``` +registry=https://npm.nose.space +``` + +Required for installing `@because/*` packages. + +**index.tsx:** + +```tsx +import { Hype } from '@because/hype' + +const app = new Hype() + +app.get('/', c => c.html(

Hello World

)) +app.get('/ok', c => c.text('ok')) + +export default app.defaults +``` + +That's it. Push to the server and it runs. + +### Using Hype + +Hype wraps [Hono](https://hono.dev). Everything you know from Hono works here. Hype adds a few extras: + +**Basic routing:** + +```tsx +import { Hype } from '@because/hype' + +const app = new Hype() + +app.get('/', c => c.html(

Home

)) +app.get('/about', c => c.html(

About

)) +app.post('/api/items', async c => { + const body = await c.req.json() + return c.json({ ok: true }) +}) +app.get('/ok', c => c.text('ok')) + +export default app.defaults +``` + +**Sub-routers:** + +```tsx +const api = Hype.router() +api.get('/items', c => c.json([])) +api.post('/items', async c => { + const body = await c.req.json() + return c.json({ ok: true }) +}) + +app.route('/api', api) // mounts at /api/items +``` + +**Server-Sent Events:** + +```tsx +app.sse('/stream', (send, c) => { + send({ hello: 'world' }) + const interval = setInterval(() => send({ time: Date.now() }), 1000) + return () => clearInterval(interval) // cleanup on disconnect +}) +``` + +**Constructor options:** + +```tsx +const app = new Hype({ + layout: true, // Wraps pages in an HTML layout (default: true) + prettyHTML: true, // Pretty-print HTML output (default: true) + logging: true, // Log requests to stdout (default: true) +}) +``` + +### Using Forge + +Forge is a CSS-in-JS library that creates styled JSX components. Define a component once, use it everywhere. + +**Basic usage:** + +```tsx +import { define, stylesToCSS } from '@because/forge' + +const Box = define('Box', { + padding: 20, + borderRadius: '6px', + backgroundColor: '#f5f5f5', +}) +// content renders
content
+``` + +Numbers auto-convert to `px` (except `flex`, `opacity`, `zIndex`, `fontWeight`). + +**Set the HTML element:** + +```tsx +const Button = define('Button', { base: 'button', padding: '8px 16px' }) +const Link = define('Link', { base: 'a', textDecoration: 'none' }) +const Input = define('Input', { base: 'input', padding: 8, border: '1px solid #ccc' }) +``` + +**Pseudo-classes (`states`):** + +```tsx +const Item = define('Item', { + padding: 12, + states: { + ':hover': { backgroundColor: '#eee' }, + ':last-child': { borderBottom: 'none' }, + }, +}) +``` + +**Nested selectors:** + +```tsx +const List = define('List', { + selectors: { + '& > li:last-child': { borderBottom: 'none' }, + }, +}) +``` + +**Variants:** + +```tsx +const Button = define('Button', { + base: 'button', + padding: '8px 16px', + variants: { + variant: { + primary: { backgroundColor: '#2563eb', color: 'white' }, + danger: { backgroundColor: '#dc2626', color: 'white' }, + }, + }, +}) +// +``` + +**Serving CSS:** + +Forge generates CSS at runtime. Serve it from a route: + +```tsx +import { stylesToCSS } from '@because/forge' + +app.get('/styles.css', c => + c.text(stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8' }) +) +``` + +Then link it in your HTML: + +```tsx + +``` + +--- + +## Creating a Tool + +### What's a Tool? + +A tool is an app that appears as a tab inside the Toes dashboard instead of in the sidebar. Tools render in an iframe and receive the currently selected app as a `?app=` query parameter. Good for things like a code editor, log viewer, env manager, or cron scheduler. + +From the server's perspective, a tool is identical to an app — same lifecycle, same health checks, same port allocation. The only differences are in `package.json` and how you render. + +### Tool Setup + +A tool needs three extra things compared to a regular app: + +1. Set `"tool": true` in `package.json` +2. Include `` in the HTML body +3. Prepend `baseStyles` to CSS output + +**package.json:** + +```json +{ + "name": "my-tool", + "module": "index.tsx", + "type": "module", + "private": true, + "scripts": { + "toes": "bun run --watch index.tsx" + }, + "toes": { + "icon": "🔧", + "tool": true + }, + "dependencies": { + "@because/forge": "*", + "@because/hype": "*", + "@because/toes": "*" + } +} +``` + +Set `"tool"` to `true` for a tab labeled with the app name, or to a string for a custom label (e.g., `"tool": ".env"`). + +**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 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

) + } + + return c.html( + +

{appName}

+

Tool content for {appName}

+
+ ) +}) + +export default app.defaults +``` + +Key points: + +- `` handles dark/light mode syncing and iframe height communication with the dashboard. +- `baseStyles` sets the body background to match the dashboard theme. +- `prettyHTML: false` is recommended for tools since their output is inside an iframe. +- The `?app=` query parameter tells you which app the user has selected in the sidebar. + +### Theme Tokens + +Tools should use theme tokens to match the dashboard's look. Import `theme` from `@because/toes/tools`: + +```tsx +import { theme } from '@because/toes/tools' + +const Card = define('Card', { + color: theme('colors-text'), + backgroundColor: theme('colors-bgElement'), + border: `1px solid ${theme('colors-border')}`, + borderRadius: theme('radius-md'), + padding: theme('spacing-lg'), +}) +``` + +Available tokens: + +| Token | Description | +|-------|-------------| +| `colors-bg` | Page background | +| `colors-bgSubtle` | Subtle background | +| `colors-bgElement` | Element background (cards, inputs) | +| `colors-bgHover` | Hover background | +| `colors-text` | Primary text | +| `colors-textMuted` | Secondary text | +| `colors-textFaint` | Tertiary/disabled text | +| `colors-border` | Borders | +| `colors-link` | Link text | +| `colors-primary` | Primary action color | +| `colors-primaryText` | Text on primary color | +| `colors-error` | Error color | +| `colors-dangerBorder` | Danger state border | +| `colors-dangerText` | Danger text | +| `colors-success` | Success color | +| `colors-successBg` | Success background | +| `colors-statusRunning` | Running indicator | +| `colors-statusStopped` | Stopped indicator | +| `fonts-sans` | Sans-serif font stack | +| `fonts-mono` | Monospace font stack | +| `spacing-xs` | 4px | +| `spacing-sm` | 8px | +| `spacing-md` | 12px | +| `spacing-lg` | 16px | +| `spacing-xl` | 24px | +| `radius-md` | 6px | + +### Accessing App Data + +**Reading app files:** + +```tsx +import { join } from 'path' + +const APPS_DIR = process.env.APPS_DIR! + +app.get('/', c => { + const appName = c.req.query('app') + if (!appName) return c.html(

No app selected

) + + const appPath = join(APPS_DIR, appName, 'current') + // Read files from appPath... +}) +``` + +Always go through the `current` symlink — never access version directories directly. + +**Calling the Toes API:** + +```tsx +const TOES_URL = process.env.TOES_URL! + +// List all apps +const apps = await fetch(`${TOES_URL}/api/apps`).then(r => r.json()) + +// Get a specific app +const app = await fetch(`${TOES_URL}/api/apps/${name}`).then(r => r.json()) +``` + +**Linking between tools:** + +```html +Edit in Code +``` + +Tool URLs go through `/tool/:name` which redirects to the tool's subdomain with query params preserved. + +**Listening to lifecycle events:** + +```tsx +import { on } from '@because/toes/tools' + +const unsub = on('app:start', event => { + console.log(`${event.app} started at ${event.time}`) +}) + +// Event types: 'app:start', 'app:stop', 'app:create', 'app:delete', 'app:activate' +``` + +--- + +## CLI Reference + +The CLI connects to your Toes server over HTTP. By default it connects to `http://toes.local`. Set `TOES_URL` to point elsewhere, or set `DEV=1` to use `http://localhost:3000`. + +Most commands accept an optional app name. If omitted, the CLI uses the current directory's `package.json` name. + +### App Management + +**`toes list`** — List all apps and their status. + +```bash +toes list # Show apps and tools +toes list --apps # Apps only (exclude tools) +toes list --tools # Tools only +``` + +**`toes new [name]`** — Create a new app from a template. + +```bash +toes new my-app # SSR template (default) +toes new my-app --bare # Minimal template +toes new my-app --spa # SPA template +``` + +Creates the app locally, then pushes it to the server. If run without a name, scaffolds the current directory. + +**`toes info [name]`** — Show details for an app (state, URL, port, PID, uptime). + +**`toes get `** — Download an app from the server to your local machine. + +```bash +toes get my-app # Creates ./my-app/ with all files +cd my-app +bun install +bun dev # Develop locally +``` + +**`toes open [name]`** — Open a running app in your browser. + +**`toes rename [name] `** — Rename an app. Requires typing a confirmation. + +**`toes rm [name]`** — Permanently delete an app from the server. Requires typing a confirmation. + +### Lifecycle + +**`toes start [name]`** — Start a stopped app. + +**`toes stop [name]`** — Stop a running app. + +**`toes restart [name]`** — Stop and start an app. + +**`toes logs [name]`** — View logs for an app. + +```bash +toes logs my-app # Today's logs +toes logs my-app -f # Follow (tail) logs in real-time +toes logs my-app -d 2026-01-15 # Logs from a specific date +toes logs my-app -s 2d # Logs from the last 2 days +toes logs my-app -g error # Filter logs by pattern +toes logs my-app -f -g error # Follow and filter +``` + +Duration formats for `--since`: `1h` (hours), `2d` (days), `1w` (weeks), `1m` (months). + +### Syncing Code + +Toes uses a manifest-based sync protocol. Each file is tracked by SHA-256 hash. The server stores versioned snapshots with timestamps. + +**`toes push`** — Push local changes to the server. + +```bash +toes push # Push changes (fails if server changed) +toes push --force # Overwrite server changes +``` + +Creates a new version on the server, uploads changed files, deletes removed files, then activates the new version. The app auto-restarts. + +**`toes pull`** — Pull changes from the server. + +```bash +toes pull # Pull changes (fails if you have local changes) +toes pull --force # Overwrite local changes +``` + +**`toes status`** — Show what would be pushed or pulled. + +```bash +toes status +# Changes to push: +# * index.tsx +# + new-file.ts +# - removed-file.ts +``` + +**`toes diff`** — Show a line-by-line diff of changed files. + +**`toes sync`** — Watch for changes and sync bidirectionally in real-time. Useful during development when editing on the server. + +**`toes clean`** — Remove local files that don't exist on the server. + +```bash +toes clean # Interactive confirmation +toes clean --force # No confirmation +toes clean --dry-run # Show what would be removed +``` + +**`toes stash`** — Stash local changes (like `git stash`). + +```bash +toes stash # Save local changes +toes stash pop # Restore stashed changes +toes stash list # List all stashes +``` + +### Environment Variables + +**`toes env [name]`** — List environment variables for an app. + +```bash +toes env my-app # List app vars +toes env -g # List global vars +``` + +**`toes env set [name] [value]`** — Set a variable. + +```bash +toes env set my-app API_KEY sk-123 # Set for an app +toes env set my-app API_KEY=sk-123 # KEY=value format also works +toes env set -g API_KEY sk-123 # Set globally (shared by all apps) +``` + +Setting a variable automatically restarts the app. + +**`toes env rm [name] `** — Remove a variable. + +```bash +toes env rm my-app API_KEY # Remove from an app +toes env rm -g API_KEY # Remove global var +``` + +### Versioning + +Every push creates a timestamped version. The server keeps the last 5 versions. + +**`toes versions [name]`** — List deployed versions. + +```bash +toes versions my-app +# Versions for my-app: +# +# → 20260219-143022 2/19/2026, 2:30:22 PM (current) +# 20260218-091500 2/18/2026, 9:15:00 AM +# 20260217-160845 2/17/2026, 4:08:45 PM +``` + +**`toes history [name]`** — Show file changes between versions. + +**`toes rollback [name]`** — Rollback to a previous version. + +```bash +toes rollback my-app # Interactive version picker +toes rollback my-app -v 20260218-091500 # Rollback to specific version +``` + +### Cron Jobs + +Cron commands talk to the cron tool running on your Toes server. + +**`toes cron [app]`** — List all cron jobs, or jobs for a specific app. + +**`toes cron status `** — Show details for a specific job. + +```bash +toes cron status my-app:backup +# my-app:backup ok +# +# Schedule: day +# State: idle +# Last run: 2h ago +# Duration: 3s +# Exit code: 0 +# Next run: in 22h +``` + +**`toes cron run `** — Trigger a job immediately. + +```bash +toes cron run my-app:backup +``` + +**`toes cron log [target]`** — View cron logs. + +```bash +toes cron log # All cron logs +toes cron log my-app # Cron logs for an app +toes cron log my-app:backup # Logs for a specific job +toes cron log -f # Follow logs +``` + +### Metrics + +**`toes metrics [name]`** — Show CPU, memory, and disk usage. + +```bash +toes metrics # All apps +toes metrics my-app # Single app +``` + +### Sharing + +**`toes share [name]`** — Create a public tunnel to share an app over the internet. + +```bash +toes share my-app +# ↗ Sharing my-app... https://abc123.trycloudflare.com +``` + +**`toes unshare [name]`** — Stop sharing an app. + +### Configuration + +**`toes config`** — Show the current server URL and sync state. + +--- + +## Environment Variables + +Toes injects these variables into every app process automatically: + +| Variable | Description | +|----------|-------------| +| `PORT` | Assigned port (3001-3100). Your app must listen on this port. | +| `APPS_DIR` | Path to the apps directory on the server. | +| `DATA_DIR` | Per-app data directory for persistent storage. | +| `TOES_URL` | Base URL of the Toes server (e.g., `http://toes.local:3000`). | +| `TOES_DIR` | Path to the Toes config directory. | + +You can set custom variables per-app or globally. Global variables are inherited by all apps. Per-app variables override globals. + +```bash +# Set per-app +toes env set my-app OPENAI_API_KEY sk-123 + +# Set globally (shared by all apps) +toes env set -g DATABASE_URL postgres://localhost/mydb +``` + +Access them in your app: + +```tsx +const apiKey = process.env.OPENAI_API_KEY +``` + +--- + +## Health Checks + +Toes checks `GET /ok` on every app every 30 seconds. Your app must return a 2xx response. + +Three consecutive failures trigger an automatic restart with exponential backoff (1s, 2s, 4s, 8s, 16s, 32s). After 5 restart failures, the app is marked as errored and restart is disabled. + +The simplest health check: + +```tsx +app.get('/ok', c => c.text('ok')) +``` + +--- + +## App Lifecycle + +Apps move through these states: + +``` +invalid → stopped → starting → running → stopping → stopped + ↓ + error +``` + +- **invalid** — Missing `package.json` or `scripts.toes`. Fix the config and start manually. +- **stopped** — Not running. Start with `toes start` or the dashboard. +- **starting** — Process spawned, waiting for `/ok` to return 200. Times out after 30 seconds. +- **running** — Healthy and serving requests. +- **stopping** — SIGTERM sent, waiting for process to exit. Escalates to SIGKILL after 10 seconds. +- **error** — Crashed too many times. Start manually to retry. + +On startup, `bun install` runs automatically before the app's `scripts.toes` command. + +Apps are accessed via subdomain: `http://my-app.toes.local` or `http://my-app.localhost`. The Toes server proxies requests to the app's assigned port. + +--- + +## Cron Jobs + +Place TypeScript files in a `cron/` directory inside your app: + +```ts +// cron/daily-cleanup.ts +export const schedule = "day" + +export default async function() { + console.log("Running daily cleanup") + // Your job logic here +} +``` + +The cron tool auto-discovers jobs by scanning `cron/*.ts` in all apps. New jobs are picked up within 60 seconds. + +### Schedules + +| Value | When | +|-------|------| +| `1 minute` | Every minute | +| `5 minutes` | Every 5 minutes | +| `15 minutes` | Every 15 minutes | +| `30 minutes` | Every 30 minutes | +| `hour` | Top of every hour | +| `noon` | 12:00 daily | +| `midnight` / `day` | 00:00 daily | +| `week` / `sunday` | 00:00 Sunday | +| `monday` - `saturday` | 00:00 on that day | + +Jobs inherit the app's working directory and all environment variables. + +--- + +## Data Persistence + +Use the filesystem for data storage. The `DATA_DIR` environment variable points to a per-app directory that persists across deployments and restarts: + +```tsx +import { join } from 'path' +import { readFileSync, writeFileSync, existsSync } from 'fs' + +const DATA_DIR = process.env.DATA_DIR! + +function loadData(): MyData { + const path = join(DATA_DIR, 'data.json') + if (!existsSync(path)) return { items: [] } + return JSON.parse(readFileSync(path, 'utf-8')) +} + +function saveData(data: MyData) { + writeFileSync(join(DATA_DIR, 'data.json'), JSON.stringify(data, null, 2)) +} +``` + +`DATA_DIR` is separate from your app's code directory, so pushes and rollbacks won't affect stored data.