diff options
| author | Adam Malczewski <[email protected]> | 2026-06-21 20:26:39 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-21 20:26:39 +0900 |
| commit | 4434fbf951d06a721597e805094069477851178e (patch) | |
| tree | 4d3103ccb0512ff77d9d472d7b4178c11c8b73b3 | |
| parent | ac69e5f8bea9377887a6f89ff362c6be0db1c874 (diff) | |
| download | dispatch-4434fbf951d06a721597e805094069477851178e.tar.gz dispatch-4434fbf951d06a721597e805094069477851178e.zip | |
feat: standalone build + systemd install (Arch Linux)
bin/build: compiles standalone binaries (dispatch-server + dispatch CLI)
via bun build --compile, builds the frontend static bundle with
VITE_HTTP_PORT=24991 + VITE_WS_PORT=24990, copies to dist/web/.
bin/install: installs binaries to /usr/bin/, frontend to
/usr/share/dispatch/web/, systemd service to /etc/systemd/system/,
config to /etc/dispatch/env, data dirs to /var/lib/dispatch/ +
/var/log/dispatch/. Enables + starts the dispatch systemd service.
Supports --uninstall and --no-build flags.
systemd/dispatch.service: Type=simple, reads /etc/dispatch/env,
restarts on failure, logs to journald.
systemd/dispatch.env: template config (ports 24991 HTTP + 24990 WS,
DISPATCH_WEB_DIR, API key, data paths).
transport-http: optional webDir static file serving — unmatched GET
requests fall through to Bun.file() serving with SPA index.html
fallback. Gated on DISPATCH_WEB_DIR env var (backward compatible).
| -rwxr-xr-x | bin/build | 56 | ||||
| -rwxr-xr-x | bin/install | 119 | ||||
| -rw-r--r-- | packages/transport-http/src/app.ts | 27 | ||||
| -rw-r--r-- | packages/transport-http/src/extension.ts | 3 | ||||
| -rw-r--r-- | systemd/dispatch.env | 29 | ||||
| -rw-r--r-- | systemd/dispatch.service | 17 |
6 files changed, 251 insertions, 0 deletions
diff --git a/bin/build b/bin/build new file mode 100755 index 0000000..0517af5 --- /dev/null +++ b/bin/build @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# bin/build — compile the backend + CLI binaries and the frontend static bundle. +# +# Output: +# dist/dispatch-server — standalone backend binary (Bun compile) +# dist/dispatch — standalone CLI binary (Bun compile) +# dist/web/ — built frontend static files (vite build) +# +# The frontend is built with VITE_HTTP_PORT=24991 + VITE_WS_PORT=24990 so the +# bundle talks to the backend on :24991 (HTTP) and :24990 (WS) at runtime. +# The backend serves the frontend from DISPATCH_WEB_DIR (set in the systemd env). +# +# Usage: bin/build [--no-frontend] (skip the frontend build if already built) + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$HERE/.." && pwd)" +BACKEND="$ROOT" +FRONTEND="$(cd "$ROOT/.." && pwd)/dispatch-web" + +BUILD_FRONTEND=1 +if [[ "${1:-}" == "--no-frontend" ]]; then + BUILD_FRONTEND=0 +fi + +echo "[build] backend binary → dist/dispatch-server" +mkdir -p "$ROOT/dist" +bun build --compile "$BACKEND/packages/host-bin/src/main.ts" \ + --outfile "$ROOT/dist/dispatch-server" \ + --minify 2>&1 | tail -3 + +echo "[build] CLI binary → dist/dispatch" +bun build --compile "$BACKEND/packages/cli/src/main.ts" \ + --outfile "$ROOT/dist/dispatch" \ + --minify 2>&1 | tail -3 + +if [[ "$BUILD_FRONTEND" -eq 1 ]]; then + if [[ ! -d "$FRONTEND" ]]; then + echo "[build] frontend not found at $FRONTEND — skipping" >&2 + else + echo "[build] frontend → dist/web/ (VITE_HTTP_PORT=24991 VITE_WS_PORT=24990)" + ( + cd "$FRONTEND" + VITE_HTTP_PORT=24991 VITE_WS_PORT=24990 bun run build 2>&1 | tail -5 + ) + rm -rf "$ROOT/dist/web" + cp -r "$FRONTEND/dist" "$ROOT/dist/web" + echo "[build] copied $(find "$ROOT/dist/web" -type f | wc -l) files" + fi +fi + +echo "[build] done." +echo " dist/dispatch-server ($(du -h "$ROOT/dist/dispatch-server" | cut -f1))" +echo " dist/dispatch ($(du -h "$ROOT/dist/dispatch" | cut -f1))" +[[ -d "$ROOT/dist/web" ]] && echo " dist/web/ ($(du -sh "$ROOT/dist/web" | cut -f1))" diff --git a/bin/install b/bin/install new file mode 100755 index 0000000..57d0684 --- /dev/null +++ b/bin/install @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# bin/install — build + install Dispatch as a systemd service on Arch Linux. +# +# Usage: +# sudo bin/install # build + install + enable + start +# sudo bin/install --no-build # install only (skip the build step) +# sudo bin/install --uninstall # stop + disable + remove files +# +# What it installs: +# /usr/bin/dispatch-server — backend binary +# /usr/bin/dispatch — CLI binary +# /usr/share/dispatch/web/ — frontend static files +# /etc/dispatch/env — server config (EnvironmentFile) +# /etc/systemd/system/dispatch.service — systemd unit +# /var/lib/dispatch/ — data directory (SQLite DBs) +# /var/log/dispatch/ — journal + trace logs +# +# After install: systemctl status dispatch +# Logs: journalctl -u dispatch -f +# Config: edit /etc/dispatch/env then `systemctl restart dispatch` + +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$HERE/.." && pwd)" + +if [[ "$(id -u)" -ne 0 ]]; then + echo "install: must run as root (use sudo)" >&2 + exit 1 +fi + +ACTION="install" +NO_BUILD=0 +for arg in "$@"; do + case "$arg" in + --no-build) NO_BUILD=1 ;; + --uninstall) ACTION="uninstall" ;; + *) echo "install: unknown flag: $arg" >&2; exit 1 ;; + esac +done + +# ─── Uninstall ─────────────────────────────────────────────────────────────── +if [[ "$ACTION" == "uninstall" ]]; then + echo "[uninstall] stopping service…" + systemctl stop dispatch 2>/dev/null || true + systemctl disable dispatch 2>/dev/null || true + rm -f /etc/systemd/system/dispatch.service + systemctl daemon-reload + echo "[uninstall] removing files…" + rm -f /usr/bin/dispatch-server /usr/bin/dispatch + rm -rf /usr/share/dispatch/web + # Keep config + data (user might want them) + echo "[uninstall] done." + echo " Kept: /etc/dispatch/env, /var/lib/dispatch/, /var/log/dispatch/" + echo " Remove manually if desired: rm -rf /etc/dispatch /var/lib/dispatch /var/log/dispatch" + exit 0 +fi + +# ─── Build ─────────────────────────────────────────────────────────────────── +if [[ "$NO_BUILD" -eq 0 ]]; then + echo "[install] building…" + "$ROOT/bin/build" +fi + +# Verify outputs exist +if [[ ! -f "$ROOT/dist/dispatch-server" ]]; then + echo "install: dist/dispatch-server not found — run bin/build first" >&2 + exit 1 +fi + +# ─── Install binaries ──────────────────────────────────────────────────────── +echo "[install] binaries → /usr/bin/" +install -Dm755 "$ROOT/dist/dispatch-server" /usr/bin/dispatch-server +install -Dm755 "$ROOT/dist/dispatch" /usr/bin/dispatch + +# ─── Install frontend ──────────────────────────────────────────────────────── +if [[ -d "$ROOT/dist/web" ]]; then + echo "[install] frontend → /usr/share/dispatch/web/" + mkdir -p /usr/share/dispatch/web + cp -r "$ROOT/dist/web/"* /usr/share/dispatch/web/ +fi + +# ─── Install systemd service ───────────────────────────────────────────────── +echo "[install] systemd service → /etc/systemd/system/" +install -Dm644 "$ROOT/systemd/dispatch.service" /etc/systemd/system/dispatch.service + +# ─── Config (don't overwrite if it exists) ─────────────────────────────────── +if [[ ! -f /etc/dispatch/env ]]; then + echo "[install] config → /etc/dispatch/env (edit with your API key)" + install -Dm600 "$ROOT/systemd/dispatch.env" /etc/dispatch/env + echo " ⚠️ Edit /etc/dispatch/env and set DISPATCH_API_KEY before starting!" +else + echo "[install] config /etc/dispatch/env already exists — keeping" +fi + +# ─── Data + log directories ────────────────────────────────────────────────── +mkdir -p /var/lib/dispatch /var/log/dispatch + +# ─── Enable + start ────────────────────────────────────────────────────────── +systemctl daemon-reload +systemctl enable dispatch + +if grep -q 'sk-\.\.\.' /etc/dispatch/env 2>/dev/null; then + echo "" + echo "[install] done — but DISPATCH_API_KEY is still the placeholder." + echo " 1. Edit /etc/dispatch/env and set your API key" + echo " 2. systemctl start dispatch" + echo " 3. journalctl -u dispatch -f" +else + echo "[install] starting service…" + systemctl start dispatch + echo "" + echo "[install] done!" + echo " Status: systemctl status dispatch" + echo " Logs: journalctl -u dispatch -f" + echo " Config: /etc/dispatch/env" + echo " Frontend: http://localhost:24991" + echo " API: http://localhost:24991/chat" +fi diff --git a/packages/transport-http/src/app.ts b/packages/transport-http/src/app.ts index 86bac0d..9e4fc8a 100644 --- a/packages/transport-http/src/app.ts +++ b/packages/transport-http/src/app.ts @@ -64,6 +64,12 @@ export interface CreateServerOptions { * that endpoint responds `500 { error: "not available" }`. */ readonly emit?: HostAPI["emit"]; + /** + * Directory containing built frontend static files. When set, unmatched GET + * requests fall through to static file serving (SPA fallback to index.html). + * When absent, no static serving (API-only — backward compatible). + */ + readonly webDir?: string; } const noopLogger: Logger = { @@ -669,5 +675,26 @@ export function createApp(opts: CreateServerOptions): Hono { } }); + // ─── Static frontend serving (catch-all, API routes take precedence) ────── + if (opts.webDir !== undefined) { + const webDir = opts.webDir; + app.get("*", async (c) => { + const urlPath = new URL(c.req.url).pathname; + const filePath = `${webDir}${urlPath}`; + const file = Bun.file(filePath); + if (await file.exists()) { + return new Response(file); + } + // SPA fallback: serve index.html for client-side routing + const indexFile = Bun.file(`${webDir}/index.html`); + if (await indexFile.exists()) { + return new Response(indexFile, { + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); + } + return c.json({ error: "Not found" }, 404); + }); + } + return app; } diff --git a/packages/transport-http/src/extension.ts b/packages/transport-http/src/extension.ts index e402213..3555f8e 100644 --- a/packages/transport-http/src/extension.ts +++ b/packages/transport-http/src/extension.ts @@ -73,6 +73,9 @@ export function createTransportHttpExtension(): Extension & { lspService, logger, emit: host.emit.bind(host), + ...(process.env.DISPATCH_WEB_DIR !== undefined + ? { webDir: process.env.DISPATCH_WEB_DIR } + : {}), }); const port = host.config.get<number>("httpPort") ?? 24203; diff --git a/systemd/dispatch.env b/systemd/dispatch.env new file mode 100644 index 0000000..37887cb --- /dev/null +++ b/systemd/dispatch.env @@ -0,0 +1,29 @@ +# /etc/dispatch/env — Dispatch server configuration (systemd EnvironmentFile) +# Copy this to /etc/dispatch/env and fill in the values. + +# ─── Ports ─────────────────────────────────────────────────────────────────── +# Backend HTTP (serves API + frontend static files) +BACKEND_PORT=24991 +# Surface WebSocket (frontend live updates + surfaces) +SURFACE_WS_PORT=24990 + +# ─── Frontend ──────────────────────────────────────────────────────────────── +# Directory containing the built frontend static files (served by the backend) +DISPATCH_WEB_DIR=/usr/share/dispatch/web + +# ─── Provider (OpenCode Go / OpenAI-compatible) ────────────────────────────── +DISPATCH_API_KEY=sk-... +DISPATCH_BASE_URL=https://opencode.ai/zen/go/v1 +DISPATCH_MODEL=deepseek-v4-flash + +# ─── Umans provider (optional, set key to enable) ──────────────────────────── +# UMANS_API_KEY=sk-... + +# ─── Data paths ────────────────────────────────────────────────────────────── +DISPATCH_DB=/var/lib/dispatch/dispatch.db +DISPATCH_TRACE_DB=/var/lib/dispatch/traces.db +DISPATCH_JOURNAL=/var/log/dispatch/app.ndjson + +# ─── Observability ─────────────────────────────────────────────────────────── +# Trace DB path (defaults to ./traces.db if unset) +# DISPATCH_TRACE_DB=/var/lib/dispatch/traces.db diff --git a/systemd/dispatch.service b/systemd/dispatch.service new file mode 100644 index 0000000..88e8352 --- /dev/null +++ b/systemd/dispatch.service @@ -0,0 +1,17 @@ +[Unit] +Description=Dispatch AI Agent Server +After=network.target + +[Service] +Type=simple +ExecStart=/usr/bin/dispatch-server +EnvironmentFile=/etc/dispatch/env +WorkingDirectory=/var/lib/dispatch +Restart=on-failure +RestartSec=5 +# Journal the output +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target |
