#!/usr/bin/env bash # ๐Ÿš‚ DEJA One-Command Installer # Usage: curl -fsSL https://install.dejadcc.com | bash # # Developer mode (builds from local repo instead of downloading tarball): # ./install.sh --dev # ./install.sh --dev --repo /path/to/DEJA.js set -euo pipefail DEJA_DIR="${DEJA_DIR:-$HOME/.deja}" DEJA_BIN="${DEJA_DIR}/bin" SERVER_DIR="${DEJA_DIR}/server" ENV_FILE="${DEJA_DIR}/.env" CONFIG_FILE="${DEJA_DIR}/config.json" MIN_NODE_VERSION=20 INSTALL_BASE_URL="${DEJA_INSTALL_URL:-https://install.dejadcc.com}" # --- Mode --- DEV_MODE=false DEV_REPO="" # --- Colors & Styles --- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' info() { echo -e " ๐Ÿ’ฌ ${BLUE}$*${NC}"; } ok() { echo -e " โœ… ${GREEN}$*${NC}"; } warn() { echo -e " โš ๏ธ ${YELLOW}$*${NC}"; } err() { echo -e " โŒ ${RED}$*${NC}" >&2; } print_banner() { echo -e " โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— " echo -e " โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ• โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—" echo -e " โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘" echo -e " โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ• โ–ˆโ–ˆ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘" echo -e " โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘" echo -e " โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ• โ•šโ•โ• โ•šโ•โ•" } # ====================================================================== # ๐Ÿ” Step 1: Platform detection # ====================================================================== detect_platform() { echo "" echo -e " ${BOLD}๐Ÿ” Detecting platform...${NC}" OS="$(uname -s)" ARCH="$(uname -m)" case "${OS}" in Linux*) PLATFORM="linux" ;; Darwin*) PLATFORM="macos" ;; *) err "Unsupported OS: ${OS}." info "๐ŸชŸ Windows: open PowerShell and run: irm https://install.dejadcc.com/install.ps1 | iex" exit 1 ;; esac case "${ARCH}" in x86_64|amd64) ;; # โœ… supported aarch64|arm64) ;; # โœ… supported (Raspberry Pi, Apple Silicon) *) err "Unsupported architecture: ${ARCH}"; exit 1 ;; esac ok "Platform: ${PLATFORM} (${ARCH})" } # ====================================================================== # โฌข Step 2: Check Node.js # ====================================================================== check_node() { echo "" echo -e " ${BOLD}โฌข Checking Node.js...${NC}" if ! command -v node &>/dev/null; then err "Node.js is not installed." echo "" info "๐Ÿ“ฆ Install Node.js ${MIN_NODE_VERSION}+ from: https://nodejs.org/" if [ "${PLATFORM}" = "linux" ]; then info "Or use nvm: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash" info "Then: nvm install ${MIN_NODE_VERSION}" elif [ "${PLATFORM}" = "macos" ]; then info "Or with Homebrew: brew install node@${MIN_NODE_VERSION}" fi exit 1 fi local node_version node_version=$(node --version | sed 's/v//' | cut -d. -f1) if [ "${node_version}" -lt "${MIN_NODE_VERSION}" ]; then err "Node.js ${MIN_NODE_VERSION}+ required. Found: $(node --version)" info "๐Ÿ“ฆ Update Node.js: https://nodejs.org/" exit 1 fi ok "Node.js $(node --version)" } # ====================================================================== # ๐Ÿ“ Step 4: Environment setup # ====================================================================== setup_environment() { echo "" echo -e " ${BOLD}๐Ÿ“ Setting up environment...${NC}" mkdir -p "${DEJA_DIR}" # Required Firebase client vars โ€” used to validate both an existing .env and # freshly-fetched config before anything is written. local required_fb_vars="VITE_FIREBASE_API_KEY VITE_FIREBASE_AUTH_DOMAIN \ VITE_FIREBASE_PROJECT_ID VITE_FIREBASE_DATABASE_URL VITE_FIREBASE_STORAGE_BUCKET \ VITE_FIREBASE_MESSAGING_SENDER_ID VITE_FIREBASE_APP_ID" if [ -f "${ENV_FILE}" ]; then # Trust an existing .env only if every required var is present AND non-blank. # A partial/blank .env from an earlier failed install must be repaired, not kept. local existing_missing="" for v in ${required_fb_vars}; do if ! grep -qE "^${v}=.+" "${ENV_FILE}" 2>/dev/null; then existing_missing="${existing_missing} ${v}" fi done if [ -z "${existing_missing}" ]; then ok "Environment file exists (${ENV_FILE})" return fi warn "Existing ${ENV_FILE} is missing Firebase config:${existing_missing}" cp "${ENV_FILE}" "${ENV_FILE}.bak" 2>/dev/null || true info "Backed up to ${ENV_FILE}.bak โ€” regenerating with fresh config" fi # Firebase client config โ€” public client-side keys, safe to embed. # CI replaces these placeholders at release time. local fb_api_key="AIzaSyAJZyQcSPNRkNP9PSrEvFXFkTvLpViosqA" local fb_auth_domain="dejajs.firebaseapp.com" local fb_project_id="dejajs" local fb_database_url="https://dejajs-default-rtdb.firebaseio.com" local fb_storage_bucket="dejajs.firebasestorage.app" local fb_messaging_id="39887853609" local fb_app_id="1:39887853609:web:4ccdddc1653852a8eb6438" if [[ "${fb_api_key}" == __* ]]; then # Placeholders not replaced โ€” try local .env, then fetch from install API local repo_env="" local repo_dir="${DEV_REPO}" [ -z "${repo_dir}" ] && repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" for ef in "${repo_dir}/.env" "${repo_dir}/.env.local"; do [ -f "${ef}" ] && repo_env="${ef}" && break done if [ -z "${repo_env}" ] && [ -f "${DEJA_DIR}/.env" ]; then repo_env="${DEJA_DIR}/.env" fi if [ -n "${repo_env}" ]; then read_env_var() { grep "^${1}=" "${repo_env}" 2>/dev/null | head -1 | cut -d= -f2- | sed 's/^["'"'"']//;s/["'"'"']$//' || echo ""; } fb_api_key=$(read_env_var VITE_FIREBASE_API_KEY) fb_auth_domain=$(read_env_var VITE_FIREBASE_AUTH_DOMAIN) fb_project_id=$(read_env_var VITE_FIREBASE_PROJECT_ID) fb_database_url=$(read_env_var VITE_FIREBASE_DATABASE_URL) fb_storage_bucket=$(read_env_var VITE_FIREBASE_STORAGE_BUCKET) fb_messaging_id=$(read_env_var VITE_FIREBASE_MESSAGING_SENDER_ID) fb_app_id=$(read_env_var VITE_FIREBASE_APP_ID) ok "Firebase config loaded from ${repo_env}" else # Fetch public Firebase client config from install API info "Fetching Firebase config from ${INSTALL_BASE_URL}..." local config_json config_json=$(curl -fsSL "${INSTALL_BASE_URL}/api/config" 2>/dev/null) || { err "Failed to fetch Firebase config from ${INSTALL_BASE_URL}/api/config" exit 1 } json_val() { echo "${config_json}" | grep -o "\"${1}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | sed "s/.*\"${1}\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/"; } fb_api_key=$(json_val VITE_FIREBASE_API_KEY) fb_auth_domain=$(json_val VITE_FIREBASE_AUTH_DOMAIN) fb_project_id=$(json_val VITE_FIREBASE_PROJECT_ID) fb_database_url=$(json_val VITE_FIREBASE_DATABASE_URL) fb_storage_bucket=$(json_val VITE_FIREBASE_STORAGE_BUCKET) fb_messaging_id=$(json_val VITE_FIREBASE_MESSAGING_SENDER_ID) fb_app_id=$(json_val VITE_FIREBASE_APP_ID) ok "Firebase config fetched from install API" fi else ok "Firebase client config embedded" fi # Sanitize each value before it is interpolated into the heredoc below: strip newlines # and anything outside the charset Firebase client config actually uses, so a crafted # value can't inject extra KEY=VALUE lines into the generated .env. Runs BEFORE the # blank-check below so an all-garbage value normalizes to empty and gets caught. sanitize_fb() { printf '%s' "$1" | tr -d '\n\r' | LC_ALL=C sed 's/[^A-Za-z0-9._:/-]//g'; } fb_api_key=$(sanitize_fb "${fb_api_key}") fb_auth_domain=$(sanitize_fb "${fb_auth_domain}") fb_project_id=$(sanitize_fb "${fb_project_id}") fb_database_url=$(sanitize_fb "${fb_database_url}") fb_storage_bucket=$(sanitize_fb "${fb_storage_bucket}") fb_messaging_id=$(sanitize_fb "${fb_messaging_id}") fb_app_id=$(sanitize_fb "${fb_app_id}") # ๐Ÿ”’ Validate every required Firebase var resolved to a non-empty value. # Never write a partial/blank .env โ€” the server crashes on the first missing var. local blank_vars="" [ -z "${fb_api_key}" ] && blank_vars="${blank_vars} VITE_FIREBASE_API_KEY" [ -z "${fb_auth_domain}" ] && blank_vars="${blank_vars} VITE_FIREBASE_AUTH_DOMAIN" [ -z "${fb_project_id}" ] && blank_vars="${blank_vars} VITE_FIREBASE_PROJECT_ID" [ -z "${fb_database_url}" ] && blank_vars="${blank_vars} VITE_FIREBASE_DATABASE_URL" [ -z "${fb_storage_bucket}" ] && blank_vars="${blank_vars} VITE_FIREBASE_STORAGE_BUCKET" [ -z "${fb_messaging_id}" ] && blank_vars="${blank_vars} VITE_FIREBASE_MESSAGING_SENDER_ID" [ -z "${fb_app_id}" ] && blank_vars="${blank_vars} VITE_FIREBASE_APP_ID" if [ -n "${blank_vars}" ]; then err "Firebase config is incomplete โ€” missing:${blank_vars}" err "The install API (${INSTALL_BASE_URL}/api/config) returned blank values." err "This is a server-side misconfiguration. Please report it and retry later." exit 1 fi cat > "${ENV_FILE}" </dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1 elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1 else return 1 fi } # ๐Ÿ” Download to and check it against the release's .sha256 โ€” fails closed: # no checksum, a malformed one or a mismatch โ†’ error, nothing left at . Catches corrupt, # truncated and half-published downloads (the checksum lives beside the tarball; not a signature). download_verified() { local url="$1" dest="$2" name expected actual name="$(basename "${url}")" expected="$(curl -fsSL "${url}.sha256" 2>/dev/null | head -n1 | cut -d' ' -f1 | tr 'A-F' 'a-f')" || expected="" if ! printf '%s' "${expected}" | grep -Eq '^[0-9a-f]{64}$'; then err "The release has no checksum for ${name} โ€” not installing it. If a release is being published right now, try again in a minute." return 1 fi curl -fsSL "${url}" -o "${dest}" || { err "Failed to download ${name}. Check your internet connection." return 1 } actual="$(sha256_of "${dest}")" || { rm -f "${dest}" err "Need sha256sum or shasum to verify ${name}." return 1 } if [ "${actual}" != "${expected}" ]; then rm -f "${dest}" err "${name} failed its checksum โ€” not installing it. Try again in a few minutes." return 1 fi } install_server() { echo "" echo -e " ${BOLD}๐Ÿ“ฆ Installing server...${NC}" if [ "${DEV_MODE}" = true ]; then install_server_dev return fi info "โฌ‡๏ธ Fetching latest release..." local tarball_url="${INSTALL_BASE_URL}/releases/latest/deja-server.tar.gz" local tmp_dir tmp_dir=$(mktemp -d) trap "rm -rf '${tmp_dir}'" EXIT download_verified "${tarball_url}" "${tmp_dir}/deja-server.tar.gz" || exit 1 ok "Server download verified ๐Ÿ”" mkdir -p "${SERVER_DIR}" # ๐Ÿงน firmware/ is replaced wholesale โ€” a file a release removed must never reach a sketch rm -rf "${SERVER_DIR}/firmware" tar -xzf "${tmp_dir}/deja-server.tar.gz" -C "${SERVER_DIR}" rm -rf "${tmp_dir}" trap - EXIT ok "Server files extracted" local version="unknown" if [ -f "${SERVER_DIR}/version.txt" ]; then version=$(cat "${SERVER_DIR}/version.txt") fi info "๐Ÿ“ฆ Installing dependencies (this may take a minute)..." (cd "${SERVER_DIR}" && npm install --production 2>&1 | tail -1) ok "Server ${version} installed ๐ŸŽ‰" } install_server_dev() { local repo_dir="${DEV_REPO}" if [ -z "${repo_dir}" ]; then # Try to detect the repo from the script's own location local script_dir script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ -f "${script_dir}/apps/server/package.json" ]; then repo_dir="${script_dir}" else err "Cannot find DEJA.js repo. Use --repo /path/to/DEJA.js" exit 1 fi fi if [ ! -f "${repo_dir}/apps/server/package.json" ]; then err "Not a valid DEJA.js repo: ${repo_dir}" exit 1 fi info "๐Ÿ› ๏ธ Building server from local source: ${repo_dir}" # Check for pnpm if ! command -v pnpm &>/dev/null; then err "pnpm is required for dev builds. Install: npm install -g pnpm" exit 1 fi # Build the server bundle with tsup cd "${repo_dir}" pnpm --filter=deja-serverts run build:docker || { err "Server build failed. Check for errors above." exit 1 } # Copy built output to ~/.deja/server mkdir -p "${SERVER_DIR}" cp "${repo_dir}/apps/server/dist/index.js" "${SERVER_DIR}/index.js" # ๐Ÿ“‹ deja devices on this install (user-scoped auth, no admin keys) cp "${repo_dir}/apps/server/dist/devices.js" "${SERVER_DIR}/devices.js" # ๐Ÿ“Ÿ deja deploy on this install: the wizard + the firmware it builds from (replaced, never merged) cp "${repo_dir}/apps/server/dist/deploy.js" "${SERVER_DIR}/deploy.js" rm -rf "${SERVER_DIR}/firmware" cp -R "${repo_dir}/apps/server/dist/firmware" "${SERVER_DIR}/firmware" # ๐Ÿท๏ธ Release version lives in the monorepo ROOT package.json # (apps/server/package.json is a workspace placeholder, "0.1.0") local version version=$(node -e "console.log(require('${repo_dir}/package.json').version)") echo "${version}-dev" > "${SERVER_DIR}/version.txt" # Generate a distribution package.json with only runtime externals. # Workspace @repo/* deps are bundled into index.js and must NOT appear here # (npm can't resolve "workspace:*" specifiers). node -e " const { dependencies } = require('${repo_dir}/apps/server/dist-deps.json'); const pkg = {name:'deja-server',version:'${version}-dev',private:true,type:'module',engines:{node:'>= 20'},dependencies}; require('fs').writeFileSync('${SERVER_DIR}/package.json', JSON.stringify(pkg, null, 2)); " info "๐Ÿ“ฆ Installing native dependencies..." (cd "${SERVER_DIR}" && npm install --production 2>&1 | tail -1) ok "Server ${version}-dev built and installed from source ๐Ÿ› ๏ธ" } # ====================================================================== # โšก Step 7: Install CLI # ====================================================================== install_cli() { echo "" echo -e " ${BOLD}โšก Installing CLI...${NC}" mkdir -p "${DEJA_BIN}" if [ "${DEV_MODE}" = true ]; then # In dev mode, copy CLI from the repo local repo_dir="${DEV_REPO}" if [ -z "${repo_dir}" ]; then repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" fi # Node CLI entry + shims + command modules + Ink TUI cp "${repo_dir}/apps/cli/deja" "${repo_dir}/apps/cli/deja.cmd" "${repo_dir}/apps/cli/deja.mjs" \ "${repo_dir}/apps/cli/deja-ui-ink.mjs" "${DEJA_BIN}/" chmod +x "${DEJA_BIN}/deja" rm -rf "${DEJA_BIN}/tui" "${DEJA_BIN}/commands" cp -r "${repo_dir}/apps/cli/tui" "${repo_dir}/apps/cli/commands" "${DEJA_BIN}/" # Match the release tarball: tests aren't part of an install (they need vitest). find "${DEJA_BIN}/tui" "${DEJA_BIN}/commands" -name '*.test.mjs' -delete 2>/dev/null || true # Install CLI/TUI dependencies at ~/.deja/ so node_modules resolves from # ~/.deja/bin/**. The canonical dep list lives in apps/cli/package.json. if [ -f "${repo_dir}/apps/cli/package.json" ]; then cp "${repo_dir}/apps/cli/package.json" "${DEJA_DIR}/package.json" info "๐Ÿ“ฆ Installing CLI dependencies..." (cd "${DEJA_DIR}" && npm install 2>&1 | tail -1) fi ok "CLI installed from local repo ๐Ÿ› ๏ธ" else # Download the CLI bundle (Node CLI + shims + Ink TUI + package.json) from the install API. local tmp_cli tmp_cli="$(mktemp -d)" download_verified "${INSTALL_BASE_URL}/releases/latest/deja-cli.tar.gz" "${tmp_cli}/deja-cli.tar.gz" || { rm -rf "${tmp_cli}" exit 1 } tar -xzf "${tmp_cli}/deja-cli.tar.gz" -C "${DEJA_BIN}" chmod +x "${DEJA_BIN}/deja" # CLI package.json lives at ~/.deja so node_modules resolves from ~/.deja/bin/** if [ -f "${DEJA_BIN}/package.json" ]; then mv "${DEJA_BIN}/package.json" "${DEJA_DIR}/package.json" info "๐Ÿ“ฆ Installing CLI dependencies..." (cd "${DEJA_DIR}" && npm install --silent 2>&1 | tail -1) fi rm -rf "${tmp_cli}" ok "CLI + interactive TUI installed" fi # Add to PATH if not already present if ! echo "${PATH}" | grep -q "${DEJA_BIN}"; then local shell_rc="" if [ -f "${HOME}/.zshrc" ]; then shell_rc="${HOME}/.zshrc" elif [ -f "${HOME}/.bashrc" ]; then shell_rc="${HOME}/.bashrc" fi if [ -n "${shell_rc}" ]; then if ! grep -q '.deja/bin' "${shell_rc}" 2>/dev/null; then echo '' >> "${shell_rc}" echo '# ๐Ÿš‚ DEJA CLI' >> "${shell_rc}" echo 'export PATH="${HOME}/.deja/bin:${PATH}"' >> "${shell_rc}" info "๐Ÿ“ Added ${DEJA_BIN} to PATH in ${shell_rc}" info "๐Ÿ”„ Run: ${CYAN}source ${shell_rc}${NC} (or open a new terminal)" fi else warn "Add ${DEJA_BIN} to your PATH manually." fi fi ok "CLI ready at ${DEJA_BIN}/deja โšก" } # ====================================================================== # ๐Ÿ”‘ Step 7: Login # ====================================================================== login_and_start() { export PATH="${DEJA_BIN}:${PATH}" echo "" echo -e " ${GREEN}${BOLD}โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•${NC}" echo -e " ${GREEN}${BOLD} ๐Ÿš‚ DEJA installed! ${NC}" echo -e " ${GREEN}${BOLD}โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•${NC}" echo "" if [ -t 0 ]; then # Interactive terminal โ€” prompt for login "${DEJA_BIN}/deja" login || { warn "Login skipped. Run ${CYAN}deja login${NC} later to connect your account." echo "" return } echo "" info "๐Ÿš€ Starting DEJA Server..." "${DEJA_BIN}/deja" start -b else # Piped from curl โ€” skip interactive login echo "" info "Run ${CYAN}deja login${NC} to connect your account, then ${CYAN}deja start${NC} to launch the server." fi echo "" echo -e " ๐Ÿš‚ ${DIM}Throttle${NC} ${CYAN}https://throttle.dejadcc.com${NC}" echo -e " ๐Ÿ“ ${DIM}Config${NC} ${CYAN}${DEJA_DIR}/${NC}" echo -e " ๐Ÿ“‹ ${DIM}Logs${NC} ${CYAN}deja logs -f${NC}" echo -e " ๐Ÿ“Š ${DIM}Status${NC} ${CYAN}deja status${NC}" echo "" } # ====================================================================== # ๐Ÿš‚ Main # ====================================================================== main() { echo "" print_banner echo "" echo -e " ${BOLD}๐Ÿš‚ DEJA${NC} โ€” Installer" if [ "${DEV_MODE}" = true ]; then echo -e " ${YELLOW}${BOLD}๐Ÿ› ๏ธ Developer Mode${NC}" fi echo "" detect_platform check_node setup_environment detect_serial install_server install_cli login_and_start } # Parse arguments while [ $# -gt 0 ]; do case "$1" in --dev) DEV_MODE=true ;; --repo) shift; DEV_REPO="${1:-}" ;; --repo=*) DEV_REPO="${1#*=}" ;; *) ;; esac shift done main