# 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)
  - [Deploying Code](#deploying-code)
  - [Environment Variables](#environment-variables)
  - [Cron Jobs](#cron-jobs-1)
  - [Metrics](#metrics)
  - [Sharing](#sharing)
- [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 (scaffolds, inits git, and pushes to server)
toes new my-app

# Enter the directory, install deps, and develop locally
cd my-app
bun install
bun dev

# Deploy changes (standard git)
git add . && git commit -m "my changes"
git push toes main

# Open in browser
toes open
```

Your app is now running at `http://my-app.toes.local`.

`toes new` automatically sets up a `toes` git remote pointing at the server. Pushing to it triggers a deploy.

---

## 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/
  .gitignore          # Files to exclude from sync and deploy
  .npmrc              # Points to the private registry
  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(<h1>Hello World</h1>))
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(<h1>Home</h1>))
app.get('/about', c => c.html(<h1>About</h1>))
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',
})
// <Box>content</Box> renders <div class="Box">content</div>
```

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' },
    },
  },
})
// <Button variant="primary">Save</Button>
```

**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
<link rel="stylesheet" href="/styles.css" />
```

---

## 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 `<ToolScript />` 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 (
    <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>)
  }

  return c.html(
    <Layout title="My Tool">
      <h2>{appName}</h2>
      <p>Tool content for {appName}</p>
    </Layout>
  )
})

export default app.defaults
```

Key points:

- `<ToolScript />` 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(<p>No app selected</p>)

  const appPath = join(APPS_DIR, appName)
  // Read files from appPath...
})
```

**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
<a href="/tool/code?app=my-app&file=index.tsx">Edit in Code</a>
```

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
```

Scaffolds the app locally, initializes a git repo with a `toes` remote pointing at the server, and pushes. The git push triggers a deploy. If run without a name, scaffolds the current directory.

**`toes info [name]`** — Show details for an app (state, URL, port, PID, uptime).

**`toes get <name>`** — Clone an app from the server to your local machine.

```bash
toes get my-app     # Clones into ./my-app/
cd my-app
bun install
bun dev             # Develop locally
```

The clone comes with a `toes` remote already configured, so `git push toes main` deploys.

**`toes open [name]`** — Open a running app in your browser.

**`toes rename [name] <new-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).

### Deploying Code

Toes uses git for deployments. Each app has a `toes` remote that points to the server's git tool. Pushing to it extracts the latest commit and deploys it.

```bash
# Make changes, commit, and deploy
git add .
git commit -m "update homepage"
git push toes main
```

The git push triggers the server to:
1. Store the commit in a bare repo at `DATA_DIR/repos/<name>.git`
2. Extract HEAD into the app directory
3. Run `bun install` and restart the app

Use standard git commands for history, diffing, and rollback:

```bash
git log              # View deploy history
git diff HEAD~1      # See what changed
git revert HEAD      # Undo last deploy
git push toes main   # Deploy the revert
```

To clone an existing app from the server:

```bash
git clone http://git.toes.local/my-app
cd my-app
bun install
bun dev              # Develop locally
```

### 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] <KEY> [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] <KEY>`** — 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 `git push toes main` creates a new deploy. Version history is managed through git.

```bash
git log --oneline    # List deploys
git revert HEAD      # Undo last change
git push toes main   # Deploy the revert
```

### 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 <app:name>`** — 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 <app:name>`** — 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.

---

## 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.
