toes/CLAUDE.md
2026-02-11 20:50:18 -08:00

7.1 KiB

Toes

Personal web appliance that auto-discovers and runs multiple web apps on your home network.

"Plug it in, turn it on, and forget about the cloud."

How It Works

  1. Server scans APPS_DIR for directories with a package.json containing a scripts.toes entry
  2. Each app is spawned as a child process with a unique port (3001-3100)
  3. Dashboard UI shows all apps with status, logs, and links via SSE
  4. CLI communicates with the server over HTTP

Tech Stack

  • Bun runtime (not Node)
  • Hype (custom HTTP framework wrapping Hono) from @because/hype
  • Forge (typed CSS-in-JS) from @because/forge
  • Commander + kleur for CLI
  • TypeScript + Hono JSX
  • Client renders with hono/jsx/dom (no build step, served directly)

Running

bun run dev    # Hot reload (deletes pub/client/index.js first)
bun run start  # Production
bun run check  # Type check
bun run test   # Tests

Project Structure

src/
  server/         # HTTP server and process management ($)
  client/         # Browser-side dashboard
  shared/         # Types shared between server and client (@)
  lib/            # Code shared between CLI and server (%)
  cli/            # CLI tool
  tools/          # @because/toes package exports
  pages/          # Hype page routes

Path aliases: $ = server, @ = shared, % = lib (defined in tsconfig.json).

Server (src/server/)

  • apps.ts -- The heart: app discovery, process spawning, health checks, auto-restart, port allocation, log management, graceful shutdown. Exports APPS_DIR, TOES_DIR, TOES_URL, and the App type (extends shared App with process/timer fields).
  • api/apps.ts -- REST API + SSE stream. Routes: GET / (list), GET /stream (SSE), POST /:name/start|stop|restart, GET /:name/logs, POST / (create), DELETE /:name, PUT /:name/rename, PUT /:name/icon.
  • api/sync.ts -- File sync protocol: manifest comparison, push/pull with hash-based diffing.
  • index.tsx -- Entry point. Mounts API routers, tool URL redirects (/tool/:tool), tool API proxy (/api/tools/:tool/*), initializes apps.
  • shell.tsx -- Minimal HTML shell for the SPA.
  • tui.ts -- Terminal UI for the server process (renders app status table when TTY).

Client (src/client/)

Client-side SPA rendered with hono/jsx/dom. No build step -- Bun serves .tsx files directly.

  • index.tsx -- Entry point. Initializes rendering, SSE connection, theme, tool iframes.
  • state.ts -- Mutable module-level state (apps, selectedApp, sidebarCollapsed, etc.) with localStorage persistence. Components import state directly.
  • api.ts -- Fetch wrappers for server API calls.
  • tool-iframes.ts -- Manages tool iframe lifecycle (caching, visibility, height communication).
  • update.tsx -- SSE connection to /api/apps/stream for real-time state updates.
  • components/ -- Dashboard, Sidebar, AppDetail, Nav, AppSelector, LogsSection.
  • modals/ -- NewApp, RenameApp, DeleteApp dialogs.
  • styles/ -- Forge CSS-in-JS (themes, buttons, forms, layout).
  • themes/ -- Light/dark theme token definitions.

CLI (src/cli/)

  • index.ts -- Entry point (#!/usr/bin/env bun).
  • setup.ts -- Commander program definition with all commands.
  • commands/ -- Command implementations.
  • http.ts -- HTTP client for talking to the toes server.
  • name.ts -- App name resolution (argument or current directory).
  • prompts.ts -- Interactive prompts.
  • pager.ts -- Pipe output through system pager.

CLI commands:

  • Apps: list, info, new, get, open, rename, rm
  • Lifecycle: start, stop, restart, logs, 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

TS files should be organized in the following way:

  • imports
  • re-exports
  • const/lets
  • enums
  • interfaces
  • types
  • classes
  • functions
  • module init (top level function calls)

In each section, put the exports first, in alphabetical order.

Then, after the exports (if there were any), put everything else, also in alphabetical order.

For single-line functions, use const fn = () => {} and put them in the "functions" section of the file.

All other functions use the function blah(){} format.

Example:

import { code } from "coders"
import { something } from "somewhere"

export type { SomeType }

const RETRY_TIMES = 5
const WIDTH = 480

enum State {
  Stopped,
  Starting,
  Running,
}

interface Config {
  name: string
  port: number
}

type Handler = (req: Request) => Response

class App {
  config: Config

  constructor(config: Config) {
    this.config = config
  }
}

const isApp = (name: string) =>
  apps.has(name)

function createApp(name: string): App {
  const app = new App({ name, port: 3000 })
  apps.set(name, app)
  return app
}

function start(app: App): void {
  console.log(`Starting ${app.config.name}`)
}

Writing Apps and Tools

See docs/CLAUDE.md for the guide to writing toes apps and tools.