Skip to content

Commit ef91c6a

Browse files
Merge pull request #18 from velopulent/service
OS service management with cross-platform support and refactor
2 parents 87e196f + f386b51 commit ef91c6a

104 files changed

Lines changed: 5130 additions & 1365 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/dependabot.yml

Lines changed: 0 additions & 36 deletions
This file was deleted.

.github/workflows/ci.yml

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ jobs:
4242
filters: |
4343
rust:
4444
- 'apps/backend/**'
45+
- 'xtask/**'
4546
- 'libs/proto/**'
4647
- 'Cargo.toml'
4748
- 'Cargo.lock'
@@ -276,6 +277,30 @@ jobs:
276277
- name: Build
277278
run: bunx nx run dashboard:build
278279

280+
packaging:
281+
name: Packaging checks
282+
needs: changes
283+
if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.other == 'true'
284+
runs-on: ubuntu-latest
285+
timeout-minutes: 15
286+
steps:
287+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
288+
- uses: dtolnay/rust-toolchain@67ef31d5b988238dd797d409d6f9574278e20537 # master
289+
with:
290+
toolchain: "1.94"
291+
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
292+
with:
293+
workspaces: ". -> target"
294+
shared-key: linux-packaging
295+
cache-on-failure: true
296+
- name: xtask tests
297+
run: cargo test -p xtask --locked
298+
- name: package dry-run
299+
run: |
300+
cargo run -p xtask --locked -- package --kind all --version 0.0.0-ci --dry-run --skip-build --target-os linux --arch amd64
301+
cargo run -p xtask --locked -- package --kind host --version 0.0.0-ci --dry-run --skip-build --target-os macos --arch arm64
302+
cargo run -p xtask --locked -- package --kind host --version 0.0.0-ci --dry-run --skip-build --target-os windows --arch amd64
303+
279304
web:
280305
name: Web (landing + docs)
281306
needs: changes
@@ -307,7 +332,7 @@ jobs:
307332
# "skipped required check stays pending forever" trap of naive path filtering.
308333
ci-status:
309334
name: CI status
310-
needs: [changes, lint, security, test, test-os, frontend, web]
335+
needs: [changes, lint, security, test, test-os, frontend, packaging, web]
311336
if: always()
312337
runs-on: ubuntu-latest
313338
timeout-minutes: 5

.github/workflows/release.yml

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ on:
1212
tags:
1313
- "v*"
1414

15-
permissions:
16-
contents: write
17-
1815
jobs:
1916
# Build the static dashboard once. Every build job consumes this same dist.
2017
dashboard:
@@ -45,7 +42,7 @@ jobs:
4542
if-no-files-found: error
4643

4744
build:
48-
name: Build (${{ matrix.os }}-${{ matrix.arch }})
45+
name: Build/package (${{ matrix.os }}-${{ matrix.arch }})
4946
needs: dashboard
5047
runs-on: ${{ matrix.runner }}
5148
strategy:
@@ -55,33 +52,21 @@ jobs:
5552
- os: linux
5653
arch: amd64
5754
runner: ubuntu-latest
58-
bin: vcms
59-
asset: vcms-${{ github.ref_name }}-linux-amd64
6055
- os: linux
6156
arch: arm64
6257
runner: ubuntu-24.04-arm
63-
bin: vcms
64-
asset: vcms-${{ github.ref_name }}-linux-arm64
6558
- os: macos
6659
arch: amd64
6760
runner: macos-26-intel
68-
bin: vcms
69-
asset: vcms-${{ github.ref_name }}-macos-amd64
7061
- os: macos
7162
arch: arm64
7263
runner: macos-26
73-
bin: vcms
74-
asset: vcms-${{ github.ref_name }}-macos-arm64
7564
- os: windows
7665
arch: amd64
7766
runner: windows-latest
78-
bin: vcms.exe
79-
asset: vcms-${{ github.ref_name }}-windows-amd64.exe
8067
- os: windows
8168
arch: arm64
8269
runner: windows-11-arm
83-
bin: vcms.exe
84-
asset: vcms-${{ github.ref_name }}-windows-arm64.exe
8570
steps:
8671
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
8772
with:
@@ -107,20 +92,54 @@ jobs:
10792
workspaces: ". -> target"
10893
key: release-${{ matrix.os }}-${{ matrix.arch }}
10994

