Bun workspaces with one app per folder: the Next.js forge in apps/web and the Android app in apps/mobile (a Gradle project; its package.json only wraps ./gradlew). The root package.json runs either app's scripts (bun dev, bun run build/lint/test, bun run mobile:build/deploy). The web app now runs from apps/web, so data/ and bin/ live there too. The Dockerfiles moved into apps/web and still build from the repository root, for the workspace's lockfile. @types/mdast is declared now that dependencies aren't hoisted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Showing 375 changed files with 207 additions and 99 deletions.
| @@ -1,12 +1,14 @@ | ||
| 1 | 1 | .git |
| 2 | - .next | |
| 3 | - node_modules | |
| 4 | - bin | |
| 5 | - data | |
| 2 | + **/node_modules | |
| 3 | + **/.next | |
| 4 | + **/*.tsbuildinfo | |
| 5 | + **/next-env.d.ts | |
| 6 | 6 | .env* |
| 7 | - *.tsbuildinfo | |
| 8 | - next-env.d.ts | |
| 9 | - Dockerfile | |
| 7 | + apps/web/bin | |
| 8 | + apps/web/data | |
| 9 | + apps/web/Dockerfile* | |
| 10 | + # Only the Android app's package.json, which the workspace's lockfile lists. | |
| 11 | + apps/mobile | |
| 12 | + !apps/mobile/package.json | |
| 10 | 13 | docker-compose.yml |
| 11 | 14 | .dockerignore |
| 12 | - mobile | |
| @@ -1,8 +1,9 @@ | ||
| 1 | 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. |
| 2 | + # Each app in apps/ has its own .gitignore for what it builds. | |
| 2 | 3 | |
| 3 | 4 | # dependencies |
| 4 | - /node_modules | |
| 5 | - /.pnp | |
| 5 | + node_modules/ | |
| 6 | + .pnp | |
| 6 | 7 | .pnp.* |
| 7 | 8 | .yarn/* |
| 8 | 9 | !.yarn/patches |
| @@ -10,16 +11,6 @@ | ||
| 10 | 11 | !.yarn/releases |
| 11 | 12 | !.yarn/versions |
| 12 | 13 | |
| 13 | - # testing | |
| 14 | - /coverage | |
| 15 | - | |
| 16 | - # next.js | |
| 17 | - /.next/ | |
| 18 | - /out/ | |
| 19 | - | |
| 20 | - # production | |
| 21 | - /build | |
| 22 | - | |
| 23 | 14 | # misc |
| 24 | 15 | .DS_Store |
| 25 | 16 | *.pem |
| @@ -33,15 +24,6 @@ yarn-error.log* | ||
| 33 | 24 | # env files (can opt-in for committing if needed) |
| 34 | 25 | .env* |
| 35 | 26 | |
| 36 | - # vercel | |
| 37 | - .vercel | |
| 38 | - | |
| 39 | - # typescript | |
| 40 | - *.tsbuildinfo | |
| 41 | - next-env.d.ts | |
| 42 | - | |
| 43 | - # git repositories and issue data | |
| 44 | - /data/ | |
| 45 | - | |
| 46 | - # the PocketBase server binary (scripts/install-pocketbase.mjs) | |
| 47 | - /bin/ | |
| 27 | + # editors | |
| 28 | + .idea/ | |
| 29 | + *.iml | |
| @@ -1,9 +1,11 @@ | ||
| 1 | - <!-- BEGIN:nextjs-agent-rules --> | |
| 1 | + # Vectrum monorepo | |
| 2 | 2 | |
| 3 | - # This is NOT the Next.js you know | |
| 3 | + Bun workspaces, one app per folder in `apps/`: | |
| 4 | 4 | |
| 5 | - This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. | |
| 5 | + - `apps/web`: the forge itself, a Next.js app (website, git over SSH and HTTPS, MCP, the `/api/v1` JSON API, runners' | |
| 6 | + API). Read `apps/web/AGENTS.md` before changing it. | |
| 7 | + - `apps/mobile`: the Android app, in Kotlin with Jetpack Compose, built with Gradle (`apps/mobile/gradlew`). It only | |
| 8 | + talks to the web app over `/api/v1` (`apps/web/lib/app-api.ts`); keep both sides in step when changing that API. | |
| 6 | 9 | |
| 7 | - This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. | |
| 8 | - | |
| 9 | - <!-- END:nextjs-agent-rules --> | |
| 10 | + Run commands from the repository root (`bun dev`, `bun run lint`, `bun run test`, `bun run mobile:build`), or inside an | |
| 11 | + app's folder. Product documentation is in `docs/`. | |
| @@ -12,6 +12,11 @@ registry, CI on self-hosted runners, and an Android app. See [all features](docs | ||
| 12 | 12 | |
| 13 | 13 | Website: [vectrum.cc](https://vectrum.cc) |
| 14 | 14 | |
| 15 | + This repository holds both apps, as Bun workspaces: | |
| 16 | + | |
| 17 | + - [`apps/web`](apps/web): the forge, a Next.js app | |
| 18 | + - [`apps/mobile`](apps/mobile): the Android app, in Kotlin with Jetpack Compose | |
| 19 | + | |
| 15 | 20 | ## Getting started |
| 16 | 21 | |
| 17 | 22 | Requires Node.js 22+ and `git`. |
| @@ -23,6 +28,13 @@ bun dev | ||
| 23 | 28 | |
| 24 | 29 | Open http://localhost:3000, sign up, and create a repository. |
| 25 | 30 | |
| 31 | + To build the Android app (needs JDK 17+ and the Android SDK, see [mobile.md](docs/mobile.md)): | |
| 32 | + | |
| 33 | + ```bash | |
| 34 | + bun run mobile:build # apps/mobile/app/build/outputs/apk/debug/app-debug.apk | |
| 35 | + bun run mobile:deploy # onto a connected device or emulator | |
| 36 | + ``` | |
| 37 | + | |
| 26 | 38 | To run it with Docker: |
| 27 | 39 | |
| 28 | 40 | ```bash |
| @@ -41,4 +53,5 @@ docker compose up -d --build | ||
| 41 | 53 | |
| 42 | 54 | ## Contributing |
| 43 | 55 | |
| 44 | - Run `bun run lint` and `bun test` before opening a pull request. | |
| 56 | + Run `bun run lint` and `bun run test` before opening a pull request, and `bun run mobile:build` if you changed the | |
| 57 | + Android app or the API it uses (`apps/web/lib/app-api.ts`). | |
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
| @@ -0,0 +1,13 @@ | ||
| 1 | + { | |
| 2 | + "name": "@vectrum/mobile", | |
| 3 | + "version": "0.1.0", | |
| 4 | + "private": true, | |
| 5 | + "description": "Vectrum for Android (Kotlin, Jetpack Compose). Built with Gradle; these scripts only wrap ./gradlew.", | |
| 6 | + "scripts": { | |
| 7 | + "build": "./gradlew assembleDebug", | |
| 8 | + "build:release": "./gradlew assembleRelease", | |
| 9 | + "deploy": "./gradlew installDebug", | |
| 10 | + "lint": "./gradlew lintDebug", | |
| 11 | + "clean": "./gradlew clean" | |
| 12 | + } | |
| 13 | + } | |
File mode or name changed only.
File mode or name changed only.
| @@ -0,0 +1,22 @@ | ||
| 1 | + # testing | |
| 2 | + /coverage | |
| 3 | + | |
| 4 | + # next.js | |
| 5 | + /.next/ | |
| 6 | + /out/ | |
| 7 | + | |
| 8 | + # production | |
| 9 | + /build | |
| 10 | + | |
| 11 | + # vercel | |
| 12 | + .vercel | |
| 13 | + | |
| 14 | + # typescript | |
| 15 | + *.tsbuildinfo | |
| 16 | + next-env.d.ts | |
| 17 | + | |
| 18 | + # git repositories and issue data | |
| 19 | + /data/ | |
| 20 | + | |
| 21 | + # the PocketBase server binary (scripts/install-pocketbase.mjs) | |
| 22 | + /bin/ | |
| @@ -0,0 +1,9 @@ | ||
| 1 | + <!-- BEGIN:nextjs-agent-rules --> | |
| 2 | + | |
| 3 | + # This is NOT the Next.js you know | |
| 4 | + | |
| 5 | + This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. | |
| 6 | + | |
| 7 | + This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. | |
| 8 | + | |
| 9 | + <!-- END:nextjs-agent-rules --> | |
| @@ -0,0 +1 @@ | ||
| 1 | + @AGENTS.md | |
| @@ -1,15 +1,21 @@ | ||
| 1 | - # ---- build: install dependencies (which also downloads PocketBase into bin/) and build Next.js | |
| 1 | + # The web app's image. Build it from the repository root, which holds the workspace's lockfile: | |
| 2 | + # | |
| 3 | + # docker build -f apps/web/Dockerfile -t vectrum . | |
| 4 | + # | |
| 5 | + # ---- build: install dependencies (which also downloads PocketBase into apps/web/bin/) and build Next.js | |
| 2 | 6 | FROM node:24-bookworm-slim AS build |
| 3 | 7 | WORKDIR /app |
| 4 | 8 | COPY --from=oven/bun:1.4.2 /usr/local/bin/bun /usr/local/bin/bun |
| 5 | 9 | RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates unzip && rm -rf /var/lib/apt/lists/* |
| 6 | 10 | |
| 7 | 11 | COPY package.json bun.lock ./ |
| 8 | - COPY scripts ./scripts | |
| 9 | 12 | COPY patches ./patches |
| 13 | + COPY apps/mobile/package.json ./apps/mobile/ | |
| 14 | + COPY apps/web/package.json ./apps/web/ | |
| 15 | + COPY apps/web/scripts ./apps/web/scripts | |
| 10 | 16 | RUN bun install --frozen-lockfile |
| 11 | 17 | |
| 12 | - COPY . . | |
| 18 | + COPY apps/web ./apps/web | |
| 13 | 19 | ENV NEXT_TELEMETRY_DISABLED=1 NEXT_OUTPUT=standalone |
| 14 | 20 | RUN bun run build |
| 15 | 21 | |
| @@ -18,12 +24,14 @@ FROM node:24-bookworm-slim | ||
| 18 | 24 | RUN apt-get update && apt-get install -y --no-install-recommends git tini ca-certificates && rm -rf /var/lib/apt/lists/* |
| 19 | 25 | WORKDIR /app |
| 20 | 26 | |
| 21 | - COPY --from=build --chown=node:node /app/.next/standalone ./ | |
| 22 | - COPY --from=build --chown=node:node /app/.next/static ./.next/static | |
| 23 | - COPY --from=build --chown=node:node /app/public ./public | |
| 24 | - COPY --from=build --chown=node:node /app/pb_migrations ./pb_migrations | |
| 25 | - COPY --from=build --chown=node:node /app/bin ./bin | |
| 26 | - COPY --from=build --chown=node:node /app/scripts/create-user.mjs ./scripts/ | |
| 27 | + # The standalone output mirrors the workspace: the server is apps/web/server.js, next to a trimmed node_modules/. | |
| 28 | + COPY --from=build --chown=node:node /app/apps/web/.next/standalone ./ | |
| 29 | + WORKDIR /app/apps/web | |
| 30 | + COPY --from=build --chown=node:node /app/apps/web/.next/static ./.next/static | |
| 31 | + COPY --from=build --chown=node:node /app/apps/web/public ./public | |
| 32 | + COPY --from=build --chown=node:node /app/apps/web/pb_migrations ./pb_migrations | |
| 33 | + COPY --from=build --chown=node:node /app/apps/web/bin ./bin | |
| 34 | + COPY --from=build --chown=node:node /app/apps/web/scripts/create-user.mjs ./scripts/ | |
| 27 | 35 | |
| 28 | 36 | RUN mkdir /data && chown node:node /data |
| 29 | 37 | VOLUME /data |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | # A Vectrum runner in a container: Node.js, Bun, git and the usual build tools. |
| 2 | 2 | # |
| 3 | - # docker build -f Dockerfile.runner -t vectrum-runner . | |
| 3 | + # docker build -f apps/web/Dockerfile.runner -t vectrum-runner . (from the repository root) | |
| 4 | 4 | # docker run -d -e VECTRUM_URL=https://vectrum.example -e VECTRUM_RUNNER_TOKEN=vrn_… vectrum-runner |
| 5 | 5 | # |
| 6 | 6 | # Steps run as the unprivileged node user, inside this container only (no Docker socket). |
| @@ -10,7 +10,7 @@ RUN ln -s /usr/local/bin/bun /usr/local/bin/bunx \ | ||
| 10 | 10 | && apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/* |
| 11 | 11 | |
| 12 | 12 | WORKDIR /runner |
| 13 | - COPY --chown=node:node public/vectrum-runner.mjs ./ | |
| 13 | + COPY --chown=node:node apps/web/public/vectrum-runner.mjs ./ | |
| 14 | 14 | RUN mkdir _work && chown node:node _work |
| 15 | 15 | USER node |
| 16 | 16 | |
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
| @@ -54,7 +54,7 @@ import { SIGNUPS_ENABLED } from "./signups"; | ||
| 54 | 54 | import { authenticateUser, createUser, getUserByUsername, UserError, type User } from "./users"; |
| 55 | 55 | |
| 56 | 56 | /* |
| 57 | - * The JSON API of the Vectrum mobile app (mobile/), under /api/v1. The app signs in with a username | |
| 57 | + * The JSON API of the Vectrum mobile app (apps/mobile), under /api/v1. The app signs in with a username | |
| 58 | 58 | * and password and gets the same session token the web UI keeps in its cookie; it sends it as a |
| 59 | 59 | * bearer token. Everything here acts as that person, with the web UI's permission checks (see |
| 60 | 60 | * app/actions/*), so the app can do nothing the website wouldn't let them do. |
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
| @@ -0,0 +1,47 @@ | ||
| 1 | + { | |
| 2 | + "name": "@vectrum/web", | |
| 3 | + "version": "0.1.0", | |
| 4 | + "private": true, | |
| 5 | + "scripts": { | |
| 6 | + "dev": "next dev", | |
| 7 | + "build": "next build", | |
| 8 | + "start": "next start", | |
| 9 | + "lint": "eslint", | |
| 10 | + "postinstall": "node scripts/install-pocketbase.mjs", | |
| 11 | + "pocketbase:install": "node scripts/install-pocketbase.mjs", | |
| 12 | + "test": "bun test" | |
| 13 | + }, | |
| 14 | + "dependencies": { | |
| 15 | + "@codemirror/language": "^6.12.4", | |
| 16 | + "@codemirror/language-data": "^6.5.2", | |
| 17 | + "@codemirror/view": "^6.43.13", | |
| 18 | + "@lezer/highlight": "^1.2.5", | |
| 19 | + "@uiw/codemirror-themes": "^4.25.12", | |
| 20 | + "@uiw/react-codemirror": "^4.25.12", | |
| 21 | + "@unlocalhosted/metalui": "^0.3.3", | |
| 22 | + "next": "16.3.7", | |
| 23 | + "pocketbase": "^0.28.1", | |
| 24 | + "react": "19.2.8", | |
| 25 | + "react-dom": "19.2.8", | |
| 26 | + "react-markdown": "^10.1.0", | |
| 27 | + "rehype-raw": "^7.0.0", | |
| 28 | + "rehype-sanitize": "^6.0.0", | |
| 29 | + "remark-gfm": "^4.0.1", | |
| 30 | + "server-only": "^0.0.1", | |
| 31 | + "shiki": "^4.4.3", | |
| 32 | + "ssh2": "^1.17.0", | |
| 33 | + "yaml": "^2.9.1" | |
| 34 | + }, | |
| 35 | + "devDependencies": { | |
| 36 | + "@tailwindcss/postcss": "^4", | |
| 37 | + "@types/mdast": "^4.0.4", | |
| 38 | + "@types/node": "^26.6.3", | |
| 39 | + "@types/react": "^19", | |
| 40 | + "@types/react-dom": "^19", | |
| 41 | + "@types/ssh2": "^1.15.6", | |
| 42 | + "eslint": "^9", | |
| 43 | + "eslint-config-next": "16.3.7", | |
| 44 | + "tailwindcss": "^4", | |
| 45 | + "typescript": "^5" | |
| 46 | + } | |
| 47 | + } | |
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
File mode or name changed only.
| @@ -4,6 +4,14 @@ | ||
| 4 | 4 | "workspaces": { |
| 5 | 5 | "": { |
| 6 | 6 | "name": "vectrum", |
| 7 | + }, | |
| 8 | + "apps/mobile": { | |
| 9 | + "name": "@vectrum/mobile", | |
| 10 | + "version": "0.1.0", | |
| 11 | + }, | |
| 12 | + "apps/web": { | |
| 13 | + "name": "@vectrum/web", | |
| 14 | + "version": "0.1.0", | |
| 7 | 15 | "dependencies": { |
| 8 | 16 | "@codemirror/language": "^6.12.4", |
| 9 | 17 | "@codemirror/language-data": "^6.5.2", |
| @@ -27,6 +35,7 @@ | ||
| 27 | 35 | }, |
| 28 | 36 | "devDependencies": { |
| 29 | 37 | "@tailwindcss/postcss": "^4", |
| 38 | + "@types/mdast": "^4.0.4", | |
| 30 | 39 | "@types/node": "^26.6.3", |
| 31 | 40 | "@types/react": "^19", |
| 32 | 41 | "@types/react-dom": "^19", |
| @@ -472,6 +481,10 @@ | ||
| 472 | 481 | |
| 473 | 482 | "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.12.2", "", { "os": "win32", "cpu": "x64" }, "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA=="], |
| 474 | 483 | |
| 484 | + "@vectrum/mobile": ["@vectrum/mobile@workspace:apps/mobile"], | |
| 485 | + | |
| 486 | + "@vectrum/web": ["@vectrum/web@workspace:apps/web"], | |
| 487 | + | |
| 475 | 488 | "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], |
| 476 | 489 | |
| 477 | 490 | "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], |
| @@ -1,6 +1,8 @@ | ||
| 1 | 1 | services: |
| 2 | 2 | vectrum: |
| 3 | - build: . | |
| 3 | + build: | |
| 4 | + context: . | |
| 5 | + dockerfile: apps/web/Dockerfile | |
| 4 | 6 | image: vectrum |
| 5 | 7 | restart: unless-stopped |
| 6 | 8 | ports: |
| @@ -3,12 +3,14 @@ | ||
| 3 | 3 | Requires Node.js 22+ (24.7+ for post-quantum SSH key exchange) and `git` on the `PATH`. |
| 4 | 4 | |
| 5 | 5 | ```bash |
| 6 | - bun install # also downloads the PocketBase server into bin/ | |
| 6 | + bun install # also downloads the PocketBase server into apps/web/bin/ | |
| 7 | 7 | bun dev |
| 8 | 8 | ``` |
| 9 | 9 | |
| 10 | 10 | Open http://localhost:3000, sign up, and create a repository. |
| 11 | 11 | |
| 12 | + The web app is `apps/web`; paths below are relative to it. Commands run from the repository root or from `apps/web`. | |
| 13 | + | |
| 12 | 14 | The data lives in [PocketBase](https://pocketbase.io). Vectrum starts `bin/pocketbase` itself on `127.0.0.1:8090`, with its data in `data/pb_data` and the schema from `pb_migrations/`; its superuser login is generated into `data/pocketbase-superuser.json` (also good for the PocketBase dashboard at http://127.0.0.1:8090/_/). Run `bun run pocketbase:install` if the binary is missing. To use a PocketBase server of your own instead, set `PB_URL` and the superuser credentials; it needs the migrations in `pb_migrations/` (`pocketbase serve --migrationsDir pb_migrations`). |
| 13 | 15 | |
| 14 | 16 | ## Running with Docker |
| @@ -23,7 +25,9 @@ The image runs the web UI on port 3000 and the SSH server on port 2222, with all | ||
| 23 | 25 | docker compose exec -it vectrum node scripts/create-user.mjs <username> <email> [name] |
| 24 | 26 | ``` |
| 25 | 27 | |
| 26 | - It asks for the password (or reads `VECTRUM_PASSWORD`). Outside Docker, run `node scripts/create-user.mjs` the same way while Vectrum runs. Clone URLs show `SSH_PORT`, so publish SSH on the same port on the host, and set `SSH_HOST` to your domain. Put a TLS reverse proxy in front of port 3000 if the server is reachable from the internet, and set `PUBLIC_URL` to its address. | |
| 28 | + It asks for the password (or reads `VECTRUM_PASSWORD`). Outside Docker, run `node scripts/create-user.mjs` the same way from `apps/web` while Vectrum runs. Clone URLs show `SSH_PORT`, so publish SSH on the same port on the host, and set `SSH_HOST` to your domain. Put a TLS reverse proxy in front of port 3000 if the server is reachable from the internet, and set `PUBLIC_URL` to its address. | |
| 29 | + | |
| 30 | + To build the image without Compose, run `docker build -f apps/web/Dockerfile -t vectrum .` from the repository root. | |
| 27 | 31 | |
| 28 | 32 | ## Configuration |
| 29 | 33 | |
| @@ -1,5 +1,19 @@ | ||
| 1 | 1 | # Code layout |
| 2 | 2 | |
| 3 | + The repository is a Bun workspace with one app per folder in `apps/`: | |
| 4 | + | |
| 5 | + - `apps/web`: the forge, a Next.js app. Its `Dockerfile` builds the server image and `Dockerfile.runner` the runner's | |
| 6 | + - `apps/mobile`: the Android app, a Gradle project (see [mobile.md](mobile.md)). Its `package.json` only wraps | |
| 7 | + `./gradlew` so the root's `bun run mobile:*` scripts can reach it | |
| 8 | + - `docs/`: this documentation; `patches/`: patches to dependencies, applied by `bun install` | |
| 9 | + | |
| 10 | + The root `package.json` runs either app's scripts (`bun dev`, `bun run build`, `bun run lint`, `bun run test`, | |
| 11 | + `bun run mobile:build`, `bun run mobile:deploy`). | |
| 12 | + | |
| 13 | + ## The web app | |
| 14 | + | |
| 15 | + Paths are relative to `apps/web`. | |
| 16 | + | |
| 3 | 17 | - `lib/db.ts`: starting and connecting to PocketBase; `pb_migrations/`: the schema |
| 4 | 18 | - `lib/users.ts`, `lib/auth.ts`: accounts and sessions (PocketBase auth records and tokens) |
| 5 | 19 | - `lib/repos.ts`: repository records, permissions (`repoAccess`), collaborators |
| @@ -18,4 +32,4 @@ | ||
| 18 | 32 | - `lib/releases.ts`, `lib/release-http.ts`: releases and their files, and the download, upload and source archive routes |
| 19 | 33 | - `lib/workflows.ts`, `lib/ci.ts`, `lib/runs.ts`, `lib/runners.ts`: workflow parsing, starting runs on push, the job queue and runner registration; `app/api/runner/*` is the runner API and `public/vectrum-runner.mjs` the runner |
| 20 | 34 | - `app/actions/*`: server actions, each re-checking the session and permissions |
| 21 | - - `lib/app-api.ts`: the JSON API of the Android app (`app/api/v1`); `mobile/`: the app itself (see [mobile.md](mobile.md)) | |
| 35 | + - `lib/app-api.ts`: the JSON API of the Android app (`app/api/v1`), which lives in `apps/mobile` | |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | # Android app |
| 2 | 2 | |
| 3 | - `mobile/` is Vectrum for Android, in Kotlin with Jetpack Compose. It looks like the website: MetalUI's | |
| 3 | + `apps/mobile/` is Vectrum for Android, in Kotlin with Jetpack Compose. It looks like the website: MetalUI's | |
| 4 | 4 | bone and graphite colorways, raised plates and sunk wells, Geist and Geist Mono, engraved mono labels. |
| 5 | 5 | |
| 6 | 6 | What it does: |
| @@ -22,15 +22,17 @@ What it does: | ||
| 22 | 22 | ## Building |
| 23 | 23 | |
| 24 | 24 | Needs JDK 17+ and the Android SDK (platform 37, build tools 36). With `ANDROID_HOME` set (or `sdk.dir` in |
| 25 | - `mobile/local.properties`): | |
| 25 | + `apps/mobile/local.properties`): | |
| 26 | 26 | |
| 27 | 27 | ```bash |
| 28 | - cd mobile | |
| 28 | + cd apps/mobile | |
| 29 | 29 | ./gradlew assembleDebug # app/build/outputs/apk/debug/app-debug.apk |
| 30 | 30 | ./gradlew installDebug # onto a connected device or emulator |
| 31 | 31 | ./gradlew assembleRelease # minified, signed with the debug key; sign with your own key to publish |
| 32 | 32 | ``` |
| 33 | 33 | |
| 34 | + From the repository root, `bun run mobile:build` and `bun run mobile:deploy` do the first two. | |
| 35 | + | |
| 34 | 36 | The app runs on Android 8.0 (API 26) and newer. |
| 35 | 37 | |
| 36 | 38 | ## Setting up |
| @@ -54,7 +56,7 @@ non-local server without HTTPS. | ||
| 54 | 56 | |
| 55 | 57 | ## The API |
| 56 | 58 | |
| 57 | - The app talks to `/api/v1` (`app/api/v1/[...path]/route.ts`, `lib/app-api.ts`): JSON, with the session token | |
| 59 | + The app talks to `/api/v1` (`apps/web/app/api/v1/[...path]/route.ts`, `apps/web/lib/app-api.ts`): JSON, with the session token | |
| 58 | 60 | as `Authorization: Bearer <token>`. It acts as the signed-in person and applies the website's permission |
| 59 | 61 | checks, so the app can't do anything the website wouldn't let that person do: private repositories are "not |
| 60 | 62 | found" to others, anyone who can read a repository may open issues and comment, and changing issues, |
| @@ -78,6 +80,8 @@ Errors are `{ "error": "…" }` with a 4xx status; 401 means the session ended. | ||
| 78 | 80 | |
| 79 | 81 | ## Code layout |
| 80 | 82 | |
| 83 | + In `apps/mobile/app/src/main/java/cc/vectrum/app/`: | |
| 84 | + | |
| 81 | 85 | - `data/`: the API's models (`Models.kt`), the HTTP client (`Api.kt`) and the stored session (`Session.kt`) |
| 82 | 86 | - `ui/theme/`: MetalUI's colorways (`Palette.kt`), type roles (`Type.kt`) and materials (`Materials.kt`: |
| 83 | 87 | plates, wells and caps as stacks of shadows, from MetalUI's tokens) |
| @@ -1,47 +1,19 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "name": "vectrum", |
| 3 | - "version": "0.1.0", | |
| 4 | 3 | "private": true, |
| 4 | + "workspaces": [ | |
| 5 | + "apps/*" | |
| 6 | + ], | |
| 5 | 7 | "scripts": { |
| 6 | - "dev": "next dev", | |
| 7 | - "build": "next build", | |
| 8 | - "start": "next start", | |
| 9 | - "lint": "eslint", | |
| 10 | - "postinstall": "node scripts/install-pocketbase.mjs", | |
| 11 | - "pocketbase:install": "node scripts/install-pocketbase.mjs", | |
| 12 | - "test": "bun test" | |
| 13 | - }, | |
| 14 | - "dependencies": { | |
| 15 | - "@codemirror/language": "^6.12.4", | |
| 16 | - "@codemirror/language-data": "^6.5.2", | |
| 17 | - "@codemirror/view": "^6.43.13", | |
| 18 | - "@lezer/highlight": "^1.2.5", | |
| 19 | - "@uiw/codemirror-themes": "^4.25.12", | |
| 20 | - "@uiw/react-codemirror": "^4.25.12", | |
| 21 | - "@unlocalhosted/metalui": "^0.3.3", | |
| 22 | - "next": "16.3.7", | |
| 23 | - "pocketbase": "^0.28.1", | |
| 24 | - "react": "19.2.8", | |
| 25 | - "react-dom": "19.2.8", | |
| 26 | - "react-markdown": "^10.1.0", | |
| 27 | - "rehype-raw": "^7.0.0", | |
| 28 | - "rehype-sanitize": "^6.0.0", | |
| 29 | - "remark-gfm": "^4.0.1", | |
| 30 | - "server-only": "^0.0.1", | |
| 31 | - "shiki": "^4.4.3", | |
| 32 | - "ssh2": "^1.17.0", | |
| 33 | - "yaml": "^2.9.1" | |
| 34 | - }, | |
| 35 | - "devDependencies": { | |
| 36 | - "@tailwindcss/postcss": "^4", | |
| 37 | - "@types/node": "^26.6.3", | |
| 38 | - "@types/react": "^19", | |
| 39 | - "@types/react-dom": "^19", | |
| 40 | - "@types/ssh2": "^1.15.6", | |
| 41 | - "eslint": "^9", | |
| 42 | - "eslint-config-next": "16.3.7", | |
| 43 | - "tailwindcss": "^4", | |
| 44 | - "typescript": "^5" | |
| 8 | + "dev": "bun run --cwd apps/web dev", | |
| 9 | + "build": "bun run --cwd apps/web build", | |
| 10 | + "start": "bun run --cwd apps/web start", | |
| 11 | + "lint": "bun run --cwd apps/web lint", | |
| 12 | + "test": "bun run --cwd apps/web test", | |
| 13 | + "pocketbase:install": "bun run --cwd apps/web pocketbase:install", | |
| 14 | + "mobile:build": "bun run --cwd apps/mobile build", | |
| 15 | + "mobile:deploy": "bun run --cwd apps/mobile deploy", | |
| 16 | + "mobile:lint": "bun run --cwd apps/mobile lint" | |
| 45 | 17 | }, |
| 46 | 18 | "packageManager": "bun@1.4.2", |
| 47 | 19 | "ignoreScripts": [ |