summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-21 20:26:39 +0900
committerAdam Malczewski <[email protected]>2026-06-21 20:26:39 +0900
commit4434fbf951d06a721597e805094069477851178e (patch)
tree4d3103ccb0512ff77d9d472d7b4178c11c8b73b3
parentac69e5f8bea9377887a6f89ff362c6be0db1c874 (diff)
downloaddispatch-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-xbin/build56
-rwxr-xr-xbin/install119
-rw-r--r--packages/transport-http/src/app.ts27
-rw-r--r--packages/transport-http/src/extension.ts3
-rw-r--r--systemd/dispatch.env29
-rw-r--r--systemd/dispatch.service17
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