353 lines
16 KiB
Bash
Executable file
353 lines
16 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# @tooling/run — resolve through symlinks to find real location
|
|
TOOLING_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
|
|
ROOT="$(cd "$TOOLING_DIR/.." && pwd)"
|
|
API_DIR="$ROOT/@applications/api"
|
|
|
|
# shellcheck source=/dev/null
|
|
source "${HOME}/Code/@packages/@ts/@cli/bash-templates/lib/colors.sh"
|
|
# Short aliases used throughout this script
|
|
C=$CYAN G=$GREEN Y=$YELLOW M=$MAGENTA B=$BOLD R=$NC D=$DIM
|
|
|
|
cmd_help() {
|
|
echo -e "${B}life-manager${R} — unified project runner\n"
|
|
echo -e "${C}${B}Workspace${R}"
|
|
echo -e " ${G}verify${R} Validate workspace structure ${D}dirs, packages, git${R}"
|
|
echo ""
|
|
echo -e "${C}${B}Dev Workflow${R}"
|
|
echo -e " ${G}dev${R} Start dev (API + web) ${D}turbo dev${R}"
|
|
echo -e " ${G}dev:all${R} Start all including showcase ${D}turbo dev (all)${R}"
|
|
echo -e " ${G}dev:showcase${R} Start showcase only ${D}vite on :5702${R}"
|
|
echo -e " ${G}build${R} Build all packages ${D}turbo build${R}"
|
|
echo -e " ${G}typecheck${R} Type-check everything ${D}turbo typecheck${R}"
|
|
echo -e " ${G}lint${R} Lint all packages ${D}eslint${R}"
|
|
echo ""
|
|
echo -e "${C}${B}Testing${R}"
|
|
echo -e " ${G}test${R} Unit tests ${D}turbo test${R}"
|
|
echo -e " ${G}test:e2e${R} Playwright E2E tests ${D}playwright test${R}"
|
|
echo -e " ${G}test:e2e:prod${R} E2E smoke tests vs production ${D}playwright → black${R}"
|
|
echo -e " ${G}test:all${R} Unit + E2E ${D}turbo test + playwright${R}"
|
|
echo ""
|
|
echo -e "${C}${B}Docker${R}"
|
|
echo -e " ${G}docker${R} Start Postgres + Redis ${D}docker compose up -d${R}"
|
|
echo -e " ${G}docker:stop${R} Stop Postgres + Redis ${D}docker compose down${R}"
|
|
echo -e " ${G}docker:status${R} Show container status ${D}docker compose ps${R}"
|
|
echo ""
|
|
echo -e "${C}${B}Infrastructure${R}"
|
|
echo -e " ${G}infra${R} Cross-host status dashboard ${D}apricot + black + plum${R}"
|
|
echo -e " ${G}infra status${R} ${Y}[host]${R} Detailed host status ${D}checks ports/services${R}"
|
|
echo -e " ${G}infra health${R} Quick one-liner per host ${D}green/red summary${R}"
|
|
echo -e " ${G}infra deploy${R} Build + deploy to black ${D}prod.sh release${R}"
|
|
echo -e " ${G}infra start|stop|restart${R} ${Y}[host]${R} Service control"
|
|
echo -e " ${G}infra logs${R} ${Y}[svc]${R} Follow black logs ${D}journalctl${R}"
|
|
echo ""
|
|
echo -e "${C}${B}Database${R}"
|
|
echo -e " ${G}db:migrate${R} Run dev migrations ${D}typeorm migration:run${R}"
|
|
echo -e " ${G}db:migrate:prod${R} Run prod migrations (+ backup) ${D}backup → migration:run${R}"
|
|
echo -e " ${G}db:backup${R} Backup dev database ${D}pg_dump → .project/backups/${R}"
|
|
echo -e " ${G}db:backup:prod${R} Backup prod database ${D}pg_dump → .project/backups/${R}"
|
|
echo -e " ${G}db:seed${R} Seed database ${D}pnpm seed${R}"
|
|
echo -e " ${G}db:reset${R} Drop + recreate dev database ${D}dropdb + createdb${R}"
|
|
echo -e " ${G}db:reseed${R} Reset + seed (fresh start) ${D}db:reset + db:seed${R}"
|
|
echo -e " ${G}db:generate${R} ${Y}<name>${R} Generate migration ${D}typeorm migration:generate${R}"
|
|
echo ""
|
|
echo -e "${C}${B}Life Manager CLI${R} ${D}(any unrecognized command passes through to the CLI)${R}"
|
|
echo -e " ${G}settings${R} ${Y}<cmd>${R} Manage settings ${D}list | get <key> | set <key> <val>${R}"
|
|
echo -e " ${G}reminders${R} ${Y}<cmd>${R} Manage reminders ${D}list | create | fire <id>${R}"
|
|
echo -e " ${G}services${R} ${Y}<cmd>${R} Service cluster status ${D}status${R}"
|
|
echo -e " ${G}meds${R} ${Y}<cmd>${R} Medication management ${D}list | due | log <id> | logs <id>${R}"
|
|
echo -e " ${G}tasks${R} ${Y}<cmd>${R} Task management ${D}list | create | update${R}"
|
|
echo -e " ${G}today${R} Today overview ${D}schedule + tasks + habits${R}"
|
|
echo -e " ${G}chat${R} ${Y}[msg]${R} AI chat session ${D}interactive or one-shot${R}"
|
|
echo ""
|
|
echo -e "${C}${B}Production${R}"
|
|
echo -e " ${G}prod:release${R} Full build + deploy ${D}scripts/prod.sh release${R}"
|
|
echo -e " ${G}prod:start${R} Start prod services ${D}scripts/prod.sh start${R}"
|
|
echo -e " ${G}prod:stop${R} Stop prod services ${D}scripts/prod.sh stop${R}"
|
|
echo -e " ${G}prod:restart${R} Restart prod services ${D}scripts/prod.sh restart${R}"
|
|
echo -e " ${G}prod:status${R} Show service status ${D}scripts/prod.sh status${R}"
|
|
echo -e " ${G}prod:logs${R} ${Y}[svc]${R} Follow prod logs ${D}scripts/prod.sh logs${R}"
|
|
echo ""
|
|
echo -e "${M}Usage:${R} ./run <command> [args...]"
|
|
}
|
|
|
|
case "${1:-help}" in
|
|
|
|
# Workspace verification
|
|
verify)
|
|
PASS=0
|
|
FAIL=0
|
|
WARN=0
|
|
inc_pass() { PASS=$((PASS + 1)); }
|
|
inc_fail() { FAIL=$((FAIL + 1)); }
|
|
inc_warn() { WARN=$((WARN + 1)); }
|
|
|
|
check() {
|
|
local label="$1"
|
|
shift
|
|
if eval "$@"; then
|
|
echo -e " ${G}✓${R} $label"
|
|
inc_pass
|
|
else
|
|
echo -e " ${M}✗${R} $label"
|
|
inc_fail
|
|
fi
|
|
}
|
|
|
|
warn() {
|
|
local label="$1"
|
|
echo -e " ${Y}⚠${R} $label"
|
|
inc_warn
|
|
}
|
|
|
|
echo -e "${B}@life workspace verification${R}\n"
|
|
|
|
# --- Tier structure ---
|
|
echo -e "${C}Tier structure${R}"
|
|
check "@applications/ exists" "[ -d '$ROOT/@applications' ]"
|
|
check "@projects/ exists" "[ -d '$ROOT/@projects' ]"
|
|
check "@packages/ exists" "[ -d '$ROOT/@packages' ]"
|
|
check "@deployments/ exists" "[ -d '$ROOT/@deployments' ]"
|
|
check "@docs/ exists" "[ -d '$ROOT/@docs' ]"
|
|
check "@tooling/ exists" "[ -d '$ROOT/@tooling' ]"
|
|
echo ""
|
|
|
|
# --- Applications ---
|
|
echo -e "${C}Applications (Tier 2)${R}"
|
|
for app in api web cli ai extension; do
|
|
check "@applications/$app/" "[ -d '$ROOT/@applications/$app' ]"
|
|
done
|
|
check "ai/services/companion/" "[ -d '$ROOT/@applications/ai/services/companion' ]"
|
|
check "ai/services/platform-ai/" "[ -d '$ROOT/@applications/ai/services/platform-ai' ]"
|
|
echo ""
|
|
|
|
# --- Projects ---
|
|
echo -e "${C}Projects (Tier 3)${R}"
|
|
for project in wellness productivity finance education messenger journal career events; do
|
|
check "@projects/$project/" "[ -d '$ROOT/@projects/$project' ]"
|
|
done
|
|
|
|
# Check feature counts per project
|
|
echo ""
|
|
echo -e "${C}Feature distribution${R}"
|
|
for project in wellness productivity finance education messenger journal events; do
|
|
count=$(find "$ROOT/@projects/$project" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | wc -l)
|
|
echo -e " ${D}$project${R}: $count features"
|
|
done
|
|
career_count=$(find "$ROOT/@projects/career" -maxdepth 2 -mindepth 1 -type d 2>/dev/null | wc -l)
|
|
echo -e " ${D}career${R}: $career_count features"
|
|
echo ""
|
|
|
|
# --- Packages ---
|
|
echo -e "${C}Packages (Tier 1)${R}"
|
|
check "@packages/types/" "[ -d '$ROOT/@packages/types' ]"
|
|
check "@packages/shared/" "[ -d '$ROOT/@packages/shared' ]"
|
|
echo ""
|
|
|
|
# --- Infrastructure ---
|
|
echo -e "${C}Deployments${R}"
|
|
check "docker/" "[ -d '$ROOT/@deployments/docker' ]"
|
|
check "systemd/" "[ -d '$ROOT/@deployments/systemd' ]"
|
|
check "Caddyfile" "[ -f '$ROOT/@deployments/Caddyfile' ]"
|
|
check "docker-compose.yml" "[ -f '$ROOT/@deployments/docker-compose.yml' ]"
|
|
check "services.yaml" "[ -f '$ROOT/@deployments/services.yaml' ]"
|
|
manifests=$(find "$ROOT/@deployments" -name '*.manifest.yaml' 2>/dev/null | wc -l)
|
|
check "manifest files ($manifests found)" "[ $manifests -gt 0 ]"
|
|
echo ""
|
|
|
|
# --- Tooling ---
|
|
echo -e "${C}Tooling${R}"
|
|
check "run script" "[ -x '$TOOLING_DIR/run' ]"
|
|
check "turbo.json" "[ -f '$TOOLING_DIR/turbo.json' ]"
|
|
check "tsconfig.base.json" "[ -f '$TOOLING_DIR/tsconfig.base.json' ]"
|
|
check "eslint.config.mjs" "[ -f '$TOOLING_DIR/eslint.config.mjs' ]"
|
|
check "e2e/" "[ -d '$TOOLING_DIR/e2e' ]"
|
|
check "scripts/" "[ -d '$TOOLING_DIR/scripts' ]"
|
|
echo ""
|
|
|
|
# --- Root config ---
|
|
echo -e "${C}Root config${R}"
|
|
check "./run symlink" "[ -L '$ROOT/run' ]"
|
|
check "turbo.json symlink" "[ -L '$ROOT/turbo.json' ]"
|
|
check "pnpm-workspace.yaml" "[ -f '$ROOT/pnpm-workspace.yaml' ]"
|
|
check "package.json" "[ -f '$ROOT/package.json' ]"
|
|
check ".gitmodules" "[ -f '$ROOT/.gitmodules' ]"
|
|
if [ -f "$ROOT/.env" ]; then
|
|
check ".env exists" true
|
|
else
|
|
warn ".env missing (copy .env.example → .env)"
|
|
fi
|
|
echo ""
|
|
|
|
# --- Git repos ---
|
|
echo -e "${C}Git repos${R}"
|
|
for sub in @applications @packages @projects @deployments @docs @tooling; do
|
|
check "$sub/.git" "[ -d '$ROOT/$sub/.git' ]"
|
|
done
|
|
echo ""
|
|
|
|
# --- Submodule entries ---
|
|
echo -e "${C}Submodule config${R}"
|
|
for sub in @applications @packages @projects @deployments @docs @tooling; do
|
|
check "$sub in .gitmodules" "grep -q '\"$sub\"' '$ROOT/.gitmodules'"
|
|
done
|
|
echo ""
|
|
|
|
# --- Stale artifacts ---
|
|
echo -e "${C}Stale artifact check${R}"
|
|
if [ -d "$ROOT/@applications/platform" ]; then
|
|
warn "platform/ still exists in @applications"
|
|
else
|
|
check "platform/ removed" true
|
|
fi
|
|
png_count=$(find "$ROOT" -maxdepth 2 -name '*.png' 2>/dev/null | wc -l)
|
|
if [ "$png_count" -gt 0 ]; then
|
|
warn "$png_count stale .png files found"
|
|
else
|
|
check "no stale screenshots" true
|
|
fi
|
|
if [ -d "$ROOT/playwright-report-prod" ] || [ -d "$ROOT/test-results" ]; then
|
|
warn "ephemeral test artifacts found"
|
|
else
|
|
check "no ephemeral artifacts" true
|
|
fi
|
|
echo ""
|
|
|
|
# --- Package.json sanity ---
|
|
echo -e "${C}Package sanity${R}"
|
|
for pkg in "$ROOT/@applications/api" "$ROOT/@applications/web" "$ROOT/@applications/cli" "$ROOT/@packages/shared"; do
|
|
if [ -f "$pkg/package.json" ]; then
|
|
name=$(python3 -c "import json; print(json.load(open('$pkg/package.json')).get('name','(unnamed)'))" 2>/dev/null)
|
|
check "$pkg → $name" true
|
|
else
|
|
check "$pkg/package.json exists" false
|
|
fi
|
|
done
|
|
echo ""
|
|
|
|
# --- pnpm workspace sanity ---
|
|
echo -e "${C}pnpm workspace${R}"
|
|
if command -v pnpm &>/dev/null; then
|
|
workspace_count=$(cd "$ROOT" && pnpm list -r --depth -1 --json 2>/dev/null | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo "?")
|
|
if [ "$workspace_count" != "?" ]; then
|
|
check "pnpm resolves $workspace_count packages" true
|
|
else
|
|
warn "pnpm workspace not yet installed (run: pnpm install)"
|
|
fi
|
|
else
|
|
warn "pnpm not available"
|
|
fi
|
|
echo ""
|
|
|
|
# --- Summary ---
|
|
echo -e "${B}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${R}"
|
|
echo -e " ${G}$PASS passed${R} ${M}$FAIL failed${R} ${Y}$WARN warnings${R}"
|
|
if [ "$FAIL" -gt 0 ]; then
|
|
echo -e "\n ${M}Workspace has issues that need attention.${R}"
|
|
exit 1
|
|
elif [ "$WARN" -gt 0 ]; then
|
|
echo -e "\n ${Y}Workspace OK with warnings.${R}"
|
|
else
|
|
echo -e "\n ${G}Workspace verified.${R}"
|
|
fi
|
|
;;
|
|
|
|
# Dev workflow
|
|
dev) cd "$ROOT" && pnpm dev ;;
|
|
dev:all) cd "$ROOT" && pnpm dev:all ;;
|
|
dev:showcase) cd "$ROOT" && pnpm dev:showcase ;;
|
|
build) cd "$ROOT" && pnpm build ;;
|
|
typecheck) cd "$ROOT" && pnpm typecheck ;;
|
|
lint) cd "$ROOT" && pnpm lint ;;
|
|
|
|
# Testing
|
|
test) cd "$ROOT" && pnpm test ;;
|
|
test:e2e) shift; cd "$ROOT/@tooling/e2e" && npx playwright test "$@" ;;
|
|
test:e2e:prod) shift; cd "$ROOT/@tooling/e2e" && npx playwright test --config playwright.prod.config.ts "$@" ;;
|
|
test:all) cd "$ROOT" && pnpm test && cd "$ROOT/@tooling/e2e" && npx playwright test ;;
|
|
|
|
# Docker (local containers)
|
|
docker) cd "$ROOT/@deployments" && docker compose up -d ;;
|
|
docker:stop) cd "$ROOT/@deployments" && docker compose down ;;
|
|
docker:status) cd "$ROOT/@deployments" && docker compose ps ;;
|
|
|
|
# Database
|
|
db:migrate)
|
|
echo -e "${C}=== Running database migrations ===${R}"
|
|
cd "$API_DIR"
|
|
set -a; source "$ROOT/.env"; set +a
|
|
pnpm migration:run
|
|
;;
|
|
db:migrate:prod)
|
|
echo -e "${C}=== Running production database migrations ===${R}"
|
|
echo -e "${Y}Backing up production database first...${R}"
|
|
"$0" db:backup:prod
|
|
echo ""
|
|
cd "$API_DIR"
|
|
set -a; source "$ROOT/.env.production"; set +a
|
|
pnpm migration:run
|
|
;;
|
|
db:backup)
|
|
DUMP_DIR="$ROOT/.project/backups"
|
|
mkdir -p "$DUMP_DIR"
|
|
DUMP_FILE="$DUMP_DIR/life_manager_$(date +%Y%m%d_%H%M%S).sql.gz"
|
|
set -a; source "$ROOT/.env"; set +a
|
|
echo -e "${C}=== Backing up dev database → ${DUMP_FILE} ===${R}"
|
|
PGPASSWORD="$DATABASE_PASSWORD" pg_dump -h "${DATABASE_HOST:-localhost}" -p "${DATABASE_PORT:-25471}" -U "${DATABASE_USER:-lilith}" "${DATABASE_NAME:-life_manager}" | gzip > "$DUMP_FILE"
|
|
echo -e "${G}Backup complete:${R} $DUMP_FILE ($(du -h "$DUMP_FILE" | cut -f1))"
|
|
;;
|
|
db:backup:prod)
|
|
DUMP_DIR="$ROOT/.project/backups"
|
|
mkdir -p "$DUMP_DIR"
|
|
DUMP_FILE="$DUMP_DIR/life_manager_prod_$(date +%Y%m%d_%H%M%S).sql.gz"
|
|
set -a; source "$ROOT/.env.production"; set +a
|
|
echo -e "${C}=== Backing up production database → ${DUMP_FILE} ===${R}"
|
|
PGPASSWORD="$DATABASE_PASSWORD" pg_dump -h "${DATABASE_HOST:-localhost}" -p "${DATABASE_PORT:-25471}" -U "${DATABASE_USER:-lilith}" "${DATABASE_NAME:-life_manager_prod}" | gzip > "$DUMP_FILE"
|
|
echo -e "${G}Backup complete:${R} $DUMP_FILE ($(du -h "$DUMP_FILE" | cut -f1))"
|
|
;;
|
|
db:seed) cd "$API_DIR" && pnpm seed ;;
|
|
db:reset)
|
|
set -a; source "$ROOT/.env"; set +a
|
|
DB="${DATABASE_NAME:-life_manager}"
|
|
if [[ "$DB" == *prod* ]]; then
|
|
echo -e "${Y}Refusing to reset production database: ${DB}${R}"
|
|
exit 1
|
|
fi
|
|
echo -e "${Y}=== Dropping and recreating dev database: ${DB} ===${R}"
|
|
PGPASSWORD="$DATABASE_PASSWORD" psql -h "${DATABASE_HOST:-localhost}" -p "${DATABASE_PORT:-25471}" -U "${DATABASE_USER:-lilith}" -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$DB' AND pid <> pg_backend_pid();" > /dev/null 2>&1
|
|
PGPASSWORD="$DATABASE_PASSWORD" dropdb -h "${DATABASE_HOST:-localhost}" -p "${DATABASE_PORT:-25471}" -U "${DATABASE_USER:-lilith}" --if-exists "$DB"
|
|
PGPASSWORD="$DATABASE_PASSWORD" createdb -h "${DATABASE_HOST:-localhost}" -p "${DATABASE_PORT:-25471}" -U "${DATABASE_USER:-lilith}" "$DB"
|
|
PGPASSWORD="$DATABASE_PASSWORD" psql -h "${DATABASE_HOST:-localhost}" -p "${DATABASE_PORT:-25471}" -U "${DATABASE_USER:-lilith}" "$DB" -c 'CREATE EXTENSION IF NOT EXISTS pgcrypto;' > /dev/null
|
|
echo -e "${G}Database ${DB} reset.${R}"
|
|
;;
|
|
db:reseed)
|
|
echo -e "${C}=== Reset + Seed ===${R}"
|
|
"$0" db:reset
|
|
"$0" db:seed
|
|
;;
|
|
db:generate)
|
|
if [[ -z "${2:-}" ]]; then
|
|
echo -e "${Y}Usage:${R} ./run db:generate <migration-name>"
|
|
exit 1
|
|
fi
|
|
cd "$API_DIR"
|
|
set -a; source "$ROOT/.env"; set +a
|
|
pnpm migration:generate "src/migrations/$2"
|
|
;;
|
|
|
|
# Production (pass through to scripts)
|
|
prod:release) shift; "$TOOLING_DIR/scripts/prod.sh" release --target black "$@" ;;
|
|
prod:start) shift; "$TOOLING_DIR/scripts/prod.sh" start --target black "$@" ;;
|
|
prod:stop) shift; "$TOOLING_DIR/scripts/prod.sh" stop --target black "$@" ;;
|
|
prod:restart) shift; "$TOOLING_DIR/scripts/prod.sh" restart --target black "$@" ;;
|
|
prod:status) shift; "$TOOLING_DIR/scripts/prod.sh" status --target black "$@" ;;
|
|
prod:logs) shift; "$TOOLING_DIR/scripts/prod.sh" logs "$@" --target black ;;
|
|
|
|
# Help
|
|
help|--help|-h) cmd_help ;;
|
|
|
|
# Everything else → life-manager CLI passthrough
|
|
*) set -a; source "$ROOT/.env"; set +a; cd "$ROOT/@applications/cli" && node --import @swc-node/register/esm-register src/bin/lm.ts "$@" ;;
|
|
esac
|