95+
- name: Install Linux packaging tools
96+
if: matrix.os == 'linux'
97+
run: sudo apt-get update && sudo apt-get install -y rpm
98+
99+
- name: Install WiX
100+
if: matrix.os == 'windows'
101+
shell: pwsh
102+
run: |
103+
dotnet tool install --global wix --version 7.*
104+
wix eula accept wix7
105+
wix extension add --global WixToolset.UI.wixext/7.0.0
106+
110107
# Build cargo directly (dist is already present from the artifact); keep the
111108
# default `embed-dashboard` feature so rust-embed bakes in apps/dashboard/dist.
112109
- name: Build release binary
113110
working-directory: apps/backend
114111
run: cargo build --release --locked
115112

116-
- name: Stage artifact
117-
shell: bash
118-
run: |
119-
mkdir -p dist
120-
cp "target/release/${{ matrix.bin }}" "dist/${{ matrix.asset }}"
113+
- name: Package artifacts
114+
run: cargo run -p xtask -- package --kind host --skip-build --target-os ${{ matrix.os }} --arch ${{ matrix.arch }}
121115

122-
- name: Attach to release
116+
- name: Upload release artifacts
117+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0 # v7.0.1
118+
with:
119+
name: release-${{ matrix.os }}-${{ matrix.arch }}
120+
path: dist/packages/*
121+
if-no-files-found: error
122+
123+
publish:
124+
name: Attest and publish release
125+
needs: build
126+
runs-on: ubuntu-latest
127+
permissions:
128+
contents: write
129+
id-token: write
130+
attestations: write
131+
steps:
132+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
133+
with:
134+
pattern: release-*
135+
path: dist/packages
136+
merge-multiple: true
137+
- name: Attest build provenance
138+
uses: actions/attest-build-provenance@v3
139+
with:
140+
subject-path: dist/packages/*
141+
- name: Attach all artifacts after matrix passes
123142
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
124143
with:
125-
files: dist/${{ matrix.asset }}
144+
files: dist/packages/*
126145
generate_release_notes: true

AGENTS.md

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
# AI Agent Instructions for CMS (Rust + React)
22

3+
## Product Identity
4+
5+
- Human-facing product and service display name: **Velopulent CMS**.
6+
- Canonical documentation: <https://cms.velopulent.com/docs>.
7+
- Keep the executable, package name, native service identifier, environment prefix, and internal identifiers as `vcms` / `VCMS_*` for backward compatibility. Do not rename persisted paths or service keys when updating branding.
8+
39
## Architecture
410

511
- **Backend**: Rust + Axum HTTP server, `SQLx` + (SQLite | PostgreSQL | MySQL), `rust-embed` (static assets)
612
- **Frontend**: React in `apps/dashboard/` with Tanstack Router, Tanstack Query, shadcn/ui
713
- **gRPC**: Separate server on port 50051 (compiled from `libs/proto/*.proto` via `tonic-build`)
814
- **Build**: Nx orchestrates `dashboard:build``backend:build`. `build.rs` compiles proto files only.
9-
- **Runtime**: Single binary serves REST API (`/api/*`), gRPC, GraphQL (`/api/graphql`), MCP (Streamable HTTP at `/mcp`), and static SPA fallback
15+
- **Runtime**: Single binary serves REST API (`/api/*`), gRPC, GraphQL (`/api/graphql`), MCP (Streamable HTTP at `/mcp`), health probes (`/health/live`, `/health/ready`), and static SPA fallback
1016

1117
## Key Directories
1218

@@ -25,6 +31,8 @@
2531
- `libs/proto/` - Protocol Buffer definitions (`cms.proto`)
2632
- `apps/dashboard/` - React frontend app
2733
- `apps/web/` - Landing Page and Documentation (NextJS + Fumadocs)
34+
- `packaging/` - Native Linux, macOS, Windows, Debian, RPM, and Arch definitions and lifecycle scripts
35+
- `xtask/` - Typed release/package orchestration; platform builders live in separate modules
2836

2937
## Developer Commands
3038

@@ -75,16 +83,35 @@ vcms admin reset-password --email U --password P
7583
vcms backup create [--scope instance|site] [--site ID] [--out FILE] [--no-files] [--encrypt]
7684
vcms backup list # list recorded backups
7785
vcms restore --file PATH [--scope instance|site] [--site ID] [--import-as-new] --yes
86+
vcms mcp stdio # thin HTTP proxy to a running server's /mcp (for MCP clients)
87+
vcms service status # normalized native-service status and manager details
88+
vcms doctor # validate config, storage, database, bind ports, and service identity
7889
```
7990

8091
`backup`/`restore` run offline (no HTTP server) against the configured database —
8192
the disaster-recovery path when the instance won't boot. `restore` is destructive
8293
and requires `--yes`.
8394

95+
`mcp stdio` opens no database, secrets, or search index of its own: it forwards
96+
JSON-RPC between stdin/stdout and the server's `/mcp` Streamable-HTTP endpoint,
97+
reading only `VCMS_MCP_TOKEN` (bearer) and `VCMS_MCP_URL` (default
98+
`http://127.0.0.1:3000`). So it works even when the data is owned by the OS-service
99+
account. The installed service pins `VCMS_HOME` to a system dir so the daemon stores
100+
everything under one owned root.
101+
84102
Global flags (highest precedence): `--config <PATH>`, `--bind <ADDR>`, `--database-url <URL>`, `--log-level <LEVEL>`.
85103

86104
The server auto-migrates the database on every startup; there is no separate migrate command.
87105

106+
## Packaging and Releases
107+
108+
- Keep native package definitions and service files in `packaging/`; do not embed them in Rust source or workflow YAML.
109+
- `xtask` orchestrates deterministic staging and packaging. Keep `xtask/src/main.rs` limited to CLI parsing and dispatch, with shared and platform-specific implementation in modules.
110+
- Keep the release workflow thin: build the dashboard, run native build/package jobs, assemble artifacts, attest, and publish.
111+
- Ordinary CI validates packaging templates, `xtask` tests, and deterministic dry-runs. It must not install or mutate host services.
112+
- Release artifacts include portable archives, Debian, RPM, MSI, and macOS PKG packages. Arch is maintained as a package recipe consuming published Linux archives; render it with `xtask arch-render`, not as a fake release archive.
113+
- The stable native service identifier is `vcms`; its human-facing display name is **Velopulent CMS**. Fresh Linux and macOS package installs register/enable the service but do not auto-start it; the Windows MSI installs the service for automatic startup and starts it immediately.
114+
88115
## Configuration
89116

90117
Non-secret settings live in a TOML config file; secrets stay in the environment (or `.env`).
@@ -93,29 +120,53 @@ Layers merge with precedence: **CLI flag > env var > config file > built-in defa
93120
Config file search order (first existing wins; missing is fine):
94121
1. `--config` flag / `VCMS_CONFIG` env
95122
2. `./vcms.toml` (current dir)
96-
3. `~/.vcms/config.toml` (CMS home; `$VCMS_HOME/config.toml` if set) — where `vcms config init` writes
123+
3. the platform config dir (`config.toml`; `$VCMS_HOME/config.toml` in single-dir mode) — where `vcms config init` writes
97124
4. `/etc/vcms/config.toml`
98125

99-
## Data directory (CMS home)
126+
## Data directory
127+
128+
Resolution lives in `apps/backend/src/paths.rs` and has two layouts:
100129

101-
All runtime files live under one home directory: `$VCMS_HOME` if set, else `~/.vcms`
102-
(same layout on Windows, macOS, Linux via the `directories` crate). `vcms serve`
103-
creates it on first run. Resolution lives in `apps/backend/src/paths.rs`.
130+
**Split (default, interactive installs)** — files land in the platform-conventional
131+
per-type directories via the `directories` crate (`ProjectDirs`):
132+
133+
| File(s) | Dir | Linux | macOS | Windows |
134+
|---------|-----|-------|-------|---------|
135+
| `config.toml`, `secrets.toml`, `.env` | config | `~/.config/vcms` | `~/Library/Application Support/vcms` | `%APPDATA%\vcms\config` |
136+
| `vcms.db`, `storage/`, `backups/` | data | `~/.local/share/vcms` | `~/Library/Application Support/vcms` | `%APPDATA%\vcms\data` |
137+
| `search/` (derived, rebuildable) | cache | `~/.cache/vcms` | `~/Library/Caches/vcms` | `%LOCALAPPDATA%\vcms\cache` |
138+
| `logs/` | state | `~/.local/state/vcms` | `~/Library/Application Support/vcms` | `%LOCALAPPDATA%\vcms\data` |
139+
140+
(`logs/` uses the state dir where the platform has one — Linux — else the local data dir.)
141+
142+
**Single** — everything nests under one root. Chosen (in precedence order) when:
143+
1. **`$VCMS_HOME` is set** — forces the root explicitly.
144+
2. **the system service home dir exists** — Linux `/var/lib/vcms`, macOS
145+
`/Library/Application Support/vcms`, Windows `C:\ProgramData\vcms`. The
146+
platform installer creates it (and leaves it behind on uninstall), so a plain
147+
`vcms serve`/`admin`/`backup` **follows the service's data instead of forking to a
148+
per-user split store**. This path is defined once in `paths::system_home()` and
149+
imported by the Windows SCM host.
150+
3. **a legacy `~/.vcms` exists** — an existing install keeps working untouched.
151+
152+
Otherwise (dev/eval boxes with no service) files use the platform split dirs.
104153

105154
```text
106-
~/.vcms/
107-
config.toml # non-secret config (vcms config init target)
108-
secrets.toml # auto-generated HMAC_SECRET + backup key (0600 on unix)
109-
vcms.db # default SQLite database (+ -wal / -shm)
110-
logs/ # rolling logs when [log] output = "file"
111-
storage/ # default filesystem storage for uploads
155+
$VCMS_HOME/ # or system home, or ~/.vcms (legacy)
156+
config.toml secrets.toml .env
157+
vcms.db (+ -wal / -shm) logs/ storage/ backups/ search/
112158
```
113159

114-
Secrets: on first `serve`/`admin`, a random `HMAC_SECRET` is generated
115-
and persisted to `secrets.toml` (`apps/backend/src/secrets.rs`), then loaded by
116-
every process — including `vcms mcp stdio`, which is launched from an arbitrary cwd
117-
and so cannot rely on a cwd `.env`. Env vars still override the file. `mcp stdio`
118-
is read-only: it never creates the home dir, database, or secrets file.
160+
When the active home is the system service home (owned by SYSTEM/root), the
161+
data-touching commands (`serve`/`admin`/`backup`/`restore`) require elevation — a
162+
non-elevated invocation **fails fast** with an "Administrator/root" hint (in
163+
`paths::ensure`'s preflight) rather than silently forking to a second store.
164+
165+
Secrets: on first `serve`/`admin`, a random `HMAC_SECRET` is generated and persisted to
166+
`secrets.toml` (`apps/backend/src/secrets.rs`), then loaded by the server processes.
167+
`vcms mcp stdio` does **not** load secrets, the database, or any data-dir file: it is a
168+
thin HTTP proxy to the running server's `/mcp` (see CLI), forwarding `VCMS_MCP_TOKEN` as
169+
the bearer; the server owns all disk I/O.
119170

120171
Env-only secrets (never read from `config.toml` by convention, omitted from
121172
`config init`): `DATABASE_URL`, `HMAC_SECRET`, `S3_ACCESS_KEY_ID`,
@@ -153,8 +204,10 @@ Logging keys map to the `[log]` table: `RUST_LOG`→`log.level`, `LOG_OUTPUT`→
153204
| Variable | Default | Description |
154205
|----------|---------|-------------|
155206
| `VCMS_CONFIG` | - | Explicit config file path (same as `--config`) |
156-
| `VCMS_HOME` | `~/.vcms` | CMS home directory (db, config, secrets, logs, storage) |
157-
| `DATABASE_URL` | `sqlite://~/.vcms/vcms.db` | Database URL: `sqlite:path`, `postgres://...`, `mysql://...` |
207+
| `VCMS_HOME` | - | If set, forces single-dir mode: db/config/secrets/logs/storage all nest under this root (else files use the platform split dirs) |
208+
| `VCMS_MCP_TOKEN` | - | `vcms_site_*` access token forwarded by `vcms mcp stdio` as the bearer credential (required for stdio) |
209+
| `VCMS_MCP_URL` | `http://127.0.0.1:3000` | Running server's base URL that `vcms mcp stdio` proxies to (`{url}/mcp`) |
210+
| `DATABASE_URL` | `sqlite://<data dir>/vcms.db` | Database URL: `sqlite:path`, `postgres://...`, `mysql://...` |
158211
| `HMAC_SECRET` | auto | HMAC key for token lookup (required; auto-generated to `secrets.toml`, env overrides) |
159212
| `BIND_ADDRESS` | `0.0.0.0:3000` | REST API listen address |
160213
| `GRPC_BIND_ADDRESS` | `0.0.0.0:50051` | gRPC server listen address |
@@ -167,14 +220,15 @@ Logging keys map to the `[log]` table: `RUST_LOG`→`log.level`, `LOG_OUTPUT`→
167220
| `S3_PUBLIC_URL` | - | Public URL for S3 assets |
168221
| `BACKUP_ENABLED` | `true` | Run the scheduled-backup poller / allow backups |
169222
| `BACKUP_DESTINATION` | `filesystem` | Backup destination: `filesystem` or `s3` |
170-
| `BACKUP_LOCAL_PATH` | `~/.vcms/backups` | Local backup dir (when destination is filesystem) |
223+
| `BACKUP_LOCAL_PATH` | `<data dir>/backups` | Local backup dir (when destination is filesystem) |
171224
| `BACKUP_ZSTD_LEVEL` | `12` | zstd compression level for backups |
172225
| `BACKUP_DEFAULT_RETENTION` | `7` | Default "keep last N" for new schedules |
173226
| `BACKUP_S3_BUCKET` / `_REGION` / `_ENDPOINT` / `_PUBLIC_URL` | - | S3 backup destination (non-secret parts) |
174227
| `BACKUP_S3_ACCESS_KEY_ID` | - | S3 backup access key (secret, env-only) |
175228
| `BACKUP_S3_SECRET_ACCESS_KEY` | - | S3 backup secret key (secret, env-only) |
176229
| `BACKUP_ENCRYPTION_KEY` | auto | AES-256 backup key (hex); auto-generated to `secrets.toml` |
177230
| `MAX_UPLOAD_SIZE_MB` | `50` | Max upload size in MB |
231+
| `UPLOAD_TOKEN_EXPIRY_SECS` | `900` | Signed upload URL lifetime (seconds) |
178232
| `COOKIE_SECURE` | `false` | Require HTTPS cookies |
179233
| `DB_MAX_CONNECTIONS` | `10` | Max DB connections |
180234
| `DB_MIN_CONNECTIONS` | `2` | Min DB connections |
@@ -185,10 +239,10 @@ Logging keys map to the `[log]` table: `RUST_LOG`→`log.level`, `LOG_OUTPUT`→
185239
| `LOG_OUTPUT` | `stdout` | `stdout` or `file` (`[log] output`) |
186240
| `LOG_FORMAT` | `pretty` | `pretty` or `json` (`[log] format`) |
187241
| `LOG_ANNOTATIONS` | `false` | Include file + line numbers (`[log] annotations`) |
188-
| `LOG_DIR` | `~/.vcms/logs` | Log directory when `output = file` (`[log] dir`) |
242+
| `LOG_DIR` | `<state dir>/logs` | Log directory when `output = file` (`[log] dir`) |
189243

190-
**Note**: `HMAC_SECRET` is auto-generated and persisted to
191-
`~/.vcms/secrets.toml` on first run. Set it explicitly via env to override.
244+
**Note**: `HMAC_SECRET` is auto-generated and persisted to the config dir's
245+
`secrets.toml` on first run. Set it explicitly via env to override.
192246

193247
## Proto Compilation
194248

0 commit comments

Comments
 (0)