#!/usr/bin/env bash # ============================================================================= # devlab — DevLab Management CLI # Usage: devlab [args] # # Install to PATH: sudo cp devlab.sh /usr/local/bin/devlab && chmod +x /usr/local/bin/devlab # ============================================================================= set -euo pipefail CONFIG_FILE="/opt/lab/.devlab-config" [[ -f "$CONFIG_FILE" ]] && source "$CONFIG_FILE" LAB_ROOT="${LAB_ROOT:-/opt/lab}" RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' SERVICES=(caddy code-server gitea gitea-runner ollama open-webui portainer watchtower) cmd_help() { cat < Tail logs for a service update Pull latest images and recreate containers backup Run a manual backup now models List downloaded Ollama models pull Pull an Ollama model (e.g. codellama:34b) remove Remove an Ollama model urls Print all service URLs compose up Start a test environment from a compose file compose down Stop a test environment compose ls List running compose stacks ${BOLD}SERVICES${RESET} ${SERVICES[*]} ${BOLD}EXAMPLES${RESET} devlab status devlab logs gitea devlab pull mistral:latest devlab compose up /opt/lab/compose/examples/lamp-stack.yml devlab restart code-server EOF } cmd_status() { echo -e "\n${BOLD}DevLab Service Status${RESET}\n" printf "%-20s %-12s %s\n" "CONTAINER" "STATUS" "IMAGE" printf '%.0s─' {1..60}; echo for svc in "${SERVICES[@]}"; do local info info=$(docker inspect "$svc" --format '{{.State.Status}} {{.Config.Image}}' 2>/dev/null) || { printf "%-20s ${RED}%-12s${RESET} %s\n" "$svc" "not found" "-" continue } local status="${info%% *}" local image="${info#* }" local color="$RED" [[ "$status" == "running" ]] && color="$GREEN" printf "%-20s ${color}%-12s${RESET} %s\n" "$svc" "$status" "$image" done echo "" } cmd_start() { for svc in "${SERVICES[@]}"; do docker start "$svc" 2>/dev/null && \ echo -e " ${GREEN}✓${RESET} ${svc}" || \ echo -e " ${RED}✗${RESET} ${svc} (not found)" done } cmd_stop() { read -rp "Stop all DevLab services? [y/N]: " confirm [[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; return; } for svc in "${SERVICES[@]}"; do docker stop "$svc" 2>/dev/null && \ echo -e " ${YELLOW}■${RESET} ${svc} stopped" || true done } cmd_restart() { local target="${1:-all}" if [[ "$target" == "all" ]]; then for svc in "${SERVICES[@]}"; do docker restart "$svc" 2>/dev/null && \ echo -e " ${GREEN}↺${RESET} ${svc}" || true done else docker restart "$target" && echo -e " ${GREEN}↺${RESET} ${target} restarted" fi } cmd_logs() { local svc="${1:-}" [[ -z "$svc" ]] && { echo "Usage: devlab logs "; exit 1; } docker logs -f --tail=100 "$svc" } cmd_update() { echo -e "${CYAN}Pulling latest images...${RESET}" for svc in "${SERVICES[@]}"; do local image image=$(docker inspect "$svc" --format '{{.Config.Image}}' 2>/dev/null) || continue echo -e " Updating ${image}..." docker pull "$image" || true docker restart "$svc" || true done echo -e "${GREEN}Update complete.${RESET}" } cmd_backup() { [[ -x "${LAB_ROOT}/backup.sh" ]] || { echo "Backup script not found."; exit 1; } "${LAB_ROOT}/backup.sh" } cmd_models() { docker exec ollama ollama list } cmd_pull() { local model="${1:-}" [[ -z "$model" ]] && { echo "Usage: devlab pull "; exit 1; } docker exec ollama ollama pull "$model" } cmd_remove_model() { local model="${1:-}" [[ -z "$model" ]] && { echo "Usage: devlab remove "; exit 1; } docker exec ollama ollama rm "$model" } cmd_urls() { echo "" echo -e "${BOLD}DevLab Service URLs${RESET}" echo -e " Web IDE → ${GREEN}https://${CODE_HOST:-code.yourdomain.com}${RESET}" echo -e " Git → ${GREEN}https://${GIT_HOST:-git.yourdomain.com}${RESET}" echo -e " AI Chat → ${GREEN}https://${AI_HOST:-ai.yourdomain.com}${RESET}" echo -e " Portainer → ${GREEN}https://${PM_HOST:-portainer.yourdomain.com}${RESET}" echo "" } cmd_compose() { local action="${1:-}" shift case "$action" in up) local file="${1:-}" [[ -z "$file" ]] && { echo "Usage: devlab compose up "; exit 1; } docker compose -f "$file" up -d echo -e "${GREEN}Stack started.${RESET}" ;; down) local file="${1:-}" [[ -z "$file" ]] && { echo "Usage: devlab compose down "; exit 1; } docker compose -f "$file" down ;; ls) docker compose ls ;; *) echo "Usage: devlab compose {up|down|ls} [file]" ;; esac } # ─── Router ────────────────────────────────────────────────────────────────── COMMAND="${1:-help}" shift 2>/dev/null || true case "$COMMAND" in status) cmd_status ;; start) cmd_start ;; stop) cmd_stop ;; restart) cmd_restart "$@" ;; logs) cmd_logs "$@" ;; update) cmd_update ;; backup) cmd_backup ;; models) cmd_models ;; pull) cmd_pull "$@" ;; remove) cmd_remove_model "$@" ;; urls) cmd_urls ;; compose) cmd_compose "$@" ;; help|--help|-h) cmd_help ;; *) echo "Unknown command: ${COMMAND}. Run: devlab help" && exit 1 ;; esac