#!/bin/sh # Clawdie Shell — Deployment Module # Purpose: Extract Clawdie-AI tarball and run installation # POSIX-compliant (no bash-isms) set -eu # Configuration (can be overridden for testing) CLAWDIE_HOME="${CLAWDIE_HOME:-/home/clawdie}" CLAWDIE_AI_DIR="${CLAWDIE_AI_DIR:-$CLAWDIE_HOME/clawdie-ai}" CLAWDIE_TARBALL="${CLAWDIE_TARBALL:-/usr/local/share/clawdie-iso/clawdie-ai.tar.gz}" ENV_FILE="${ENV_FILE:-$CLAWDIE_HOME/.env}" LOG_FILE="${LOG_FILE:-/var/log/clawdie-firstboot.log}" PROGRESS_FILE="${PROGRESS_FILE:-/var/log/clawdie-firstboot.progress}" USB_LLM_MODELS_PATH="${USB_LLM_MODELS_PATH:-/mnt/media/llm-models}" LLAMA_CPP_JAIL_NAME="${LLAMA_CPP_JAIL_NAME:-llamacpp}" LLAMA_CPP_MODELS_DIR="${LLAMA_CPP_MODELS_DIR:-/var/db/llm-models}" SETUP_TOKEN_DIR="${SETUP_TOKEN_DIR:-/var/db/clawdie-installer}" SETUP_TOKEN_FILE="${SETUP_TOKEN_FILE:-${SETUP_TOKEN_DIR}/setup-token}" SETUP_MOTD_FILE="${SETUP_MOTD_FILE:-/etc/motd}" # ============================================================================ # MAIN ENTRY POINT # ============================================================================ clawdie_shell_deploy() { # Main orchestrator: fresh install or upgrade, then run the Clawdie-AI installer log_msg "[deploy] Starting Clawdie-AI deployment (mode: ${CLAWDIE_BOOT_MODE:-install})" if [ "${CLAWDIE_BOOT_MODE:-install}" = "upgrade" ]; then clawdie_shell_deploy_upgrade else clawdie_shell_deploy_fresh fi # Common post-deploy steps cd "$CLAWDIE_AI_DIR" || { log_msg "[deploy] ERROR: Failed to cd to $CLAWDIE_AI_DIR" return 1 } # Run installer (handles db migrations, secrets, services) log_msg "[deploy] Running installer..." clawdie_shell_deploy_run_install_all || { log_msg "[deploy] ERROR: installer failed" return 1 } log_msg "[deploy] Installer completed successfully" if [ -f "${CLAWDIE_HOME}/.env.upgrade-backup" ]; then rm -f "${CLAWDIE_HOME}/.env.upgrade-backup" 2>/dev/null || true log_msg "[deploy] Cleared upgrade .env backup" fi if [ "${SHELL_DEPLOY_TEST:-0}" -eq 1 ]; then log_msg "[deploy] TEST MODE: skipping model seeding, Aider venv, and verification" else if [ "${CLAWDIE_BOOT_MODE:-install}" = "install" ]; then clawdie_shell_deploy_write_setup_token || { log_msg "[deploy] ERROR: Failed to rotate or write setup token" return 1 } fi # Seed llama-cpp models if selected and available on USB clawdie_shell_deploy_seed_llama_cpp_models || { log_msg "[deploy] WARNING: Failed to seed llama-cpp models" } # Configure Aider venv for FreeBSD tree-sitter compatibility clawdie_shell_deploy_setup_aider_venv || { log_msg "[deploy] WARNING: Aider venv setup failed" } # Verify clawdie_shell_deploy_verify || { log_msg "[deploy] WARNING: Post-install verification found issues" } fi echo "[DEPLOY] SUCCESS" >> "$PROGRESS_FILE" log_msg "[deploy] Clawdie-AI deployment complete" } # ============================================================================ # FRESH INSTALL PATH # ============================================================================ clawdie_shell_deploy_fresh() { # Validate inputs if [ -z "${ASSISTANT_NAME:-}" ]; then log_msg "[deploy] ERROR: ASSISTANT_NAME not set" return 1 fi if [ -z "${AGENT_DOMAIN:-}" ]; then log_msg "[deploy] ERROR: AGENT_DOMAIN not set" return 1 fi # Extract tarball clawdie_shell_deploy_extract_tarball || { log_msg "[deploy] ERROR: Failed to extract tarball" return 1 } # Validate extracted directory if [ ! -d "$CLAWDIE_AI_DIR" ] || [ ! -f "$CLAWDIE_AI_DIR/package.json" ]; then log_msg "[deploy] ERROR: Tarball extraction failed or invalid" return 1 fi log_msg "[deploy] Package.json verified" # Copy firstboot .env seed into the repo root for installer if [ -f "$ENV_FILE" ]; then cp "$ENV_FILE" "$CLAWDIE_AI_DIR/.env" 2>/dev/null || { log_msg "[deploy] WARNING: Failed to copy $ENV_FILE to $CLAWDIE_AI_DIR/.env" } chmod 600 "$CLAWDIE_AI_DIR/.env" 2>/dev/null || true chown clawdie:clawdie "$CLAWDIE_AI_DIR/.env" 2>/dev/null || true log_msg "[deploy] Seeded $CLAWDIE_AI_DIR/.env from firstboot" else log_msg "[deploy] WARNING: ENV_FILE not found at $ENV_FILE (installer will generate defaults)" fi } # ============================================================================ # UPGRADE PATH # ============================================================================ clawdie_shell_deploy_upgrade() { log_msg "[deploy] Upgrade mode — preserving existing config and customizations" if [ ! -d "$CLAWDIE_AI_DIR" ] || [ ! -f "$CLAWDIE_AI_DIR/package.json" ]; then log_msg "[deploy] WARNING: No existing install found — falling back to fresh install" clawdie_shell_deploy_fresh return fi if [ ! -f "$CLAWDIE_TARBALL" ]; then log_msg "[deploy] ERROR: Tarball not found: $CLAWDIE_TARBALL" return 1 fi # Save existing version _old_version=$(cd "$CLAWDIE_AI_DIR" && node -p "require('./package.json').version" 2>/dev/null || echo "unknown") log_msg "[deploy] Existing version: $_old_version" # Step 1: Extract new tarball to a staging directory STAGING_DIR="${CLAWDIE_HOME}/clawdie-ai-upgrade-staging" rm -rf "$STAGING_DIR" mkdir -p "$STAGING_DIR" if ! tar -xzf "$CLAWDIE_TARBALL" -C "$STAGING_DIR" --strip-components=1 2>/dev/null; then # Try without --strip-components (Forgejo archives have a top-level dir) tar -xzf "$CLAWDIE_TARBALL" -C "$(dirname "$STAGING_DIR")" 2>/dev/null || { log_msg "[deploy] ERROR: Failed to extract upgrade tarball" return 1 } # Rename Clawdie-AI → staging if [ -d "$(dirname "$STAGING_DIR")/Clawdie-AI" ]; then rm -rf "$STAGING_DIR" mv "$(dirname "$STAGING_DIR")/Clawdie-AI" "$STAGING_DIR" fi fi _new_version=$(cd "$STAGING_DIR" && node -p "require('./package.json').version" 2>/dev/null || echo "unknown") log_msg "[deploy] New version: $_new_version" if [ ! -f "$STAGING_DIR/package.json" ]; then log_msg "[deploy] ERROR: Staging directory missing package.json" return 1 fi # Step 2: Preserve .env (never overwrite with seed) if [ -f "$CLAWDIE_AI_DIR/.env" ]; then cp "$CLAWDIE_AI_DIR/.env" "${CLAWDIE_HOME}/.env.upgrade-backup" log_msg "[deploy] Backed up existing .env" fi # Step 3: Apply update via skills-engine (three-way merge) if [ -d "$CLAWDIE_AI_DIR/.nanoclaw" ] && [ -f "$CLAWDIE_AI_DIR/.nanoclaw/state.yaml" ]; then log_msg "[deploy] Skills engine found — running three-way merge update" _tsx="$CLAWDIE_AI_DIR/node_modules/.bin/tsx" [ -x "$_tsx" ] || _tsx="tsx" # Run applyUpdate with the staging dir as the new core _update_script=' import { applyUpdate } from "./skills-engine/update.js"; const result = await applyUpdate(process.argv[1]); console.log(JSON.stringify(result, null, 2)); if (!result.success) process.exit(1); ' if [ "$(id -u)" -eq 0 ]; then su - clawdie -c "cd $CLAWDIE_AI_DIR && $_tsx --eval '$_update_script' '$STAGING_DIR'" 2>&1 | tee -a "$LOG_FILE" || { log_msg "[deploy] WARNING: Skills-engine update failed — falling back to overwrite" clawdie_shell_deploy_upgrade_overwrite "$STAGING_DIR" || return 1 } else cd "$CLAWDIE_AI_DIR" && "$_tsx" --eval "$_update_script" "$STAGING_DIR" 2>&1 | tee -a "$LOG_FILE" || { log_msg "[deploy] WARNING: Skills-engine update failed — falling back to overwrite" clawdie_shell_deploy_upgrade_overwrite "$STAGING_DIR" || return 1 } fi else # No skills engine — overwrite and migrate log_msg "[deploy] No skills engine — overwriting and migrating" clawdie_shell_deploy_upgrade_overwrite "$STAGING_DIR" || return 1 fi # Step 4: Restore .env if [ -f "${CLAWDIE_HOME}/.env.upgrade-backup" ]; then cp "${CLAWDIE_HOME}/.env.upgrade-backup" "$CLAWDIE_AI_DIR/.env" chmod 600 "$CLAWDIE_AI_DIR/.env" 2>/dev/null || true chown clawdie:clawdie "$CLAWDIE_AI_DIR/.env" 2>/dev/null || true log_msg "[deploy] Restored existing .env" fi # Cleanup staging rm -rf "$STAGING_DIR" chown -R clawdie:clawdie "$CLAWDIE_AI_DIR" 2>/dev/null || true log_msg "[deploy] Upgrade complete: $_old_version → $_new_version" } # Fallback: overwrite existing files with new tarball, then migrate clawdie_shell_deploy_upgrade_overwrite() { _staging="$1" log_msg "[deploy] Overwriting existing install with new version" # Preserve data directories that shouldn't be replaced for _dir in data store logs groups .nanoclaw node_modules; do if [ -e "${CLAWDIE_HOME}/_upgrade_${_dir}" ]; then rm -rf "${CLAWDIE_HOME}/_upgrade_${_dir}.stale" 2>/dev/null || true mv "${CLAWDIE_HOME}/_upgrade_${_dir}" "${CLAWDIE_HOME}/_upgrade_${_dir}.stale" 2>/dev/null || true log_msg "[deploy] Moved existing backup for $_dir to _upgrade_${_dir}.stale" fi if [ -d "$CLAWDIE_AI_DIR/$_dir" ]; then mv "$CLAWDIE_AI_DIR/$_dir" "${CLAWDIE_HOME}/_upgrade_${_dir}" 2>/dev/null || true fi done # Copy new files over existing if ! (cd "$_staging" && tar -cf - .) | (cd "$CLAWDIE_AI_DIR" && tar -xf -); then log_msg "[deploy] ERROR: Failed to copy new files from staging" return 1 fi # Restore preserved directories for _dir in data store logs groups .nanoclaw node_modules; do if [ -d "${CLAWDIE_HOME}/_upgrade_${_dir}" ]; then rm -rf "$CLAWDIE_AI_DIR/$_dir" 2>/dev/null || true mv "${CLAWDIE_HOME}/_upgrade_${_dir}" "$CLAWDIE_AI_DIR/$_dir" fi done # Run migrateExisting if skills engine is available but wasn't initialized if [ ! -d "$CLAWDIE_AI_DIR/.nanoclaw" ]; then _tsx="$CLAWDIE_AI_DIR/node_modules/.bin/tsx" [ -x "$_tsx" ] || _tsx="tsx" _migrate_script='import { migrateExisting } from "./skills-engine/migrate.js"; migrateExisting();' if [ "$(id -u)" -eq 0 ]; then su - clawdie -c "cd $CLAWDIE_AI_DIR && $_tsx --eval '$_migrate_script'" 2>&1 | tee -a "$LOG_FILE" || { log_msg "[deploy] WARNING: migrateExisting failed — skills tracking unavailable" } else cd "$CLAWDIE_AI_DIR" && "$_tsx" --eval "$_migrate_script" 2>&1 | tee -a "$LOG_FILE" || { log_msg "[deploy] WARNING: migrateExisting failed — skills tracking unavailable" } fi fi } # ============================================================================ # TARBALL EXTRACTION # ============================================================================ clawdie_shell_deploy_extract_tarball() { # Extract Clawdie-AI tarball to CLAWDIE_HOME if [ ! -f "$CLAWDIE_TARBALL" ]; then log_msg "[deploy] ERROR: Tarball not found: $CLAWDIE_TARBALL" return 1 fi log_msg "[deploy] Extracting $CLAWDIE_TARBALL" # Create parent directory if needed mkdir -p "$CLAWDIE_HOME" # Extract tarball # Note: tarball structure is Clawdie-AI/... so extract to CLAWDIE_HOME parent # and rename to clawdie-ai if ! tar -xzf "$CLAWDIE_TARBALL" -C "$(dirname "$CLAWDIE_HOME")" 2>/dev/null; then log_msg "[deploy] ERROR: tar extraction failed" return 1 fi # Rename if extraction created Clawdie-AI directory if [ -d "$(dirname "$CLAWDIE_HOME")/Clawdie-AI" ] && [ ! -d "$CLAWDIE_AI_DIR" ]; then mv "$(dirname "$CLAWDIE_HOME")/Clawdie-AI" "$CLAWDIE_AI_DIR" log_msg "[deploy] Renamed Clawdie-AI to clawdie-ai" fi # Fix ownership chown -R clawdie:clawdie "$CLAWDIE_HOME" 2>/dev/null || true log_msg "[deploy] Tarball extraction complete" return 0 } # ============================================================================ # INSTALLER # ============================================================================ clawdie_shell_deploy_ensure_node_modules() { # The ISO should bundle node_modules in clawdie-ai.tar.gz for offline firstboot. # If node_modules are missing (custom tarball, manual install), attempt an online # install as a fallback and fail with a clear error if that isn't possible. if [ -d "$CLAWDIE_AI_DIR/node_modules" ]; then return 0 fi log_msg "[deploy] ERROR: node_modules missing in $CLAWDIE_AI_DIR" if ! command -v npm >/dev/null 2>&1; then log_msg "[deploy] ERROR: npm not found; cannot install dependencies" log_msg "[deploy] Hint: rebuild the ISO so clawdie-ai.tar.gz includes node_modules" return 1 fi log_msg "[deploy] Attempting npm ci to install dependencies (requires network)..." if [ "$(id -u)" -eq 0 ]; then su - clawdie -c "cd $CLAWDIE_AI_DIR && npm ci --no-audit --no-fund" 2>&1 | tee -a "$LOG_FILE" || return 1 else ( cd "$CLAWDIE_AI_DIR" && npm ci --no-audit --no-fund ) 2>&1 | tee -a "$LOG_FILE" || return 1 fi if [ ! -d "$CLAWDIE_AI_DIR/node_modules" ]; then log_msg "[deploy] ERROR: npm ci completed but node_modules/ still missing" return 1 fi return 0 } clawdie_shell_deploy_run_install_all() { # Run installer with error handling if [ ! -f "$CLAWDIE_AI_DIR/package.json" ]; then log_msg "[deploy] ERROR: package.json not found in $CLAWDIE_AI_DIR" return 1 fi clawdie_shell_deploy_ensure_node_modules || return 1 local run_cmd="just install" if ! command -v just >/dev/null 2>&1; then # Fallback for dev/test environments where just isn't installed. run_cmd="npm run install" fi log_msg "[deploy] Installer command: ${run_cmd}" # Run as root regardless — setup/service.ts requires root to install the rc.d # service, write /usr/local/etc/rc.d/{agent}, and run sysrc. When root, # setup/service.ts handles privilege drop automatically: it chowns runtime dirs # (data/, logs/, groups/) to the agent user and adds -u {agent} to daemon args # so the running process is never root. Dropping to clawdie here would cause # setup/service.ts to fall back to start/stop wrapper scripts instead of rc.d. ( cd "$CLAWDIE_AI_DIR" && $run_cmd ) 2>&1 | tee -a "$LOG_FILE" || return 1 return 0 } clawdie_shell_deploy_wait_for_setup_route() { local attempts="${1:-60}" local delay="${2:-2}" local setup_url="http://127.0.0.1:3100/api/setup/status" local i=0 while [ "$i" -lt "$attempts" ]; do if fetch -qo - "$setup_url" >/dev/null 2>&1; then return 0 fi i=$((i + 1)) sleep "$delay" done return 1 } clawdie_shell_deploy_write_setup_token() { log_msg "[deploy] Waiting for controlplane setup route on 127.0.0.1:3100" if ! clawdie_shell_deploy_wait_for_setup_route 60 2; then log_msg "[deploy] ERROR: controlplane setup route did not become ready" return 1 fi local setup_output token expires_at if ! setup_output=$(cd "$CLAWDIE_AI_DIR" && npm run setup-token -- rotate 2>&1); then log_msg "[deploy] ERROR: setup-token rotation failed" echo "$setup_output" | while IFS= read -r line; do [ -n "$line" ] && log_msg "[deploy] ${line}" done return 1 fi token=$(printf '%s\n' "$setup_output" | awk ' /^Clawdie setup bootstrap token:/ { getline; gsub(/^[[:space:]]+|[[:space:]]+$/, "", $0); print; exit } ') expires_at=$(printf '%s\n' "$setup_output" | awk -F': ' '/^Expires:/ { print $2; exit }') if [ -z "${token:-}" ]; then log_msg "[deploy] ERROR: setup-token rotation returned no token" return 1 fi rm -f "$SETUP_TOKEN_FILE" 2>/dev/null || true mkdir -p "$SETUP_TOKEN_DIR" chown root:clawdie "$SETUP_TOKEN_DIR" 2>/dev/null || true chmod 750 "$SETUP_TOKEN_DIR" 2>/dev/null || true { echo "$token" } > "$SETUP_TOKEN_FILE" chown clawdie:clawdie "$SETUP_TOKEN_FILE" 2>/dev/null || true chmod 600 "$SETUP_TOKEN_FILE" 2>/dev/null || true clawdie_shell_deploy_write_setup_motd "$expires_at" log_msg "[deploy] Setup token written to ${SETUP_TOKEN_FILE} (expires: ${expires_at:-unknown})" return 0 } clawdie_shell_deploy_write_setup_motd() { local expires_at="${1:-unknown}" cat > "$SETUP_MOTD_FILE" < cat ${SETUP_TOKEN_FILE} # then open http://127.0.0.1:3100/setup in your laptop browser Bootstrap token file: ${SETUP_TOKEN_FILE} Owner: clawdie Mode: 0600 Expires: ${expires_at} Do not expose port 3100 directly on tailscale0 or the public internet. Use SSH tunnel access by default. EOF chmod 644 "$SETUP_MOTD_FILE" 2>/dev/null || true } # ============================================================================ # AIDER VENV SETUP (FREEBSD) # ============================================================================ clawdie_shell_deploy_setup_aider_venv() { local venv_dir="/opt/clawdie/venv/aider" local tmp_dir="${CLAWDIE_AI_DIR}/tmp" local python_bin="${venv_dir}/bin/python" local pip_bin="${venv_dir}/bin/pip" if [ ! -d "$CLAWDIE_AI_DIR" ]; then log_msg "[deploy] WARNING: $CLAWDIE_AI_DIR missing — skipping Aider venv" return 0 fi mkdir -p "$tmp_dir" 2>/dev/null || true mkdir -p "/opt/clawdie/venv" 2>/dev/null || true if id clawdie >/dev/null 2>&1; then chown -R clawdie:clawdie "/opt/clawdie" 2>/dev/null || true fi if [ ! -x "$python_bin" ]; then log_msg "[deploy] Creating Aider venv at $venv_dir" if [ "$(id -u)" -eq 0 ]; then su - clawdie -c "python3 -m venv --system-site-packages '$venv_dir'" || return 1 else python3 -m venv --system-site-packages "$venv_dir" || return 1 fi fi # Convenience symlink for operator tooling. if [ ! -L "/home/clawdie/.venv/aider" ]; then mkdir -p "/home/clawdie/.venv" 2>/dev/null || true rm -rf "/home/clawdie/.venv/aider" 2>/dev/null || true ln -s "$venv_dir" "/home/clawdie/.venv/aider" 2>/dev/null || true chown -h clawdie:clawdie "/home/clawdie/.venv/aider" 2>/dev/null || true fi if [ ! -x "$pip_bin" ]; then log_msg "[deploy] WARNING: Aider venv pip missing — skipping" return 1 fi log_msg "[deploy] Pinning Aider venv deps (litellm + tree_sitter)" if [ "$(id -u)" -eq 0 ]; then su - clawdie -c "TMPDIR='$tmp_dir' '$pip_bin' install --no-user --upgrade --ignore-installed aider-chat==0.86.2" || return 1 su - clawdie -c "TMPDIR='$tmp_dir' '$pip_bin' install --no-user --upgrade --ignore-installed litellm==1.81.10" || return 1 su - clawdie -c "TMPDIR='$tmp_dir' '$pip_bin' install --no-user --upgrade --force-reinstall tree_sitter==0.20.4" || return 1 else TMPDIR="$tmp_dir" "$pip_bin" install --no-user --upgrade --ignore-installed aider-chat==0.86.2 || return 1 TMPDIR="$tmp_dir" "$pip_bin" install --no-user --upgrade --ignore-installed litellm==1.81.10 || return 1 TMPDIR="$tmp_dir" "$pip_bin" install --no-user --upgrade --force-reinstall tree_sitter==0.20.4 || return 1 fi return 0 } # ============================================================================ # POST-INSTALL VERIFICATION # ============================================================================ clawdie_shell_deploy_verify() { # Verify basic services are running log_msg "[deploy] Running post-install verification" local failed=0 # Check if jails are running (optional, may not be ready yet) if command -v jls >/dev/null 2>&1; then local jail_count=$(jls -N 2>/dev/null | wc -l || echo "0") if [ "$jail_count" -gt 1 ]; then log_msg "[deploy] ✓ Jails active: $((jail_count - 1)) jails" else log_msg "[deploy] ⚠ No jails detected (may still be provisioning)" fi fi # Check if clawdie service is enabled if grep -q "clawdie_enable" /etc/rc.conf 2>/dev/null; then log_msg "[deploy] ✓ clawdie service configured in rc.conf" else log_msg "[deploy] ⚠ clawdie service not in rc.conf" failed=1 fi # Check if .env file was created if [ -f "$ENV_FILE" ]; then log_msg "[deploy] ✓ .env file created" else log_msg "[deploy] ⚠ .env file not found" failed=1 fi # Check if skills engine was initialized if [ -d "$CLAWDIE_AI_DIR/.nanoclaw" ] && [ -f "$CLAWDIE_AI_DIR/.nanoclaw/state.yaml" ]; then log_msg "[deploy] ✓ Skills engine initialized (.nanoclaw/)" else log_msg "[deploy] ⚠ Skills engine not initialized (.nanoclaw/ missing)" fi # Check if bootstrap knowledge was imported if [ -f "$CLAWDIE_AI_DIR/bootstrap/skills-memory/artifact.sql" ]; then log_msg "[deploy] ✓ Bootstrap knowledge artifact present" fi # Check if Aider CLI is available (controlplane harness dependency) if command -v aider >/dev/null 2>&1; then local aider_version aider_version=$(aider --version 2>/dev/null | head -n 1 || true) if [ -n "$aider_version" ]; then log_msg "[deploy] ✓ Aider available: ${aider_version}" else log_msg "[deploy] ✓ Aider available" fi else log_msg "[deploy] ⚠ Aider CLI not found (Aider venv setup missing)" fi return $failed } # ============================================================================ # LLAMA-CPP MODEL SEEDING # ============================================================================ clawdie_shell_deploy_seed_llama_cpp_models() { if [ "${LOCAL_LLM_PROVIDER:-none}" != "llama_cpp" ]; then return 0 fi if [ ! -d "$USB_LLM_MODELS_PATH" ]; then log_msg "[deploy] No USB model pack found at $USB_LLM_MODELS_PATH" return 0 fi if ! find "$USB_LLM_MODELS_PATH" -name "*.gguf" -type f 2>/dev/null | head -1 >/dev/null; then log_msg "[deploy] No GGUF models found in $USB_LLM_MODELS_PATH" return 0 fi if ! command -v bastille >/dev/null 2>&1; then log_msg "[deploy] bastille not available — cannot seed llama-cpp models" return 1 fi log_msg "[deploy] Seeding llama-cpp models into ${LLAMA_CPP_JAIL_NAME}:${LLAMA_CPP_MODELS_DIR}" bastille cmd "$LLAMA_CPP_JAIL_NAME" install -d -m 755 "$LLAMA_CPP_MODELS_DIR" || return 1 for model in "$USB_LLM_MODELS_PATH"/*.gguf; do [ -f "$model" ] || continue dest="/usr/local/bastille/jails/${LLAMA_CPP_JAIL_NAME}/root${LLAMA_CPP_MODELS_DIR}/$(basename "$model")" if [ -f "$dest" ]; then log_msg "[deploy] Model already present: $(basename "$model")" continue fi cp "$model" "$dest" 2>/dev/null || return 1 log_msg "[deploy] Copied model: $(basename "$model")" done return 0 } # ============================================================================ # LOGGING HELPER # ============================================================================ log_msg() { echo "$(date '+%H:%M:%S') $1" | tee -a "$LOG_FILE" 2>/dev/null || true } # Only run if sourced directly (not during test) if [ "${SHELL_DEPLOY_TEST:-0}" -eq 0 ]; then clawdie_shell_deploy fi