Add the self-hosted installer and release workflows
CI / check (push) Canceled after 0s

This commit is contained in:
2026-10-20 07:02:58 +01:00
parent 1208eac6b7
commit d0798a93a5
12 changed files with 991 additions and 233 deletions
+1
View File
@@ -15,3 +15,4 @@ db/.exports
coverage
logs
*.log
assets
+3 -15
View File
@@ -1,8 +1,11 @@
# Development settings. The installer writes its own .env; see README.md.
PORT=7227
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# SMTP host, user and password
EMAIL_DOMAIN=
EMAIL_USER=
EMAIL_PASS=
@@ -14,29 +17,14 @@ DEV_EMAIL_RECIPIENT=
ENCRYPTION_KEY=
ENCRYPTION_SALT=
SOURCE_DIRECTORY=/home/user/projects/bunext-sqlite
DEPLOY_SERVER_IP=192.168.5.27
DATA_DIR=/data
# Set to 1 on the one container that should run cron tasks in production
MAIN_SERVER=
# Web Push: generated into Settings > Environment on first start. Set a
# fixed pair here only if you want to keep one across installs.
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
# Optional public envs exposed to the client through __root.server.ts
PAYSTACK_PUBLIC_KEY=
R2_PUBLIC_DOMAIN=
# Seed script overrides (bun run seed)
SEED_SUPER_ADMIN_EMAIL=
SEED_SUPER_ADMIN_USERNAME=
SEED_SUPER_ADMIN_FIRST_NAME=
SEED_SUPER_ADMIN_LAST_NAME=
SEED_SUPER_ADMIN_PASSWORD=
# Production: sitemap lastmod cache file on the /data volume
PAGE_LASTMOD_FILE=
+35
View File
@@ -0,0 +1,35 @@
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.3
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Check installer syntax
run: bash -n install.sh
- name: Type-check
run: bun run typecheck
- name: Test
run: bun run test
+82
View File
@@ -0,0 +1,82 @@
name: Release image
on:
release:
types: [published]
workflow_dispatch:
permissions:
contents: read
packages: write
concurrency:
group: release-image
cancel-in-progress: false
jobs:
image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Gitea
uses: docker/login-action@v3
with:
registry: git.tben.me
username: ${{ secrets.GITEA_USERNAME }}
password: ${{ secrets.GITEA_TOKEN }}
- name: Work out image tags
id: tags
env:
EVENT: ${{ github.event_name }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
sha="sha-${GITHUB_SHA::12}"
if [ "$EVENT" = "release" ] && [ -n "$RELEASE_TAG" ]; then
version="$RELEASE_TAG"
minor="$(printf '%s' "$version" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/\1.\2/')"
tags="$(printf '%s\n' \
"ghcr.io/moduletrace/cobble:latest" \
"ghcr.io/moduletrace/cobble:${version}" \
"ghcr.io/moduletrace/cobble:${minor}" \
"ghcr.io/moduletrace/cobble:${sha}" \
"git.tben.me/moduletrace/cobble:latest" \
"git.tben.me/moduletrace/cobble:${version}" \
"git.tben.me/moduletrace/cobble:${minor}" \
"git.tben.me/moduletrace/cobble:${sha}")"
else
tags="$(printf '%s\n' \
"ghcr.io/moduletrace/cobble:${sha}" \
"git.tben.me/moduletrace/cobble:${sha}")"
fi
{
echo "tags<<EOF"
echo "$tags"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.tags.outputs.tags }}
provenance: false
sbom: false
cache-from: type=gha
cache-to: type=gha,mode=max
+6 -2
View File
@@ -111,9 +111,13 @@ Inspect live data with `bun llm-code-assistant/query-db.ts "<sql>"` - a read-onl
- **Schedules (routines in the UI):** `cron_jobs` (per user and bot, `workspace_id` null for its own chat, `title`, `instructions` (the brief), `schedule` (a `Bun.cron` expression, UTC), `computer_id` (not a foreign key; null runs on the bot's current one), `enabled`, `max_runs` (1 for a one-off), `run_count`, `next_run_at`, `last_run_at`, `last_job_id`, `last_error`). Helpers in `src/functions/backend/db/cron-jobs/` (`create-cron-job`, `update-cron-job`, `delete-cron-job`, `grab-cron-jobs`, `grab-user-cron-job`, `fire-cron-job`), all ownership-checked, and with `bot_id` limited to that bot's own. `nextCronRun` (`src/utils/next-cron-run.ts`, `Bun.cron.parse`) validates a schedule and refuses one that runs more often than `AppData.CronJobMinIntervalMs`; a bot has at most `AppData.CronJobMaxPerBot`. Every bot gets `create_schedule`, `list_schedules`, `update_schedule`, `delete_schedule` (`grabBotTools`, every run; descriptions via `describe-cron-job.ts`). The `run-cron-jobs` cron fires due ones (`fireCronJob`: claims the run atomically by moving `next_run_at`, skips it while the last run's job is still open, queues the brief as a task with `enqueueBotJob` in the bot's active session there, turns off after `max_runs` or when there's no next run, keeps why it couldn't in `last_error`, logs each run under `cron`). A run missed while the server was down runs once, then carries on from now. `/admin/routines` lists them (pause/resume, edit, delete; `api/admin/routines/update-routine`, `delete-routine`); bots create them from chat.
- **Not built yet:** task steps (nothing writes `bot_task_steps` yet).
### Production container
### Installer and release
`deploy/run-app-container.sh` builds the image and (re)runs the app's container (`deploy/deploy-prod.sh` calls it on the server); rerun it to update. The app container and every computer container share the `cobble` network. Mounts: `.env` at `/app/.env` (read only; bun loads it), the host data dir (`COBBLE_DATA_DIR`, default `/data/cobble`) at `/data`, and the docker socket. Everything that must survive an update lives on `/data`: media and computers' home dirs (`DATA_DIR=/data`), the database with its backups and exports (`DB_DIR=/data/db`, read by `bun-sqlite.config.ts`; the schema always comes from `db/schema.ts`), and bun-sqlite's live schema record (`entrypoint.sh` links `/app/.bun-sqlite` there). `MAIN_SERVER=1` is set on that one container.
`install.sh` is the only supported install and update path: `curl -fsSL https://raw.githubusercontent.com/moduletrace/cobble/main/install.sh | sudo bash`. It is idempotent, so the same script is the updater, and `--uninstall [--purge]` is the way back out. It installs Docker when missing (Linux), creates the `cobble` network, writes `$COBBLE_HOME/install.conf` and a generated `.env` (mode 600), and starts one `cobble` container. Existing secrets are never regenerated; the database is backed up to `$COBBLE_HOME/backups` before a container is replaced. The default home is `/opt/cobble`, configurable with `--home`, and everything that must survive an update lives in its `data/` directory (`db`, `media`, `computers`).
The container runs with `--network cobble`, `-p BIND:PORT`, `.env` at `/app/.env` read only, the host data directory at `/data`, the docker socket at `/var/run/docker.sock`, `MAIN_SERVER=1` and `PORT`, `--init`, a `unless-stopped` restart policy and bounded json-file logs. `/data` must stay a bind mount: `resolveHostPath` turns paths under the app's own mounts into host paths for computer containers, and a named volume would break them. `DATA_DIR=/data` and `DB_DIR=/data/db` come from the Dockerfile; `entrypoint.sh` keeps bun-sqlite's schema record on the same volume.
Images are built by GitHub Actions on a published release (`.github/workflows/release.yml`), for `linux/amd64` and `linux/arm64`, and pushed to both `ghcr.io/moduletrace/cobble` and `git.tben.me/moduletrace/cobble` with `latest`, version, `major.minor` and commit-sha tags. `.github/workflows/ci.yml` runs `bun run typecheck` and `bun run test` on every push and pull request, and checks that `install.sh` parses. The Dockerfile pins Bun 1.4.3 to match CI. The repository is mirrored on Gitea, and the installer defaults to the GHCR image with `--image git.tben.me/moduletrace/cobble` as the Gitea alternative.
### Envs
+6 -1
View File
@@ -1,4 +1,9 @@
FROM oven/bun:1.3.14-debian
FROM oven/bun:1.4.3-debian
LABEL org.opencontainers.image.title="Cobble" \
org.opencontainers.image.description="A self-hosted crew of small AI worker bots." \
org.opencontainers.image.source="https://github.com/Moduletrace/cobble" \
org.opencontainers.image.licenses="MIT"
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl bash nano wget zip unzip \
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Moduletrace
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+131 -90
View File
@@ -1,126 +1,167 @@
# bunext-sqlite
# Cobble
A preset for [bunext](https://github.com/moduletrace/bunext) running [bun-sqlite](https://github.com/moduletrace/bun-sqlite) as the database management system. Use this as the starting point for new full-stack Bun projects: filesystem routing, SQLite with generated types, password login, an admin CRUD API, WebSockets, cron, audit logging, and email all wired up out of the box.
A self-hosted crew of small AI worker bots.
## Stack
Cobble gives you a coordinator bot and a crew of workers, each with its own
job, models, memory and workspace. The crew talks to you in a chat, shares
workspaces, runs routines on a schedule, and drives its own computers:
Docker sandboxes with a browser, terminal, code editor and desktop that a
bot can use and hand back to you when it needs help.
| Layer | Choice |
| --------- | -------------------------------------------------- |
| Runtime | Bun (never npm/npx - use `bun`) |
| Framework | `@moduletrace/bunext` (filesystem routing, SSR) |
| Database | `@moduletrace/bun-sqlite` (schema + generated types) |
| UI | Tailwind CSS v4 + `twui` component library |
| Icons | `lucide-react` |
| Validation| `zod` (v4) |
| Email | `nodemailer` (React-rendered templates) |
It is an install, not a SaaS. One SQLite database, one Docker container, and
your data on your own machine.
## Requirements
- Website: [cobble.moduletrace.org](https://cobble.moduletrace.org)
- Source: [GitHub](https://github.com/Moduletrace/cobble) and [Gitea](https://git.tben.me/Moduletrace/cobble)
- License: MIT
- Bun >= 1.3
- No database server required - `bun-sqlite` manages a local SQLite file at `db/<db_name>` (`db/bsqlite` by default).
## Install
## Local Development
On a Linux machine with Docker (the installer adds Docker when it is
missing):
1. `cp .env.example .env` (if present) or create `.env` with `ENCRYPTION_KEY`/`ENCRYPTION_SALT` values.
2. `bun install`
3. `bun run db:schema` - creates tables and regenerates `db/types/db.ts`.
4. `bun run dev` - starts the dev server (default port `7227`).
5. Open the app. With no accounts yet, every page sends you to `/auth/signup` to create the super admin (user #1). That is the only time signup is shown.
```bash
curl -fsSL https://raw.githubusercontent.com/moduletrace/cobble/main/install.sh | sudo bash
```
Headless alternative to step 5: `SEED_SUPER_ADMIN_PASSWORD=... bun run seed` creates the super admin from the CLI (default `[email protected]`, override with `SEED_SUPER_ADMIN_EMAIL`, `SEED_SUPER_ADMIN_USERNAME`, `SEED_SUPER_ADMIN_FIRST_NAME`, `SEED_SUPER_ADMIN_LAST_NAME`).
Open the address it prints, usually `http://<your-ip>:7227`, and create the
super admin account. The first signup also creates the coordinator and the
main computer, which pulls the sandbox image in the background. Give it a
few minutes.
Log in at `/auth/login` with your email or username and password.
Then open **Settings > Models**, add an OpenRouter key (or another provider)
and wait for the model list to sync. This is what the bots think with.
## Available Scripts
### Requirements
| Purpose | Command |
| ----------------------------- | ---------------------------------- |
| Dev server | `bun run dev` |
| Production | `bun run start` |
| Type-check only | `bun run typecheck` |
| Run tests | `bun run test` |
| Run a single test | `bun test <path/to/file.test.ts>` |
| Regenerate favicon/brand | `bun run gen:favicon` |
| DB schema (tables) | `bun run db:schema` |
| DB admin UI | `bun run db:admin` |
| DB backup | `bun run db:backup` |
| Seed super admin user | `bun run seed` |
| Query DB (CLI) | `bun llm-code-assistant/query-db.ts "<sql>"` |
| Deploy to production | `./deploy/deploy-prod.sh` |
- Linux amd64 or arm64, with Docker. The installer installs Docker from
get.docker.com when it is missing.
- 2 CPU cores, 8 GB RAM and 20 GB free disk for the app plus one computer.
Each computer is its own container with its own memory limit.
- Docker Desktop on macOS works for development and small installs, but
Linux is the supported host.
- Port 7227, or another port with `--port`.
- HTTPS in front of the app before you expose it to the internet. Web Push
and installable PWA features need it. A Cloudflare Tunnel or Caddy in
front of `127.0.0.1:7227` is enough.
## App Structure
The installer binds to `0.0.0.0` so the app is reachable from your phone on
the same network. Use `--bind 127.0.0.1` when a reverse proxy is the only
thing that should reach it.
| Directory | Purpose |
| ------------------------------ | ------------------------------------------------------------- |
| `db/schema.ts` | SQLite schema - the source of truth for `db/types/db.ts` |
| `src/server.ts` | Entrypoint: `bunextInit()`, `Bun.serve` (media, `/ws` auth'd upgrade), cron startup |
| `src/pages/` | Filesystem routes (`__root.server.ts` auth guard, `__root.tsx` layout/context) |
| `src/pages/api/` | API routes - incl. the CRUD catch-all at `api/admin/crud/[[...paths]].ts` |
| `src/websocket/` | WebSocket handler: upgrade-time auth, room system, event dispatch |
| `src/cron/` | Cron tasks - `Bun.cron` schedules, folder-per-task |
| `@moduletrace/twui` | The `twui` component library (external package, shared across projects) |
| `src/components/general/` | Shared page components built on twui |
| `src/functions/backend/` | Server-only logic: auth, DB queries, email, PDF generation, media, logger |
| `src/functions/frontend/` | Client-safe: CRUD/fetch handlers, media upload helpers, document actions |
| `src/hooks/` | Shared React hooks (`use-admin-crud-get`, `use-form-init`, ...) |
| `src/schemas/` | zod request schemas for public/auth endpoints |
| `src/utils/` | Utility modules: cookies, crypt, safe-compare, ejson, money |
| `src/utils/rbac/` | RBAC engine: pass-through users + declarative assignment matrix |
| `src/dict/` | Dictionaries: user types (+ assignment matrices), media types, document statuses, websocket rooms, log categories |
| `src/data/` | Static data: `site-data.ts` (brand literals), `app-data.ts` (constants) |
| `src/scripts/` | CLI scripts: brand asset generator, seed |
| `llm-code-assistant/` | `query-db.ts` - read-only SQL CLI for coding assistants |
## Update and uninstall
## Architecture
The same script is the updater. It keeps the database, media, computers'
home directories and generated secrets, backs up the database before
replacing the container, and does nothing when the image has not changed.
### Auth
```bash
curl -fsSL https://raw.githubusercontent.com/moduletrace/cobble/main/install.sh | sudo bash
```
- **Auth guard:** `src/pages/__root.server.ts` - runs per request. There are no public pages: anything outside `/admin` and `/auth` redirects to `/admin`. With no accounts, everything redirects to `/auth/signup`; once one exists, signup redirects away. `/admin/*` requires login; `/auth/*` redirects logged-in users.
- **Session:** encrypted cookie (`AES-256-GCM`, PBKDF2-derived key) + a matching CSRF cookie, verified constant-time on every request (`src/functions/backend/auth/user-auth.ts`). 60-day expiry.
- **Login:** email or username plus password (`api/auth/password-login.ts`), no email step. Set/change password at `/admin/settings/change-password`. Google login (`api/auth/google-login.ts`) works for existing accounts only when `GOOGLE_CLIENT_ID` is set; it never creates accounts.
- **Signup:** first-run only (`api/auth/signup.tsx`): creates the super admin with a password and logs them in. Refused once any user exists.
- **RBAC:** `src/utils/rbac/` - `super_admin` passes everything; other grants/revocations are governed by `UserTypeAssignmentMatrix`/`UserTypeDeletionMatrix` in `src/dict/user-types-dict.ts`.
- **Audit trail:** every auth event (signup, login, failure, logout) is written to the `logs` table via `src/functions/backend/logger.ts`.
```bash
# Stop the app and keep the data
curl -fsSL https://raw.githubusercontent.com/moduletrace/cobble/main/install.sh | sudo bash -s -- --uninstall
### Admin CRUD API
# Stop the app and delete the data
curl -fsSL https://raw.githubusercontent.com/moduletrace/cobble/main/install.sh | sudo bash -s -- --uninstall --purge
```
`api/admin/crud/[[...paths]].ts` dispatches `GET/POST/PUT/DELETE` by table name. Tables without a custom handler fall through to generic CRUD (super admin only). Customize a table by adding a folder under `src/pages/api/admin/crud/(tables)/<table>/` with per-method authorization.
### Options
### Documents (Letters / Emails / Invoices / Quotes)
| Option | Meaning |
| --------------------- | -------------------------------------------------------------- |
| `--home DIR` | Install directory, default `/opt/cobble` |
| `--data-dir DIR` | Data directory mounted at `/data`, default `HOME/data` |
| `--port PORT` | Host port, default `7227` |
| `--bind ADDRESS` | Host address to publish on, default `0.0.0.0` |
| `--image IMAGE` | Container image, default `ghcr.io/moduletrace/cobble` |
| `--version TAG` | Image tag, default `latest` |
| `--extra-network NET` | Join another Docker network, for a tunnel or reverse proxy |
| `--force` | Recreate the container even when it is current |
| `--no-pull` | Use an image that is already on the machine |
| `--skip-backup` | Update without a database backup |
| `--no-docker-install` | Fail instead of installing Docker |
| `--uninstall` | Stop and remove the app container |
| `--purge` | With `--uninstall`, also delete the data directory |
A complete document suite ships on top of the generic CRUD: `letters`, `emails`, `email_attachments`, `invoices`, `invoice_items`, `quotes`, `quote_items`, `invoice_quotes` tables with admin sections at `/admin/{letters,emails,invoices,quotes}`. Invoices and quotes get auto-generated numbers (from `SiteData["SiteSlug"]`), sortable line items, quote-to-invoice linking, branded PDF generation (`src/functions/backend/pdf/`), and sending via React-rendered HTML email (`src/functions/backend/email/`, shell in `src/email/root.tsx`). See `AGENTS.md` for the full map.
## Data and settings
### Backups
Everything that must survive an update lives under the install directory:
Super admins can create, list, download, and restore database snapshots from `/admin/backups` (`src/functions/backend/backups/`, API under `api/admin/backups/`). Backups are written to `db/.backups`; restore accepts a listed snapshot or an uploaded SQLite file. Every action is audit-logged.
```
/opt/cobble/
.env # generated once, mode 600
install.conf # installer state
backups/ # pre-update database snapshots
data/
db/cobble
db/.backups/
media/
computers/<id>/ # each computer's home directory
```
### Settings
`.env` holds the port and the encryption keys. The keys encrypt sessions and
settings, so keep a copy and do not rotate them without re-entering provider
keys and other secrets. Provider keys, MCP servers, tools and app settings
are managed in the app, not in `.env`.
`/admin/settings` redirects to `/admin/settings/general` (theme: System, Light or Dark, saved per device). `/admin/settings/profile` is the current user's profile (editable by every logged-in user, including `read_only`). `/admin/settings/change-password` sets or changes the user's own password. The sub pages are listed in the rail (`src/dict/settings-pages-dict.ts`).
Edit `.env` and run `docker restart cobble` to apply changes. The installer
only appends missing values and keeps what is already there.
### WebSockets
The app container is started with the Docker socket mounted and
`MAIN_SERVER=1`, so it can create computer containers and run cron, the
queue and Web Push.
`/ws` upgrades only after `userAuth` succeeds; the user session is frozen into the socket. Rooms are declared in `src/dict/websocket-rooms.ts` and managed by the dual-index store in `src/websocket/(data)/rooms.ts`. Client events dispatch through `src/websocket/(functions)/handle-message.ts`; add per-event handlers as `src/websocket/client-requests/<event>/index.ts`. The twui `useWebSocket` hook is the client counterpart.
## Images
### Cron
Every release publishes a multi-arch image to both registries:
`src/cron/index.ts` registers tasks (gated to the `MAIN_SERVER` container in production). Each task is a folder with a default-exported async function scheduled with `Bun.cron` through `registerCronJob` (`src/cron/register-cron-job.ts`), which logs before each run and catches errors. Schedules are interpreted in UTC. The bundled heartbeat task doubles as a DB-liveness probe.
- `ghcr.io/moduletrace/cobble`
- `git.tben.me/moduletrace/cobble`
## UI Conventions
Tags are `latest`, the release version, `major.minor` and the commit sha.
Install from Gitea with
`--image git.tben.me/moduletrace/cobble`, or build from source with
`docker build -t cobble:local .` and `--image cobble --version local --no-pull`.
- Build with `twui` components (`Section`, `Container`, `Row`, `Stack`, `Card`, `Button`, ...) and `src/components/general/` - never raw HTML for layout.
- Themable tokens live only in `src/styles/main.css` (`@theme inline`). No hardcoded hex colors or radii in components.
- Brand name/slug live only in `src/data/site-data.ts`; the logo mark only in `public/logos/logo.svg`.
## Development
## Page Directory Rules
```bash
cp .env.example .env
# set ENCRYPTION_KEY and ENCRYPTION_SALT (openssl rand -hex 32)
bun install
bun run db:schema
bun run dev
```
Route-local code under `src/pages` uses non-routed meta folders: `(sections)`, `(partials)`, `(data)`, `(functions)`, `(utils)`, `(hooks)`. Keep route files thin. Dynamic routes use `[param]`; catch-alls use `[[...paths]]`. Server functions live in `index.server.ts` beside `index.tsx`.
Open `http://localhost:7227`. With no users, every page goes to
`/auth/signup` to create the super admin. For a headless setup:
## Verification
```bash
SEED_SUPER_ADMIN_PASSWORD=... bun run seed
```
`bun run typecheck` and `bun run test` are the verification steps (no linter/formatter configured). CI runs both on every push and pull request (`.github/workflows/ci.yml`). Unit tests are colocated `*.test.ts` files run individually with `bun test <file>`.
| Command | Purpose |
| -------------------- | ------------------------------------ |
| `bun run dev` | Development server |
| `bun run start` | Production server on the host |
| `bun run typecheck` | Type-check |
| `bun run test` | Run the test suite |
| `bun run seed` | Create the super admin from the CLI |
| `bun run seed:bots` | Add a sample crew |
| `bun run db:schema` | Sync the schema and types |
| `bun run gen:favicon` | Regenerate favicons and PWA assets |
## Maintainer Notes
The app is Bun, [`@moduletrace/bunext`](https://github.com/Moduletrace/bunext),
SQLite via [`@moduletrace/bun-sqlite`](https://github.com/Moduletrace/bun-sqlite),
Tailwind CSS and the `@moduletrace/twui` component library. See
[AGENTS.md](AGENTS.md) for the full architecture, routes, queue, harness and
data model.
- `.env` and `secrets/` are gitignored. Never commit them.
- Don't patch `@moduletrace/twui` in `node_modules` - it is a shared publishable library. Appearance overrides belong in `src/styles/main.css`.
CI runs type-check and tests on every push and pull request. Releases are
built by GitHub Actions and pushed to both registries.
## License
MIT. See [LICENSE](LICENSE).
-65
View File
@@ -1,65 +0,0 @@
#!/bin/bash
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$PROJECT_ROOT/.env"
read_env_var() {
local key="$1" line value
line="$(grep -E "^[[:space:]]*(export[[:space:]]+)?${key}=" "$ENV_FILE" | tail -n 1 || true)"
value="${line#*=}"
value="${value%$'\r'}"
if [[ "$value" =~ ^\"(.*)\"$ ]] || [[ "$value" =~ ^\'(.*)\'$ ]]; then
value="${BASH_REMATCH[1]}"
fi
printf '%s' "$value"
}
if [ ! -f "$ENV_FILE" ]; then
echo "Error: $ENV_FILE not found." >&2
exit 1
fi
DEPLOY_NAME="cobble"
SOURCE_PATH="${SOURCE_DIRECTORY:-$(read_env_var SOURCE_DIRECTORY)}"
SERVER_IP="${DEPLOY_SERVER_IP:-$(read_env_var DEPLOY_SERVER_IP)}"
SSH_USER="${SSH_USER:-$(read_env_var SSH_USER)}"
SSH_USER="${SSH_USER:-root}"
SERVER_PATH="/docker/$DEPLOY_NAME"
SSH_KEY="$HOME/.ssh/tben"
SSH_OPTS=(-i "$SSH_KEY")
REMOTE="$SSH_USER@$SERVER_IP"
if [ ! -d "$SOURCE_PATH" ] || [ "$(realpath -m "$SOURCE_PATH")" = "/" ]; then
echo "Error: SOURCE_DIRECTORY is invalid, missing, or set to root path ('/')." >&2
exit 1
fi
if [ -z "$SERVER_IP" ]; then
echo "Error: DEPLOY_SERVER_IP is not set (shell env or $ENV_FILE)." >&2
exit 1
fi
if [ ! -f "$SOURCE_PATH/secrets/prod.env" ]; then
echo "Error: $SOURCE_PATH/secrets/prod.env not found." >&2
exit 1
fi
cd "$SOURCE_PATH"
rsync -avz -e "ssh -i $SSH_KEY" --delete \
--exclude='secrets/' --exclude='.env' \
--filter=':- .gitignore' \
"$SOURCE_PATH/" "$REMOTE:$SERVER_PATH/"
rsync -avz -e "ssh -i $SSH_KEY" --chmod=F600 \
"$SOURCE_PATH/secrets/prod.env" "$REMOTE:$SERVER_PATH/.env"
ssh "${SSH_OPTS[@]}" "$REMOTE" bash -s -- "$DEPLOY_NAME" << 'EOF'
set -euo pipefail
DEPLOY_NAME="$1"
cd "/docker/$DEPLOY_NAME"
COBBLE_DATA_DIR="/data/$DEPLOY_NAME/.data" COBBLE_EXTRA_NETWORK=cloudflare \
./deploy/run-app-container.sh
EOF
-59
View File
@@ -1,59 +0,0 @@
#!/bin/bash
# Builds the app's image and (re)starts its container. Rerun it to update:
# the database, media and computers' home directories (the data dir) and
# .env stay on the host. The container needs the docker socket, since it
# creates every computer as a container on the cobble network.
#
# COBBLE_DATA_DIR host data dir, mounted at /data (default /data/cobble)
# COBBLE_BIND host address the app is published on (default 127.0.0.1)
# COBBLE_EXTRA_NETWORK another docker network to join, e.g. a tunnel's
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="$PROJECT_ROOT/.env"
NAME="cobble"
NETWORK="cobble"
DATA_PATH="${COBBLE_DATA_DIR:-/data/cobble}"
BIND="${COBBLE_BIND:-127.0.0.1}"
DOCKER_SOCKET="/var/run/docker.sock"
if [ ! -f "$ENV_FILE" ]; then
echo "Error: $ENV_FILE not found. Copy .env.example and set ENCRYPTION_KEY and ENCRYPTION_SALT." >&2
exit 1
fi
if [ ! -S "$DOCKER_SOCKET" ]; then
echo "Error: no docker socket at $DOCKER_SOCKET. Computers run in docker, so the app needs it." >&2
exit 1
fi
PORT="$(grep -E '^[[:space:]]*PORT=' "$ENV_FILE" | tail -n 1 | cut -d= -f2- | tr -d "\"' \r")"
PORT="${PORT:-7227}"
mkdir -p "$DATA_PATH"
docker network inspect "$NETWORK" >/dev/null 2>&1 || docker network create "$NETWORK" >/dev/null
docker build -t "$NAME" "$PROJECT_ROOT"
docker stop -t 30 "$NAME" >/dev/null 2>&1 || true
docker rm -f "$NAME" >/dev/null 2>&1 || true
docker run --name "$NAME" -d \
--network "$NETWORK" \
-p "$BIND:$PORT:$PORT" \
-v "$ENV_FILE:/app/.env:ro" \
-v "$DATA_PATH:/data" \
-v "$DOCKER_SOCKET:$DOCKER_SOCKET" \
-e MAIN_SERVER=1 \
--cpus 1 --memory 1g \
--restart unless-stopped \
"$NAME:latest"
if [ -n "${COBBLE_EXTRA_NETWORK:-}" ]; then
docker network connect "$COBBLE_EXTRA_NETWORK" "$NAME"
fi
docker image prune -f >/dev/null
echo "Cobble is starting on http://$BIND:$PORT (data in $DATA_PATH)"
+706
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -12,7 +12,6 @@
"db:backup": "bunx bun-sqlite backup",
"seed": "bun src/scripts/seed-super-admin.tsx",
"seed:bots": "bun src/scripts/seed-bots.ts",
"deploy:prod": "./deploy/deploy-prod.sh",
"start": "NODE_ENV=production bun src/server.ts",
"typecheck": "tsc --noEmit",
"test": "bun test"