diff options
| author | Adam Malczewski <[email protected]> | 2026-05-27 23:17:18 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-27 23:17:18 +0900 |
| commit | 60f56367dfc1e3c5095d034bed4fb4f572e32b55 (patch) | |
| tree | aa4bc7be095e7030ce0b1a67e33b1abe4cc8c36a | |
| parent | 4f0ed4ed9456e30344228f0106f6bb104417da3d (diff) | |
| download | dispatch-60f56367dfc1e3c5095d034bed4fb4f572e32b55.tar.gz dispatch-60f56367dfc1e3c5095d034bed4fb4f572e32b55.zip | |
refactor(packaging): split into dispatch/dispatch-systemd/dispatch-s6, separate dispatch-electron, add bun-based frontend serve
PKGBUILD is now a split package producing three .pkg.tar.zst files in one
makepkg run:
- dispatch base application files (/opt/dispatch), CLI wrappers
(dispatch-api, dispatch-frontend), env configs
(/etc/dispatch/dispatch-api.conf,
/etc/dispatch/dispatch-frontend.conf)
- dispatch-systemd systemd user units for dispatch-api + dispatch-frontend
(conflicts with dispatch-s6)
- dispatch-s6 s6-rc service pipelines (-srv + -log) for both services
(conflicts with dispatch-systemd)
The Electron desktop wrapper moved to its own self-contained PKGBUILD at
packaging/electron/, producing dispatch-electron. It bundles its own copy of
the frontend dist + electron entry points under /opt/dispatch-electron and
does not depend on the base dispatch package, so users can install it
standalone and point VITE_API_URL at any backend at build time.
Files under packaging/ are now read directly from ${_projectdir}/packaging/
rather than via source=(); the two s6 service dirs share basenames (run,
type) which would collide inside ${srcdir}.
New frontend static server: packages/frontend/serve.ts uses Bun's built-in
HTTP server (no extra runtime deps) with SPA fallback to index.html and
path-traversal protection. PORT/HOST/DIST_DIR overridable via env. Exposed
as 'bun run --cwd packages/frontend serve' and via /usr/bin/dispatch-frontend.
Build scripts:
- bin/build-pkg now prints the freshest built packages
- bin/install-pkg installs dispatch + dispatch-systemd by default;
accepts package names or --all; searches both
packaging/ and packaging/electron/
- bin/build-pkg-electron new, builds the electron split
- bin/build-pkg-windows replaces bin/windows-pkg; drops the hard-coded
WSL copy step, just prints the win-unpacked path
.gitignore extended to cover packaging/*.tar.zst and packaging/electron/{src,pkg,*.pkg.tar.zst,*.tar.zst}.
36 files changed, 695 insertions, 100 deletions
@@ -13,4 +13,9 @@ references/ packaging/src/ packaging/pkg/ packaging/*.pkg.tar.zst +packaging/*.tar.zst +packaging/electron/src/ +packaging/electron/pkg/ +packaging/electron/*.pkg.tar.zst +packaging/electron/*.tar.zst packages/frontend/release/ diff --git a/bin/build-pkg b/bin/build-pkg index 904a0bb..d257efc 100755 --- a/bin/build-pkg +++ b/bin/build-pkg @@ -1,8 +1,23 @@ #!/usr/bin/env bash +# Build the dispatch Arch split package: dispatch + dispatch-systemd + dispatch-s6. +# One makepkg run produces three .pkg.tar.zst files in packaging/. +# +# Usage: +# bin/build-pkg # build +# bin/build-pkg --noconfirm # forward extra args to makepkg +# +# Override frontend build target with VITE_API_URL, e.g.: +# VITE_API_URL="https://api.example.com" bin/build-pkg + set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PACKAGING_DIR="$(dirname "$SCRIPT_DIR")/packaging" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +PACKAGING_DIR="$PROJECT_DIR/packaging" cd "$PACKAGING_DIR" makepkg -fd "$@" + +echo "" +echo "Built packages (newest first):" +ls -1t "$PACKAGING_DIR"/*.pkg.tar.zst 2>/dev/null | head -5 diff --git a/bin/build-pkg-electron b/bin/build-pkg-electron new file mode 100755 index 0000000..ceec321 --- /dev/null +++ b/bin/build-pkg-electron @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Build the dispatch-electron Arch package (Linux desktop wrapper). +# Depends on the `dispatch` package being built/installed at the matching version. +# +# Usage: +# bin/build-pkg-electron # build +# bin/build-pkg-electron --noconfirm + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +PACKAGING_DIR="$PROJECT_DIR/packaging/electron" + +cd "$PACKAGING_DIR" +makepkg -fd "$@" + +echo "" +echo "Built package:" +ls -1t "$PACKAGING_DIR"/*.pkg.tar.zst 2>/dev/null | head -1 diff --git a/bin/build-pkg-windows b/bin/build-pkg-windows new file mode 100755 index 0000000..c5760e0 --- /dev/null +++ b/bin/build-pkg-windows @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Build the Windows Electron output via electron-builder. +# Produces an unpacked directory at packages/frontend/release/win-unpacked. +# +# Usage: +# bin/build-pkg-windows +# +# Override the API URL the frontend bundle points to: +# VITE_API_URL="http://your-host:18390" bin/build-pkg-windows + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +FRONTEND_DIR="$PROJECT_DIR/packages/frontend" +VITE_API_URL="${VITE_API_URL:-http://localhost:18390}" + +cd "$PROJECT_DIR" + +# Install deps (electron + electron-builder live as frontend devDependencies) +bun install + +# Build the SPA bundle, then package it for Windows (unpacked dir) +cd "$FRONTEND_DIR" +VITE_API_URL="$VITE_API_URL" bun run dist:win + +WIN_BUILD="$FRONTEND_DIR/release/win-unpacked" + +if [ ! -d "$WIN_BUILD" ]; then + echo "Build failed: no win-unpacked directory found at $WIN_BUILD" >&2 + exit 1 +fi + +echo "" +echo "Windows Electron build ready at:" +echo " $WIN_BUILD" +echo "" +echo "Copy that folder to a Windows machine and run Dispatch.exe." diff --git a/bin/install-pkg b/bin/install-pkg index 89e55b2..72e218b 100755 --- a/bin/install-pkg +++ b/bin/install-pkg @@ -1,15 +1,68 @@ #!/usr/bin/env bash +# Install one or more built dispatch packages with yay -U. +# +# Default: installs the freshest dispatch + dispatch-systemd from packaging/. +# Pass package names (without version) to install a custom set. +# +# Usage: +# bin/install-pkg # dispatch + dispatch-systemd +# bin/install-pkg dispatch dispatch-s6 # dispatch + dispatch-s6 +# bin/install-pkg dispatch dispatch-electron # dispatch + electron wrapper +# bin/install-pkg --all # every freshest pkg found + set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PACKAGING_DIR="$(dirname "$SCRIPT_DIR")/packaging" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +PACKAGING_DIR="$PROJECT_DIR/packaging" +ELECTRON_DIR="$PROJECT_DIR/packaging/electron" + +# Find the most recent .pkg.tar.zst matching a package name in a given dir. +find_pkg() { + local name="$1" dir="$2" + ls -t "$dir"/"$name"-[0-9]*-x86_64.pkg.tar.zst 2>/dev/null | head -1 +} + +# Resolve a package name to its freshest tarball, searching both dirs. +resolve_pkg() { + local name="$1" path + path=$(find_pkg "$name" "$PACKAGING_DIR") + [ -n "$path" ] || path=$(find_pkg "$name" "$ELECTRON_DIR") + echo "$path" +} + +declare -a names +declare -a paths + +if [ $# -eq 0 ]; then + names=(dispatch dispatch-systemd) +elif [ "${1:-}" = "--all" ]; then + names=(dispatch dispatch-systemd dispatch-s6 dispatch-electron) +else + names=("$@") +fi -PKG=$(ls -t "$PACKAGING_DIR"/dispatch-[0-9]*-x86_64.pkg.tar.zst 2>/dev/null | head -1) +for name in "${names[@]}"; do + path=$(resolve_pkg "$name") + if [ -z "$path" ]; then + # --all is lenient: skip missing packages + if [ "${1:-}" = "--all" ]; then + echo "warn: no built package found for '$name', skipping" >&2 + continue + fi + echo "error: no built package found for '$name'" >&2 + echo " run bin/build-pkg (or bin/build-pkg-electron) first." >&2 + exit 1 + fi + paths+=("$path") +done -if [ -z "$PKG" ]; then - echo "No package found. Run bin/build-pkg first." >&2 +if [ ${#paths[@]} -eq 0 ]; then + echo "error: nothing to install." >&2 exit 1 fi -echo "Installing $PKG" -yay -U "$PKG" "$@" +echo "Installing:" +printf ' %s\n' "${paths[@]}" +echo "" +yay -U "${paths[@]}" diff --git a/bin/windows-pkg b/bin/windows-pkg deleted file mode 100755 index 693168c..0000000 --- a/bin/windows-pkg +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_DIR="$(dirname "$SCRIPT_DIR")" -FRONTEND_DIR="$PROJECT_DIR/packages/frontend" -DEST="/mnt/c/Users/micro/OneDrive/Documents/Dispatch" - -# Install deps (includes electron + electron-builder as devDependencies) -cd "$PROJECT_DIR" -bun install - -# Build frontend and package for Windows (use production port) -cd "$FRONTEND_DIR" -VITE_API_URL="http://localhost:18390" bun run dist:win - -# Find the output directory -WIN_BUILD=$(ls -d "$FRONTEND_DIR/release/win-unpacked" 2>/dev/null || true) - -if [ -z "$WIN_BUILD" ]; then - echo "Build failed: no win-unpacked directory found in release/" >&2 - exit 1 -fi - -# Copy to Windows Documents -rm -rf "$DEST" -mkdir -p "$DEST" -cp -r "$WIN_BUILD"/. "$DEST"/ - -echo "Windows build copied to: C:\\Users\\micro\\OneDrive\\Documents\\Dispatch" -echo "Run Dispatch.exe from that folder." diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 453df07..1d11524 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -8,6 +8,7 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", + "serve": "bun serve.ts", "test": "vitest run", "test:watch": "vitest", "typecheck": "svelte-check --tsconfig ./tsconfig.json", diff --git a/packages/frontend/serve.ts b/packages/frontend/serve.ts new file mode 100644 index 0000000..75b0a9f --- /dev/null +++ b/packages/frontend/serve.ts @@ -0,0 +1,66 @@ +// Static file server for the built frontend (packages/frontend/dist). +// Uses Bun's built-in HTTP server — no extra runtime dependencies. +// +// Environment variables: +// PORT — port to listen on (default: 18391) +// HOST — interface to bind to (default: 0.0.0.0) +// DIST_DIR — absolute path to the static assets directory +// (default: <this-file's-dir>/dist) +// +// SPA fallback: requests that don't match a file on disk fall back to +// index.html so client-side routing works. + +import { existsSync, statSync } from "node:fs"; +import { join, normalize, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const moduleDir = (() => { + try { + // @ts-expect-error — Bun provides import.meta.dir + return import.meta.dir as string; + } catch { + return resolve(fileURLToPath(import.meta.url), ".."); + } +})(); + +const port = Number(process.env.PORT) || 18391; +const host = process.env.HOST || "0.0.0.0"; +const distDir = resolve(process.env.DIST_DIR || join(moduleDir, "dist")); + +if (!existsSync(distDir) || !statSync(distDir).isDirectory()) { + console.error(`[dispatch-frontend] dist directory not found: ${distDir}`); + console.error("[dispatch-frontend] Build the frontend first (bun run --cwd packages/frontend build)"); + process.exit(1); +} + +const indexPath = join(distDir, "index.html"); + +function safeResolve(urlPath: string): string | null { + // Normalize and prevent path-traversal outside distDir. + const decoded = decodeURIComponent(urlPath); + const candidate = resolve(distDir, "." + normalize(decoded)); + if (!candidate.startsWith(distDir)) return null; + return candidate; +} + +Bun.serve({ + port, + hostname: host, + async fetch(req) { + const url = new URL(req.url); + let pathname = url.pathname; + if (pathname === "/" || pathname === "") pathname = "/index.html"; + + const resolved = safeResolve(pathname); + if (resolved && existsSync(resolved) && statSync(resolved).isFile()) { + return new Response(Bun.file(resolved)); + } + + // SPA fallback — serve index.html for unknown routes + return new Response(Bun.file(indexPath), { + headers: { "content-type": "text/html; charset=utf-8" }, + }); + }, +}); + +console.log(`[dispatch-frontend] serving ${distDir} on http://${host}:${port}`); diff --git a/packaging/PKGBUILD b/packaging/PKGBUILD index de49d4e..4ac87a8 100644 --- a/packaging/PKGBUILD +++ b/packaging/PKGBUILD @@ -1,30 +1,36 @@ # Maintainer: dispatch -pkgname=dispatch +# +# Split package: builds the base application once and produces three packages: +# dispatch — application files + CLI wrappers + env configs +# dispatch-systemd — systemd user units for dispatch-api / dispatch-frontend +# dispatch-s6 — s6 service definitions for dispatch-api / dispatch-frontend +# +# The Electron desktop wrapper lives in a separate PKGBUILD at packaging/electron/. +# +# Build with: +# bin/build-pkg # makepkg -fd in packaging/ + +pkgbase=dispatch +pkgname=('dispatch' 'dispatch-systemd' 'dispatch-s6') pkgver=0.0.1 pkgrel=1 -pkgdesc='AI Agent Dispatch Interface' arch=('x86_64') url='https://github.com/anomalyco/dispatch' license=('MIT') -depends=('electron' 'bun') -makedepends=() -install=dispatch.install -backup=('etc/dispatch/dispatch-api.conf') -source=( - 'dispatch-api.service' - 'dispatch-api.conf' - 'dispatch-electron-wrapper.sh' - 'dispatch-api-wrapper.sh' - 'dispatch.desktop' - 'dispatch.svg' -) -sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP') +makedepends=('bun') +# All static files are read directly from ${_projectdir}/packaging/. We don't +# use source=() because two of the s6 files share basenames (run, type), which +# would collide inside ${srcdir}. +source=() +sha256sums=() _projectdir="${startdir}/.." +_packagingdir="${_projectdir}/packaging" prepare() { mkdir -p "${srcdir}/dispatch-${pkgver}" - # Copy project files into the src build directory (use -a to preserve symlinks) + + # Copy project files into the src build directory (preserve symlinks) cp -a \ "${_projectdir}/packages" \ "${_projectdir}/package.json" \ @@ -32,7 +38,6 @@ prepare() { "${_projectdir}/tsconfig.base.json" \ "${srcdir}/dispatch-${pkgver}/" - # Copy dispatch.toml if it exists if [ -f "${_projectdir}/dispatch.toml" ]; then cp "${_projectdir}/dispatch.toml" "${srcdir}/dispatch-${pkgver}/" fi @@ -41,18 +46,30 @@ prepare() { build() { cd "${srcdir}/dispatch-${pkgver}" - # Install all deps (including devDependencies needed for vite build) + # Install all deps (including devDependencies for vite build) bun install --frozen-lockfile # Build the frontend with production API port - VITE_API_URL="http://localhost:18390" bun run --cwd packages/frontend build + VITE_API_URL="${VITE_API_URL:-http://localhost:18390}" \ + bun run --cwd packages/frontend build - # Reinstall with production only to slim down node_modules for packaging + # Slim node_modules for runtime rm -rf node_modules bun install --frozen-lockfile --production } -package() { +# ---------------------------------------------------------------------------- +# dispatch — application files, CLI wrappers, env configs +# ---------------------------------------------------------------------------- +package_dispatch() { + pkgdesc='AI Agent Dispatch — backend + frontend (application files)' + depends=('bun') + install=dispatch.install + backup=( + 'etc/dispatch/dispatch-api.conf' + 'etc/dispatch/dispatch-frontend.conf' + ) + cd "${srcdir}/dispatch-${pkgver}" local optdir="${pkgdir}/opt/dispatch" @@ -67,61 +84,112 @@ package() { install -dm755 "${optdir}/packages/core" cp -a packages/core/. "${optdir}/packages/core/" - # Frontend: built dist only + # Frontend: built dist + serve script + package.json (for workspace resolution) install -dm755 "${optdir}/packages/frontend/dist" cp -a packages/frontend/dist/. "${optdir}/packages/frontend/dist/" - - # Frontend: electron launcher files - install -dm755 "${optdir}/packages/frontend/electron" - cp -a packages/frontend/electron/. "${optdir}/packages/frontend/electron/" - - # Frontend: package.json (needed for workspace resolution) + install -Dm644 packages/frontend/serve.ts "${optdir}/packages/frontend/serve.ts" install -Dm644 packages/frontend/package.json "${optdir}/packages/frontend/package.json" # Runtime node_modules (preserve symlinks for bun workspaces) install -dm755 "${optdir}/node_modules" cp -a node_modules/. "${optdir}/node_modules/" - # Root manifest and lockfile + # Root manifest + lockfile install -Dm644 package.json "${optdir}/package.json" install -Dm644 bun.lock "${optdir}/bun.lock" - # Default config (if present) + # Optional default config if [ -f dispatch.toml ]; then install -Dm644 dispatch.toml "${optdir}/dispatch.toml" fi - # --- systemd user service --- - install -Dm644 \ - "${srcdir}/dispatch-api.service" \ - "${pkgdir}/usr/lib/systemd/user/dispatch-api.service" - - # --- Environment config (preserved across upgrades via backup=()) --- + # --- Environment configs (preserved across upgrades via backup=()) --- install -Dm644 \ - "${srcdir}/dispatch-api.conf" \ + "${_packagingdir}/dispatch-api.conf" \ "${pkgdir}/etc/dispatch/dispatch-api.conf" + install -Dm644 \ + "${_packagingdir}/dispatch-frontend.conf" \ + "${pkgdir}/etc/dispatch/dispatch-frontend.conf" - # --- Wrapper scripts --- - install -Dm755 \ - "${srcdir}/dispatch-electron-wrapper.sh" \ - "${pkgdir}/usr/bin/dispatch" - + # --- CLI wrappers --- install -Dm755 \ - "${srcdir}/dispatch-api-wrapper.sh" \ + "${_packagingdir}/dispatch-api-wrapper.sh" \ "${pkgdir}/usr/bin/dispatch-api" - - # --- Desktop integration --- - install -Dm644 \ - "${srcdir}/dispatch.desktop" \ - "${pkgdir}/usr/share/applications/dispatch.desktop" - - install -Dm644 \ - "${srcdir}/dispatch.svg" \ - "${pkgdir}/usr/share/icons/hicolor/scalable/apps/dispatch.svg" + install -Dm755 \ + "${_packagingdir}/dispatch-frontend-wrapper.sh" \ + "${pkgdir}/usr/bin/dispatch-frontend" # --- License --- if [ -f "${_projectdir}/LICENSE" ]; then install -Dm644 "${_projectdir}/LICENSE" \ - "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" + "${pkgdir}/usr/share/licenses/dispatch/LICENSE" fi } + +# ---------------------------------------------------------------------------- +# dispatch-systemd — systemd user unit files +# ---------------------------------------------------------------------------- +package_dispatch-systemd() { + pkgdesc='Systemd user units for the Dispatch API and Frontend services' + depends=("dispatch=${pkgver}" 'systemd') + conflicts=('dispatch-s6') + install=dispatch-systemd.install + + install -Dm644 \ + "${_packagingdir}/dispatch-api.service" \ + "${pkgdir}/usr/lib/systemd/user/dispatch-api.service" + + install -Dm644 \ + "${_packagingdir}/dispatch-frontend.service" \ + "${pkgdir}/usr/lib/systemd/user/dispatch-frontend.service" +} + +# ---------------------------------------------------------------------------- +# dispatch-s6 — s6 service definitions (for Artix / standalone s6) +# ---------------------------------------------------------------------------- +package_dispatch-s6() { + pkgdesc='s6-rc service definitions for the Dispatch API and Frontend services' + depends=("dispatch=${pkgver}" 's6' 's6-rc') + conflicts=('dispatch-systemd') + install=dispatch-s6.install + + # s6-rc layout: each service is split into a `-srv` (the daemon) and a + # `-log` (s6-log consumer), linked by producer-for/consumer-for/pipeline-name. + # The pipeline-name lets `s6-rc -u change dispatch-api` bring both up together. + + # --- dispatch-api-srv --- + install -Dm755 "${_packagingdir}/s6/dispatch-api-srv/run" \ + "${pkgdir}/etc/s6/sv/dispatch-api-srv/run" + install -Dm644 "${_packagingdir}/s6/dispatch-api-srv/type" \ + "${pkgdir}/etc/s6/sv/dispatch-api-srv/type" + install -Dm644 "${_packagingdir}/s6/dispatch-api-srv/producer-for" \ + "${pkgdir}/etc/s6/sv/dispatch-api-srv/producer-for" + + # --- dispatch-api-log --- + install -Dm755 "${_packagingdir}/s6/dispatch-api-log/run" \ + "${pkgdir}/etc/s6/sv/dispatch-api-log/run" + install -Dm644 "${_packagingdir}/s6/dispatch-api-log/type" \ + "${pkgdir}/etc/s6/sv/dispatch-api-log/type" + install -Dm644 "${_packagingdir}/s6/dispatch-api-log/consumer-for" \ + "${pkgdir}/etc/s6/sv/dispatch-api-log/consumer-for" + install -Dm644 "${_packagingdir}/s6/dispatch-api-log/pipeline-name" \ + "${pkgdir}/etc/s6/sv/dispatch-api-log/pipeline-name" + + # --- dispatch-frontend-srv --- + install -Dm755 "${_packagingdir}/s6/dispatch-frontend-srv/run" \ + "${pkgdir}/etc/s6/sv/dispatch-frontend-srv/run" + install -Dm644 "${_packagingdir}/s6/dispatch-frontend-srv/type" \ + "${pkgdir}/etc/s6/sv/dispatch-frontend-srv/type" + install -Dm644 "${_packagingdir}/s6/dispatch-frontend-srv/producer-for" \ + "${pkgdir}/etc/s6/sv/dispatch-frontend-srv/producer-for" + + # --- dispatch-frontend-log --- + install -Dm755 "${_packagingdir}/s6/dispatch-frontend-log/run" \ + "${pkgdir}/etc/s6/sv/dispatch-frontend-log/run" + install -Dm644 "${_packagingdir}/s6/dispatch-frontend-log/type" \ + "${pkgdir}/etc/s6/sv/dispatch-frontend-log/type" + install -Dm644 "${_packagingdir}/s6/dispatch-frontend-log/consumer-for" \ + "${pkgdir}/etc/s6/sv/dispatch-frontend-log/consumer-for" + install -Dm644 "${_packagingdir}/s6/dispatch-frontend-log/pipeline-name" \ + "${pkgdir}/etc/s6/sv/dispatch-frontend-log/pipeline-name" +} diff --git a/packaging/dispatch-electron-wrapper.sh b/packaging/dispatch-electron-wrapper.sh deleted file mode 100755 index df24713..0000000 --- a/packaging/dispatch-electron-wrapper.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -exec /usr/bin/electron --name="Dispatch" /opt/dispatch/packages/frontend/electron/main.cjs "$@" diff --git a/packaging/dispatch-frontend-wrapper.sh b/packaging/dispatch-frontend-wrapper.sh new file mode 100755 index 0000000..06aa9b0 --- /dev/null +++ b/packaging/dispatch-frontend-wrapper.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# dispatch-frontend-wrapper.sh +# Wrapper script for running the Dispatch Frontend static-file server +# outside of systemd / s6. +# Install to /usr/bin/dispatch-frontend (chmod 755). +# +# Usage: +# dispatch-frontend # runs the server, inheriting the current environment +# dispatch-frontend --help # passes flags through to bun + +set -euo pipefail + +DISPATCH_CONF="/etc/dispatch/dispatch-frontend.conf" +DISPATCH_DIR="/opt/dispatch" +BUN="/usr/bin/bun" +ENTRY_POINT="packages/frontend/serve.ts" + +# Source the environment file if it exists +if [[ -f "$DISPATCH_CONF" ]]; then + set -o allexport + # shellcheck source=/etc/dispatch/dispatch-frontend.conf + source "$DISPATCH_CONF" + set +o allexport +fi + +# Change to the application directory +cd "$DISPATCH_DIR" + +# Hand off to bun — exec replaces this process so signals are forwarded correctly +exec "$BUN" "$ENTRY_POINT" "$@" diff --git a/packaging/dispatch-frontend.conf b/packaging/dispatch-frontend.conf new file mode 100644 index 0000000..37d2984 --- /dev/null +++ b/packaging/dispatch-frontend.conf @@ -0,0 +1,21 @@ +# /etc/dispatch/dispatch-frontend.conf +# Environment configuration for the Dispatch Frontend static-file server. +# Sourced by systemd (EnvironmentFile=), the s6 run script, and the +# dispatch-frontend-wrapper.sh script for manual invocation. +# +# Lines beginning with '#' are comments and are ignored. +# Syntax: KEY=value (no spaces around '=', no 'export' keyword needed for systemd) + +# --------------------------------------------------------------------------- +# Server +# --------------------------------------------------------------------------- + +# Port the static file server listens on (default: 18391). +PORT=18391 + +# Interface to bind to (default: 0.0.0.0). +# HOST=0.0.0.0 + +# Directory containing the built frontend assets. +# Defaults to /opt/dispatch/packages/frontend/dist +# DIST_DIR=/opt/dispatch/packages/frontend/dist diff --git a/packaging/dispatch-frontend.service b/packaging/dispatch-frontend.service new file mode 100644 index 0000000..4aca1a3 --- /dev/null +++ b/packaging/dispatch-frontend.service @@ -0,0 +1,16 @@ +[Unit] +Description=Dispatch Frontend (static file server) +After=dispatch-api.service + +[Service] +Type=simple +WorkingDirectory=/opt/dispatch +ExecStart=/usr/bin/bun packages/frontend/serve.ts +EnvironmentFile=-/etc/dispatch/dispatch-frontend.conf +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=default.target diff --git a/packaging/dispatch-s6.install b/packaging/dispatch-s6.install new file mode 100644 index 0000000..efa4c9b --- /dev/null +++ b/packaging/dispatch-s6.install @@ -0,0 +1,46 @@ +post_install() { + install -dm755 -o tradam -g tradam /var/log/dispatch-api + install -dm755 -o tradam -g tradam /var/log/dispatch-frontend + + echo "" + echo "==> Dispatch s6-rc services installed at /etc/s6/sv/" + echo " Service pipelines: dispatch-api (= dispatch-api-srv | dispatch-api-log)" + echo " dispatch-frontend (= dispatch-frontend-srv | dispatch-frontend-log)" + echo " To enable and start:" + echo " s6 repository sync" + echo " s6 set enable -I pull dispatch-api-srv dispatch-api-log" + echo " s6 set enable -I pull dispatch-frontend-srv dispatch-frontend-log" + echo " s6 set commit -f" + echo " s6 live install" + echo " s6-rc -u change dispatch-api" + echo " s6-rc -u change dispatch-frontend" + echo "" +} + +post_upgrade() { + install -dm755 -o tradam -g tradam /var/log/dispatch-api 2>/dev/null || true + install -dm755 -o tradam -g tradam /var/log/dispatch-frontend 2>/dev/null || true + + # Remove any stale legacy service dirs from the old (pre-srv/-log split) + # layout, which would otherwise confuse s6 repository sync. + for legacy in /etc/s6/sv/dispatch-api /etc/s6/sv/dispatch-frontend; do + if [ -d "$legacy" ] && [ -d "$legacy/log" ] && [ ! -e "$legacy/producer-for" ]; then + rm -rf "$legacy" + fi + done + + echo "" + echo "==> Dispatch s6 services upgraded." + echo " Restart:" + echo " s6-rc -d change dispatch-api && s6-rc -u change dispatch-api" + echo " s6-rc -d change dispatch-frontend && s6-rc -u change dispatch-frontend" + echo "" +} + +pre_remove() { + echo "" + echo "==> Stopping dispatch services..." + s6-rc -d change dispatch-api 2>/dev/null || true + s6-rc -d change dispatch-frontend 2>/dev/null || true + echo "" +} diff --git a/packaging/dispatch-systemd.install b/packaging/dispatch-systemd.install new file mode 100644 index 0000000..3bd29be --- /dev/null +++ b/packaging/dispatch-systemd.install @@ -0,0 +1,24 @@ +post_install() { + echo "" + echo "==> Dispatch systemd user units installed." + echo " Enable and start:" + echo " systemctl --user daemon-reload" + echo " systemctl --user enable --now dispatch-api dispatch-frontend" + echo "" +} + +post_upgrade() { + echo "" + echo "==> Dispatch systemd units upgraded." + echo " Reload and restart:" + echo " systemctl --user daemon-reload" + echo " systemctl --user restart dispatch-api dispatch-frontend" + echo "" +} + +pre_remove() { + echo "" + echo "==> Stop dispatch services before removal:" + echo " systemctl --user disable --now dispatch-api dispatch-frontend" + echo "" +} diff --git a/packaging/dispatch.install b/packaging/dispatch.install index 7d1439f..76837f5 100644 --- a/packaging/dispatch.install +++ b/packaging/dispatch.install @@ -1,16 +1,31 @@ post_install() { echo "" - echo "==> Dispatch has been installed." - echo " Edit /etc/dispatch/dispatch-api.conf to configure the API." - echo " Then enable and start the service:" - echo " systemctl --user enable --now dispatch-api" + echo "==> Dispatch (base) has been installed." + echo " Application files: /opt/dispatch" + echo " Config:" + echo " /etc/dispatch/dispatch-api.conf" + echo " /etc/dispatch/dispatch-frontend.conf" + echo "" + echo " Run manually:" + echo " dispatch-api # backend (port 18390 by default)" + echo " dispatch-frontend # static frontend (port 18391 by default)" + echo "" + echo " To run as a service, install one of:" + echo " pacman -S dispatch-systemd # systemd user services" + echo " pacman -S dispatch-s6 # s6 services (Artix)" + echo "" + echo " For the Electron desktop wrapper:" + echo " pacman -S dispatch-electron" echo "" } post_upgrade() { echo "" - echo "==> Dispatch has been upgraded." - echo " You may need to restart the service:" - echo " systemctl --user restart dispatch-api" + echo "==> Dispatch (base) upgraded." + echo " If you use dispatch-systemd, restart the units:" + echo " systemctl --user restart dispatch-api dispatch-frontend" + echo " If you use dispatch-s6, restart the services:" + echo " s6-svc -r /run/service/dispatch-api" + echo " s6-svc -r /run/service/dispatch-frontend" echo "" } diff --git a/packaging/electron/PKGBUILD b/packaging/electron/PKGBUILD new file mode 100644 index 0000000..a259418 --- /dev/null +++ b/packaging/electron/PKGBUILD @@ -0,0 +1,100 @@ +# Maintainer: dispatch +# +# Electron desktop wrapper for Dispatch on Linux. +# +# Self-contained: bundles its own copy of the built frontend dist + electron +# entry points under /opt/dispatch-electron/. Does NOT depend on the `dispatch` +# package — install that separately (with dispatch-systemd or dispatch-s6) if +# you want to run the backend locally, or point VITE_API_URL at a remote +# instance at build time. +# +# Build with: +# bin/build-pkg-electron # makepkg -fd in packaging/electron/ +# +# Override the backend URL the bundled frontend talks to: +# VITE_API_URL="http://your-host:18390" bin/build-pkg-electron + +pkgname=dispatch-electron +pkgver=0.0.1 +pkgrel=1 +pkgdesc='Electron desktop wrapper for Dispatch (self-contained)' +arch=('x86_64') +url='https://github.com/anomalyco/dispatch' +license=('MIT') +depends=('electron') +makedepends=('bun') +install=dispatch-electron.install +source=() +sha256sums=() + +_projectdir="${startdir}/../.." +_packagingdir="${_projectdir}/packaging" +_electrondir="${_packagingdir}/electron" + +prepare() { + mkdir -p "${srcdir}/dispatch-electron-${pkgver}" + + # Copy project files into the src build directory (preserve symlinks) + cp -a \ + "${_projectdir}/packages" \ + "${_projectdir}/package.json" \ + "${_projectdir}/bun.lock" \ + "${_projectdir}/tsconfig.base.json" \ + "${srcdir}/dispatch-electron-${pkgver}/" +} + +build() { + cd "${srcdir}/dispatch-electron-${pkgver}" + + # Install all deps (including devDependencies for vite build) + bun install --frozen-lockfile + + # Build the SPA. VITE_API_URL is baked in at build time. + VITE_API_URL="${VITE_API_URL:-http://localhost:18390}" \ + bun run --cwd packages/frontend build +} + +package() { + cd "${srcdir}/dispatch-electron-${pkgver}" + + local optdir="${pkgdir}/opt/dispatch-electron" + + # --- Bundled SPA dist --- + install -dm755 "${optdir}/dist" + cp -a packages/frontend/dist/. "${optdir}/dist/" + + # --- Electron entry points --- + # main.cjs loads ../dist/index.html — so the dist must sit alongside electron/. + install -Dm644 \ + packages/frontend/electron/main.cjs \ + "${optdir}/electron/main.cjs" + install -Dm644 \ + packages/frontend/electron/preload.cjs \ + "${optdir}/electron/preload.cjs" + + # --- /usr/bin/dispatch (electron wrapper) --- + install -Dm755 \ + "${_electrondir}/dispatch-electron-wrapper.sh" \ + "${pkgdir}/usr/bin/dispatch" + + # --- Desktop integration --- + install -Dm644 \ + "${_electrondir}/dispatch.desktop" \ + "${pkgdir}/usr/share/applications/dispatch.desktop" + + install -Dm644 \ + "${_electrondir}/dispatch.svg" \ + "${pkgdir}/usr/share/icons/hicolor/scalable/apps/dispatch.svg" + + if [ -f "${_electrondir}/dispatch.png" ]; then + install -Dm644 \ + "${_electrondir}/dispatch.png" \ + "${pkgdir}/usr/share/icons/hicolor/512x512/apps/dispatch.png" + fi + + # --- License --- + if [ -f "${_projectdir}/LICENSE" ]; then + install -Dm644 "${_projectdir}/LICENSE" \ + "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" + fi +} diff --git a/packaging/electron/dispatch-electron-wrapper.sh b/packaging/electron/dispatch-electron-wrapper.sh new file mode 100755 index 0000000..948db3b --- /dev/null +++ b/packaging/electron/dispatch-electron-wrapper.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec /usr/bin/electron --name="Dispatch" /opt/dispatch-electron/electron/main.cjs "$@" diff --git a/packaging/electron/dispatch-electron.install b/packaging/electron/dispatch-electron.install new file mode 100644 index 0000000..9eea6b1 --- /dev/null +++ b/packaging/electron/dispatch-electron.install @@ -0,0 +1,32 @@ +post_install() { + echo "" + echo "==> Dispatch Electron desktop wrapper installed (self-contained)." + echo " Launch from your menu (Dispatch) or run:" + echo " dispatch" + echo "" + echo " The bundled frontend talks to the API URL baked in at build" + echo " time (default: http://localhost:18390). To run a local backend:" + echo " pacman -S dispatch dispatch-systemd # systemd" + echo " pacman -S dispatch dispatch-s6 # s6 / Artix" + echo "" + echo " To point the wrapper at a different backend, rebuild with:" + echo " VITE_API_URL=\"http://your-host:18390\" bin/build-pkg-electron" + echo "" + + if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications 2>/dev/null || true + fi + if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -q /usr/share/icons/hicolor 2>/dev/null || true + fi +} + +post_upgrade() { + post_install +} + +post_remove() { + if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications 2>/dev/null || true + fi +} diff --git a/packaging/dispatch.desktop b/packaging/electron/dispatch.desktop index 0ed44af..0ed44af 100644 --- a/packaging/dispatch.desktop +++ b/packaging/electron/dispatch.desktop diff --git a/packaging/dispatch.png b/packaging/electron/dispatch.png Binary files differindex 84d743a..84d743a 100644 --- a/packaging/dispatch.png +++ b/packaging/electron/dispatch.png diff --git a/packaging/dispatch.svg b/packaging/electron/dispatch.svg index 2aae97f..2aae97f 100644 --- a/packaging/dispatch.svg +++ b/packaging/electron/dispatch.svg diff --git a/packaging/s6/dispatch-api-log/consumer-for b/packaging/s6/dispatch-api-log/consumer-for new file mode 100644 index 0000000..8a696f4 --- /dev/null +++ b/packaging/s6/dispatch-api-log/consumer-for @@ -0,0 +1 @@ +dispatch-api-srv diff --git a/packaging/s6/dispatch-api-log/pipeline-name b/packaging/s6/dispatch-api-log/pipeline-name new file mode 100644 index 0000000..9dab7e6 --- /dev/null +++ b/packaging/s6/dispatch-api-log/pipeline-name @@ -0,0 +1 @@ +dispatch-api diff --git a/packaging/s6/dispatch-api-log/run b/packaging/s6/dispatch-api-log/run new file mode 100644 index 0000000..5d1b821 --- /dev/null +++ b/packaging/s6/dispatch-api-log/run @@ -0,0 +1,6 @@ +#!/bin/sh +# Logger for dispatch-api. Reads stdout from dispatch-api-srv and writes to +# /var/log/dispatch-api/ using s6-log (auto-rotation, timestamping). + +exec /usr/bin/s6-setuidgid tradam \ + /usr/bin/s6-log -b -d3 -- n10 s10000000 T /var/log/dispatch-api diff --git a/packaging/s6/dispatch-api-log/type b/packaging/s6/dispatch-api-log/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/packaging/s6/dispatch-api-log/type @@ -0,0 +1 @@ +longrun diff --git a/packaging/s6/dispatch-api-srv/producer-for b/packaging/s6/dispatch-api-srv/producer-for new file mode 100644 index 0000000..88f7df2 --- /dev/null +++ b/packaging/s6/dispatch-api-srv/producer-for @@ -0,0 +1 @@ +dispatch-api-log diff --git a/packaging/s6/dispatch-api-srv/run b/packaging/s6/dispatch-api-srv/run new file mode 100644 index 0000000..5754e3d --- /dev/null +++ b/packaging/s6/dispatch-api-srv/run @@ -0,0 +1,29 @@ +#!/bin/sh +# dispatch-api service — runs the Dispatch backend API as the `tradam` user. +# stdout/stderr are piped to dispatch-api-log via the s6-rc pipeline. + +DISPATCH_CONF="/etc/dispatch/dispatch-api.conf" +DISPATCH_DIR="/opt/dispatch" + +if [ -f "$DISPATCH_CONF" ]; then + set -a + . "$DISPATCH_CONF" + set +a +fi + +cd "$DISPATCH_DIR" || exit 1 + +# Merge stderr into stdout so both get logged by the consumer. +exec 2>&1 + +# Drop privileges to tradam and run bun. +exec /usr/bin/s6-setuidgid tradam \ + /usr/bin/env \ + HOME=/home/tradam \ + USER=tradam \ + LOGNAME=tradam \ + PATH=/usr/local/bin:/usr/bin:/bin \ + NODE_ENV="${NODE_ENV:-production}" \ + PORT="${PORT:-18390}" \ + OPENCODE_API_KEY="${OPENCODE_API_KEY:-}" \ + /usr/bin/bun packages/api/src/index.ts diff --git a/packaging/s6/dispatch-api-srv/type b/packaging/s6/dispatch-api-srv/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/packaging/s6/dispatch-api-srv/type @@ -0,0 +1 @@ +longrun diff --git a/packaging/s6/dispatch-frontend-log/consumer-for b/packaging/s6/dispatch-frontend-log/consumer-for new file mode 100644 index 0000000..f941d04 --- /dev/null +++ b/packaging/s6/dispatch-frontend-log/consumer-for @@ -0,0 +1 @@ +dispatch-frontend-srv diff --git a/packaging/s6/dispatch-frontend-log/pipeline-name b/packaging/s6/dispatch-frontend-log/pipeline-name new file mode 100644 index 0000000..0ee914b --- /dev/null +++ b/packaging/s6/dispatch-frontend-log/pipeline-name @@ -0,0 +1 @@ +dispatch-frontend diff --git a/packaging/s6/dispatch-frontend-log/run b/packaging/s6/dispatch-frontend-log/run new file mode 100644 index 0000000..4dda559 --- /dev/null +++ b/packaging/s6/dispatch-frontend-log/run @@ -0,0 +1,6 @@ +#!/bin/sh +# Logger for dispatch-frontend. Reads stdout from dispatch-frontend-srv and +# writes to /var/log/dispatch-frontend/ using s6-log. + +exec /usr/bin/s6-setuidgid tradam \ + /usr/bin/s6-log -b -d3 -- n10 s10000000 T /var/log/dispatch-frontend diff --git a/packaging/s6/dispatch-frontend-log/type b/packaging/s6/dispatch-frontend-log/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/packaging/s6/dispatch-frontend-log/type @@ -0,0 +1 @@ +longrun diff --git a/packaging/s6/dispatch-frontend-srv/producer-for b/packaging/s6/dispatch-frontend-srv/producer-for new file mode 100644 index 0000000..c29eb09 --- /dev/null +++ b/packaging/s6/dispatch-frontend-srv/producer-for @@ -0,0 +1 @@ +dispatch-frontend-log diff --git a/packaging/s6/dispatch-frontend-srv/run b/packaging/s6/dispatch-frontend-srv/run new file mode 100644 index 0000000..3411476 --- /dev/null +++ b/packaging/s6/dispatch-frontend-srv/run @@ -0,0 +1,25 @@ +#!/bin/sh +# dispatch-frontend service — serves the built frontend assets as the `tradam` user. +# stdout/stderr are piped to dispatch-frontend-log via the s6-rc pipeline. + +DISPATCH_CONF="/etc/dispatch/dispatch-frontend.conf" +DISPATCH_DIR="/opt/dispatch" + +if [ -f "$DISPATCH_CONF" ]; then + set -a + . "$DISPATCH_CONF" + set +a +fi + +cd "$DISPATCH_DIR" || exit 1 + +exec 2>&1 + +exec /usr/bin/s6-setuidgid tradam \ + /usr/bin/env \ + HOME=/home/tradam \ + USER=tradam \ + LOGNAME=tradam \ + PATH=/usr/local/bin:/usr/bin:/bin \ + PORT="${PORT:-18391}" \ + /usr/bin/bun packages/frontend/serve.ts diff --git a/packaging/s6/dispatch-frontend-srv/type b/packaging/s6/dispatch-frontend-srv/type new file mode 100644 index 0000000..5883cff --- /dev/null +++ b/packaging/s6/dispatch-frontend-srv/type @@ -0,0 +1 @@ +longrun |
