#!/usr/bin/env bash # ---------------------------------------------------------------------------- # AliasVault dev app starter. # # Use this to start AliasVault apps in dev mode with a preconfigured port # setup: every service runs from source on a single, consistent set of ports, # so the apps always know where to find each other. # # Usage: # ./scripts/dev.sh # interactive menu (pick an app) # ./scripts/dev.sh # start one app: api | client | admin | smtp | taskrunner | ext | mobile # ./scripts/dev.sh db-start # start (only) the dev database # ./scripts/dev.sh db-stop # stop & remove this instance's dev database # ./scripts/dev.sh ports # print the resolved port map and exit # ---------------------------------------------------------------------------- set -euo pipefail # --- Resolve repo paths -------------------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$ROOT_DIR" # --- Colors (no-op if not a TTY) ---------------------------------------------- if [ -t 1 ]; then BOLD=$'\033[1m'; DIM=$'\033[2m'; GREEN=$'\033[32m'; CYAN=$'\033[36m' YELLOW=$'\033[33m'; RED=$'\033[31m'; NC=$'\033[0m' else BOLD=""; DIM=""; GREEN=""; CYAN=""; YELLOW=""; RED=""; NC="" fi info() { printf "%s==>%s %s\n" "$CYAN" "$NC" "$1"; } warn() { printf "%s! %s%s\n" "$YELLOW" "$1" "$NC"; } die() { printf "%sError:%s %s\n" "$RED" "$NC" "$1" >&2; exit 1; } LOCAL_ENV="$ROOT_DIR/dev.env" # --- Config defaults ----------------------------------------------------------- DEFAULT_BASE_PORT=5100 # first port of the block; every service counts up from here DEFAULT_PORT_STRIDE=10 # how far apart consecutive instances are spaced # Write the dev.env template, with AV_INSTANCE pinned to the given value. seed_local_env() { cat > "$LOCAL_ENV" < ${DEFAULT_BASE_PORT}..$((DEFAULT_BASE_PORT + 8)) (default) # AV_INSTANCE=1 -> $((DEFAULT_BASE_PORT + DEFAULT_PORT_STRIDE))..$((DEFAULT_BASE_PORT + DEFAULT_PORT_STRIDE + 8)) # AV_INSTANCE=2 -> $((DEFAULT_BASE_PORT + 2 * DEFAULT_PORT_STRIDE))..$((DEFAULT_BASE_PORT + 2 * DEFAULT_PORT_STRIDE + 8)) # ... # ---------------------------------------------------------------------------- AV_INSTANCE=$1 AV_BASE_PORT=$DEFAULT_BASE_PORT AV_PORT_STRIDE=$DEFAULT_PORT_STRIDE # UseDebugEncryptionKey for the Blazor client. 'true' uses a fixed debug # encryption key so local logins skip the slow key derivation, speeds up dev. # Note: this can conflict with some things like password verification steps. # Switch to 'false' to use the actual user unlock flow compatible with production. AV_USE_DEBUG_ENCRYPTION_KEY=true ENV } # --- Load port config ---------------------------------------------------------- # First run: create a dev.env set to instance 0. if [ ! -f "$LOCAL_ENV" ]; then seed_local_env 0 info "Created dev.env (instance 0). Bump AV_INSTANCE there if the default ports are in use." fi # shellcheck disable=SC1091 set -a . "$LOCAL_ENV" set +a # Precedence: dev.env > built-in defaults. AV_INSTANCE="${AV_INSTANCE:-0}" AV_BASE_PORT="${AV_BASE_PORT:-$DEFAULT_BASE_PORT}" AV_PORT_STRIDE="${AV_PORT_STRIDE:-$DEFAULT_PORT_STRIDE}" # Default true for pre-existing dev.env files that predate this setting. AV_USE_DEBUG_ENCRYPTION_KEY="${AV_USE_DEBUG_ENCRYPTION_KEY:-true}" case "$AV_INSTANCE" in (*[!0-9]*|"") die "AV_INSTANCE must be a non-negative integer (got '$AV_INSTANCE').";; esac case "$AV_USE_DEBUG_ENCRYPTION_KEY" in true|false) ;; (*) die "AV_USE_DEBUG_ENCRYPTION_KEY must be 'true' or 'false' (got '$AV_USE_DEBUG_ENCRYPTION_KEY').";; esac # --- Resolve effective ports --------------------------------------------------- SERVICE_COUNT=10 # block width (offsets 0..9); DB sits last so it ends in 9 case "$AV_PORT_STRIDE" in (*[!0-9]*|"") die "AV_PORT_STRIDE must be a positive integer (got '$AV_PORT_STRIDE').";; esac [ "$AV_PORT_STRIDE" -ge "$SERVICE_COUNT" ] || die "AV_PORT_STRIDE ($AV_PORT_STRIDE) must be >= $SERVICE_COUNT so instances don't overlap." case "$AV_BASE_PORT" in (*[!0-9]*|"") die "AV_BASE_PORT must be a positive integer (got '$AV_BASE_PORT').";; esac BLOCK_BASE=$(( AV_BASE_PORT + AV_INSTANCE * AV_PORT_STRIDE )) # Dev only serves HTTP, so each web app reserves a single port. API_HTTP=$(( BLOCK_BASE + 0 )) CLIENT_HTTP=$(( BLOCK_BASE + 1 )) ADMIN_HTTP=$(( BLOCK_BASE + 2 )) EXT_PORT=$(( BLOCK_BASE + 3 )) EXPO_PORT=$(( BLOCK_BASE + 4 )) # DB sits at the last offset so its port always ends in 9 (e.g. 5109) — easy to # remember. Offsets 5..8 are left free as spare slots inside the block. DB_PORT=$(( BLOCK_BASE + 9 )) # Always one dev DB container, named 'aliasvault-dev'. AV_INSTANCE only shifts # the published port (above), not the container identity — so install.sh and any # other tooling can find it by a single, stable project name. DB_PROJECT="aliasvault-dev" DB_COMPOSE="$ROOT_DIR/dockerfiles/docker-compose.dev.yml" CONN_STR="Host=localhost;Port=$DB_PORT;Database=aliasvault;Username=aliasvault;Password=password;Maximum Pool Size=20;Minimum Pool Size=1" # --- Helpers ------------------------------------------------------------------- RULE="────────────────────────────────────────────────────────────" print_ports() { printf "%sAliasVault dev — instance %s%s (base %s, stride %s → block %s..%s)\n" \ "$BOLD" "$AV_INSTANCE" "$NC" "$AV_BASE_PORT" "$AV_PORT_STRIDE" "$BLOCK_BASE" "$((BLOCK_BASE + SERVICE_COUNT - 1))" printf " %-14s http://localhost:%s\n" "API" "$API_HTTP" printf " %-14s http://localhost:%s\n" "Client" "$CLIENT_HTTP" printf " %-14s http://localhost:%s\n" "Admin" "$ADMIN_HTTP" printf " %-14s localhost:%s\n" "Postgres DB" "$DB_PORT" printf " %-14s :%s\n" "Browser ext" "$EXT_PORT" printf " %-14s :%s\n" "Mobile" "$EXPO_PORT" } # Print a clear, scannable header before a service takes over the terminal, so # it's obvious at a glance which service it is and which ports it's on. banner() { local title="$1"; shift printf "\n%s%s%s\n" "$CYAN" "$RULE" "$NC" printf "%s ▶ %s%s %sinstance %s%s\n" "$BOLD" "$title" "$NC" "$DIM" "$AV_INSTANCE" "$NC" local line for line in "$@"; do printf " %s\n" "$line" done printf "%s%s%s\n\n" "$CYAN" "$RULE" "$NC" } require() { command -v "$1" >/dev/null 2>&1 || die "'$1' is required but not installed."; } # Generate the Blazor client's dev config. write_client_dev_settings() { local target="$ROOT_DIR/apps/server/AliasVault.Client/wwwroot/appsettings.Development.json" cat > "$target" </dev/null)" ] } start_db() { require docker banner "Database — $DB_PROJECT" "Postgres localhost:$DB_PORT" # --wait blocks until the container is healthy, so callers (and CI) can assume # the database is ready to accept connections once this returns. AV_DB_PORT="$DB_PORT" \ docker compose -f "$DB_COMPOSE" -p "$DB_PROJECT" up -d --build --wait --wait-timeout 60 info "Database ready: $CONN_STR" } stop_db() { require docker info "Stopping dev database '$DB_PROJECT' ..." AV_DB_PORT="$DB_PORT" \ docker compose -f "$DB_COMPOSE" -p "$DB_PROJECT" down } # Toggle: stop the dev database if it's running, otherwise start it. toggle_db() { require docker if db_running; then stop_db else start_db fi } # Run a .NET app with ports + DB driven by env/CLI flags. # Binds to 0.0.0.0 so the service is reachable from other devices on the LAN # (e.g. a phone running the mobile app), not just from localhost. run_dotnet() { local proj="$1" http="$2" require dotnet banner "$proj" \ "HTTP http://localhost:$http" \ "Database localhost:$DB_PORT" ASPNETCORE_ENVIRONMENT="Development" \ ConnectionStrings__AliasServerDbContext="$CONN_STR" \ dotnet watch --project "$ROOT_DIR/apps/server/$proj" --no-launch-profile \ --urls "http://0.0.0.0:$http" } # Dev-only secrets. These are static fake values that are used only in dev. DEV_JWT_KEY="${JWT_KEY:-12345678901234567890123456789012}" DEV_DATA_PROTECTION_CERT_PASS="${DATA_PROTECTION_CERT_PASS:-Development}" DEV_ADMIN_PASSWORD_HASH="${ADMIN_PASSWORD_HASH:-AQAAAAIAAYagAAAAEKWfKfa2gh9Z72vjAlnNP1xlME7FsunRznzyrfqFte40FToufRwa3kX8wwDwnEXZag==}" DEV_ADMIN_PASSWORD_GENERATED="${ADMIN_PASSWORD_GENERATED:-2030-01-01T00:00:00Z}" DEV_PRIVATE_EMAIL_DOMAINS="${PRIVATE_EMAIL_DOMAINS:-example.tld,example2.tld,aliasvault.net,disabled.tld}" DEV_HIDDEN_PRIVATE_EMAIL_DOMAINS="${HIDDEN_PRIVATE_EMAIL_DOMAINS:-disabled.tld}" start_api() { JWT_KEY="$DEV_JWT_KEY" \ DATA_PROTECTION_CERT_PASS="$DEV_DATA_PROTECTION_CERT_PASS" \ PUBLIC_REGISTRATION_ENABLED="true" \ PRIVATE_EMAIL_DOMAINS="$DEV_PRIVATE_EMAIL_DOMAINS" \ HIDDEN_PRIVATE_EMAIL_DOMAINS="$DEV_HIDDEN_PRIVATE_EMAIL_DOMAINS" \ IP_LOGGING_ENABLED="true" \ MAX_UPLOAD_SIZE_MB="100" \ run_dotnet "AliasVault.Api" "$API_HTTP" } start_admin() { ADMIN_PASSWORD_HASH="$DEV_ADMIN_PASSWORD_HASH" \ ADMIN_PASSWORD_GENERATED="$DEV_ADMIN_PASSWORD_GENERATED" \ DATA_PROTECTION_CERT_PASS="$DEV_DATA_PROTECTION_CERT_PASS" \ IP_LOGGING_ENABLED="true" \ run_dotnet "AliasVault.Admin" "$ADMIN_HTTP" } start_client() { write_client_dev_settings run_dotnet "AliasVault.Client" "$CLIENT_HTTP" } # Background workers (no HTTP port). run_worker() { local proj="$1" require dotnet banner "$proj (worker)" "Database localhost:$DB_PORT" ConnectionStrings__AliasServerDbContext="$CONN_STR" \ dotnet watch --project "$ROOT_DIR/apps/server/$proj" } start_smtp() { run_worker "Services/AliasVault.SmtpService"; } start_taskrunner() { run_worker "Services/AliasVault.TaskRunner"; } start_ext() { require npm banner "Browser extension (Chrome dev)" \ "Dev server http://localhost:$EXT_PORT" \ "API http://localhost:$API_HTTP" cd "$ROOT_DIR/apps/browser-extension" npm run dev:chrome -- --port "$EXT_PORT" } start_mobile() { require npx banner "Mobile app (Expo)" \ "Metro http://localhost:$EXPO_PORT" \ "API http://localhost:$API_HTTP" cd "$ROOT_DIR/apps/mobile-app" EXPO_PUBLIC_API_URL="http://localhost:$API_HTTP" \ npx expo start --port "$EXPO_PORT" } # --- Interactive menu ---------------------------------------------------------- menu() { print_ports printf "\n%sWhich app do you want to start?%s\n" "$BOLD" "$NC" # Quick live check so the DB entry shows the action it'll take + current state. local db_label if db_running; then db_label="Stop dev DB ${GREEN}(running)${NC}" else db_label="Start dev DB ${DIM}(stopped)${NC}" fi local options=( "api:API" "client:Client" "admin:Admin" "ext:Browser extension" "mobile:Mobile (Expo)" "db:$db_label" ) local i=1 for o in "${options[@]}"; do printf " %s%2d%s) %s\n" "$CYAN" "$i" "$NC" "${o#*:}"; i=$((i+1)) done printf "%sSelect [1-%d]:%s " "$BOLD" "${#options[@]}" "$NC" local choice; read -r choice case "$choice" in (*[!0-9]*|"") die "Invalid selection.";; esac [ "$choice" -ge 1 ] && [ "$choice" -le "${#options[@]}" ] || die "Selection out of range." local picked="${options[$((choice-1))]}" dispatch "${picked%%:*}" } # --- Dispatch ------------------------------------------------------------------ dispatch() { case "${1:-}" in api) start_api ;; client) start_client ;; admin) start_admin ;; smtp) start_smtp ;; taskrunner) start_taskrunner ;; ext) start_ext ;; mobile) start_mobile ;; db) toggle_db ;; db-start) start_db ;; db-stop) stop_db ;; ports) print_ports ;; quit|q) exit 0 ;; "") menu ;; -h|--help|help) sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//' ;; *) die "Unknown command '$1'. start: api/client/admin/smtp/taskrunner/ext/mobile db: db (toggle) / db-start / db-stop" ;; esac } dispatch "${1:-}"