# Alchemist — Justfile # https://github.com/casey/just # # Install: cargo install just | brew install just | pacman -S just set shell := ["bash", "-euo", "pipefail", "-c"] set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] # ───────────────────────────────────────── # Variables # ───────────────────────────────────────── VERSION := if os_family() == "windows" { `(Get-Content VERSION -Raw).Trim()` } else { `tr -d '[:space:]' < VERSION` } # ───────────────────────────────────────── # Default — list all recipes # ───────────────────────────────────────── [private] default: @just --list # ───────────────────────────────────────── # DEVELOPMENT # ───────────────────────────────────────── # Install repo dependencies needed for local development install: @just {{ if os_family() == "windows" { "install-w" } else { "install-u" } }} [private] install-u: @command -v cargo >/dev/null || { echo "error: cargo is required"; exit 1; } @command -v bun >/dev/null || { echo "error: bun is required"; exit 1; } @echo "── Rust dependencies ──" cargo fetch --locked @echo "── Web dependencies ──" cd web && bun install --frozen-lockfile @echo "── Docs dependencies ──" cd docs && bun install --frozen-lockfile @echo "── E2E dependencies ──" cd web-e2e && bun install --frozen-lockfile && bunx playwright install chromium @if ! command -v ffmpeg >/dev/null; then \ echo "warning: ffmpeg is not installed; media integration tests will not run"; \ fi @echo "Repo ready for development." @echo "Next: just dev" install-w: @powershell.exe -NoLogo -ExecutionPolicy Bypass -File .\\scripts\\install_dev_windows.ps1 # Build frontend assets, then start the backend server dev: @just {{ if os_family() == "windows" { "dev-w" } else { "dev-u" } }} [private] dev-u: web-build @just run dev-w: @powershell.exe -NoLogo -ExecutionPolicy Bypass -File .\\scripts\\dev_windows.ps1 # Start the backend only run: cargo run # Start frontend dev server only web: cd web && bun install --frozen-lockfile && bun run dev # Start documentation dev server only docs: cd docs && bun install --frozen-lockfile && bun run start # ───────────────────────────────────────── # BUILD # ───────────────────────────────────────── # Full production build — frontend first, then Rust build: @echo "Building frontend..." cd web && bun install --frozen-lockfile && bun run build @echo "Building Rust binary..." cargo build --release @echo "Done → target/release/alchemist" # Build frontend assets only web-build: cd web && bun install --frozen-lockfile && bun run build # Build Rust only (assumes web/dist already exists) rust-build: cargo build --release # ───────────────────────────────────────── # CHECKS — mirrors CI exactly # ───────────────────────────────────────── # Run all checks (fmt + clippy + typecheck + frontend build) check: @just {{ if os_family() == "windows" { "check-w" } else { "check-u" } }} [private] check-u: @echo "── Rust format ──" cargo fmt --all -- --check @echo "── Rust clippy ──" cargo clippy --all-targets --all-features -- -D warnings -D clippy::unwrap_used -D clippy::expect_used @echo "── Rust check ──" cargo check --all-targets @echo "── Frontend typecheck ──" cd web && bun install --frozen-lockfile && bun run typecheck && echo "── Frontend build ──" && bun run build @echo "All checks passed ✓" check-w: @powershell.exe -NoLogo -ExecutionPolicy Bypass -File .\\scripts\\check_windows.ps1 # Rust checks only (faster) check-rust: cargo fmt --all -- --check cargo clippy --all-targets --all-features -- -D warnings -D clippy::unwrap_used -D clippy::expect_used cargo check --all-targets # Frontend checks only check-web: cd web && bun install --frozen-lockfile && bun run typecheck && bun run build cd web-e2e && bun install --frozen-lockfile && bun run test # ───────────────────────────────────────── # TESTS # ───────────────────────────────────────── # Run all Rust tests test: cargo test # Run Rust tests with output shown test-verbose: cargo test -- --nocapture # Run a specific test by name (e.g. just test-filter stream_rules) test-filter FILTER: cargo test {{FILTER}} -- --nocapture # Run frontend e2e reliability tests test-e2e: cd web-e2e && bun install --frozen-lockfile && bun run test:reliability # Run all e2e tests headed (for debugging) test-e2e-headed: cd web-e2e && bun install --frozen-lockfile && bun run test:headed # Run all e2e tests with Playwright UI test-e2e-ui: cd web-e2e && bun install --frozen-lockfile && bun run test:ui # ───────────────────────────────────────── # DATABASE # ───────────────────────────────────────── # Wipe the dev database (essential for re-testing the setup wizard) db-reset: @DB="${ALCHEMIST_DB_PATH:-${XDG_CONFIG_HOME:-$HOME/.config}/alchemist/alchemist.db}"; \ echo "Deleting $DB"; \ rm -f "$DB" "$DB-wal" "$DB-shm"; \ echo "Done — next run will re-apply migrations." # Wipe dev database AND config (full clean slate, triggers setup wizard) db-reset-all: @DB="${ALCHEMIST_DB_PATH:-${XDG_CONFIG_HOME:-$HOME/.config}/alchemist/alchemist.db}"; \ CFG="${ALCHEMIST_CONFIG_PATH:-${XDG_CONFIG_HOME:-$HOME/.config}/alchemist/config.toml}"; \ echo "Deleting $DB and $CFG"; \ rm -f "$DB" "$DB-wal" "$DB-shm" "$CFG"; \ echo "Done — setup wizard will run on next launch." # Open the dev database in sqlite3 db-shell: @sqlite3 "${ALCHEMIST_DB_PATH:-${XDG_CONFIG_HOME:-$HOME/.config}/alchemist/alchemist.db}" # Show applied migrations db-migrations: @sqlite3 "${ALCHEMIST_DB_PATH:-${XDG_CONFIG_HOME:-$HOME/.config}/alchemist/alchemist.db}" \ "SELECT version, description, installed_on FROM _sqlx_migrations ORDER BY installed_on;" # ───────────────────────────────────────── # DOCKER # ───────────────────────────────────────── # Build the Docker image locally docker-build: docker build -t alchemist:dev . # Build multi-arch image (requires buildx; add --push to push to registry) docker-build-multi: docker buildx build --platform linux/amd64,linux/arm64 -t alchemist:dev . # Run Alchemist in Docker for local testing docker-run: docker run --rm \ -p 3000:3000 \ -v "$(pwd)/dev-data:/app/data" \ -e ALCHEMIST_DB_PATH=/app/data/alchemist.db \ -e ALCHEMIST_CONFIG_MUTABLE=true \ -e RUST_LOG=info \ alchemist:dev # Start the Docker Compose stack docker-up: docker compose up -d # Stop the Docker Compose stack docker-down: docker compose down # Tail Docker Compose logs docker-logs: docker compose logs -f # ───────────────────────────────────────── # VERSIONING & RELEASE # ───────────────────────────────────────── # Bump version across repo version files only (e.g. just bump 0.3.0 or just bump 0.3.0-rc.1) bump NEW_VERSION: @echo "Bumping to {{NEW_VERSION}}..." bash scripts/bump_version.sh {{NEW_VERSION}} [private] release-verify: @echo "── Rust format ──" cargo fmt --all -- --check @echo "── Rust clippy ──" cargo clippy --locked --all-targets --all-features -- -D warnings -D clippy::unwrap_used -D clippy::expect_used @echo "── Rust check ──" cargo check --locked --all-targets --all-features @echo "── Rust tests ──" cargo test --locked --all-targets -- --test-threads=4 @echo "── Actionlint ──" actionlint .github/workflows/*.yml @echo "── Web verify ──" cd web && bun install --frozen-lockfile && bun run verify && python3 ../scripts/run_bun_audit.py . @echo "── Docs verify ──" cd docs && bun install --frozen-lockfile && bun run build && python3 ../scripts/run_bun_audit.py . @echo "── E2E backend build ──" rm -rf target/debug/incremental CARGO_INCREMENTAL=0 cargo build --locked --no-default-features @echo "── E2E reliability ──" @E2E_PORT=""; \ for port in $(seq 4173 4273); do \ if ! lsof -nPiTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then \ E2E_PORT="$port"; \ break; \ fi; \ done; \ if [ -z "$E2E_PORT" ]; then \ echo "error: no free web-e2e port found in 4173-4273" >&2; \ exit 1; \ fi; \ echo "Using web-e2e port ${E2E_PORT}"; \ cd web-e2e && bun install --frozen-lockfile && ALCHEMIST_E2E_PORT="${E2E_PORT}" bun run test:reliability # Checkpoint dirty local work with confirmation, then bump, validate, commit, tag, and push # (blocks behind/diverged remote state; e.g. just update 0.3.0-rc.1 or just update v0.3.0-rc.1) update NEW_VERSION: @RAW_VERSION="{{NEW_VERSION}}"; \ NEW_VERSION="${RAW_VERSION#v}"; \ CURRENT_VERSION="{{VERSION}}"; \ TAG="v${NEW_VERSION}"; \ BRANCH="$(git branch --show-current)"; \ if [ -z "${NEW_VERSION}" ]; then \ echo "error: version must not be empty" >&2; \ exit 1; \ fi; \ if [ "${NEW_VERSION}" = "${CURRENT_VERSION}" ]; then \ echo "error: version ${NEW_VERSION} is already current" >&2; \ exit 1; \ fi; \ if [ -z "${BRANCH}" ]; then \ echo "error: detached HEAD is not supported for just update" >&2; \ exit 1; \ fi; \ if ! git remote get-url origin >/dev/null 2>&1; then \ echo "error: origin remote does not exist" >&2; \ exit 1; \ fi; \ if [ -n "$(git status --porcelain)" ]; then \ echo "── Local changes detected ──"; \ git status --short; \ if [ ! -r /dev/tty ]; then \ echo "error: interactive confirmation requires a TTY" >&2; \ exit 1; \ fi; \ printf "Checkpoint current local changes before release? [y/N] " > /dev/tty; \ read -r RESPONSE < /dev/tty; \ case "${RESPONSE}" in \ [Yy]|[Yy][Ee][Ss]) \ git add -A; \ git commit -m "chore: checkpoint before release ${TAG}"; \ ;; \ *) \ echo "error: aborted because local changes were not checkpointed" >&2; \ exit 1; \ ;; \ esac; \ fi; \ git fetch --quiet --prune --tags origin; \ if git show-ref --verify --quiet "refs/remotes/origin/${BRANCH}"; then \ LOCAL_HEAD="$(git rev-parse HEAD)"; \ REMOTE_HEAD="$(git rev-parse "refs/remotes/origin/${BRANCH}")"; \ BASE_HEAD="$(git merge-base HEAD "refs/remotes/origin/${BRANCH}")"; \ if [ "${LOCAL_HEAD}" != "${REMOTE_HEAD}" ]; then \ if [ "${BASE_HEAD}" = "${LOCAL_HEAD}" ]; then \ echo "error: branch ${BRANCH} is behind origin/${BRANCH}; pull before running just update" >&2; \ exit 1; \ fi; \ if [ "${BASE_HEAD}" != "${REMOTE_HEAD}" ]; then \ echo "error: branch ${BRANCH} has diverged from origin/${BRANCH}; reconcile it before running just update" >&2; \ exit 1; \ fi; \ fi; \ fi; \ if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null 2>&1; then \ echo "error: local tag ${TAG} already exists" >&2; \ exit 1; \ fi; \ if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then \ echo "error: remote tag ${TAG} already exists on origin" >&2; \ exit 1; \ fi; \ echo "── Bump version to ${NEW_VERSION} ──"; \ bash scripts/bump_version.sh "${NEW_VERSION}"; \ just release-verify; \ PACKAGE_FILES=(); \ while IFS= read -r line; do [ -n "$line" ] && PACKAGE_FILES+=("$line"); done < <(git ls-files -- 'package.json' '*/package.json'); \ CHANGED_TRACKED=(); \ while IFS= read -r line; do [ -n "$line" ] && CHANGED_TRACKED+=("$line"); done < <(git diff --name-only --); \ if [ "${#CHANGED_TRACKED[@]}" -eq 0 ]; then \ echo "error: bump completed but no tracked files changed" >&2; \ exit 1; \ fi; \ for file in "${CHANGED_TRACKED[@]}"; do \ case "${file}" in \ VERSION|Cargo.toml|Cargo.lock|CHANGELOG.md|docs/docs/changelog.md|package.json|*/package.json) ;; \ *) \ echo "error: unexpected tracked change after validation: ${file}" >&2; \ exit 1; \ ;; \ esac; \ done; \ git add -- VERSION Cargo.toml Cargo.lock CHANGELOG.md docs/docs/changelog.md; \ if [ "${#PACKAGE_FILES[@]}" -gt 0 ]; then \ git add -- "${PACKAGE_FILES[@]}"; \ fi; \ if git diff --cached --quiet; then \ echo "error: no version files were staged for commit" >&2; \ exit 1; \ fi; \ echo "── Commit release ──"; \ git commit -m "chore: release ${TAG}"; \ echo "── Tag release ──"; \ git tag -a "${TAG}" -m "${TAG}"; \ echo "── Push branch ──"; \ git push origin "${BRANCH}"; \ echo "── Push tag ──"; \ git push origin "refs/tags/${TAG}" # Show current version version: @echo "{{VERSION}}" # Open CHANGELOG.md changelog: ${EDITOR:-vi} CHANGELOG.md # Print a pre-filled changelog entry header for pasting changelog-entry: @printf '\n## [{{VERSION}}] - %s\n\n### Added\n- \n\n### Changed\n- \n\n### Fixed\n- \n\n' \ "$(date +%Y-%m-%d)" # Run all checks and tests, then print release steps release-check: @echo "── Release checklist for v{{VERSION}} ──" @just release-verify @echo "" @echo "✓ All checks passed. Next steps:" @echo " 1. Update CHANGELOG.md and docs/docs/changelog.md" @echo " 2. Complete the manual checklist in RELEASING.md" @echo " 3. Commit and merge the release-prep changes" @echo " 4. Tag v{{VERSION}} on the exact merged commit when ready" # ───────────────────────────────────────── # UTILITIES # ───────────────────────────────────────── # Format all Rust code fmt: cargo fmt --all # Clean all build artifacts clean: cargo clean rm -rf web/dist web/node_modules web-e2e/node_modules docs/node_modules docs/build # Count lines of source code loc: @echo "── Rust ──" @count=0; \ if [ -d src ]; then \ count=$(find src -type f -name "*.rs" -exec cat {} + | wc -l | tr -d '[:space:]'); \ fi; \ printf "%8s total\n" "$count" @echo "── Frontend ──" @count=0; \ if [ -d web/src ]; then \ count=$(find web/src -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.astro" \) -exec cat {} + | wc -l | tr -d '[:space:]'); \ fi; \ printf "%8s total\n" "$count" @echo "── Tests ──" @count=0; \ paths=(); \ [ -d tests ] && paths+=(tests); \ [ -d web-e2e/tests ] && paths+=(web-e2e/tests); \ if [ ${#paths[@]} -gt 0 ]; then \ count=$(find "${paths[@]}" -type f \( -name "*.rs" -o -name "*.ts" \) -exec cat {} + | wc -l | tr -d '[:space:]'); \ fi; \ printf "%8s total\n" "$count" # Show all environment variables Alchemist respects env-help: @echo "ALCHEMIST_CONFIG_PATH Config file path" @echo " Linux/macOS default: ~/.config/alchemist/config.toml" @echo " Windows default: %APPDATA%\\Alchemist\\config.toml" @echo "ALCHEMIST_CONFIG Alias for ALCHEMIST_CONFIG_PATH" @echo "ALCHEMIST_DB_PATH SQLite database path" @echo " Linux/macOS default: ~/.config/alchemist/alchemist.db" @echo " Windows default: %APPDATA%\\Alchemist\\alchemist.db" @echo "ALCHEMIST_DATA_DIR Override data directory for the DB file" @echo "ALCHEMIST_CONFIG_MUTABLE Allow runtime config writes (default: true)" @echo "XDG_CONFIG_HOME Respected on Linux/macOS if set" @echo "RUST_LOG Log level (e.g. info, debug, alchemist=trace)"