mirror of
https://github.com/bybrooklyn/alchemist.git
synced 2026-04-18 01:43:34 -04:00
445 lines
17 KiB
Makefile
445 lines
17 KiB
Makefile
# 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)"
|