From 57de5573104d242d54e68c0f6a8c4cb74ebba5c1 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 22 Feb 2026 14:58:59 +0600 Subject: [PATCH 01/48] dash update --- lds | 97 +++++++++++++++++++++++++++++++++---------------------------- 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/lds b/lds index b4734e4..9c241a6 100755 --- a/lds +++ b/lds @@ -115,6 +115,33 @@ need() { done } + +# Stream search helper: prefers ripgrep (rg) when available, falls back to grep. +# Reads from stdin, prints matching lines with line numbers. +text_grep() { + local ins=0 + if [[ "${1:-}" == "-i" ]]; then + ins=1 + shift || true + fi + + local pat="${1:-}" + [[ -n "$pat" ]] || return 0 + + if command -v rg >/dev/null 2>&1; then + if ((ins)); then + rg -n -i -- "$pat" + else + rg -n -- "$pat" + fi + else + if ((ins)); then + grep -ni -- "$pat" + else + grep -n -- "$pat" + fi + fi +} ensure_files_exist() { local rel abs dir for rel in "$@"; do @@ -193,30 +220,28 @@ docker_compose() { lds_project() { printf '%s' "${__LDS_PROJECT:-$(basename -- "$DIR")}"; } # (QUIET by default) ──────────────────────────────── -dc_up() { - if ((VERBOSE)); then - docker_compose up "$@" - else - docker_compose up --quiet-pull "$@" - fi -} +# Centralize quiet/verbose handling for compose subcommands. +# Usage: dc_cmd [args...] +dc_cmd() { + local sub="${1:-}" + shift || true -dc_pull() { - if ((VERBOSE)); then - docker_compose pull "$@" - else - docker_compose pull -q "$@" + local -a quiet=() + if ((VERBOSE == 0)); then + case "$sub" in + up) quiet+=(--quiet-pull) ;; + pull) quiet+=(-q) ;; + build) quiet+=(--quiet) ;; + esac fi -} -dc_build() { - if ((VERBOSE)); then - docker_compose build "$@" - else - docker_compose build --quiet "$@" - fi + docker_compose "$sub" "${quiet[@]}" "$@" } +dc_up() { dc_cmd up "$@"; } +dc_pull() { dc_cmd pull "$@"; } +dc_build() { dc_cmd build "$@"; } + # helper for our own minimal logging (still shows in quiet mode) logv() { ((VERBOSE)) && printf "%b[%s]%b %s\n" "$CYAN" "${1:-info}" "$NC" "${2:-}" >&2 || true; } logq() { printf "%b[%s]%b %s\n" "$CYAN" "${1:-info}" "$NC" "${2:-}" >&2; } @@ -1216,15 +1241,6 @@ compose_has_build() { jq -e --arg s "$svc" '.services[$s].build != null' >/dev/null <<<"$json" return $? fi - if command -v python3 >/dev/null 2>&1; then - COMPOSE_CFG_JSON="$json" python3 - "$svc" <<'PY' -import json, os, sys -svc = sys.argv[1] -cfg = json.loads(os.environ.get("COMPOSE_CFG_JSON", "") or "{}") -sys.exit(0 if cfg.get("services", {}).get(svc, {}).get("build") is not None else 1) -PY - return $? - fi fi compose_cfg_yaml | awk -v s="$svc" ' @@ -1244,15 +1260,6 @@ compose_image_for_service() { jq -r --arg s "$svc" '.services[$s].image // empty' <<<"$json" return 0 fi - if command -v python3 >/dev/null 2>&1; then - COMPOSE_CFG_JSON="$json" python3 - "$svc" <<'PY' -import json, os, sys -svc = sys.argv[1] -cfg = json.loads(os.environ.get("COMPOSE_CFG_JSON", "") or "{}") -print(cfg.get("services", {}).get(svc, {}).get("image", "") or "") -PY - return 0 - fi fi compose_cfg_yaml | awk -v s="$svc" ' @@ -1607,13 +1614,13 @@ cmd_logs() { local s; s="$(resolve_service "$svc" || true)" [[ -n "$s" ]] || die "Unknown service: $svc" if [[ -n "$grep_pat" ]]; then - docker_compose logs "${args[@]}" "$s" 2>&1 | rg -n -- "$grep_pat" + docker_compose logs "${args[@]}" "$s" 2>&1 | text_grep "$grep_pat" else docker_compose logs "${args[@]}" "$s" fi else if [[ -n "$grep_pat" ]]; then - docker_compose logs "${args[@]}" 2>&1 | rg -n -- "$grep_pat" + docker_compose logs "${args[@]}" 2>&1 | text_grep "$grep_pat" else docker_compose logs "${args[@]}" fi @@ -1978,7 +1985,8 @@ cmd_rebuild() { # accepts: "all" or "1,3,5-7" or mix with names "nginx,2,5-6" # ----------------------------- _pick_targets_interactive() { - mapfile -t all_svcs < <(docker_compose config --services 2>/dev/null) + compose_services_load + all_svcs=("${__COMPOSE_SVCS[@]}") [[ ${#all_svcs[@]} -gt 0 ]] || die "No services found (docker compose config --services failed?)" echo @@ -2051,7 +2059,8 @@ cmd_rebuild() { if (($# == 0)); then _pick_targets_interactive elif [[ "${1,,}" == "all" ]]; then - mapfile -t targets < <(docker_compose config --services 2>/dev/null) + compose_services_load + targets=("${__COMPOSE_SVCS[@]}") [[ ${#targets[@]} -gt 0 ]] || die "No services found (docker compose config --services failed?)" else for arg in "$@"; do @@ -2307,8 +2316,8 @@ cmd_doctor() { shift || true local pat="${1:-error|failed|panic|segfault|permission denied|fatal}" local project; project="$(lds_project)" - # tail recent logs from compose services and grep with rg - docker_compose logs --since 30m 2>&1 | rg -n -i -- "$pat" || true + # tail recent logs from compose services and search (rg/grep) + docker_compose logs --since 30m 2>&1 | text_grep -i "$pat" || true return 0 fi @@ -3396,7 +3405,7 @@ ${CYAN}Config:${NC} ${CYAN}Doctor:${NC} doctor [run] Full environment checks doctor lint shellcheck (inside SERVER_TOOLS) - doctor scan-logs [pattern] rg scan (recent logs) + doctor scan-logs [pattern] log scan (recent logs) doctor fix Safe auto-fixes (when available) ${CYAN}Support:${NC} From 07668c0f55d6a03ae0909b3c5b4c1fa8a88a0971 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 22 Feb 2026 15:42:39 +0600 Subject: [PATCH 02/48] dash update --- lds | 420 +++++++++++++++++++++--------------------------------------- 1 file changed, 144 insertions(+), 276 deletions(-) diff --git a/lds b/lds index 9c241a6..ee59d25 100755 --- a/lds +++ b/lds @@ -1324,18 +1324,91 @@ cmd_ps() { fi } -cmd_stats() { +# Internal: stats table (service optional) +_status_show_stats() { local svc="${1:-}" - local project; project="$(lds_project)" + # docker stats does not reliably support --filter across versions; + # pass container names derived from compose itself. + local -a names=() if [[ -n "$svc" ]]; then - # accept service or container name - local s; s="$(resolve_service "$svc" || true)" - if [[ -n "$s" ]]; then - docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}' --filter "name=${project}_${s}" - return 0 + local s; s="$(resolve_service "$svc" 2>/dev/null || true)" + [[ -n "$s" ]] || { printf "(no matching containers)\n"; return 0; } + mapfile -t names < <(docker_compose ps "$s" --format '{{.Name}}' 2>/dev/null | sed '/^[[:space:]]*$/d') + else + mapfile -t names < <(docker_compose ps --format '{{.Name}}' 2>/dev/null | sed '/^[[:space:]]*$/d') + fi + + if (( ${#names[@]} )); then + docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}" "${names[@]}" + else + printf "(no matching containers)\n" + fi +} + +# Internal: compact checks for `status` (2 groups: System + Project) +_status_checks() { + _has() { command -v "$1" >/dev/null 2>&1; } + + local -a sys_ok=() sys_bad=() proj_ok=() proj_bad=() + + # System group + [[ -t 0 ]] && sys_ok+=(stdin) || sys_bad+=(stdin) + [[ -t 1 ]] && sys_ok+=(stdout) || sys_bad+=(stdout) + [[ -r /dev/tty ]] && sys_ok+=(tty) || sys_bad+=(tty) + + local c + for c in awk sed grep find sort; do + _has "$c" && sys_ok+=("$c") || sys_bad+=("$c") + done + + if _has docker || _has docker.exe; then + sys_ok+=(docker) + else + sys_bad+=(docker) + fi + + if docker_compose version >/dev/null 2>&1; then sys_ok+=(compose); else sys_bad+=(compose); fi + _has openssl && sys_ok+=(openssl) || sys_bad+=(openssl) + (_has curl || _has wget) && sys_ok+=(http) || sys_bad+=(http) + _has jq && sys_ok+=(jq) || sys_bad+=(jq) + + # Project group + [[ -f "$ENV_DOCKER" ]] && proj_ok+=(env) || proj_bad+=(env) + [[ -d "$DIR/docker" ]] && proj_ok+=(docker_dir) || proj_bad+=(docker_dir) + [[ -d "$DIR/configuration" ]] && proj_ok+=(config_dir) || proj_bad+=(config_dir) + [[ -d "$DIR/data" ]] && proj_ok+=(data_dir) || proj_bad+=(data_dir) + [[ -d "$DIR/logs" ]] && proj_ok+=(logs_dir) || proj_bad+=(logs_dir) + [[ -f "$DIR/configuration/rootCA/rootCA.pem" ]] && proj_ok+=(rootCA) || proj_bad+=(rootCA) + + if [[ -f "$DIR/lds" ]]; then + if grep -U $"\r" "$DIR/lds" >/dev/null 2>&1; then + proj_bad+=(crlf) + else + proj_ok+=(lf) fi fi - docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}' --filter "label=com.docker.compose.project=$project" + + # Print exactly two lines (tab separated) + printf "%bSystem%b\t" "$CYAN" "$NC" + if (( ${#sys_bad[@]} )); then + printf "%b!%b %s" "$YELLOW" "$NC" "${sys_bad[*]}" + if (( ${#sys_ok[@]} )); then printf "%b | %b" "$DIM" "$NC"; fi + fi + (( ${#sys_ok[@]} )) && printf "%b✓%b %s" "$GREEN" "$NC" "${sys_ok[*]}" + printf "\n" + + printf "%bProject%b\t" "$CYAN" "$NC" + if (( ${#proj_bad[@]} )); then + printf "%b!%b %s" "$YELLOW" "$NC" "${proj_bad[*]}" + if (( ${#proj_ok[@]} )); then printf "%b | %b" "$DIM" "$NC"; fi + fi + (( ${#proj_ok[@]} )) && printf "%b✓%b %s" "$GREEN" "$NC" "${proj_ok[*]}" + printf "\n" +} +_status_show_disk() { + docker system df + printf "\n%bProject data:%b\n" "$CYAN" "$NC" + du -sh "$DIR/data" 2>/dev/null || true } _status_urls() { @@ -1386,15 +1459,9 @@ _status_health_line() { fi } -cmd_status() { - local json=0 quiet=0 - while [[ "${1:-}" ]]; do - case "$1" in - --json) json=1; shift ;; - --quiet|-q) quiet=1; shift ;; - *) break ;; - esac - done +# Internal: main status output (containers/urls/json) +_status_core() { + local json="$1" quiet="$2" local project; project="$(lds_project)" @@ -1449,7 +1516,7 @@ cmd_status() { esac } - # NEW: health icon (✓, !, ×) - safe + simple + # health icon (✓, !, ×) _health_icon() { local h="$1" if [[ -z "$h" || "$h" == "null" || "$h" == "-" ]]; then @@ -1463,7 +1530,7 @@ cmd_status() { fi } - # NEW: running/total summary + # running/total summary local total running total=${#ctrs[@]} running=0 @@ -1490,7 +1557,7 @@ cmd_status() { first=0 local health_disp="${health:-}" - [[ -z "$health_disp" || "$health_disp" == "null" ]] && health_disp="-" + [[ -z "$health_disp" || "$health_disp" == "null" ]] && health_disp='-' printf '{' printf '"name":"%s",' "$(_json_escape "$name")" @@ -1528,13 +1595,11 @@ cmd_status() { printf "%bProject:%b %s\n" "$CYAN" "$NC" "$project" [[ -n "$profiles" ]] && printf "%bProfiles:%b %s\n" "$CYAN" "$NC" "$profiles" - # UPDATED: Containers line shows running/total summary printf "%bContainers:%b %b(%s/%s running)%b\n" "$CYAN" "$NC" "$DIM" "$running" "$total" "$NC" if ((${#ctrs[@]} == 0)); then printf " (none)\n" else - # column widths local w_name=4 w_svc=7 local line name svc state health for line in "${ctrs[@]}"; do @@ -1545,14 +1610,12 @@ cmd_status() { (( w_name > 34 )) && w_name=34 (( w_svc > 18 )) && w_svc=18 - # header printf " %b%-*s%b %b%-*s%b %b%-10s%b %b%s%b\n" \ "$BOLD" "$w_name" "NAME" "$NC" \ "$BOLD" "$w_svc" "SERVICE" "$NC" \ "$BOLD" "STATE" "$NC" \ "$BOLD" "HEALTH" "$NC" - # rows for line in "${ctrs[@]}"; do IFS=$'\t' read -r name svc state health <<<"$line" @@ -1565,15 +1628,14 @@ cmd_status() { hc="$(_health_color "${health:-}")" local health_disp="${health:-}" - [[ -z "$health_disp" || "$health_disp" == "null" ]] && health_disp="-" + [[ -z "$health_disp" || "$health_disp" == "null" ]] && health_disp='-' - # NEW: icon prefix local hi; hi="$(_health_icon "$health_disp")" printf " %-*s %-*s %b%-10s%b %b%s %s%b\n" \ "$w_name" "$name_disp" \ "$w_svc" "$svc_disp" \ - "$stc" "${state:-"-"}" "$NC" \ + "$stc" "${state:-'-'}" "$NC" \ "$hc" "$hi" "$health_disp" "$NC" done fi @@ -1592,6 +1654,31 @@ cmd_status() { fi } +cmd_status() { + local json=0 quiet=0 + while [[ "${1:-}" ]]; do + case "$1" in + --json) json=1; shift ;; + --quiet|-q) quiet=1; shift ;; + *) break ;; + esac + done + + # JSON output is intentionally scoped to core stack info only. + _status_core "$json" "$quiet" + if (( json )) || (( quiet )); then + return 0 + fi + + printf "\n%bStats:%b\n" "$CYAN" "$NC" + _status_show_stats "${1:-}" || true + + printf "\n%bDisk:%b\n" "$CYAN" "$NC" + _status_show_disk || true + + printf "\n%bChecks:%b\n" "$CYAN" "$NC" + _status_checks || true +} # ───────────────────────────────────────────────────────────────────────────── # 6b. LOGS / OPEN # ───────────────────────────────────────────────────────────────────────────── @@ -1895,16 +1982,6 @@ cmd_verify() { done < <(shopt -s nullglob; for f in "$DIR/configuration/nginx/"*.conf; do basename -- "$f" .conf; done; shopt -u nullglob) } -cmd_disk() { - docker system df - printf " -%bProject data:%b -" "$CYAN" "$NC" - du -sh "$DIR/data" 2>/dev/null || true -} - -cmd_du() { cmd_disk; } - # ───────────────────────────────────────────────────────────────────────────── # 6g. NGINX INTROSPECTION # ───────────────────────────────────────────────────────────────────────────── @@ -2295,10 +2372,9 @@ cmd_certificate() { } cmd_doctor() { - # Optional focused modes + # Focused modes stay on doctor (status uses the full run only). if [[ "${1:-}" == "--lint" ]]; then shift || true - # Run shellcheck inside SERVER_TOOLS against lds + bin scripts (if present) docker exec -i SERVER_TOOLS sh -lc ' set -e command -v shellcheck >/dev/null 2>&1 || { echo "shellcheck not found in SERVER_TOOLS"; exit 1; } @@ -2315,14 +2391,16 @@ cmd_doctor() { if [[ "${1:-}" == "--scan-logs" ]]; then shift || true local pat="${1:-error|failed|panic|segfault|permission denied|fatal}" - local project; project="$(lds_project)" - # tail recent logs from compose services and search (rg/grep) docker_compose logs --since 30m 2>&1 | text_grep -i "$pat" || true return 0 fi + _doctor_run +} + +_doctor_run() { local os_id os_like - IFS='|' read -r os_id os_like < <(detect_os_family) + IFS="|" read -r os_id os_like < <(detect_os_family) local is_win=0 [[ "${OSTYPE:-}" =~ (msys|cygwin|win32) ]] && is_win=1 @@ -2335,259 +2413,52 @@ cmd_doctor() { _warn() { printf " %b!%b %s\n" "$YELLOW" "$NC" "$*"; } _bad() { printf " %b✘%b %s\n" "$RED" "$NC" "$*"; } _has() { command -v "$1" >/dev/null 2>&1; } - _first() { - local c - for c in "$@"; do _has "$c" && { - echo "$c" - return 0 - }; done - return 1 - } - # ──────────────────────────────────────────────────────────────────────────── - # TTY sanity (prompt/menus) - # ──────────────────────────────────────────────────────────────────────────── + # TTY sanity [[ -t 0 ]] && _ok "stdin: ok" || _warn "stdin: not a TTY" [[ -t 1 ]] && _ok "stdout: ok" || _warn "stdout: not a TTY" [[ -r /dev/tty ]] && _ok "tty: ok" || _warn "tty: not readable" - # ──────────────────────────────────────────────────────────────────────────── - # Required base tools - # ──────────────────────────────────────────────────────────────────────────── + # Shell tool availability (grouped) + local -a avail=() miss=() local c for c in awk sed grep find sort; do - _has "$c" && _ok "$c: ok" || _bad "$c: missing" + if _has "$c"; then avail+=("$c"); else miss+=("$c"); fi done + printf " %bAvailable%b\t%s\n" "$GREEN" "$NC" "${avail[*]:- -}" + printf " %bUnavailable%b\t%s\n" "$RED" "$NC" "${miss[*]:- -}" - # ──────────────────────────────────────────────────────────────────────────── - # Windows essentials - # ──────────────────────────────────────────────────────────────────────────── - if ((is_win)); then - _has powershell.exe && _ok "powershell.exe: ok" || _bad "powershell.exe: missing" - _has cygpath && _ok "cygpath: ok" || _bad "cygpath: missing" - _has git.exe && _ok "git.exe: ok" || _warn "git.exe: missing" - _has bash && _ok "bash: ok" || _bad "bash: missing" - fi - - # ──────────────────────────────────────────────────────────────────────────── - # Docker CLI + daemon + context - # ──────────────────────────────────────────────────────────────────────────── - local docker_cmd - docker_cmd="$(_first docker docker.exe || true)" - + # Docker + daemon + local docker_cmd="" + if _has docker; then docker_cmd=docker; elif _has docker.exe; then docker_cmd=docker.exe; fi if [[ -n "$docker_cmd" ]]; then _ok "docker: $($docker_cmd --version 2>/dev/null || echo ok)" - - if $docker_cmd context show >/dev/null 2>&1; then - _ok "docker context: $($docker_cmd context show 2>/dev/null)" - else - _warn "docker context: unavailable" - fi - - if $docker_cmd info >/dev/null 2>&1; then - _ok "docker daemon: ok" - else - _bad "docker daemon: not reachable" - ((is_win)) && _warn "hint: start Docker Desktop; enable WSL2 engine" - fi + if $docker_cmd info >/dev/null 2>&1; then _ok "docker daemon: ok"; else _bad "docker daemon: not reachable"; fi else _bad "docker: missing" fi - # Compose presence (doctor should NOT print compose file path) - if docker_compose version >/dev/null 2>&1; then - _ok "compose: ok" - else - _bad "compose: missing" - fi - - # Compose config validation (reuse your wrapper; keep quiet) - if docker_compose config -q >/dev/null 2>&1; then - _ok "compose config: ok" - else - _bad "compose config: invalid (run: lds config)" - fi + # Compose + if docker_compose version >/dev/null 2>&1; then _ok "compose: ok"; else _bad "compose: missing"; fi + if docker_compose config -q >/dev/null 2>&1; then _ok "compose config: ok"; else _bad "compose config: invalid (run: lds config)"; fi - # ──────────────────────────────────────────────────────────────────────────── - # Optional tools (high-signal only) - # ──────────────────────────────────────────────────────────────────────────── + # Optional tools (quiet) _has openssl && _ok "openssl: ok" || _warn "openssl: missing" - (_has wget || _has curl) && _ok "http client: ok" || _warn "http client: missing (curl/wget)" + (_has curl || _has wget) && _ok "http client: ok" || _warn "http client: missing (curl/wget)" _has jq && _ok "jq: ok" || _warn "jq: missing" - # ──────────────────────────────────────────────────────────────────────────── # File/dir sanity (only show path when NOT ok) - # ──────────────────────────────────────────────────────────────────────────── [[ -f "$ENV_DOCKER" ]] && _ok "env: ok" || _warn "env: missing ($ENV_DOCKER)" [[ -d "$DIR/docker" ]] && _ok "docker dir: ok" || _bad "docker dir: missing ($DIR/docker)" [[ -d "$DIR/configuration" ]] && _ok "configuration dir: ok" || _bad "configuration dir: missing ($DIR/configuration)" [[ -d "$DIR/data" ]] && _ok "data dir: ok" || _warn "data dir: missing ($DIR/data)" [[ -d "$DIR/logs" ]] && _ok "logs dir: ok" || _warn "logs dir: missing ($DIR/logs)" - # Writable checks (mount stability) - if [[ -d "$DIR/data" ]]; then - [[ -w "$DIR/data" ]] && _ok "data writable: ok" || _bad "data writable: no ($DIR/data)" - fi - if [[ -d "$DIR/logs" ]]; then - [[ -w "$DIR/logs" ]] && _ok "logs writable: ok" || _bad "logs writable: no ($DIR/logs)" - fi - - # Cert input (needed by your cert commands) - [[ -f "$DIR/configuration/rootCA/rootCA.pem" ]] && - _ok "rootCA.pem: ok" || - _warn "rootCA.pem: missing ($DIR/configuration/rootCA/rootCA.pem)" + [[ -f "$DIR/configuration/rootCA/rootCA.pem" ]] && _ok "rootCA.pem: ok" || _warn "rootCA.pem: missing ($DIR/configuration/rootCA/rootCA.pem)" - # CRLF risk (bash scripts break) if [[ -f "$DIR/lds" ]]; then - if grep -U $'\r' "$DIR/lds" >/dev/null 2>&1; then - _warn "line endings: CRLF detected ($DIR/lds)" - else - _ok "line endings: ok" - fi + if grep -U $"\r" "$DIR/lds" >/dev/null 2>&1; then _warn "line endings: CRLF detected ($DIR/lds)"; else _ok "line endings: ok"; fi fi - - # ──────────────────────────────────────────────────────────────────────────── - # More checks (requested) - # ──────────────────────────────────────────────────────────────────────────── - printf "\n%bMore checks%b\n" "$CYAN" "$NC" - - # Ports availability — only public ports: 80/443 - doctor_port_in_use() { - local p="$1" - if ((is_win)); then - netstat -ano 2>/dev/null | awk '{print $2}' | grep -Eq ":${p}\$" && return 0 || return 1 - else - if _has ss; then - ss -lnt 2>/dev/null | awk '{print $4}' | grep -Eq "[:.]${p}\$" && return 0 || return 1 - elif _has lsof; then - lsof -nP -iTCP:"$p" -sTCP:LISTEN >/dev/null 2>&1 && return 0 || return 1 - else - return 2 - fi - fi - } - - local ports=(80 443) - local any_unknown=0 p - for p in "${ports[@]}"; do - if doctor_port_in_use "$p"; then - _warn "port $p: in use" - else - local rc=$? - if [[ $rc -eq 2 ]]; then - any_unknown=1 - else - _ok "port $p: ok" - fi - fi - done - ((any_unknown)) && _warn "port scan: limited (install ss or lsof)" - - # Disk space (warn only if low) - if df -Pk "$DIR" >/dev/null 2>&1; then - local avail_kb - avail_kb="$(df -Pk "$DIR" | awk 'NR==2{print $4}')" - if [[ -n "$avail_kb" ]] && ((avail_kb < 5 * 1024 * 1024)); then - _warn "disk: low (<5GiB free)" - else - _ok "disk: ok" - fi - else - _warn "disk: unable to check" - fi - - # WSL detection (Windows) - if ((is_win)); then - if _has wsl.exe; then - if wsl.exe -l -q >/dev/null 2>&1; then - _ok "WSL: ok" - else - _warn "WSL: present but not configured" - fi - else - _warn "WSL: not found" - fi - fi - - # Compose profiles sanity (.env vs supported profiles) - doctor_list_enabled_profiles() { - [[ -f "$ENV_DOCKER" ]] || return 0 - local line - line="$(grep -E '^(COMPOSE_PROFILES|PROFILES)=' "$ENV_DOCKER" 2>/dev/null | tail -n 1 || true)" - [[ -n "$line" ]] || return 0 - line="${line#*=}" - line="${line%\"}" - line="${line#\"}" - line="${line%\'}" - line="${line#\'}" - echo "$line" | tr ', ' '\n' | awk 'NF{print}' - } - - local -A valid_profiles=() - local key slug - for key in "${SERVICE_ORDER[@]}"; do - slug="${SERVICES[$key]}" - [[ -n "$slug" ]] && valid_profiles["$slug"]=1 - done - - local seen_any=0 bad_any=0 prof - while IFS= read -r prof; do - [[ -n "$prof" ]] || continue - seen_any=1 - if [[ -n "${valid_profiles[$prof]:-}" ]]; then - _ok "profiles: '$prof' ok" - else - bad_any=1 - _warn "profiles: unknown '$prof'" - fi - done < <(doctor_list_enabled_profiles || true) - - ((seen_any == 0)) && _warn "profiles: none set (COMPOSE_PROFILES=...)" - ((bad_any)) && _warn "profiles: sync COMPOSE_PROFILES with supported services" - - # ──────────────────────────────────────────────────────────────────────────── - # Tips - # ──────────────────────────────────────────────────────────────────────────── - printf "\n%bTips%b\n" "$CYAN" "$NC" - printf " - Use: %blds certificate uninstall --all%b if you switched distros/paths.\n" "$BLUE" "$NC" - printf " - Use: %blds config%b to inspect full resolved compose config.\n" "$BLUE" "$NC" - - # ──────────────────────────────────────────────────────────────────────────── - # Install hints (bottom) - # ──────────────────────────────────────────────────────────────────────────── - printf "\n%bInstall hints%b\n" "$CYAN" "$NC" - - if ((is_win)); then - printf " - Install Docker Desktop (WSL2 engine recommended)\n" - printf " - Install Git for Windows (Git Bash)\n" - printf " - Ensure docker.exe is on PATH and Docker Desktop is running\n" - printf " - (Optional) Install WSL: wsl --install\n" - elif _has apt-get; then - printf " - Debian/Ubuntu:\n" - printf " sudo apt-get update && sudo apt-get install -y ca-certificates curl wget openssl p11-kit jq\n" - printf " sudo apt-get install -y docker-compose-plugin\n" - elif _has dnf; then - printf " - Fedora/RHEL (dnf):\n" - printf " sudo dnf install -y ca-certificates curl wget openssl p11-kit-trust jq\n" - elif _has yum; then - printf " - RHEL/CentOS (yum):\n" - printf " sudo yum install -y ca-certificates curl wget openssl p11-kit-trust jq\n" - elif _has pacman; then - printf " - Arch:\n" - printf " sudo pacman -Syu --noconfirm ca-certificates curl wget openssl p11-kit jq\n" - elif _has apk; then - printf " - Alpine:\n" - printf " sudo apk add --no-cache ca-certificates curl wget openssl p11-kit jq\n" - printf " sudo update-ca-certificates\n" - elif _has zypper; then - printf " - SUSE:\n" - printf " sudo zypper install -y ca-certificates curl wget openssl p11-kit jq\n" - else - printf " - Install: docker + compose + ca-certificates + openssl + p11-kit (+ jq optional)\n" - fi - - # Extra feature: show quick “where to go next” suggestion only if env missing - [[ -f "$ENV_DOCKER" ]] || _warn "next: run 'lds setup init' to generate env defaults" } ############################################################################### @@ -3142,16 +3013,13 @@ cmd_stack() { ps) cmd_ps "$@" ;; logs) cmd_logs "$@" ;; exec) cmd_exec "$@" ;; - stats) cmd_stats "$@" ;; events) cmd_events "$@" ;; clean) cmd_clean "$@" ;; verify) cmd_verify "$@" ;; - disk) cmd_disk "$@" ;; - du) cmd_du "$@" ;; config) cmd_config "$@" ;; http) cmd_http "$@" ;; *) - die "stack " + die "stack " ;; esac } @@ -3283,7 +3151,7 @@ cmd_help() { ## Stack (compose + runtime) - `lds stack up` *(aliases: `up`)* -- `lds stack up` *(aliases: `start`)* +- `lds stack start` *(aliases: `start`)* - `lds stack down [--volumes --yes]` *(aliases: `down`, `stop`)* - `lds stack restart [svc]` *(aliases: `restart`, `reboot`)* - `lds stack reload` @@ -3291,11 +3159,11 @@ cmd_help() { - `lds stack ps` *(alias: `ps`)* - `lds stack logs [svc] [--follow] [--since ] [--grep ]` *(alias: `logs`)* - `lds stack exec [cmd…]` *(alias: `exec`)* -- `lds stack stats [svc]` *(alias: `stats`)* +- `lds stack status` also includes live `docker stats`, disk usage, and doctor summary. - `lds stack events [--since ]` *(alias: `events`)* - `lds stack clean --yes [--volumes]` *(alias: `clean`)* - `lds stack verify` *(alias: `verify`)* -- `lds stack disk` / `lds stack du` *(aliases: `disk`, `du`)* + ## Domain (vhost lifecycle + routing) - `lds domain add …` @@ -3379,13 +3247,13 @@ MD ${BOLD}LocalDevStack (lds)${NC} ${CYAN}Stack (compose + runtime):${NC} - stack up|start|down|restart|reload|status|ps|logs|exec|stats|events|clean|verify|disk|du|config + stack up|start|down|restart|reload|status|ps|logs|exec|events|clean|verify|config up Alias of: stack up start Alias of: stack start down|stop Alias of: stack down restart|reboot Alias of: stack restart - status|ps|logs|exec|stats|events Alias of: stack <...> - clean|verify|disk|du Alias of: stack <...> + status|ps|logs|exec|events Alias of: stack <...> + clean|verify Alias of: stack <...> ${CYAN}Domain (vhosts + routing):${NC} domain add|rm|ls|check|nginx From 06e16aa72f97378959371a9dd5f738844f4655cb Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 22 Feb 2026 16:00:44 +0600 Subject: [PATCH 03/48] dash update --- lds | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lds b/lds index ee59d25..eaae61b 100755 --- a/lds +++ b/lds @@ -1284,7 +1284,6 @@ cmd_start() { http_reload } -cmd_reload() { cmd_start "$@"; } cmd_stop() { docker_compose down; } @@ -1883,7 +1882,7 @@ cmd_host() { esac } -cmd_ui() { cmd_lzd; } +cmd_ui() { docker exec -it SERVER_TOOLS lazydocker; } cmd_runtime() { local which="${1:-}"; [[ -n "$which" ]] || die "runtime " @@ -2211,8 +2210,6 @@ cmd_tools() { ;; esac } -cmd_lzd() { docker exec -it SERVER_TOOLS lazydocker; } -cmd_lazydocker() { cmd_lzd; } cmd_http() { [[ ${1:-} == reload ]] && http_reload; } cmd_cli() { local ctr="${1:-}" @@ -3008,7 +3005,6 @@ cmd_stack() { start) cmd_start "$@" ;; down|stop) cmd_down "$@" ;; restart|reboot) cmd_restart "$@" ;; - reload) cmd_reload "$@" ;; status) cmd_status "$@" ;; ps) cmd_ps "$@" ;; logs) cmd_logs "$@" ;; @@ -3019,7 +3015,7 @@ cmd_stack() { config) cmd_config "$@" ;; http) cmd_http "$@" ;; *) - die "stack " + die "stack " ;; esac } @@ -3154,7 +3150,6 @@ cmd_help() { - `lds stack start` *(aliases: `start`)* - `lds stack down [--volumes --yes]` *(aliases: `down`, `stop`)* - `lds stack restart [svc]` *(aliases: `restart`, `reboot`)* -- `lds stack reload` - `lds stack status [--json] [--quiet]` *(alias: `status`)* - `lds stack ps` *(alias: `ps`)* - `lds stack logs [svc] [--follow] [--since ] [--grep ]` *(alias: `logs`)* @@ -3217,7 +3212,6 @@ Shortcuts: `open`, `bundle`, `notify`, `ui` map to `support …`. - `lds tools sh` - `lds tools exec ""` - `lds tools file ` -- `lds lzd` / `lds lazydocker` ## Setup - `lds setup init|permissions|domain|profiles` @@ -3247,7 +3241,7 @@ MD ${BOLD}LocalDevStack (lds)${NC} ${CYAN}Stack (compose + runtime):${NC} - stack up|start|down|restart|reload|status|ps|logs|exec|events|clean|verify|config + stack up|start|down|restart|status|ps|logs|exec|events|clean|verify|config up Alias of: stack up start Alias of: stack start down|stop Alias of: stack down @@ -3289,7 +3283,6 @@ ${CYAN}Secrets / Env:${NC} ${CYAN}Tools (SERVER_TOOLS):${NC} tools sh|exec|file - lzd|lazydocker ${CYAN}Setup:${NC} setup init|permissions|domain|profiles From be070c252ab86e1fa0b62fb584a23872147ee8c0 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Mon, 23 Feb 2026 14:20:00 +0600 Subject: [PATCH 04/48] dash update --- docker/conf/www-php.conf | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docker/conf/www-php.conf diff --git a/docker/conf/www-php.conf b/docker/conf/www-php.conf new file mode 100644 index 0000000..c2918bc --- /dev/null +++ b/docker/conf/www-php.conf @@ -0,0 +1,15 @@ +[www] +listen = 0.0.0.0:9000 +user = www-data +group = www-data + +pm = dynamic +pm.max_children = 30 +pm.start_servers = 3 +pm.min_spare_servers = 2 +pm.max_spare_servers = 8 +pm.max_requests = 300 + +catch_workers_output = yes +php_admin_flag[log_errors] = on +php_admin_value[error_log] = /var/log/php-fpm/_default.error.log \ No newline at end of file From cad2ca9287afdc331185cda9bd2bade749407f46 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Mon, 23 Feb 2026 16:20:13 +0600 Subject: [PATCH 05/48] dash update --- configuration/php/.gitignore | 1 + configuration/php/pools/.gitignore | 2 ++ docker/compose/php.yaml | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 configuration/php/pools/.gitignore diff --git a/configuration/php/.gitignore b/configuration/php/.gitignore index c96a04f..60bbdd2 100644 --- a/configuration/php/.gitignore +++ b/configuration/php/.gitignore @@ -1,2 +1,3 @@ * +!pools !.gitignore \ No newline at end of file diff --git a/configuration/php/pools/.gitignore b/configuration/php/pools/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/configuration/php/pools/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/docker/compose/php.yaml b/docker/compose/php.yaml index 657ac84..dbba576 100644 --- a/docker/compose/php.yaml +++ b/docker/compose/php.yaml @@ -10,7 +10,7 @@ x-php-service: &php-service datastore: {} volumes: - "${PROJECT_DIR:-./../../../application}:/app" - - ../conf/www.conf:/usr/local/etc/php-fpm.d/www.conf + - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf - ../conf/openssl.cnf:/etc/ssl/openssl.cnf - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini - "../../configuration/ssh:/home/${USER}/.ssh:ro" From 4c90f206c7328b9eb385778dd8f0e317aea5348a Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Tue, 24 Feb 2026 14:30:59 +0600 Subject: [PATCH 06/48] dash update --- docker/compose/companion.yaml | 1 + docker/compose/http.yaml | 2 ++ docker/compose/php.yaml | 2 ++ docker/conf/filebeat.yml | 60 ++++++++++++++++++++++++++++++++++- logs/.gitignore | 1 + logs/php-fpm/.gitignore | 2 ++ 6 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 logs/php-fpm/.gitignore diff --git a/docker/compose/companion.yaml b/docker/compose/companion.yaml index b824041..65f860f 100644 --- a/docker/compose/companion.yaml +++ b/docker/compose/companion.yaml @@ -20,6 +20,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock - "${SOP_REPO:-./../../../sops-repo}:/etc/share/vhosts/sops" - "${HOME}/.gitconfig:/home/root/.gitconfig:ro" + - ../../configuration/php/pools:/etc/share/vhosts/fpm networks: backend: ipv4_address: 172.29.0.10 diff --git a/docker/compose/http.yaml b/docker/compose/http.yaml index 19b6766..333f5f3 100644 --- a/docker/compose/http.yaml +++ b/docker/compose/http.yaml @@ -15,6 +15,7 @@ services: - ../../configuration/ssl:/etc/mkcert:ro - ../../configuration/rootCA:/etc/share/rootCA:ro - "${PROJECT_DIR:-./../../../application}:/app" + - php_fpm_run:/run/php-fpm extra_hosts: - "host.docker.internal:host-gateway" networks: @@ -38,6 +39,7 @@ services: - ../../logs/apache:/var/log/apache2 - ../../configuration/rootCA:/etc/share/rootCA:ro - "${PROJECT_DIR:-./../../../application}:/app" + - php_fpm_run:/run/php-fpm depends_on: - nginx networks: diff --git a/docker/compose/php.yaml b/docker/compose/php.yaml index dbba576..98cadc2 100644 --- a/docker/compose/php.yaml +++ b/docker/compose/php.yaml @@ -16,6 +16,8 @@ x-php-service: &php-service - "../../configuration/ssh:/home/${USER}/.ssh:ro" - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - ../../configuration/rootCA:/etc/share/rootCA:ro + - php_fpm_run:/run/php-fpm + - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro depends_on: - server-tools healthcheck: diff --git a/docker/conf/filebeat.yml b/docker/conf/filebeat.yml index 9fc7b3e..693dc4a 100644 --- a/docker/conf/filebeat.yml +++ b/docker/conf/filebeat.yml @@ -1,11 +1,15 @@ filebeat.inputs: + # 1) Global logs (everything), but EXCLUDE php-fpm so we can handle it separately with multiline, etc. - type: filestream id: lds-global-logs enabled: true paths: - /global/log/*/*.log - /global/log/*/*/*.log - exclude_files: ['-\d{8}(\.gz)?$'] + exclude_files: + - '-\d{8}(\.gz)?$' + - '^/global/log/php-fpm/.*\.error\.log$' + - '^/global/log/php-fpm/.*\.access\.log$' processors: - dissect: tokenizer: "/global/log/%{service}/%{file}" @@ -16,16 +20,70 @@ filebeat.inputs: - {from: "service", to: "service", type: "string"} ignore_missing: true fail_on_error: false + + # 2) PHP-FPM ERROR logs (multiline) + - type: filestream + id: php-fpm-error + enabled: true + paths: + - /global/log/php-fpm/*.error.log + exclude_files: ['-\d{8}(\.gz)?$'] + parsers: + - multiline: + type: pattern + # PHP error lines usually start: [23-Feb-2026 12:54:42] ... + pattern: '^\[[0-9]{2}-[A-Za-z]{3}-[0-9]{4} ' + negate: true + match: after + processors: + - dissect: + tokenizer: "/global/log/%{service}/%{file}" + field: "log.file.path" + target_prefix: "" + - dissect: + tokenizer: "%{domain}.%{log_kind}.log" + field: "file" + target_prefix: "" + - add_fields: + target: "" + fields: + log_kind: "error" + + # 3) PHP-FPM ACCESS logs + - type: filestream + id: php-fpm-access + enabled: true + paths: + - /global/log/php-fpm/*.access.log + exclude_files: ['-\d{8}(\.gz)?$'] + processors: + - dissect: + tokenizer: "/global/log/%{service}/%{file}" + field: "log.file.path" + target_prefix: "" + - dissect: + # filename: .access.log + tokenizer: "%{domain}.%{log_kind}.log" + field: "file" + target_prefix: "" + - add_fields: + target: "" + fields: + log_kind: "access" + output.elasticsearch: hosts: ["http://elasticsearch:9200"] index: "lds-%{[service]}-%{+yyyyMMdd}" + setup.template.enabled: true setup.template.name: "lds" setup.template.pattern: "lds-*-*" setup.template.overwrite: false + setup.dashboards.enabled: true setup.dashboards.retry.enabled: true setup.dashboards.retry.interval: 10s setup.dashboards.retry.maximum: 30 + setup.kibana: host: "http://kibana:5601" \ No newline at end of file diff --git a/logs/.gitignore b/logs/.gitignore index 8ba10b5..1b0dd2d 100755 --- a/logs/.gitignore +++ b/logs/.gitignore @@ -12,5 +12,6 @@ !mongodb !postgresql !redis +!php-fpm !olds !.gitignore \ No newline at end of file diff --git a/logs/php-fpm/.gitignore b/logs/php-fpm/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/logs/php-fpm/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file From e75faf1f09a0526e9dc051361a78a8da1c1839cd Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Tue, 24 Feb 2026 15:59:41 +0600 Subject: [PATCH 07/48] dash update --- docker/compose/main.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docker/compose/main.yaml b/docker/compose/main.yaml index 9362489..54948c1 100644 --- a/docker/compose/main.yaml +++ b/docker/compose/main.yaml @@ -24,6 +24,13 @@ networks: config: - subnet: 172.30.0.0/24 gateway: 172.30.0.1 +volumes: + php_fpm_run: + name: FPMStore + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "php-fpm sockets" include: - docker/compose/companion.yaml From 742e48e1fd6bc8c62db5bd87d2bbecebe45b30ab Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Tue, 24 Feb 2026 20:47:16 +0600 Subject: [PATCH 08/48] volume --- docker/compose/http.yaml | 4 ++-- docker/compose/main.yaml | 2 +- docker/compose/php.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker/compose/http.yaml b/docker/compose/http.yaml index 333f5f3..aa3e777 100644 --- a/docker/compose/http.yaml +++ b/docker/compose/http.yaml @@ -15,7 +15,7 @@ services: - ../../configuration/ssl:/etc/mkcert:ro - ../../configuration/rootCA:/etc/share/rootCA:ro - "${PROJECT_DIR:-./../../../application}:/app" - - php_fpm_run:/run/php-fpm + - lds_fpm_sock:/run/php-fpm extra_hosts: - "host.docker.internal:host-gateway" networks: @@ -39,7 +39,7 @@ services: - ../../logs/apache:/var/log/apache2 - ../../configuration/rootCA:/etc/share/rootCA:ro - "${PROJECT_DIR:-./../../../application}:/app" - - php_fpm_run:/run/php-fpm + - lds_fpm_sock:/run/php-fpm depends_on: - nginx networks: diff --git a/docker/compose/main.yaml b/docker/compose/main.yaml index 54948c1..2fd10f2 100644 --- a/docker/compose/main.yaml +++ b/docker/compose/main.yaml @@ -25,7 +25,7 @@ networks: - subnet: 172.30.0.0/24 gateway: 172.30.0.1 volumes: - php_fpm_run: + lds_fpm_sock: name: FPMStore labels: com.infocyph.lds: "1" diff --git a/docker/compose/php.yaml b/docker/compose/php.yaml index 98cadc2..b409262 100644 --- a/docker/compose/php.yaml +++ b/docker/compose/php.yaml @@ -16,7 +16,7 @@ x-php-service: &php-service - "../../configuration/ssh:/home/${USER}/.ssh:ro" - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - ../../configuration/rootCA:/etc/share/rootCA:ro - - php_fpm_run:/run/php-fpm + - lds_fpm_sock:/run/php-fpm - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro depends_on: - server-tools From ffaee8e774af5c34008a25dc375da2e44f67bbba Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Tue, 24 Feb 2026 21:34:54 +0600 Subject: [PATCH 09/48] Update main.yaml --- docker/compose/main.yaml | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/docker/compose/main.yaml b/docker/compose/main.yaml index 2fd10f2..9877dd9 100644 --- a/docker/compose/main.yaml +++ b/docker/compose/main.yaml @@ -26,11 +26,35 @@ networks: gateway: 172.30.0.1 volumes: lds_fpm_sock: - name: FPMStore + name: FPMSocks labels: com.infocyph.lds: "1" com.infocyph.stack: "LocalDevStack" - com.infocyph.purpose: "php-fpm sockets" + com.infocyph.purpose: "PHP-FPM Sockets" + lds_fpm_pools: + name: FPMPools + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "PHP-FPM Pools" + lds_nginx_host: + name: NginxHosts + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "Nginx Host Configs" + lds_apache_host: + name: ApacheHosts + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "Apache Host Configs" + lds_ssl: + name: SSLKeys + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "SSL Keys" include: - docker/compose/companion.yaml From 7d4998a3cc107277dd9f81ee594a711442853305 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 25 Feb 2026 16:07:48 +0600 Subject: [PATCH 10/48] dash update --- docker/compose/db-client.yaml | 2 +- docker/conf/filebeat.yml | 25 ++++++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docker/compose/db-client.yaml b/docker/compose/db-client.yaml index 395a10a..1646d2e 100644 --- a/docker/compose/db-client.yaml +++ b/docker/compose/db-client.yaml @@ -88,7 +88,7 @@ services: container_name: FILEBEAT restart: unless-stopped user: root - profiles: [elasticsearch] + profiles: [filebeat] depends_on: - elasticsearch volumes: diff --git a/docker/conf/filebeat.yml b/docker/conf/filebeat.yml index 693dc4a..e859977 100644 --- a/docker/conf/filebeat.yml +++ b/docker/conf/filebeat.yml @@ -1,5 +1,4 @@ filebeat.inputs: - # 1) Global logs (everything), but EXCLUDE php-fpm so we can handle it separately with multiline, etc. - type: filestream id: lds-global-logs enabled: true @@ -15,13 +14,25 @@ filebeat.inputs: tokenizer: "/global/log/%{service}/%{file}" field: "log.file.path" target_prefix: "" + # Try: domain.access.log / domain.error.log / anything..log + - dissect: + tokenizer: "%{stem}.%{log_kind}.log" + field: "file" + target_prefix: "" + ignore_failure: true + # Fallback for logs that don't match "..log" (e.g. mysql.log) + - add_fields: + when: + not: + has_fields: ["log_kind"] + target: "" + fields: + log_kind: "app" - convert: fields: - {from: "service", to: "service", type: "string"} ignore_missing: true fail_on_error: false - - # 2) PHP-FPM ERROR logs (multiline) - type: filestream id: php-fpm-error enabled: true @@ -44,12 +55,12 @@ filebeat.inputs: tokenizer: "%{domain}.%{log_kind}.log" field: "file" target_prefix: "" + ignore_failure: true - add_fields: target: "" fields: log_kind: "error" - # 3) PHP-FPM ACCESS logs - type: filestream id: php-fpm-access enabled: true @@ -62,10 +73,10 @@ filebeat.inputs: field: "log.file.path" target_prefix: "" - dissect: - # filename: .access.log tokenizer: "%{domain}.%{log_kind}.log" field: "file" target_prefix: "" + ignore_failure: true - add_fields: target: "" fields: @@ -73,11 +84,11 @@ filebeat.inputs: output.elasticsearch: hosts: ["http://elasticsearch:9200"] - index: "lds-%{[service]}-%{+yyyyMMdd}" + index: "lds-%{[service]}-%{[log_kind]}-%{+yyyyMMdd}" setup.template.enabled: true setup.template.name: "lds" -setup.template.pattern: "lds-*-*" +setup.template.pattern: "lds-*-*-*" setup.template.overwrite: false setup.dashboards.enabled: true From 31e761b143e62a1c891690df92d9cf7f73f46142 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 26 Feb 2026 16:13:20 +0600 Subject: [PATCH 11/48] dash update --- docker/compose/companion.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker/compose/companion.yaml b/docker/compose/companion.yaml index 65f860f..a7badee 100644 --- a/docker/compose/companion.yaml +++ b/docker/compose/companion.yaml @@ -21,7 +21,10 @@ services: - "${SOP_REPO:-./../../../sops-repo}:/etc/share/vhosts/sops" - "${HOME}/.gitconfig:/home/root/.gitconfig:ro" - ../../configuration/php/pools:/etc/share/vhosts/fpm + - ../../logs:/global/log:ro networks: + frontend: + ipv4_address: 172.28.0.13 backend: ipv4_address: 172.29.0.10 datastore: From 7e2656d004f033eeb785bf4d07f23d87f040a7e7 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Fri, 27 Feb 2026 12:25:41 +0600 Subject: [PATCH 12/48] lds split --- lds | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 92 insertions(+), 18 deletions(-) diff --git a/lds b/lds index eaae61b..22fd58a 100755 --- a/lds +++ b/lds @@ -7,7 +7,7 @@ if [ -z "${BASH_VERSION:-}" ]; then exec /usr/bin/env bash "$0" "$@"; fi set -euo pipefail ############################################################################### -# 0. PATHS & CONSTANTS +# 0. BOOTSTRAP / PATHS / CONSTANTS ############################################################################### # Resolve script directory portably (Linux/macOS/WSL) _realpath() { @@ -72,7 +72,7 @@ err() { say "${RED}$*${NC}"; } VERBOSE=0 #─────────────────────────────────────────────────────────────────────────────── -# 0. GLOBAL ERROR HANDLER +# 0a. GLOBAL ERROR HANDLER #─────────────────────────────────────────────────────────────────────────────── command_not_found_handle() { local unknown="$1" @@ -90,7 +90,7 @@ on_error() { } ############################################################################### -# 1. COMMON HELPERS +# 1. COMMON / IO HELPERS ############################################################################### die() { printf "%bError:%b %s\n" "$RED" "$NC" "$*" @@ -172,6 +172,10 @@ ensure_files_exist() { done } +############################################################################### +# 1a. DOCKER COMPOSE WRAPPER +############################################################################### + # ── compose extras (docker/extras/*.y{a,}ml) ──────────────────────────────── __EXTRAS_LOADED=0 declare -a __EXTRA_FILES=() @@ -246,6 +250,10 @@ dc_build() { dc_cmd build "$@"; } logv() { ((VERBOSE)) && printf "%b[%s]%b %s\n" "$CYAN" "${1:-info}" "$NC" "${2:-}" >&2 || true; } logq() { printf "%b[%s]%b %s\n" "$CYAN" "${1:-info}" "$NC" "${2:-}" >&2; } +############################################################################### +# 1b. PROMPTS + DOTENV HELPERS +############################################################################### + # Unified prompt helper (used by env_init + profiles) tty_readline() { # Robust prompt/read across Linux/macOS/WSL/Windows Git Bash. @@ -340,6 +348,10 @@ update_env() { fi } +############################################################################### +# 1c. HTTP / WEB SERVER HELPERS +############################################################################### + http_reload() { printf "%bReloading HTTP...%b" "$MAGENTA" "$NC" docker ps -qf name=NGINX &>/dev/null && docker exec NGINX nginx -s reload &>/dev/null || true @@ -348,7 +360,7 @@ http_reload() { } ############################################################################### -# 2. PERMISSIONS FIX-UP +# 2. INSTALL / PERMISSIONS (HOST) ############################################################################### add_to_windows_path() { [[ "$OSTYPE" =~ (msys|cygwin) ]] || return 0 @@ -412,7 +424,7 @@ fix_perms() { } ############################################################################### -# 3. DOMAIN & PROFILE UTILITIES +# 3. DOMAIN / PROFILE INTEGRATION ############################################################################### mkhost() { docker exec SERVER_TOOLS mkhost "$@"; } delhost() { docker exec SERVER_TOOLS delhost "$@"; } @@ -482,6 +494,10 @@ modify_profiles() { # Profiles # ───────────────────────────────────────────────────────────────────────────── +############################################################################### +# 3a. PROFILES: DEFINITIONS + SETUP FLOW +############################################################################### + declare -A SERVICES=( [POSTGRESQL]="postgresql" [MYSQL]="mysql" @@ -647,7 +663,7 @@ process_all() { } ############################################################################### -# 4a. LAUNCH PHP CONTAINER INSIDE DOCROOT +# 4. CONTAINER SHELL HELPERS (PHP / NODE) ############################################################################### launch_php() { local domain=$1 suffix @@ -678,9 +694,9 @@ launch_php() { docker exec -it "$php" bash --login -c "cd '$docroot' && exec bash" } -############################################################################### -# 4b. LAUNCH NODE CONTAINER (always /app) -############################################################################### +# ----------------------------------------------------------------------------- +# Node shell (always /app) +# ----------------------------------------------------------------------------- launch_node() { local domain="${1:-}" [[ -n "$domain" ]] || die "Usage: lds core " @@ -745,7 +761,7 @@ conf_node_container() { } ############################################################################### -# 5. ENV + CERT +# 5. ENVIRONMENT + CERT / CA ############################################################################### detect_timezone() { if command -v timedatectl &>/dev/null; then @@ -1275,7 +1291,7 @@ compose_image_for_service() { } ############################################################################### -# 6. COMMANDS +# 6. STACK COMMANDS (CLI) ############################################################################### cmd_up() { dc_up "$@"; } @@ -2676,7 +2692,16 @@ run_find_container() { run_build() { local tag="$1" dir="$2" - printf "%b[run]%b Building image %b%s%b from %s\n" "$CYAN" "$NC" "$BLUE" "$tag" "$NC" "$dir" + + # Build only if the image doesn't already exist. + if docker image inspect "$tag" >/dev/null 2>&1; then + printf "%b[run]%b Image exists, skipping build: %b%s%b +" "$CYAN" "$NC" "$BLUE" "$tag" "$NC" + return 0 + fi + + printf "%b[run]%b Building image %b%s%b from %s +" "$CYAN" "$NC" "$BLUE" "$tag" "$NC" "$dir" docker build -t "$tag" "$dir" } @@ -2809,16 +2834,20 @@ run_exec_shell() { } cmd_run() { - local action="shell" dir="$PWD" name="" tag="" nobuild=0 keepalive=1 sock=0 + local action="*" dir="$PWD" name="" tag="" nobuild=0 keepalive=1 sock=0 local -a publish=() mounts=() local open_port="" open_path="/" open_proto="http" while [[ $# -gt 0 ]]; do case "$1" in - stop | rm | ps | shell | logs | open) + stop | rm | ps | shell | logs | open | "*") action="$1" shift ;; + build) + action="*" + shift + ;; --name) name="${2:-}" shift 2 @@ -2888,7 +2917,17 @@ cmd_run() { # Plan/name/tag should be based on the real project identity (POSIX dir) IFS='|' read -r def_name def_tag _def_dir < <(run_plan "$dir_posix") name="${name:-$def_name}" - tag="${tag:-$def_tag}" + + # Tag rules: + # - Default tag is ":local" (from run_plan) + # - If user passes --tag without ":", append ":local" + if [[ -n "${tag:-}" ]]; then + if [[ "$tag" != *:* ]]; then + tag="${tag}:local" + fi + else + tag="$def_tag" + fi _find_for_dir() { local found @@ -2904,6 +2943,32 @@ cmd_run() { return 1 } + _run_summary() { + local img="$1" build_dir="$2" cname="$3" + local tag_only="${img##*:}" + + printf "\n%b[run]%b Summary\n" "$CYAN" "$NC" + printf " %bImage:%b %s\n" "$BOLD" "$NC" "$img" + printf " %bTag:%b %s\n" "$BOLD" "$NC" "$tag_only" + printf " %bDir:%b %s\n" "$BOLD" "$NC" "$build_dir" + printf " %bName:%b %s\n" "$BOLD" "$NC" "$cname" + printf " %bKeepalive:%b %s\n" "$BOLD" "$NC" "$keepalive" + printf " %bSock:%b %s\n" "$BOLD" "$NC" "$sock" + + if ((${#publish[@]})); then + printf " %bPublish:%b %s\n" "$BOLD" "$NC" "${publish[*]}" + else + printf " %bPublish:%b (none)\n" "$BOLD" "$NC" + fi + + if ((${#mounts[@]})); then + printf " %bMounts:%b %s\n" "$BOLD" "$NC" "${mounts[*]}" + else + printf " %bMounts:%b (none)\n" "$BOLD" "$NC" + fi + printf "\n" + } + case "$action" in ps) docker ps -a --filter "label=com.infocyph.lds.run=1" \ @@ -2967,7 +3032,7 @@ cmd_run() { fi return 0 ;; - shell | *) + shell | "*") if ((nobuild == 0)); then # Build needs docker.exe-friendly path on Windows run_build "$tag" "$dir_docker" @@ -2975,6 +3040,8 @@ cmd_run() { printf "%b[run]%b Skipping build (--no-build)\n" "$YELLOW" "$NC" fi + _run_summary "$tag" "$dir_posix" "$name" + if docker inspect -f '{{.State.Running}}' "$name" 2>/dev/null | grep -q true; then printf "%b[run]%b Container already running: %s\n" "$GREEN" "$NC" "$name" else @@ -2988,7 +3055,14 @@ cmd_run() { "${publish[@]}" -- "${mounts[@]}" fi - run_exec_shell "$name" + # "shell" enters the container; "*" / "build" does not. + if [[ "$action" == "shell" ]]; then + run_exec_shell "$name" + else + printf "%b[run]%b Built/started. Use %blds run shell%b to enter, %blds run logs%b to follow logs.\n" \ + "$GREEN" "$NC" "$BLUE" "$NC" "$BLUE" "$NC" + return 0 + fi ;; esac } @@ -3217,7 +3291,7 @@ Shortcuts: `open`, `bundle`, `notify`, `ui` map to `support …`. - `lds setup init|permissions|domain|profiles` ## Runner (ad‑hoc Dockerfile runner) -- `lds run` (+ `ps|logs|stop|rm|open` and flags: `--publish|-p`, `--no-keepalive`, `--mount`, `--sock`) +- `lds run` *(default: build+start only)* / `lds run shell` *(build+start+enter)* / `lds run *` *(same as default)* (+ `ps|logs|stop|rm|open` and flags: `--publish|-p`, `--no-keepalive`, `--mount`, `--sock`, `--tag`, `--name`) ## Runtime awareness - `lds runtime php|node` From a8ee27ddc335e419ac7774ec358e1b6c67f196b2 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Fri, 27 Feb 2026 12:55:49 +0600 Subject: [PATCH 13/48] lds split --- lds | 163 ++++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 119 insertions(+), 44 deletions(-) diff --git a/lds b/lds index 22fd58a..2f4f667 100755 --- a/lds +++ b/lds @@ -53,20 +53,42 @@ COLOR() { printf '[%sm' "$1"; } ############################################################################### # Colors + UI (higher contrast; aligned with mkhost.sh) ############################################################################### -BOLD=$'' -DIM=$'' -RED=$'' -GREEN=$'' -CYAN=$'' -YELLOW=$'' -BLUE=$'' -MAGENTA=$'' -NC=$'' - -say() { echo -e "$*"; } -ok() { say "${GREEN}$*${NC}"; } -warn() { say "${YELLOW}$*${NC}"; } -err() { say "${RED}$*${NC}"; } +# Color control: +# - If stdout isn't a TTY, disable colors by default. +# - If NO_COLOR is set, disable colors. +# - Set LDS_FORCE_COLOR=1 to force colors. +_is_tty() { [[ -t 1 ]]; } + +_use_color=1 +if [[ "${LDS_FORCE_COLOR:-0}" != "1" ]]; then + if [[ -n "${NO_COLOR:-}" ]] || ! _is_tty; then + _use_color=0 + fi +fi + +if ((_use_color)); then + BOLD=$'\033[1m' + DIM=$'\033[2m' + RED=$'\033[1;31m' + GREEN=$'\033[1;32m' + CYAN=$'\033[1;36m' + YELLOW=$'\033[1;33m' + BLUE=$'\033[1;34m' + MAGENTA=$'\033[1;35m' + NC=$'\033[0m' +else + BOLD='' DIM='' RED='' GREEN='' CYAN='' YELLOW='' BLUE='' MAGENTA='' NC='' +fi + +# Output control: +# - --quiet suppresses non-error output +QUIET=0 + +say() { ((QUIET)) || printf '%b\n' "$*"; } +ok() { ((QUIET)) || printf '%b\n' "${GREEN}$*${NC}"; } +warn() { ((QUIET)) || printf '%b\n' "${YELLOW}$*${NC}"; } +err() { printf '%b\n' "${RED}$*${NC}" >&2; } + # Default behavior: QUIET VERBOSE=0 @@ -84,9 +106,23 @@ command_not_found_handle() { trap 'on_error $? $LINENO "$BASH_COMMAND"' ERR on_error() { - printf "\n%bError:%b '%s' failed at line %d (exit %d)\n\n" \ - "$RED" "$NC" "$3" "$2" "$1" - exit "$1" + local code="$1" line="$2" cmd="$3" + local fn="${FUNCNAME[1]:-main}" + local src="${BASH_SOURCE[1]:-$0}" + + printf "\n%bError:%b %s:%s in %s() (exit %d)\n" "$RED" "$NC" "$src" "$line" "$fn" "$code" >&2 + printf "%bCommand:%b %s\n" "$RED" "$NC" "$cmd" >&2 + + if ((VERBOSE)); then + printf "%bStack:%b\n" "$DIM" "$NC" >&2 + local i=1 + while caller "$i" >/dev/null 2>&1; do + caller "$i" >&2 + i=$((i+1)) + done + fi + printf "\n" >&2 + exit "$code" } ############################################################################### @@ -181,20 +217,25 @@ __EXTRAS_LOADED=0 declare -a __EXTRA_FILES=() load_extras() { + # Set LDS_EXTRAS_RELOAD=1 (or global --reload-extras) to re-scan templates every call. + if [[ "${LDS_EXTRAS_RELOAD:-0}" == "1" ]]; then + __EXTRAS_LOADED=0 + fi + ((__EXTRAS_LOADED)) && return 0 __EXTRAS_LOADED=1 [[ -d "$EXTRAS_DIR" ]] || return 0 mapfile -t __EXTRA_FILES < <( - find "$EXTRAS_DIR" -maxdepth 1 -type f \( -name '*.yaml' -o -name '*.yml' \) -print 2>/dev/null \ - | sort \ - | sed '/^[[:space:]]*$/d' + find "$EXTRAS_DIR" -maxdepth 1 -type f \( -name '*.yaml' -o -name '*.yml' \) -print 2>/dev/null | sort | sed '/^[[:space:]]*$/d' ) } docker_compose() { load_extras + # Create required runtime files only when docker/compose operations are invoked. + ((EUID == 0)) || ensure_files_exist "/docker/.env" "/configuration/php/php.ini" "/.env" if [[ -z "${__LDS_DC_BIN:-}" ]]; then if docker compose version >/dev/null 2>&1; then __LDS_DC_BIN=(docker compose) @@ -2943,11 +2984,11 @@ cmd_run() { return 1 } - _run_summary() { + _run_build_summary() { local img="$1" build_dir="$2" cname="$3" local tag_only="${img##*:}" - printf "\n%b[run]%b Summary\n" "$CYAN" "$NC" + printf "\n%b[run]%b Build summary\n" "$CYAN" "$NC" printf " %bImage:%b %s\n" "$BOLD" "$NC" "$img" printf " %bTag:%b %s\n" "$BOLD" "$NC" "$tag_only" printf " %bDir:%b %s\n" "$BOLD" "$NC" "$build_dir" @@ -2969,6 +3010,26 @@ cmd_run() { printf "\n" } + _run_runtime_summary() { + local cname="$1" + local id img state ports + id="$(docker inspect -f '{{.Id}}' "$cname" 2>/dev/null | cut -c1-12 || true)" + img="$(docker inspect -f '{{.Config.Image}}' "$cname" 2>/dev/null || true)" + state="$(docker inspect -f '{{.State.Status}}' "$cname" 2>/dev/null || true)" + ports="$(docker port "$cname" 2>/dev/null | sed '/^[[:space:]]*$/d' | tr '\n' '; ' | sed 's/; $//' || true)" + + printf "%b[run]%b Runtime summary\n" "$CYAN" "$NC" + printf " %bContainer:%b %s\n" "$BOLD" "$NC" "${cname}${id:+ ($id)}" + [[ -n "$img" ]] && printf " %bImage:%b %s\n" "$BOLD" "$NC" "$img" + [[ -n "$state" ]] && printf " %bState:%b %s\n" "$BOLD" "$NC" "$state" + if [[ -n "$ports" ]]; then + printf " %bPorts:%b %s\n" "$BOLD" "$NC" "$ports" + else + printf " %bPorts:%b (none published)\n" "$BOLD" "$NC" + fi + printf "\n" + } + case "$action" in ps) docker ps -a --filter "label=com.infocyph.lds.run=1" \ @@ -3040,7 +3101,7 @@ cmd_run() { printf "%b[run]%b Skipping build (--no-build)\n" "$YELLOW" "$NC" fi - _run_summary "$tag" "$dir_posix" "$name" + _run_build_summary "$tag" "$dir_posix" "$name" if docker inspect -f '{{.State.Running}}' "$name" 2>/dev/null | grep -q true; then printf "%b[run]%b Container already running: %s\n" "$GREEN" "$NC" "$name" @@ -3055,7 +3116,9 @@ cmd_run() { "${publish[@]}" -- "${mounts[@]}" fi - # "shell" enters the container; "*" / "build" does not. + _run_runtime_summary "$name" + +# "shell" enters the container; "*" / "build" does not. if [[ "$action" == "shell" ]]; then run_exec_shell "$name" else @@ -3381,45 +3444,57 @@ EOF # 7. MAIN ############################################################################### main() { - need docker - ((EUID == 0)) || ensure_files_exist "/docker/.env" "/configuration/php/php.ini" "/.env" - - [[ $# -gt 0 ]] || { - cmd_help - exit 1 - } + [[ $# -gt 0 ]] || { cmd_help; exit 1; } while [[ $# -gt 0 ]]; do case "$1" in - -v | --verbose) + -v|--verbose) VERBOSE=1 + QUIET=0 shift ;; - -q | --quiet) + -q|--quiet) + QUIET=1 VERBOSE=0 shift ;; + --reload-extras) + export LDS_EXTRAS_RELOAD=1 + shift + ;; --) shift break ;; + -h|--help) + cmd_help + exit 0 + ;; -*) die "Unknown global option: $1" ;; *) break ;; esac done - [[ $# -gt 0 ]] || { - cmd_help - exit 1 - } + [[ $# -gt 0 ]] || { cmd_help; exit 1; } + + local cmd="${1,,}" + shift || true + + if [[ "$cmd" == "help" ]]; then + cmd_help "$@" + exit 0 + fi + + # Do not require docker for pure help output; everything else expects the stack. + need docker - case "${1,,}" in - php | composer | node | npm | npx) exec "$DIR/bin/$1" "${@:2}" ;; - pg | pg_restore | pg-restore | pgrestore | pg_dump | pgdump | pg-dump | psql) exec "$DIR/bin/pg" "${@:2}" ;; - maria | mariadb | mariadbdump | mariadb-dump | mariadb_dump) exec "$DIR/bin/maria" "${@:2}" ;; - my | mysql | mysqldump | mysql-dump | mysql_dump) exec "$DIR/bin/my" "${@:2}" ;; - redis | redis-cli) exec "$DIR/bin/redis-cli" "${@:2}" ;; - *) cmd_"${1,,}" "${@:2}" ;; + case "$cmd" in + php|composer|node|npm|npx) exec "$DIR/bin/$cmd" "$@" ;; + pg|pg_restore|pg-restore|pgrestore|pg_dump|pgdump|pg-dump|psql) exec "$DIR/bin/pg" "$@" ;; + maria|mariadb|mariadbdump|mariadb-dump|mariadb_dump) exec "$DIR/bin/maria" "$@" ;; + my|mysql|mysqldump|mysql-dump|mysql_dump) exec "$DIR/bin/my" "$@" ;; + redis|redis-cli) exec "$DIR/bin/redis-cli" "$@" ;; + *) "cmd_$cmd" "$@" ;; esac } From df2503e35997c4be2f66d3af4127de16bfa478f6 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Fri, 27 Feb 2026 21:20:17 +0600 Subject: [PATCH 14/48] lds split --- lds | 327 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 319 insertions(+), 8 deletions(-) diff --git a/lds b/lds index 2f4f667..8c2d5fb 100755 --- a/lds +++ b/lds @@ -3130,6 +3130,311 @@ cmd_run() { esac } + +############################################################################### +# 6w. NEW FEATURES: stack diff | domain export/import | support trace +############################################################################### + +# stack diff: show what would run (compose) vs what's running (docker) +cmd_stack_diff() { + local json=0 + local show_config=0 + while [[ "${1:-}" ]]; do + case "$1" in + --json) json=1; shift ;; + --config) show_config=1; shift ;; + *) break ;; + esac + done + + local project; project="$(lds_project)" + local cfg_json="" + + if docker_compose config --format json >/dev/null 2>&1; then + cfg_json="$(docker_compose config --format json)" + else + # fallback: best-effort text config + cfg_json="" + fi + + # running: service -> image + declare -A running=() + local line + while IFS= read -r line; do + [[ -n "$line" ]] || continue + local svc="${line%%|*}" + local img="${line#*|}" + running["$svc"]="$img" + done < <(docker ps \ + --filter "label=com.docker.compose.project=$project" \ + --format '{{index .Labels "com.docker.compose.service"}}|{{.Image}}' 2>/dev/null || true) + + # desired: service -> image/build context (best-effort) + declare -A desired_img=() + declare -A desired_ctx=() + declare -A desired_df=() + + if [[ -n "$cfg_json" ]]; then + if command -v jq >/dev/null 2>&1; then + while IFS= read -r line; do + local svc="${line%%|*}" + local img="${line#*|}" + desired_img["$svc"]="$img" + done < <(printf '%s' "$cfg_json" | jq -r '.services | to_entries[] | "\(.key)|\(.value.image // "")"') + while IFS= read -r line; do + local svc="${line%%|*}" + local ctx="${line#*|}" + desired_ctx["$svc"]="$ctx" + done < <(printf '%s' "$cfg_json" | jq -r '.services | to_entries[] | "\(.key)|\(.value.build.context // "")"') + while IFS= read -r line; do + local svc="${line%%|*}" + local df="${line#*|}" + desired_df["$svc"]="$df" + done < <(printf '%s' "$cfg_json" | jq -r '.services | to_entries[] | "\(.key)|\(.value.build.dockerfile // "")"') + else + # try via SERVER_TOOLS jq + while IFS= read -r line; do + local svc="${line%%|*}" + local img="${line#*|}" + desired_img["$svc"]="$img" + done < <(_tools_exec "printf %q \"$cfg_json\" | jq -r '.services | to_entries[] | \"\\(.key)|\\(.value.image // \\\"\\\")\"' " 2>/dev/null || true) + fi + fi + + # Build result object + if ((json)); then + if command -v jq >/dev/null 2>&1; then + # assemble in bash -> jq + local tmp; tmp="$(mktemp)" + { + printf '{' + printf '"project":%s,' "$(printf '%s' "$project" | jq -Rsa .)" + printf '"compose_file":%s,' "$(printf '%s' "$COMPOSE_FILE" | jq -Rsa .)" + printf '"running":{' + local first=1 k + for k in "${!running[@]}"; do + ((first)) || printf ',' + first=0 + printf '%s:%s' "$(printf '%s' "$k" | jq -R .)" "$(printf '%s' "${running[$k]}" | jq -R .)" + done + printf '},' + printf '"desired":{' + first=1 + for k in "${!desired_img[@]}"; do + ((first)) || printf ',' + first=0 + printf '%s:%s' "$(printf '%s' "$k" | jq -R .)" "$(printf '%s' "${desired_img[$k]}" | jq -R .)" + done + printf '},' + printf '"diff":[' + first=1 + # union keys + declare -A seen=() + for k in "${!running[@]}"; do seen["$k"]=1; done + for k in "${!desired_img[@]}"; do seen["$k"]=1; done + for k in "${!seen[@]}"; do + local r="${running[$k]:-}" + local d="${desired_img[$k]:-}" + if [[ "$r" != "$d" ]]; then + ((first)) || printf ',' + first=0 + printf '{"service":%s,"running":%s,"desired":%s}' \ + "$(printf '%s' "$k" | jq -R .)" \ + "$(printf '%s' "$r" | jq -R .)" \ + "$(printf '%s' "$d" | jq -R .)" + fi + done + printf ']' + printf '}\n' + } >"$tmp" + cat "$tmp" | jq . + rm -f "$tmp" + else + die "jq required for --json (or run inside SERVER_TOOLS)" + fi + return 0 + fi + + printf "%bStack diff%b (project=%s)\n" "$CYAN" "$NC" "$project" + printf "%bCompose file:%b %s\n" "$DIM" "$NC" "$COMPOSE_FILE" + + if ((show_config)); then + if [[ -n "$cfg_json" ]]; then + printf "\n%bEffective compose config (json):%b\n" "$DIM" "$NC" + printf '%s\n' "$cfg_json" + else + printf "\n%bEffective compose config:%b\n" "$DIM" "$NC" + docker_compose config || true + fi + fi + + # union services + declare -A all=() + local svc + for svc in "${!running[@]}"; do all["$svc"]=1; done + for svc in "${!desired_img[@]}"; do all["$svc"]=1; done + + printf "\n%-22s %-40s %-40s %s\n" "SERVICE" "RUNNING" "DESIRED" "STATUS" + printf "%-22s %-40s %-40s %s\n" "------" "-------" "-------" "------" + for svc in $(printf '%s\n' "${!all[@]}" | sort); do + local r="${running[$svc]:-}" + local d="${desired_img[$svc]:-}" + local st + if [[ -z "$r" ]]; then st="(not running)" + elif [[ -z "$d" ]]; then st="(not in config)" + elif [[ "$r" == "$d" ]]; then st="OK" + else st="DIFF" + fi + printf "%-22s %-40.40s %-40.40s %s\n" "$svc" "$r" "$d" "$st" + done + + printf "\n%bNotes:%b\n" "$DIM" "$NC" + printf " - Desired image is derived from 'docker compose config'. If a service uses only 'build:' and no 'image:', desired may be empty.\n" + printf " - Use: lds stack diff --config (to print resolved compose config)\n" +} + +# domain export: dump nginx vhosts (and a few inferred properties) to JSON +cmd_domain_export() { + local out="" pretty=1 + while [[ "${1:-}" ]]; do + case "$1" in + --out) out="${2:-}"; shift 2 ;; + --compact) pretty=0; shift ;; + *) break ;; + esac + done + + local dir_ng="$DIR/configuration/nginx" + [[ -d "$dir_ng" ]] || die "nginx vhost dir not found: $dir_ng" + + local tmp; tmp="$(mktemp)" + printf '{ "version": 1, "exported_at": "%s", "root": "%s", "domains": [' "$(date -Iseconds)" "$DIR" >"$tmp" + + local first=1 f dom typ upstream cmb + shopt -s nullglob + for f in "$dir_ng"/*.conf; do + dom="$(basename -- "$f" .conf)" + typ="static" + upstream="" + cmb="" + if grep -q 'fastcgi_pass' "$f"; then + typ="php" + upstream="$(grep -Eo 'fastcgi_pass[[:space:]]+[^;]+' "$f" | awk '{print $2}' | head -n1 || true)" + elif grep -q 'proxy_pass' "$f"; then + typ="proxy" + upstream="$(grep -m1 -Eo 'proxy_pass[[:space:]]+http[s]?://[^;]+' "$f" | awk '{print $2}' | head -n1 || true)" + fi + cmb="$(grep -m1 -Eo 'client_max_body_size[[:space:]]+[^;]+' "$f" | awk '{print $2}' | head -n1 || true)" + + ((first)) || printf ',' >>"$tmp" + first=0 + # include raw conf as base64 so import can restore exactly + local b64 + b64="$(base64 -w0 <"$f" 2>/dev/null || base64 <"$f" | tr -d '\n')" + printf '\n{ "domain": "%s", "type": "%s", "upstream": "%s", "client_max_body_size": "%s", "files": { "nginx_conf": "%s" } }' \ + "$dom" "$typ" "$upstream" "$cmb" "$b64" >>"$tmp" + done + shopt -u nullglob + printf '\n] }\n' >>"$tmp" + + if [[ -n "$out" ]]; then + if ((pretty)) && command -v jq >/dev/null 2>&1; then + cat "$tmp" | jq . >"$out" + else + cat "$tmp" >"$out" + fi + ok "[ok] domain export: $out\n" + else + if ((pretty)) && command -v jq >/dev/null 2>&1; then + cat "$tmp" | jq . + else + cat "$tmp" + fi + fi + rm -f "$tmp" +} + +# domain import: restore nginx vhosts from JSON created by domain export +cmd_domain_import() { + local in="${1:-}" + [[ -n "$in" ]] || die "domain import " + [[ -r "$in" ]] || die "cannot read: $in" + + local dir_ng="$DIR/configuration/nginx" + mkdir -p "$dir_ng" 2>/dev/null || true + + command -v jq >/dev/null 2>&1 || die "jq required for domain import" + + local count=0 + while IFS= read -r dom; do + local b64 + b64="$(jq -r --arg d "$dom" '.domains[] | select(.domain==$d) | .files.nginx_conf' "$in")" + [[ -n "$b64" && "$b64" != "null" ]] || continue + printf '%s' "$b64" | base64 -d >"$dir_ng/$dom.conf" + count=$((count+1)) + done < <(jq -r '.domains[].domain' "$in") + + ok "[ok] domain import: restored $count nginx vhost(s) into $dir_ng\n" + warn "[info] Next steps: run 'lds stack restart nginx' (or 'lds stack up') to apply changes.\n" +} + +# support trace: quick end-to-end trace for a domain +cmd_support_trace() { + local dom="${1:-}" + [[ -n "$dom" ]] || die "support trace " + + local nconf="$DIR/configuration/nginx/$dom.conf" + printf "%bTrace%b: %s\n" "$CYAN" "$NC" "$dom" + + # 1) DNS + if docker inspect -f '{{.State.Running}}' SERVER_TOOLS 2>/dev/null | grep -qx true; then + printf "\n%b[DNS]%b\n" "$DIM" "$NC" + _tools_exec "dig +short $(_shq "$dom") || true; getent hosts $(_shq "$dom") 2>/dev/null || true" + else + printf "\n%b[DNS]%b\n" "$DIM" "$NC" + (command -v dig >/dev/null 2>&1 && dig +short "$dom") || true + (command -v getent >/dev/null 2>&1 && getent hosts "$dom") || true + fi + + # 2) TLS certificate + printf "\n%b[TLS]%b\n" "$DIM" "$NC" + if docker inspect -f '{{.State.Running}}' SERVER_TOOLS 2>/dev/null | grep -qx true; then + _tools_exec "echo | openssl s_client -connect $(_shq "$dom"):443 -servername $(_shq "$dom") -showcerts 2>/dev/null | openssl x509 -noout -subject -issuer -dates 2>/dev/null || true" + else + echo | openssl s_client -connect "${dom}:443" -servername "$dom" -showcerts 2>/dev/null | openssl x509 -noout -subject -issuer -dates 2>/dev/null || true + fi + + # 3) HTTP probe (timings) + printf "\n%b[HTTP]%b\n" "$DIM" "$NC" + if docker inspect -f '{{.State.Running}}' SERVER_TOOLS 2>/dev/null | grep -qx true; then + _tools_exec "curl -sk -o /dev/null -D - -w 'time_namelookup=%{time_namelookup}\ntime_connect=%{time_connect}\ntime_appconnect=%{time_appconnect}\ntime_starttransfer=%{time_starttransfer}\ntime_total=%{time_total}\nhttp_code=%{http_code}\n' https://$(_shq "$dom") | sed -n '1,30p'" + else + curl -sk -o /dev/null -D - -w $'time_namelookup=%{time_namelookup}\ntime_connect=%{time_connect}\ntime_appconnect=%{time_appconnect}\ntime_starttransfer=%{time_starttransfer}\ntime_total=%{time_total}\nhttp_code=%{http_code}\n' "https://$dom" | sed -n '1,30p' + fi + + # 4) Upstream inference from nginx conf (if exists) + printf "\n%b[Upstream]%b\n" "$DIM" "$NC" + if [[ -r "$nconf" ]]; then + if grep -q fastcgi_pass "$nconf"; then + local php; php="$(grep -Eo 'fastcgi_pass[[:space:]]+[^;]+' "$nconf" | awk '{print $2}' | head -n1 || true)" + printf "type=php\nfastcgi_pass=%s\n" "${php:-unknown}" + elif grep -q proxy_pass "$nconf"; then + local up; up="$(grep -m1 -Eo 'proxy_pass[[:space:]]+http[s]?://[^;]+' "$nconf" | awk '{print $2}' | head -n1 || true)" + printf "type=proxy\nproxy_pass=%s\n" "${up:-unknown}" + else + printf "type=static\n" + fi + else + printf "nginx_conf=%s (missing)\n" "$nconf" + fi + + # 5) Recent nginx logs (compose) + printf "\n%b[Recent nginx logs]%b\n" "$DIM" "$NC" + docker_compose logs --no-color --tail 120 nginx 2>/dev/null | text_grep -i "$dom" || docker_compose logs --no-color --tail 120 nginx 2>/dev/null || true + + printf "\n%bDone.%b If this still looks wrong, run: lds doctor run | lds diag tls %s\n" "$GREEN" "$NC" "$dom" +} + ############################################################################### # 6x. GROUPED COMMAND ROUTERS (stack/domain/support) + backward-compatible aliases ############################################################################### @@ -3149,7 +3454,8 @@ cmd_stack() { events) cmd_events "$@" ;; clean) cmd_clean "$@" ;; verify) cmd_verify "$@" ;; - config) cmd_config "$@" ;; + config) cmd_config "$@" ;; diff) cmd_stack_diff "$@" ;; + http) cmd_http "$@" ;; *) die "stack " @@ -3161,14 +3467,16 @@ cmd_stack() { cmd_domain() { local sub="${1:-}"; shift || true case "${sub,,}" in - ""|help|-h|--help) die "domain " ;; + ""|help|-h|--help) die "domain " ;; add) cmd_host add "$@" ;; rm|remove|del|delete) cmd_host rm "$@" ;; ls|list) cmd_host list "$@" ;; + export) cmd_domain_export "$@" ;; + import) cmd_domain_import "$@" ;; check) cmd_check upstream "$@" ;; nginx) cmd_nginx "$@" ;; *) - die "domain " + die "domain " ;; esac } @@ -3176,13 +3484,13 @@ cmd_domain() { cmd_support() { local sub="${1:-}"; shift || true case "${sub,,}" in - ""|help|-h|--help) die "support " ;; + ""|help|-h|--help) die "support " ;; open) cmd_open "$@" ;; bundle) cmd_bundle "$@" ;; notify) cmd_notify "$@" ;; ui) cmd_ui "$@" ;; *) - die "support " + die "support " ;; esac } @@ -3295,6 +3603,7 @@ cmd_help() { - `lds stack events [--since ]` *(alias: `events`)* - `lds stack clean --yes [--volumes]` *(alias: `clean`)* - `lds stack verify` *(alias: `verify`)* +- `lds stack diff [--config] [--json]` *(shows desired vs running images)* ## Domain (vhost lifecycle + routing) @@ -3334,7 +3643,8 @@ Legacy alias: `lds host …` → same subcommands as `domain`. - `lds doctor fix` ## Support -- `lds support open ` +- `lds support open + support trace ` - `lds support bundle [--redact|--full]` - `lds support notify …` - `lds support ui` @@ -3378,7 +3688,7 @@ MD ${BOLD}LocalDevStack (lds)${NC} ${CYAN}Stack (compose + runtime):${NC} - stack up|start|down|restart|status|ps|logs|exec|events|clean|verify|config + stack up|start|down|restart|status|ps|logs|exec|events|clean|verify|config|diff up Alias of: stack up start Alias of: stack start down|stop Alias of: stack down @@ -3387,7 +3697,7 @@ ${CYAN}Stack (compose + runtime):${NC} clean|verify Alias of: stack <...> ${CYAN}Domain (vhosts + routing):${NC} - domain add|rm|ls|check|nginx + domain add|rm|ls|export|import|check|nginx host add|rm|list|check|nginx Legacy alias of: domain <...> ${CYAN}Certificates (TLS):${NC} @@ -3409,6 +3719,7 @@ ${CYAN}Doctor:${NC} ${CYAN}Support:${NC} support open + support trace support bundle [--redact|--full] support notify ... support ui From 2d70363f35fce7005b818005c331929b2de9ab29 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Fri, 27 Feb 2026 22:24:02 +0600 Subject: [PATCH 15/48] lds split --- lds | 346 ++++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 219 insertions(+), 127 deletions(-) diff --git a/lds b/lds index 8c2d5fb..5279476 100755 --- a/lds +++ b/lds @@ -6,6 +6,181 @@ if [ -z "${BASH_VERSION:-}" ]; then exec /usr/bin/env bash "$0" "$@"; fi set -euo pipefail +############################################################################### +# 1. COMMON / IO HELPERS +############################################################################### +die() { + printf "%bError:%b %s\n" "$RED" "$NC" "$*" + exit 1 +} + +# ───────────────────────────────────────────────────────────────────────────── +# Tool proxy (enabled only when LDS_PROXY_TOOLS=1) +# ───────────────────────────────────────────────────────────────────────────── +lds_tools_cmd() { + local cmd="${1:-}" + shift || true + [[ -n "$cmd" ]] || { echo "lds_tools_cmd: missing command" >&2; return 2; } + + local -a flags=() + [[ -t 0 ]] && flags+=(-i) + [[ -t 1 ]] && flags+=(-t) + + # if command exists on host + if has_cmd "$cmd"; then + command "$cmd" "$@" + return $? + fi + + # If SERVER_TOOLS is running + if docker inspect -f '{{.State.Running}}' SERVER_TOOLS 2>/dev/null | grep -qx true; then + if docker exec "${flags[@]}" SERVER_TOOLS "$cmd" "$@"; then + return 0 + fi + fi + + echo "Error: '$cmd' not available (SERVER_TOOLS not running and host command missing)" >&2 + return 127 +} + +if ((${LDS_PROXY_TOOLS:-0})); then + jq() { lds_tools_cmd jq "$@"; } + yq() { lds_tools_cmd yq "$@"; } + rg() { lds_tools_cmd rg "$@"; } + fd() { lds_tools_cmd fd "$@"; } + shellcheck() { lds_tools_cmd shellcheck "$@"; } + tree() { lds_tools_cmd tree "$@"; } +fi +# ───────────────────────────────────────────────────────────────────────────── +# Command presence (cached) + Safe Tool proxy integration +# - has_cmd: fast cached command lookup +# - need_cmd: hard requirement (host-only) +# - need_tool: requirement that may be satisfied via SERVER_TOOLS when LDS_PROXY_TOOLS=1 +# ───────────────────────────────────────────────────────────────────────────── +declare -A __LDS_HAS_CMD=() + +# Fast cached host-side command existence check +has_cmd() { + local c="${1:-}" + [[ -n "$c" ]] || return 1 + if [[ -n "${__LDS_HAS_CMD[$c]+x}" ]]; then + return "${__LDS_HAS_CMD[$c]}" + fi + command -v "$c" >/dev/null 2>&1 + __LDS_HAS_CMD[$c]=$? + return "${__LDS_HAS_CMD[$c]}" +} + +need_cmd() { + local c="${1:-}" hint="${2:-}" + has_cmd "$c" && return 0 + die "Missing required command: $c${hint:+ ($hint)}" +} + +# Commands that must NEVER be proxied to SERVER_TOOLS (host control plane / privileged) +_is_host_only_cmd() { + case "${1:-}" in + docker|sudo|su|systemctl|service|ip|iptables|nft|sysctl|mount|umount|modprobe|insmod|rmmod|rm|mv|cp|chmod|chown) + return 0 ;; + esac + return 1 +} + +# Check if a tool exists either on host OR (when enabled and safe) inside SERVER_TOOLS +has_tool() { + local c="${1:-}" + has_cmd "$c" && return 0 + + ((${LDS_PROXY_TOOLS:-0})) || return 1 + _is_host_only_cmd "$c" && return 1 + + # proxy needs host docker to interrogate SERVER_TOOLS + has_cmd docker || return 1 + docker inspect -f '{{.State.Running}}' SERVER_TOOLS 2>/dev/null | grep -qx true || return 1 + + docker exec SERVER_TOOLS sh -lc "command -v "$c" >/dev/null 2>&1" >/dev/null 2>&1 +} + +need_tool() { + local c="${1:-}" hint="${2:-}" + has_tool "$c" && return 0 + die "Missing required command: $c${hint:+ ($hint)}" +} + +need() { + local group found cmd + for group in "$@"; do + IFS='|,' read -ra alts <<<"$group" + found=0 + for cmd in "${alts[@]}"; do + has_cmd "$cmd" && { + found=1 + break + } + done + ((found)) && continue + local miss=${alts[*]} + miss=${miss// / or } + die "Missing command(s): $miss" + done +} + + +# Stream search helper: prefers ripgrep (rg) when available, falls back to grep. +# Reads from stdin, prints matching lines with line numbers. +text_grep() { + local ins=0 + if [[ "${1:-}" == "-i" ]]; then + ins=1 + shift || true + fi + + local pat="${1:-}" + [[ -n "$pat" ]] || return 0 + + if has_cmd rg; then + if ((ins)); then + rg -n -i -- "$pat" + else + rg -n -- "$pat" + fi + else + if ((ins)); then + grep -ni -- "$pat" + else + grep -n -- "$pat" + fi + fi +} +ensure_files_exist() { + local rel abs dir + for rel in "$@"; do + abs="${DIR}${rel}" + dir="${abs%/*}" + + if [[ ! -d $dir ]]; then + if mkdir -p "$dir" 2>/dev/null; then + printf "%b- Created directory %s%b\n" "$YELLOW" "$dir" "$NC" + else + printf "%b- Warning:%b cannot create directory %s (permissions?)\n" \ + "$YELLOW" "$NC" "$dir" + continue + fi + elif [[ ! -w $dir ]]; then + printf "%b- Warning:%b directory not writable: %s\n" "$YELLOW" "$NC" "$dir" + fi + + if [[ -e $abs ]]; then + [[ -w $abs ]] || printf "%b- Warning:%b file not writable: %s\n" "$YELLOW" "$NC" "$abs" + else + if : >"$abs" 2>/dev/null; then + printf "%b- Created file %s%b\n" "$YELLOW" "$abs" "$NC" + else + printf "%b- Error:%b cannot create file %s (permissions?)\n" "$RED" "$NC" "$abs" + fi + fi + done +} ############################################################################### # 0. BOOTSTRAP / PATHS / CONSTANTS ############################################################################### @@ -13,7 +188,7 @@ set -euo pipefail _realpath() { local p="$1" - if command -v realpath >/dev/null 2>&1; then + if has_cmd realpath; then realpath "$p" return 0 fi @@ -25,7 +200,7 @@ _realpath() { fi # macOS with coreutils - if command -v greadlink >/dev/null 2>&1; then + if has_cmd greadlink; then greadlink -f "$p" return 0 fi @@ -125,89 +300,6 @@ on_error() { exit "$code" } -############################################################################### -# 1. COMMON / IO HELPERS -############################################################################### -die() { - printf "%bError:%b %s\n" "$RED" "$NC" "$*" - exit 1 -} - -need() { - local group found cmd - for group in "$@"; do - IFS='|,' read -ra alts <<<"$group" - found=0 - for cmd in "${alts[@]}"; do - command -v "$cmd" &>/dev/null && { - found=1 - break - } - done - ((found)) && continue - local miss=${alts[*]} - miss=${miss// / or } - die "Missing command(s): $miss" - done -} - - -# Stream search helper: prefers ripgrep (rg) when available, falls back to grep. -# Reads from stdin, prints matching lines with line numbers. -text_grep() { - local ins=0 - if [[ "${1:-}" == "-i" ]]; then - ins=1 - shift || true - fi - - local pat="${1:-}" - [[ -n "$pat" ]] || return 0 - - if command -v rg >/dev/null 2>&1; then - if ((ins)); then - rg -n -i -- "$pat" - else - rg -n -- "$pat" - fi - else - if ((ins)); then - grep -ni -- "$pat" - else - grep -n -- "$pat" - fi - fi -} -ensure_files_exist() { - local rel abs dir - for rel in "$@"; do - abs="${DIR}${rel}" - dir="${abs%/*}" - - if [[ ! -d $dir ]]; then - if mkdir -p "$dir" 2>/dev/null; then - printf "%b- Created directory %s%b\n" "$YELLOW" "$dir" "$NC" - else - printf "%b- Warning:%b cannot create directory %s (permissions?)\n" \ - "$YELLOW" "$NC" "$dir" - continue - fi - elif [[ ! -w $dir ]]; then - printf "%b- Warning:%b directory not writable: %s\n" "$YELLOW" "$NC" "$dir" - fi - - if [[ -e $abs ]]; then - [[ -w $abs ]] || printf "%b- Warning:%b file not writable: %s\n" "$YELLOW" "$NC" "$abs" - else - if : >"$abs" 2>/dev/null; then - printf "%b- Created file %s%b\n" "$YELLOW" "$abs" "$NC" - else - printf "%b- Error:%b cannot create file %s (permissions?)\n" "$RED" "$NC" "$abs" - fi - fi - done -} - ############################################################################### # 1a. DOCKER COMPOSE WRAPPER ############################################################################### @@ -405,7 +497,7 @@ http_reload() { ############################################################################### add_to_windows_path() { [[ "$OSTYPE" =~ (msys|cygwin) ]] || return 0 - command -v cygpath >/dev/null 2>&1 || return 0 + has_cmd cygpath || return 0 # Only add if lds.bat exists where we think it is [[ -f "$DIR/lds.bat" ]] || return 0 @@ -805,13 +897,13 @@ conf_node_container() { # 5. ENVIRONMENT + CERT / CA ############################################################################### detect_timezone() { - if command -v timedatectl &>/dev/null; then + if has_cmd timedatectl; then timedatectl show -p Timezone --value elif [[ -n ${TZ-} ]]; then printf '%s' "$TZ" elif [[ -r /etc/timezone ]]; then /dev/null; then + elif has_cmd powershell.exe; then powershell.exe -NoProfile -Command "[System.TimeZoneInfo]::Local.Id" 2>/dev/null | tr -d '\r' else date +%Z @@ -865,7 +957,7 @@ detect_os_family() { . /etc/os-release || true id="${ID:-unknown}" like="${ID_LIKE:-unknown}" - elif command -v uname >/dev/null 2>&1; then + elif has_cmd uname; then # fallback for macOS / other unix case "$(uname -s 2>/dev/null || true)" in Darwin) @@ -912,14 +1004,14 @@ is_windows_shell() { } need_windows_tools() { - command -v cygpath >/dev/null 2>&1 || die "Windows certificate install needs 'cygpath' (Git Bash)." - command -v powershell.exe >/dev/null 2>&1 || die "Windows certificate install needs 'powershell.exe' on PATH." + has_cmd cygpath || die "Windows certificate install needs 'cygpath' (Git Bash)." + has_cmd powershell.exe || die "Windows certificate install needs 'powershell.exe' on PATH." } # Import CA into the invoking user's NSS DB (Chrome/Chromium/Firefox on many Linux setups) install_ca_nss_user() { local ca_file="$1" - command -v certutil >/dev/null 2>&1 || return 0 + has_cmd certutil || return 0 local user="${SUDO_USER:-}" [[ -n "$user" && "$user" != "root" ]] || return 0 @@ -996,7 +1088,7 @@ install_ca() { case "$family" in debian | alpine) - if command -v update-ca-certificates >/dev/null 2>&1; then + if has_cmd update-ca-certificates; then printf "%bUpdating trust store%b (update-ca-certificates)…\n" "$CYAN" "$NC" if update-ca-certificates; then printf "%b✔ Trust store updated%b\n" "$GREEN" "$NC" @@ -1009,7 +1101,7 @@ install_ca() { fi # Optional p11-kit sync: best-effort only (can be missing helper on minimal installs) - if command -v trust >/dev/null 2>&1; then + if has_cmd trust; then printf "%bSyncing p11-kit%b (trust extract-compat)…\n" "$CYAN" "$NC" if trust extract-compat >/dev/null 2>&1; then printf "%b✔ p11-kit trust synced%b\n" "$GREEN" "$NC" @@ -1021,7 +1113,7 @@ install_ca() { fi ;; rhel) - if command -v update-ca-trust >/dev/null 2>&1; then + if has_cmd update-ca-trust; then printf "%bUpdating trust store%b (update-ca-trust extract)…\n" "$CYAN" "$NC" if update-ca-trust extract; then printf "%b✔ Trust store updated%b\n" "$GREEN" "$NC" @@ -1033,7 +1125,7 @@ install_ca() { fi ;; arch) - if command -v trust >/dev/null 2>&1; then + if has_cmd trust; then printf "%bUpdating trust store%b (trust extract-compat)…\n" "$CYAN" "$NC" if trust extract-compat >/dev/null 2>&1; then printf "%b✔ Trust store updated%b\n" "$GREEN" "$NC" @@ -1093,7 +1185,7 @@ uninstall_ca_windows() { } uninstall_ca_nss_user() { - command -v certutil >/dev/null 2>&1 || return 0 + has_cmd certutil || return 0 local user="${SUDO_USER:-}" [[ -n "$user" && "$user" != "root" ]] || return 0 @@ -1160,20 +1252,20 @@ uninstall_ca() { case "$family" in debian | alpine) - if command -v update-ca-certificates >/dev/null 2>&1; then + if has_cmd update-ca-certificates; then printf "%bUpdating trust store%b (update-ca-certificates)…\n" "$CYAN" "$NC" update-ca-certificates || printf "%bWARN%b: update-ca-certificates failed.\n" "$YELLOW" "$NC" >&2 else printf "%bWARN%b: update-ca-certificates not found; trust store not refreshed.\n" "$YELLOW" "$NC" >&2 fi - if command -v trust >/dev/null 2>&1; then + if has_cmd trust; then printf "%bSyncing p11-kit%b (trust extract-compat)…\n" "$CYAN" "$NC" trust extract-compat >/dev/null 2>&1 || printf "%bWARN%b: trust extract-compat failed. Skipping.\n" "$YELLOW" "$NC" >&2 fi ;; rhel) - if command -v update-ca-trust >/dev/null 2>&1; then + if has_cmd update-ca-trust; then printf "%bUpdating trust store%b (update-ca-trust extract)…\n" "$CYAN" "$NC" update-ca-trust extract || printf "%bWARN%b: update-ca-trust extract failed.\n" "$YELLOW" "$NC" >&2 else @@ -1181,7 +1273,7 @@ uninstall_ca() { fi ;; arch) - if command -v trust >/dev/null 2>&1; then + if has_cmd trust; then printf "%bUpdating trust store%b (trust extract-compat)…\n" "$CYAN" "$NC" trust extract-compat >/dev/null 2>&1 || printf "%bWARN%b: trust extract-compat failed.\n" "$YELLOW" "$NC" >&2 else @@ -1189,15 +1281,15 @@ uninstall_ca() { fi ;; *) - if command -v update-ca-certificates >/dev/null 2>&1; then + if has_cmd update-ca-certificates; then printf "%bUpdating trust store%b (update-ca-certificates)…\n" "$CYAN" "$NC" update-ca-certificates || true fi - if command -v update-ca-trust >/dev/null 2>&1; then + if has_cmd update-ca-trust; then printf "%bUpdating trust store%b (update-ca-trust extract)…\n" "$CYAN" "$NC" update-ca-trust extract || true fi - if command -v trust >/dev/null 2>&1; then + if has_cmd trust; then printf "%bSyncing p11-kit%b (trust extract-compat)…\n" "$CYAN" "$NC" trust extract-compat >/dev/null 2>&1 || true fi @@ -1294,7 +1386,7 @@ compose_has_build() { local svc="$1" json json="$(compose_cfg_json)" if [[ -n "$json" ]]; then - if command -v jq >/dev/null 2>&1; then + if has_cmd jq; then jq -e --arg s "$svc" '.services[$s].build != null' >/dev/null <<<"$json" return $? fi @@ -1313,7 +1405,7 @@ compose_image_for_service() { local svc="$1" json json="$(compose_cfg_json)" if [[ -n "$json" ]]; then - if command -v jq >/dev/null 2>&1; then + if has_cmd jq; then jq -r --arg s "$svc" '.services[$s].image // empty' <<<"$json" return 0 fi @@ -1403,7 +1495,7 @@ _status_show_stats() { # Internal: compact checks for `status` (2 groups: System + Project) _status_checks() { - _has() { command -v "$1" >/dev/null 2>&1; } + _has() { has_cmd "$1"; } local -a sys_ok=() sys_bad=() proj_ok=() proj_bad=() @@ -2466,7 +2558,7 @@ _doctor_run() { _ok() { printf " %b✔%b %s\n" "$GREEN" "$NC" "$*"; } _warn() { printf " %b!%b %s\n" "$YELLOW" "$NC" "$*"; } _bad() { printf " %b✘%b %s\n" "$RED" "$NC" "$*"; } - _has() { command -v "$1" >/dev/null 2>&1; } + _has() { has_cmd "$1"; } # TTY sanity [[ -t 0 ]] && _ok "stdin: ok" || _warn "stdin: not a TTY" @@ -2532,7 +2624,7 @@ notify_watch() { local timeout="${1:-2500}" urgency="${2:-normal}" title="${3:-Notification}" body="${4:-}" # Linux desktop (or WSLg) - if command -v notify-send >/dev/null 2>&1; then + if has_cmd notify-send; then (env DISPLAY="${_disp-}" DBUS_SESSION_BUS_ADDRESS="${_dbus-}" \ setsid -f notify-send -u "$urgency" -t "$timeout" "$title" "$body" \ >/dev/null 2>&1 || true) & @@ -2540,7 +2632,7 @@ notify_watch() { fi # Windows toast (Git Bash) / WSL-on-Windows - if command -v powershell.exe >/dev/null 2>&1; then + if has_cmd powershell.exe; then # Pass values as args to avoid quoting issues entirely. # Note: urgency/timeout not used by toast api here; kept for parity. powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \ @@ -2591,7 +2683,7 @@ notify_watch() { trap _watcher_int_term INT TERM local grep_cmd=(grep -a --line-buffered -E "^${prefix}([[:space:]]|$)") - command -v stdbuf >/dev/null 2>&1 && grep_cmd=(stdbuf -oL -eL "${grep_cmd[@]}") + has_cmd stdbuf && grep_cmd=(stdbuf -oL -eL "${grep_cmd[@]}") printf "%bNotify Watch:%b monitoring is active. Ctrl+C to stop.\n" "$GREEN" "$NC" @@ -2667,25 +2759,25 @@ open_url() { # WSL/Windows helpers first when available if grep -qi microsoft /proc/version 2>/dev/null; then - if command -v powershell.exe >/dev/null 2>&1; then + if has_cmd powershell.exe; then powershell.exe -NoProfile -Command "Start-Process '$url'" >/dev/null 2>&1 || true return 0 fi - if command -v cmd.exe >/dev/null 2>&1; then + if has_cmd cmd.exe; then cmd.exe /c start "" "$url" >/dev/null 2>&1 || true return 0 fi fi - if command -v xdg-open >/dev/null 2>&1; then + if has_cmd xdg-open; then (xdg-open "$url" >/dev/null 2>&1 &) return 0 fi - if command -v open >/dev/null 2>&1; then + if has_cmd open; then (open "$url" >/dev/null 2>&1 &) return 0 fi - if command -v powershell >/dev/null 2>&1; then + if has_cmd powershell; then (powershell -NoProfile -Command "Start-Process '$url'" >/dev/null 2>&1 &) return 0 fi @@ -2698,9 +2790,9 @@ open_url() { ############################################################################### hash_short() { local s="$1" - if command -v sha1sum >/dev/null 2>&1; then + if has_cmd sha1sum; then printf '%s' "$s" | sha1sum | cut -c1-8 - elif command -v shasum >/dev/null 2>&1; then + elif has_cmd shasum; then printf '%s' "$s" | shasum -a 1 | cut -c1-8 else # POSIX fallback; stable (not cryptographic) @@ -2811,7 +2903,7 @@ run_start() { fi # If user provided Windows path (E:\...), convert to POSIX for existence check - if [[ "$host" =~ ^[A-Za-z]:[\\/].* ]] && command -v cygpath >/dev/null 2>&1; then + if [[ "$host" =~ ^[A-Za-z]:[\\/].* ]] && has_cmd cygpath; then host="$(cygpath -u "$host")" fi @@ -2950,7 +3042,7 @@ cmd_run() { if is_windows_shell; then export MSYS_NO_PATHCONV=1 export MSYS2_ARG_CONV_EXCL='*' - if command -v cygpath >/dev/null 2>&1; then + if has_cmd cygpath; then dir_docker="$(cygpath -w "$dir_posix")" fi fi @@ -3175,7 +3267,7 @@ cmd_stack_diff() { declare -A desired_df=() if [[ -n "$cfg_json" ]]; then - if command -v jq >/dev/null 2>&1; then + if has_cmd jq; then while IFS= read -r line; do local svc="${line%%|*}" local img="${line#*|}" @@ -3203,7 +3295,7 @@ cmd_stack_diff() { # Build result object if ((json)); then - if command -v jq >/dev/null 2>&1; then + if has_cmd jq; then # assemble in bash -> jq local tmp; tmp="$(mktemp)" { @@ -3338,14 +3430,14 @@ cmd_domain_export() { printf '\n] }\n' >>"$tmp" if [[ -n "$out" ]]; then - if ((pretty)) && command -v jq >/dev/null 2>&1; then + if ((pretty)) && has_cmd jq; then cat "$tmp" | jq . >"$out" else cat "$tmp" >"$out" fi ok "[ok] domain export: $out\n" else - if ((pretty)) && command -v jq >/dev/null 2>&1; then + if ((pretty)) && has_cmd jq; then cat "$tmp" | jq . else cat "$tmp" @@ -3363,7 +3455,7 @@ cmd_domain_import() { local dir_ng="$DIR/configuration/nginx" mkdir -p "$dir_ng" 2>/dev/null || true - command -v jq >/dev/null 2>&1 || die "jq required for domain import" + has_cmd jq || die "jq required for domain import" local count=0 while IFS= read -r dom; do @@ -3392,8 +3484,8 @@ cmd_support_trace() { _tools_exec "dig +short $(_shq "$dom") || true; getent hosts $(_shq "$dom") 2>/dev/null || true" else printf "\n%b[DNS]%b\n" "$DIM" "$NC" - (command -v dig >/dev/null 2>&1 && dig +short "$dom") || true - (command -v getent >/dev/null 2>&1 && getent hosts "$dom") || true + (has_cmd dig && dig +short "$dom") || true + (has_cmd getent && getent hosts "$dom") || true fi # 2) TLS certificate From dbf7b3885966d574fbaf08abeb28b4fd3f4d591d Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Fri, 27 Feb 2026 23:05:43 +0600 Subject: [PATCH 16/48] lds split --- lds | 224 ++++++++++++++++++++++++++++++++++-------------------------- 1 file changed, 129 insertions(+), 95 deletions(-) diff --git a/lds b/lds index 5279476..d9dc4e8 100755 --- a/lds +++ b/lds @@ -7,98 +7,138 @@ if [ -z "${BASH_VERSION:-}" ]; then exec /usr/bin/env bash "$0" "$@"; fi set -euo pipefail ############################################################################### -# 1. COMMON / IO HELPERS +# 0. CORE / BOOTSTRAP-SAFE HELPERS (no Docker required) ############################################################################### + +# Color defaults (overridden later by init_colors); keep safe under set -u +BOLD="${BOLD:-}"; DIM="${DIM:-}" +RED="${RED:-}"; GREEN="${GREEN:-}"; CYAN="${CYAN:-}"; YELLOW="${YELLOW:-}" +BLUE="${BLUE:-}"; MAGENTA="${MAGENTA:-}"; NC="${NC:-}" + die() { - printf "%bError:%b %s\n" "$RED" "$NC" "$*" + printf "%bError:%b %s\n" "${RED:-}" "${NC:-}" "$*" >&2 exit 1 } -# ───────────────────────────────────────────────────────────────────────────── -# Tool proxy (enabled only when LDS_PROXY_TOOLS=1) -# ───────────────────────────────────────────────────────────────────────────── -lds_tools_cmd() { - local cmd="${1:-}" - shift || true - [[ -n "$cmd" ]] || { echo "lds_tools_cmd: missing command" >&2; return 2; } - - local -a flags=() - [[ -t 0 ]] && flags+=(-i) - [[ -t 1 ]] && flags+=(-t) - - # if command exists on host - if has_cmd "$cmd"; then - command "$cmd" "$@" - return $? - fi - - # If SERVER_TOOLS is running - if docker inspect -f '{{.State.Running}}' SERVER_TOOLS 2>/dev/null | grep -qx true; then - if docker exec "${flags[@]}" SERVER_TOOLS "$cmd" "$@"; then - return 0 - fi - fi +############################################################################### +# Command presence + tool proxy rules +# - Host-only commands MUST exist on host (e.g. docker) +# - Proxy-safe tools may run from SERVER_TOOLS when LDS_PROXY_TOOLS=1 and container is up +# - Anything inside `docker exec ... sh -lc '...'` must continue to use `command -v` +############################################################################### - echo "Error: '$cmd' not available (SERVER_TOOLS not running and host command missing)" >&2 - return 127 -} +# Cached executable lookup (host binaries only; ignores shell functions/aliases) +declare -A __LDS_HAS_BIN=() -if ((${LDS_PROXY_TOOLS:-0})); then - jq() { lds_tools_cmd jq "$@"; } - yq() { lds_tools_cmd yq "$@"; } - rg() { lds_tools_cmd rg "$@"; } - fd() { lds_tools_cmd fd "$@"; } - shellcheck() { lds_tools_cmd shellcheck "$@"; } - tree() { lds_tools_cmd tree "$@"; } -fi -# ───────────────────────────────────────────────────────────────────────────── -# Command presence (cached) + Safe Tool proxy integration -# - has_cmd: fast cached command lookup -# - need_cmd: hard requirement (host-only) -# - need_tool: requirement that may be satisfied via SERVER_TOOLS when LDS_PROXY_TOOLS=1 -# ───────────────────────────────────────────────────────────────────────────── -declare -A __LDS_HAS_CMD=() +# Back-compat: treat has_cmd as 'host binary exists' (functions/aliases do not count). +has_cmd() { has_bin "$@"; } -# Fast cached host-side command existence check -has_cmd() { +has_bin() { local c="${1:-}" [[ -n "$c" ]] || return 1 - if [[ -n "${__LDS_HAS_CMD[$c]+x}" ]]; then - return "${__LDS_HAS_CMD[$c]}" + if [[ -n "${__LDS_HAS_BIN[$c]+x}" ]]; then + return "${__LDS_HAS_BIN[$c]}" fi - command -v "$c" >/dev/null 2>&1 - __LDS_HAS_CMD[$c]=$? - return "${__LDS_HAS_CMD[$c]}" + type -P -- "$c" >/dev/null 2>&1 + __LDS_HAS_BIN[$c]=$? + return "${__LDS_HAS_BIN[$c]}" } -need_cmd() { +bin_path() { type -P -- "${1:?}"; } + +need_bin() { local c="${1:-}" hint="${2:-}" - has_cmd "$c" && return 0 + has_bin "$c" && return 0 die "Missing required command: $c${hint:+ ($hint)}" } -# Commands that must NEVER be proxied to SERVER_TOOLS (host control plane / privileged) +# Commands that must NEVER be proxied to SERVER_TOOLS (host control plane / privileged / filesystem) _is_host_only_cmd() { case "${1:-}" in - docker|sudo|su|systemctl|service|ip|iptables|nft|sysctl|mount|umount|modprobe|insmod|rmmod|rm|mv|cp|chmod|chown) - return 0 ;; + docker|sudo|su|systemctl|service|ip|iptables|nft|sysctl|mount|umount|modprobe|insmod|rmmod|rm|mv|cp|chmod|chown) + return 0 ;; esac return 1 } -# Check if a tool exists either on host OR (when enabled and safe) inside SERVER_TOOLS -has_tool() { +_server_tools_running() { + has_bin docker || return 1 + "$(bin_path docker)" inspect -f '{{.State.Running}}' SERVER_TOOLS 2>/dev/null | grep -qx true +} + +_server_tools_has() { local c="${1:-}" - has_cmd "$c" && return 0 + [[ -n "$c" ]] || return 1 + _server_tools_running || return 1 + "$(bin_path docker)" exec SERVER_TOOLS sh -lc "command -v \"$c\" >/dev/null 2>&1" >/dev/null 2>&1 +} - ((${LDS_PROXY_TOOLS:-0})) || return 1 - _is_host_only_cmd "$c" && return 1 +# Unified command runner for proxy-safe tools +# Resolution order (your rule-set): +# 1) if cmd exists on host -> run host binary +# 2) if host-only and missing -> error +# 3) if proxy-safe and LDS_PROXY_TOOLS=1 and SERVER_TOOLS up and cmd exists there -> run via docker exec +# 4) otherwise -> error +lds_tools_cmd() { + local cmd="${1:-}" + shift || true + [[ -n "$cmd" ]] || { echo "lds_tools_cmd: missing command" >&2; return 2; } - # proxy needs host docker to interrogate SERVER_TOOLS - has_cmd docker || return 1 - docker inspect -f '{{.State.Running}}' SERVER_TOOLS 2>/dev/null | grep -qx true || return 1 + # 1) host binary wins + if has_bin "$cmd"; then + local p; p="$(bin_path "$cmd")" + "$p" "$@" + return $? + fi - docker exec SERVER_TOOLS sh -lc "command -v "$c" >/dev/null 2>&1" >/dev/null 2>&1 + # 2) host-only commands must be present on host + if _is_host_only_cmd "$cmd"; then + echo "Error: '$cmd' is required on host but was not found" >&2 + return 127 + fi + + # 3) proxy-safe tools from SERVER_TOOLS (opt-in) + if ((${LDS_PROXY_TOOLS:-0})); then + if ! has_bin docker; then + echo "Error: '$cmd' not available on host; tool proxy requires host 'docker'" >&2 + return 127 + fi + if ! _server_tools_running; then + echo "Error: '$cmd' not available on host and SERVER_TOOLS is not running (set LDS_PROXY_TOOLS=1 and start the stack)" >&2 + return 127 + fi + if ! _server_tools_has "$cmd"; then + echo "Error: '$cmd' not found on host nor inside SERVER_TOOLS" >&2 + return 127 + fi + + local -a flags=() + [[ -t 0 ]] && flags+=(-i) + [[ -t 1 ]] && flags+=(-t) + "$(bin_path docker)" exec "${flags[@]}" SERVER_TOOLS "$cmd" "$@" + return $? + fi + + # 4) not found and proxy not enabled + echo "Error: '$cmd' not available on host (set LDS_PROXY_TOOLS=1 to proxy via SERVER_TOOLS)" >&2 + return 127 +} + +# Convenience wrappers for common proxy-safe tools +jq() { lds_tools_cmd jq "$@"; } +yq() { lds_tools_cmd yq "$@"; } +rg() { lds_tools_cmd rg "$@"; } +fd() { lds_tools_cmd fd "$@"; } +tree() { lds_tools_cmd tree "$@"; } +shellcheck() { lds_tools_cmd shellcheck "$@"; } + +# Tool availability checks (host binary OR proxy-available) +has_tool() { + local c="${1:-}" + has_bin "$c" && return 0 + _is_host_only_cmd "$c" && return 1 + ((${LDS_PROXY_TOOLS:-0})) || return 1 + _server_tools_has "$c" } need_tool() { @@ -107,16 +147,17 @@ need_tool() { die "Missing required command: $c${hint:+ ($hint)}" } +############################################################################### +# Misc helpers used across the script +############################################################################### + need() { local group found cmd for group in "$@"; do IFS='|,' read -ra alts <<<"$group" found=0 for cmd in "${alts[@]}"; do - has_cmd "$cmd" && { - found=1 - break - } + has_bin "$cmd" && { found=1; break; } done ((found)) && continue local miss=${alts[*]} @@ -125,8 +166,7 @@ need() { done } - -# Stream search helper: prefers ripgrep (rg) when available, falls back to grep. +# Stream search helper: prefers ripgrep (rg) when available (host or proxied), falls back to grep. # Reads from stdin, prints matching lines with line numbers. text_grep() { local ins=0 @@ -138,20 +178,13 @@ text_grep() { local pat="${1:-}" [[ -n "$pat" ]] || return 0 - if has_cmd rg; then - if ((ins)); then - rg -n -i -- "$pat" - else - rg -n -- "$pat" - fi + if has_tool rg; then + if ((ins)); then rg -n -i -- "$pat"; else rg -n -- "$pat"; fi else - if ((ins)); then - grep -ni -- "$pat" - else - grep -n -- "$pat" - fi + if ((ins)); then grep -ni -- "$pat"; else grep -n -- "$pat"; fi fi } + ensure_files_exist() { local rel abs dir for rel in "$@"; do @@ -160,27 +193,28 @@ ensure_files_exist() { if [[ ! -d $dir ]]; then if mkdir -p "$dir" 2>/dev/null; then - printf "%b- Created directory %s%b\n" "$YELLOW" "$dir" "$NC" + printf "%b- Created directory %s%b\n" "${YELLOW:-}" "$dir" "${NC:-}" else printf "%b- Warning:%b cannot create directory %s (permissions?)\n" \ - "$YELLOW" "$NC" "$dir" + "${YELLOW:-}" "${NC:-}" "$dir" continue fi elif [[ ! -w $dir ]]; then - printf "%b- Warning:%b directory not writable: %s\n" "$YELLOW" "$NC" "$dir" + printf "%b- Warning:%b directory not writable: %s\n" "${YELLOW:-}" "${NC:-}" "$dir" fi if [[ -e $abs ]]; then - [[ -w $abs ]] || printf "%b- Warning:%b file not writable: %s\n" "$YELLOW" "$NC" "$abs" + [[ -w $abs ]] || printf "%b- Warning:%b file not writable: %s\n" "${YELLOW:-}" "${NC:-}" "$abs" else if : >"$abs" 2>/dev/null; then - printf "%b- Created file %s%b\n" "$YELLOW" "$abs" "$NC" + printf "%b- Created file %s%b\n" "${YELLOW:-}" "$abs" "${NC:-}" else - printf "%b- Error:%b cannot create file %s (permissions?)\n" "$RED" "$NC" "$abs" + printf "%b- Error:%b cannot create file %s (permissions?)\n" "${RED:-}" "${NC:-}" "$abs" fi fi done } + ############################################################################### # 0. BOOTSTRAP / PATHS / CONSTANTS ############################################################################### @@ -1386,7 +1420,7 @@ compose_has_build() { local svc="$1" json json="$(compose_cfg_json)" if [[ -n "$json" ]]; then - if has_cmd jq; then + if has_tool jq; then jq -e --arg s "$svc" '.services[$s].build != null' >/dev/null <<<"$json" return $? fi @@ -1405,7 +1439,7 @@ compose_image_for_service() { local svc="$1" json json="$(compose_cfg_json)" if [[ -n "$json" ]]; then - if has_cmd jq; then + if has_tool jq; then jq -r --arg s "$svc" '.services[$s].image // empty' <<<"$json" return 0 fi @@ -3267,7 +3301,7 @@ cmd_stack_diff() { declare -A desired_df=() if [[ -n "$cfg_json" ]]; then - if has_cmd jq; then + if has_tool jq; then while IFS= read -r line; do local svc="${line%%|*}" local img="${line#*|}" @@ -3295,7 +3329,7 @@ cmd_stack_diff() { # Build result object if ((json)); then - if has_cmd jq; then + if has_tool jq; then # assemble in bash -> jq local tmp; tmp="$(mktemp)" { @@ -3430,14 +3464,14 @@ cmd_domain_export() { printf '\n] }\n' >>"$tmp" if [[ -n "$out" ]]; then - if ((pretty)) && has_cmd jq; then + if ((pretty)) && has_tool jq; then cat "$tmp" | jq . >"$out" else cat "$tmp" >"$out" fi ok "[ok] domain export: $out\n" else - if ((pretty)) && has_cmd jq; then + if ((pretty)) && has_tool jq; then cat "$tmp" | jq . else cat "$tmp" @@ -3455,7 +3489,7 @@ cmd_domain_import() { local dir_ng="$DIR/configuration/nginx" mkdir -p "$dir_ng" 2>/dev/null || true - has_cmd jq || die "jq required for domain import" + need_tool jq "required for domain import" local count=0 while IFS= read -r dom; do From 96b6c8c7e157d4a8b054696322aab5c689e7d691 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sat, 28 Feb 2026 22:58:20 +0600 Subject: [PATCH 17/48] lds split --- lds | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lds b/lds index d9dc4e8..917797d 100755 --- a/lds +++ b/lds @@ -2989,6 +2989,7 @@ run_start() { printf "%b[run]%b docker run failed.\n" "$RED" "$NC" >&2 return 1 fi + printf "\n" } run_exec_shell() { @@ -3153,6 +3154,9 @@ cmd_run() { else printf " %bPorts:%b (none published)\n" "$BOLD" "$NC" fi + printf "%b\n[run]%b Example Usage (in Composer)\n" "$CYAN" "$NC" + printf " %bimage:%b %s\n" "$BOLD" "$NC" "$img" + printf " %bpull_policy:%b never\n" "$BOLD" "$NC" printf "\n" } @@ -3230,7 +3234,7 @@ cmd_run() { _run_build_summary "$tag" "$dir_posix" "$name" if docker inspect -f '{{.State.Running}}' "$name" 2>/dev/null | grep -q true; then - printf "%b[run]%b Container already running: %s\n" "$GREEN" "$NC" "$name" + printf "%b[run]%b Container already running: %s\n\n" "$GREEN" "$NC" "$name" else if docker inspect "$name" >/dev/null 2>&1; then docker rm -f "$name" >/dev/null 2>&1 || true From 263ce0d777b00506c50b18300226ac2a4d3ec23f Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 1 Mar 2026 15:17:24 +0600 Subject: [PATCH 18/48] dash update --- docker/compose/http.yaml | 4 ++-- docker/compose/php.yaml | 3 ++- docker/dockerfiles/php.Dockerfile | 5 +---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docker/compose/http.yaml b/docker/compose/http.yaml index aa3e777..b5a7011 100644 --- a/docker/compose/http.yaml +++ b/docker/compose/http.yaml @@ -15,7 +15,7 @@ services: - ../../configuration/ssl:/etc/mkcert:ro - ../../configuration/rootCA:/etc/share/rootCA:ro - "${PROJECT_DIR:-./../../../application}:/app" - - lds_fpm_sock:/run/php-fpm + - lds_fpm_sock:/run/php-fpm:ro extra_hosts: - "host.docker.internal:host-gateway" networks: @@ -39,7 +39,7 @@ services: - ../../logs/apache:/var/log/apache2 - ../../configuration/rootCA:/etc/share/rootCA:ro - "${PROJECT_DIR:-./../../../application}:/app" - - lds_fpm_sock:/run/php-fpm + - lds_fpm_sock:/run/php-fpm:ro depends_on: - nginx networks: diff --git a/docker/compose/php.yaml b/docker/compose/php.yaml index b409262..ea03e24 100644 --- a/docker/compose/php.yaml +++ b/docker/compose/php.yaml @@ -16,8 +16,9 @@ x-php-service: &php-service - "../../configuration/ssh:/home/${USER}/.ssh:ro" - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - ../../configuration/rootCA:/etc/share/rootCA:ro - - lds_fpm_sock:/run/php-fpm + - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro + - ../../logs/php-fpm:/var/log/php-fpm depends_on: - server-tools healthcheck: diff --git a/docker/dockerfiles/php.Dockerfile b/docker/dockerfiles/php.Dockerfile index 2f2acc4..fac8606 100644 --- a/docker/dockerfiles/php.Dockerfile +++ b/docker/dockerfiles/php.Dockerfile @@ -23,10 +23,7 @@ ENV GIT_USER_NAME="" \ LC_ALL=en_US.UTF-8 ADD https://raw.githubusercontent.com/infocyph/Scriptomatic/master/bash/php-cli-setup.sh /usr/local/bin/cli-setup.sh -RUN apk add --no-cache bash && \ - bash /usr/local/bin/cli-setup.sh "${USERNAME}" "${PHP_VERSION}" && \ - rm -f /usr/local/bin/cli-setup.sh && \ - rm -rf /var/cache/apk/* /tmp/* /var/tmp/* +RUN apk add --no-cache bash && bash /usr/local/bin/cli-setup.sh "${USERNAME}" "${PHP_VERSION}" USER ${USERNAME} RUN sudo /usr/local/bin/git-default From a8770e83e7df14dde0ba34a716f50347944ba4cf Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Mon, 2 Mar 2026 22:22:48 +0600 Subject: [PATCH 19/48] lds split --- configuration/rootCA/.gitignore | 2 -- docker/compose/companion.yaml | 8 +++++--- docker/compose/http.yaml | 8 ++++---- docker/compose/main.yaml | 8 +++++++- docker/compose/php.yaml | 2 +- 5 files changed, 17 insertions(+), 11 deletions(-) delete mode 100644 configuration/rootCA/.gitignore diff --git a/configuration/rootCA/.gitignore b/configuration/rootCA/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/configuration/rootCA/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/docker/compose/companion.yaml b/docker/compose/companion.yaml index a7badee..28b8efa 100644 --- a/docker/compose/companion.yaml +++ b/docker/compose/companion.yaml @@ -6,6 +6,7 @@ services: restart: unless-stopped environment: - TZ=${TZ:-} + - USERNAME=${USER} volumes: - ../../configuration/apache:/etc/share/vhosts/apache - ../../configuration/nginx:/etc/share/vhosts/nginx @@ -13,15 +14,16 @@ services: - ../../configuration/sops/config:/etc/share/sops/config - ../../configuration/sops/global:/etc/share/sops/global - ../../configuration/sops/keys:/etc/share/sops/keys - - ../../configuration/ssl:/etc/mkcert + - lds_ssl_keys:/etc/mkcert - "../../configuration/ssh:/home/root/.ssh:ro" - "${PROJECT_DIR:-./../../../application}:/app" - - ../../configuration/rootCA:/etc/share/rootCA + - lds_ssl_roots:/etc/share/rootCA - /var/run/docker.sock:/var/run/docker.sock - "${SOP_REPO:-./../../../sops-repo}:/etc/share/vhosts/sops" - "${HOME}/.gitconfig:/home/root/.gitconfig:ro" - ../../configuration/php/pools:/etc/share/vhosts/fpm - ../../logs:/global/log:ro + - ../../configuration/ssl:/etc/share/certs networks: frontend: ipv4_address: 172.28.0.13 @@ -75,7 +77,7 @@ services: - MP_SMTP_AUTH_ACCEPT_ANY=1 volumes: - ../../data/mailpit:/data - - ../../configuration/ssl:/certs:ro + - lds_ssl_keys:/certs:ro depends_on: - server-tools networks: diff --git a/docker/compose/http.yaml b/docker/compose/http.yaml index b5a7011..1067d11 100644 --- a/docker/compose/http.yaml +++ b/docker/compose/http.yaml @@ -12,8 +12,8 @@ services: volumes: - ../../logs/nginx:/var/log/nginx - ../../configuration/nginx:/etc/nginx/conf.d:ro - - ../../configuration/ssl:/etc/mkcert:ro - - ../../configuration/rootCA:/etc/share/rootCA:ro + - lds_ssl_keys:/etc/mkcert:ro + - lds_ssl_roots:/etc/share/rootCA:ro - "${PROJECT_DIR:-./../../../application}:/app" - lds_fpm_sock:/run/php-fpm:ro extra_hosts: @@ -34,10 +34,10 @@ services: environment: - TZ=${TZ:-} volumes: - - ../../configuration/ssl:/etc/mkcert:ro + - lds_ssl_keys:/etc/mkcert:ro - ../../configuration/apache:/usr/local/apache2/conf/extra:ro - ../../logs/apache:/var/log/apache2 - - ../../configuration/rootCA:/etc/share/rootCA:ro + - lds_ssl_roots:/etc/share/rootCA:ro - "${PROJECT_DIR:-./../../../application}:/app" - lds_fpm_sock:/run/php-fpm:ro depends_on: diff --git a/docker/compose/main.yaml b/docker/compose/main.yaml index 9877dd9..8b05e76 100644 --- a/docker/compose/main.yaml +++ b/docker/compose/main.yaml @@ -49,12 +49,18 @@ volumes: com.infocyph.lds: "1" com.infocyph.stack: "LocalDevStack" com.infocyph.purpose: "Apache Host Configs" - lds_ssl: + lds_ssl_keys: name: SSLKeys labels: com.infocyph.lds: "1" com.infocyph.stack: "LocalDevStack" com.infocyph.purpose: "SSL Keys" + lds_ssl_roots: + name: SSLRootCA + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "SSL Root CA" include: - docker/compose/companion.yaml diff --git a/docker/compose/php.yaml b/docker/compose/php.yaml index ea03e24..5aa17ca 100644 --- a/docker/compose/php.yaml +++ b/docker/compose/php.yaml @@ -15,7 +15,7 @@ x-php-service: &php-service - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini - "../../configuration/ssh:/home/${USER}/.ssh:ro" - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - - ../../configuration/rootCA:/etc/share/rootCA:ro + - lds_ssl_roots:/etc/share/rootCA:ro - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro - ../../logs/php-fpm:/var/log/php-fpm From c31836119c850c599462b5fe4c313dac79ccf596 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 4 Mar 2026 22:18:09 +0600 Subject: [PATCH 20/48] lds split --- docker/compose/php.yaml | 111 ++++++++++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 15 deletions(-) diff --git a/docker/compose/php.yaml b/docker/compose/php.yaml index 5aa17ca..56dbc6d 100644 --- a/docker/compose/php.yaml +++ b/docker/compose/php.yaml @@ -4,21 +4,6 @@ x-php-service: &php-service - TZ=${TZ:-} env_file: - "../../.env" - networks: - frontend: {} - backend: {} - datastore: {} - volumes: - - "${PROJECT_DIR:-./../../../application}:/app" - - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf - - ../conf/openssl.cnf:/etc/ssl/openssl.cnf - - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini - - "../../configuration/ssh:/home/${USER}/.ssh:ro" - - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - - lds_ssl_roots:/etc/share/rootCA:ro - - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm - - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro - - ../../logs/php-fpm:/var/log/php-fpm depends_on: - server-tools healthcheck: @@ -52,6 +37,18 @@ services: PHP_VERSION: 7.3 PHP_EXT_VERSIONED: ${PHP_EXT_73:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_73:-} + volumes: + - "${PROJECT_DIR:-./../../../application}:/app" + - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf + - ../conf/openssl.cnf:/etc/ssl/openssl.cnf + - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini + - "../../configuration/ssh:/home/${USER}/.ssh:ro" + - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" + - lds_ssl_roots:/etc/share/rootCA:ro + - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw + - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro + - ../../logs/php-fpm:/var/log/php-fpm + - ../../configuration/php/pools/php73:/usr/local/etc/php-fpm.domains:ro profiles: [php, php73] php74: @@ -78,6 +75,18 @@ services: PHP_VERSION: 7.4 PHP_EXT_VERSIONED: ${PHP_EXT_74:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_74:-} + volumes: + - "${PROJECT_DIR:-./../../../application}:/app" + - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf + - ../conf/openssl.cnf:/etc/ssl/openssl.cnf + - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini + - "../../configuration/ssh:/home/${USER}/.ssh:ro" + - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" + - lds_ssl_roots:/etc/share/rootCA:ro + - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw + - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro + - ../../logs/php-fpm:/var/log/php-fpm + - ../../configuration/php/pools/php74:/usr/local/etc/php-fpm.domains:ro profiles: [php, php74] php80: @@ -104,6 +113,18 @@ services: PHP_VERSION: 8.0 PHP_EXT_VERSIONED: ${PHP_EXT_80:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_80:-} + volumes: + - "${PROJECT_DIR:-./../../../application}:/app" + - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf + - ../conf/openssl.cnf:/etc/ssl/openssl.cnf + - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini + - "../../configuration/ssh:/home/${USER}/.ssh:ro" + - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" + - lds_ssl_roots:/etc/share/rootCA:ro + - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw + - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro + - ../../logs/php-fpm:/var/log/php-fpm + - ../../configuration/php/pools/php80:/usr/local/etc/php-fpm.domains:ro profiles: [php, php80] php81: @@ -130,6 +151,18 @@ services: PHP_VERSION: 8.1 PHP_EXT_VERSIONED: ${PHP_EXT_81:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_81:-} + volumes: + - "${PROJECT_DIR:-./../../../application}:/app" + - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf + - ../conf/openssl.cnf:/etc/ssl/openssl.cnf + - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini + - "../../configuration/ssh:/home/${USER}/.ssh:ro" + - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" + - lds_ssl_roots:/etc/share/rootCA:ro + - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw + - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro + - ../../logs/php-fpm:/var/log/php-fpm + - ../../configuration/php/pools/php81:/usr/local/etc/php-fpm.domains:ro profiles: [php, php81] php82: @@ -156,6 +189,18 @@ services: PHP_VERSION: 8.2 PHP_EXT_VERSIONED: ${PHP_EXT_82:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_82:-} + volumes: + - "${PROJECT_DIR:-./../../../application}:/app" + - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf + - ../conf/openssl.cnf:/etc/ssl/openssl.cnf + - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini + - "../../configuration/ssh:/home/${USER}/.ssh:ro" + - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" + - lds_ssl_roots:/etc/share/rootCA:ro + - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw + - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro + - ../../logs/php-fpm:/var/log/php-fpm + - ../../configuration/php/pools/php82:/usr/local/etc/php-fpm.domains:ro profiles: [php, php82] php83: @@ -182,6 +227,18 @@ services: PHP_VERSION: 8.3 PHP_EXT_VERSIONED: ${PHP_EXT_83:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_83:-} + volumes: + - "${PROJECT_DIR:-./../../../application}:/app" + - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf + - ../conf/openssl.cnf:/etc/ssl/openssl.cnf + - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini + - "../../configuration/ssh:/home/${USER}/.ssh:ro" + - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" + - lds_ssl_roots:/etc/share/rootCA:ro + - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw + - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro + - ../../logs/php-fpm:/var/log/php-fpm + - ../../configuration/php/pools/php83:/usr/local/etc/php-fpm.domains:ro profiles: [php, php83] php84: @@ -208,6 +265,18 @@ services: PHP_VERSION: 8.4 PHP_EXT_VERSIONED: ${PHP_EXT_84:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_84:-} + volumes: + - "${PROJECT_DIR:-./../../../application}:/app" + - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf + - ../conf/openssl.cnf:/etc/ssl/openssl.cnf + - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini + - "../../configuration/ssh:/home/${USER}/.ssh:ro" + - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" + - lds_ssl_roots:/etc/share/rootCA:ro + - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw + - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro + - ../../logs/php-fpm:/var/log/php-fpm + - ../../configuration/php/pools/php84:/usr/local/etc/php-fpm.domains:ro profiles: [php, php84] php85: @@ -234,4 +303,16 @@ services: PHP_VERSION: 8.5 PHP_EXT_VERSIONED: ${PHP_EXT_85:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_85:-} + volumes: + - "${PROJECT_DIR:-./../../../application}:/app" + - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf + - ../conf/openssl.cnf:/etc/ssl/openssl.cnf + - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini + - "../../configuration/ssh:/home/${USER}/.ssh:ro" + - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" + - lds_ssl_roots:/etc/share/rootCA:ro + - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw + - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro + - ../../logs/php-fpm:/var/log/php-fpm + - ../../configuration/php/pools/php85:/usr/local/etc/php-fpm.domains:ro profiles: [php, php85] From a5baea0b4c1a96af69452cbad1c0073bee66c621 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Mon, 2 Mar 2026 22:22:48 +0600 Subject: [PATCH 21/48] lds split # Conflicts: # docker/compose/php.yaml --- docker/compose/php.yaml | 111 ++++++---------------------------------- 1 file changed, 15 insertions(+), 96 deletions(-) diff --git a/docker/compose/php.yaml b/docker/compose/php.yaml index 56dbc6d..5aa17ca 100644 --- a/docker/compose/php.yaml +++ b/docker/compose/php.yaml @@ -4,6 +4,21 @@ x-php-service: &php-service - TZ=${TZ:-} env_file: - "../../.env" + networks: + frontend: {} + backend: {} + datastore: {} + volumes: + - "${PROJECT_DIR:-./../../../application}:/app" + - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf + - ../conf/openssl.cnf:/etc/ssl/openssl.cnf + - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini + - "../../configuration/ssh:/home/${USER}/.ssh:ro" + - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" + - lds_ssl_roots:/etc/share/rootCA:ro + - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm + - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro + - ../../logs/php-fpm:/var/log/php-fpm depends_on: - server-tools healthcheck: @@ -37,18 +52,6 @@ services: PHP_VERSION: 7.3 PHP_EXT_VERSIONED: ${PHP_EXT_73:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_73:-} - volumes: - - "${PROJECT_DIR:-./../../../application}:/app" - - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf - - ../conf/openssl.cnf:/etc/ssl/openssl.cnf - - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini - - "../../configuration/ssh:/home/${USER}/.ssh:ro" - - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - - lds_ssl_roots:/etc/share/rootCA:ro - - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw - - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro - - ../../logs/php-fpm:/var/log/php-fpm - - ../../configuration/php/pools/php73:/usr/local/etc/php-fpm.domains:ro profiles: [php, php73] php74: @@ -75,18 +78,6 @@ services: PHP_VERSION: 7.4 PHP_EXT_VERSIONED: ${PHP_EXT_74:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_74:-} - volumes: - - "${PROJECT_DIR:-./../../../application}:/app" - - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf - - ../conf/openssl.cnf:/etc/ssl/openssl.cnf - - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini - - "../../configuration/ssh:/home/${USER}/.ssh:ro" - - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - - lds_ssl_roots:/etc/share/rootCA:ro - - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw - - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro - - ../../logs/php-fpm:/var/log/php-fpm - - ../../configuration/php/pools/php74:/usr/local/etc/php-fpm.domains:ro profiles: [php, php74] php80: @@ -113,18 +104,6 @@ services: PHP_VERSION: 8.0 PHP_EXT_VERSIONED: ${PHP_EXT_80:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_80:-} - volumes: - - "${PROJECT_DIR:-./../../../application}:/app" - - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf - - ../conf/openssl.cnf:/etc/ssl/openssl.cnf - - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini - - "../../configuration/ssh:/home/${USER}/.ssh:ro" - - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - - lds_ssl_roots:/etc/share/rootCA:ro - - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw - - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro - - ../../logs/php-fpm:/var/log/php-fpm - - ../../configuration/php/pools/php80:/usr/local/etc/php-fpm.domains:ro profiles: [php, php80] php81: @@ -151,18 +130,6 @@ services: PHP_VERSION: 8.1 PHP_EXT_VERSIONED: ${PHP_EXT_81:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_81:-} - volumes: - - "${PROJECT_DIR:-./../../../application}:/app" - - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf - - ../conf/openssl.cnf:/etc/ssl/openssl.cnf - - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini - - "../../configuration/ssh:/home/${USER}/.ssh:ro" - - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - - lds_ssl_roots:/etc/share/rootCA:ro - - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw - - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro - - ../../logs/php-fpm:/var/log/php-fpm - - ../../configuration/php/pools/php81:/usr/local/etc/php-fpm.domains:ro profiles: [php, php81] php82: @@ -189,18 +156,6 @@ services: PHP_VERSION: 8.2 PHP_EXT_VERSIONED: ${PHP_EXT_82:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_82:-} - volumes: - - "${PROJECT_DIR:-./../../../application}:/app" - - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf - - ../conf/openssl.cnf:/etc/ssl/openssl.cnf - - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini - - "../../configuration/ssh:/home/${USER}/.ssh:ro" - - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - - lds_ssl_roots:/etc/share/rootCA:ro - - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw - - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro - - ../../logs/php-fpm:/var/log/php-fpm - - ../../configuration/php/pools/php82:/usr/local/etc/php-fpm.domains:ro profiles: [php, php82] php83: @@ -227,18 +182,6 @@ services: PHP_VERSION: 8.3 PHP_EXT_VERSIONED: ${PHP_EXT_83:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_83:-} - volumes: - - "${PROJECT_DIR:-./../../../application}:/app" - - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf - - ../conf/openssl.cnf:/etc/ssl/openssl.cnf - - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini - - "../../configuration/ssh:/home/${USER}/.ssh:ro" - - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - - lds_ssl_roots:/etc/share/rootCA:ro - - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw - - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro - - ../../logs/php-fpm:/var/log/php-fpm - - ../../configuration/php/pools/php83:/usr/local/etc/php-fpm.domains:ro profiles: [php, php83] php84: @@ -265,18 +208,6 @@ services: PHP_VERSION: 8.4 PHP_EXT_VERSIONED: ${PHP_EXT_84:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_84:-} - volumes: - - "${PROJECT_DIR:-./../../../application}:/app" - - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf - - ../conf/openssl.cnf:/etc/ssl/openssl.cnf - - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini - - "../../configuration/ssh:/home/${USER}/.ssh:ro" - - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - - lds_ssl_roots:/etc/share/rootCA:ro - - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw - - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro - - ../../logs/php-fpm:/var/log/php-fpm - - ../../configuration/php/pools/php84:/usr/local/etc/php-fpm.domains:ro profiles: [php, php84] php85: @@ -303,16 +234,4 @@ services: PHP_VERSION: 8.5 PHP_EXT_VERSIONED: ${PHP_EXT_85:-} LINUX_PKG_VERSIONED: ${LINUX_PKG_85:-} - volumes: - - "${PROJECT_DIR:-./../../../application}:/app" - - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf - - ../conf/openssl.cnf:/etc/ssl/openssl.cnf - - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini - - "../../configuration/ssh:/home/${USER}/.ssh:ro" - - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - - lds_ssl_roots:/etc/share/rootCA:ro - - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm:rw - - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro - - ../../logs/php-fpm:/var/log/php-fpm - - ../../configuration/php/pools/php85:/usr/local/etc/php-fpm.domains:ro profiles: [php, php85] From f63e9040a2a25ad457e459a9e2b444d4698e6c71 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 5 Mar 2026 20:14:15 +0600 Subject: [PATCH 22/48] Use named volume for PHP FPM pools --- configuration/php/pools/.gitignore | 2 -- docker/compose/companion.yaml | 2 +- docker/compose/php.yaml | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 configuration/php/pools/.gitignore diff --git a/configuration/php/pools/.gitignore b/configuration/php/pools/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/configuration/php/pools/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/docker/compose/companion.yaml b/docker/compose/companion.yaml index 28b8efa..5c790b8 100644 --- a/docker/compose/companion.yaml +++ b/docker/compose/companion.yaml @@ -21,7 +21,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock - "${SOP_REPO:-./../../../sops-repo}:/etc/share/vhosts/sops" - "${HOME}/.gitconfig:/home/root/.gitconfig:ro" - - ../../configuration/php/pools:/etc/share/vhosts/fpm + - lds_fpm_pools:/etc/share/vhosts/fpm - ../../logs:/global/log:ro - ../../configuration/ssl:/etc/share/certs networks: diff --git a/docker/compose/php.yaml b/docker/compose/php.yaml index 5aa17ca..c7cb45d 100644 --- a/docker/compose/php.yaml +++ b/docker/compose/php.yaml @@ -17,7 +17,7 @@ x-php-service: &php-service - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - lds_ssl_roots:/etc/share/rootCA:ro - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm - - ../../configuration/php/pools:/usr/local/etc/php-fpm.domains:ro + - lds_fpm_pools:/usr/local/etc/php-fpm.domains:ro - ../../logs/php-fpm:/var/log/php-fpm depends_on: - server-tools From b3fc0fd32a3ef5e6ad04811857c1f19e0f987bcb Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 5 Mar 2026 21:28:38 +0600 Subject: [PATCH 23/48] Use named volumes for host configs --- configuration/apache/.gitignore | 2 -- configuration/nginx/.gitignore | 2 -- docker/compose/companion.yaml | 4 ++-- docker/compose/http.yaml | 4 ++-- docker/compose/main.yaml | 24 ++++++++++++------------ docker/dockerfiles/node.Dockerfile | 6 ++---- docker/dockerfiles/php.Dockerfile | 2 +- 7 files changed, 19 insertions(+), 25 deletions(-) delete mode 100644 configuration/apache/.gitignore delete mode 100644 configuration/nginx/.gitignore diff --git a/configuration/apache/.gitignore b/configuration/apache/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/configuration/apache/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/configuration/nginx/.gitignore b/configuration/nginx/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/configuration/nginx/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/docker/compose/companion.yaml b/docker/compose/companion.yaml index 5c790b8..ffe3fcb 100644 --- a/docker/compose/companion.yaml +++ b/docker/compose/companion.yaml @@ -8,8 +8,8 @@ services: - TZ=${TZ:-} - USERNAME=${USER} volumes: - - ../../configuration/apache:/etc/share/vhosts/apache - - ../../configuration/nginx:/etc/share/vhosts/nginx + - lds_apache_host:/etc/share/vhosts/apache + - lds_nginx_host:/etc/share/vhosts/nginx - ../../configuration/node:/etc/share/vhosts/node - ../../configuration/sops/config:/etc/share/sops/config - ../../configuration/sops/global:/etc/share/sops/global diff --git a/docker/compose/http.yaml b/docker/compose/http.yaml index 1067d11..bc41e2b 100644 --- a/docker/compose/http.yaml +++ b/docker/compose/http.yaml @@ -11,7 +11,7 @@ services: - "${HTTPS_PORT:-443}:443" volumes: - ../../logs/nginx:/var/log/nginx - - ../../configuration/nginx:/etc/nginx/conf.d:ro + - lds_nginx_host:/etc/nginx/conf.d:ro - lds_ssl_keys:/etc/mkcert:ro - lds_ssl_roots:/etc/share/rootCA:ro - "${PROJECT_DIR:-./../../../application}:/app" @@ -35,7 +35,7 @@ services: - TZ=${TZ:-} volumes: - lds_ssl_keys:/etc/mkcert:ro - - ../../configuration/apache:/usr/local/apache2/conf/extra:ro + - lds_apache_host:/usr/local/apache2/conf/extra:ro - ../../logs/apache:/var/log/apache2 - lds_ssl_roots:/etc/share/rootCA:ro - "${PROJECT_DIR:-./../../../application}:/app" diff --git a/docker/compose/main.yaml b/docker/compose/main.yaml index 8b05e76..007c144 100644 --- a/docker/compose/main.yaml +++ b/docker/compose/main.yaml @@ -37,18 +37,6 @@ volumes: com.infocyph.lds: "1" com.infocyph.stack: "LocalDevStack" com.infocyph.purpose: "PHP-FPM Pools" - lds_nginx_host: - name: NginxHosts - labels: - com.infocyph.lds: "1" - com.infocyph.stack: "LocalDevStack" - com.infocyph.purpose: "Nginx Host Configs" - lds_apache_host: - name: ApacheHosts - labels: - com.infocyph.lds: "1" - com.infocyph.stack: "LocalDevStack" - com.infocyph.purpose: "Apache Host Configs" lds_ssl_keys: name: SSLKeys labels: @@ -61,6 +49,18 @@ volumes: com.infocyph.lds: "1" com.infocyph.stack: "LocalDevStack" com.infocyph.purpose: "SSL Root CA" + lds_nginx_host: + name: NginxHosts + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "Nginx Host Configs" + lds_apache_host: + name: ApacheHosts + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "Apache Host Configs" include: - docker/compose/companion.yaml diff --git a/docker/dockerfiles/node.Dockerfile b/docker/dockerfiles/node.Dockerfile index 8f0321e..197d5c6 100644 --- a/docker/dockerfiles/node.Dockerfile +++ b/docker/dockerfiles/node.Dockerfile @@ -1,7 +1,7 @@ ARG NODE_VERSION=current FROM node:${NODE_VERSION}-alpine -LABEL org.opencontainers.image.source="https://github.com/infocyph/LocalDock" +LABEL org.opencontainers.image.source="https://github.com/infocyph/LocalDevStack" LABEL org.opencontainers.image.description="NodeJS Alpine" LABEL org.opencontainers.image.licenses="MIT" LABEL org.opencontainers.image.authors="infocyph,abmmhasan" @@ -23,9 +23,7 @@ ENV PATH="/usr/local/bin:/usr/bin:/bin:/usr/games:$PATH" \ ADD https://raw.githubusercontent.com/infocyph/Scriptomatic/master/bash/node-cli-setup.sh /usr/local/bin/cli-setup.sh RUN apk add --no-cache bash && \ NODE_VERSION="$(node -v | sed 's/^v//')" && \ - bash /usr/local/bin/cli-setup.sh "${USERNAME}" "${NODE_VERSION}" && \ - rm -f /usr/local/bin/cli-setup.sh && \ - rm -rf /var/cache/apk/* /tmp/* /var/tmp/* + bash /usr/local/bin/cli-setup.sh "${USERNAME}" "${NODE_VERSION}" USER ${USERNAME} RUN sudo /usr/local/bin/git-default diff --git a/docker/dockerfiles/php.Dockerfile b/docker/dockerfiles/php.Dockerfile index fac8606..42ffc07 100644 --- a/docker/dockerfiles/php.Dockerfile +++ b/docker/dockerfiles/php.Dockerfile @@ -1,7 +1,7 @@ ARG PHP_VERSION=8.4 FROM php:${PHP_VERSION}-fpm-alpine -LABEL org.opencontainers.image.source="https://github.com/infocyph/LocalDock" +LABEL org.opencontainers.image.source="https://github.com/infocyph/LocalDevStack" LABEL org.opencontainers.image.description="PHP FPM Alpine" LABEL org.opencontainers.image.licenses="MIT" LABEL org.opencontainers.image.authors="infocyph,abmmhasan" From 32f5a7d1ff3ab0f7233857843a5bcb7827bd3618 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 5 Mar 2026 22:17:06 +0600 Subject: [PATCH 24/48] lds split --- lds | 159 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 2 deletions(-) diff --git a/lds b/lds index 917797d..879e03c 100755 --- a/lds +++ b/lds @@ -16,7 +16,24 @@ RED="${RED:-}"; GREEN="${GREEN:-}"; CYAN="${CYAN:-}"; YELLOW="${YELLOW:-}" BLUE="${BLUE:-}"; MAGENTA="${MAGENTA:-}"; NC="${NC:-}" die() { - printf "%bError:%b %s\n" "${RED:-}" "${NC:-}" "$*" >&2 + printf "%bError:%b %s +" "${RED:-}" "${NC:-}" "$*" >&2 + + printf " +%bDisk:%b +" "${CYAN:-}" "${NC:-}" >&2 + _status_show_disk 2>/dev/null || true + + printf " +%bVolumes:%b +" "${CYAN:-}" "${NC:-}" >&2 + _status_show_volumes 2>/dev/null || true + + printf " +%bChecks:%b +" "${CYAN:-}" "${NC:-}" >&2 + _status_checks 2>/dev/null || true + exit 1 } @@ -1589,10 +1606,145 @@ _status_checks() { } _status_show_disk() { docker system df - printf "\n%bProject data:%b\n" "$CYAN" "$NC" + printf " +%bProject data:%b +" "$CYAN" "$NC" du -sh "$DIR/data" 2>/dev/null || true } +# Internal: human readable bytes (B/K/M/G/T). Accepts integer bytes. +_human_bytes() { + local b="${1:-0}" + awk -v b="$b" 'BEGIN{ + split("B KB MB GB TB PB EB",u," "); + i=1; + while (b>=1024 && i/dev/null | awk '{print $1}' 2>/dev/null)"; then + [[ -n "$out" ]] && { printf '%s' "$out"; return 0; } + fi + + # Fallback: du -sk * 1024 + out="$(du -sk "$p" 2>/dev/null | awk '{print $1}' 2>/dev/null || true)" + [[ -n "$out" ]] || out="0" + awk -v k="$out" 'BEGIN{ printf "%.0f", k*1024 }' +} + +# Internal: volumes list + sizes for this compose project +_status_show_volumes() { + local project; project="$(lds_project)" + + # 1) volumes by compose label (if any) + local -a vols=() + mapfile -t vols < <( + docker volume ls -q --filter "label=com.docker.compose.project=$project" 2>/dev/null | sed '/^[[:space:]]*$/d' + ) + + # 2) volumes mounted by this project's containers (covers external/unlabeled) + local -a cids=() + mapfile -t cids < <(docker_compose ps -q 2>/dev/null | sed '/^[[:space:]]*$/d') + + if (( ${#cids[@]} > 0 )); then + local v + while IFS= read -r v; do + [[ -n "$v" ]] && vols+=("$v") + done < <( + docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\n"}}{{end}}{{end}}' "${cids[@]}" 2>/dev/null \ + | sed '/^[[:space:]]*$/d' + ) + fi + + # uniq + if (( ${#vols[@]} > 0 )); then + mapfile -t vols < <(printf "%s\n" "${vols[@]}" | awk '!seen[$0]++') + fi + + if (( ${#vols[@]} == 0 )); then + printf "(none)\n" + return 0 + fi + + # size map from `docker system df -v` + declare -A __VOL_SZ=() + local df + df="$(docker system df -v 2>/dev/null || true)" + if [[ -n "$df" ]]; then + # Extract the "Local Volumes space usage:" table: VOLUME NAME | LINKS | SIZE + while read -r name links size rest; do + [[ -n "$name" && "$name" != "VOLUME" ]] || continue + [[ -n "$size" ]] || continue + __VOL_SZ["$name"]="$size" + done < <( + printf "%s\n" "$df" | + awk ' + /^Local Volumes space usage:/ {inside=1; next} + inside && /^Build cache usage:/ {exit} + inside && /^[[:space:]]*$/ {next} + inside {print} + ' | awk 'NR==1{next} {print $1, $2, $3}' + ) + fi + + # inspect metadata + local -a rows=() + mapfile -t rows < <( + docker volume inspect -f '{{.Name}} {{.Driver}}' "${vols[@]}" 2>/dev/null | sed '/^[[:space:]]*$/d' + ) + + if (( ${#rows[@]} == 0 )); then + printf "(none)\n" + return 0 + fi + + local w_name=6 w_drv=6 + local line name drv + for line in "${rows[@]}"; do + IFS=$'\t' read -r name drv <<<"$line" + ((${#name} > w_name)) && w_name=${#name} + ((${#drv} > w_drv)) && w_drv=${#drv} + done + (( w_name > 40 )) && w_name=40 + (( w_drv > 12 )) && w_drv=12 + + printf " %b%-*s%b %b%-*s%b %b%s%b\n" \ + "$BOLD" "$w_name" "NAME" "$NC" \ + "$BOLD" "$w_drv" "DRIVER" "$NC" \ + "$BOLD" "SIZE" "$NC" + + local total_note=0 + for line in "${rows[@]}"; do + IFS=$'\t' read -r name drv <<<"$line" + + local n_disp="$name" d_disp="$drv" + if ((${#n_disp} > w_name)); then n_disp="${n_disp:0:w_name-1}…"; fi + if ((${#d_disp} > w_drv)); then d_disp="${d_disp:0:w_drv-1}…"; fi + + local sz="${__VOL_SZ[$name]:--}" + [[ "$sz" != "-" ]] && total_note=1 + + printf " %-*s %-*s %s\n" \ + "$w_name" "$n_disp" \ + "$w_drv" "${d_disp:-'-'}" \ + "$sz" + done + + if (( total_note == 0 )); then + printf "\n %bNote:%b volume sizes unavailable (docker system df -v did not provide volume table)\n" \ + "$YELLOW" "$NC" + fi +} + _status_urls() { local f d shopt -s nullglob @@ -1858,6 +2010,9 @@ cmd_status() { printf "\n%bDisk:%b\n" "$CYAN" "$NC" _status_show_disk || true + printf "\n%bVolumes:%b\n" "$CYAN" "$NC" + _status_show_volumes || true + printf "\n%bChecks:%b\n" "$CYAN" "$NC" _status_checks || true } From 3e5eddef1a2e4e8e21a266e20d5dccd25185e06d Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 5 Mar 2026 22:24:59 +0600 Subject: [PATCH 25/48] lds split --- lds | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/lds b/lds index 879e03c..adb9855 100755 --- a/lds +++ b/lds @@ -1577,15 +1577,8 @@ _status_checks() { [[ -d "$DIR/configuration" ]] && proj_ok+=(config_dir) || proj_bad+=(config_dir) [[ -d "$DIR/data" ]] && proj_ok+=(data_dir) || proj_bad+=(data_dir) [[ -d "$DIR/logs" ]] && proj_ok+=(logs_dir) || proj_bad+=(logs_dir) - [[ -f "$DIR/configuration/rootCA/rootCA.pem" ]] && proj_ok+=(rootCA) || proj_bad+=(rootCA) - - if [[ -f "$DIR/lds" ]]; then - if grep -U $"\r" "$DIR/lds" >/dev/null 2>&1; then - proj_bad+=(crlf) - else - proj_ok+=(lf) - fi - fi + [[ -f "$DIR/configuration/ssl/rootCA.pem" ]] && proj_ok+=(rootCA) || proj_bad+=(rootCA) + [[ -f "$DIR/configuration/ssl/mTLS-user.p12" ]] && proj_ok+=(mTLS) || proj_bad+=(mTLS) # Print exactly two lines (tab separated) printf "%bSystem%b\t" "$CYAN" "$NC" From 8617d6b301cbfeba52466c4ecfbf61084e49a776 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Fri, 6 Mar 2026 20:35:26 +0600 Subject: [PATCH 26/48] lds split --- data/.gitignore | 2 - docker/compose/companion.yaml | 14 +- docker/compose/db-client.yaml | 8 +- docker/compose/db.yaml | 12 +- docker/compose/http.yaml | 10 +- docker/compose/main.yaml | 66 ++++++ docker/compose/php.yaml | 6 +- lds | 418 ++++++++++++++++++++++++++++++++-- 8 files changed, 488 insertions(+), 48 deletions(-) delete mode 100644 data/.gitignore diff --git a/data/.gitignore b/data/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/data/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/docker/compose/companion.yaml b/docker/compose/companion.yaml index ffe3fcb..d11ee3c 100644 --- a/docker/compose/companion.yaml +++ b/docker/compose/companion.yaml @@ -8,22 +8,22 @@ services: - TZ=${TZ:-} - USERNAME=${USER} volumes: + - "${PROJECT_DIR:-./../../../application}:/app" + - lds_ssl_roots:/etc/share/rootCA - lds_apache_host:/etc/share/vhosts/apache - lds_nginx_host:/etc/share/vhosts/nginx + - lds_ssl_keys:/etc/mkcert + - lds_fpm_pools:/etc/share/vhosts/fpm - ../../configuration/node:/etc/share/vhosts/node - ../../configuration/sops/config:/etc/share/sops/config - ../../configuration/sops/global:/etc/share/sops/global - ../../configuration/sops/keys:/etc/share/sops/keys - - lds_ssl_keys:/etc/mkcert - "../../configuration/ssh:/home/root/.ssh:ro" - - "${PROJECT_DIR:-./../../../application}:/app" - - lds_ssl_roots:/etc/share/rootCA - - /var/run/docker.sock:/var/run/docker.sock - "${SOP_REPO:-./../../../sops-repo}:/etc/share/vhosts/sops" - "${HOME}/.gitconfig:/home/root/.gitconfig:ro" - - lds_fpm_pools:/etc/share/vhosts/fpm - - ../../logs:/global/log:ro - ../../configuration/ssl:/etc/share/certs + - ../../logs:/global/log:ro + - /var/run/docker.sock:/var/run/docker.sock networks: frontend: ipv4_address: 172.28.0.13 @@ -76,8 +76,8 @@ services: - MP_SMTP_REQUIRE_STARTTLS=true - MP_SMTP_AUTH_ACCEPT_ANY=1 volumes: - - ../../data/mailpit:/data - lds_ssl_keys:/certs:ro + - lds_mail:/data depends_on: - server-tools networks: diff --git a/docker/compose/db-client.yaml b/docker/compose/db-client.yaml index 1646d2e..a7bebfd 100644 --- a/docker/compose/db-client.yaml +++ b/docker/compose/db-client.yaml @@ -16,7 +16,7 @@ services: environment: - RI_REDIS_HOST=redis volumes: - - ../../data/redis-insight:/data + - lds_ri:/data - ../../logs/redis-insight:/var/log/redis-insight networks: datastore: @@ -33,7 +33,7 @@ services: environment: - TZ=${TZ:-} volumes: - - ../../data/cloudbeaver:/opt/cloudbeaver/workspace + - lds_cb:/opt/cloudbeaver/workspace - ../../logs/cloudbeaver:/opt/cloudbeaver/logs networks: datastore: @@ -75,7 +75,7 @@ services: - TZ=${TZ:-} - "ELASTICSEARCH_HOSTS=http://elasticsearch:9200" volumes: - - ../../data/kibana:/usr/share/kibana/data + - lds_kb:/usr/share/kibana/data - ../../logs/kibana:/usr/share/kibana/logs networks: datastore: @@ -92,9 +92,9 @@ services: depends_on: - elasticsearch volumes: + - lds_fbeat:/usr/share/filebeat/data - ../conf/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro - ../../logs:/global/log:ro - - ../../data/filebeat:/usr/share/filebeat/data command: ["--strict.perms=false"] networks: datastore: diff --git a/docker/compose/db.yaml b/docker/compose/db.yaml index 51b004d..9af78c1 100644 --- a/docker/compose/db.yaml +++ b/docker/compose/db.yaml @@ -11,7 +11,7 @@ services: image: redis/redis-stack-server:${REDIS_VERSION:-latest} profiles: [redis] volumes: - - ../../data/redis:/data + - lds_redis:/data - ../../logs/redis:/var/log/redis environment: - TZ=${TZ:-} @@ -37,7 +37,7 @@ services: - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres} - POSTGRES_DB=${POSTGRES_DATABASE:-postgres} volumes: - - ../../data/postgresql:/var/lib/postgresql + - lds_pg:/var/lib/postgresql - ../../logs/postgresql:/var/log/postgresql - ../conf/pg_hba.conf:/etc/postgresql/pg_hba.conf # - ../conf/postgresql.conf:/etc/postgresql/postgresql.conf @@ -73,7 +73,7 @@ services: - MYSQL_PASSWORD=${MYSQL_PASSWORD:-12345} - MYSQL_DATABASE=${MYSQL_DATABASE:-localdb} volumes: - - ../../data/mysql:/var/lib/mysql + - lds_my:/var/lib/mysql - ../../logs/mysql:/var/log/mysql healthcheck: test: ["CMD-SHELL", "mysqladmin ping -h127.0.0.1 -u root -p${MYSQL_ROOT_PASSWORD:-12345}"] @@ -95,7 +95,7 @@ services: - MONGO_INITDB_ROOT_USERNAME=${MONGODB_ROOT_USERNAME:-root} - MONGO_INITDB_ROOT_PASSWORD=${MONGODB_ROOT_PASSWORD:-12345} volumes: - - ../../data/mongo:/data/db + - lds_mongo:/data/db - ../../logs/mongodb:/var/log/mongodb healthcheck: test: @@ -123,7 +123,7 @@ services: - MARIADB_PASSWORD=${MARIADB_PASSWORD:-12345} - MARIADB_DATABASE=${MARIADB_DATABASE:-localdb} volumes: - - ../../data/mariadb:/var/lib/mysql + - lds_maria:/var/lib/mysql - ../../logs/mariadb:/var/log/mysql healthcheck: test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -p${MARIADB_ROOT_PASSWORD:-12345}"] @@ -147,7 +147,7 @@ services: - "cluster.name=single_node_cluster" - "node.name=elasticsearch-node-single" volumes: - - ../../data/elasticsearch:/usr/share/elasticsearch/data + - lds_es:/usr/share/elasticsearch/data - ../../logs/elasticsearch:/usr/share/elasticsearch/logs healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"] diff --git a/docker/compose/http.yaml b/docker/compose/http.yaml index bc41e2b..de16a44 100644 --- a/docker/compose/http.yaml +++ b/docker/compose/http.yaml @@ -10,12 +10,12 @@ services: - "${HTTP_PORT:-80}:80" - "${HTTPS_PORT:-443}:443" volumes: - - ../../logs/nginx:/var/log/nginx + - "${PROJECT_DIR:-./../../../application}:/app" - lds_nginx_host:/etc/nginx/conf.d:ro - lds_ssl_keys:/etc/mkcert:ro - lds_ssl_roots:/etc/share/rootCA:ro - - "${PROJECT_DIR:-./../../../application}:/app" - lds_fpm_sock:/run/php-fpm:ro + - ../../logs/nginx:/var/log/nginx extra_hosts: - "host.docker.internal:host-gateway" networks: @@ -34,12 +34,12 @@ services: environment: - TZ=${TZ:-} volumes: + - "${PROJECT_DIR:-./../../../application}:/app" - lds_ssl_keys:/etc/mkcert:ro - - lds_apache_host:/usr/local/apache2/conf/extra:ro - - ../../logs/apache:/var/log/apache2 + - lds_apache_host:/usr/local/apache2/conf/vhosts:ro - lds_ssl_roots:/etc/share/rootCA:ro - - "${PROJECT_DIR:-./../../../application}:/app" - lds_fpm_sock:/run/php-fpm:ro + - ../../logs/apache:/var/log/apache2 depends_on: - nginx networks: diff --git a/docker/compose/main.yaml b/docker/compose/main.yaml index 007c144..0151d53 100644 --- a/docker/compose/main.yaml +++ b/docker/compose/main.yaml @@ -61,6 +61,72 @@ volumes: com.infocyph.lds: "1" com.infocyph.stack: "LocalDevStack" com.infocyph.purpose: "Apache Host Configs" + lds_mail: + name: EmailStore + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "Email Store" + lds_redis: + name: RedisStore + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "Redis Data" + lds_ri: + name: RedisInsightStore + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "Redis Insight Data" + lds_pg: + name: PostgresStore + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "PostgreSQL Data" + lds_my: + name: MySQLStore + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "MySQL Data" + lds_maria: + name: MariaDBStore + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "MariaDB Data" + lds_mongo: + name: MongoDBStore + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "MongoDB Data" + lds_es: + name: ElasticSearchStore + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "ElasticSearch Data" + lds_kb: + name: KibanaStore + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "Kibana Data" + lds_cb: + name: CloudBeaverStore + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "CloudBeaver Data" + lds_fbeat: + name: FilebeatStore + labels: + com.infocyph.lds: "1" + com.infocyph.stack: "LocalDevStack" + com.infocyph.purpose: "Filebeat Data" include: - docker/compose/companion.yaml diff --git a/docker/compose/php.yaml b/docker/compose/php.yaml index c7cb45d..9461cb5 100644 --- a/docker/compose/php.yaml +++ b/docker/compose/php.yaml @@ -10,14 +10,14 @@ x-php-service: &php-service datastore: {} volumes: - "${PROJECT_DIR:-./../../../application}:/app" + - lds_ssl_roots:/etc/share/rootCA:ro + - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm + - lds_fpm_pools:/usr/local/etc/php-fpm.domains:ro - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf - ../conf/openssl.cnf:/etc/ssl/openssl.cnf - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini - "../../configuration/ssh:/home/${USER}/.ssh:ro" - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - - lds_ssl_roots:/etc/share/rootCA:ro - - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm - - lds_fpm_pools:/usr/local/etc/php-fpm.domains:ro - ../../logs/php-fpm:/var/log/php-fpm depends_on: - server-tools diff --git a/lds b/lds index adb9855..b62d615 100755 --- a/lds +++ b/lds @@ -18,25 +18,9 @@ BLUE="${BLUE:-}"; MAGENTA="${MAGENTA:-}"; NC="${NC:-}" die() { printf "%bError:%b %s " "${RED:-}" "${NC:-}" "$*" >&2 - - printf " -%bDisk:%b -" "${CYAN:-}" "${NC:-}" >&2 - _status_show_disk 2>/dev/null || true - - printf " -%bVolumes:%b -" "${CYAN:-}" "${NC:-}" >&2 - _status_show_volumes 2>/dev/null || true - - printf " -%bChecks:%b -" "${CYAN:-}" "${NC:-}" >&2 - _status_checks 2>/dev/null || true - exit 1 } - +LDS_STATUS_PROBE=1 ############################################################################### # Command presence + tool proxy rules # - Host-only commands MUST exist on host (e.g. docker) @@ -1544,6 +1528,360 @@ _status_show_stats() { fi } + +# Internal: list container IDs for current compose project +_status_project_cids() { + docker_compose ps -q 2>/dev/null | sed '/^[[:space:]]*$/d' || true +} + +# Internal: list container names (compose ps) optionally including stopped +_status_project_names() { + docker_compose ps -a --format '{{.Name}}' 2>/dev/null | sed '/^[[:space:]]*$/d' || true +} + +# 1) Problems: unhealthy/restarting/exited + restart count + last exit +_status_show_problems() { + local -a cids=() + mapfile -t cids < <(_status_project_cids) + if (( ${#cids[@]} == 0 )); then + printf "(none)\n" + return 0 + fi + + local -a rows=() + mapfile -t rows < <( + docker inspect -f '{{.Name}}\t{{.State.Status}}\t{{if .State.Health}}{{.State.Health.Status}}{{end}}\t{{.RestartCount}}\t{{.State.ExitCode}}\t{{.State.FinishedAt}}' \ + "${cids[@]}" 2>/dev/null | sed 's#^/##' + ) + + local printed=0 + local w=4 + local line name st health rest exitc fin + for line in "${rows[@]}"; do + IFS=$'\t' read -r name st health rest exitc fin <<<"$line" + ((${#name} > w)) && w=${#name} + done + (( w > 34 )) && w=34 + + printf " %b%-*s%b %b%-10s%b %b%-10s%b %b%-7s%b %b%s%b\n" \ + "$BOLD" "$w" "NAME" "$NC" \ + "$BOLD" "STATE" "$NC" \ + "$BOLD" "HEALTH" "$NC" \ + "$BOLD" "RESTART" "$NC" \ + "$BOLD" "LAST_EXIT" "$NC" + + for line in "${rows[@]}"; do + IFS=$'\t' read -r name st health rest exitc fin <<<"$line" + local bad=0 + [[ "$st" != "running" ]] && bad=1 + [[ "$health" == "unhealthy" ]] && bad=1 + [[ "$st" == "restarting" ]] && bad=1 + + (( bad )) || continue + + local n="$name"; ((${#n} > w)) && n="${n:0:w-1}…" + local stc="$(_state_color "$st")" + local hc="$(_health_color "$health")" + local he="${health:-"-"}"; [[ -z "$he" ]] && he="-" + local last="-" + if [[ "$st" != "running" || "$exitc" != "0" ]]; then + last="code=$exitc" + fi + + printf " %-*s %b%-10s%b %b%-10s%b %-7s %s\n" \ + "$w" "$n" \ + "$stc" "${st:-'-'}" "$NC" \ + "$hc" "$he" "$NC" \ + "${rest:-0}" \ + "$last" + printed=1 + done + + (( printed )) || printf " (none)\n" +} + +# 2) Top consumers: top-5 by MEM and CPU (from docker stats) +_status_show_top_consumers() { + local -a names=() + mapfile -t names < <(_status_project_names) + if (( ${#names[@]} == 0 )); then + printf "(none)\n" + return 0 + fi + + local stats + stats="$(docker stats --no-stream --format '{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}' "${names[@]}" 2>/dev/null || true)" + [[ -n "$stats" ]] || { printf "(no stats)\n"; return 0; } + + _to_bytes() { + local v="$1" + v="${v//,/}" + local num unit + num="$(printf '%s' "$v" | awk '{gsub(/[^0-9.]/,""); print}')" + unit="$(printf '%s' "$v" | awk '{gsub(/[0-9.]/,""); print}')" + awk -v n="$num" -v u="$unit" 'BEGIN{ + mul=1; + if(u=="B"||u=="") mul=1; + else if(u=="kB"||u=="KB") mul=1000; + else if(u=="MB") mul=1000^2; + else if(u=="GB") mul=1000^3; + else if(u=="TB") mul=1000^4; + else if(u=="KiB") mul=1024; + else if(u=="MiB") mul=1024^2; + else if(u=="GiB") mul=1024^3; + else if(u=="TiB") mul=1024^4; + printf "%.0f", (n+0)*mul; + }' + } + + printf " %bTop by MEM:%b\n" "$BOLD" "$NC" + printf "%s\n" "$stats" | awk -F'\t' ' + {name=$1; cpu=$2; mem=$3; split(mem,a,"/"); gsub(/^[[:space:]]+|[[:space:]]+$/,"",a[1]); print name "\t" cpu "\t" a[1]} + ' | while IFS=$'\t' read -r n cpu mem; do + printf "%s\t%s\t%s\t%s\n" "$n" "$cpu" "$mem" "$(_to_bytes "$mem")" + done | sort -t$'\t' -k4,4nr | head -n 5 | awk -F'\t' '{printf " %-16s CPU=%-7s MEM=%s\n",$1,$2,$3}' + + printf " %bTop by CPU:%b\n" "$BOLD" "$NC" + printf "%s\n" "$stats" | awk -F'\t' ' + {name=$1; cpu=$2; gsub(/%/,"",cpu); print name "\t" cpu} + ' | sort -t$'\t' -k2,2nr | head -n 5 | awk -F'\t' '{printf " %-16s CPU=%s%%\n",$1,$2}' +} + +# 3) Volumes: mounted volumes + size + links + mounted-by +_status_show_volumes() { + local -a cids=() + mapfile -t cids < <(_status_project_cids) + if (( ${#cids[@]} == 0 )); then + printf "(none)\n" + return 0 + fi + + declare -A v2c=() + local cid cname v + for cid in "${cids[@]}"; do + cname="$(docker inspect -f '{{.Name}}' "$cid" 2>/dev/null | sed 's#^/##')" + while IFS= read -r v; do + [[ -n "$v" ]] || continue + if [[ -n "${v2c[$v]+x}" ]]; then + v2c["$v"]+=",${cname}" + else + v2c["$v"]="${cname}" + fi + done < <(docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\\n"}}{{end}}{{end}}' "$cid" 2>/dev/null) + done + + local -a vols=() + mapfile -t vols < <(printf "%s\n" "${!v2c[@]}" | sort) + + if (( ${#vols[@]} == 0 )); then + printf "(none)\n" + return 0 + fi + + declare -A vsize=() vlinks=() + local df + df="$(docker system df -v 2>/dev/null || true)" + if [[ -n "$df" ]]; then + while read -r name links size _; do + [[ -n "$name" && "$name" != "VOLUME" && "$name" != "VOLUME_NAME" ]] || continue + [[ -n "$size" && "$size" != "SIZE" ]] || continue + vlinks["$name"]="$links" + vsize["$name"]="$size" + done < <( + printf "%s\n" "$df" | + awk ' + /^Local Volumes space usage:/ {inside=1; next} + inside && /^[A-Za-z].*usage:$/ {exit} + inside && /^[[:space:]]*$/ {next} + inside {print} + ' | awk 'NR==1{next} {print $1, $2, $3, $4}' + ) + fi + + local -a rows=() + mapfile -t rows < <(docker volume inspect -f '{{.Name}}\t{{.Driver}}' "${vols[@]}" 2>/dev/null) + + local w_name=6 w_drv=6 + local line name drv + for line in "${rows[@]}"; do + IFS=$'\t' read -r name drv <<<"$line" + ((${#name} > w_name)) && w_name=${#name} + ((${#drv} > w_drv)) && w_drv=${#drv} + done + (( w_name > 36 )) && w_name=36 + (( w_drv > 12 )) && w_drv=12 + + printf " %b%-*s%b %b%-*s%b %b%-8s%b %b%-6s%b %b%s%b\n" \ + "$BOLD" "$w_name" "VOLUME" "$NC" \ + "$BOLD" "$w_drv" "DRIVER" "$NC" \ + "$BOLD" "SIZE" "$NC" \ + "$BOLD" "LINKS" "$NC" \ + "$BOLD" "MOUNTED_BY" "$NC" + + local any_size=0 + for line in "${rows[@]}"; do + IFS=$'\t' read -r name drv <<<"$line" + + local sz="${vsize[$name]:--}" + local lk="${vlinks[$name]:--}" + [[ "$sz" != "-" ]] && any_size=1 + + local mb="${v2c[$name]:-}" + local shown="$mb" extra="" + local cnt + cnt="$(printf '%s' "$mb" | awk -F',' '{print NF}')" + if (( cnt > 3 )); then + shown="$(printf '%s' "$mb" | awk -F',' '{print $1","$2","$3}')" + extra=",…(+${cnt}-3)" + extra=",…(+$(($cnt-3)))" + fi + + local n="$name"; ((${#n} > w_name)) && n="${n:0:w_name-1}…" + local d="$drv"; ((${#d} > w_drv)) && d="${d:0:w_drv-1}…" + + printf " %-*s %-*s %-8s %-6s %s%s\n" \ + "$w_name" "$n" "$w_drv" "${d:-'-'}" "$sz" "$lk" "$shown" "$extra" + done + + (( any_size )) || printf "\n %bNote:%b volume sizes unavailable (docker system df -v did not provide volume table)\n" "$YELLOW" "$NC" +} + +# 4) Networks + IPs +_status_show_networks() { + local project; project="$(lds_project)" + + printf " %bNetworks:%b\n" "$BOLD" "$NC" + docker network ls --filter "label=com.docker.compose.project=$project" --format ' {{.Name}}' 2>/dev/null || printf " (none)\n" + + local -a cids=() + mapfile -t cids < <(_status_project_cids) + if (( ${#cids[@]} == 0 )); then + return 0 + fi + + printf " %bContainer IPs:%b\n" "$BOLD" "$NC" + local out + out="$(docker inspect -f '{{.Name}}\t{{range $k,$v := .NetworkSettings.Networks}}{{$k}}\t{{$v.IPAddress}}{{"\\n"}}{{end}}' "${cids[@]}" 2>/dev/null | sed 's#^/##' || true)" + if [[ -z "$out" ]]; then + printf " (none)\n" + return 0 + fi + printf "%s\n" "$out" | awk -F'\t' 'NF>=3{printf " %-18s %-22s %s\n",$1,$2,$3}' +} + +# 5) Optional endpoint probes (gated) +_status_show_probes() { + ((${LDS_STATUS_PROBE:-0})) || { printf "(disabled; set LDS_STATUS_PROBE=1)\n"; return 0; } + if ! has_tool curl; then + printf "(curl missing)\n" + return 0 + fi + + local -a urls=() + mapfile -t urls < <(_status_urls 2>/dev/null || true) + + if (( ${#urls[@]} == 0 )); then + printf "(no urls)\n" + return 0 + fi + + local u host code t + for u in "${urls[@]}"; do + host="${u#https://}" + code="$(curl -ksS --max-time 2 --resolve "${host}:443:127.0.0.1" -o /dev/null -w '%{http_code}' "$u" 2>/dev/null || echo 000)" + t="$(curl -ksS --max-time 2 --resolve "${host}:443:127.0.0.1" -o /dev/null -w '%{time_total}' "$u" 2>/dev/null || echo 0)" + printf " %-32s %s %ss\n" "$host" "$code" "$t" + done +} + +# 6) Recent errors for problematic containers (tail logs) +_status_show_recent_errors() { + local -a cids=() + mapfile -t cids < <(_status_project_cids) + (( ${#cids[@]} )) || { printf "(none)\n"; return 0; } + + local -a bad=() + local cid st health + for cid in "${cids[@]}"; do + st="$(docker inspect -f '{{.State.Status}}' "$cid" 2>/dev/null || true)" + health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$cid" 2>/dev/null || true)" + if [[ "$st" != "running" || "$health" == "unhealthy" || "$st" == "restarting" ]]; then + bad+=("$cid") + fi + done + + if (( ${#bad[@]} == 0 )); then + printf "(none)\n" + return 0 + fi + + local cid name + for cid in "${bad[@]}"; do + name="$(docker inspect -f '{{.Name}}' "$cid" 2>/dev/null | sed 's#^/##')" + printf " %b%s%b\n" "$BOLD" "$name" "$NC" + docker logs --tail 6 "$cid" 2>&1 | sed 's/^/ /' || true + done +} + +# 7) Drift / hygiene checks +_status_show_drift() { + local project; project="$(lds_project)" + + local -a compose_names=() + mapfile -t compose_names < <(docker_compose ps -a --format '{{.Name}}' 2>/dev/null | sed '/^[[:space:]]*$/d' || true) + + declare -A have=() + local n + for n in "${compose_names[@]}"; do have["$n"]=1; done + + local -a labeled=() + mapfile -t labeled < <(docker ps -a --filter "label=com.docker.compose.project=$project" --format '{{.Names}}' 2>/dev/null | sed '/^[[:space:]]*$/d' || true) + + local -a orphans=() + for n in "${labeled[@]}"; do + [[ -n "${have[$n]+x}" ]] || orphans+=("$n") + done + + if (( ${#orphans[@]} )); then + printf " %bOrphan containers:%b %s\n" "$YELLOW" "$NC" "${orphans[*]}" + else + printf " %bOrphan containers:%b (none)\n" "$GREEN" "$NC" + fi + + local -a v_lab=() v_mnt=() + mapfile -t v_lab < <(docker volume ls -q --filter "label=com.docker.compose.project=$project" 2>/dev/null | sed '/^[[:space:]]*$/d' || true) + + local -a cids=() + mapfile -t cids < <(_status_project_cids) + if (( ${#cids[@]} )); then + mapfile -t v_mnt < <( + docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\\n"}}{{end}}{{end}}' "${cids[@]}" 2>/dev/null \ + | sed '/^[[:space:]]*$/d' | awk '!seen[$0]++' + ) + fi + declare -A m=() + for n in "${v_mnt[@]}"; do m["$n"]=1; done + + local -a unused=() + for n in "${v_lab[@]}"; do + [[ -n "${m[$n]+x}" ]] || unused+=("$n") + done + + if (( ${#v_lab[@]} == 0 )); then + printf " %bLabeled volumes:%b (none)\n" "$DIM" "$NC" + elif (( ${#unused[@]} )); then + printf " %bUnused labeled volumes:%b %s\n" "$YELLOW" "$NC" "${unused[*]}" + else + printf " %bUnused labeled volumes:%b (none)\n" "$GREEN" "$NC" + fi + + local reclaim + reclaim="$(docker system df 2>/dev/null | awk '/Build Cache/ {print $NF}' || true)" + if [[ -n "$reclaim" && "$reclaim" != "0B" ]]; then + printf " %bHint:%b docker builder prune can reclaim build cache\n" "$DIM" "$NC" + fi +} + # Internal: compact checks for `status` (2 groups: System + Project) _status_checks() { _has() { has_cmd "$1"; } @@ -1997,16 +2335,54 @@ cmd_status() { return 0 fi - printf "\n%bStats:%b\n" "$CYAN" "$NC" + printf " +%bProblems:%b +" "$CYAN" "$NC" + _status_show_problems || true + + printf " +%bTop consumers:%b +" "$CYAN" "$NC" + _status_show_top_consumers || true + + printf " +%bStats:%b +" "$CYAN" "$NC" _status_show_stats "${1:-}" || true - printf "\n%bDisk:%b\n" "$CYAN" "$NC" + printf " +%bDisk:%b +" "$CYAN" "$NC" _status_show_disk || true - printf "\n%bVolumes:%b\n" "$CYAN" "$NC" + printf " +%bVolumes:%b +" "$CYAN" "$NC" _status_show_volumes || true - printf "\n%bChecks:%b\n" "$CYAN" "$NC" + printf " +%bNetworks:%b +" "$CYAN" "$NC" + _status_show_networks || true + + printf " +%bProbes:%b +" "$CYAN" "$NC" + _status_show_probes || true + + printf " +%bRecent errors:%b +" "$CYAN" "$NC" + _status_show_recent_errors || true + + printf " +%bDrift:%b +" "$CYAN" "$NC" + _status_show_drift || true + + printf " +%bChecks:%b +" "$CYAN" "$NC" _status_checks || true } # ───────────────────────────────────────────────────────────────────────────── From 600191d133c18cfc1ac410980457f4e178d93a41 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Fri, 6 Mar 2026 22:07:04 +0600 Subject: [PATCH 27/48] lds split --- docker/compose/main.yaml | 12 +++ lds | 155 +++++++++++++++++++++------------------ 2 files changed, 96 insertions(+), 71 deletions(-) diff --git a/docker/compose/main.yaml b/docker/compose/main.yaml index 0151d53..a2ced2e 100644 --- a/docker/compose/main.yaml +++ b/docker/compose/main.yaml @@ -4,6 +4,10 @@ networks: frontend: name: Frontend driver: bridge + labels: + com.docker.compose.project: "LocalDevStack" + com.docker.compose.network: "frontend" + com.infocyph.stack: "LocalDevStack" ipam: config: - subnet: 172.28.0.0/24 @@ -12,6 +16,10 @@ networks: backend: name: Backend driver: bridge + labels: + com.docker.compose.project: "LocalDevStack" + com.docker.compose.network: "backend" + com.infocyph.stack: "LocalDevStack" ipam: config: - subnet: 172.29.0.0/24 @@ -20,6 +28,10 @@ networks: datastore: name: DataStore driver: bridge + labels: + com.docker.compose.project: "LocalDevStack" + com.docker.compose.network: "datastore" + com.infocyph.stack: "LocalDevStack" ipam: config: - subnet: 172.30.0.0/24 diff --git a/lds b/lds index b62d615..aa38181 100755 --- a/lds +++ b/lds @@ -573,13 +573,6 @@ fix_perms() { chmod 755 "$DIR/docker" find "$DIR/docker" -type f ! -perm 644 -exec chmod 644 {} + - chmod 2777 "$DIR/data" - mkdir -p "$DIR/data/cloudbeaver" "$DIR/data/mysql" "$DIR/data/postgresql" "$DIR/data/mongo" \ - "$DIR/data/mariadb" "$DIR/data/elasticsearch" "$DIR/data/redis" "$DIR/data/mailpit" \ - "$DIR/data/redis-insight" "$DIR/data/kibana" "$DIR/data/filebeat" - find "$DIR/data" -mindepth 1 -maxdepth 1 -type d -exec chmod 2777 {} + - find "$DIR/data" -type f -exec chmod 666 {} + - chmod -R 777 "$DIR/logs" chown -R "$USER:docker" "$DIR/logs" @@ -612,7 +605,8 @@ setup_domain() { http_reload } -cmd_delhost() { +delete_domain() { + local domain="${1:-}" [[ -n "$domain" ]] || die "Usage: lds delhost " @@ -1542,62 +1536,47 @@ _status_project_names() { # 1) Problems: unhealthy/restarting/exited + restart count + last exit _status_show_problems() { local -a cids=() - mapfile -t cids < <(_status_project_cids) - if (( ${#cids[@]} == 0 )); then - printf "(none)\n" - return 0 - fi + mapfile -t cids < <(docker_compose ps -q 2>/dev/null | sed '/^[[:space:]]*$/d') + (( ${#cids[@]} == 0 )) && { printf "(none)\n"; return 0; } + + # Collect rows as TAB-separated fields + # name | state | health | restart | exit | finishedAt + local raw + raw="$( + docker inspect -f '{{.Name}} {{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}} {{.RestartCount}} {{.State.ExitCode}} {{.State.FinishedAt}}' \ + "${cids[@]}" 2>/dev/null | sed 's|^/||' + )" local -a rows=() - mapfile -t rows < <( - docker inspect -f '{{.Name}}\t{{.State.Status}}\t{{if .State.Health}}{{.State.Health.Status}}{{end}}\t{{.RestartCount}}\t{{.State.ExitCode}}\t{{.State.FinishedAt}}' \ - "${cids[@]}" 2>/dev/null | sed 's#^/##' - ) + local line name state health restart exitcode finishedAt + + # Only include actual problems: + # - not running + # - OR health == unhealthy + # Note: containers with no healthcheck (health="") are NOT problems. + while IFS=$'\t' read -r name state health restart exitcode finishedAt; do + [[ -n "$name" ]] || continue + if [[ "$state" != "running" || "$health" == "unhealthy" ]]; then + rows+=("$name"$'\t'"$state"$'\t'"${health:-'-'}"$'\t'"${restart:-0}"$'\t'"${exitcode:-0}"$'\t'"${finishedAt:-'-'}") + fi + done <<< "$raw" - local printed=0 - local w=4 - local line name st health rest exitc fin - for line in "${rows[@]}"; do - IFS=$'\t' read -r name st health rest exitc fin <<<"$line" - ((${#name} > w)) && w=${#name} - done - (( w > 34 )) && w=34 + if (( ${#rows[@]} == 0 )); then + printf "(none)\n" + return 0 + fi - printf " %b%-*s%b %b%-10s%b %b%-10s%b %b%-7s%b %b%s%b\n" \ - "$BOLD" "$w" "NAME" "$NC" \ - "$BOLD" "STATE" "$NC" \ - "$BOLD" "HEALTH" "$NC" \ - "$BOLD" "RESTART" "$NC" \ - "$BOLD" "LAST_EXIT" "$NC" + # Pretty table + printf " %-18s %-10s %-10s %-7s %-18s\n" "NAME" "STATE" "HEALTH" "RESTART" "LAST_EXIT" for line in "${rows[@]}"; do - IFS=$'\t' read -r name st health rest exitc fin <<<"$line" - local bad=0 - [[ "$st" != "running" ]] && bad=1 - [[ "$health" == "unhealthy" ]] && bad=1 - [[ "$st" == "restarting" ]] && bad=1 - - (( bad )) || continue - - local n="$name"; ((${#n} > w)) && n="${n:0:w-1}…" - local stc="$(_state_color "$st")" - local hc="$(_health_color "$health")" - local he="${health:-"-"}"; [[ -z "$he" ]] && he="-" - local last="-" - if [[ "$st" != "running" || "$exitc" != "0" ]]; then - last="code=$exitc" - fi - - printf " %-*s %b%-10s%b %b%-10s%b %-7s %s\n" \ - "$w" "$n" \ - "$stc" "${st:-'-'}" "$NC" \ - "$hc" "$he" "$NC" \ - "${rest:-0}" \ - "$last" - printed=1 + IFS=$'\t' read -r name state health restart exitcode finishedAt <<< "$line" + # LAST_EXIT: show exit code; add finishedAt for non-running states (helps debugging) + local last="code=${exitcode}" + [[ "$state" != "running" && "$finishedAt" != "-" ]] && last+=" @${finishedAt}" + printf " %-18s %-10s %-10s %-7s %-18s\n" \ + "$name" "$state" "${health:-'-'}" "${restart:-0}" "$last" done - - (( printed )) || printf " (none)\n" } # 2) Top consumers: top-5 by MEM and CPU (from docker stats) @@ -1750,23 +1729,62 @@ _status_show_volumes() { _status_show_networks() { local project; project="$(lds_project)" - printf " %bNetworks:%b\n" "$BOLD" "$NC" - docker network ls --filter "label=com.docker.compose.project=$project" --format ' {{.Name}}' 2>/dev/null || printf " (none)\n" - + # Project containers (fast, single compose call) local -a cids=() mapfile -t cids < <(_status_project_cids) - if (( ${#cids[@]} == 0 )); then - return 0 + (( ${#cids[@]} )) || { printf " (none)\n"; return 0; } + + printf " %bNetworks:%b\n" "$BOLD" "$NC" + + # 1) Prefer label-based listing (now that you add labels in compose) + # 2) Fallback: derive from containers (works for external/unlabeled networks) + local nets="" + nets="$(docker network ls --filter "label=com.docker.compose.project=$project" --format '{{.Name}}' 2>/dev/null | sed '/^[[:space:]]*$/d' || true)" + if [[ -z "$nets" ]]; then + nets="$( + docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' \ + "${cids[@]}" 2>/dev/null \ + | sed '/^[[:space:]]*$/d' \ + | sort -u + )" + fi + + if [[ -z "$nets" ]]; then + printf " (none)\n" + else + printf " %-22s %-8s %-18s %-18s %-6s\n" "NAME" "DRIVER" "SUBNET" "GATEWAY" "CNTS" + + # Count attached containers per network via inspect (cheap for a few nets) + while IFS= read -r n; do + [[ -n "$n" ]] || continue + + local driver subnet gateway cnt + driver="$(docker network inspect -f '{{.Driver}}' "$n" 2>/dev/null || true)" + subnet="$(docker network inspect -f '{{with index .IPAM.Config 0}}{{.Subnet}}{{end}}' "$n" 2>/dev/null || true)" + gateway="$(docker network inspect -f '{{with index .IPAM.Config 0}}{{.Gateway}}{{end}}' "$n" 2>/dev/null || true)" + cnt="$(docker network inspect -f '{{len .Containers}}' "$n" 2>/dev/null || true)" + + printf " %-22s %-8s %-18s %-18s %-6s\n" \ + "$n" "${driver:-'-'}" "${subnet:-'-'}" "${gateway:-'-'}" "${cnt:-0}" + done <<< "$nets" fi printf " %bContainer IPs:%b\n" "$BOLD" "$NC" + + # Format: NAMENETWORKIP local out - out="$(docker inspect -f '{{.Name}}\t{{range $k,$v := .NetworkSettings.Networks}}{{$k}}\t{{$v.IPAddress}}{{"\\n"}}{{end}}' "${cids[@]}" 2>/dev/null | sed 's#^/##' || true)" - if [[ -z "$out" ]]; then + out="$( + docker inspect -f '{{.Name}}{{"\t"}}{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{"\t"}}{{$v.IPAddress}}{{"\n"}}{{end}}' \ + "${cids[@]}" 2>/dev/null | sed 's#^/##' || true + )" + + if [[ -z "${out//$'\n'/}" ]]; then printf " (none)\n" return 0 fi - printf "%s\n" "$out" | awk -F'\t' 'NF>=3{printf " %-18s %-22s %s\n",$1,$2,$3}' + + printf " %-18s %-22s %s\n" "NAME" "NETWORK" "IP" + printf "%s\n" "$out" | awk -F'\t' 'NF>=3 && $2!="" {printf " %-18s %-22s %s\n",$1,$2,$3}' } # 5) Optional endpoint probes (gated) @@ -1913,7 +1931,6 @@ _status_checks() { [[ -f "$ENV_DOCKER" ]] && proj_ok+=(env) || proj_bad+=(env) [[ -d "$DIR/docker" ]] && proj_ok+=(docker_dir) || proj_bad+=(docker_dir) [[ -d "$DIR/configuration" ]] && proj_ok+=(config_dir) || proj_bad+=(config_dir) - [[ -d "$DIR/data" ]] && proj_ok+=(data_dir) || proj_bad+=(data_dir) [[ -d "$DIR/logs" ]] && proj_ok+=(logs_dir) || proj_bad+=(logs_dir) [[ -f "$DIR/configuration/ssl/rootCA.pem" ]] && proj_ok+=(rootCA) || proj_bad+=(rootCA) [[ -f "$DIR/configuration/ssl/mTLS-user.p12" ]] && proj_ok+=(mTLS) || proj_bad+=(mTLS) @@ -1937,10 +1954,6 @@ _status_checks() { } _status_show_disk() { docker system df - printf " -%bProject data:%b -" "$CYAN" "$NC" - du -sh "$DIR/data" 2>/dev/null || true } # Internal: human readable bytes (B/K/M/G/T). Accepts integer bytes. @@ -2573,7 +2586,7 @@ cmd_host() { ;; rm|remove|del|delete) local dom="${1:-}"; [[ -n "$dom" ]] || die "host rm " - cmd_delhost "$dom" + delete_domain "$dom" ;; list) shopt -s nullglob From b83cbed7390c4643b1dc99c6c97601ce3661d7ed Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Fri, 6 Mar 2026 23:18:37 +0600 Subject: [PATCH 28/48] Update lds --- lds | 50 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/lds b/lds index aa38181..7c4f8b3 100755 --- a/lds +++ b/lds @@ -588,7 +588,7 @@ fix_perms() { # 3. DOMAIN / PROFILE INTEGRATION ############################################################################### mkhost() { docker exec SERVER_TOOLS mkhost "$@"; } -delhost() { docker exec SERVER_TOOLS delhost "$@"; } +rmhost() { docker exec SERVER_TOOLS rmhost "$@"; } setup_domain() { mkhost --RESET @@ -606,13 +606,50 @@ setup_domain() { } delete_domain() { + rmhost --RESET + + # interactive delete + docker exec -it SERVER_TOOLS rmhost "$@" + + local php_cont apache_cont node_cont + php_cont=$(rmhost --DELETE_PHP_PROFILE || true) + apache_cont=$(rmhost --APACHE_DELETE || true) + node_cont=$(rmhost --DELETE_NODE_PROFILE || true) + + # helper: stop+remove containers by a comma-separated list of exact names + _rm_containers_csv() { + local csv="$1" + [[ -n "$csv" ]] || return 0 + local IFS=',' name + for name in $csv; do + name="$(echo "$name" | xargs)" + [[ -n "$name" ]] || continue + docker rm -f "$name" >/dev/null 2>&1 || true + done + } - local domain="${1:-}" - [[ -n "$domain" ]] || die "Usage: lds delhost " + # Stop/remove containers immediately (exact names from rmhost) + _rm_containers_csv "$php_cont" + _rm_containers_csv "$apache_cont" + _rm_containers_csv "$node_cont" - delhost "$domain" - delhost --RESET >/dev/null 2>&1 || true + # Remove profiles (if your modify_profiles expects profile keys, keep using what rmhost returns for profiles; + # if rmhost also returns exact profile names here, this is fine too.) + if [[ -n "$php_cont" ]]; then + local IFS=',' p + for p in $php_cont; do p="$(echo "$p" | xargs)"; [[ -n "$p" ]] && modify_profiles remove "$p"; done + fi + if [[ -n "$apache_cont" ]]; then + local IFS=',' p + for p in $apache_cont; do p="$(echo "$p" | xargs)"; [[ -n "$p" ]] && modify_profiles remove "$p"; done + fi + if [[ -n "$node_cont" ]]; then + local IFS=',' p + for p in $node_cont; do p="$(echo "$p" | xargs)"; [[ -n "$p" ]] && modify_profiles remove "$p"; done + fi + rmhost --RESET + dc_up -d http_reload } @@ -2585,8 +2622,7 @@ cmd_host() { setup_domain ;; rm|remove|del|delete) - local dom="${1:-}"; [[ -n "$dom" ]] || die "host rm " - delete_domain "$dom" + delete_domain "$@" ;; list) shopt -s nullglob From a8a568d0e9bf82bf284a62938da29ae61091d801 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sat, 7 Mar 2026 11:35:49 +0600 Subject: [PATCH 29/48] lds split --- lds | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/lds b/lds index 7c4f8b3..a311ab4 100755 --- a/lds +++ b/lds @@ -589,6 +589,49 @@ fix_perms() { ############################################################################### mkhost() { docker exec SERVER_TOOLS mkhost "$@"; } rmhost() { docker exec SERVER_TOOLS rmhost "$@"; } +php_reload_or_restart_project() { + local project; project="$(lds_project)" + [[ -n "$project" ]] || return 0 + + local -a php_names=() + mapfile -t php_names < <( + docker ps --format '{{.Names}}\t{{.Label "com.docker.compose.project"}}' 2>/dev/null \ + | awk -F'\t' -v pr="$project" '$2==pr && $1 ~ /^PHP_/ {print $1}' + ) + ((${#php_names[@]})) || return 0 + + local name + for name in "${php_names[@]}"; do + if docker exec "$name" sh -lc ' + set -e + + # 1) config test (safe to skip if not supported) + if command -v php-fpm >/dev/null 2>&1; then + php-fpm -tt >/dev/null 2>&1 || exit 2 + fi + + # 2) find master pid + pid="" + for f in /var/run/php-fpm.pid /run/php-fpm/php-fpm.pid /run/php-fpm.pid; do + if [ -s "$f" ]; then pid="$(cat "$f" 2>/dev/null || true)"; break; fi + done + + if [ -z "$pid" ] && command -v ps >/dev/null 2>&1; then + # busybox ps output; pick "php-fpm: master" line if present + pid="$(ps 2>/dev/null | awk "/php-fpm: master/ {print \$1; exit}")" + fi + + # common case: php-fpm is PID 1 in the container + [ -n "$pid" ] || pid="1" + + kill -USR2 "$pid" >/dev/null 2>&1 + ' >/dev/null 2>&1; then + : # reloaded OK + else + docker restart "$name" >/dev/null 2>&1 || true + fi + done +} setup_domain() { mkhost --RESET @@ -601,6 +644,7 @@ setup_domain() { [[ -n $svr_prof ]] && modify_profiles add "$svr_prof" [[ -n $node_prof ]] && modify_profiles add "$node_prof" mkhost --RESET + php_reload_or_restart_project dc_up -d http_reload } @@ -649,6 +693,7 @@ delete_domain() { fi rmhost --RESET + php_reload_or_restart_project dc_up -d http_reload } From 08567296433238766ac870b51e13b4867a4dad17 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sat, 7 Mar 2026 12:38:32 +0600 Subject: [PATCH 30/48] lds core --- lds | 1068 +++++++++++++++++++++++++++++++---------------------------- 1 file changed, 554 insertions(+), 514 deletions(-) diff --git a/lds b/lds index a311ab4..941d04b 100755 --- a/lds +++ b/lds @@ -11,9 +11,15 @@ set -euo pipefail ############################################################################### # Color defaults (overridden later by init_colors); keep safe under set -u -BOLD="${BOLD:-}"; DIM="${DIM:-}" -RED="${RED:-}"; GREEN="${GREEN:-}"; CYAN="${CYAN:-}"; YELLOW="${YELLOW:-}" -BLUE="${BLUE:-}"; MAGENTA="${MAGENTA:-}"; NC="${NC:-}" +BOLD="${BOLD:-}" +DIM="${DIM:-}" +RED="${RED:-}" +GREEN="${GREEN:-}" +CYAN="${CYAN:-}" +YELLOW="${YELLOW:-}" +BLUE="${BLUE:-}" +MAGENTA="${MAGENTA:-}" +NC="${NC:-}" die() { printf "%bError:%b %s @@ -56,8 +62,9 @@ need_bin() { # Commands that must NEVER be proxied to SERVER_TOOLS (host control plane / privileged / filesystem) _is_host_only_cmd() { case "${1:-}" in - docker|sudo|su|systemctl|service|ip|iptables|nft|sysctl|mount|umount|modprobe|insmod|rmmod|rm|mv|cp|chmod|chown) - return 0 ;; + docker | sudo | su | systemctl | service | ip | iptables | nft | sysctl | mount | umount | modprobe | insmod | rmmod | rm | mv | cp | chmod | chown) + return 0 + ;; esac return 1 } @@ -83,11 +90,15 @@ _server_tools_has() { lds_tools_cmd() { local cmd="${1:-}" shift || true - [[ -n "$cmd" ]] || { echo "lds_tools_cmd: missing command" >&2; return 2; } + [[ -n "$cmd" ]] || { + echo "lds_tools_cmd: missing command" >&2 + return 2 + } # 1) host binary wins if has_bin "$cmd"; then - local p; p="$(bin_path "$cmd")" + local p + p="$(bin_path "$cmd")" "$p" "$@" return $? fi @@ -158,7 +169,10 @@ need() { IFS='|,' read -ra alts <<<"$group" found=0 for cmd in "${alts[@]}"; do - has_bin "$cmd" && { found=1; break; } + has_bin "$cmd" && { + found=1 + break + } done ((found)) && continue local miss=${alts[*]} @@ -294,11 +308,10 @@ fi # - --quiet suppresses non-error output QUIET=0 -say() { ((QUIET)) || printf '%b\n' "$*"; } -ok() { ((QUIET)) || printf '%b\n' "${GREEN}$*${NC}"; } +say() { ((QUIET)) || printf '%b\n' "$*"; } +ok() { ((QUIET)) || printf '%b\n' "${GREEN}$*${NC}"; } warn() { ((QUIET)) || printf '%b\n' "${YELLOW}$*${NC}"; } -err() { printf '%b\n' "${RED}$*${NC}" >&2; } - +err() { printf '%b\n' "${RED}$*${NC}" >&2; } # Default behavior: QUIET VERBOSE=0 @@ -320,7 +333,7 @@ on_error() { local fn="${FUNCNAME[1]:-main}" local src="${BASH_SOURCE[1]:-$0}" - printf "\n%bError:%b %s:%s in %s() (exit %d)\n" "$RED" "$NC" "$src" "$line" "$fn" "$code" >&2 + printf "\n%bError:%b %s:%s in %s() (exit %d)\n" "$RED" "$NC" "$src" "$line" "$fn" "$code" >&2 printf "%bCommand:%b %s\n" "$RED" "$NC" "$cmd" >&2 if ((VERBOSE)); then @@ -328,7 +341,7 @@ on_error() { local i=1 while caller "$i" >/dev/null 2>&1; do caller "$i" >&2 - i=$((i+1)) + i=$((i + 1)) done fi printf "\n" >&2 @@ -355,7 +368,7 @@ load_extras() { [[ -d "$EXTRAS_DIR" ]] || return 0 mapfile -t __EXTRA_FILES < <( - find "$EXTRAS_DIR" -maxdepth 1 -type f \( -name '*.yaml' -o -name '*.yml' \) -print 2>/dev/null | sort | sed '/^[[:space:]]*$/d' + find "$EXTRAS_DIR" -maxdepth 1 -type f \( -name '*.yaml' -o -name '*.yml' \) -print 2>/dev/null | sort | sed '/^[[:space:]]*$/d' ) } @@ -376,16 +389,16 @@ docker_compose() { for f in "${__EXTRA_FILES[@]:-}"; do [[ -f "$f" ]] || continue case "$f" in - *.yml|*.yaml) extra_f+=(-f "$f") ;; + *.yml | *.yaml) extra_f+=(-f "$f") ;; esac done "${__LDS_DC_BIN[@]}" \ - --project-directory "$DIR" \ - -f "$COMPOSE_FILE" \ - "${extra_f[@]}" \ - --env-file "$ENV_DOCKER" \ - "$@" + --project-directory "$DIR" \ + -f "$COMPOSE_FILE" \ + "${extra_f[@]}" \ + --env-file "$ENV_DOCKER" \ + "$@" } # helper: print project name @@ -401,17 +414,17 @@ dc_cmd() { local -a quiet=() if ((VERBOSE == 0)); then case "$sub" in - up) quiet+=(--quiet-pull) ;; - pull) quiet+=(-q) ;; - build) quiet+=(--quiet) ;; + up) quiet+=(--quiet-pull) ;; + pull) quiet+=(-q) ;; + build) quiet+=(--quiet) ;; esac fi docker_compose "$sub" "${quiet[@]}" "$@" } -dc_up() { dc_cmd up "$@"; } -dc_pull() { dc_cmd pull "$@"; } +dc_up() { dc_cmd up "$@"; } +dc_pull() { dc_cmd pull "$@"; } dc_build() { dc_cmd build "$@"; } # helper for our own minimal logging (still shows in quiet mode) @@ -590,13 +603,14 @@ fix_perms() { mkhost() { docker exec SERVER_TOOLS mkhost "$@"; } rmhost() { docker exec SERVER_TOOLS rmhost "$@"; } php_reload_or_restart_project() { - local project; project="$(lds_project)" + local project + project="$(lds_project)" [[ -n "$project" ]] || return 0 local -a php_names=() mapfile -t php_names < <( - docker ps --format '{{.Names}}\t{{.Label "com.docker.compose.project"}}' 2>/dev/null \ - | awk -F'\t' -v pr="$project" '$2==pr && $1 ~ /^PHP_/ {print $1}' + docker ps --format '{{.Names}}\t{{.Label "com.docker.compose.project"}}' 2>/dev/null | + awk -F'\t' -v pr="$project" '$2==pr && $1 ~ /^PHP_/ {print $1}' ) ((${#php_names[@]})) || return 0 @@ -681,15 +695,24 @@ delete_domain() { # if rmhost also returns exact profile names here, this is fine too.) if [[ -n "$php_cont" ]]; then local IFS=',' p - for p in $php_cont; do p="$(echo "$p" | xargs)"; [[ -n "$p" ]] && modify_profiles remove "$p"; done + for p in $php_cont; do + p="$(echo "$p" | xargs)" + [[ -n "$p" ]] && modify_profiles remove "$p" + done fi if [[ -n "$apache_cont" ]]; then local IFS=',' p - for p in $apache_cont; do p="$(echo "$p" | xargs)"; [[ -n "$p" ]] && modify_profiles remove "$p"; done + for p in $apache_cont; do + p="$(echo "$p" | xargs)" + [[ -n "$p" ]] && modify_profiles remove "$p" + done fi if [[ -n "$node_cont" ]]; then local IFS=',' p - for p in $node_cont; do p="$(echo "$p" | xargs)"; [[ -n "$p" ]] && modify_profiles remove "$p"; done + for p in $node_cont; do + p="$(echo "$p" | xargs)" + [[ -n "$p" ]] && modify_profiles remove "$p" + done fi rmhost --RESET @@ -906,104 +929,6 @@ process_all() { printf "\n%b✅ Selected services configured!%b\n" "$GREEN" "$NC" } -############################################################################### -# 4. CONTAINER SHELL HELPERS (PHP / NODE) -############################################################################### -launch_php() { - local domain=$1 suffix - local nconf="$DIR/configuration/nginx/$domain.conf" - local aconf="$DIR/configuration/apache/$domain.conf" - [[ -f $nconf ]] || die "No Nginx config for $domain" - - local docroot php - if grep -q fastcgi_pass "$nconf"; then - php=$(grep -Eo 'fastcgi_pass ([^:]+):9000' "$nconf" | awk '{print $2}' | sed 's/:9000$//') - docroot=$(grep -m1 -Eo 'root [^;]+' "$nconf" | awk '{print $2}') - else - [[ -f $aconf ]] || die "No Apache config for $domain" - docroot=$(grep -m1 -Eo 'DocumentRoot [^ ]+' "$aconf" | awk '{print $2}') - php=$(grep -Eo 'proxy:fcgi://([^:]+):9000' "$aconf" | sed 's/.*:\/\/\([^:]*\):.*/\1/') - fi - - [[ $php ]] || die "Could not detect PHP container for $domain" - [[ $docroot ]] || docroot=/app - for suffix in public dist public_html; do - [[ $docroot == */$suffix ]] && { - docroot=${docroot%/*} - break - } - done - - php=$(echo "$php" | tr ' \n' '\n' | awk 'NF && !seen[$0]++' | paste -sd' ' -) - docker exec -it "$php" bash --login -c "cd '$docroot' && exec bash" -} - -# ----------------------------------------------------------------------------- -# Node shell (always /app) -# ----------------------------------------------------------------------------- -launch_node() { - local domain="${1:-}" - [[ -n "$domain" ]] || die "Usage: lds core " - - local nconf="$DIR/configuration/nginx/$domain.conf" - [[ -f "$nconf" ]] || die "No Nginx config for $domain" - - # Expect: proxy_pass http://node_:; - local upstream host token ctr - upstream="$( - grep -m1 -Eo 'proxy_pass[[:space:]]+http://[^;]+' "$nconf" 2>/dev/null | - awk '{print $2}' | - sed 's|^http://||' - )" - - [[ -n "${upstream:-}" ]] || die "Could not detect node upstream for $domain" - host="${upstream%%:*}" # node_resume_sparkle_localhost - - [[ -n "${host:-}" ]] || die "Could not parse upstream host for $domain" - - # Standard mapping: node_ -> NODE_ - ctr="" - if docker inspect "$host" >/dev/null 2>&1; then - ctr="$host" - elif [[ "$host" == node_* ]]; then - token="${host#node_}" - ctr="NODE_${token^^}" - docker inspect "$ctr" >/dev/null 2>&1 || ctr="" - fi - - [[ -n "${ctr:-}" ]] || die "Node container not found for upstream '$host' (domain: $domain)" - docker inspect -f '{{.State.Running}}' "$ctr" 2>/dev/null | grep -qx true || die "Container not running: $ctr" - - docker exec -it "$ctr" sh -lc ' - cd /app 2>/dev/null || cd / || true - if command -v bash >/dev/null 2>&1; then exec bash --login; fi - exec sh - ' -} - -conf_node_container() { - local f="$1" - - # Nginx node vhost: proxy_pass http://node_:; - local host token ctr - - host="$( - grep -m1 -Eo 'proxy_pass[[:space:]]+http://[^;]+' "$f" 2>/dev/null | - awk '{print $2}' | - sed 's|^http://||' | - awk -F: '{print $1}' - )" - - [[ -n "${host:-}" ]] || return 0 - [[ "$host" == node_* ]] || return 0 - - token="${host#node_}" - ctr="NODE_${token^^}" - - docker inspect "$ctr" >/dev/null 2>&1 || return 0 - printf '%s' "$ctr" -} - ############################################################################### # 5. ENVIRONMENT + CERT / CA ############################################################################### @@ -1544,7 +1469,6 @@ cmd_start() { http_reload } - cmd_stop() { docker_compose down; } cmd_down() { @@ -1554,13 +1478,26 @@ cmd_down() { local -a args=() while [[ "${1:-}" ]]; do case "$1" in - --yes|-y) yes=1; shift ;; - --volumes|-v) vols=1; args+=("--volumes"); shift ;; - --remove-orphans) args+=("--remove-orphans"); shift ;; - *) args+=("$1"); shift ;; + --yes | -y) + yes=1 + shift + ;; + --volumes | -v) + vols=1 + args+=("--volumes") + shift + ;; + --remove-orphans) + args+=("--remove-orphans") + shift + ;; + *) + args+=("$1") + shift + ;; esac done - if ((vols)) && ((yes==0)); then + if ((vols)) && ((yes == 0)); then die "Refusing: down --volumes requires --yes" fi docker_compose down "${args[@]}" @@ -1576,7 +1513,7 @@ cmd_reboot() { cmd_restart; } # 6a. STATUS / PS / STATS # ───────────────────────────────────────────────────────────────────────────── cmd_ps() { - if (( $# )); then + if (($#)); then docker_compose ps "$@" else docker_compose ps @@ -1590,21 +1527,24 @@ _status_show_stats() { # pass container names derived from compose itself. local -a names=() if [[ -n "$svc" ]]; then - local s; s="$(resolve_service "$svc" 2>/dev/null || true)" - [[ -n "$s" ]] || { printf "(no matching containers)\n"; return 0; } + local s + s="$(resolve_service "$svc" 2>/dev/null || true)" + [[ -n "$s" ]] || { + printf "(no matching containers)\n" + return 0 + } mapfile -t names < <(docker_compose ps "$s" --format '{{.Name}}' 2>/dev/null | sed '/^[[:space:]]*$/d') else mapfile -t names < <(docker_compose ps --format '{{.Name}}' 2>/dev/null | sed '/^[[:space:]]*$/d') fi - if (( ${#names[@]} )); then + if ((${#names[@]})); then docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}" "${names[@]}" else printf "(no matching containers)\n" fi } - # Internal: list container IDs for current compose project _status_project_cids() { docker_compose ps -q 2>/dev/null | sed '/^[[:space:]]*$/d' || true @@ -1619,7 +1559,10 @@ _status_project_names() { _status_show_problems() { local -a cids=() mapfile -t cids < <(docker_compose ps -q 2>/dev/null | sed '/^[[:space:]]*$/d') - (( ${#cids[@]} == 0 )) && { printf "(none)\n"; return 0; } + ((${#cids[@]} == 0)) && { + printf "(none)\n" + return 0 + } # Collect rows as TAB-separated fields # name | state | health | restart | exit | finishedAt @@ -1641,9 +1584,9 @@ _status_show_problems() { if [[ "$state" != "running" || "$health" == "unhealthy" ]]; then rows+=("$name"$'\t'"$state"$'\t'"${health:-'-'}"$'\t'"${restart:-0}"$'\t'"${exitcode:-0}"$'\t'"${finishedAt:-'-'}") fi - done <<< "$raw" + done <<<"$raw" - if (( ${#rows[@]} == 0 )); then + if ((${#rows[@]} == 0)); then printf "(none)\n" return 0 fi @@ -1652,7 +1595,7 @@ _status_show_problems() { printf " %-18s %-10s %-10s %-7s %-18s\n" "NAME" "STATE" "HEALTH" "RESTART" "LAST_EXIT" for line in "${rows[@]}"; do - IFS=$'\t' read -r name state health restart exitcode finishedAt <<< "$line" + IFS=$'\t' read -r name state health restart exitcode finishedAt <<<"$line" # LAST_EXIT: show exit code; add finishedAt for non-running states (helps debugging) local last="code=${exitcode}" [[ "$state" != "running" && "$finishedAt" != "-" ]] && last+=" @${finishedAt}" @@ -1665,14 +1608,17 @@ _status_show_problems() { _status_show_top_consumers() { local -a names=() mapfile -t names < <(_status_project_names) - if (( ${#names[@]} == 0 )); then + if ((${#names[@]} == 0)); then printf "(none)\n" return 0 fi local stats stats="$(docker stats --no-stream --format '{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}' "${names[@]}" 2>/dev/null || true)" - [[ -n "$stats" ]] || { printf "(no stats)\n"; return 0; } + [[ -n "$stats" ]] || { + printf "(no stats)\n" + return 0 + } _to_bytes() { local v="$1" @@ -1699,8 +1645,8 @@ _status_show_top_consumers() { printf "%s\n" "$stats" | awk -F'\t' ' {name=$1; cpu=$2; mem=$3; split(mem,a,"/"); gsub(/^[[:space:]]+|[[:space:]]+$/,"",a[1]); print name "\t" cpu "\t" a[1]} ' | while IFS=$'\t' read -r n cpu mem; do - printf "%s\t%s\t%s\t%s\n" "$n" "$cpu" "$mem" "$(_to_bytes "$mem")" - done | sort -t$'\t' -k4,4nr | head -n 5 | awk -F'\t' '{printf " %-16s CPU=%-7s MEM=%s\n",$1,$2,$3}' + printf "%s\t%s\t%s\t%s\n" "$n" "$cpu" "$mem" "$(_to_bytes "$mem")" + done | sort -t$'\t' -k4,4nr | head -n 5 | awk -F'\t' '{printf " %-16s CPU=%-7s MEM=%s\n",$1,$2,$3}' printf " %bTop by CPU:%b\n" "$BOLD" "$NC" printf "%s\n" "$stats" | awk -F'\t' ' @@ -1712,7 +1658,7 @@ _status_show_top_consumers() { _status_show_volumes() { local -a cids=() mapfile -t cids < <(_status_project_cids) - if (( ${#cids[@]} == 0 )); then + if ((${#cids[@]} == 0)); then printf "(none)\n" return 0 fi @@ -1734,7 +1680,7 @@ _status_show_volumes() { local -a vols=() mapfile -t vols < <(printf "%s\n" "${!v2c[@]}" | sort) - if (( ${#vols[@]} == 0 )); then + if ((${#vols[@]} == 0)); then printf "(none)\n" return 0 fi @@ -1767,14 +1713,14 @@ _status_show_volumes() { for line in "${rows[@]}"; do IFS=$'\t' read -r name drv <<<"$line" ((${#name} > w_name)) && w_name=${#name} - ((${#drv} > w_drv)) && w_drv=${#drv} + ((${#drv} > w_drv)) && w_drv=${#drv} done - (( w_name > 36 )) && w_name=36 - (( w_drv > 12 )) && w_drv=12 + ((w_name > 36)) && w_name=36 + ((w_drv > 12)) && w_drv=12 printf " %b%-*s%b %b%-*s%b %b%-8s%b %b%-6s%b %b%s%b\n" \ "$BOLD" "$w_name" "VOLUME" "$NC" \ - "$BOLD" "$w_drv" "DRIVER" "$NC" \ + "$BOLD" "$w_drv" "DRIVER" "$NC" \ "$BOLD" "SIZE" "$NC" \ "$BOLD" "LINKS" "$NC" \ "$BOLD" "MOUNTED_BY" "$NC" @@ -1791,30 +1737,36 @@ _status_show_volumes() { local shown="$mb" extra="" local cnt cnt="$(printf '%s' "$mb" | awk -F',' '{print NF}')" - if (( cnt > 3 )); then + if ((cnt > 3)); then shown="$(printf '%s' "$mb" | awk -F',' '{print $1","$2","$3}')" extra=",…(+${cnt}-3)" - extra=",…(+$(($cnt-3)))" + extra=",…(+$(($cnt - 3)))" fi - local n="$name"; ((${#n} > w_name)) && n="${n:0:w_name-1}…" - local d="$drv"; ((${#d} > w_drv)) && d="${d:0:w_drv-1}…" + local n="$name" + ((${#n} > w_name)) && n="${n:0:w_name-1}…" + local d="$drv" + ((${#d} > w_drv)) && d="${d:0:w_drv-1}…" printf " %-*s %-*s %-8s %-6s %s%s\n" \ "$w_name" "$n" "$w_drv" "${d:-'-'}" "$sz" "$lk" "$shown" "$extra" done - (( any_size )) || printf "\n %bNote:%b volume sizes unavailable (docker system df -v did not provide volume table)\n" "$YELLOW" "$NC" + ((any_size)) || printf "\n %bNote:%b volume sizes unavailable (docker system df -v did not provide volume table)\n" "$YELLOW" "$NC" } # 4) Networks + IPs _status_show_networks() { - local project; project="$(lds_project)" + local project + project="$(lds_project)" # Project containers (fast, single compose call) local -a cids=() mapfile -t cids < <(_status_project_cids) - (( ${#cids[@]} )) || { printf " (none)\n"; return 0; } + ((${#cids[@]})) || { + printf " (none)\n" + return 0 + } printf " %bNetworks:%b\n" "$BOLD" "$NC" @@ -1825,9 +1777,9 @@ _status_show_networks() { if [[ -z "$nets" ]]; then nets="$( docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' \ - "${cids[@]}" 2>/dev/null \ - | sed '/^[[:space:]]*$/d' \ - | sort -u + "${cids[@]}" 2>/dev/null | + sed '/^[[:space:]]*$/d' | + sort -u )" fi @@ -1848,7 +1800,7 @@ _status_show_networks() { printf " %-22s %-8s %-18s %-18s %-6s\n" \ "$n" "${driver:-'-'}" "${subnet:-'-'}" "${gateway:-'-'}" "${cnt:-0}" - done <<< "$nets" + done <<<"$nets" fi printf " %bContainer IPs:%b\n" "$BOLD" "$NC" @@ -1871,7 +1823,10 @@ _status_show_networks() { # 5) Optional endpoint probes (gated) _status_show_probes() { - ((${LDS_STATUS_PROBE:-0})) || { printf "(disabled; set LDS_STATUS_PROBE=1)\n"; return 0; } + ((${LDS_STATUS_PROBE:-0})) || { + printf "(disabled; set LDS_STATUS_PROBE=1)\n" + return 0 + } if ! has_tool curl; then printf "(curl missing)\n" return 0 @@ -1880,7 +1835,7 @@ _status_show_probes() { local -a urls=() mapfile -t urls < <(_status_urls 2>/dev/null || true) - if (( ${#urls[@]} == 0 )); then + if ((${#urls[@]} == 0)); then printf "(no urls)\n" return 0 fi @@ -1898,7 +1853,10 @@ _status_show_probes() { _status_show_recent_errors() { local -a cids=() mapfile -t cids < <(_status_project_cids) - (( ${#cids[@]} )) || { printf "(none)\n"; return 0; } + ((${#cids[@]})) || { + printf "(none)\n" + return 0 + } local -a bad=() local cid st health @@ -1910,7 +1868,7 @@ _status_show_recent_errors() { fi done - if (( ${#bad[@]} == 0 )); then + if ((${#bad[@]} == 0)); then printf "(none)\n" return 0 fi @@ -1925,7 +1883,8 @@ _status_show_recent_errors() { # 7) Drift / hygiene checks _status_show_drift() { - local project; project="$(lds_project)" + local project + project="$(lds_project)" local -a compose_names=() mapfile -t compose_names < <(docker_compose ps -a --format '{{.Name}}' 2>/dev/null | sed '/^[[:space:]]*$/d' || true) @@ -1942,7 +1901,7 @@ _status_show_drift() { [[ -n "${have[$n]+x}" ]] || orphans+=("$n") done - if (( ${#orphans[@]} )); then + if ((${#orphans[@]})); then printf " %bOrphan containers:%b %s\n" "$YELLOW" "$NC" "${orphans[*]}" else printf " %bOrphan containers:%b (none)\n" "$GREEN" "$NC" @@ -1953,10 +1912,10 @@ _status_show_drift() { local -a cids=() mapfile -t cids < <(_status_project_cids) - if (( ${#cids[@]} )); then + if ((${#cids[@]})); then mapfile -t v_mnt < <( - docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\\n"}}{{end}}{{end}}' "${cids[@]}" 2>/dev/null \ - | sed '/^[[:space:]]*$/d' | awk '!seen[$0]++' + docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\\n"}}{{end}}{{end}}' "${cids[@]}" 2>/dev/null | + sed '/^[[:space:]]*$/d' | awk '!seen[$0]++' ) fi declare -A m=() @@ -1967,9 +1926,9 @@ _status_show_drift() { [[ -n "${m[$n]+x}" ]] || unused+=("$n") done - if (( ${#v_lab[@]} == 0 )); then + if ((${#v_lab[@]} == 0)); then printf " %bLabeled volumes:%b (none)\n" "$DIM" "$NC" - elif (( ${#unused[@]} )); then + elif ((${#unused[@]})); then printf " %bUnused labeled volumes:%b %s\n" "$YELLOW" "$NC" "${unused[*]}" else printf " %bUnused labeled volumes:%b (none)\n" "$GREEN" "$NC" @@ -2019,19 +1978,19 @@ _status_checks() { # Print exactly two lines (tab separated) printf "%bSystem%b\t" "$CYAN" "$NC" - if (( ${#sys_bad[@]} )); then + if ((${#sys_bad[@]})); then printf "%b!%b %s" "$YELLOW" "$NC" "${sys_bad[*]}" - if (( ${#sys_ok[@]} )); then printf "%b | %b" "$DIM" "$NC"; fi + if ((${#sys_ok[@]})); then printf "%b | %b" "$DIM" "$NC"; fi fi - (( ${#sys_ok[@]} )) && printf "%b✓%b %s" "$GREEN" "$NC" "${sys_ok[*]}" + ((${#sys_ok[@]})) && printf "%b✓%b %s" "$GREEN" "$NC" "${sys_ok[*]}" printf "\n" printf "%bProject%b\t" "$CYAN" "$NC" - if (( ${#proj_bad[@]} )); then + if ((${#proj_bad[@]})); then printf "%b!%b %s" "$YELLOW" "$NC" "${proj_bad[*]}" - if (( ${#proj_ok[@]} )); then printf "%b | %b" "$DIM" "$NC"; fi + if ((${#proj_ok[@]})); then printf "%b | %b" "$DIM" "$NC"; fi fi - (( ${#proj_ok[@]} )) && printf "%b✓%b %s" "$GREEN" "$NC" "${proj_ok[*]}" + ((${#proj_ok[@]})) && printf "%b✓%b %s" "$GREEN" "$NC" "${proj_ok[*]}" printf "\n" } _status_show_disk() { @@ -2053,12 +2012,18 @@ _human_bytes() { # Internal: directory size in bytes (portable-ish across GNU/BSD du) _dir_size_bytes() { local p="${1:-}" - [[ -n "$p" && -d "$p" ]] || { printf '0'; return 0; } + [[ -n "$p" && -d "$p" ]] || { + printf '0' + return 0 + } # GNU du supports -b; BSD does not. local out if out="$(du -sb "$p" 2>/dev/null | awk '{print $1}' 2>/dev/null)"; then - [[ -n "$out" ]] && { printf '%s' "$out"; return 0; } + [[ -n "$out" ]] && { + printf '%s' "$out" + return 0 + } fi # Fallback: du -sk * 1024 @@ -2069,7 +2034,8 @@ _dir_size_bytes() { # Internal: volumes list + sizes for this compose project _status_show_volumes() { - local project; project="$(lds_project)" + local project + project="$(lds_project)" # 1) volumes by compose label (if any) local -a vols=() @@ -2081,22 +2047,22 @@ _status_show_volumes() { local -a cids=() mapfile -t cids < <(docker_compose ps -q 2>/dev/null | sed '/^[[:space:]]*$/d') - if (( ${#cids[@]} > 0 )); then + if ((${#cids[@]} > 0)); then local v while IFS= read -r v; do [[ -n "$v" ]] && vols+=("$v") done < <( - docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\n"}}{{end}}{{end}}' "${cids[@]}" 2>/dev/null \ - | sed '/^[[:space:]]*$/d' + docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\n"}}{{end}}{{end}}' "${cids[@]}" 2>/dev/null | + sed '/^[[:space:]]*$/d' ) fi # uniq - if (( ${#vols[@]} > 0 )); then + if ((${#vols[@]} > 0)); then mapfile -t vols < <(printf "%s\n" "${vols[@]}" | awk '!seen[$0]++') fi - if (( ${#vols[@]} == 0 )); then + if ((${#vols[@]} == 0)); then printf "(none)\n" return 0 fi @@ -2128,7 +2094,7 @@ _status_show_volumes() { docker volume inspect -f '{{.Name}} {{.Driver}}' "${vols[@]}" 2>/dev/null | sed '/^[[:space:]]*$/d' ) - if (( ${#rows[@]} == 0 )); then + if ((${#rows[@]} == 0)); then printf "(none)\n" return 0 fi @@ -2138,14 +2104,14 @@ _status_show_volumes() { for line in "${rows[@]}"; do IFS=$'\t' read -r name drv <<<"$line" ((${#name} > w_name)) && w_name=${#name} - ((${#drv} > w_drv)) && w_drv=${#drv} + ((${#drv} > w_drv)) && w_drv=${#drv} done - (( w_name > 40 )) && w_name=40 - (( w_drv > 12 )) && w_drv=12 + ((w_name > 40)) && w_name=40 + ((w_drv > 12)) && w_drv=12 printf " %b%-*s%b %b%-*s%b %b%s%b\n" \ "$BOLD" "$w_name" "NAME" "$NC" \ - "$BOLD" "$w_drv" "DRIVER" "$NC" \ + "$BOLD" "$w_drv" "DRIVER" "$NC" \ "$BOLD" "SIZE" "$NC" local total_note=0 @@ -2154,18 +2120,18 @@ _status_show_volumes() { local n_disp="$name" d_disp="$drv" if ((${#n_disp} > w_name)); then n_disp="${n_disp:0:w_name-1}…"; fi - if ((${#d_disp} > w_drv)); then d_disp="${d_disp:0:w_drv-1}…"; fi + if ((${#d_disp} > w_drv)); then d_disp="${d_disp:0:w_drv-1}…"; fi local sz="${__VOL_SZ[$name]:--}" [[ "$sz" != "-" ]] && total_note=1 printf " %-*s %-*s %s\n" \ "$w_name" "$n_disp" \ - "$w_drv" "${d_disp:-'-'}" \ + "$w_drv" "${d_disp:-'-'}" \ "$sz" done - if (( total_note == 0 )); then + if ((total_note == 0)); then printf "\n %bNote:%b volume sizes unavailable (docker system df -v did not provide volume table)\n" \ "$YELLOW" "$NC" fi @@ -2204,17 +2170,17 @@ _status_health_line() { # Prefer health when present, otherwise State.Status if [[ -n "$health" ]]; then case "$health" in - healthy) printf '%b%s%b' "$GREEN" "$line" "$NC" ;; - starting) printf '%b%s%b' "$YELLOW" "$line" "$NC" ;; - unhealthy) printf '%b%s%b' "$RED" "$line" "$NC" ;; - *) printf '%b%s%b' "$MAGENTA" "$line" "$NC" ;; + healthy) printf '%b%s%b' "$GREEN" "$line" "$NC" ;; + starting) printf '%b%s%b' "$YELLOW" "$line" "$NC" ;; + unhealthy) printf '%b%s%b' "$RED" "$line" "$NC" ;; + *) printf '%b%s%b' "$MAGENTA" "$line" "$NC" ;; esac else case "$st" in - running) printf '%b%s%b' "$GREEN" "$line" "$NC" ;; - exited|dead) printf '%b%s%b' "$RED" "$line" "$NC" ;; - created|paused|restarting) printf '%b%s%b' "$YELLOW" "$line" "$NC" ;; - *) printf '%b%s%b' "$MAGENTA" "$line" "$NC" ;; + running) printf '%b%s%b' "$GREEN" "$line" "$NC" ;; + exited | dead) printf '%b%s%b' "$RED" "$line" "$NC" ;; + created | paused | restarting) printf '%b%s%b' "$YELLOW" "$line" "$NC" ;; + *) printf '%b%s%b' "$MAGENTA" "$line" "$NC" ;; esac fi } @@ -2223,7 +2189,8 @@ _status_health_line() { _status_core() { local json="$1" quiet="$2" - local project; project="$(lds_project)" + local project + project="$(lds_project)" local profiles="" if [[ -r "$ENV_DOCKER" ]]; then @@ -2233,8 +2200,8 @@ _status_core() { # containers (Name \t Service \t State \t Health) local -a ctrs=() mapfile -t ctrs < <( - docker_compose ps -a --format '{{.Name}}\t{{.Service}}\t{{.State}}\t{{.Health}}' 2>/dev/null \ - | sed '/^[[:space:]]*$/d' + docker_compose ps -a --format '{{.Name}}\t{{.Service}}\t{{.State}}\t{{.Health}}' 2>/dev/null | + sed '/^[[:space:]]*$/d' ) # urls @@ -2270,7 +2237,7 @@ _status_core() { local s="$1" case "$s" in running) printf '%s' "$GREEN" ;; - exited|dead) printf '%s' "$RED" ;; + exited | dead) printf '%s' "$RED" ;; restarting) printf '%s' "$YELLOW" ;; *) printf '%s' "$DIM" ;; esac @@ -2294,7 +2261,7 @@ _status_core() { local total running total=${#ctrs[@]} running=0 - if (( total )); then + if ((total)); then local line _n _svc st _h for line in "${ctrs[@]}"; do IFS=$'\t' read -r _n _svc st _h <<<"$line" @@ -2303,7 +2270,7 @@ _status_core() { fi # ---- JSON mode ---- - if (( json )); then + if ((json)); then printf '{' printf '"project":"%s",' "$(_json_escape "$project")" printf '"profiles":"%s",' "$(_json_escape "$profiles")" @@ -2313,16 +2280,16 @@ _status_core() { local first=1 line name svc state health for line in "${ctrs[@]}"; do IFS=$'\t' read -r name svc state health <<<"$line" - (( first )) || printf ',' + ((first)) || printf ',' first=0 local health_disp="${health:-}" [[ -z "$health_disp" || "$health_disp" == "null" ]] && health_disp='-' printf '{' - printf '"name":"%s",' "$(_json_escape "$name")" + printf '"name":"%s",' "$(_json_escape "$name")" printf '"service":"%s",' "$(_json_escape "$svc")" - printf '"state":"%s",' "$(_json_escape "$state")" + printf '"state":"%s",' "$(_json_escape "$state")" printf '"health":"%s",' "$(_json_escape "$health_disp")" printf '"health_icon":"%s"' "$(_json_escape "$(_health_icon "$health_disp")")" printf '}' @@ -2336,7 +2303,7 @@ _status_core() { local u for u in "${urls[@]}"; do [[ -n "$u" ]] || continue - (( first )) || printf ',' + ((first)) || printf ',' first=0 printf '"%s"' "$(_json_escape "$u")" done @@ -2347,7 +2314,7 @@ _status_core() { fi # ---- Quiet mode ---- - if (( quiet )); then + if ((quiet)); then return 0 fi @@ -2365,14 +2332,14 @@ _status_core() { for line in "${ctrs[@]}"; do IFS=$'\t' read -r name svc state health <<<"$line" ((${#name} > w_name)) && w_name=${#name} - ((${#svc} > w_svc)) && w_svc=${#svc} + ((${#svc} > w_svc)) && w_svc=${#svc} done - (( w_name > 34 )) && w_name=34 - (( w_svc > 18 )) && w_svc=18 + ((w_name > 34)) && w_name=34 + ((w_svc > 18)) && w_svc=18 printf " %b%-*s%b %b%-*s%b %b%-10s%b %b%s%b\n" \ "$BOLD" "$w_name" "NAME" "$NC" \ - "$BOLD" "$w_svc" "SERVICE" "$NC" \ + "$BOLD" "$w_svc" "SERVICE" "$NC" \ "$BOLD" "STATE" "$NC" \ "$BOLD" "HEALTH" "$NC" @@ -2390,11 +2357,12 @@ _status_core() { local health_disp="${health:-}" [[ -z "$health_disp" || "$health_disp" == "null" ]] && health_disp='-' - local hi; hi="$(_health_icon "$health_disp")" + local hi + hi="$(_health_icon "$health_disp")" printf " %-*s %-*s %b%-10s%b %b%s %s%b\n" \ "$w_name" "$name_disp" \ - "$w_svc" "$svc_disp" \ + "$w_svc" "$svc_disp" \ "$stc" "${state:-'-'}" "$NC" \ "$hc" "$hi" "$health_disp" "$NC" done @@ -2418,15 +2386,21 @@ cmd_status() { local json=0 quiet=0 while [[ "${1:-}" ]]; do case "$1" in - --json) json=1; shift ;; - --quiet|-q) quiet=1; shift ;; + --json) + json=1 + shift + ;; + --quiet | -q) + quiet=1 + shift + ;; *) break ;; esac done # JSON output is intentionally scoped to core stack info only. _status_core "$json" "$quiet" - if (( json )) || (( quiet )); then + if ((json)) || ((quiet)); then return 0 fi @@ -2487,10 +2461,22 @@ cmd_logs() { local svc="" follow=0 since="" grep_pat="" while [[ "${1:-}" ]]; do case "$1" in - -f|--follow) follow=1; shift ;; - --since) since="${2:-}"; shift 2 ;; - --grep) grep_pat="${2:-}"; shift 2 ;; - *) svc="${1:-}"; shift ;; + -f | --follow) + follow=1 + shift + ;; + --since) + since="${2:-}" + shift 2 + ;; + --grep) + grep_pat="${2:-}" + shift 2 + ;; + *) + svc="${1:-}" + shift + ;; esac done @@ -2499,7 +2485,8 @@ cmd_logs() { [[ -n "$since" ]] && args+=("--since" "$since") if [[ -n "$svc" ]]; then - local s; s="$(resolve_service "$svc" || true)" + local s + s="$(resolve_service "$svc" || true)" [[ -n "$s" ]] || die "Unknown service: $svc" if [[ -n "$grep_pat" ]]; then docker_compose logs "${args[@]}" "$s" 2>&1 | text_grep "$grep_pat" @@ -2520,14 +2507,14 @@ cmd_open() { [[ -n "$target" ]] || die "open " local url="" case "${target,,}" in - mail|mailpit|webmail) url="https://webmail.localhost" ;; - db|cloudbeaver) url="https://db.localhost" ;; - redis|redisinsight|redis-insight|rds) url="http://ri.localhost" ;; - mongo|me|mongoexpress|mongo-express) url="http://me.localhost" ;; - kibana|kbn) url="http://kibana.localhost" ;; - *) - url="https://${target}" - ;; + mail | mailpit | webmail) url="https://webmail.localhost" ;; + db | cloudbeaver) url="https://db.localhost" ;; + redis | redisinsight | redis-insight | rds) url="http://ri.localhost" ;; + mongo | me | mongoexpress | mongo-express) url="http://me.localhost" ;; + kibana | kbn) url="http://kibana.localhost" ;; + *) + url="https://${target}" + ;; esac open_url "$url" } @@ -2557,40 +2544,40 @@ cmd_profiles() { local action="${1:-list}" shift || true case "${action,,}" in - list|"") - local cur="" - [[ -r "$ENV_DOCKER" ]] && cur="$(grep -E '^COMPOSE_PROFILES=' "$ENV_DOCKER" | tail -n1 | cut -d= -f2- | tr -d '\r' || true)" - printf "%bEnabled profiles:%b %s + list | "") + local cur="" + [[ -r "$ENV_DOCKER" ]] && cur="$(grep -E '^COMPOSE_PROFILES=' "$ENV_DOCKER" | tail -n1 | cut -d= -f2- | tr -d '\r' || true)" + printf "%bEnabled profiles:%b %s " "$CYAN" "$NC" "${cur:-}" - printf "%bAvailable profiles:%b + printf "%bAvailable profiles:%b " "$CYAN" "$NC" - printf ' - %s + printf ' - %s ' "${SERVICES[@]}" | LC_ALL=C sort -u - # warn if enabled profile has no mention in compose - if [[ -n "$cur" ]]; then - local p - IFS=',' read -r -a __ps <<<"$cur" - for p in "${__ps[@]}"; do - p="${p//[[:space:]]/}" - [[ -n "$p" ]] || continue - _known_profile "$p" || printf "%b[warn]%b enabled profile '%s' has no matching services in compose + # warn if enabled profile has no mention in compose + if [[ -n "$cur" ]]; then + local p + IFS=',' read -r -a __ps <<<"$cur" + for p in "${__ps[@]}"; do + p="${p//[[:space:]]/}" + [[ -n "$p" ]] || continue + _known_profile "$p" || printf "%b[warn]%b enabled profile '%s' has no matching services in compose " "$YELLOW" "$NC" "$p" - done - fi - ;; - add) - [[ $# -gt 0 ]] || die "profiles add " - for p in "$@"; do - modify_profiles add "$p" done - ;; - remove|rm|del) - [[ $# -gt 0 ]] || die "profiles remove " - modify_profiles remove "$@" - ;; - *) - die "profiles " - ;; + fi + ;; + add) + [[ $# -gt 0 ]] || die "profiles add " + for p in "$@"; do + modify_profiles add "$p" + done + ;; + remove | rm | del) + [[ $# -gt 0 ]] || die "profiles remove " + modify_profiles remove "$@" + ;; + *) + die "profiles " + ;; esac } @@ -2611,41 +2598,47 @@ cmd_diag() { shift || true case "${sub,,}" in - dns) - local dom="${1:-}"; [[ -n "$dom" ]] || die "diag dns " - local qdom; qdom="$(_shq "$dom")" - _tools_exec "dig +short $qdom; echo; nslookup $qdom 2>/dev/null || true; echo; getent hosts $qdom 2>/dev/null || true" - ;; - route|net) - _tools_exec "ip r; echo; ip a; echo; ss -tulpen 2>/dev/null || netstat -tulpen 2>/dev/null || true" - ;; - tcp) - local h="${1:-}"; local p="${2:-}" - [[ -n "$h" && -n "$p" ]] || die "diag tcp " - _tools_exec "nc -vz -w2 $(_shq "$h") $(_shq "$p")" - ;; - http) - local url="${1:-}"; shift || true - [[ -n "$url" ]] || die "diag http [curl-args...]" - local -a qargs=() - local a - for a in "$@"; do qargs+=("$(printf '%q' "$a")"); done - _tools_exec "curl -vkI $(_shq "$url") ${qargs[*]}" - ;; - tls) - local dom="${1:-}"; [[ -n "$dom" ]] || die "diag tls " - local qdom; qdom="$(_shq "$dom")" - _tools_exec "echo | openssl s_client -connect ${qdom}:443 -servername $qdom -showcerts 2>/dev/null | sed -n '1,60p'" - ;; - *) - die "diag " - ;; + dns) + local dom="${1:-}" + [[ -n "$dom" ]] || die "diag dns " + local qdom + qdom="$(_shq "$dom")" + _tools_exec "dig +short $qdom; echo; nslookup $qdom 2>/dev/null || true; echo; getent hosts $qdom 2>/dev/null || true" + ;; + route | net) + _tools_exec "ip r; echo; ip a; echo; ss -tulpen 2>/dev/null || netstat -tulpen 2>/dev/null || true" + ;; + tcp) + local h="${1:-}" + local p="${2:-}" + [[ -n "$h" && -n "$p" ]] || die "diag tcp " + _tools_exec "nc -vz -w2 $(_shq "$h") $(_shq "$p")" + ;; + http) + local url="${1:-}" + shift || true + [[ -n "$url" ]] || die "diag http [curl-args...]" + local -a qargs=() + local a + for a in "$@"; do qargs+=("$(printf '%q' "$a")"); done + _tools_exec "curl -vkI $(_shq "$url") ${qargs[*]}" + ;; + tls) + local dom="${1:-}" + [[ -n "$dom" ]] || die "diag tls " + local qdom + qdom="$(_shq "$dom")" + _tools_exec "echo | openssl s_client -connect ${qdom}:443 -servername $qdom -showcerts 2>/dev/null | sed -n '1,60p'" + ;; + *) + die "diag " + ;; esac } - cmd_sniff() { - local url="${1:-}"; shift || true + local url="${1:-}" + shift || true [[ -n "$url" ]] || die "sniff [curl-args...]" local -a qargs=() local a @@ -2661,32 +2654,34 @@ cmd_secrets() { docker exec -it SERVER_TOOLS senv "$@"; } cmd_cert() { docker exec -it SERVER_TOOLS certify "$@"; } cmd_host() { - local sub="${1:-}"; shift || true + local sub="${1:-}" + shift || true case "${sub,,}" in - add) - setup_domain - ;; - rm|remove|del|delete) - delete_domain "$@" - ;; - list) - shopt -s nullglob - for f in "$DIR/configuration/nginx/"*.conf; do - printf '%s + add) + setup_domain + ;; + rm | remove | del | delete) + delete_domain "$@" + ;; + list) + shopt -s nullglob + for f in "$DIR/configuration/nginx/"*.conf; do + printf '%s ' "$(basename -- "$f" .conf)" - done - shopt -u nullglob - ;; - *) - die "host " - ;; + done + shopt -u nullglob + ;; + *) + die "host " + ;; esac } cmd_ui() { docker exec -it SERVER_TOOLS lazydocker; } cmd_runtime() { - local which="${1:-}"; [[ -n "$which" ]] || die "runtime " + local which="${1:-}" + [[ -n "$which" ]] || die "runtime " local f="" for f in "$DIR/runtime-versions.json" "$DIR/docker/runtime-versions.json" "$DIR/configuration/runtime-versions.json"; do [[ -r "$f" ]] && break || f="" @@ -2703,9 +2698,11 @@ cmd_runtime() { # 6f. EXEC / CHECK / EVENTS / CLEAN / ENV / VERIFY / DISK # ───────────────────────────────────────────────────────────────────────────── cmd_exec() { - local svc="${1:-}"; shift || true + local svc="${1:-}" + shift || true [[ -n "$svc" ]] || die "exec [cmd...]" - local s; s="$(resolve_service "$svc" || true)" + local s + s="$(resolve_service "$svc" || true)" [[ -n "$s" ]] || die "Unknown service: $svc" if [[ $# -gt 0 ]]; then docker_compose exec "$s" "$@" @@ -2715,37 +2712,42 @@ cmd_exec() { } cmd_check() { - local sub="${1:-}"; shift || true + local sub="${1:-}" + shift || true case "${sub,,}" in - upstream) - local dom="${1:-}"; [[ -n "$dom" ]] || die "check upstream " - local nconf="$DIR/configuration/nginx/$dom.conf" - [[ -r "$nconf" ]] || die "No nginx conf for domain: $dom" - printf "%bDomain:%b %s + upstream) + local dom="${1:-}" + [[ -n "$dom" ]] || die "check upstream " + local nconf="$DIR/configuration/nginx/$dom.conf" + [[ -r "$nconf" ]] || die "No nginx conf for domain: $dom" + printf "%bDomain:%b %s " "$CYAN" "$NC" "$dom" - if grep -q fastcgi_pass "$nconf"; then - local php; php="$(grep -Eo 'fastcgi_pass ([^:]+):9000' "$nconf" | awk '{print $2}' | sed 's/:9000$//' | head -n1 || true)" - printf "Upstream (php): %s + if grep -q fastcgi_pass "$nconf"; then + local php + php="$(grep -Eo 'fastcgi_pass ([^:]+):9000' "$nconf" | awk '{print $2}' | sed 's/:9000$//' | head -n1 || true)" + printf "Upstream (php): %s " "${php:-unknown}" - [[ -n "$php" ]] && _tools_exec "nc -vz -w2 "$php" 9000 || true" - elif grep -q proxy_pass "$nconf"; then - local up; up="$(grep -m1 -Eo 'proxy_pass[[:space:]]+http://[^;]+' "$nconf" | awk '{print $2}' | sed 's|^http://||' || true)" - printf "Upstream (http): %s + [[ -n "$php" ]] && _tools_exec "nc -vz -w2 "$php" 9000 || true" + elif grep -q proxy_pass "$nconf"; then + local up + up="$(grep -m1 -Eo 'proxy_pass[[:space:]]+http://[^;]+' "$nconf" | awk '{print $2}' | sed 's|^http://||' || true)" + printf "Upstream (http): %s " "${up:-unknown}" - local h="${up%%:*}" p="${up##*:}" - [[ -n "$h" && -n "$p" && "$h" != "$up" ]] && _tools_exec "nc -vz -w2 "$h" "$p" || true" - fi - _tools_exec "curl -vkI "https://$dom" || true" - ;; - *) - die "check upstream " - ;; + local h="${up%%:*}" p="${up##*:}" + [[ -n "$h" && -n "$p" && "$h" != "$up" ]] && _tools_exec "nc -vz -w2 "$h" "$p" || true" + fi + _tools_exec "curl -vkI "https://$dom" || true" + ;; + *) + die "check upstream " + ;; esac } cmd_events() { local since="${1:-1h}" - local project; project="$(lds_project)" + local project + project="$(lds_project)" docker events --since "$since" --filter "label=com.docker.compose.project=$project" } @@ -2753,13 +2755,20 @@ cmd_clean() { local yes=0 vols=0 while [[ "${1:-}" ]]; do case "$1" in - --yes|-y) yes=1; shift ;; - --volumes|-v) vols=1; shift ;; - *) shift ;; + --yes | -y) + yes=1 + shift + ;; + --volumes | -v) + vols=1 + shift + ;; + *) shift ;; esac done ((yes)) || die "clean requires --yes" - local project; project="$(lds_project)" + local project + project="$(lds_project)" # remove stopped containers for this project docker rm -f $(docker ps -a --filter "label=com.docker.compose.project=$project" --filter "status=exited" -q) 2>/dev/null || true if ((vols)); then @@ -2779,7 +2788,11 @@ cmd_verify() { while IFS= read -r d; do [[ -n "$d" ]] || continue _tools_exec "curl -skI "https://$d" | head -n 1" - done < <(shopt -s nullglob; for f in "$DIR/configuration/nginx/"*.conf; do basename -- "$f" .conf; done; shopt -u nullglob) + done < <( + shopt -s nullglob + for f in "$DIR/configuration/nginx/"*.conf; do basename -- "$f" .conf; done + shopt -u nullglob + ) } # ───────────────────────────────────────────────────────────────────────────── @@ -2994,11 +3007,11 @@ cmd_tools() { local sub="${1:-sh}" shift || true case "${sub,,}" in - sh|shell|"") + sh | shell | "") docker_shell SERVER_TOOLS ;; exec) - [[ $# -gt 0 ]] || die "tools exec """ + [[ $# -gt 0 ]] || die "tools exec " "" docker exec -it SERVER_TOOLS sh -lc "$*" ;; file) @@ -3043,103 +3056,90 @@ cmd_cli() { cmd_core() { # Usage: - # lds core -> open correct container for that domain (PHP/Node) - # lds core -> list domains and let user pick - - local domain="${1:-}" - if [[ -z "$domain" ]]; then - # No domain supplied: show an indexed list so selection is explicit and reliable. - domain="$(core_pick_domain)" || { - local rc=$? - ((rc == 130)) && return 130 - die "No domain selected" - } - fi - - local nconf="$DIR/configuration/nginx/$domain.conf" - [[ -f "$nconf" ]] || die "No Nginx config for $domain" - - # Node vhost? - if grep -Eq 'proxy_pass[[:space:]]+http://node_[A-Za-z0-9._-]+:[0-9]+' "$nconf"; then - launch_node "$domain" - return 0 - fi - - # Otherwise keep legacy PHP behavior - launch_php "$domain" - return 0 -} + # lds core -> open correct container for that domain (PHP/Node) + # lds core -> open a shell in that container + # lds core -> list domains and let user pick -core_pick_domain() { - local -a domains=() - local f d + local target="${1:-}" - shopt -s nullglob - for f in "$DIR/configuration/nginx/"*.conf; do - d="$(basename -- "$f" .conf)" - [[ -n "$d" ]] && domains+=("$d") - done - shopt -u nullglob + # domain regex (same as domain-which/mkhost family) + local re='^([a-zA-Z0-9]([-a-zA-Z0-9]{0,61}[a-zA-Z0-9])?\.)+(localhost|local|test|loc|[a-zA-Z]{2,})$' - ((${#domains[@]} > 0)) || die "No domains found in $DIR/configuration/nginx" + # If no target -> prompt from domain-which list + if [[ -z "$target" ]]; then + local -a domains=() + mapfile -t domains < <(docker exec SERVER_TOOLS domain-which --list-domains 2>/dev/null | sed '/^[[:space:]]*$/d' || true) - # stable ordering - IFS=$'\n' domains=($(printf '%s\n' "${domains[@]}" | LC_ALL=C sort -u)) + ((${#domains[@]} > 0)) || die "No domains found" - # If there's only one domain, just use it. - if ((${#domains[@]} == 1)); then - printf '%s' "${domains[0]}" - return 0 - fi + # stable ordering + IFS=$'\n' domains=($(printf '%s\n' "${domains[@]}" | LC_ALL=C sort -u)) - # Must be interactive to pick. - if [[ ! -t 0 ]]; then - printf "%b[core]%b No domain provided. Available domains:\n" "$YELLOW" "$NC" >&2 - local i=1 - for d in "${domains[@]}"; do - printf " %2d) %s\n" "$i" "$d" >&2 - ((i++)) - done - die "No TTY to prompt. Use: lds core " - fi - - printf "%bSelect domain:%b\n" "$CYAN" "$NC" >&2 - local i=1 - for d in "${domains[@]}"; do - printf " %2d) %s\n" "$i" "$d" >&2 - ((i++)) - done - printf " %2d) %s\n" 0 "Cancel" >&2 + if ((${#domains[@]} == 1)); then + target="${domains[0]}" + else + if [[ ! -t 0 ]]; then + printf "%b[core]%b No domain provided. Available domains:\n" "$YELLOW" "$NC" >&2 + local i=1 + local d + for d in "${domains[@]}"; do + printf " %2d) %s\n" "$i" "$d" >&2 + ((i++)) + done + die "No TTY to prompt. Use: lds core " + fi - local ans idx - while true; do - printf "%bDomain #%b " "$GREEN" "$NC" >&2 - tty_readline ans "" || return 130 - ans="${ans//[[:space:]]/}" - [[ -n "$ans" ]] || continue + printf "%bSelect domain:%b\n" "$CYAN" "$NC" >&2 + local i=1 d + for d in "${domains[@]}"; do + printf " %b%2d)%b %s\n" "$CYAN" "$i" "$NC" "$d" >&2 + ((i++)) + done - if [[ "$ans" == "0" ]]; then - return 130 + local ans="" + while true; do + read -r -p "Enter number (1-${#domains[@]}): " ans + ans="$(echo "$ans" | xargs)" + [[ "$ans" =~ ^[0-9]+$ ]] || { + printf "%bInvalid input.%b\n" "$YELLOW" "$NC" >&2 + continue + } + ((ans >= 1 && ans <= ${#domains[@]})) || { + printf "%bOut of range.%b\n" "$YELLOW" "$NC" >&2 + continue + } + target="${domains[$((ans - 1))]}" + break + done fi + fi - if [[ "$ans" =~ ^[0-9]+$ ]]; then - idx=$((ans - 1)) - if ((idx >= 0 && idx < ${#domains[@]})); then - printf '%s' "${domains[$idx]}" - return 0 + # If target looks like a domain -> resolve via domain-which then shell in + if [[ "$target" =~ $re ]]; then + local info type container profile docroot + info="$(docker exec SERVER_TOOLS domain-which "$target" 2>/dev/null)" || die "Unknown domain: $target" + IFS=$' \t' read -r type container profile docroot <<<"$info" + [[ -n "${container:-}" ]] || die "No container resolved for: $target" + + local wd="/app" + case "$type" in + node) wd="/app" ;; + php) + if [[ -n "${docroot:-}" && "${docroot:-}" == /* ]]; then + wd="/app${docroot}" + else + wd="/app" fi - else - # allow typing domain directly - for d in "${domains[@]}"; do - if [[ "$d" == "$ans" ]]; then - printf '%s' "$d" - return 0 - fi - done - fi + ;; + *) wd="/app" ;; + esac - printf "%bInvalid selection.%b\n" "$YELLOW" "$NC" >&2 - done + docker exec -it "$container" bash -lc "cd \"$wd\" 2>/dev/null || cd /app 2>/dev/null || cd /; exec bash" + return 0 + fi + + # Otherwise treat target as a container name + docker exec -it "$target" bash } cmd_setup() { @@ -3864,9 +3864,9 @@ cmd_run() { "${publish[@]}" -- "${mounts[@]}" fi - _run_runtime_summary "$name" + _run_runtime_summary "$name" -# "shell" enters the container; "*" / "build" does not. + # "shell" enters the container; "*" / "build" does not. if [[ "$action" == "shell" ]]; then run_exec_shell "$name" else @@ -3878,7 +3878,6 @@ cmd_run() { esac } - ############################################################################### # 6w. NEW FEATURES: stack diff | domain export/import | support trace ############################################################################### @@ -3889,13 +3888,20 @@ cmd_stack_diff() { local show_config=0 while [[ "${1:-}" ]]; do case "$1" in - --json) json=1; shift ;; - --config) show_config=1; shift ;; - *) break ;; + --json) + json=1 + shift + ;; + --config) + show_config=1 + shift + ;; + *) break ;; esac done - local project; project="$(lds_project)" + local project + project="$(lds_project)" local cfg_json="" if docker_compose config --format json >/dev/null 2>&1; then @@ -3914,8 +3920,8 @@ cmd_stack_diff() { local img="${line#*|}" running["$svc"]="$img" done < <(docker ps \ - --filter "label=com.docker.compose.project=$project" \ - --format '{{index .Labels "com.docker.compose.service"}}|{{.Image}}' 2>/dev/null || true) + --filter "label=com.docker.compose.project=$project" \ + --format '{{index .Labels "com.docker.compose.service"}}|{{.Image}}' 2>/dev/null || true) # desired: service -> image/build context (best-effort) declare -A desired_img=() @@ -3953,7 +3959,8 @@ cmd_stack_diff() { if ((json)); then if has_tool jq; then # assemble in bash -> jq - local tmp; tmp="$(mktemp)" + local tmp + tmp="$(mktemp)" { printf '{' printf '"project":%s,' "$(printf '%s' "$project" | jq -Rsa .)" @@ -4028,10 +4035,14 @@ cmd_stack_diff() { local r="${running[$svc]:-}" local d="${desired_img[$svc]:-}" local st - if [[ -z "$r" ]]; then st="(not running)" - elif [[ -z "$d" ]]; then st="(not in config)" - elif [[ "$r" == "$d" ]]; then st="OK" - else st="DIFF" + if [[ -z "$r" ]]; then + st="(not running)" + elif [[ -z "$d" ]]; then + st="(not in config)" + elif [[ "$r" == "$d" ]]; then + st="OK" + else + st="DIFF" fi printf "%-22s %-40.40s %-40.40s %s\n" "$svc" "$r" "$d" "$st" done @@ -4046,16 +4057,23 @@ cmd_domain_export() { local out="" pretty=1 while [[ "${1:-}" ]]; do case "$1" in - --out) out="${2:-}"; shift 2 ;; - --compact) pretty=0; shift ;; - *) break ;; + --out) + out="${2:-}" + shift 2 + ;; + --compact) + pretty=0 + shift + ;; + *) break ;; esac done local dir_ng="$DIR/configuration/nginx" [[ -d "$dir_ng" ]] || die "nginx vhost dir not found: $dir_ng" - local tmp; tmp="$(mktemp)" + local tmp + tmp="$(mktemp)" printf '{ "version": 1, "exported_at": "%s", "root": "%s", "domains": [' "$(date -Iseconds)" "$DIR" >"$tmp" local first=1 f dom typ upstream cmb @@ -4119,7 +4137,7 @@ cmd_domain_import() { b64="$(jq -r --arg d "$dom" '.domains[] | select(.domain==$d) | .files.nginx_conf' "$in")" [[ -n "$b64" && "$b64" != "null" ]] || continue printf '%s' "$b64" | base64 -d >"$dir_ng/$dom.conf" - count=$((count+1)) + count=$((count + 1)) done < <(jq -r '.domains[].domain' "$in") ok "[ok] domain import: restored $count nginx vhost(s) into $dir_ng\n" @@ -4164,10 +4182,12 @@ cmd_support_trace() { printf "\n%b[Upstream]%b\n" "$DIM" "$NC" if [[ -r "$nconf" ]]; then if grep -q fastcgi_pass "$nconf"; then - local php; php="$(grep -Eo 'fastcgi_pass[[:space:]]+[^;]+' "$nconf" | awk '{print $2}' | head -n1 || true)" + local php + php="$(grep -Eo 'fastcgi_pass[[:space:]]+[^;]+' "$nconf" | awk '{print $2}' | head -n1 || true)" printf "type=php\nfastcgi_pass=%s\n" "${php:-unknown}" elif grep -q proxy_pass "$nconf"; then - local up; up="$(grep -m1 -Eo 'proxy_pass[[:space:]]+http[s]?://[^;]+' "$nconf" | awk '{print $2}' | head -n1 || true)" + local up + up="$(grep -m1 -Eo 'proxy_pass[[:space:]]+http[s]?://[^;]+' "$nconf" | awk '{print $2}' | head -n1 || true)" printf "type=proxy\nproxy_pass=%s\n" "${up:-unknown}" else printf "type=static\n" @@ -4188,58 +4208,61 @@ cmd_support_trace() { ############################################################################### cmd_stack() { - local sub="${1:-}"; shift || true + local sub="${1:-}" + shift || true case "${sub,,}" in - ""|help|-h|--help) cmd_help stack ;; - up) cmd_up "$@" ;; - start) cmd_start "$@" ;; - down|stop) cmd_down "$@" ;; - restart|reboot) cmd_restart "$@" ;; - status) cmd_status "$@" ;; - ps) cmd_ps "$@" ;; - logs) cmd_logs "$@" ;; - exec) cmd_exec "$@" ;; - events) cmd_events "$@" ;; - clean) cmd_clean "$@" ;; - verify) cmd_verify "$@" ;; - config) cmd_config "$@" ;; diff) cmd_stack_diff "$@" ;; - - http) cmd_http "$@" ;; - *) - die "stack " - ;; + "" | help | -h | --help) cmd_help stack ;; + up) cmd_up "$@" ;; + start) cmd_start "$@" ;; + down | stop) cmd_down "$@" ;; + restart | reboot) cmd_restart "$@" ;; + status) cmd_status "$@" ;; + ps) cmd_ps "$@" ;; + logs) cmd_logs "$@" ;; + exec) cmd_exec "$@" ;; + events) cmd_events "$@" ;; + clean) cmd_clean "$@" ;; + verify) cmd_verify "$@" ;; + config) cmd_config "$@" ;; diff) cmd_stack_diff "$@" ;; + + http) cmd_http "$@" ;; + *) + die "stack " + ;; esac } # Canonical: domain. Legacy: host. cmd_domain() { - local sub="${1:-}"; shift || true + local sub="${1:-}" + shift || true case "${sub,,}" in - ""|help|-h|--help) die "domain " ;; - add) cmd_host add "$@" ;; - rm|remove|del|delete) cmd_host rm "$@" ;; - ls|list) cmd_host list "$@" ;; - export) cmd_domain_export "$@" ;; - import) cmd_domain_import "$@" ;; - check) cmd_check upstream "$@" ;; - nginx) cmd_nginx "$@" ;; - *) - die "domain " - ;; + "" | help | -h | --help) die "domain " ;; + add) cmd_host add "$@" ;; + rm | remove | del | delete) cmd_host rm "$@" ;; + ls | list) cmd_host list "$@" ;; + export) cmd_domain_export "$@" ;; + import) cmd_domain_import "$@" ;; + check) cmd_check upstream "$@" ;; + nginx) cmd_nginx "$@" ;; + *) + die "domain " + ;; esac } cmd_support() { - local sub="${1:-}"; shift || true + local sub="${1:-}" + shift || true case "${sub,,}" in - ""|help|-h|--help) die "support " ;; - open) cmd_open "$@" ;; - bundle) cmd_bundle "$@" ;; - notify) cmd_notify "$@" ;; - ui) cmd_ui "$@" ;; - *) - die "support " - ;; + "" | help | -h | --help) die "support " ;; + open) cmd_open "$@" ;; + bundle) cmd_bundle "$@" ;; + notify) cmd_notify "$@" ;; + ui) cmd_ui "$@" ;; + *) + die "support " + ;; esac } @@ -4252,17 +4275,29 @@ cmd_bundle() { local out="${1:-}" while [[ "${1:-}" ]]; do case "$1" in - --redact) mode="redact"; shift ;; - --full) mode="full"; shift ;; - *.zip) out="$1"; shift ;; - *) break ;; + --redact) + mode="redact" + shift + ;; + --full) + mode="full" + shift + ;; + *.zip) + out="$1" + shift + ;; + *) break ;; esac done need zip - local ts; ts="$(date +%Y%m%d_%H%M%S)" - local project; project="$(lds_project)" - local tmp; tmp="$(mktemp -d "${TMPDIR:-/tmp}/lds_bundle.XXXXXX")" + local ts + ts="$(date +%Y%m%d_%H%M%S)" + local project + project="$(lds_project)" + local tmp + tmp="$(mktemp -d "${TMPDIR:-/tmp}/lds_bundle.XXXXXX")" local base="lds_bundle_${project}_${ts}" [[ -n "$out" ]] || out="$PWD/${base}.zip" @@ -4498,21 +4533,23 @@ ${CYAN}Help:${NC} EOF } - ############################################################################### # 7. MAIN ############################################################################### main() { - [[ $# -gt 0 ]] || { cmd_help; exit 1; } + [[ $# -gt 0 ]] || { + cmd_help + exit 1 + } while [[ $# -gt 0 ]]; do case "$1" in - -v|--verbose) + -v | --verbose) VERBOSE=1 QUIET=0 shift ;; - -q|--quiet) + -q | --quiet) QUIET=1 VERBOSE=0 shift @@ -4525,7 +4562,7 @@ main() { shift break ;; - -h|--help) + -h | --help) cmd_help exit 0 ;; @@ -4534,7 +4571,10 @@ main() { esac done - [[ $# -gt 0 ]] || { cmd_help; exit 1; } + [[ $# -gt 0 ]] || { + cmd_help + exit 1 + } local cmd="${1,,}" shift || true @@ -4548,12 +4588,12 @@ main() { need docker case "$cmd" in - php|composer|node|npm|npx) exec "$DIR/bin/$cmd" "$@" ;; - pg|pg_restore|pg-restore|pgrestore|pg_dump|pgdump|pg-dump|psql) exec "$DIR/bin/pg" "$@" ;; - maria|mariadb|mariadbdump|mariadb-dump|mariadb_dump) exec "$DIR/bin/maria" "$@" ;; - my|mysql|mysqldump|mysql-dump|mysql_dump) exec "$DIR/bin/my" "$@" ;; - redis|redis-cli) exec "$DIR/bin/redis-cli" "$@" ;; - *) "cmd_$cmd" "$@" ;; + php | composer | node | npm | npx) exec "$DIR/bin/$cmd" "$@" ;; + pg | pg_restore | pg-restore | pgrestore | pg_dump | pgdump | pg-dump | psql) exec "$DIR/bin/pg" "$@" ;; + maria | mariadb | mariadbdump | mariadb-dump | mariadb_dump) exec "$DIR/bin/maria" "$@" ;; + my | mysql | mysqldump | mysql-dump | mysql_dump) exec "$DIR/bin/my" "$@" ;; + redis | redis-cli) exec "$DIR/bin/redis-cli" "$@" ;; + *) "cmd_$cmd" "$@" ;; esac } From 08e5fbb460b556d06fd6e56fe09df222d2962552 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sat, 7 Mar 2026 13:05:26 +0600 Subject: [PATCH 31/48] lds core --- docker/compose/php.yaml | 4 ---- lds | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/docker/compose/php.yaml b/docker/compose/php.yaml index 9461cb5..3a35b8d 100644 --- a/docker/compose/php.yaml +++ b/docker/compose/php.yaml @@ -4,10 +4,6 @@ x-php-service: &php-service - TZ=${TZ:-} env_file: - "../../.env" - networks: - frontend: {} - backend: {} - datastore: {} volumes: - "${PROJECT_DIR:-./../../../application}:/app" - lds_ssl_roots:/etc/share/rootCA:ro diff --git a/lds b/lds index 941d04b..9292286 100755 --- a/lds +++ b/lds @@ -3139,7 +3139,7 @@ cmd_core() { fi # Otherwise treat target as a container name - docker exec -it "$target" bash + docker exec -it "$(printf '%s' "$target" | tr '[:lower:]' '[:upper:]')" sh -lc 'exec bash -i || exec sh' } cmd_setup() { From 2e049dfca7c1bd3c21f9c7b9a982601e80926af5 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sat, 7 Mar 2026 21:55:50 +0600 Subject: [PATCH 32/48] lds core --- bin/composer | 521 ++++++++++++++++++++++++++++++++++---------------- bin/es | 434 +++++++++++++++++++++++++++++++++++++++++ bin/maria | 24 ++- bin/mongo | 381 ++++++++++++++++++++++++++++++++++++ bin/my | 453 ++++++++++++++++++++----------------------- bin/pg | 470 ++++++++++++++++++++++++--------------------- bin/php | 268 ++++++++++++++++---------- bin/redis-cli | 27 ++- lds | 4 +- 9 files changed, 1844 insertions(+), 738 deletions(-) create mode 100644 bin/es create mode 100644 bin/mongo diff --git a/bin/composer b/bin/composer index 292f617..745be1f 100755 --- a/bin/composer +++ b/bin/composer @@ -1,24 +1,21 @@ #!/usr/bin/env bash set -euo pipefail +# Composer runner for LocalDevStack +# - Prefers running PHP_X.Y containers +# - Falls back to SERVER_TOOLS when no PHP container satisfies the request +# - Runs composer in a throwaway container using the selected image +# - Shares the selected container network namespace + die() { echo "Error: $*" >&2 exit 1 } -# ───────────────────────────────────────────────────────────────────────────── -# Repo layout: -# /bin/composer (this script) -# /docker/.env -# /configuration/ssh -# ───────────────────────────────────────────────────────────────────────────── ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ENV_FILE="$ROOT_DIR/docker/.env" SSH_DIR="$ROOT_DIR/configuration/ssh" -# ───────────────────────────────────────────────────────────────────────────── -# Pretty output (auto-disable when stderr isn't a TTY) -# ───────────────────────────────────────────────────────────────────────────── if [[ -t 2 ]]; then C_RESET=$'\033[0m' C_CYAN=$'\033[0;36m' @@ -34,19 +31,6 @@ kv() { printf "%s%s%s %s\n" "$C_CYAN" "$title" "$C_RESET" "$value" >&2 } -# ───────────────────────────────────────────────────────────────────────────── -# Load docker/.env safely (skip UID/EUID because bash has readonly UID/EUID) -# ───────────────────────────────────────────────────────────────────────────── -if [[ -r "$ENV_FILE" ]]; then - set -o allexport - # shellcheck disable=SC1090 - source <(grep -vE '^(UID|EUID)=' "$ENV_FILE") - set +o allexport -fi - -# ───────────────────────────────────────────────────────────────────────────── -# MSYS/Git Bash detection (parser-safe) -# ───────────────────────────────────────────────────────────────────────────── is_msys() { case "${OSTYPE:-}" in msys* | cygwin*) return 0 ;; @@ -54,10 +38,6 @@ is_msys() { esac } -# ───────────────────────────────────────────────────────────────────────────── -# MSYS/Git Bash: prevent path auto-conversion when calling docker.exe -# Fixes: "-w /workspace" becoming "-w D:/Program Files/Git/workspace" -# ───────────────────────────────────────────────────────────────────────────── msys_no_pathconv() { if is_msys; then export MSYS_NO_PATHCONV=1 @@ -65,29 +45,65 @@ msys_no_pathconv() { fi } -# ───────────────────────────────────────────────────────────────────────────── -# Resolve host working directory for Docker Desktop bind mount. -# Priority: -# 1) WORKDIR_WIN (from server.bat wrapper) -# 2) WORKING_DIR (from docker/.env) -# 3) $PWD -# 4) repo root (safe fallback) -# NOTE: host path should be Windows absolute for Docker Desktop on Windows. -# ───────────────────────────────────────────────────────────────────────────── +trim() { + local s="$1" + s="${s#"${s%%[![:space:]]*}"}" + s="${s%"${s##*[![:space:]]}"}" + printf '%s' "$s" +} + +strip_quotes() { + local v + v="$(trim "$1")" + if [[ "$v" =~ ^\"(.*)\"$ ]]; then + printf '%s' "${BASH_REMATCH[1]}" + return 0 + fi + if [[ "$v" =~ ^\'(.*)\'$ ]]; then + printf '%s' "${BASH_REMATCH[1]}" + return 0 + fi + printf '%s' "$v" +} + +load_env_file() { + [[ -r "$ENV_FILE" ]] || return 0 + + local line key value + while IFS= read -r line || [[ -n "$line" ]]; do + line="$(trim "$line")" + [[ -z "$line" || "${line:0:1}" == "#" ]] && continue + [[ "$line" == export* ]] && line="$(trim "${line#export}")" + [[ "$line" == *=* ]] || continue + + key="${line%%=*}" + value="${line#*=}" + key="$(trim "$key")" + value="$(strip_quotes "$value")" + + case "$key" in + UID | EUID | '') + continue + ;; + *) + export "$key=$value" + ;; + esac + done <"$ENV_FILE" +} + resolve_host_pwd() { local host="${WORKDIR_WIN:-${WORKING_DIR:-}}" - if [[ -z "${host}" ]]; then + if [[ -z "$host" ]]; then host="$PWD" fi - [[ -n "${host}" ]] || host="$ROOT_DIR" + [[ -n "$host" ]] || host="$ROOT_DIR" if command -v cygpath >/dev/null 2>&1; then - # Convert MSYS /c/... -> C:\... for docker.exe bind mount [[ "$host" == /* ]] && host="$(cygpath -w "$host")" fi - # Guard: if launched in Git install "workspace", fallback to repo root case "$host" in *"Program Files/Git/workspace"* | *"Program Files\\Git\\workspace"*) if command -v cygpath >/dev/null 2>&1; then @@ -101,37 +117,55 @@ resolve_host_pwd() { printf '%s' "$host" } -# ───────────────────────────────────────────────────────────────────────────── -# Version helpers -# ───────────────────────────────────────────────────────────────────────────── +resolve_source_dir() { + local host="$1" + + if [[ -d "$host" ]]; then + printf '%s' "$host" + return 0 + fi + + if command -v cygpath >/dev/null 2>&1; then + if host="$(cygpath -u "$host" 2>/dev/null)" && [[ -d "$host" ]]; then + printf '%s' "$host" + return 0 + fi + fi + + printf '%s' "$PWD" +} + ver_norm() { local v="$1" - if [[ "$v" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then - local a b c - IFS='.' read -r a b c <<<"$v" - b="${b:-0}" - c="${c:-0}" - echo "${a}.${b}.${c}" - else echo "$v"; fi + local a b c + IFS='.' read -r a b c <<<"$v" + a="${a:-0}" + b="${b:-0}" + c="${c:-0}" + printf '%d.%d.%d' "$a" "$b" "$c" } + ver_cmp_ge() { local a b a="$(ver_norm "$1")" b="$(ver_norm "$2")" - [[ "$(printf "%s\n%s\n" "$b" "$a" | sort -V | tail -n1)" == "$a" ]] + [[ "$(printf '%s\n%s\n' "$b" "$a" | sort -V | tail -n1)" == "$a" ]] } + ver_cmp_gt() { local a b a="$(ver_norm "$1")" b="$(ver_norm "$2")" [[ "$a" != "$b" ]] && ver_cmp_ge "$a" "$b" } + ver_cmp_le() { local a b a="$(ver_norm "$1")" b="$(ver_norm "$2")" - [[ "$(printf "%s\n%s\n" "$a" "$b" | sort -V | head -n1)" == "$a" ]] + [[ "$(printf '%s\n%s\n' "$a" "$b" | sort -V | head -n1)" == "$a" ]] } + ver_cmp_lt() { local a b a="$(ver_norm "$1")" @@ -139,145 +173,276 @@ ver_cmp_lt() { [[ "$a" != "$b" ]] && ver_cmp_le "$a" "$b" } -read_php_constraint() { - [[ -f composer.json ]] || return 1 +version_in_range() { + local cand="$1" lower="$2" upper="$3" + ver_cmp_ge "$cand" "$lower" && ver_cmp_lt "$cand" "$upper" +} - if command -v jq >/dev/null 2>&1; then - local v - v="$(jq -r '.require.php // empty' composer.json 2>/dev/null || true)" - [[ -n "$v" && "$v" != "null" ]] && { - echo "$v" - return 0 - } +next_minor() { + local major minor + IFS='.' read -r major minor _ <<<"$(ver_norm "$1")" + printf '%d.%d.0' "$major" "$((minor + 1))" +} + +next_major() { + local major + IFS='.' read -r major _ <<<"$(ver_norm "$1")" + printf '%d.0.0' "$((major + 1))" +} + +caret_upper_bound() { + local major minor patch + IFS='.' read -r major minor patch <<<"$(ver_norm "$1")" + + if ((major > 0)); then + printf '%d.0.0' "$((major + 1))" + elif ((minor > 0)); then + printf '0.%d.0' "$((minor + 1))" + else + printf '0.0.%d' "$((patch + 1))" fi +} - if command -v python3 >/dev/null 2>&1; then - python3 - <<'PY' 2>/dev/null || true -import json -try: - with open("composer.json","r",encoding="utf-8") as f: - data=json.load(f) - v=(data.get("require") or {}).get("php") or "" - if isinstance(v,str) and v.strip(): - print(v.strip()) -except Exception: - pass -PY +constraint_atom_allows() { + local raw="$1" cand="$2" + local tok + tok="$(trim "$raw")" + [[ -z "$tok" ]] && return 0 + + tok="${tok#v}" + tok="${tok#V}" + tok="${tok%%-dev}" + tok="${tok%%+*}" + tok="${tok%%@*}" + + case "$tok" in + '*' | 'x' | 'X') return 0 + ;; + esac + + if [[ "$tok" =~ ^([0-9]+)$ ]]; then + version_in_range "$cand" "${BASH_REMATCH[1]}.0.0" "$((BASH_REMATCH[1] + 1)).0.0" + return fi - local line - line="$(grep -Eo '"php"[[:space:]]*:[[:space:]]*"[^"]+"' composer.json 2>/dev/null | head -n1 || true)" - [[ -n "$line" ]] || return 1 - echo "$line" | sed -E 's/.*"php"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/' -} + if [[ "$tok" =~ ^([0-9]+)[.](x|X|\*)$ ]]; then + version_in_range "$cand" "${BASH_REMATCH[1]}.0.0" "$((BASH_REMATCH[1] + 1)).0.0" + return + fi -constraint_allows() { - local constraint="$1" cand="$2" - local cand_full - cand_full="$(ver_norm "$cand")" - [[ -z "${constraint// /}" ]] && return 0 - local c="$constraint" - c="${c//,/ }" - - local IFS=$'\n' parts=() - # shellcheck disable=SC2207 - parts=($(echo "$c" | sed -E 's/[[:space:]]*\|\|[[:space:]]*/\n/g; s/[[:space:]]+\|[[:space:]]+/\n/g')) - - local part tok - for part in "${parts[@]}"; do - part="${part#"${part%%[![:space:]]*}"}" - part="${part%"${part##*[![:space:]]}"}" - [[ -z "$part" ]] && continue - - local ok=true - for tok in $part; do - tok="${tok#"${tok%%[![:space:]]*}"}" - tok="${tok%"${tok##*[![:space:]]}"}" - [[ -z "$tok" ]] && continue - - if [[ "$tok" == "*" || "$tok" == "x" || "$tok" == "X" ]]; then continue; fi - if [[ "$tok" =~ ^[0-9]+\.x$ || "$tok" =~ ^[0-9]+\.X$ || "$tok" =~ ^[0-9]+\.\*$ ]]; then - [[ "${cand%%.*}" == "${tok%%.*}" ]] || ok=false - continue - fi - if [[ "$tok" =~ ^[0-9]+\.[0-9]+\.x$ || "$tok" =~ ^[0-9]+\.[0-9]+\.X$ || "$tok" =~ ^[0-9]+\.[0-9]+\.?\*$ ]]; then - [[ "$cand" == "${tok%.*}" ]] || ok=false - continue - fi + if [[ "$tok" =~ ^([0-9]+)[.]([0-9]+)$ ]]; then + version_in_range "$cand" "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.0" "$(next_minor "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.0")" + return + fi - if [[ "$tok" =~ ^\^([0-9]+)\.([0-9]+) ]]; then - local maj="${BASH_REMATCH[1]}" min="${BASH_REMATCH[2]}" - local lower="${maj}.${min}" upper="$((maj + 1)).0" - ver_cmp_ge "$cand_full" "$(ver_norm "$lower")" && ver_cmp_lt "$cand_full" "$(ver_norm "$upper")" || ok=false - continue - fi + if [[ "$tok" =~ ^([0-9]+)[.]([0-9]+)[.](x|X|\*)$ ]]; then + version_in_range "$cand" "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.0" "$(next_minor "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.0")" + return + fi - if [[ "$tok" =~ ^~([0-9]+)\.([0-9]+) ]]; then - local maj="${BASH_REMATCH[1]}" min="${BASH_REMATCH[2]}" - local lower="${maj}.${min}" upper="${maj}.$((min + 1))" - ver_cmp_ge "$cand_full" "$(ver_norm "$lower")" && ver_cmp_lt "$cand_full" "$(ver_norm "$upper")" || ok=false - continue - fi + if [[ "$tok" =~ ^([0-9]+)[.]([0-9]+)[.]([0-9]+)$ ]]; then + version_in_range "$cand" "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" "$(next_minor "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.0")" + return + fi - if [[ "$tok" =~ ^(>=|<=|>|<)([0-9]+(\.[0-9]+)?)$ ]]; then - local op="${BASH_REMATCH[1]}" v="${BASH_REMATCH[2]}" - case "$op" in - '>=') ver_cmp_ge "$cand_full" "$(ver_norm "$v")" || ok=false ;; - '<=') ver_cmp_le "$cand_full" "$(ver_norm "$v")" || ok=false ;; - '>') ver_cmp_gt "$cand_full" "$(ver_norm "$v")" || ok=false ;; - '<') ver_cmp_lt "$cand_full" "$(ver_norm "$v")" || ok=false ;; - esac - continue - fi + if [[ "$tok" =~ ^\^([0-9]+(?:\.[0-9]+){0,2})$ ]]; then + local lower upper + lower="$(ver_norm "${BASH_REMATCH[1]}")" + upper="$(caret_upper_bound "$lower")" + version_in_range "$cand" "$lower" "$upper" + return + fi - if [[ "$tok" =~ ^([0-9]+)\.([0-9]+)$ ]]; then - [[ "$cand" == "${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" ]] || ok=false - continue - fi - : - done + if [[ "$tok" =~ ^~([0-9]+(?:\.[0-9]+){0,2})$ ]]; then + local base normalized upper + base="${BASH_REMATCH[1]}" + normalized="$(ver_norm "$base")" + if [[ "$base" =~ ^[0-9]+$ ]]; then + upper="$(next_major "$normalized")" + elif [[ "$base" =~ ^[0-9]+\.[0-9]+$ ]]; then + upper="$(next_major "$normalized")" + else + upper="$(next_minor "$normalized")" + fi + version_in_range "$cand" "$normalized" "$upper" + return + fi - [[ "$ok" == true ]] && return 0 + if [[ "$tok" =~ ^(>=|<=|>|<|=)[[:space:]]*([0-9]+(?:\.[0-9]+){0,2})$ ]]; then + local op="${BASH_REMATCH[1]}" v="$(ver_norm "${BASH_REMATCH[2]}")" + case "$op" in + '>=') ver_cmp_ge "$cand" "$v" ;; + '<=') ver_cmp_le "$cand" "$v" ;; + '>') ver_cmp_gt "$cand" "$v" ;; + '<') ver_cmp_lt "$cand" "$v" ;; + '=') version_in_range "$cand" "$v" "$(next_minor "$v")" ;; + esac + return + fi + + return 1 +} + +constraint_and_allows() { + local expr="$1" cand="$2" + local normalized + + normalized="$expr" + normalized="${normalized//,/ }" + normalized="${normalized//>=/>= }" + normalized="${normalized//<=/<= }" + normalized="${normalized///> }" + normalized="${normalized//= / = }" + normalized="${normalized//= /= }" + normalized="$(trim "$normalized")" + + local tok + for tok in $normalized; do + [[ -z "$tok" ]] && continue + constraint_atom_allows "$tok" "$cand" || return 1 done + + return 0 +} + +constraint_allows() { + local constraint="$1" cand="$2" + local expr + constraint="$(trim "$constraint")" + [[ -z "$constraint" ]] && return 0 + + while IFS= read -r expr; do + expr="$(trim "$expr")" + [[ -z "$expr" ]] && continue + if constraint_and_allows "$expr" "$cand"; then + return 0 + fi + done < <(printf '%s\n' "$constraint" | sed -E 's/[[:space:]]*\|\|[[:space:]]*/\n/g; s/[[:space:]]+\|[[:space:]]+/\n/g') + return 1 } +read_php_constraint_from() { + local dir="$1" + local file="$dir/composer.json" + [[ -f "$file" ]] || return 1 + + if command -v jq >/dev/null 2>&1; then + local v + v="$(jq -r '.require.php // empty' "$file" 2>/dev/null || true)" + [[ -n "$v" && "$v" != "null" ]] && { + printf '%s' "$v" + return 0 + } + fi + + local line + line="$(grep -Eo '"php"[[:space:]]*:[[:space:]]*"[^"]+"' "$file" 2>/dev/null | head -n1 || true)" + [[ -n "$line" ]] || return 1 + printf '%s' "$line" | sed -E 's/.*"php"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/' +} + running_php_versions() { docker ps --format '{{.Names}}' | awk -F'_' '/^PHP_[0-9]+\.[0-9]+$/ {print $2}' | sort -Vr } -pick_container_by_version() { echo "PHP_$1"; } -pick_best_container() { - local explicit_ver="$1" constraint="${2:-}" +php_container_for_version() { + printf 'PHP_%s' "$1" +} + +container_is_running() { + local name="$1" + docker ps --format '{{.Names}}' | grep -qx "$name" +} + +get_container_image() { + docker inspect -f '{{.Config.Image}}' "$1" 2>/dev/null +} + +server_tools_php_version() { + container_is_running SERVER_TOOLS || return 1 + docker exec SERVER_TOOLS sh -lc 'php -r "echo PHP_MAJOR_VERSION, \".\", PHP_MINOR_VERSION;"' 2>/dev/null | tr -d '\r\n' +} + +select_runtime() { + local explicit_ver="$1" constraint="$2" local versions=() + local matched=() + local st_ver="" + mapfile -t versions < <(running_php_versions) - [[ ${#versions[@]} -gt 0 ]] || die "no PHP container is running (expected names like PHP_8.4)." + + if container_is_running SERVER_TOOLS; then + st_ver="$(server_tools_php_version || true)" + fi if [[ -n "$explicit_ver" ]]; then local cname - cname="$(pick_container_by_version "$explicit_ver")" - docker ps --format '{{.Names}}' | grep -qx "$cname" || die "container '$cname' is not running." - printf "%s\n%s\n%s\n" "$cname" "$explicit_ver" "explicit via -V" - return 0 + cname="$(php_container_for_version "$explicit_ver")" + + if container_is_running "$cname"; then + printf '%s\n%s\n%s\n%s\n' "$cname" "$explicit_ver" "explicit via -V" "container" + return 0 + fi + + if [[ -n "$st_ver" && "$st_ver" == "$explicit_ver" ]]; then + printf '%s\n%s\n%s\n%s\n' "SERVER_TOOLS" "$st_ver" "explicit via -V" "server_tools" + return 0 + fi + + if [[ -n "$st_ver" ]]; then + die "requested PHP $explicit_ver, but '$cname' is not running and SERVER_TOOLS provides PHP $st_ver." + fi + + die "requested PHP $explicit_ver, but '$cname' is not running and SERVER_TOOLS is unavailable." fi - if [[ -n "${constraint// /}" ]]; then + if [[ -n "$constraint" ]]; then local v for v in "${versions[@]}"; do if constraint_allows "$constraint" "$v"; then - printf "%s\n%s\n%s\n" "$(pick_container_by_version "$v")" "$v" "composer.json: ${constraint}" - return 0 + matched+=("$v") fi done + + if [[ ${#matched[@]} -gt 0 ]]; then + printf '%s\n%s\n%s\n%s\n' "$(php_container_for_version "${matched[0]}")" "${matched[0]}" "composer.json: $constraint" "container" + return 0 + fi + + if [[ -n "$st_ver" ]] && constraint_allows "$constraint" "$st_ver"; then + printf '%s\n%s\n%s\n%s\n' "SERVER_TOOLS" "$st_ver" "composer.json: $constraint" "server_tools" + return 0 + fi + + if [[ ${#versions[@]} -gt 0 ]]; then + die "no running PHP container satisfies composer.json PHP constraint '$constraint'; SERVER_TOOLS${st_ver:+ ($st_ver)} also does not satisfy it." + fi + + if [[ -n "$st_ver" ]]; then + die "SERVER_TOOLS is running with PHP $st_ver, which does not satisfy composer.json PHP constraint '$constraint'." + fi + + die "no PHP runtime available that satisfies composer.json PHP constraint '$constraint'." + fi + + if [[ ${#versions[@]} -gt 0 ]]; then + printf '%s\n%s\n%s\n%s\n' "$(php_container_for_version "${versions[0]}")" "${versions[0]}" "fallback: highest running container" "container" + return 0 fi - printf "%s\n%s\n%s\n" "$(pick_container_by_version "${versions[0]}")" "${versions[0]}" "fallback: highest running container" + if [[ -n "$st_ver" ]]; then + printf '%s\n%s\n%s\n%s\n' "SERVER_TOOLS" "$st_ver" "fallback: SERVER_TOOLS" "server_tools" + return 0 + fi + + die "no PHP_X.Y container is running and SERVER_TOOLS is unavailable." } -# ───────────────────────────────────────────────────────────────────────────── -# Args -# ───────────────────────────────────────────────────────────────────────────── +load_env_file + EXPLICIT_VER="" ARGS=() @@ -300,44 +465,62 @@ while [[ $# -gt 0 ]]; do esac done +HOST_PWD="$(resolve_host_pwd)" +SOURCE_DIR="$(resolve_source_dir "$HOST_PWD")" PHP_CONSTRAINT="" -[[ -z "$EXPLICIT_VER" ]] && PHP_CONSTRAINT="$(read_php_constraint || true)" + +if [[ -z "$EXPLICIT_VER" ]]; then + PHP_CONSTRAINT="$(read_php_constraint_from "$SOURCE_DIR" || true)" +fi TARGET_CONTAINER="" DETECTED_VER="" DETECT_REASON="" +RUNTIME_KIND="" { read -r TARGET_CONTAINER read -r DETECTED_VER read -r DETECT_REASON -} < <(pick_best_container "$EXPLICIT_VER" "$PHP_CONSTRAINT") + read -r RUNTIME_KIND +} < <(select_runtime "$EXPLICIT_VER" "$PHP_CONSTRAINT") -IMAGE="$(docker inspect -f '{{.Config.Image}}' "$TARGET_CONTAINER" 2>/dev/null)" || - die "failed to inspect container '$TARGET_CONTAINER'." +IMAGE="$(get_container_image "$TARGET_CONTAINER")" || die "failed to inspect container '$TARGET_CONTAINER'." kv "PHP detected:" "${C_YELLOW}${DETECTED_VER}${C_RESET} ${C_CYAN}(${DETECT_REASON})${C_RESET}" +kv "Runtime:" "${C_YELLOW}${RUNTIME_KIND}${C_RESET}" kv "Container:" "${C_YELLOW}${TARGET_CONTAINER}${C_RESET}" kv "Image:" "${C_YELLOW}${IMAGE}${C_RESET}" -HOST_PWD="$(resolve_host_pwd)" msys_no_pathconv -RUN_FLAGS=(--rm -v "$HOST_PWD":/workspace -w /workspace --network "container:$TARGET_CONTAINER") -[[ -t 0 ]] && RUN_FLAGS+=(-it) || RUN_FLAGS+=(-i) -RUN_FLAGS+=(-u "$(id -u):$(id -g)") +RUN_FLAGS=( + --rm + -v "$HOST_PWD:/workspace" + -w /workspace + --network "container:$TARGET_CONTAINER" + -e HOME=/tmp/composer-home + -e COMPOSER_HOME=/tmp/composer-home + -e COMPOSER_CACHE_DIR=/tmp/composer-cache +) + +if [[ -t 0 && -t 1 ]]; then + RUN_FLAGS+=(-it) +else + RUN_FLAGS+=(-i) +fi + +if command -v id >/dev/null 2>&1; then + RUN_FLAGS+=(-u "$(id -u):$(id -g)") +fi if [[ -d "$SSH_DIR" ]]; then - RUN_FLAGS+=(-v "$SSH_DIR:/home/${USER:-root}/.ssh:ro") + RUN_FLAGS+=(-v "$SSH_DIR:/tmp/composer-home/.ssh:ro") fi if [[ -n "${HOME:-}" && -d "$HOME" ]]; then mkdir -p "$HOME/.cache/composer" "$HOME/.composer" 2>/dev/null || true - RUN_FLAGS+=( - -e "COMPOSER_CACHE_DIR=/tmp/composer-cache" - -v "$HOME/.cache/composer:/tmp/composer-cache" - -v "$HOME/.composer:/tmp/composer-home" - -e "COMPOSER_HOME=/tmp/composer-home" - ) + RUN_FLAGS+=(-v "$HOME/.cache/composer:/tmp/composer-cache") + RUN_FLAGS+=(-v "$HOME/.composer:/tmp/composer-home") fi exec docker run "${RUN_FLAGS[@]}" "$IMAGE" composer "${ARGS[@]}" diff --git a/bin/es b/bin/es new file mode 100644 index 0000000..dc1611b --- /dev/null +++ b/bin/es @@ -0,0 +1,434 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." 2>/dev/null && pwd || pwd)" +ENV_FILE="$REPO_ROOT/.env" + +SERVICE="${ELASTICSEARCH_SERVICE:-ELASTICSEARCH}" +ES_SCHEME="${ELASTICSEARCH_SCHEME:-http}" +ES_HOST="${ELASTICSEARCH_HOST:-127.0.0.1}" +ES_PORT="${ELASTICSEARCH_PORT:-9200}" +ES_USER="${ELASTICSEARCH_USERNAME:-${ES_USERNAME:-}}" +ES_PASS="${ELASTICSEARCH_PASSWORD:-${ES_PASSWORD:-}}" +ES_API_KEY="${ELASTICSEARCH_API_KEY:-${ES_API_KEY:-}}" +ES_INSECURE="${ELASTICSEARCH_INSECURE:-0}" + +is_msys() { + case "${OSTYPE:-}" in + msys*|cygwin*|win32*) return 0 ;; + esac + return 1 +} + +load_env_file() { + [[ -f "$ENV_FILE" ]] || return 0 + + local key raw value + while IFS= read -r raw || [[ -n "$raw" ]]; do + raw="${raw%$'\r'}" + [[ -z "$raw" || "$raw" =~ ^[[:space:]]*# ]] && continue + raw="${raw#export }" + [[ "$raw" == *=* ]] || continue + + key="${raw%%=*}" + value="${raw#*=}" + + key="${key##[[:space:]]}" + key="${key%%[[:space:]]}" + [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue + + if [[ "$value" =~ ^\".*\"$ || "$value" =~ ^\'.*\'$ ]]; then + value="${value:1:${#value}-2}" + fi + + case "$key" in + ELASTICSEARCH_SERVICE) [[ -z "${ELASTICSEARCH_SERVICE:-}" ]] && SERVICE="$value" ;; + ELASTICSEARCH_SCHEME) [[ -z "${ELASTICSEARCH_SCHEME:-}" ]] && ES_SCHEME="$value" ;; + ELASTICSEARCH_HOST) [[ -z "${ELASTICSEARCH_HOST:-}" ]] && ES_HOST="$value" ;; + ELASTICSEARCH_PORT) [[ -z "${ELASTICSEARCH_PORT:-}" ]] && ES_PORT="$value" ;; + ELASTICSEARCH_USERNAME|ES_USERNAME) [[ -z "$ES_USER" ]] && ES_USER="$value" ;; + ELASTICSEARCH_PASSWORD|ES_PASSWORD) [[ -z "$ES_PASS" ]] && ES_PASS="$value" ;; + ELASTICSEARCH_API_KEY|ES_API_KEY) [[ -z "$ES_API_KEY" ]] && ES_API_KEY="$value" ;; + ELASTICSEARCH_INSECURE) [[ -z "${ELASTICSEARCH_INSECURE:-}" ]] && ES_INSECURE="$value" ;; + esac + done < "$ENV_FILE" +} + +container_env() { + local name="$1" + docker inspect -f "{{range .Config.Env}}{{println .}}{{end}}" "$SERVICE" 2>/dev/null \ + | awk -F= -v key="$name" '$1==key { sub(/^[^=]*=/, ""); print; exit }' +} + +container_running() { + docker inspect -f '{{.State.Running}}' "$SERVICE" 2>/dev/null | grep -qi '^true$' +} + +require_service() { + if ! container_running; then + echo "Error: $SERVICE is not running." >&2 + exit 1 + fi +} + +resolve_container_defaults() { + local v + + v="$(container_env ELASTICSEARCH_USERNAME || true)" + [[ -n "$v" && -z "$ES_USER" ]] && ES_USER="$v" + + v="$(container_env ELASTICSEARCH_PASSWORD || true)" + [[ -n "$v" && -z "$ES_PASS" ]] && ES_PASS="$v" + + v="$(container_env ES_USERNAME || true)" + [[ -n "$v" && -z "$ES_USER" ]] && ES_USER="$v" + + v="$(container_env ES_PASSWORD || true)" + [[ -n "$v" && -z "$ES_PASS" ]] && ES_PASS="$v" + + v="$(container_env ELASTICSEARCH_API_KEY || true)" + [[ -n "$v" && -z "$ES_API_KEY" ]] && ES_API_KEY="$v" + + v="$(container_env ES_API_KEY || true)" + [[ -n "$v" && -z "$ES_API_KEY" ]] && ES_API_KEY="$v" + + v="$(container_env ELASTICSEARCH_HOST || true)" + [[ -n "$v" && -z "${ELASTICSEARCH_HOST:-}" ]] && ES_HOST="$v" + + v="$(container_env ELASTICSEARCH_PORT || true)" + [[ -n "$v" && -z "${ELASTICSEARCH_PORT:-}" ]] && ES_PORT="$v" +} + +base_url() { + printf '%s://%s:%s' "$ES_SCHEME" "$ES_HOST" "$ES_PORT" +} + +have_stdin() { + [[ ! -t 0 ]] +} + +join_by() { + local sep="$1"; shift || true + local first=1 out="" + local s + for s in "$@"; do + if (( first )); then + out="$s" + first=0 + else + out+="$sep$s" + fi + done + printf '%s' "$out" +} + +curl_exec() { + local method="$1" + local path="$2" + local data_file="${3:-}" + local content_type="${4:-application/json}" + shift $(( $# >= 4 ? 4 : $# )) || true + local extra=("$@") + + local url + if [[ "$path" =~ ^https?:// ]]; then + url="$path" + else + [[ "$path" == /* ]] || path="/$path" + url="$(base_url)$path" + fi + + local cmd=(docker exec -i "$SERVICE" curl -sS -X "$method" "$url") + (( ES_INSECURE )) && cmd+=(--insecure) + cmd+=(-H 'Accept: application/json') + + if [[ -n "$ES_API_KEY" ]]; then + cmd+=(-H "Authorization: ApiKey $ES_API_KEY") + elif [[ -n "$ES_USER" || -n "$ES_PASS" ]]; then + cmd+=(-u "${ES_USER}:${ES_PASS}") + fi + + if [[ -n "$data_file" ]]; then + cmd+=(-H "Content-Type: $content_type" --data-binary "@$data_file") + fi + + if ((${#extra[@]})); then + cmd+=("${extra[@]}") + fi + + "${cmd[@]}" +} + +mktemp_host() { + mktemp 2>/dev/null || mktemp -t es +} + +cleanup_files=() +cleanup() { + local f + for f in "${cleanup_files[@]:-}"; do + [[ -n "$f" && -f "$f" ]] && rm -f -- "$f" + done +} +trap cleanup EXIT + +prepare_body_file() { + local src="${1:-}" + local tmp + tmp="$(mktemp_host)" + cleanup_files+=("$tmp") + + if [[ -n "$src" ]]; then + if [[ "$src" == @* ]]; then + src="${src#@}" + fi + [[ -f "$src" ]] || { echo "Error: body file not found: $src" >&2; exit 1; } + cat -- "$src" > "$tmp" + else + cat > "$tmp" + fi + + printf '%s' "$tmp" +} + +print_usage() { + cat < # GET /_cat/?v + es get # GET + es delete # DELETE + es head # HEAD + es post [@file|-] # POST JSON body + es put [@file|-] # PUT JSON body + es bulk [@file|-] # POST /_bulk (ndjson) + es search [@file|-] # POST //_search + es index [id] [@file|-] # POST/PUT document + es raw [body] # Generic passthrough + +Examples: + es health + es get /_cluster/settings?pretty + es post /my-index/_search @query.json + cat bulk.ndjson | es bulk - + es index my-index @doc.json + es index my-index 42 @doc.json +USAGE +} + +main() { + load_env_file + require_service + resolve_container_defaults + + local cmd="${1:-status}" + [[ $# -gt 0 ]] && shift || true + + case "$cmd" in + -h|--help|help) + print_usage + ;; + + status) + curl_exec GET '/?pretty' + ;; + + health) + curl_exec GET '/_cluster/health?pretty' + ;; + + indices) + curl_exec GET '/_cat/indices?v' + ;; + + aliases) + curl_exec GET '/_cat/aliases?v' + ;; + + nodes) + curl_exec GET '/_cat/nodes?v' + ;; + + shards) + curl_exec GET '/_cat/shards?v' + ;; + + cat) + local path="${1:-}" + [[ -n "$path" ]] || { echo 'Error: cat requires a path.' >&2; exit 1; } + shift || true + path="${path#/}" + if [[ "$path" == _cat/* ]]; then + curl_exec GET "/${path}?v" '' '' "$@" + else + curl_exec GET "/_cat/${path}?v" '' '' "$@" + fi + ;; + + get) + local path="${1:-/}" + shift || true + curl_exec GET "$path" '' '' "$@" + ;; + + delete) + local path="${1:-}" + [[ -n "$path" ]] || { echo 'Error: delete requires a path.' >&2; exit 1; } + shift || true + curl_exec DELETE "$path" '' '' "$@" + ;; + + head) + local path="${1:-}" + [[ -n "$path" ]] || { echo 'Error: head requires a path.' >&2; exit 1; } + shift || true + curl_exec HEAD "$path" '' '' -I "$@" + ;; + + post|put) + local method="${cmd^^}" + local path="${1:-}" + [[ -n "$path" ]] || { echo "Error: $cmd requires a path." >&2; exit 1; } + shift || true + local body_arg="${1:-}" + [[ $# -gt 0 ]] && shift || true + local body_file='' + if [[ -n "$body_arg" ]]; then + if [[ "$body_arg" == '-' ]]; then + body_file="$(prepare_body_file)" + else + body_file="$(prepare_body_file "$body_arg")" + fi + elif have_stdin; then + body_file="$(prepare_body_file)" + fi + curl_exec "$method" "$path" "$body_file" 'application/json' "$@" + ;; + + bulk) + local body_arg="${1:-}" + [[ $# -gt 0 ]] && shift || true + local body_file='' + if [[ -n "$body_arg" ]]; then + if [[ "$body_arg" == '-' ]]; then + body_file="$(prepare_body_file)" + else + body_file="$(prepare_body_file "$body_arg")" + fi + elif have_stdin; then + body_file="$(prepare_body_file)" + else + echo 'Error: bulk requires @file or stdin.' >&2 + exit 1 + fi + curl_exec POST '/_bulk' "$body_file" 'application/x-ndjson' "$@" + ;; + + search) + local index="${1:-}" + [[ -n "$index" ]] || { echo 'Error: search requires an index or path.' >&2; exit 1; } + shift || true + local body_arg="${1:-}" + [[ $# -gt 0 ]] && shift || true + local body_file='' + if [[ -n "$body_arg" ]]; then + if [[ "$body_arg" == '-' ]]; then + body_file="$(prepare_body_file)" + else + body_file="$(prepare_body_file "$body_arg")" + fi + elif have_stdin; then + body_file="$(prepare_body_file)" + fi + if [[ "$index" == /* ]]; then + curl_exec POST "$index" "$body_file" 'application/json' "$@" + else + curl_exec POST "/${index}/_search" "$body_file" 'application/json' "$@" + fi + ;; + + index) + local index_name="${1:-}" + [[ -n "$index_name" ]] || { echo 'Error: index requires an index name.' >&2; exit 1; } + shift || true + local maybe_id="${1:-}" + local id='' + local body_arg='' + + if [[ -n "$maybe_id" ]]; then + case "$maybe_id" in + @*|-) + body_arg="$maybe_id" + shift || true + ;; + *) + id="$maybe_id" + shift || true + body_arg="${1:-}" + [[ $# -gt 0 ]] && shift || true + ;; + esac + fi + + local body_file='' + if [[ -n "$body_arg" ]]; then + if [[ "$body_arg" == '-' ]]; then + body_file="$(prepare_body_file)" + else + body_file="$(prepare_body_file "$body_arg")" + fi + elif have_stdin; then + body_file="$(prepare_body_file)" + else + echo 'Error: index requires a document body via @file or stdin.' >&2 + exit 1 + fi + + if [[ -n "$id" ]]; then + curl_exec PUT "/${index_name}/_doc/${id}" "$body_file" 'application/json' "$@" + else + curl_exec POST "/${index_name}/_doc" "$body_file" 'application/json' "$@" + fi + ;; + + raw) + local method="${1:-}" + local path="${2:-}" + [[ -n "$method" && -n "$path" ]] || { echo 'Error: raw requires METHOD and path.' >&2; exit 1; } + shift 2 || true + local body_arg="${1:-}" + [[ $# -gt 0 ]] && shift || true + local body_file='' + if [[ -n "$body_arg" ]]; then + if [[ "$body_arg" == '-' ]]; then + body_file="$(prepare_body_file)" + else + body_file="$(prepare_body_file "$body_arg")" + fi + elif have_stdin && [[ ! "$method" =~ ^(GET|HEAD|DELETE|get|head|delete)$ ]]; then + body_file="$(prepare_body_file)" + fi + curl_exec "${method^^}" "$path" "$body_file" 'application/json' "$@" + ;; + + *) + if [[ "$cmd" == /* || "$cmd" =~ ^_ ]]; then + curl_exec GET "$cmd" '' '' "$@" + else + echo "Error: unknown command: $cmd" >&2 + echo >&2 + print_usage >&2 + exit 1 + fi + ;; + esac +} + +main "$@" diff --git a/bin/maria b/bin/maria index 8f26cf0..b48a28e 100755 --- a/bin/maria +++ b/bin/maria @@ -56,7 +56,9 @@ SERVICE="${MARIADB_CONTAINER:-${MYSQL_CONTAINER:-MARIADB}}" MARIADB_PORT_IN="${MARIADB_PORT_IN:-${MYSQL_PORT_IN:-}}" require_container() { - docker inspect "$SERVICE" >/dev/null 2>&1 || die "container '$SERVICE' is not running." + local running + running="$(docker inspect -f '{{.State.Running}}' "$SERVICE" 2>/dev/null || true)" + [[ "$running" == "true" ]] || die "container '$SERVICE' is not running." } is_tty() { [[ -t 0 ]] && echo "-it" || echo "-i"; } @@ -93,12 +95,25 @@ infer_port() { # Host mount (keep it simple & reliable like your working mysql script): # always mount *current directory* for file ops. +resolve_host_pwd() { + local wd="${WORKDIR_WIN:-${WORKING_DIR:-$PWD}}" + + if is_msys && [[ "$wd" == *"Program Files/Git/workspace"* ]]; then + wd="$PWD" + fi + + printf "%s" "$wd" +} + mount_host_pwd() { + local wd + wd="$(resolve_host_pwd)" + msys_no_pathconv if is_msys && command -v cygpath >/dev/null 2>&1; then - cygpath -w "$PWD" + cygpath -w "$wd" else - printf "%s" "$PWD" + printf "%s" "$wd" fi } @@ -108,7 +123,7 @@ to_workspace_path() { [[ -f "$host_path" ]] || die "file not found: $host_path" local abs cwd rel - cwd="$(cd "$PWD" && pwd)" + cwd="$(cd "$(resolve_host_pwd)" && pwd)" abs="$(cd "$(dirname "$host_path")" && pwd)/$(basename "$host_path")" if command -v python3 >/dev/null 2>&1; then @@ -364,6 +379,7 @@ EOF shift local db="${1:-$DEF_DB}" [[ -n "$db" ]] || die "tables requires a db name (or set MARIADB_DATABASE/MYSQL_DATABASE)." + [[ "$db" =~ ^[A-Za-z0-9_]+$ ]] || die "invalid db name '$db' (allowed: A-Z a-z 0-9 _)" # across schemas (but scoped to selected DB unless you want global — keep predictable): if [[ "$as_user" == true ]]; then db_exec_user "$client" "$db" -e \ diff --git a/bin/mongo b/bin/mongo new file mode 100644 index 0000000..072299c --- /dev/null +++ b/bin/mongo @@ -0,0 +1,381 @@ +#!/usr/bin/env bash +set -euo pipefail + +SERVICE_DEFAULT="MONGODB" +WORKSPACE_MOUNT="/workspace" + +trim() { + local s="${1-}" + s="${s#${s%%[![:space:]]*}}" + s="${s%${s##*[![:space:]]}}" + printf '%s' "$s" +} + +err() { + echo "[mongo] $*" >&2 + exit 1 +} + +warn() { + echo "[mongo] $*" >&2 +} + +is_msys() { + case "$(uname -s 2>/dev/null || true)" in + MINGW*|MSYS*|CYGWIN*) return 0 ;; + *) return 1 ;; + esac +} + +repo_root() { + git rev-parse --show-toplevel 2>/dev/null || pwd +} + +load_env() { + local env_file="${LDS_ENV_FILE:-.env}" + [[ -f "$env_file" ]] || return 0 + + local line key val + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line%$'\r'}" + [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue + [[ "$line" == export* ]] && line="${line#export }" + [[ "$line" == *=* ]] || continue + + key="${line%%=*}" + val="${line#*=}" + key="$(trim "$key")" + val="$(trim "$val")" + + [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue + case "$key" in + MONGODB_SERVICE|MONGODB_ROOT_USERNAME|MONGODB_ROOT_PASSWORD|MONGO_INITDB_ROOT_USERNAME|MONGO_INITDB_ROOT_PASSWORD|WORKING_DIR|WORKDIR|WORKDIR_WIN) + ;; + *) + continue + ;; + esac + + if [[ "$val" == \"*\" && "$val" == *\" ]]; then + val="${val:1:${#val}-2}" + elif [[ "$val" == \'*\' && "$val" == *\' ]]; then + val="${val:1:${#val}-2}" + fi + + export "$key=$val" + done < "$env_file" +} + +container_running() { + local service="$1" + [[ -n "$service" ]] || return 1 + [[ "$(docker inspect -f '{{.State.Running}}' "$service" 2>/dev/null || true)" == "true" ]] +} + +container_env() { + local service="$1" key="$2" + docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$service" 2>/dev/null \ + | awk -F= -v k="$key" '$1==k {sub($1"=", ""); print; exit}' +} + +service_name() { + printf '%s' "${MONGODB_SERVICE:-$SERVICE_DEFAULT}" +} + +mongo_user() { + local service="$1" + local v + for v in \ + "${MONGODB_ROOT_USERNAME:-}" \ + "${MONGO_INITDB_ROOT_USERNAME:-}" \ + "$(container_env "$service" MONGO_INITDB_ROOT_USERNAME)"; do + [[ -n "$v" ]] && { printf '%s' "$v"; return 0; } + done + printf 'root' +} + +mongo_pass() { + local service="$1" + local v + for v in \ + "${MONGODB_ROOT_PASSWORD:-}" \ + "${MONGO_INITDB_ROOT_PASSWORD:-}" \ + "$(container_env "$service" MONGO_INITDB_ROOT_PASSWORD)"; do + [[ -n "$v" ]] && { printf '%s' "$v"; return 0; } + done + printf '12345' +} + +mongo_image() { + docker inspect -f '{{.Config.Image}}' "$1" 2>/dev/null +} + +workspace_host() { + local wd="${WORKDIR_WIN:-${WORKING_DIR:-${WORKDIR:-$PWD}}}" + + if is_msys; then + if [[ "$wd" == *"/Program Files/Git/workspace"* ]] || [[ "$wd" == "/workspace" ]]; then + wd="$(repo_root)" + fi + if command -v cygpath >/dev/null 2>&1; then + cygpath -aw "$wd" + return 0 + fi + fi + + printf '%s' "$wd" +} + +inside_workspace_path() { + local input="$1" + local host_root + host_root="$(workspace_host)" + + [[ -n "$input" ]] || err "missing file path" + + if [[ "$input" != /* ]]; then + input="$PWD/$input" + fi + input="$(cd "$(dirname "$input")" 2>/dev/null && pwd)/$(basename "$input")" + + local host_root_norm="$host_root" + if is_msys && command -v cygpath >/dev/null 2>&1; then + host_root_norm="$(cygpath -am "$host_root")" + input="$(cygpath -am "$input")" + fi + + case "$input" in + "$host_root_norm"/*) + printf '%s/%s' "$WORKSPACE_MOUNT" "${input#"$host_root_norm"/}" + ;; + "$host_root_norm") + printf '%s' "$WORKSPACE_MOUNT" + ;; + *) + err "file must be under workspace: $host_root_norm" + ;; + esac +} + +common_uri() { + local service="$1" + local user pass + user="$(mongo_user "$service")" + pass="$(mongo_pass "$service")" + printf 'mongodb://%s:%s@127.0.0.1:27017/admin?authSource=admin' "$user" "$pass" +} + +mongosh_args() { + local service="$1" db="${2:-admin}" + local user pass + user="$(mongo_user "$service")" + pass="$(mongo_pass "$service")" + printf '%s\n' --host 127.0.0.1 --port 27017 --username "$user" --password "$pass" --authenticationDatabase admin "$db" +} + +run_mongosh() { + local service="$1"; shift + local -a args=( "$@" ) + docker exec -it "$service" mongosh "${args[@]}" +} + +run_mongosh_non_tty() { + local service="$1"; shift + local -a args=( "$@" ) + docker exec -i "$service" mongosh "${args[@]}" +} + +run_helper() { + local service="$1"; shift + local image + image="$(mongo_image "$service")" + [[ -n "$image" ]] || err "unable to inspect image for $service" + + local host_ws + host_ws="$(workspace_host)" + + local -a cmd=( docker run --rm ) + if is_msys; then + cmd+=( -e MSYS_NO_PATHCONV=1 -e MSYS2_ARG_CONV_EXCL='*' ) + fi + cmd+=( + --network "container:$service" + -v "$host_ws:$WORKSPACE_MOUNT" + -w "$WORKSPACE_MOUNT" + "$image" + "$@" + ) + "${cmd[@]}" +} + +require_db_name() { + local db="$1" + [[ -n "$db" ]] || err "database name is required" + [[ "$db" =~ ^[A-Za-z0-9._-]+$ ]] || err "invalid database name: $db" +} + +require_collection_name() { + local coll="$1" + [[ -n "$coll" ]] || err "collection name is required" + [[ "$coll" =~ ^[A-Za-z0-9._-]+$ ]] || err "invalid collection name: $coll" +} + +print_help() { + cat <<'HELP' +Usage: + mongo Open mongosh (admin) + mongo shell [db] Open mongosh for a database + mongo status Ping server + mongo dbs List databases + mongo collections [db] List collections + mongo eval [db] Execute JavaScript + mongo create-db Create database lazily via temp collection + mongo drop-db Drop database + mongo import [--drop] [--type json|jsonl|csv] + mongo export [file] [--jsonArray] + +Notes: + - File paths for import/export must be inside the current workspace. + - Import/export run in a one-off helper container from the same Mongo image. +HELP +} + +main() { + load_env + + local service + service="$(service_name)" + container_running "$service" || err "$service is not running" + + local cmd="${1:-shell}" + if [[ $# -gt 0 ]]; then shift; fi + + case "$cmd" in + help|-h|--help) + print_help + ;; + shell) + local db="${1:-admin}" + run_mongosh "$service" $(mongosh_args "$service" "$db") + ;; + status) + run_mongosh_non_tty "$service" $(mongosh_args "$service" admin) --quiet --eval 'db.adminCommand({ ping: 1 })' + ;; + dbs) + run_mongosh_non_tty "$service" $(mongosh_args "$service" admin) --quiet --eval 'db.adminCommand({ listDatabases: 1 }).databases.forEach(d => print(d.name))' + ;; + collections) + local db="${1:-admin}" + run_mongosh_non_tty "$service" $(mongosh_args "$service" "$db") --quiet --eval 'db.getCollectionNames().forEach(c => print(c))' + ;; + eval) + local js="${1:-}" + local db="${2:-admin}" + [[ -n "$js" ]] || err "JavaScript expression is required" + run_mongosh_non_tty "$service" $(mongosh_args "$service" "$db") --quiet --eval "$js" + ;; + create-db) + local db="${1:-}" + require_db_name "$db" + run_mongosh_non_tty "$service" $(mongosh_args "$service" "$db") --quiet --eval 'db.__lds_init.insertOne({createdAt:new Date()}); db.__lds_init.deleteMany({}); print(db.getName())' + ;; + drop-db) + local db="${1:-}" + require_db_name "$db" + run_mongosh_non_tty "$service" $(mongosh_args "$service" "$db") --quiet --eval 'db.dropDatabase()' + ;; + import) + local db="${1:-}" coll="${2:-}" src="${3:-}" + shift $(( $# >= 3 ? 3 : $# )) || true + require_db_name "$db" + require_collection_name "$coll" + [[ -n "$src" ]] || err "import file is required" + [[ -f "$src" ]] || err "import file not found: $src" + + local path type ext + path="$(inside_workspace_path "$src")" + type="json" + ext="${src##*.}" + case "$ext" in + csv|CSV) type="csv" ;; + jsonl|ndjson|JSONL|NDJSON) type="json" ;; + json|JSON) type="json" ;; + esac + + local -a extra=() + while [[ $# -gt 0 ]]; do + case "$1" in + --type) + shift + [[ $# -gt 0 ]] || err "--type requires a value" + type="$1" + ;; + --drop) + extra+=( "$1" ) + ;; + *) + extra+=( "$1" ) + ;; + esac + shift || true + done + + if [[ "$type" == "json" ]]; then + case "$ext" in + jsonl|ndjson|JSONL|NDJSON) extra+=( --jsonArray=false ) ;; + json|JSON) : ;; + esac + fi + + run_helper "$service" mongoimport \ + --uri "$(common_uri "$service")" \ + --db "$db" \ + --collection "$coll" \ + --file "$path" \ + --type "$type" \ + "${extra[@]}" + ;; + export) + local db="${1:-}" coll="${2:-}" out="${3:-}" + shift $(( $# >= 2 ? 2 : $# )) || true + require_db_name "$db" + require_collection_name "$coll" + + if [[ -n "$out" && "$out" != --* ]]; then + shift || true + local host_out dir_in_ws file_in_ws + host_out="$out" + if [[ "$host_out" != /* ]]; then + host_out="$PWD/$host_out" + fi + mkdir -p "$(dirname "$host_out")" + dir_in_ws="$(inside_workspace_path "$(dirname "$host_out")")" + file_in_ws="$dir_in_ws/$(basename "$host_out")" + + run_helper "$service" mongoexport \ + --uri "$(common_uri "$service")" \ + --db "$db" \ + --collection "$coll" \ + --out "$file_in_ws" \ + "$@" + else + run_helper "$service" mongoexport \ + --uri "$(common_uri "$service")" \ + --db "$db" \ + --collection "$coll" \ + --out - \ + "$@" + fi + ;; + *) + # convenience: `mongo mydb` opens shell for db + if [[ $# -eq 0 && "$cmd" =~ ^[A-Za-z0-9._-]+$ ]]; then + run_mongosh "$service" $(mongosh_args "$service" "$cmd") + else + err "unknown command: $cmd" + fi + ;; + esac +} + +main "$@" diff --git a/bin/my b/bin/my index c775923..40866be 100755 --- a/bin/my +++ b/bin/my @@ -36,8 +36,8 @@ if [[ -r "$ENV_FILE" ]]; then [[ -z "${k:-}" || "$k" =~ ^[[:space:]]*# ]] && continue k="${k//[[:space:]]/}" case "$k" in - MYSQL_CONTAINER|MYSQL_PORT_IN) ;; - *) continue ;; + MYSQL_CONTAINER|MYSQL_PORT_IN|WORKDIR|WORKDIR_WIN) ;; + *) continue ;; esac v="${v%$'\r'}" v="${v#\"}"; v="${v%\"}" @@ -50,10 +50,12 @@ SERVICE="${MYSQL_CONTAINER:-MYSQL}" MYSQL_PORT_IN="${MYSQL_PORT_IN:-}" require_container() { - docker inspect "$SERVICE" >/dev/null 2>&1 || die "container '$SERVICE' is not running." + local state + state="$(docker inspect -f '{{.State.Status}}' "$SERVICE" 2>/dev/null || true)" + [[ "$state" == "running" ]] || die "container '$SERVICE' is not running." } -is_tty(){ [[ -t 0 ]] && echo "-it" || echo "-i"; } +is_tty(){ [[ -t 0 && -t 1 ]] && echo "-it" || echo "-i"; } # POSIX-safe env read from container (works in sh/ash/dash) cenv() { @@ -76,12 +78,11 @@ infer_port() { MYSQL_PORT_IN="${MYSQL_PORT_IN:-3306}" } -# Run mysql inside the container using a temp defaults file (no password in argv) mysql_exec_root() { local flags; flags="$(is_tty)" exec docker exec $flags "$SERVICE" sh -lc ' umask 077 - f="$(mktemp)"; trap "rm -f $f" EXIT + f="$(mktemp)"; trap "rm -f \"$f\"" EXIT cat >"$f" <"$f" <"$f" <"$f" <"$f" <"$f" <"$f" < [db] - NAME shorthand: uses ./NAME.sql or ./NAME.sql.gz if exists; db defaults to NAME -Export/Dump (writes local files; uses mount): +Export/Dump (writes local files): my dump [db] [out.sql|out.sql.gz] my export [db] [out.sql|out.sql.gz] # alias of dump Convenience: my "SELECT 1" -> runs on default DB my mydb "SELECT 1" -> runs on mydb - cat file.sql | mysql -> runs on default DB (stdin) + cat file.sql | my -> runs on default DB Notes: - tables shows all tables across schemas: information_schema.tables @@ -209,50 +279,22 @@ main() { shift fi - # Prefer container env (these are your defaults) local ROOT_PW USER USER_PW DEF_DB ROOT_PW="$(cenv_any MYSQL_ROOT_PASSWORD || true)" USER="$(cenv_any MYSQL_USER || true)" USER_PW="$(cenv_any MYSQL_PASSWORD || true)" DEF_DB="$(cenv_any MYSQL_DATABASE || true)" - # Stdin pipe mode: cat file.sql | my if [[ $# -eq 0 && ! -t 0 ]]; then [[ -n "${DEF_DB:-}" ]] || die "No default database set in container (MYSQL_DATABASE)." if [[ "$as_user" == true ]]; then - exec docker exec -i "$SERVICE" sh -lc ' - db="$1" - umask 077 - f="$(mktemp)"; trap "rm -f $f" EXIT - cat >"$f" <"$f" < interactive mysql shell if [[ $# -eq 0 ]]; then if [[ "$as_user" == true ]]; then mysql_exec_user; else mysql_exec_root; fi exit 0 @@ -260,53 +302,51 @@ EOF local cmd="${1:-}" case "$cmd" in - status) - docker ps --filter "name=^/${SERVICE}$" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" - exit 0 - ;; + status) + docker ps --filter "name=^/${SERVICE}$" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + exit 0 + ;; esac case "$cmd" in - dbs) - if [[ "$as_user" == true ]]; then - mysql_exec_user -e "SHOW DATABASES;" - else - mysql_exec_root -e "SHOW DATABASES;" - fi - ;; - - tables) - shift - local db="${1:-$DEF_DB}" - - if [[ -n "${db:-}" ]]; then - # Show tables for a specific db (includes views) + dbs) if [[ "$as_user" == true ]]; then - mysql_exec_user -N -e "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM information_schema.tables WHERE TABLE_SCHEMA='${db}' ORDER BY TABLE_NAME;" + mysql_exec_user -e "SHOW DATABASES;" else - mysql_exec_root -N -e "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM information_schema.tables WHERE TABLE_SCHEMA='${db}' ORDER BY TABLE_NAME;" + mysql_exec_root -e "SHOW DATABASES;" fi - else - # All schemas (avoid "no tables" confusion) - if [[ "$as_user" == true ]]; then - mysql_exec_user -N -e "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM information_schema.tables WHERE TABLE_SCHEMA NOT IN ('mysql','sys','performance_schema','information_schema') ORDER BY TABLE_SCHEMA, TABLE_NAME;" + ;; + + tables) + shift + local db="${1:-$DEF_DB}" + if [[ -n "${db:-}" ]]; then + [[ "$db" =~ ^[A-Za-z0-9_]+$ ]] || die "invalid db name '$db' (allowed: A-Z a-z 0-9 _)" + if [[ "$as_user" == true ]]; then + mysql_exec_user -N -e "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM information_schema.tables WHERE TABLE_SCHEMA='${db}' ORDER BY TABLE_NAME;" + else + mysql_exec_root -N -e "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM information_schema.tables WHERE TABLE_SCHEMA='${db}' ORDER BY TABLE_NAME;" + fi else - mysql_exec_root -N -e "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM information_schema.tables WHERE TABLE_SCHEMA NOT IN ('mysql','sys','performance_schema','information_schema') ORDER BY TABLE_SCHEMA, TABLE_NAME;" + if [[ "$as_user" == true ]]; then + mysql_exec_user -N -e "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM information_schema.tables WHERE TABLE_SCHEMA NOT IN ('mysql','sys','performance_schema','information_schema') ORDER BY TABLE_SCHEMA, TABLE_NAME;" + else + mysql_exec_root -N -e "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE FROM information_schema.tables WHERE TABLE_SCHEMA NOT IN ('mysql','sys','performance_schema','information_schema') ORDER BY TABLE_SCHEMA, TABLE_NAME;" + fi fi - fi - ;; + ;; - create-db) - shift - local db="${1:-$DEF_DB}" - [[ -n "${db:-}" ]] || die "create-db requires a db name (or set MYSQL_DATABASE)." - [[ "$db" =~ ^[A-Za-z0-9_]+$ ]] || die "invalid db name '$db' (allowed: A-Z a-z 0-9 _)" + create-db) + shift + local db="${1:-$DEF_DB}" + [[ -n "${db:-}" ]] || die "create-db requires a db name (or set MYSQL_DATABASE)." + [[ "$db" =~ ^[A-Za-z0-9_]+$ ]] || die "invalid db name '$db' (allowed: A-Z a-z 0-9 _)" - docker exec -i "$SERVICE" sh -lc ' + docker exec -i "$SERVICE" sh -lc ' db="$1" u="${MYSQL_USER-}" umask 077 - f="$(mktemp)"; trap "rm -f $f" EXIT + f="$(mktemp)"; trap "rm -f \"$f\"" EXIT cat >"$f" <&2 - ;; + echo "OK: database '$db' ensured${USER:+ + privileges granted to $USER}" >&2 + ;; - drop-db) - shift - local db="${1:-}" - [[ -n "${db:-}" ]] || die "drop-db requires a db name." - [[ "$db" =~ ^[A-Za-z0-9_]+$ ]] || die "invalid db name '$db' (allowed: A-Z a-z 0-9 _)" + drop-db) + shift + local db="${1:-}" + [[ -n "${db:-}" ]] || die "drop-db requires a db name." + [[ "$db" =~ ^[A-Za-z0-9_]+$ ]] || die "invalid db name '$db' (allowed: A-Z a-z 0-9 _)" - docker exec -i "$SERVICE" sh -lc ' + docker exec -i "$SERVICE" sh -lc ' db="$1" umask 077 - f="$(mktemp)"; trap "rm -f $f" EXIT + f="$(mktemp)"; trap "rm -f \"$f\"" EXIT cat >"$f" <&2 - ;; + echo "OK: dropped database '$db' (if existed)" >&2 + ;; - import) - shift - local in="${1:-}"; shift || true - [[ -n "${in:-}" ]] || die "import requires ." - - local file - if ! file="$(resolve_import_path "$in")"; then - die "file not found: $in (also tried ./${in}.sql and ./${in}.sql.gz)" - fi + import) + shift + local in="${1:-}"; shift || true + [[ -n "${in:-}" ]] || die "import requires ." - local db="${1:-}" - if [[ -z "${db:-}" ]]; then - # If shorthand NAME was used, default DB = NAME - if [[ "$in" != *.* ]]; then db="$in"; else db="${DEF_DB:-}"; fi - fi - [[ -n "${db:-}" ]] || die "import requires a db name (or set MYSQL_DATABASE)." - [[ "$db" =~ ^[A-Za-z0-9_]+$ ]] || die "invalid db name '$db' (allowed: A-Z a-z 0-9 _)" + local file + if ! file="$(resolve_import_path "$in")"; then + die "file not found: $in (also tried ./${in}.sql and ./${in}.sql.gz)" + fi - if [[ "$file" == *.gz ]]; then - if command -v gunzip >/dev/null 2>&1; then - gunzip -c "$file" | docker exec -i "$SERVICE" sh -lc ' - db="$1" - umask 077 - f="$(mktemp)"; trap "rm -f $f" EXIT - cat >"$f" </dev/null 2>&1; then + if [[ "$as_user" == true ]]; then + gunzip -c "$file" | mysql_import_stream_user "$db" + else + gunzip -c "$file" | mysql_import_stream_root "$db" + fi + else + die "gunzip not found on host; cannot import .gz" + fi else - die "gunzip not found on host; cannot import .gz" + if [[ "$as_user" == true ]]; then + cat "$file" | mysql_import_stream_user "$db" + else + cat "$file" | mysql_import_stream_root "$db" + fi fi - else - cat "$file" | docker exec -i "$SERVICE" sh -lc ' - db="$1" - umask 077 - f="$(mktemp)"; trap "rm -f $f" EXIT - cat >"$f" < db '$db'" >&2 - ;; + echo "OK: imported '$file' -> db '$db'" >&2 + ;; - export|dump) - shift - local db="${1:-$DEF_DB}" - [[ -n "${db:-}" ]] || die "dump requires a db name (or set MYSQL_DATABASE)." - [[ "$db" =~ ^[A-Za-z0-9_]+$ ]] || die "invalid db name '$db' (allowed: A-Z a-z 0-9 _)" + export|dump) + shift + local db="${1:-$DEF_DB}" + [[ -n "${db:-}" ]] || die "dump requires a db name (or set MYSQL_DATABASE)." + [[ "$db" =~ ^[A-Za-z0-9_]+$ ]] || die "invalid db name '$db' (allowed: A-Z a-z 0-9 _)" - local out="${2:-${db}.sql}" - # Write on HOST (fixes /workspace permission + uid mapping issues) - umask 077 + local out="${2:-${db}.sql}" + umask 077 - if [[ "$out" == *.gz ]]; then - if command -v gzip >/dev/null 2>&1; then - mysqldump_stdout_root "$db" | gzip -c > "$out" - else - # fallback gzip inside container - docker exec -i "$SERVICE" sh -lc ' + if [[ "$out" == *.gz ]]; then + if command -v gzip >/dev/null 2>&1; then + mysqldump_stdout_root "$db" | gzip -c > "$out" + else + docker exec -i "$SERVICE" sh -lc ' umask 077 - f="$(mktemp)"; trap "rm -f $f" EXIT + f="$(mktemp)"; trap "rm -f \"$f\"" EXIT cat >"$f" < "$out" + fi + else + mysqldump_stdout_root "$db" > "$out" fi - else - mysqldump_stdout_root "$db" > "$out" - fi - - echo "OK: dumped '$db' -> ./$out" >&2 - ;; - *) - shift 0 + echo "OK: dumped '$db' -> ./$out" >&2 + ;; - # explicit -e passthrough - if has_execute_flag "$@"; then - if [[ "$as_user" == true ]]; then mysql_exec_user "$@"; else mysql_exec_root "$@"; fi - exit 0 - fi + *) + if has_execute_flag "$@"; then + if [[ "$as_user" == true ]]; then mysql_exec_user "$@"; else mysql_exec_root "$@"; fi + exit 0 + fi - # Convenience: my "SELECT 1" - if [[ $# -eq 1 ]]; then - if looks_like_sql "$1"; then + if [[ $# -eq 1 ]] && looks_like_sql "$1"; then [[ -n "${DEF_DB:-}" ]] || die "No default database set (MYSQL_DATABASE). Provide db: my \"SQL...\"" if [[ "$as_user" == true ]]; then - exec docker exec "$(is_tty)" "$SERVICE" sh -lc ' - db="$1"; sql="$2" - umask 077 - f="$(mktemp)"; trap "rm -f $f" EXIT - cat >"$f" <"$f" <"$f" <"$f" </dev/null 2>&1; then - posix_root="$(cygpath -u "$WORKDIR_WIN" 2>/dev/null || true)" - fi + if [[ -z "$posix_root" && -n "${WORKDIR_WIN:-}" ]] && command -v cygpath >/dev/null 2>&1; then + posix_root="$(cygpath -u "$WORKDIR_WIN" 2>/dev/null || true)" fi if [[ -z "$posix_root" && -n "${WORKING_DIR:-}" ]]; then if [[ -d "$WORKING_DIR" ]]; then posix_root="$WORKING_DIR" - else - if command -v cygpath >/dev/null 2>&1; then - posix_root="$(cygpath -u "$WORKING_DIR" 2>/dev/null || true)" - fi + elif command -v cygpath >/dev/null 2>&1; then + posix_root="$(cygpath -u "$WORKING_DIR" 2>/dev/null || true)" fi fi [[ -n "$posix_root" && -d "$posix_root" ]] || posix_root="$PWD" posix_root="$(cd "$posix_root" && pwd)" + # Git Bash can sometimes resolve to a synthetic workspace under Program Files/Git. + # Prefer the repository root in that case to avoid invalid Docker Desktop mounts. + if is_msys && [[ "$posix_root" == *"/Program Files/Git/workspace"* || "$posix_root" == *"/Program Files/Git/workspace/"* ]]; then + posix_root="$ROOT_DIR" + fi + if is_msys && command -v cygpath >/dev/null 2>&1; then host_path="$(cygpath -w "$posix_root")" else @@ -79,21 +81,27 @@ resolve_mount_root() { # ───────────────────────────────────────────────────────────────────────────── # Optional host .env load (whitelist only; optional fallback) # ───────────────────────────────────────────────────────────────────────────── +load_env_value() { + local raw="${1:-}" + raw="${raw%$'\r'}" + raw="${raw#\"}" + raw="${raw%\"}" + raw="${raw#\'}" + raw="${raw%\'}" + printf '%s' "$raw" +} + if [[ -r "$ENV_FILE" ]]; then while IFS='=' read -r k v; do [[ -z "${k:-}" || "$k" =~ ^[[:space:]]*# ]] && continue k="${k//[[:space:]]/}" case "$k" in - POSTGRESQL_CONTAINER | POSTGRES_CONTAINER | POSTGRES_PORT_IN | WORKING_DIR) ;; - *) continue ;; + POSTGRESQL_CONTAINER|POSTGRES_CONTAINER|POSTGRES_PORT_IN|WORKING_DIR|WORKDIR|WORKDIR_WIN) ;; + *) continue ;; esac - v="${v%$'\r'}" - v="${v#\"}" - v="${v%\"}" - v="${v#\'}" - v="${v%\'}" + v="$(load_env_value "${v:-}")" export "$k=$v" - done <"$ENV_FILE" + done < "$ENV_FILE" fi SERVICE="${POSTGRESQL_CONTAINER:-${POSTGRES_CONTAINER:-POSTGRESQL}}" @@ -223,14 +231,29 @@ NOTES: TXT } -case "${1:-}" in help | -h | --help) - usage - exit 0 - ;; +case "${1:-}" in + help|-h|--help) + usage + exit 0 + ;; esac -require_container() { docker inspect "$SERVICE" >/dev/null 2>&1 || die "container '$SERVICE' is not running."; } -is_tty() { [[ -t 0 ]] && echo "-it" || echo "-i"; } +require_container() { + docker inspect "$SERVICE" >/dev/null 2>&1 || die "container '$SERVICE' does not exist." + local running + running="$(docker inspect -f '{{.State.Running}}' "$SERVICE" 2>/dev/null || true)" + [[ "$running" == "true" ]] || die "container '$SERVICE' is not running." +} + +is_tty() { [[ -t 0 && -t 1 ]]; } + +docker_tty_flags() { + if is_tty; then + printf '%s\n' '-it' + else + printf '%s\n' '-i' + fi +} # POSIX-safe env read from container cenv() { @@ -242,10 +265,10 @@ cenv_any() { local v key for key in "$@"; do v="$(cenv "$key")" - [[ -n "${v:-}" ]] && { - printf "%s" "$v" + if [[ -n "${v:-}" ]]; then + printf '%s' "$v" return 0 - } + fi done return 1 } @@ -256,13 +279,19 @@ infer_port() { POSTGRES_PORT_IN="${POSTGRES_PORT_IN:-5432}" } -pg_image() { docker inspect -f '{{.Config.Image}}' "$SERVICE" 2>/dev/null || die "failed to inspect image for '$SERVICE'."; } +pg_image() { + docker inspect -f '{{.Config.Image}}' "$SERVICE" 2>/dev/null || die "failed to inspect image for '$SERVICE'." +} run_pg_mount() { local image="$1" shift local flags=() - [[ -t 0 ]] && flags+=(-it) || flags+=(-i) + if is_tty; then + flags=(-it) + else + flags=(-i) + fi msys_no_pathconv docker run --rm "${flags[@]}" \ -v "$MOUNT_HOST_PATH":/workspace -w /workspace \ @@ -271,43 +300,58 @@ run_pg_mount() { } # file -> /workspace/ if under mount root; else empty +file_abs_path() { + local file="$1" + cd "$(dirname "$file")" && printf '%s/%s' "$PWD" "$(basename "$file")" +} + to_workspace_path_under_mount() { local file="$1" [[ -f "$file" ]] || return 1 local abs rel="" - abs="$(cd "$(dirname "$file")" && pwd)/$(basename "$file")" + abs="$(file_abs_path "$file")" if command -v python3 >/dev/null 2>&1; then - rel="$( - python3 - "$MOUNT_POSIX_ROOT" "$abs" <<'PY' 2>/dev/null || true + rel="$(python3 - "$MOUNT_POSIX_ROOT" "$abs" <<'PY' 2>/dev/null || true import os,sys -root=sys.argv[1]; p=sys.argv[2] +root=sys.argv[1] +p=sys.argv[2] try: - r=os.path.relpath(p,root) - if r and not r.startswith("..") and not os.path.isabs(r): - print(r) + r=os.path.relpath(p, root) + if r and not r.startswith('..') and not os.path.isabs(r): + print(r) except Exception: - pass + pass PY - )" +)" else case "$abs/" in - "$MOUNT_POSIX_ROOT"/*) rel="${abs#"$MOUNT_POSIX_ROOT/"}" ;; - *) rel="" ;; + "$MOUNT_POSIX_ROOT"/*) rel="${abs#"$MOUNT_POSIX_ROOT/"}" ;; + *) rel="" ;; esac fi [[ -n "$rel" ]] || return 1 - printf "/workspace/%s" "$rel" + printf '/workspace/%s' "$rel" } psql_exec() { - local flags - flags="$(is_tty)" local db="$1" user="$2" pass="$3" shift 3 - exec docker exec $flags -e "PGPASSWORD=$pass" "$SERVICE" \ - psql -h127.0.0.1 -p"$POSTGRES_PORT_IN" -U"$user" ${db:+-d "$db"} "$@" + + local docker_flags=() + if is_tty; then + docker_flags=(-it) + else + docker_flags=(-i) + fi + + local psql_args=(-h127.0.0.1 -p"$POSTGRES_PORT_IN" -U"$user") + [[ -n "$db" ]] && psql_args+=(-d "$db") + psql_args+=("$@") + + exec docker exec "${docker_flags[@]}" -e "PGPASSWORD=$pass" "$SERVICE" \ + psql "${psql_args[@]}" } is_safe_ident() { [[ "$1" =~ ^[A-Za-z0-9_]+$ ]]; } @@ -356,7 +400,7 @@ main() { resolve_mount_root msys_no_pathconv - # Your container env names: + # Container env names: # POSTGRES_PORT, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DATABASE local PGUSER PGPASS PGDB IMAGE PGUSER="$(cenv_any POSTGRES_USER PGUSER || true)" @@ -380,243 +424,241 @@ main() { fi case "${1:-}" in - status) - docker ps --filter "name=^/${SERVICE}$" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" - exit 0 - ;; + status) + docker ps -a --filter "name=^/${SERVICE}$" --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' + exit 0 + ;; esac local cmd="${1:-}" case "$cmd" in - dbs) - psql_exec "${PGDB:-postgres}" "$PGUSER" "$PGPASS" -v ON_ERROR_STOP=1 -c '\l' - ;; - - tables) - shift - local db="${1:-$PGDB}" - [[ -n "$db" ]] || die "tables requires a db name (or set POSTGRES_DATABASE)." - psql_exec "$db" "$PGUSER" "$PGPASS" -v ON_ERROR_STOP=1 -c '\dt *.*' - ;; - - create-db) - shift - local db="${1:-$PGDB}" - [[ -n "$db" ]] || die "create-db requires a db name." - ensure_db "$db" "$PGUSER" "$PGPASS" - ;; - - drop-db) - shift - local db="${1:-}" - [[ -n "$db" ]] || die "drop-db requires a db name." - drop_db "$db" "$PGUSER" "$PGPASS" - ;; - - import) - shift - local in="${1:-}" - shift || true - [[ -n "$in" ]] || die "import requires ." - - local file="$in" - local db="${1:-}" - - # shorthand: "import reporting_db" -> ./reporting_db.sql(.gz), db defaults to reporting_db - if [[ ! -f "$file" ]]; then - if [[ -f "./${in}.sql" ]]; then - file="./${in}.sql" - [[ -n "${db:-}" ]] || db="$in" - elif [[ -f "./${in}.sql.gz" ]]; then - file="./${in}.sql.gz" - [[ -n "${db:-}" ]] || db="$in" + dbs) + psql_exec "${PGDB:-postgres}" "$PGUSER" "$PGPASS" -v ON_ERROR_STOP=1 -c '\l' + ;; + + tables) + shift + local db="${1:-$PGDB}" + [[ -n "$db" ]] || die "tables requires a db name (or set POSTGRES_DATABASE)." + psql_exec "$db" "$PGUSER" "$PGPASS" -v ON_ERROR_STOP=1 -c '\dt *.*' + ;; + + create-db) + shift + local db="${1:-$PGDB}" + [[ -n "$db" ]] || die "create-db requires a db name." + ensure_db "$db" "$PGUSER" "$PGPASS" + ;; + + drop-db) + shift + local db="${1:-}" + [[ -n "$db" ]] || die "drop-db requires a db name." + drop_db "$db" "$PGUSER" "$PGPASS" + ;; + + import) + shift + local in="${1:-}" + shift || true + [[ -n "$in" ]] || die "import requires ." + + local file="$in" + local db="${1:-}" + + if [[ ! -f "$file" ]]; then + if [[ -f "./${in}.sql" ]]; then + file="./${in}.sql" + [[ -n "${db:-}" ]] || db="$in" + elif [[ -f "./${in}.sql.gz" ]]; then + file="./${in}.sql.gz" + [[ -n "${db:-}" ]] || db="$in" + fi fi - fi - [[ -f "$file" ]] || die "file not found: $file" - [[ -n "${db:-$PGDB}" ]] || die "import requires a db name (or set POSTGRES_DATABASE)." - db="${db:-$PGDB}" + [[ -f "$file" ]] || die "file not found: $file" + [[ -n "${db:-$PGDB}" ]] || die "import requires a db name (or set POSTGRES_DATABASE)." + db="${db:-$PGDB}" - local ws="" - ws="$(to_workspace_path_under_mount "$file" || true)" - if [[ -n "$ws" ]]; then - if [[ "$file" == *.gz ]]; then - run_pg_mount "$IMAGE" sh -lc ' + local ws="" + ws="$(to_workspace_path_under_mount "$file" || true)" + if [[ -n "$ws" ]]; then + if [[ "$file" == *.gz ]]; then + run_pg_mount "$IMAGE" sh -lc ' f="$1"; db="$2"; user="$3"; pass="$4"; port="$5" gunzip -c "$f" | PGPASSWORD="$pass" psql -h127.0.0.1 -p"$port" -U"$user" -d "$db" -v ON_ERROR_STOP=1 ' sh "$ws" "$db" "$PGUSER" "$PGPASS" "$POSTGRES_PORT_IN" - else - run_pg_mount "$IMAGE" sh -lc ' + else + run_pg_mount "$IMAGE" sh -lc ' f="$1"; db="$2"; user="$3"; pass="$4"; port="$5" PGPASSWORD="$pass" psql -h127.0.0.1 -p"$port" -U"$user" -d "$db" -v ON_ERROR_STOP=1 -f "$f" ' sh "$ws" "$db" "$PGUSER" "$PGPASS" "$POSTGRES_PORT_IN" - fi - else - if [[ "$file" == *.gz ]]; then - gunzip -c "$file" | docker exec -i -e "PGPASSWORD=$PGPASS" "$SERVICE" \ - psql -h127.0.0.1 -p"$POSTGRES_PORT_IN" -U"$PGUSER" -d "$db" -v ON_ERROR_STOP=1 + fi else - cat "$file" | docker exec -i -e "PGPASSWORD=$PGPASS" "$SERVICE" \ - psql -h127.0.0.1 -p"$POSTGRES_PORT_IN" -U"$PGUSER" -d "$db" -v ON_ERROR_STOP=1 + if [[ "$file" == *.gz ]]; then + gunzip -c "$file" | docker exec -i -e "PGPASSWORD=$PGPASS" "$SERVICE" \ + psql -h127.0.0.1 -p"$POSTGRES_PORT_IN" -U"$PGUSER" -d "$db" -v ON_ERROR_STOP=1 + else + cat "$file" | docker exec -i -e "PGPASSWORD=$PGPASS" "$SERVICE" \ + psql -h127.0.0.1 -p"$POSTGRES_PORT_IN" -U"$PGUSER" -d "$db" -v ON_ERROR_STOP=1 + fi fi - fi - echo "OK: imported '$file' -> db '$db'" >&2 - ;; + echo "OK: imported '$file' -> db '$db'" >&2 + ;; - dump) - shift - local db="${1:-$PGDB}" - [[ -n "$db" ]] || die "dump requires a db name (or set POSTGRES_DATABASE)." - local out="${2:-${db}.sql}" + dump) + shift + local db="${1:-$PGDB}" + [[ -n "$db" ]] || die "dump requires a db name (or set POSTGRES_DATABASE)." + local out="${2:-${db}.sql}" - if [[ "$out" == *.gz ]]; then - run_pg_mount "$IMAGE" sh -lc ' + if [[ "$out" == *.gz ]]; then + run_pg_mount "$IMAGE" sh -lc ' db="$1"; out="$2"; user="$3"; pass="$4"; port="$5" PGPASSWORD="$pass" pg_dump -h127.0.0.1 -p"$port" -U"$user" -d "$db" --no-owner --no-privileges \ | gzip -c > "/workspace/$out" ' sh "$db" "$out" "$PGUSER" "$PGPASS" "$POSTGRES_PORT_IN" - else - run_pg_mount "$IMAGE" sh -lc ' + else + run_pg_mount "$IMAGE" sh -lc ' db="$1"; out="$2"; user="$3"; pass="$4"; port="$5" PGPASSWORD="$pass" pg_dump -h127.0.0.1 -p"$port" -U"$user" -d "$db" --no-owner --no-privileges \ > "/workspace/$out" ' sh "$db" "$out" "$PGUSER" "$PGPASS" "$POSTGRES_PORT_IN" - fi - echo "OK: dumped '$db' -> ./$out" >&2 - ;; + fi + echo "OK: dumped '$db' -> ./$out" >&2 + ;; - dump-all) - shift - local out="${1:-all.sql}" - if [[ "$out" == *.gz ]]; then - run_pg_mount "$IMAGE" sh -lc ' + dump-all) + shift + local out="${1:-all.sql}" + if [[ "$out" == *.gz ]]; then + run_pg_mount "$IMAGE" sh -lc ' out="$1"; user="$2"; pass="$3"; port="$4" PGPASSWORD="$pass" pg_dumpall -h127.0.0.1 -p"$port" -U"$user" \ | gzip -c > "/workspace/$out" ' sh "$out" "$PGUSER" "$PGPASS" "$POSTGRES_PORT_IN" - else - run_pg_mount "$IMAGE" sh -lc ' + else + run_pg_mount "$IMAGE" sh -lc ' out="$1"; user="$2"; pass="$3"; port="$4" PGPASSWORD="$pass" pg_dumpall -h127.0.0.1 -p"$port" -U"$user" \ > "/workspace/$out" ' sh "$out" "$PGUSER" "$PGPASS" "$POSTGRES_PORT_IN" - fi - echo "OK: dumped all databases -> ./$out" >&2 - ;; + fi + echo "OK: dumped all databases -> ./$out" >&2 + ;; - roles) - shift - local out="${1:-globals.sql}" - if [[ "$out" == *.gz ]]; then - run_pg_mount "$IMAGE" sh -lc ' + roles) + shift + local out="${1:-globals.sql}" + if [[ "$out" == *.gz ]]; then + run_pg_mount "$IMAGE" sh -lc ' out="$1"; user="$2"; pass="$3"; port="$4" PGPASSWORD="$pass" pg_dumpall -h127.0.0.1 -p"$port" -U"$user" --globals-only \ | gzip -c > "/workspace/$out" ' sh "$out" "$PGUSER" "$PGPASS" "$POSTGRES_PORT_IN" - else - run_pg_mount "$IMAGE" sh -lc ' + else + run_pg_mount "$IMAGE" sh -lc ' out="$1"; user="$2"; pass="$3"; port="$4" PGPASSWORD="$pass" pg_dumpall -h127.0.0.1 -p"$port" -U"$user" --globals-only \ > "/workspace/$out" ' sh "$out" "$PGUSER" "$PGPASS" "$POSTGRES_PORT_IN" - fi - echo "OK: dumped globals (roles) -> ./$out" >&2 - ;; - - schema) - shift - local db="${1:-$PGDB}" - [[ -n "$db" ]] || die "schema requires a db name." - local out="${2:-${db}.schema.sql}" - if [[ "$out" == *.gz ]]; then - run_pg_mount "$IMAGE" sh -lc ' + fi + echo "OK: dumped globals (roles) -> ./$out" >&2 + ;; + + schema) + shift + local db="${1:-$PGDB}" + [[ -n "$db" ]] || die "schema requires a db name." + local out="${2:-${db}.schema.sql}" + if [[ "$out" == *.gz ]]; then + run_pg_mount "$IMAGE" sh -lc ' db="$1"; out="$2"; user="$3"; pass="$4"; port="$5" PGPASSWORD="$pass" pg_dump -h127.0.0.1 -p"$port" -U"$user" -d "$db" --schema-only --no-owner --no-privileges \ | gzip -c > "/workspace/$out" ' sh "$db" "$out" "$PGUSER" "$PGPASS" "$POSTGRES_PORT_IN" - else - run_pg_mount "$IMAGE" sh -lc ' + else + run_pg_mount "$IMAGE" sh -lc ' db="$1"; out="$2"; user="$3"; pass="$4"; port="$5" PGPASSWORD="$pass" pg_dump -h127.0.0.1 -p"$port" -U"$user" -d "$db" --schema-only --no-owner --no-privileges \ > "/workspace/$out" ' sh "$db" "$out" "$PGUSER" "$PGPASS" "$POSTGRES_PORT_IN" - fi - echo "OK: schema dumped '$db' -> ./$out" >&2 - ;; - - data) - shift - local db="${1:-$PGDB}" - [[ -n "$db" ]] || die "data requires a db name." - local out="${2:-${db}.data.sql}" - if [[ "$out" == *.gz ]]; then - run_pg_mount "$IMAGE" sh -lc ' + fi + echo "OK: schema dumped '$db' -> ./$out" >&2 + ;; + + data) + shift + local db="${1:-$PGDB}" + [[ -n "$db" ]] || die "data requires a db name." + local out="${2:-${db}.data.sql}" + if [[ "$out" == *.gz ]]; then + run_pg_mount "$IMAGE" sh -lc ' db="$1"; out="$2"; user="$3"; pass="$4"; port="$5" PGPASSWORD="$pass" pg_dump -h127.0.0.1 -p"$port" -U"$user" -d "$db" --data-only --no-owner --no-privileges \ | gzip -c > "/workspace/$out" ' sh "$db" "$out" "$PGUSER" "$PGPASS" "$POSTGRES_PORT_IN" - else - run_pg_mount "$IMAGE" sh -lc ' + else + run_pg_mount "$IMAGE" sh -lc ' db="$1"; out="$2"; user="$3"; pass="$4"; port="$5" PGPASSWORD="$pass" pg_dump -h127.0.0.1 -p"$port" -U"$user" -d "$db" --data-only --no-owner --no-privileges \ > "/workspace/$out" ' sh "$db" "$out" "$PGUSER" "$PGPASS" "$POSTGRES_PORT_IN" - fi - echo "OK: data dumped '$db' -> ./$out" >&2 - ;; - - list) - shift - local file="${1:-}" - [[ -n "$file" ]] || die "list requires a dump file." - [[ -f "$file" ]] || die "file not found: $file" - - local ws="" - ws="$(to_workspace_path_under_mount "$file" || true)" - if [[ -n "$ws" ]]; then - run_pg_mount "$IMAGE" sh -lc ' + fi + echo "OK: data dumped '$db' -> ./$out" >&2 + ;; + + list) + shift + local file="${1:-}" + [[ -n "$file" ]] || die "list requires a dump file." + [[ -f "$file" ]] || die "file not found: $file" + + local ws="" + ws="$(to_workspace_path_under_mount "$file" || true)" + if [[ -n "$ws" ]]; then + run_pg_mount "$IMAGE" sh -lc ' file="$1"; user="$2"; pass="$3"; port="$4" PGPASSWORD="$pass" exec pg_restore -h127.0.0.1 -p"$port" -U"$user" -l "$file" ' sh "$ws" "$PGUSER" "$PGPASS" "$POSTGRES_PORT_IN" - else - cat "$file" | docker exec -i -e "PGPASSWORD=$PGPASS" "$SERVICE" \ - pg_restore -h127.0.0.1 -p"$POSTGRES_PORT_IN" -U"$PGUSER" -l - - fi - ;; - - restore) - shift - local file="${1:-}" - shift || true - [[ -n "$file" ]] || die "restore requires a dump file." - [[ -f "$file" ]] || die "file not found: $file" - - local db="${1:-$PGDB}" - [[ -n "$db" ]] || die "restore requires a db name (or set POSTGRES_DATABASE)." - ensure_db "$db" "$PGUSER" "$PGPASS" - - local ws="" - ws="$(to_workspace_path_under_mount "$file" || true)" - if [[ -n "$ws" ]]; then - run_pg_mount "$IMAGE" sh -lc ' + else + cat "$file" | docker exec -i -e "PGPASSWORD=$PGPASS" "$SERVICE" \ + pg_restore -h127.0.0.1 -p"$POSTGRES_PORT_IN" -U"$PGUSER" -l - + fi + ;; + + restore) + shift + local file="${1:-}" + shift || true + [[ -n "$file" ]] || die "restore requires a dump file." + [[ -f "$file" ]] || die "file not found: $file" + + local db="${1:-$PGDB}" + [[ -n "$db" ]] || die "restore requires a db name (or set POSTGRES_DATABASE)." + ensure_db "$db" "$PGUSER" "$PGPASS" + + local ws="" + ws="$(to_workspace_path_under_mount "$file" || true)" + if [[ -n "$ws" ]]; then + run_pg_mount "$IMAGE" sh -lc ' file="$1"; db="$2"; user="$3"; pass="$4"; port="$5" PGPASSWORD="$pass" exec pg_restore -h127.0.0.1 -p"$port" -U"$user" -d "$db" \ --clean --if-exists --no-owner --no-privileges "$file" ' sh "$ws" "$db" "$PGUSER" "$PGPASS" "$POSTGRES_PORT_IN" - else - cat "$file" | docker exec -i -e "PGPASSWORD=$PGPASS" "$SERVICE" \ - pg_restore -h127.0.0.1 -p"$POSTGRES_PORT_IN" -U"$PGUSER" -d "$db" \ - --clean --if-exists --no-owner --no-privileges - - fi + else + cat "$file" | docker exec -i -e "PGPASSWORD=$PGPASS" "$SERVICE" \ + pg_restore -h127.0.0.1 -p"$POSTGRES_PORT_IN" -U"$PGUSER" -d "$db" \ + --clean --if-exists --no-owner --no-privileges - + fi - echo "OK: restored '$file' -> db '$db'" >&2 - ;; + echo "OK: restored '$file' -> db '$db'" >&2 + ;; - *) - # passthrough (psql args...) - psql_exec "${PGDB:-}" "$PGUSER" "$PGPASS" "$@" - ;; + *) + psql_exec "${PGDB:-}" "$PGUSER" "$PGPASS" "$@" + ;; esac } diff --git a/bin/php b/bin/php index ca1955a..cee6731 100755 --- a/bin/php +++ b/bin/php @@ -17,23 +17,84 @@ else C_YELLOW="" C_DIM="" fi + kv() { printf "%s%s%s %s%s%s\n" "$C_CYAN" "$1" "$C_RESET" "$C_YELLOW" "$2" "$C_RESET" >&2; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +SERVER_TOOLS_CONTAINER="${SERVER_TOOLS_CONTAINER:-SERVER_TOOLS}" normalize_container() { - local v="$1" + local v="${1:-}" [[ -z "$v" ]] && return 0 if [[ "$v" =~ ^PHP_[0-9]+\.[0-9]+$ ]]; then - echo "$v" + printf '%s\n' "$v" elif [[ "$v" =~ ^[0-9]+\.[0-9]+$ ]]; then - echo "PHP_$v" + printf 'PHP_%s\n' "$v" else die "invalid version '$v' (use like 8.4)" fi } +container_is_running() { + local name="$1" + docker ps --format '{{.Names}}' | grep -qx "$name" +} + +running_php_containers() { + docker ps --format '{{.Names}}' | awk '/^PHP_[0-9]+\.[0-9]+$/' | sort -u +} + +pick_highest_php_container() { + local names + names="$(running_php_containers || true)" + [[ -n "$names" ]] || return 1 + printf '%s\n' "$names" | sed 's/^PHP_//' | sort -Vr | head -n1 | awk 'NF{print "PHP_"$0}' +} + +php_container_version() { + local name="$1" + [[ "$name" =~ ^PHP_([0-9]+\.[0-9]+)$ ]] || return 1 + printf '%s\n' "${BASH_REMATCH[1]}" +} + +server_tools_php_version() { + container_is_running "$SERVER_TOOLS_CONTAINER" || return 1 + docker exec "$SERVER_TOOLS_CONTAINER" sh -lc 'command -v php >/dev/null 2>&1 && php -r "echo PHP_MAJOR_VERSION, \".\", PHP_MINOR_VERSION;"' 2>/dev/null | tr -d '\r\n' +} + +resolve_host_pwd() { + local host="${WORKDIR_WIN:-${WORKING_DIR:-}}" + if [[ -z "$host" ]]; then + host="$PWD" + fi + [[ -n "$host" ]] || host="$REPO_ROOT" + + if command -v cygpath >/dev/null 2>&1; then + [[ "$host" == /* ]] && host="$(cygpath -w "$host")" + fi + + case "$host" in + *"Program Files"/Git/workspace*|*"Program Files\\Git\\workspace"*) host="$REPO_ROOT" ;; + esac + + printf '%s' "$host" +} + +is_msys() { + case "${OSTYPE:-}" in + msys*|cygwin*) return 0 ;; + *) return 1 ;; + esac +} + +msys_no_pathconv() { + if is_msys; then + export MSYS_NO_PATHCONV=1 + export MSYS2_ARG_CONV_EXCL='*' + fi +} + relpath_under_pwd() { local p="$1" local cwd abs rel @@ -41,17 +102,15 @@ relpath_under_pwd() { abs="$(cd "$(dirname "$p")" && pwd)/$(basename "$p")" if command -v python3 >/dev/null 2>&1; then - rel="$( - python3 - "$cwd" "$abs" <<'PY' 2>/dev/null || true + rel="$({ python3 - "$cwd" "$abs" <<'PY' import os,sys cwd=sys.argv[1]; p=sys.argv[2] try: - r=os.path.relpath(p,cwd) - print(r) + print(os.path.relpath(p, cwd)) except Exception: pass PY - )" + } 2>/dev/null || true)" else if [[ "$abs" == "$cwd/"* ]]; then rel="${abs#"$cwd/"}" @@ -61,7 +120,7 @@ PY fi [[ -n "${rel:-}" && "$rel" != /* && "$rel" != ..* ]] || return 1 - printf "%s" "$rel" + printf '%s' "$rel" } port_in_use() { @@ -81,39 +140,9 @@ port_in_use() { return 1 } -pick_highest_php_container() { - local names - names="$(docker ps --format '{{.Names}}' | awk '/^PHP_[0-9]+\.[0-9]+$/')" - [[ -n "$names" ]] || return 1 - printf "%s\n" "$names" | sed 's/^PHP_//' | sort -Vr | head -n1 | awk 'NF{print "PHP_"$0}' -} - -resolve_host_pwd() { - local host="${WORKDIR_WIN:-${WORKING_DIR:-}}" - if [[ -z "${host}" ]]; then - host="$PWD" - fi - [[ -n "${host}" ]] || host="$REPO_ROOT" - - if command -v cygpath >/dev/null 2>&1; then - [[ "$host" == /* ]] && host="$(cygpath -w "$host")" - fi - - printf '%s' "$host" -} - -is_msys() { - case "${OSTYPE:-}" in - msys* | cygwin*) return 0 ;; - *) return 1 ;; - esac -} - -msys_no_pathconv() { - if is_msys; then - export MSYS_NO_PATHCONV=1 - export MSYS2_ARG_CONV_EXCL='*' - fi +pick_container_network() { + local name="$1" + docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{println $k}}{{end}}' "$name" 2>/dev/null | awk 'NF{print; exit}' } EXPLICIT_VER="" @@ -121,39 +150,75 @@ ARGS=() while [[ $# -gt 0 ]]; do case "$1" in - -V | --v | --php) - [[ -n "${2:-}" ]] || die "$1 requires a version (e.g. $1 8.4)" - EXPLICIT_VER="$2" - shift 2 - ;; - --) - shift - ARGS+=("$@") - break - ;; - *) - ARGS+=("$1") - shift - ;; + -V|--v|--php) + [[ -n "${2:-}" ]] || die "$1 requires a version (e.g. $1 8.4)" + EXPLICIT_VER="$2" + shift 2 + ;; + --) + shift + ARGS+=("$@") + break + ;; + *) + ARGS+=("$1") + shift + ;; esac done -TARGET="$(normalize_container "$EXPLICIT_VER")" - -if [[ -n "$TARGET" ]]; then - docker ps --format '{{.Names}}' | grep -qx "$TARGET" || die "container '$TARGET' is not running." +TARGET_CONTAINER="" +TARGET_IMAGE="" +NETWORK_SOURCE_CONTAINER="" +RUNTIME_LABEL="" +SERVER_TOOLS_VERSION="$(server_tools_php_version || true)" +NORMALIZED_EXPLICIT="$(normalize_container "$EXPLICIT_VER")" + +if [[ -n "$NORMALIZED_EXPLICIT" ]]; then + if container_is_running "$NORMALIZED_EXPLICIT"; then + TARGET_CONTAINER="$NORMALIZED_EXPLICIT" + NETWORK_SOURCE_CONTAINER="$NORMALIZED_EXPLICIT" + TARGET_IMAGE="$(docker inspect -f '{{.Config.Image}}' "$NORMALIZED_EXPLICIT" 2>/dev/null)" || die "failed to inspect '$NORMALIZED_EXPLICIT'." + RUNTIME_LABEL="$NORMALIZED_EXPLICIT" + elif [[ -n "$SERVER_TOOLS_VERSION" && "$SERVER_TOOLS_VERSION" == "$(php_container_version "$NORMALIZED_EXPLICIT")" ]]; then + TARGET_CONTAINER="$SERVER_TOOLS_CONTAINER" + NETWORK_SOURCE_CONTAINER="$SERVER_TOOLS_CONTAINER" + TARGET_IMAGE="$(docker inspect -f '{{.Config.Image}}' "$SERVER_TOOLS_CONTAINER" 2>/dev/null)" || die "failed to inspect '$SERVER_TOOLS_CONTAINER'." + RUNTIME_LABEL="$SERVER_TOOLS_CONTAINER (php $SERVER_TOOLS_VERSION)" + else + die "requested PHP ${EXPLICIT_VER} is not available in a running PHP container or $SERVER_TOOLS_CONTAINER." + fi else - TARGET="$(pick_highest_php_container)" || die "no PHP container is running (expected names like PHP_8.4)." + if TARGET_CONTAINER="$(pick_highest_php_container)"; then + NETWORK_SOURCE_CONTAINER="$TARGET_CONTAINER" + TARGET_IMAGE="$(docker inspect -f '{{.Config.Image}}' "$TARGET_CONTAINER" 2>/dev/null)" || die "failed to inspect '$TARGET_CONTAINER'." + RUNTIME_LABEL="$TARGET_CONTAINER" + elif [[ -n "$SERVER_TOOLS_VERSION" ]]; then + TARGET_CONTAINER="$SERVER_TOOLS_CONTAINER" + NETWORK_SOURCE_CONTAINER="$SERVER_TOOLS_CONTAINER" + TARGET_IMAGE="$(docker inspect -f '{{.Config.Image}}' "$SERVER_TOOLS_CONTAINER" 2>/dev/null)" || die "failed to inspect '$SERVER_TOOLS_CONTAINER'." + RUNTIME_LABEL="$SERVER_TOOLS_CONTAINER (php $SERVER_TOOLS_VERSION)" + else + die "no PHP container is running and $SERVER_TOOLS_CONTAINER has no usable php runtime." + fi fi -IMAGE="$(docker inspect -f '{{.Config.Image}}' "$TARGET" 2>/dev/null)" || die "failed to inspect '$TARGET'." +[[ -n "$TARGET_IMAGE" ]] || die "failed to resolve runtime image." -kv "Container:" "$TARGET" -kv "Image:" "$IMAGE" +kv "Runtime:" "$RUNTIME_LABEL" +kv "Image:" "$TARGET_IMAGE" HOST_PWD="$(resolve_host_pwd)" msys_no_pathconv +common_run_flags() { + local -n _out="$1" + _out=(--rm -v "$HOST_PWD":/workspace -w /workspace) + [[ -t 0 ]] && _out+=(-it) || _out+=(-i) + _out+=(-u "$(id -u):$(id -g)") + _out+=(-e HOME=/tmp/php-home) +} + serve_mode() { local host="127.0.0.1" local port="8000" @@ -163,35 +228,35 @@ serve_mode() { while [[ $# -gt 0 ]]; do case "$1" in - --host=*) host="${1#*=}" ;; - --host) - shift - host="${1:-}" - ;; - --port=*) port="${1#*=}" ;; - --port) - shift - port="${1:-}" - ;; - --root=*) root_dir="${1#*=}" ;; - --root) - shift - root_dir="${1:-}" - ;; - --router=*) router_file="${1#*=}" ;; - --router) - shift - router_file="${1:-}" - ;; - -v | --verbose) verbose=true ;; - *) die "unknown serve option: $1" ;; + --host=*) host="${1#*=}" ;; + --host) + shift + host="${1:-}" + ;; + --port=*) port="${1#*=}" ;; + --port) + shift + port="${1:-}" + ;; + --root=*) root_dir="${1#*=}" ;; + --root) + shift + root_dir="${1:-}" + ;; + --router=*) router_file="${1#*=}" ;; + --router) + shift + router_file="${1:-}" + ;; + -v|--verbose) verbose=true ;; + *) die "unknown serve option: $1" ;; esac shift done [[ -n "${host:-}" ]] || die "serve: --host requires a value" [[ "$port" =~ ^[0-9]+$ ]] || die "serve: invalid --port '$port'" - ((port >= 1 && port <= 65535)) || die "serve: port out of range: $port" + (( port >= 1 && port <= 65535 )) || die "serve: port out of range: $port" if [[ "$root_dir" != /* ]]; then root_dir="$PWD/$root_dir"; fi [[ -d "$root_dir" ]] || die "serve: root directory not found: $root_dir" @@ -251,19 +316,22 @@ serve_mode() { publish=(-p "${host}:${port}:${port}") fi - local RUN_FLAGS=(--rm -v "$HOST_PWD":/workspace -w /workspace) - [[ -t 0 ]] && RUN_FLAGS+=(-it) || RUN_FLAGS+=(-i) - RUN_FLAGS+=(-u "$(id -u):$(id -g)") - RUN_FLAGS+=("${publish[@]}") + local network_name="" + network_name="$(pick_container_network "$NETWORK_SOURCE_CONTAINER" || true)" + + local run_flags=() + common_run_flags run_flags + run_flags+=("${publish[@]}") + [[ -n "$network_name" ]] && run_flags+=(--network "$network_name") if [[ -n "$router_rel" ]]; then - exec docker run "${RUN_FLAGS[@]}" "$IMAGE" php -S "0.0.0.0:${port}" -t "/workspace/${found_rel#./}" "/workspace/${router_rel#./}" + exec docker run "${run_flags[@]}" "$TARGET_IMAGE" php -S "0.0.0.0:${port}" -t "/workspace/${found_rel#./}" "/workspace/${router_rel#./}" fi if [[ "$found_rel" == "." ]]; then - exec docker run "${RUN_FLAGS[@]}" "$IMAGE" php -S "0.0.0.0:${port}" + exec docker run "${run_flags[@]}" "$TARGET_IMAGE" php -S "0.0.0.0:${port}" else - exec docker run "${RUN_FLAGS[@]}" "$IMAGE" php -S "0.0.0.0:${port}" -t "/workspace/${found_rel#./}" + exec docker run "${run_flags[@]}" "$TARGET_IMAGE" php -S "0.0.0.0:${port}" -t "/workspace/${found_rel#./}" fi } @@ -271,8 +339,12 @@ if [[ "${ARGS[0]:-}" == "serve" ]]; then serve_mode "${ARGS[@]:1}" fi -RUN_FLAGS=(--rm -v "$HOST_PWD":/workspace -w /workspace) -[[ -t 0 ]] && RUN_FLAGS+=(-it) || RUN_FLAGS+=(-i) -RUN_FLAGS+=(-u "$(id -u):$(id -g)") +RUN_FLAGS=() +common_run_flags RUN_FLAGS +RUN_FLAGS+=(--network "container:${NETWORK_SOURCE_CONTAINER}") + +if [[ ${#ARGS[@]} -eq 0 ]]; then + exec docker run "${RUN_FLAGS[@]}" "$TARGET_IMAGE" php +fi -exec docker run "${RUN_FLAGS[@]}" "$IMAGE" php "${ARGS[@]}" +exec docker run "${RUN_FLAGS[@]}" "$TARGET_IMAGE" php "${ARGS[@]}" diff --git a/bin/redis-cli b/bin/redis-cli index 8a82277..eb7009e 100755 --- a/bin/redis-cli +++ b/bin/redis-cli @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -euo pipefail + die() { echo "Error: $*" >&2 exit 1 @@ -29,8 +30,13 @@ fi SERVICE="${REDIS_CONTAINER:-REDIS}" -require_container() { docker inspect "$SERVICE" >/dev/null 2>&1 || die "container '$SERVICE' is not running."; } -is_tty() { [[ -t 0 ]] && echo "-it" || echo "-i"; } +require_container() { + local running + running="$(docker inspect -f '{{.State.Running}}' "$SERVICE" 2>/dev/null || true)" + [[ "$running" == "true" ]] || die "container '$SERVICE' is not running." +} + +is_tty() { [[ -t 0 && -t 1 ]] && echo "-it" || echo "-i"; } # Read env var from container (best source of truth) cenv() { @@ -40,28 +46,35 @@ cenv() { redis_password() { local pw="${REDIS_PASSWORD:-}" - [[ -n "$pw" ]] && { + if [[ -n "$pw" ]]; then printf '%s' "$pw" return 0 - } + fi + pw="$(cenv REDIS_PASSWORD || true)" + if [[ -z "$pw" ]]; then + pw="$(cenv REDISCLI_AUTH || true)" + fi + printf '%s' "$pw" } redis_db() { local db="${REDIS_DB:-}" [[ -n "$db" ]] || db="0" + [[ "$db" =~ ^[0-9]+$ ]] || die "REDIS_DB must be numeric." printf '%s' "$db" } usage() { cat >&2 <<'TXT' Usage: - redis-cli # interactive + redis-cli # interactive redis-cli # passthrough Shortcuts: redis-cli db [cmd...] # run on a specific DB index + redis-cli ping redis-cli status TXT } @@ -72,6 +85,7 @@ redis_exec() { local pw db pw="$(redis_password)" db="$(redis_db)" + if [[ -n "$pw" ]]; then exec docker exec $flags -e "REDISCLI_AUTH=$pw" "$SERVICE" redis-cli -n "$db" "$@" else @@ -96,6 +110,9 @@ main() { docker ps --filter "name=^/${SERVICE}$" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" exit 0 ;; + ping) + redis_exec PING + ;; db) shift local idx="${1:-}" diff --git a/lds b/lds index 9292286..e5a8f82 100755 --- a/lds +++ b/lds @@ -4589,10 +4589,12 @@ main() { case "$cmd" in php | composer | node | npm | npx) exec "$DIR/bin/$cmd" "$@" ;; - pg | pg_restore | pg-restore | pgrestore | pg_dump | pgdump | pg-dump | psql) exec "$DIR/bin/pg" "$@" ;; + pg | psql | pg_restore | pg-restore | pgrestore | pg_dump | pgdump | pg-dump) exec "$DIR/bin/pg" "$@" ;; maria | mariadb | mariadbdump | mariadb-dump | mariadb_dump) exec "$DIR/bin/maria" "$@" ;; my | mysql | mysqldump | mysql-dump | mysql_dump) exec "$DIR/bin/my" "$@" ;; redis | redis-cli) exec "$DIR/bin/redis-cli" "$@" ;; + es | elastic | elasticsearch) exec "$DIR/bin/es" "$@" ;; + mongo | mongodb | mongosh | mongoimport | mongoexport) exec "$DIR/bin/mongo" "$@" ;; *) "cmd_$cmd" "$@" ;; esac } From 6d9216dcb38baf5f40cf84fa0303782f095fa00d Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 8 Mar 2026 21:03:43 +0600 Subject: [PATCH 33/48] lds core --- lds | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lds b/lds index e5a8f82..022b836 100755 --- a/lds +++ b/lds @@ -26,7 +26,7 @@ die() { " "${RED:-}" "${NC:-}" "$*" >&2 exit 1 } -LDS_STATUS_PROBE=1 + ############################################################################### # Command presence + tool proxy rules # - Host-only commands MUST exist on host (e.g. docker) @@ -110,7 +110,7 @@ lds_tools_cmd() { fi # 3) proxy-safe tools from SERVER_TOOLS (opt-in) - if ((${LDS_PROXY_TOOLS:-0})); then + if ((${LDS_PROXY_TOOLS:-1})); then if ! has_bin docker; then echo "Error: '$cmd' not available on host; tool proxy requires host 'docker'" >&2 return 127 @@ -149,7 +149,7 @@ has_tool() { local c="${1:-}" has_bin "$c" && return 0 _is_host_only_cmd "$c" && return 1 - ((${LDS_PROXY_TOOLS:-0})) || return 1 + ((${LDS_PROXY_TOOLS:-1})) || return 1 _server_tools_has "$c" } @@ -1823,7 +1823,7 @@ _status_show_networks() { # 5) Optional endpoint probes (gated) _status_show_probes() { - ((${LDS_STATUS_PROBE:-0})) || { + ((${LDS_STATUS_PROBE:-1})) || { printf "(disabled; set LDS_STATUS_PROBE=1)\n" return 0 } From 005702fd721541202bb7fae0d7b4b20bacbdf6c6 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 8 Mar 2026 21:59:17 +0600 Subject: [PATCH 34/48] lds core --- bin/tool-runner | 149 ++++++++++++++++++++++++++++++++++++++++++++++++ lds | 8 ++- 2 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 bin/tool-runner diff --git a/bin/tool-runner b/bin/tool-runner new file mode 100644 index 0000000..9c3e83b --- /dev/null +++ b/bin/tool-runner @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +set -euo pipefail + +DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)" +SERVER_TOOLS_CONTAINER="SERVER_TOOLS" + +has_bin() { type -P -- "${1:?}" >/dev/null 2>&1; } +bin_path() { type -P -- "${1:?}"; } + +die() { + printf 'Error: %s\n' "$*" >&2 + exit 1 +} + +load_env_file() { + local env_file="$DIR/.env" + [[ -f "$env_file" ]] || return 0 + + local line key value + while IFS= read -r line || [[ -n "$line" ]]; do + line="${line#"${line%%[![:space:]]*}"}" + [[ -z "$line" || "${line:0:1}" == "#" ]] && continue + [[ "$line" == export\ * ]] && line="${line#export }" + + [[ "$line" == *=* ]] || continue + key="${line%%=*}" + value="${line#*=}" + + key="${key%"${key##*[![:space:]]}"}" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + + if [[ "$value" =~ ^\".*\"$ ]]; then + value="${value:1:${#value}-2}" + elif [[ "$value" =~ ^\'.*\'$ ]]; then + value="${value:1:${#value}-2}" + fi + + case "$key" in + WORKING_DIR|WORKDIR|WORKDIR_WIN) + export "$key=$value" + ;; + esac + done < "$env_file" +} + +server_tools_running() { + has_bin docker || return 1 + [[ "$("$(bin_path docker)" inspect -f '{{.State.Running}}' "$SERVER_TOOLS_CONTAINER" 2>/dev/null || true)" == "true" ]] +} + +server_tools_has() { + local cmd="${1:-}" + [[ -n "$cmd" ]] || return 1 + server_tools_running || return 1 + "$(bin_path docker)" exec "$SERVER_TOOLS_CONTAINER" sh -lc 'command -v -- "$1" >/dev/null 2>&1' sh "$cmd" +} + +server_tools_image() { + "$(bin_path docker)" inspect -f '{{.Config.Image}}' "$SERVER_TOOLS_CONTAINER" 2>/dev/null +} + +server_tools_env_value() { + local key="${1:?}" + "$(bin_path docker)" inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$SERVER_TOOLS_CONTAINER" 2>/dev/null \ + | awk -F= -v k="$key" '$1==k { sub(/^[^=]*=/, ""); print; exit }' +} + +resolve_workspace() { + local workdir git_root + + workdir="${WORKDIR_WIN:-${WORKING_DIR:-${WORKDIR:-$PWD}}}" + [[ -n "$workdir" ]] || workdir="$DIR" + + if [[ -n "${MSYSTEM:-}${CYGWIN:-}" ]]; then + if [[ "$workdir" == *"/Program Files/Git/workspace"* ]] || [[ "$workdir" == "/workspace"* ]]; then + if has_bin git; then + git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)" + [[ -n "$git_root" ]] && workdir="$git_root" + fi + fi + + if has_bin cygpath; then + cygpath -w "$workdir" + return 0 + fi + fi + + printf '%s\n' "$workdir" +} + +main() { + load_env_file + + local cmd="${1:-}" + shift || true + + [[ -n "$cmd" ]] || die "missing command" + has_bin docker || die "docker is required on host" + server_tools_running || die "$SERVER_TOOLS_CONTAINER is not running" + server_tools_has "$cmd" || die "command '$cmd' not found in $SERVER_TOOLS_CONTAINER" + + local image workspace tz username home_dir + image="$(server_tools_image)" || die "unable to inspect $SERVER_TOOLS_CONTAINER image" + [[ -n "$image" ]] || die "unable to resolve $SERVER_TOOLS_CONTAINER image" + + workspace="$(resolve_workspace)" + [[ -n "$workspace" ]] || die "unable to resolve workspace" + + tz="$(server_tools_env_value TZ || true)" + username="$(server_tools_env_value USERNAME || true)" + home_dir="/home/root" + + local -a flags envs + flags=(--rm) + envs=( + -e HOME="$home_dir" + -e USER=root + -e LOGNAME=root + -e TERM="${TERM:-xterm-256color}" + ) + + [[ -n "$tz" ]] && envs+=(-e TZ="$tz") + [[ -n "$username" ]] && envs+=(-e USERNAME="$username") + [[ -n "${COLORTERM:-}" ]] && envs+=(-e COLORTERM="$COLORTERM") + [[ -n "${NO_COLOR:-}" ]] && envs+=(-e NO_COLOR="$NO_COLOR") + [[ -n "${CLICOLOR_FORCE:-}" ]] && envs+=(-e CLICOLOR_FORCE="$CLICOLOR_FORCE") + [[ -n "${FORCE_COLOR:-}" ]] && envs+=(-e FORCE_COLOR="$FORCE_COLOR") + + [[ -t 0 ]] && flags+=(-i) + [[ -t 1 ]] && flags+=(-t) + + if [[ -n "${MSYSTEM:-}${CYGWIN:-}" ]]; then + export MSYS_NO_PATHCONV=1 + export MSYS2_ARG_CONV_EXCL='*' + fi + + flags+=( + --entrypoint "$cmd" + --network "container:$SERVER_TOOLS_CONTAINER" + --volumes-from "$SERVER_TOOLS_CONTAINER" + -v "$workspace:/workspace" + -w /workspace + ) + + exec "$(bin_path docker)" run "${flags[@]}" "${envs[@]}" "$image" "$@" +} + +main "$@" diff --git a/lds b/lds index 022b836..3979736 100755 --- a/lds +++ b/lds @@ -4595,7 +4595,13 @@ main() { redis | redis-cli) exec "$DIR/bin/redis-cli" "$@" ;; es | elastic | elasticsearch) exec "$DIR/bin/es" "$@" ;; mongo | mongodb | mongosh | mongoimport | mongoexport) exec "$DIR/bin/mongo" "$@" ;; - *) "cmd_$cmd" "$@" ;; + *) + if declare -F "cmd_$cmd" >/dev/null 2>&1; then + "cmd_$cmd" "$@" + else + exec "$DIR/bin/tool-runner" "$cmd" "$@" + fi + ;; esac } From 1485b95fc3b23e1087065ac4bfdf879bc721a537 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sun, 8 Mar 2026 23:09:58 +0600 Subject: [PATCH 35/48] lds core --- bin/tool-runner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/tool-runner b/bin/tool-runner index 9c3e83b..fe32d71 100644 --- a/bin/tool-runner +++ b/bin/tool-runner @@ -13,7 +13,7 @@ die() { } load_env_file() { - local env_file="$DIR/.env" + local env_file="$DIR/docker/.env" [[ -f "$env_file" ]] || return 0 local line key value From b98c39645faba92c3ad8d5a6868214296fa4a099 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Tue, 10 Mar 2026 15:25:36 +0600 Subject: [PATCH 36/48] lds split --- lds | 947 ++---------------------------------------------------------- 1 file changed, 26 insertions(+), 921 deletions(-) diff --git a/lds b/lds index 3979736..8a98de6 100755 --- a/lds +++ b/lds @@ -1520,939 +1520,45 @@ cmd_ps() { fi } -# Internal: stats table (service optional) -_status_show_stats() { - local svc="${1:-}" - # docker stats does not reliably support --filter across versions; - # pass container names derived from compose itself. - local -a names=() - if [[ -n "$svc" ]]; then - local s - s="$(resolve_service "$svc" 2>/dev/null || true)" - [[ -n "$s" ]] || { - printf "(no matching containers)\n" - return 0 - } - mapfile -t names < <(docker_compose ps "$s" --format '{{.Name}}' 2>/dev/null | sed '/^[[:space:]]*$/d') - else - mapfile -t names < <(docker_compose ps --format '{{.Name}}' 2>/dev/null | sed '/^[[:space:]]*$/d') - fi - - if ((${#names[@]})); then - docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}" "${names[@]}" - else - printf "(no matching containers)\n" - fi -} - -# Internal: list container IDs for current compose project -_status_project_cids() { - docker_compose ps -q 2>/dev/null | sed '/^[[:space:]]*$/d' || true -} - -# Internal: list container names (compose ps) optionally including stopped -_status_project_names() { - docker_compose ps -a --format '{{.Name}}' 2>/dev/null | sed '/^[[:space:]]*$/d' || true -} - -# 1) Problems: unhealthy/restarting/exited + restart count + last exit -_status_show_problems() { - local -a cids=() - mapfile -t cids < <(docker_compose ps -q 2>/dev/null | sed '/^[[:space:]]*$/d') - ((${#cids[@]} == 0)) && { - printf "(none)\n" - return 0 - } - - # Collect rows as TAB-separated fields - # name | state | health | restart | exit | finishedAt - local raw - raw="$( - docker inspect -f '{{.Name}} {{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{end}} {{.RestartCount}} {{.State.ExitCode}} {{.State.FinishedAt}}' \ - "${cids[@]}" 2>/dev/null | sed 's|^/||' - )" - - local -a rows=() - local line name state health restart exitcode finishedAt - - # Only include actual problems: - # - not running - # - OR health == unhealthy - # Note: containers with no healthcheck (health="") are NOT problems. - while IFS=$'\t' read -r name state health restart exitcode finishedAt; do - [[ -n "$name" ]] || continue - if [[ "$state" != "running" || "$health" == "unhealthy" ]]; then - rows+=("$name"$'\t'"$state"$'\t'"${health:-'-'}"$'\t'"${restart:-0}"$'\t'"${exitcode:-0}"$'\t'"${finishedAt:-'-'}") - fi - done <<<"$raw" - - if ((${#rows[@]} == 0)); then - printf "(none)\n" - return 0 - fi - - # Pretty table - printf " %-18s %-10s %-10s %-7s %-18s\n" "NAME" "STATE" "HEALTH" "RESTART" "LAST_EXIT" - - for line in "${rows[@]}"; do - IFS=$'\t' read -r name state health restart exitcode finishedAt <<<"$line" - # LAST_EXIT: show exit code; add finishedAt for non-running states (helps debugging) - local last="code=${exitcode}" - [[ "$state" != "running" && "$finishedAt" != "-" ]] && last+=" @${finishedAt}" - printf " %-18s %-10s %-10s %-7s %-18s\n" \ - "$name" "$state" "${health:-'-'}" "${restart:-0}" "$last" - done -} - -# 2) Top consumers: top-5 by MEM and CPU (from docker stats) -_status_show_top_consumers() { - local -a names=() - mapfile -t names < <(_status_project_names) - if ((${#names[@]} == 0)); then - printf "(none)\n" - return 0 - fi - - local stats - stats="$(docker stats --no-stream --format '{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}' "${names[@]}" 2>/dev/null || true)" - [[ -n "$stats" ]] || { - printf "(no stats)\n" - return 0 - } - - _to_bytes() { - local v="$1" - v="${v//,/}" - local num unit - num="$(printf '%s' "$v" | awk '{gsub(/[^0-9.]/,""); print}')" - unit="$(printf '%s' "$v" | awk '{gsub(/[0-9.]/,""); print}')" - awk -v n="$num" -v u="$unit" 'BEGIN{ - mul=1; - if(u=="B"||u=="") mul=1; - else if(u=="kB"||u=="KB") mul=1000; - else if(u=="MB") mul=1000^2; - else if(u=="GB") mul=1000^3; - else if(u=="TB") mul=1000^4; - else if(u=="KiB") mul=1024; - else if(u=="MiB") mul=1024^2; - else if(u=="GiB") mul=1024^3; - else if(u=="TiB") mul=1024^4; - printf "%.0f", (n+0)*mul; - }' - } - - printf " %bTop by MEM:%b\n" "$BOLD" "$NC" - printf "%s\n" "$stats" | awk -F'\t' ' - {name=$1; cpu=$2; mem=$3; split(mem,a,"/"); gsub(/^[[:space:]]+|[[:space:]]+$/,"",a[1]); print name "\t" cpu "\t" a[1]} - ' | while IFS=$'\t' read -r n cpu mem; do - printf "%s\t%s\t%s\t%s\n" "$n" "$cpu" "$mem" "$(_to_bytes "$mem")" - done | sort -t$'\t' -k4,4nr | head -n 5 | awk -F'\t' '{printf " %-16s CPU=%-7s MEM=%s\n",$1,$2,$3}' - - printf " %bTop by CPU:%b\n" "$BOLD" "$NC" - printf "%s\n" "$stats" | awk -F'\t' ' - {name=$1; cpu=$2; gsub(/%/,"",cpu); print name "\t" cpu} - ' | sort -t$'\t' -k2,2nr | head -n 5 | awk -F'\t' '{printf " %-16s CPU=%s%%\n",$1,$2}' -} - -# 3) Volumes: mounted volumes + size + links + mounted-by -_status_show_volumes() { - local -a cids=() - mapfile -t cids < <(_status_project_cids) - if ((${#cids[@]} == 0)); then - printf "(none)\n" - return 0 - fi - - declare -A v2c=() - local cid cname v - for cid in "${cids[@]}"; do - cname="$(docker inspect -f '{{.Name}}' "$cid" 2>/dev/null | sed 's#^/##')" - while IFS= read -r v; do - [[ -n "$v" ]] || continue - if [[ -n "${v2c[$v]+x}" ]]; then - v2c["$v"]+=",${cname}" - else - v2c["$v"]="${cname}" - fi - done < <(docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\\n"}}{{end}}{{end}}' "$cid" 2>/dev/null) - done - - local -a vols=() - mapfile -t vols < <(printf "%s\n" "${!v2c[@]}" | sort) - - if ((${#vols[@]} == 0)); then - printf "(none)\n" - return 0 - fi - - declare -A vsize=() vlinks=() - local df - df="$(docker system df -v 2>/dev/null || true)" - if [[ -n "$df" ]]; then - while read -r name links size _; do - [[ -n "$name" && "$name" != "VOLUME" && "$name" != "VOLUME_NAME" ]] || continue - [[ -n "$size" && "$size" != "SIZE" ]] || continue - vlinks["$name"]="$links" - vsize["$name"]="$size" - done < <( - printf "%s\n" "$df" | - awk ' - /^Local Volumes space usage:/ {inside=1; next} - inside && /^[A-Za-z].*usage:$/ {exit} - inside && /^[[:space:]]*$/ {next} - inside {print} - ' | awk 'NR==1{next} {print $1, $2, $3, $4}' - ) - fi - - local -a rows=() - mapfile -t rows < <(docker volume inspect -f '{{.Name}}\t{{.Driver}}' "${vols[@]}" 2>/dev/null) - - local w_name=6 w_drv=6 - local line name drv - for line in "${rows[@]}"; do - IFS=$'\t' read -r name drv <<<"$line" - ((${#name} > w_name)) && w_name=${#name} - ((${#drv} > w_drv)) && w_drv=${#drv} - done - ((w_name > 36)) && w_name=36 - ((w_drv > 12)) && w_drv=12 - - printf " %b%-*s%b %b%-*s%b %b%-8s%b %b%-6s%b %b%s%b\n" \ - "$BOLD" "$w_name" "VOLUME" "$NC" \ - "$BOLD" "$w_drv" "DRIVER" "$NC" \ - "$BOLD" "SIZE" "$NC" \ - "$BOLD" "LINKS" "$NC" \ - "$BOLD" "MOUNTED_BY" "$NC" - - local any_size=0 - for line in "${rows[@]}"; do - IFS=$'\t' read -r name drv <<<"$line" - - local sz="${vsize[$name]:--}" - local lk="${vlinks[$name]:--}" - [[ "$sz" != "-" ]] && any_size=1 - - local mb="${v2c[$name]:-}" - local shown="$mb" extra="" - local cnt - cnt="$(printf '%s' "$mb" | awk -F',' '{print NF}')" - if ((cnt > 3)); then - shown="$(printf '%s' "$mb" | awk -F',' '{print $1","$2","$3}')" - extra=",…(+${cnt}-3)" - extra=",…(+$(($cnt - 3)))" - fi - - local n="$name" - ((${#n} > w_name)) && n="${n:0:w_name-1}…" - local d="$drv" - ((${#d} > w_drv)) && d="${d:0:w_drv-1}…" - - printf " %-*s %-*s %-8s %-6s %s%s\n" \ - "$w_name" "$n" "$w_drv" "${d:-'-'}" "$sz" "$lk" "$shown" "$extra" - done - - ((any_size)) || printf "\n %bNote:%b volume sizes unavailable (docker system df -v did not provide volume table)\n" "$YELLOW" "$NC" -} - -# 4) Networks + IPs -_status_show_networks() { - local project +# Resolve the tools container for the active compose project. +_project_tools_container() { + local project ctr project="$(lds_project)" - # Project containers (fast, single compose call) - local -a cids=() - mapfile -t cids < <(_status_project_cids) - ((${#cids[@]})) || { - printf " (none)\n" - return 0 - } - - printf " %bNetworks:%b\n" "$BOLD" "$NC" - - # 1) Prefer label-based listing (now that you add labels in compose) - # 2) Fallback: derive from containers (works for external/unlabeled networks) - local nets="" - nets="$(docker network ls --filter "label=com.docker.compose.project=$project" --format '{{.Name}}' 2>/dev/null | sed '/^[[:space:]]*$/d' || true)" - if [[ -z "$nets" ]]; then - nets="$( - docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' \ - "${cids[@]}" 2>/dev/null | - sed '/^[[:space:]]*$/d' | - sort -u - )" - fi - - if [[ -z "$nets" ]]; then - printf " (none)\n" - else - printf " %-22s %-8s %-18s %-18s %-6s\n" "NAME" "DRIVER" "SUBNET" "GATEWAY" "CNTS" - - # Count attached containers per network via inspect (cheap for a few nets) - while IFS= read -r n; do - [[ -n "$n" ]] || continue - - local driver subnet gateway cnt - driver="$(docker network inspect -f '{{.Driver}}' "$n" 2>/dev/null || true)" - subnet="$(docker network inspect -f '{{with index .IPAM.Config 0}}{{.Subnet}}{{end}}' "$n" 2>/dev/null || true)" - gateway="$(docker network inspect -f '{{with index .IPAM.Config 0}}{{.Gateway}}{{end}}' "$n" 2>/dev/null || true)" - cnt="$(docker network inspect -f '{{len .Containers}}' "$n" 2>/dev/null || true)" - - printf " %-22s %-8s %-18s %-18s %-6s\n" \ - "$n" "${driver:-'-'}" "${subnet:-'-'}" "${gateway:-'-'}" "${cnt:-0}" - done <<<"$nets" - fi - - printf " %bContainer IPs:%b\n" "$BOLD" "$NC" - - # Format: NAMENETWORKIP - local out - out="$( - docker inspect -f '{{.Name}}{{"\t"}}{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{"\t"}}{{$v.IPAddress}}{{"\n"}}{{end}}' \ - "${cids[@]}" 2>/dev/null | sed 's#^/##' || true + # Prefer compose labels so we don't hit another project's tools container. + ctr="$( + docker ps \ + --filter "label=com.docker.compose.project=$project" \ + --filter 'label=com.docker.compose.service=server-tools' \ + --format '{{.Names}}' 2>/dev/null | sed -n '1p' )" - - if [[ -z "${out//$'\n'/}" ]]; then - printf " (none)\n" - return 0 - fi - - printf " %-18s %-22s %s\n" "NAME" "NETWORK" "IP" - printf "%s\n" "$out" | awk -F'\t' 'NF>=3 && $2!="" {printf " %-18s %-22s %s\n",$1,$2,$3}' -} - -# 5) Optional endpoint probes (gated) -_status_show_probes() { - ((${LDS_STATUS_PROBE:-1})) || { - printf "(disabled; set LDS_STATUS_PROBE=1)\n" - return 0 - } - if ! has_tool curl; then - printf "(curl missing)\n" - return 0 - fi - - local -a urls=() - mapfile -t urls < <(_status_urls 2>/dev/null || true) - - if ((${#urls[@]} == 0)); then - printf "(no urls)\n" - return 0 - fi - - local u host code t - for u in "${urls[@]}"; do - host="${u#https://}" - code="$(curl -ksS --max-time 2 --resolve "${host}:443:127.0.0.1" -o /dev/null -w '%{http_code}' "$u" 2>/dev/null || echo 000)" - t="$(curl -ksS --max-time 2 --resolve "${host}:443:127.0.0.1" -o /dev/null -w '%{time_total}' "$u" 2>/dev/null || echo 0)" - printf " %-32s %s %ss\n" "$host" "$code" "$t" - done -} - -# 6) Recent errors for problematic containers (tail logs) -_status_show_recent_errors() { - local -a cids=() - mapfile -t cids < <(_status_project_cids) - ((${#cids[@]})) || { - printf "(none)\n" - return 0 - } - - local -a bad=() - local cid st health - for cid in "${cids[@]}"; do - st="$(docker inspect -f '{{.State.Status}}' "$cid" 2>/dev/null || true)" - health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$cid" 2>/dev/null || true)" - if [[ "$st" != "running" || "$health" == "unhealthy" || "$st" == "restarting" ]]; then - bad+=("$cid") - fi - done - - if ((${#bad[@]} == 0)); then - printf "(none)\n" - return 0 - fi - - local cid name - for cid in "${bad[@]}"; do - name="$(docker inspect -f '{{.Name}}' "$cid" 2>/dev/null | sed 's#^/##')" - printf " %b%s%b\n" "$BOLD" "$name" "$NC" - docker logs --tail 6 "$cid" 2>&1 | sed 's/^/ /' || true - done -} - -# 7) Drift / hygiene checks -_status_show_drift() { - local project - project="$(lds_project)" - - local -a compose_names=() - mapfile -t compose_names < <(docker_compose ps -a --format '{{.Name}}' 2>/dev/null | sed '/^[[:space:]]*$/d' || true) - - declare -A have=() - local n - for n in "${compose_names[@]}"; do have["$n"]=1; done - - local -a labeled=() - mapfile -t labeled < <(docker ps -a --filter "label=com.docker.compose.project=$project" --format '{{.Names}}' 2>/dev/null | sed '/^[[:space:]]*$/d' || true) - - local -a orphans=() - for n in "${labeled[@]}"; do - [[ -n "${have[$n]+x}" ]] || orphans+=("$n") - done - - if ((${#orphans[@]})); then - printf " %bOrphan containers:%b %s\n" "$YELLOW" "$NC" "${orphans[*]}" - else - printf " %bOrphan containers:%b (none)\n" "$GREEN" "$NC" - fi - - local -a v_lab=() v_mnt=() - mapfile -t v_lab < <(docker volume ls -q --filter "label=com.docker.compose.project=$project" 2>/dev/null | sed '/^[[:space:]]*$/d' || true) - - local -a cids=() - mapfile -t cids < <(_status_project_cids) - if ((${#cids[@]})); then - mapfile -t v_mnt < <( - docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\\n"}}{{end}}{{end}}' "${cids[@]}" 2>/dev/null | - sed '/^[[:space:]]*$/d' | awk '!seen[$0]++' - ) - fi - declare -A m=() - for n in "${v_mnt[@]}"; do m["$n"]=1; done - - local -a unused=() - for n in "${v_lab[@]}"; do - [[ -n "${m[$n]+x}" ]] || unused+=("$n") - done - - if ((${#v_lab[@]} == 0)); then - printf " %bLabeled volumes:%b (none)\n" "$DIM" "$NC" - elif ((${#unused[@]})); then - printf " %bUnused labeled volumes:%b %s\n" "$YELLOW" "$NC" "${unused[*]}" - else - printf " %bUnused labeled volumes:%b (none)\n" "$GREEN" "$NC" - fi - - local reclaim - reclaim="$(docker system df 2>/dev/null | awk '/Build Cache/ {print $NF}' || true)" - if [[ -n "$reclaim" && "$reclaim" != "0B" ]]; then - printf " %bHint:%b docker builder prune can reclaim build cache\n" "$DIM" "$NC" - fi -} - -# Internal: compact checks for `status` (2 groups: System + Project) -_status_checks() { - _has() { has_cmd "$1"; } - - local -a sys_ok=() sys_bad=() proj_ok=() proj_bad=() - - # System group - [[ -t 0 ]] && sys_ok+=(stdin) || sys_bad+=(stdin) - [[ -t 1 ]] && sys_ok+=(stdout) || sys_bad+=(stdout) - [[ -r /dev/tty ]] && sys_ok+=(tty) || sys_bad+=(tty) - - local c - for c in awk sed grep find sort; do - _has "$c" && sys_ok+=("$c") || sys_bad+=("$c") - done - - if _has docker || _has docker.exe; then - sys_ok+=(docker) - else - sys_bad+=(docker) - fi - - if docker_compose version >/dev/null 2>&1; then sys_ok+=(compose); else sys_bad+=(compose); fi - _has openssl && sys_ok+=(openssl) || sys_bad+=(openssl) - (_has curl || _has wget) && sys_ok+=(http) || sys_bad+=(http) - _has jq && sys_ok+=(jq) || sys_bad+=(jq) - - # Project group - [[ -f "$ENV_DOCKER" ]] && proj_ok+=(env) || proj_bad+=(env) - [[ -d "$DIR/docker" ]] && proj_ok+=(docker_dir) || proj_bad+=(docker_dir) - [[ -d "$DIR/configuration" ]] && proj_ok+=(config_dir) || proj_bad+=(config_dir) - [[ -d "$DIR/logs" ]] && proj_ok+=(logs_dir) || proj_bad+=(logs_dir) - [[ -f "$DIR/configuration/ssl/rootCA.pem" ]] && proj_ok+=(rootCA) || proj_bad+=(rootCA) - [[ -f "$DIR/configuration/ssl/mTLS-user.p12" ]] && proj_ok+=(mTLS) || proj_bad+=(mTLS) - - # Print exactly two lines (tab separated) - printf "%bSystem%b\t" "$CYAN" "$NC" - if ((${#sys_bad[@]})); then - printf "%b!%b %s" "$YELLOW" "$NC" "${sys_bad[*]}" - if ((${#sys_ok[@]})); then printf "%b | %b" "$DIM" "$NC"; fi - fi - ((${#sys_ok[@]})) && printf "%b✓%b %s" "$GREEN" "$NC" "${sys_ok[*]}" - printf "\n" - - printf "%bProject%b\t" "$CYAN" "$NC" - if ((${#proj_bad[@]})); then - printf "%b!%b %s" "$YELLOW" "$NC" "${proj_bad[*]}" - if ((${#proj_ok[@]})); then printf "%b | %b" "$DIM" "$NC"; fi - fi - ((${#proj_ok[@]})) && printf "%b✓%b %s" "$GREEN" "$NC" "${proj_ok[*]}" - printf "\n" -} -_status_show_disk() { - docker system df -} - -# Internal: human readable bytes (B/K/M/G/T). Accepts integer bytes. -_human_bytes() { - local b="${1:-0}" - awk -v b="$b" 'BEGIN{ - split("B KB MB GB TB PB EB",u," "); - i=1; - while (b>=1024 && i/dev/null | awk '{print $1}' 2>/dev/null)"; then - [[ -n "$out" ]] && { - printf '%s' "$out" - return 0 - } - fi - - # Fallback: du -sk * 1024 - out="$(du -sk "$p" 2>/dev/null | awk '{print $1}' 2>/dev/null || true)" - [[ -n "$out" ]] || out="0" - awk -v k="$out" 'BEGIN{ printf "%.0f", k*1024 }' -} - -# Internal: volumes list + sizes for this compose project -_status_show_volumes() { - local project - project="$(lds_project)" - - # 1) volumes by compose label (if any) - local -a vols=() - mapfile -t vols < <( - docker volume ls -q --filter "label=com.docker.compose.project=$project" 2>/dev/null | sed '/^[[:space:]]*$/d' - ) - - # 2) volumes mounted by this project's containers (covers external/unlabeled) - local -a cids=() - mapfile -t cids < <(docker_compose ps -q 2>/dev/null | sed '/^[[:space:]]*$/d') - - if ((${#cids[@]} > 0)); then - local v - while IFS= read -r v; do - [[ -n "$v" ]] && vols+=("$v") - done < <( - docker inspect -f '{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{"\n"}}{{end}}{{end}}' "${cids[@]}" 2>/dev/null | - sed '/^[[:space:]]*$/d' - ) - fi - - # uniq - if ((${#vols[@]} > 0)); then - mapfile -t vols < <(printf "%s\n" "${vols[@]}" | awk '!seen[$0]++') - fi - - if ((${#vols[@]} == 0)); then - printf "(none)\n" - return 0 - fi - - # size map from `docker system df -v` - declare -A __VOL_SZ=() - local df - df="$(docker system df -v 2>/dev/null || true)" - if [[ -n "$df" ]]; then - # Extract the "Local Volumes space usage:" table: VOLUME NAME | LINKS | SIZE - while read -r name links size rest; do - [[ -n "$name" && "$name" != "VOLUME" ]] || continue - [[ -n "$size" ]] || continue - __VOL_SZ["$name"]="$size" - done < <( - printf "%s\n" "$df" | - awk ' - /^Local Volumes space usage:/ {inside=1; next} - inside && /^Build cache usage:/ {exit} - inside && /^[[:space:]]*$/ {next} - inside {print} - ' | awk 'NR==1{next} {print $1, $2, $3}' - ) - fi - - # inspect metadata - local -a rows=() - mapfile -t rows < <( - docker volume inspect -f '{{.Name}} {{.Driver}}' "${vols[@]}" 2>/dev/null | sed '/^[[:space:]]*$/d' - ) - - if ((${#rows[@]} == 0)); then - printf "(none)\n" + # Fallback for setups where labels/ps differ. + ctr="$(docker_compose ps server-tools --format '{{.Name}}' 2>/dev/null | sed -n '1p' || true)" + [[ -n "$ctr" ]] && { + printf '%s' "$ctr" return 0 - fi - - local w_name=6 w_drv=6 - local line name drv - for line in "${rows[@]}"; do - IFS=$'\t' read -r name drv <<<"$line" - ((${#name} > w_name)) && w_name=${#name} - ((${#drv} > w_drv)) && w_drv=${#drv} - done - ((w_name > 40)) && w_name=40 - ((w_drv > 12)) && w_drv=12 - - printf " %b%-*s%b %b%-*s%b %b%s%b\n" \ - "$BOLD" "$w_name" "NAME" "$NC" \ - "$BOLD" "$w_drv" "DRIVER" "$NC" \ - "$BOLD" "SIZE" "$NC" - - local total_note=0 - for line in "${rows[@]}"; do - IFS=$'\t' read -r name drv <<<"$line" - - local n_disp="$name" d_disp="$drv" - if ((${#n_disp} > w_name)); then n_disp="${n_disp:0:w_name-1}…"; fi - if ((${#d_disp} > w_drv)); then d_disp="${d_disp:0:w_drv-1}…"; fi - - local sz="${__VOL_SZ[$name]:--}" - [[ "$sz" != "-" ]] && total_note=1 - - printf " %-*s %-*s %s\n" \ - "$w_name" "$n_disp" \ - "$w_drv" "${d_disp:-'-'}" \ - "$sz" - done - - if ((total_note == 0)); then - printf "\n %bNote:%b volume sizes unavailable (docker system df -v did not provide volume table)\n" \ - "$YELLOW" "$NC" - fi -} - -_status_urls() { - local f d - shopt -s nullglob - for f in "$DIR/configuration/nginx/"*.conf; do - d="$(basename -- "$f" .conf)" - [[ -n "$d" ]] && printf 'https://%s\n' "$d" - done - shopt -u nullglob -} - -_status_health_line_plain() { - local c="$1" - local st health - st="$(docker inspect -f '{{.State.Status}}' "$c" 2>/dev/null || true)" - health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$c" 2>/dev/null || true)" - if [[ -n "$health" ]]; then - printf '%s:%s(%s)' "$c" "$st" "$health" - else - printf '%s:%s' "$c" "$st" - fi -} - -_status_health_line() { - # Colorized for human output (mkhost-style theme) - local c="$1" - local st health line - st="$(docker inspect -f '{{.State.Status}}' "$c" 2>/dev/null || true)" - health="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$c" 2>/dev/null || true)" - line="$(_status_health_line_plain "$c")" - - # Prefer health when present, otherwise State.Status - if [[ -n "$health" ]]; then - case "$health" in - healthy) printf '%b%s%b' "$GREEN" "$line" "$NC" ;; - starting) printf '%b%s%b' "$YELLOW" "$line" "$NC" ;; - unhealthy) printf '%b%s%b' "$RED" "$line" "$NC" ;; - *) printf '%b%s%b' "$MAGENTA" "$line" "$NC" ;; - esac - else - case "$st" in - running) printf '%b%s%b' "$GREEN" "$line" "$NC" ;; - exited | dead) printf '%b%s%b' "$RED" "$line" "$NC" ;; - created | paused | restarting) printf '%b%s%b' "$YELLOW" "$line" "$NC" ;; - *) printf '%b%s%b' "$MAGENTA" "$line" "$NC" ;; - esac - fi -} - -# Internal: main status output (containers/urls/json) -_status_core() { - local json="$1" quiet="$2" - - local project - project="$(lds_project)" - - local profiles="" - if [[ -r "$ENV_DOCKER" ]]; then - profiles="$(grep -E '^[[:space:]]*COMPOSE_PROFILES=' "$ENV_DOCKER" | tail -n1 | cut -d= -f2- | tr -d '\r' || true)" - fi - - # containers (Name \t Service \t State \t Health) - local -a ctrs=() - mapfile -t ctrs < <( - docker_compose ps -a --format '{{.Name}}\t{{.Service}}\t{{.State}}\t{{.Health}}' 2>/dev/null | - sed '/^[[:space:]]*$/d' - ) - - # urls - local -a urls=() - mapfile -t urls < <(_status_urls 2>/dev/null || true) - - # ---- helpers ---- - _json_escape() { - local s="$1" - s="${s//\\/\\\\}" - s="${s//\"/\\\"}" - s="${s//$'\n'/\\n}" - s="${s//$'\r'/\\r}" - s="${s//$'\t'/\\t}" - printf '%s' "$s" - } - - _health_color() { - local h="$1" - # avoid ${var,,} to stay compatible - if [[ -z "$h" || "$h" == "null" ]]; then - printf '%s' "$DIM" - elif [[ "$h" == *"healthy"* ]]; then - printf '%s' "$GREEN" - elif [[ "$h" == *"unhealthy"* ]]; then - printf '%s' "$RED" - else - printf '%s' "$YELLOW" - fi - } - - _state_color() { - local s="$1" - case "$s" in - running) printf '%s' "$GREEN" ;; - exited | dead) printf '%s' "$RED" ;; - restarting) printf '%s' "$YELLOW" ;; - *) printf '%s' "$DIM" ;; - esac } - # health icon (✓, !, ×) - _health_icon() { - local h="$1" - if [[ -z "$h" || "$h" == "null" || "$h" == "-" ]]; then - printf '!' - elif [[ "$h" == *"unhealthy"* ]]; then - printf '×' - elif [[ "$h" == *"healthy"* ]]; then - printf '✓' - else - printf '!' - fi - } - - # running/total summary - local total running - total=${#ctrs[@]} - running=0 - if ((total)); then - local line _n _svc st _h - for line in "${ctrs[@]}"; do - IFS=$'\t' read -r _n _svc st _h <<<"$line" - [[ "$st" == "running" ]] && ((++running)) - done - fi - - # ---- JSON mode ---- - if ((json)); then - printf '{' - printf '"project":"%s",' "$(_json_escape "$project")" - printf '"profiles":"%s",' "$(_json_escape "$profiles")" - printf '"summary":{"running":%s,"total":%s},' "$running" "$total" - - printf '"containers":[' - local first=1 line name svc state health - for line in "${ctrs[@]}"; do - IFS=$'\t' read -r name svc state health <<<"$line" - ((first)) || printf ',' - first=0 - - local health_disp="${health:-}" - [[ -z "$health_disp" || "$health_disp" == "null" ]] && health_disp='-' - - printf '{' - printf '"name":"%s",' "$(_json_escape "$name")" - printf '"service":"%s",' "$(_json_escape "$svc")" - printf '"state":"%s",' "$(_json_escape "$state")" - printf '"health":"%s",' "$(_json_escape "$health_disp")" - printf '"health_icon":"%s"' "$(_json_escape "$(_health_icon "$health_disp")")" - printf '}' - done - printf '],' - - printf '"ports":[80,443],' - - printf '"urls":[' - first=1 - local u - for u in "${urls[@]}"; do - [[ -n "$u" ]] || continue - ((first)) || printf ',' - first=0 - printf '"%s"' "$(_json_escape "$u")" - done - printf ']' - - printf '}\n' - return 0 - fi - - # ---- Quiet mode ---- - if ((quiet)); then - return 0 - fi - - # ---- Pretty output ---- - printf "%bProject:%b %s\n" "$CYAN" "$NC" "$project" - [[ -n "$profiles" ]] && printf "%bProfiles:%b %s\n" "$CYAN" "$NC" "$profiles" - - printf "%bContainers:%b %b(%s/%s running)%b\n" "$CYAN" "$NC" "$DIM" "$running" "$total" "$NC" - - if ((${#ctrs[@]} == 0)); then - printf " (none)\n" - else - local w_name=4 w_svc=7 - local line name svc state health - for line in "${ctrs[@]}"; do - IFS=$'\t' read -r name svc state health <<<"$line" - ((${#name} > w_name)) && w_name=${#name} - ((${#svc} > w_svc)) && w_svc=${#svc} - done - ((w_name > 34)) && w_name=34 - ((w_svc > 18)) && w_svc=18 - - printf " %b%-*s%b %b%-*s%b %b%-10s%b %b%s%b\n" \ - "$BOLD" "$w_name" "NAME" "$NC" \ - "$BOLD" "$w_svc" "SERVICE" "$NC" \ - "$BOLD" "STATE" "$NC" \ - "$BOLD" "HEALTH" "$NC" - - for line in "${ctrs[@]}"; do - IFS=$'\t' read -r name svc state health <<<"$line" - - local name_disp="$name" svc_disp="$svc" - if ((${#name_disp} > w_name)); then name_disp="${name_disp:0:w_name-1}…"; fi - if ((${#svc_disp} > w_svc)); then svc_disp="${svc_disp:0:w_svc-1}…"; fi - - local stc hc - stc="$(_state_color "$state")" - hc="$(_health_color "${health:-}")" - - local health_disp="${health:-}" - [[ -z "$health_disp" || "$health_disp" == "null" ]] && health_disp='-' - - local hi - hi="$(_health_icon "$health_disp")" - - printf " %-*s %-*s %b%-10s%b %b%s %s%b\n" \ - "$w_name" "$name_disp" \ - "$w_svc" "$svc_disp" \ - "$stc" "${state:-'-'}" "$NC" \ - "$hc" "$hi" "$health_disp" "$NC" - done - fi - - printf "%bPorts:%b 80, 443\n" "$CYAN" "$NC" - - printf "%bURLs:%b\n" "$CYAN" "$NC" - if ((${#urls[@]})); then - local u - for u in "${urls[@]}"; do - [[ -n "$u" ]] || continue - printf " - %b%s%b\n" "$BLUE" "$u" "$NC" - done - else - printf " (none)\n" - fi + return 1 } cmd_status() { - local json=0 quiet=0 - while [[ "${1:-}" ]]; do - case "$1" in - --json) - json=1 - shift - ;; - --quiet | -q) - quiet=1 - shift - ;; - *) break ;; - esac - done - - # JSON output is intentionally scoped to core stack info only. - _status_core "$json" "$quiet" - if ((json)) || ((quiet)); then - return 0 - fi - - printf " -%bProblems:%b -" "$CYAN" "$NC" - _status_show_problems || true - - printf " -%bTop consumers:%b -" "$CYAN" "$NC" - _status_show_top_consumers || true - - printf " -%bStats:%b -" "$CYAN" "$NC" - _status_show_stats "${1:-}" || true - - printf " -%bDisk:%b -" "$CYAN" "$NC" - _status_show_disk || true - - printf " -%bVolumes:%b -" "$CYAN" "$NC" - _status_show_volumes || true - - printf " -%bNetworks:%b -" "$CYAN" "$NC" - _status_show_networks || true - - printf " -%bProbes:%b -" "$CYAN" "$NC" - _status_show_probes || true - - printf " -%bRecent errors:%b -" "$CYAN" "$NC" - _status_show_recent_errors || true + local ctr project + project="$(lds_project)" + ctr="$(_project_tools_container || true)" + [[ -n "$ctr" ]] || die "server-tools container not found for project: $project" - printf " -%bDrift:%b -" "$CYAN" "$NC" - _status_show_drift || true + docker inspect -f '{{.State.Running}}' "$ctr" 2>/dev/null | grep -qx true || \ + die "server-tools container is not running: $ctr" - printf " -%bChecks:%b -" "$CYAN" "$NC" - _status_checks || true + local -a flags=() + [[ -t 1 ]] && flags+=(-t) + docker exec "${flags[@]}" "$ctr" status "$@" } # ───────────────────────────────────────────────────────────────────────────── # 6b. LOGS / OPEN @@ -4378,11 +3484,10 @@ cmd_help() { - `lds stack start` *(aliases: `start`)* - `lds stack down [--volumes --yes]` *(aliases: `down`, `stop`)* - `lds stack restart [svc]` *(aliases: `restart`, `reboot`)* -- `lds stack status [--json] [--quiet]` *(alias: `status`)* +- `lds stack status [status-args…]` *(alias: `status`; forwards args to tools `status`)* - `lds stack ps` *(alias: `ps`)* - `lds stack logs [svc] [--follow] [--since ] [--grep ]` *(alias: `logs`)* - `lds stack exec [cmd…]` *(alias: `exec`)* -- `lds stack status` also includes live `docker stats`, disk usage, and doctor summary. - `lds stack events [--since ]` *(alias: `events`)* - `lds stack clean --yes [--volumes]` *(alias: `clean`)* - `lds stack verify` *(alias: `verify`)* From 3edeed801f085189c4450ad4d9575824726a37d7 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Tue, 10 Mar 2026 15:42:56 +0600 Subject: [PATCH 37/48] lds split --- lds | 220 +++++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 144 insertions(+), 76 deletions(-) diff --git a/lds b/lds index 8a98de6..3613fed 100755 --- a/lds +++ b/lds @@ -30,7 +30,7 @@ die() { ############################################################################### # Command presence + tool proxy rules # - Host-only commands MUST exist on host (e.g. docker) -# - Proxy-safe tools may run from SERVER_TOOLS when LDS_PROXY_TOOLS=1 and container is up +# - Proxy-safe tools may run from the project tools container when LDS_PROXY_TOOLS=1 and container is up # - Anything inside `docker exec ... sh -lc '...'` must continue to use `command -v` ############################################################################### @@ -59,7 +59,7 @@ need_bin() { die "Missing required command: $c${hint:+ ($hint)}" } -# Commands that must NEVER be proxied to SERVER_TOOLS (host control plane / privileged / filesystem) +# Commands that must NEVER be proxied to project tools container (host control plane / privileged / filesystem) _is_host_only_cmd() { case "${1:-}" in docker | sudo | su | systemctl | service | ip | iptables | nft | sysctl | mount | umount | modprobe | insmod | rmmod | rm | mv | cp | chmod | chown) @@ -69,23 +69,59 @@ _is_host_only_cmd() { return 1 } -_server_tools_running() { +# Resolve the project-specific tools container name. +_project_tools_container() { has_bin docker || return 1 - "$(bin_path docker)" inspect -f '{{.State.Running}}' SERVER_TOOLS 2>/dev/null | grep -qx true + + local project ctr + project="$(lds_project)" + + ctr="$( + docker ps -a \ + --filter "label=com.docker.compose.project=$project" \ + --filter 'label=com.docker.compose.service=server-tools' \ + --format '{{.Names}}' 2>/dev/null | sed -n '1p' + )" + [[ -n "$ctr" ]] && { + printf '%s' "$ctr" + return 0 + } + + ctr="$(docker_compose ps -a server-tools --format '{{.Name}}' 2>/dev/null | sed -n '1p' || true)" + [[ -n "$ctr" ]] && { + printf '%s' "$ctr" + return 0 + } + + return 1 +} + +_project_tools_container_running() { + local ctr + ctr="$(_project_tools_container || true)" + [[ -n "$ctr" ]] || return 1 + docker inspect -f '{{.State.Running}}' "$ctr" 2>/dev/null | grep -qx true || return 1 + printf '%s' "$ctr" +} + +_server_tools_running() { + [[ -n "$(_project_tools_container_running || true)" ]] } _server_tools_has() { local c="${1:-}" [[ -n "$c" ]] || return 1 - _server_tools_running || return 1 - "$(bin_path docker)" exec SERVER_TOOLS sh -lc "command -v \"$c\" >/dev/null 2>&1" >/dev/null 2>&1 + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || return 1 + "$(bin_path docker)" exec "$ctr" sh -lc "command -v \"$c\" >/dev/null 2>&1" >/dev/null 2>&1 } # Unified command runner for proxy-safe tools # Resolution order (your rule-set): # 1) if cmd exists on host -> run host binary # 2) if host-only and missing -> error -# 3) if proxy-safe and LDS_PROXY_TOOLS=1 and SERVER_TOOLS up and cmd exists there -> run via docker exec +# 3) if proxy-safe and LDS_PROXY_TOOLS=1 and project tools up and cmd exists there -> run via docker exec # 4) otherwise -> error lds_tools_cmd() { local cmd="${1:-}" @@ -109,30 +145,32 @@ lds_tools_cmd() { return 127 fi - # 3) proxy-safe tools from SERVER_TOOLS (opt-in) + # 3) proxy-safe tools from project tools container (opt-in) if ((${LDS_PROXY_TOOLS:-1})); then if ! has_bin docker; then echo "Error: '$cmd' not available on host; tool proxy requires host 'docker'" >&2 return 127 fi - if ! _server_tools_running; then - echo "Error: '$cmd' not available on host and SERVER_TOOLS is not running (set LDS_PROXY_TOOLS=1 and start the stack)" >&2 + local ctr + ctr="$(_project_tools_container_running || true)" + if [[ -z "$ctr" ]]; then + echo "Error: '$cmd' not available on host and project tools container is not running (set LDS_PROXY_TOOLS=1 and start the stack)" >&2 return 127 fi if ! _server_tools_has "$cmd"; then - echo "Error: '$cmd' not found on host nor inside SERVER_TOOLS" >&2 + echo "Error: '$cmd' not found on host nor inside project tools container" >&2 return 127 fi local -a flags=() [[ -t 0 ]] && flags+=(-i) [[ -t 1 ]] && flags+=(-t) - "$(bin_path docker)" exec "${flags[@]}" SERVER_TOOLS "$cmd" "$@" + "$(bin_path docker)" exec "${flags[@]}" "$ctr" "$cmd" "$@" return $? fi # 4) not found and proxy not enabled - echo "Error: '$cmd' not available on host (set LDS_PROXY_TOOLS=1 to proxy via SERVER_TOOLS)" >&2 + echo "Error: '$cmd' not available on host (set LDS_PROXY_TOOLS=1 to proxy via project tools container)" >&2 return 127 } @@ -600,8 +638,18 @@ fix_perms() { ############################################################################### # 3. DOMAIN / PROFILE INTEGRATION ############################################################################### -mkhost() { docker exec SERVER_TOOLS mkhost "$@"; } -rmhost() { docker exec SERVER_TOOLS rmhost "$@"; } +mkhost() { + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + docker exec "$ctr" mkhost "$@" +} +rmhost() { + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + docker exec "$ctr" rmhost "$@" +} php_reload_or_restart_project() { local project project="$(lds_project)" @@ -648,8 +696,12 @@ php_reload_or_restart_project() { } setup_domain() { + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + mkhost --RESET - docker exec -it SERVER_TOOLS mkhost + docker exec -it "$ctr" mkhost local php_prof svr_prof node_prof php_prof=$(mkhost --ACTIVE_PHP_PROFILE || true) svr_prof=$(mkhost --APACHE_ACTIVE || true) @@ -664,10 +716,14 @@ setup_domain() { } delete_domain() { + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + rmhost --RESET # interactive delete - docker exec -it SERVER_TOOLS rmhost "$@" + docker exec -it "$ctr" rmhost "$@" local php_cont apache_cont node_cont php_cont=$(rmhost --DELETE_PHP_PROFILE || true) @@ -1520,41 +1576,11 @@ cmd_ps() { fi } -# Resolve the tools container for the active compose project. -_project_tools_container() { - local project ctr - project="$(lds_project)" - - # Prefer compose labels so we don't hit another project's tools container. - ctr="$( - docker ps \ - --filter "label=com.docker.compose.project=$project" \ - --filter 'label=com.docker.compose.service=server-tools' \ - --format '{{.Names}}' 2>/dev/null | sed -n '1p' - )" - [[ -n "$ctr" ]] && { - printf '%s' "$ctr" - return 0 - } - - # Fallback for setups where labels/ps differ. - ctr="$(docker_compose ps server-tools --format '{{.Name}}' 2>/dev/null | sed -n '1p' || true)" - [[ -n "$ctr" ]] && { - printf '%s' "$ctr" - return 0 - } - - return 1 -} - cmd_status() { local ctr project project="$(lds_project)" - ctr="$(_project_tools_container || true)" - [[ -n "$ctr" ]] || die "server-tools container not found for project: $project" - - docker inspect -f '{{.State.Running}}' "$ctr" 2>/dev/null | grep -qx true || \ - die "server-tools container is not running: $ctr" + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || die "server-tools container not found or not running for project: $project" local -a flags=() [[ -t 1 ]] && flags+=(-t) @@ -1691,10 +1717,11 @@ cmd_profiles() { # 6d. DIAG / SNIFF # ───────────────────────────────────────────────────────────────────────────── _tools_exec() { - docker inspect SERVER_TOOLS >/dev/null 2>&1 || die "SERVER_TOOLS container not found" - docker inspect -f '{{.State.Running}}' SERVER_TOOLS 2>/dev/null | grep -qx true || die "SERVER_TOOLS is not running" + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" # NOTE: pass a SINGLE command string; do not pass arrays here. - docker exec -i SERVER_TOOLS sh -lc "$*" + docker exec -i "$ctr" sh -lc "$*" } _shq() { printf '%q' "$1"; } @@ -1755,9 +1782,19 @@ cmd_sniff() { # ───────────────────────────────────────────────────────────────────────────── # 6e. SECRETS / CERT / HOST / UI / RUNTIME # ───────────────────────────────────────────────────────────────────────────── -cmd_secrets() { docker exec -it SERVER_TOOLS senv "$@"; } +cmd_secrets() { + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + docker exec -it "$ctr" senv "$@" +} -cmd_cert() { docker exec -it SERVER_TOOLS certify "$@"; } +cmd_cert() { + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + docker exec -it "$ctr" certify "$@" +} cmd_host() { local sub="${1:-}" @@ -1783,7 +1820,12 @@ cmd_host() { esac } -cmd_ui() { docker exec -it SERVER_TOOLS lazydocker; } +cmd_ui() { + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + docker exec -it "$ctr" lazydocker +} cmd_runtime() { local which="${1:-}" @@ -1884,7 +1926,12 @@ cmd_clean() { " "$GREEN" "$NC" } -cmd_env() { docker exec -it SERVER_TOOLS senv env "$@"; } +cmd_env() { + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + docker exec -it "$ctr" senv env "$@" +} cmd_verify() { # smoke checks: compose config, LB up, curl each domain @@ -2112,18 +2159,21 @@ docker_shell() { cmd_tools() { local sub="${1:-sh}" shift || true + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" case "${sub,,}" in sh | shell | "") - docker_shell SERVER_TOOLS + docker_shell "$ctr" ;; exec) - [[ $# -gt 0 ]] || die "tools exec " "" - docker exec -it SERVER_TOOLS sh -lc "$*" + [[ $# -gt 0 ]] || die "tools exec " + docker exec -it "$ctr" sh -lc "$*" ;; file) local p="${1:-}" [[ -n "$p" ]] || die "tools file " - docker exec -it SERVER_TOOLS sh -lc "ls -la -- \"$p\" 2>/dev/null || true; echo; sed -n '1,200p' -- \"$p\" 2>/dev/null || true" + docker exec -it "$ctr" sh -lc "ls -la -- \"$p\" 2>/dev/null || true; echo; sed -n '1,200p' -- \"$p\" 2>/dev/null || true" ;; *) die "tools " @@ -2173,8 +2223,12 @@ cmd_core() { # If no target -> prompt from domain-which list if [[ -z "$target" ]]; then + local tools_ctr + tools_ctr="$(_project_tools_container_running || true)" + [[ -n "$tools_ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + local -a domains=() - mapfile -t domains < <(docker exec SERVER_TOOLS domain-which --list-domains 2>/dev/null | sed '/^[[:space:]]*$/d' || true) + mapfile -t domains < <(docker exec "$tools_ctr" domain-which --list-domains 2>/dev/null | sed '/^[[:space:]]*$/d' || true) ((${#domains[@]} > 0)) || die "No domains found" @@ -2222,8 +2276,12 @@ cmd_core() { # If target looks like a domain -> resolve via domain-which then shell in if [[ "$target" =~ $re ]]; then + local tools_ctr + tools_ctr="$(_project_tools_container_running || true)" + [[ -n "$tools_ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + local info type container profile docroot - info="$(docker exec SERVER_TOOLS domain-which "$target" 2>/dev/null)" || die "Unknown domain: $target" + info="$(docker exec "$tools_ctr" domain-which "$target" 2>/dev/null)" || die "Unknown domain: $target" IFS=$' \t' read -r type container profile docroot <<<"$info" [[ -n "${container:-}" ]] || die "No container resolved for: $target" @@ -2279,9 +2337,12 @@ cmd_doctor() { # Focused modes stay on doctor (status uses the full run only). if [[ "${1:-}" == "--lint" ]]; then shift || true - docker exec -i SERVER_TOOLS sh -lc ' + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + docker exec -i "$ctr" sh -lc ' set -e - command -v shellcheck >/dev/null 2>&1 || { echo "shellcheck not found in SERVER_TOOLS"; exit 1; } + command -v shellcheck >/dev/null 2>&1 || { echo "shellcheck not found in server-tools container"; exit 1; } files="" for f in "'"$DIR"'"/lds "'"$DIR"'"/bin/* "'"$DIR"'"/lib/*.sh; do [ -f "$f" ] && files="$files $f" @@ -2369,7 +2430,11 @@ _doctor_run() { # NOTIFY ############################################################################### notify_watch() { - local container="${1:-SERVER_TOOLS}" + local container="${1:-}" + if [[ -z "$container" ]]; then + container="$(_project_tools_container_running || true)" + [[ -n "$container" ]] || die "server-tools container is not running for project: $(lds_project)" + fi local prefix="__HOST_NOTIFY__" need docker @@ -2499,13 +2564,16 @@ notify_watch() { notify_test() { local title="${1:-Notifier OK}" - local body="${2:-Hello from host via SERVER_TOOLS}" - docker exec SERVER_TOOLS notify -t 2500 -u normal "$title" "$body" + local body="${2:-Hello from host via project server-tools container}" + local ctr + ctr="$(_project_tools_container_running || true)" + [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" + docker exec "$ctr" notify -t 2500 -u normal "$title" "$body" } cmd_notify() { case ${1:-watch} in - watch) notify_watch "${2:-SERVER_TOOLS}" ;; + watch) notify_watch "${2:-}" ;; test) notify_test "${2:-Notifier OK}" "${3:-Hello from host}" ;; *) die "notify " ;; esac @@ -3052,7 +3120,7 @@ cmd_stack_diff() { desired_df["$svc"]="$df" done < <(printf '%s' "$cfg_json" | jq -r '.services | to_entries[] | "\(.key)|\(.value.build.dockerfile // "")"') else - # try via SERVER_TOOLS jq + # try via project tools container jq while IFS= read -r line; do local svc="${line%%|*}" local img="${line#*|}" @@ -3111,7 +3179,7 @@ cmd_stack_diff() { cat "$tmp" | jq . rm -f "$tmp" else - die "jq required for --json (or run inside SERVER_TOOLS)" + die "jq required for --json (or run inside project server-tools container)" fi return 0 fi @@ -3259,7 +3327,7 @@ cmd_support_trace() { printf "%bTrace%b: %s\n" "$CYAN" "$NC" "$dom" # 1) DNS - if docker inspect -f '{{.State.Running}}' SERVER_TOOLS 2>/dev/null | grep -qx true; then + if _server_tools_running; then printf "\n%b[DNS]%b\n" "$DIM" "$NC" _tools_exec "dig +short $(_shq "$dom") || true; getent hosts $(_shq "$dom") 2>/dev/null || true" else @@ -3270,7 +3338,7 @@ cmd_support_trace() { # 2) TLS certificate printf "\n%b[TLS]%b\n" "$DIM" "$NC" - if docker inspect -f '{{.State.Running}}' SERVER_TOOLS 2>/dev/null | grep -qx true; then + if _server_tools_running; then _tools_exec "echo | openssl s_client -connect $(_shq "$dom"):443 -servername $(_shq "$dom") -showcerts 2>/dev/null | openssl x509 -noout -subject -issuer -dates 2>/dev/null || true" else echo | openssl s_client -connect "${dom}:443" -servername "$dom" -showcerts 2>/dev/null | openssl x509 -noout -subject -issuer -dates 2>/dev/null || true @@ -3278,7 +3346,7 @@ cmd_support_trace() { # 3) HTTP probe (timings) printf "\n%b[HTTP]%b\n" "$DIM" "$NC" - if docker inspect -f '{{.State.Running}}' SERVER_TOOLS 2>/dev/null | grep -qx true; then + if _server_tools_running; then _tools_exec "curl -sk -o /dev/null -D - -w 'time_namelookup=%{time_namelookup}\ntime_connect=%{time_connect}\ntime_appconnect=%{time_appconnect}\ntime_starttransfer=%{time_starttransfer}\ntime_total=%{time_total}\nhttp_code=%{http_code}\n' https://$(_shq "$dom") | sed -n '1,30p'" else curl -sk -o /dev/null -D - -w $'time_namelookup=%{time_namelookup}\ntime_connect=%{time_connect}\ntime_appconnect=%{time_appconnect}\ntime_starttransfer=%{time_starttransfer}\ntime_total=%{time_total}\nhttp_code=%{http_code}\n' "https://$dom" | sed -n '1,30p' @@ -3543,7 +3611,7 @@ Shortcuts: `open`, `bundle`, `notify`, `ui` map to `support …`. - `lds secrets …` - `lds env …` -## Tools (SERVER_TOOLS) +## Tools (project server-tools container) - `lds tools sh` - `lds tools exec ""` - `lds tools file ` @@ -3601,7 +3669,7 @@ ${CYAN}Config:${NC} ${CYAN}Doctor:${NC} doctor [run] Full environment checks - doctor lint shellcheck (inside SERVER_TOOLS) + doctor lint shellcheck (inside project server-tools container) doctor scan-logs [pattern] log scan (recent logs) doctor fix Safe auto-fixes (when available) @@ -3617,7 +3685,7 @@ ${CYAN}Secrets / Env:${NC} secrets env -${CYAN}Tools (SERVER_TOOLS):${NC} +${CYAN}Tools (project server-tools container):${NC} tools sh|exec|file ${CYAN}Setup:${NC} From c5e1134050a838413ff9e7ff8814a1cccb24696c Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Tue, 10 Mar 2026 17:26:20 +0600 Subject: [PATCH 38/48] lds split --- docker/compose/db-client.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docker/compose/db-client.yaml b/docker/compose/db-client.yaml index a7bebfd..e55aa09 100644 --- a/docker/compose/db-client.yaml +++ b/docker/compose/db-client.yaml @@ -18,6 +18,11 @@ services: volumes: - lds_ri:/data - ../../logs/redis-insight:/var/log/redis-insight + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8978/"] + interval: 30s + timeout: 10s + retries: 3 networks: datastore: ipv4_address: 172.30.0.150 From 93a6566531485c376416403dad60db1bed5f5c7e Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Wed, 11 Mar 2026 13:57:42 +0600 Subject: [PATCH 39/48] lds split --- docker/compose/db-client.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker/compose/db-client.yaml b/docker/compose/db-client.yaml index e55aa09..1376461 100644 --- a/docker/compose/db-client.yaml +++ b/docker/compose/db-client.yaml @@ -18,11 +18,6 @@ services: volumes: - lds_ri:/data - ../../logs/redis-insight:/var/log/redis-insight - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8978/"] - interval: 30s - timeout: 10s - retries: 3 networks: datastore: ipv4_address: 172.30.0.150 @@ -40,6 +35,11 @@ services: volumes: - lds_cb:/opt/cloudbeaver/workspace - ../../logs/cloudbeaver:/opt/cloudbeaver/logs + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8978/"] + interval: 30s + timeout: 10s + retries: 3 networks: datastore: ipv4_address: 172.30.0.151 From 6a3a2aa92aa6a9dac5de4d8eb9ff40c0ad765128 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 12 Mar 2026 14:03:02 +0600 Subject: [PATCH 40/48] lds split --- bin/es | 0 bin/mongo | 0 bin/tool-runner | 0 logs/cloudbeaver/.gitignore | 0 logs/elasticsearch/.gitignore | 0 logs/kibana/.gitignore | 0 logs/mongo-express/.gitignore | 0 logs/mongodb/.gitignore | 0 logs/php-fpm/.gitignore | 0 logs/postgresql/.gitignore | 0 logs/redis-insight/.gitignore | 0 logs/redis/.gitignore | 0 12 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 bin/es mode change 100644 => 100755 bin/mongo mode change 100644 => 100755 bin/tool-runner mode change 100644 => 100755 logs/cloudbeaver/.gitignore mode change 100644 => 100755 logs/elasticsearch/.gitignore mode change 100644 => 100755 logs/kibana/.gitignore mode change 100644 => 100755 logs/mongo-express/.gitignore mode change 100644 => 100755 logs/mongodb/.gitignore mode change 100644 => 100755 logs/php-fpm/.gitignore mode change 100644 => 100755 logs/postgresql/.gitignore mode change 100644 => 100755 logs/redis-insight/.gitignore mode change 100644 => 100755 logs/redis/.gitignore diff --git a/bin/es b/bin/es old mode 100644 new mode 100755 diff --git a/bin/mongo b/bin/mongo old mode 100644 new mode 100755 diff --git a/bin/tool-runner b/bin/tool-runner old mode 100644 new mode 100755 diff --git a/logs/cloudbeaver/.gitignore b/logs/cloudbeaver/.gitignore old mode 100644 new mode 100755 diff --git a/logs/elasticsearch/.gitignore b/logs/elasticsearch/.gitignore old mode 100644 new mode 100755 diff --git a/logs/kibana/.gitignore b/logs/kibana/.gitignore old mode 100644 new mode 100755 diff --git a/logs/mongo-express/.gitignore b/logs/mongo-express/.gitignore old mode 100644 new mode 100755 diff --git a/logs/mongodb/.gitignore b/logs/mongodb/.gitignore old mode 100644 new mode 100755 diff --git a/logs/php-fpm/.gitignore b/logs/php-fpm/.gitignore old mode 100644 new mode 100755 diff --git a/logs/postgresql/.gitignore b/logs/postgresql/.gitignore old mode 100644 new mode 100755 diff --git a/logs/redis-insight/.gitignore b/logs/redis-insight/.gitignore old mode 100644 new mode 100755 diff --git a/logs/redis/.gitignore b/logs/redis/.gitignore old mode 100644 new mode 100755 From 108e8ad8c80b58c6f2cc0422f1396d1d6288dce9 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sat, 14 Mar 2026 16:04:19 +0600 Subject: [PATCH 41/48] update dynamic compose --- configuration/{node => compose}/.gitignore | 0 docker/compose/companion.yaml | 2 +- docker/compose/main.yaml | 5 - docker/compose/php.yaml | 233 --------------------- lds | 2 +- 5 files changed, 2 insertions(+), 240 deletions(-) rename configuration/{node => compose}/.gitignore (100%) delete mode 100644 docker/compose/php.yaml diff --git a/configuration/node/.gitignore b/configuration/compose/.gitignore similarity index 100% rename from configuration/node/.gitignore rename to configuration/compose/.gitignore diff --git a/docker/compose/companion.yaml b/docker/compose/companion.yaml index d11ee3c..5ba4b8d 100644 --- a/docker/compose/companion.yaml +++ b/docker/compose/companion.yaml @@ -14,7 +14,7 @@ services: - lds_nginx_host:/etc/share/vhosts/nginx - lds_ssl_keys:/etc/mkcert - lds_fpm_pools:/etc/share/vhosts/fpm - - ../../configuration/node:/etc/share/vhosts/node + - ../../configuration/compose:/etc/share/vhosts/docker-compose - ../../configuration/sops/config:/etc/share/sops/config - ../../configuration/sops/global:/etc/share/sops/global - ../../configuration/sops/keys:/etc/share/sops/keys diff --git a/docker/compose/main.yaml b/docker/compose/main.yaml index a2ced2e..b70fd04 100644 --- a/docker/compose/main.yaml +++ b/docker/compose/main.yaml @@ -1,5 +1,4 @@ name: LocalDevStack - networks: frontend: name: Frontend @@ -12,7 +11,6 @@ networks: config: - subnet: 172.28.0.0/24 gateway: 172.28.0.1 - backend: name: Backend driver: bridge @@ -24,7 +22,6 @@ networks: config: - subnet: 172.29.0.0/24 gateway: 172.29.0.1 - datastore: name: DataStore driver: bridge @@ -139,10 +136,8 @@ volumes: com.infocyph.lds: "1" com.infocyph.stack: "LocalDevStack" com.infocyph.purpose: "Filebeat Data" - include: - docker/compose/companion.yaml - docker/compose/db.yaml - docker/compose/db-client.yaml - - docker/compose/php.yaml - docker/compose/http.yaml diff --git a/docker/compose/php.yaml b/docker/compose/php.yaml deleted file mode 100644 index 3a35b8d..0000000 --- a/docker/compose/php.yaml +++ /dev/null @@ -1,233 +0,0 @@ -x-php-service: &php-service - restart: unless-stopped - environment: - - TZ=${TZ:-} - env_file: - - "../../.env" - volumes: - - "${PROJECT_DIR:-./../../../application}:/app" - - lds_ssl_roots:/etc/share/rootCA:ro - - lds_fpm_sock:/home/${USERNAME}/.run/php-fpm - - lds_fpm_pools:/usr/local/etc/php-fpm.domains:ro - - ../conf/www-php.conf:/usr/local/etc/php-fpm.d/www.conf - - ../conf/openssl.cnf:/etc/ssl/openssl.cnf - - ../../configuration/php/php.ini:/usr/local/etc/php/conf.d/99-overrides.ini - - "../../configuration/ssh:/home/${USER}/.ssh:ro" - - "${HOME}/.gitconfig:/home/${USER}/.gitconfig:ro" - - ../../logs/php-fpm:/var/log/php-fpm - depends_on: - - server-tools - healthcheck: - test: ["CMD-SHELL", "php-fpm -t || exit 1"] - interval: 30s - timeout: 10s - retries: 3 - -services: - php73: - <<: *php-service - container_name: PHP_7.3 - hostname: php-73 - networks: - frontend: - ipv4_address: 172.28.0.50 - backend: - ipv4_address: 172.29.0.50 - datastore: - ipv4_address: 172.30.0.50 - build: - context: ../dockerfiles - dockerfile: php.Dockerfile - args: - UID: ${UID:-1000} - GID: ${GID:-root} - USERNAME: ${USER} - PHP_EXT: ${PHP_EXT:-} - LINUX_PKG: ${LINUX_PKG:-} - WORKING_DIR: ${WORKING_DIR} - PHP_VERSION: 7.3 - PHP_EXT_VERSIONED: ${PHP_EXT_73:-} - LINUX_PKG_VERSIONED: ${LINUX_PKG_73:-} - profiles: [php, php73] - - php74: - <<: *php-service - container_name: PHP_7.4 - hostname: php-74 - networks: - frontend: - ipv4_address: 172.28.0.51 - backend: - ipv4_address: 172.29.0.51 - datastore: - ipv4_address: 172.30.0.51 - build: - context: ../dockerfiles - dockerfile: php.Dockerfile - args: - UID: ${UID:-1000} - GID: ${GID:-1000} - USERNAME: ${USER} - PHP_EXT: ${PHP_EXT:-} - LINUX_PKG: ${LINUX_PKG:-} - WORKING_DIR: ${WORKING_DIR} - PHP_VERSION: 7.4 - PHP_EXT_VERSIONED: ${PHP_EXT_74:-} - LINUX_PKG_VERSIONED: ${LINUX_PKG_74:-} - profiles: [php, php74] - - php80: - <<: *php-service - container_name: PHP_8.0 - hostname: php-80 - networks: - frontend: - ipv4_address: 172.28.0.52 - backend: - ipv4_address: 172.29.0.52 - datastore: - ipv4_address: 172.30.0.52 - build: - context: ../dockerfiles - dockerfile: php.Dockerfile - args: - UID: ${UID:-1000} - GID: ${GID:-root} - USERNAME: ${USER} - PHP_EXT: ${PHP_EXT:-} - LINUX_PKG: ${LINUX_PKG:-} - WORKING_DIR: ${WORKING_DIR} - PHP_VERSION: 8.0 - PHP_EXT_VERSIONED: ${PHP_EXT_80:-} - LINUX_PKG_VERSIONED: ${LINUX_PKG_80:-} - profiles: [php, php80] - - php81: - <<: *php-service - container_name: PHP_8.1 - hostname: php-81 - networks: - frontend: - ipv4_address: 172.28.0.53 - backend: - ipv4_address: 172.29.0.53 - datastore: - ipv4_address: 172.30.0.53 - build: - context: ../dockerfiles - dockerfile: php.Dockerfile - args: - UID: ${UID:-1000} - GID: ${GID:-root} - USERNAME: ${USER} - PHP_EXT: ${PHP_EXT:-} - LINUX_PKG: ${LINUX_PKG:-} - WORKING_DIR: ${WORKING_DIR} - PHP_VERSION: 8.1 - PHP_EXT_VERSIONED: ${PHP_EXT_81:-} - LINUX_PKG_VERSIONED: ${LINUX_PKG_81:-} - profiles: [php, php81] - - php82: - <<: *php-service - container_name: PHP_8.2 - hostname: php-82 - networks: - frontend: - ipv4_address: 172.28.0.54 - backend: - ipv4_address: 172.29.0.54 - datastore: - ipv4_address: 172.30.0.54 - build: - context: ../dockerfiles - dockerfile: php.Dockerfile - args: - UID: ${UID:-1000} - GID: ${GID:-root} - USERNAME: ${USER} - PHP_EXT: ${PHP_EXT:-} - LINUX_PKG: ${LINUX_PKG:-} - WORKING_DIR: ${WORKING_DIR} - PHP_VERSION: 8.2 - PHP_EXT_VERSIONED: ${PHP_EXT_82:-} - LINUX_PKG_VERSIONED: ${LINUX_PKG_82:-} - profiles: [php, php82] - - php83: - <<: *php-service - container_name: PHP_8.3 - hostname: php-83 - networks: - frontend: - ipv4_address: 172.28.0.55 - backend: - ipv4_address: 172.29.0.55 - datastore: - ipv4_address: 172.30.0.55 - build: - context: ../dockerfiles - dockerfile: php.Dockerfile - args: - UID: ${UID:-1000} - GID: ${GID:-root} - USERNAME: ${USER} - PHP_EXT: ${PHP_EXT:-} - LINUX_PKG: ${LINUX_PKG:-} - WORKING_DIR: ${WORKING_DIR} - PHP_VERSION: 8.3 - PHP_EXT_VERSIONED: ${PHP_EXT_83:-} - LINUX_PKG_VERSIONED: ${LINUX_PKG_83:-} - profiles: [php, php83] - - php84: - <<: *php-service - container_name: PHP_8.4 - hostname: php-84 - networks: - frontend: - ipv4_address: 172.28.0.56 - backend: - ipv4_address: 172.29.0.56 - datastore: - ipv4_address: 172.30.0.56 - build: - context: ../dockerfiles - dockerfile: php.Dockerfile - args: - UID: ${UID:-1000} - GID: ${GID:-root} - USERNAME: ${USER} - PHP_EXT: ${PHP_EXT:-} - LINUX_PKG: ${LINUX_PKG:-} - WORKING_DIR: ${WORKING_DIR} - PHP_VERSION: 8.4 - PHP_EXT_VERSIONED: ${PHP_EXT_84:-} - LINUX_PKG_VERSIONED: ${LINUX_PKG_84:-} - profiles: [php, php84] - - php85: - <<: *php-service - container_name: PHP_8.5 - hostname: php-85 - networks: - frontend: - ipv4_address: 172.28.0.57 - backend: - ipv4_address: 172.29.0.57 - datastore: - ipv4_address: 172.30.0.57 - build: - context: ../dockerfiles - dockerfile: php.Dockerfile - args: - UID: ${UID:-1000} - GID: ${GID:-root} - USERNAME: ${USER} - PHP_EXT: ${PHP_EXT:-} - LINUX_PKG: ${LINUX_PKG:-} - WORKING_DIR: ${WORKING_DIR} - PHP_VERSION: 8.5 - PHP_EXT_VERSIONED: ${PHP_EXT_85:-} - LINUX_PKG_VERSIONED: ${LINUX_PKG_85:-} - profiles: [php, php85] diff --git a/lds b/lds index 3613fed..48d70a7 100755 --- a/lds +++ b/lds @@ -304,7 +304,7 @@ CFG="$DIR/docker" ENV_MAIN="$DIR/.env" ENV_DOCKER="$CFG/.env" COMPOSE_FILE="$CFG/compose/main.yaml" -EXTRAS_DIR="$DIR/configuration/node" +EXTRAS_DIR="$DIR/configuration/compose" if [[ "${1:-}" == "--__win_workdir" ]]; then export WORKDIR_WIN="${2:-}" From d94c68682a0ed14096f53f9d366c8b91f4ede4c6 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sat, 14 Mar 2026 16:45:59 +0600 Subject: [PATCH 42/48] Update lds --- lds | 110 +++++------------------------------------------------------- 1 file changed, 9 insertions(+), 101 deletions(-) diff --git a/lds b/lds index 48d70a7..c7a6fbd 100755 --- a/lds +++ b/lds @@ -650,50 +650,6 @@ rmhost() { [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" docker exec "$ctr" rmhost "$@" } -php_reload_or_restart_project() { - local project - project="$(lds_project)" - [[ -n "$project" ]] || return 0 - - local -a php_names=() - mapfile -t php_names < <( - docker ps --format '{{.Names}}\t{{.Label "com.docker.compose.project"}}' 2>/dev/null | - awk -F'\t' -v pr="$project" '$2==pr && $1 ~ /^PHP_/ {print $1}' - ) - ((${#php_names[@]})) || return 0 - - local name - for name in "${php_names[@]}"; do - if docker exec "$name" sh -lc ' - set -e - - # 1) config test (safe to skip if not supported) - if command -v php-fpm >/dev/null 2>&1; then - php-fpm -tt >/dev/null 2>&1 || exit 2 - fi - - # 2) find master pid - pid="" - for f in /var/run/php-fpm.pid /run/php-fpm/php-fpm.pid /run/php-fpm.pid; do - if [ -s "$f" ]; then pid="$(cat "$f" 2>/dev/null || true)"; break; fi - done - - if [ -z "$pid" ] && command -v ps >/dev/null 2>&1; then - # busybox ps output; pick "php-fpm: master" line if present - pid="$(ps 2>/dev/null | awk "/php-fpm: master/ {print \$1; exit}")" - fi - - # common case: php-fpm is PID 1 in the container - [ -n "$pid" ] || pid="1" - - kill -USR2 "$pid" >/dev/null 2>&1 - ' >/dev/null 2>&1; then - : # reloaded OK - else - docker restart "$name" >/dev/null 2>&1 || true - fi - done -} setup_domain() { local ctr @@ -702,17 +658,12 @@ setup_domain() { mkhost --RESET docker exec -it "$ctr" mkhost - local php_prof svr_prof node_prof - php_prof=$(mkhost --ACTIVE_PHP_PROFILE || true) - svr_prof=$(mkhost --APACHE_ACTIVE || true) - node_prof=$(mkhost --ACTIVE_NODE_PROFILE || true) - [[ -n $php_prof ]] && modify_profiles add "$php_prof" + local mk_state svr_prof + mk_state="$(mkhost --JSON || true)" + svr_prof="$(printf '%s' "$mk_state" | tr -d '\r\n' | sed -n 's/.*"apache_active"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')" [[ -n $svr_prof ]] && modify_profiles add "$svr_prof" - [[ -n $node_prof ]] && modify_profiles add "$node_prof" mkhost --RESET - php_reload_or_restart_project - dc_up -d - http_reload + cmd_reboot } delete_domain() { @@ -725,56 +676,13 @@ delete_domain() { # interactive delete docker exec -it "$ctr" rmhost "$@" - local php_cont apache_cont node_cont - php_cont=$(rmhost --DELETE_PHP_PROFILE || true) - apache_cont=$(rmhost --APACHE_DELETE || true) - node_cont=$(rmhost --DELETE_NODE_PROFILE || true) - - # helper: stop+remove containers by a comma-separated list of exact names - _rm_containers_csv() { - local csv="$1" - [[ -n "$csv" ]] || return 0 - local IFS=',' name - for name in $csv; do - name="$(echo "$name" | xargs)" - [[ -n "$name" ]] || continue - docker rm -f "$name" >/dev/null 2>&1 || true - done - } - - # Stop/remove containers immediately (exact names from rmhost) - _rm_containers_csv "$php_cont" - _rm_containers_csv "$apache_cont" - _rm_containers_csv "$node_cont" - - # Remove profiles (if your modify_profiles expects profile keys, keep using what rmhost returns for profiles; - # if rmhost also returns exact profile names here, this is fine too.) - if [[ -n "$php_cont" ]]; then - local IFS=',' p - for p in $php_cont; do - p="$(echo "$p" | xargs)" - [[ -n "$p" ]] && modify_profiles remove "$p" - done - fi - if [[ -n "$apache_cont" ]]; then - local IFS=',' p - for p in $apache_cont; do - p="$(echo "$p" | xargs)" - [[ -n "$p" ]] && modify_profiles remove "$p" - done - fi - if [[ -n "$node_cont" ]]; then - local IFS=',' p - for p in $node_cont; do - p="$(echo "$p" | xargs)" - [[ -n "$p" ]] && modify_profiles remove "$p" - done - fi + local rm_state apache_cont + rm_state="$(rmhost --JSON || true)" + apache_cont="$(printf '%s' "$rm_state" | tr -d '\r\n' | sed -n 's/.*"apache_delete"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')" + [[ -n "$apache_cont" ]] && modify_profiles remove "$apache_cont" rmhost --RESET - php_reload_or_restart_project - dc_up -d - http_reload + cmd_reboot } modify_profiles() { From daae60805d10a0f70482cea1b914787d992088df Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sat, 14 Mar 2026 19:23:19 +0600 Subject: [PATCH 43/48] Update lds --- lds | 436 ++++++++++++------------------------------------------------ 1 file changed, 83 insertions(+), 353 deletions(-) diff --git a/lds b/lds index c7a6fbd..922a497 100755 --- a/lds +++ b/lds @@ -660,7 +660,11 @@ setup_domain() { docker exec -it "$ctr" mkhost local mk_state svr_prof mk_state="$(mkhost --JSON || true)" - svr_prof="$(printf '%s' "$mk_state" | tr -d '\r\n' | sed -n 's/.*"apache_active"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')" + if has_tool jq; then + svr_prof="$(printf '%s' "$mk_state" | jq -r '.state.apache_active // empty' 2>/dev/null || true)" + else + svr_prof="$(printf '%s' "$mk_state" | tr -d '\r\n' | sed -n 's/.*"apache_active"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')" + fi [[ -n $svr_prof ]] && modify_profiles add "$svr_prof" mkhost --RESET cmd_reboot @@ -678,7 +682,11 @@ delete_domain() { local rm_state apache_cont rm_state="$(rmhost --JSON || true)" - apache_cont="$(printf '%s' "$rm_state" | tr -d '\r\n' | sed -n 's/.*"apache_delete"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')" + if has_tool jq; then + apache_cont="$(printf '%s' "$rm_state" | jq -r '.state.apache_delete // empty' 2>/dev/null || true)" + else + apache_cont="$(printf '%s' "$rm_state" | tr -d '\r\n' | sed -n 's/.*"apache_delete"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')" + fi [[ -n "$apache_cont" ]] && modify_profiles remove "$apache_cont" rmhost --RESET @@ -1565,17 +1573,22 @@ cmd_open() { _known_profile() { local p="${1:-}" [[ -n "$p" ]] || return 1 - # quick/cheap check across compose files + + # Prefer compose-config JSON for exact profile membership. + local json + json="$(compose_cfg_json)" + if [[ -n "$json" ]] && has_tool jq; then + printf '%s' "$json" | jq -e --arg p "$p" ' + [ .services[]? | (.profiles // [])[] ] | index($p) != null + ' >/dev/null 2>&1 + return $? + fi + + # Fallback: text scan when jq/json path is unavailable. local f for f in "$COMPOSE_FILE" "${__EXTRA_FILES[@]:-}"; do [[ -r "$f" ]] || continue - if grep -Eq "profiles:[[:space:]]*\[.*\b${p}\b.*\]" "$f" || grep -Eq "profiles:[[:space:]]*$" "$f"; then - # fall through to deeper check with rg if present - : - fi - if grep -Eq "\b${p}\b" "$f"; then - return 0 - fi + grep -Fq -- "$p" "$f" && return 0 done return 1 } @@ -1688,7 +1701,7 @@ cmd_sniff() { } # ───────────────────────────────────────────────────────────────────────────── -# 6e. SECRETS / CERT / HOST / UI / RUNTIME +# 6e. SECRETS / CERT / HOST / UI # ───────────────────────────────────────────────────────────────────────────── cmd_secrets() { local ctr @@ -1735,23 +1748,8 @@ cmd_ui() { docker exec -it "$ctr" lazydocker } -cmd_runtime() { - local which="${1:-}" - [[ -n "$which" ]] || die "runtime " - local f="" - for f in "$DIR/runtime-versions.json" "$DIR/docker/runtime-versions.json" "$DIR/configuration/runtime-versions.json"; do - [[ -r "$f" ]] && break || f="" - done - if [[ -z "$f" ]]; then - printf "%b[warn]%b runtime-versions.json not found -" "$YELLOW" "$NC" - return 0 - fi - _tools_exec "jq -r '."${which}" // empty' "$f" 2>/dev/null || cat "$f"" -} - # ───────────────────────────────────────────────────────────────────────────── -# 6f. EXEC / CHECK / EVENTS / CLEAN / ENV / VERIFY / DISK +# 6f. EXEC / EVENTS / CLEAN / DISK # ───────────────────────────────────────────────────────────────────────────── cmd_exec() { local svc="${1:-}" @@ -1767,39 +1765,6 @@ cmd_exec() { fi } -cmd_check() { - local sub="${1:-}" - shift || true - case "${sub,,}" in - upstream) - local dom="${1:-}" - [[ -n "$dom" ]] || die "check upstream " - local nconf="$DIR/configuration/nginx/$dom.conf" - [[ -r "$nconf" ]] || die "No nginx conf for domain: $dom" - printf "%bDomain:%b %s -" "$CYAN" "$NC" "$dom" - if grep -q fastcgi_pass "$nconf"; then - local php - php="$(grep -Eo 'fastcgi_pass ([^:]+):9000' "$nconf" | awk '{print $2}' | sed 's/:9000$//' | head -n1 || true)" - printf "Upstream (php): %s -" "${php:-unknown}" - [[ -n "$php" ]] && _tools_exec "nc -vz -w2 "$php" 9000 || true" - elif grep -q proxy_pass "$nconf"; then - local up - up="$(grep -m1 -Eo 'proxy_pass[[:space:]]+http://[^;]+' "$nconf" | awk '{print $2}' | sed 's|^http://||' || true)" - printf "Upstream (http): %s -" "${up:-unknown}" - local h="${up%%:*}" p="${up##*:}" - [[ -n "$h" && -n "$p" && "$h" != "$up" ]] && _tools_exec "nc -vz -w2 "$h" "$p" || true" - fi - _tools_exec "curl -vkI "https://$dom" || true" - ;; - *) - die "check upstream " - ;; - esac -} - cmd_events() { local since="${1:-1h}" local project @@ -1819,62 +1784,36 @@ cmd_clean() { vols=1 shift ;; - *) shift ;; + *) + die "clean [--yes|-y] [--volumes|-v]" + ;; esac done + ((yes)) || die "clean requires --yes" - local project - project="$(lds_project)" - # remove stopped containers for this project - docker rm -f $(docker ps -a --filter "label=com.docker.compose.project=$project" --filter "status=exited" -q) 2>/dev/null || true - if ((vols)); then - docker volume rm $(docker volume ls -q --filter "label=com.docker.compose.project=$project") 2>/dev/null || true - fi - printf "%b[clean]%b done -" "$GREEN" "$NC" -} -cmd_env() { - local ctr - ctr="$(_project_tools_container_running || true)" - [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" - docker exec -it "$ctr" senv env "$@" -} - -cmd_verify() { - # smoke checks: compose config, LB up, curl each domain - docker_compose config >/dev/null - cmd_status --quiet >/dev/null || true - local d - while IFS= read -r d; do - [[ -n "$d" ]] || continue - _tools_exec "curl -skI "https://$d" | head -n 1" - done < <( - shopt -s nullglob - for f in "$DIR/configuration/nginx/"*.conf; do basename -- "$f" .conf; done - shopt -u nullglob - ) -} + printf "%b[clean]%b pruning stopped containers...\n" "$CYAN" "$NC" + docker container prune -f >/dev/null 2>&1 || true -# ───────────────────────────────────────────────────────────────────────────── -# 6g. NGINX INTROSPECTION -# ───────────────────────────────────────────────────────────────────────────── -cmd_nginx() { - local dom="${1:-}" - [[ -n "$dom" ]] || die "nginx " - local f="$DIR/configuration/nginx/$dom.conf" - [[ -r "$f" ]] || die "No nginx conf: $f" - printf "%bNginx vhost:%b %s -" "$CYAN" "$NC" "$f" - grep -nE 'server_name|listen|root |proxy_pass|fastcgi_pass|include |error_page' "$f" || true - if grep -q '/etc/nginx/html' "$f"; then - printf "%b[warn]%b mentions /etc/nginx/html (default root fallback risk) -" "$YELLOW" "$NC" + printf "%b[clean]%b pruning unused networks...\n" "$CYAN" "$NC" + docker network prune -f >/dev/null 2>&1 || true + + printf "%b[clean]%b pruning unused images...\n" "$CYAN" "$NC" + docker image prune -a -f >/dev/null 2>&1 || true + + printf "%b[clean]%b pruning build cache...\n" "$CYAN" "$NC" + docker builder prune -a -f >/dev/null 2>&1 || true + + if ((vols)); then + printf "%b[clean]%b pruning unused volumes...\n" "$CYAN" "$NC" + docker volume prune -f >/dev/null 2>&1 || true fi + + printf "%b[clean]%b done\n" "$GREEN" "$NC" } # ───────────────────────────────────────────────────────────────────────────── -# 6h. HELP MARKDOWN +# 6g. HELP MARKDOWN # ───────────────────────────────────────────────────────────────────────────── normalize_service() { @@ -2188,23 +2127,13 @@ cmd_core() { tools_ctr="$(_project_tools_container_running || true)" [[ -n "$tools_ctr" ]] || die "server-tools container is not running for project: $(lds_project)" - local info type container profile docroot + local info container info="$(docker exec "$tools_ctr" domain-which "$target" 2>/dev/null)" || die "Unknown domain: $target" - IFS=$' \t' read -r type container profile docroot <<<"$info" + IFS=$' \t' read -r _ container _ _ <<<"$info" [[ -n "${container:-}" ]] || die "No container resolved for: $target" + # Always open project root. Do not follow inferred/public docroot. local wd="/app" - case "$type" in - node) wd="/app" ;; - php) - if [[ -n "${docroot:-}" && "${docroot:-}" == /* ]]; then - wd="/app${docroot}" - else - wd="/app" - fi - ;; - *) wd="/app" ;; - esac docker exec -it "$container" bash -lc "cd \"$wd\" 2>/dev/null || cd /app 2>/dev/null || cd /; exec bash" return 0 @@ -2241,99 +2170,6 @@ cmd_certificate() { esac } -cmd_doctor() { - # Focused modes stay on doctor (status uses the full run only). - if [[ "${1:-}" == "--lint" ]]; then - shift || true - local ctr - ctr="$(_project_tools_container_running || true)" - [[ -n "$ctr" ]] || die "server-tools container is not running for project: $(lds_project)" - docker exec -i "$ctr" sh -lc ' - set -e - command -v shellcheck >/dev/null 2>&1 || { echo "shellcheck not found in server-tools container"; exit 1; } - files="" - for f in "'"$DIR"'"/lds "'"$DIR"'"/bin/* "'"$DIR"'"/lib/*.sh; do - [ -f "$f" ] && files="$files $f" - done - [ -n "$files" ] || { echo "No scripts found to lint"; exit 0; } - shellcheck -x $files - ' - return 0 - fi - - if [[ "${1:-}" == "--scan-logs" ]]; then - shift || true - local pat="${1:-error|failed|panic|segfault|permission denied|fatal}" - docker_compose logs --since 30m 2>&1 | text_grep -i "$pat" || true - return 0 - fi - - _doctor_run -} - -_doctor_run() { - local os_id os_like - IFS="|" read -r os_id os_like < <(detect_os_family) - - local is_win=0 - [[ "${OSTYPE:-}" =~ (msys|cygwin|win32) ]] && is_win=1 - - printf "%bDoctor%b — environment checks\n" "$MAGENTA" "$NC" - printf "%bOS%b: id=%s like=%s\n" "$CYAN" "$NC" "${os_id:-unknown}" "${os_like:-unknown}" - ((is_win)) && printf "%bShell%b: Windows\n" "$CYAN" "$NC" || printf "%bShell%b: Unix\n" "$CYAN" "$NC" - - _ok() { printf " %b✔%b %s\n" "$GREEN" "$NC" "$*"; } - _warn() { printf " %b!%b %s\n" "$YELLOW" "$NC" "$*"; } - _bad() { printf " %b✘%b %s\n" "$RED" "$NC" "$*"; } - _has() { has_cmd "$1"; } - - # TTY sanity - [[ -t 0 ]] && _ok "stdin: ok" || _warn "stdin: not a TTY" - [[ -t 1 ]] && _ok "stdout: ok" || _warn "stdout: not a TTY" - [[ -r /dev/tty ]] && _ok "tty: ok" || _warn "tty: not readable" - - # Shell tool availability (grouped) - local -a avail=() miss=() - local c - for c in awk sed grep find sort; do - if _has "$c"; then avail+=("$c"); else miss+=("$c"); fi - done - printf " %bAvailable%b\t%s\n" "$GREEN" "$NC" "${avail[*]:- -}" - printf " %bUnavailable%b\t%s\n" "$RED" "$NC" "${miss[*]:- -}" - - # Docker + daemon - local docker_cmd="" - if _has docker; then docker_cmd=docker; elif _has docker.exe; then docker_cmd=docker.exe; fi - if [[ -n "$docker_cmd" ]]; then - _ok "docker: $($docker_cmd --version 2>/dev/null || echo ok)" - if $docker_cmd info >/dev/null 2>&1; then _ok "docker daemon: ok"; else _bad "docker daemon: not reachable"; fi - else - _bad "docker: missing" - fi - - # Compose - if docker_compose version >/dev/null 2>&1; then _ok "compose: ok"; else _bad "compose: missing"; fi - if docker_compose config -q >/dev/null 2>&1; then _ok "compose config: ok"; else _bad "compose config: invalid (run: lds config)"; fi - - # Optional tools (quiet) - _has openssl && _ok "openssl: ok" || _warn "openssl: missing" - (_has curl || _has wget) && _ok "http client: ok" || _warn "http client: missing (curl/wget)" - _has jq && _ok "jq: ok" || _warn "jq: missing" - - # File/dir sanity (only show path when NOT ok) - [[ -f "$ENV_DOCKER" ]] && _ok "env: ok" || _warn "env: missing ($ENV_DOCKER)" - [[ -d "$DIR/docker" ]] && _ok "docker dir: ok" || _bad "docker dir: missing ($DIR/docker)" - [[ -d "$DIR/configuration" ]] && _ok "configuration dir: ok" || _bad "configuration dir: missing ($DIR/configuration)" - [[ -d "$DIR/data" ]] && _ok "data dir: ok" || _warn "data dir: missing ($DIR/data)" - [[ -d "$DIR/logs" ]] && _ok "logs dir: ok" || _warn "logs dir: missing ($DIR/logs)" - - [[ -f "$DIR/configuration/rootCA/rootCA.pem" ]] && _ok "rootCA.pem: ok" || _warn "rootCA.pem: missing ($DIR/configuration/rootCA/rootCA.pem)" - - if [[ -f "$DIR/lds" ]]; then - if grep -U $"\r" "$DIR/lds" >/dev/null 2>&1; then _warn "line endings: CRLF detected ($DIR/lds)"; else _ok "line endings: ok"; fi - fi -} - ############################################################################### # NOTIFY ############################################################################### @@ -2961,7 +2797,7 @@ cmd_run() { } ############################################################################### -# 6w. NEW FEATURES: stack diff | domain export/import | support trace +# 6w. NEW FEATURES: stack diff | support trace ############################################################################### # stack diff: show what would run (compose) vs what's running (docker) @@ -3027,13 +2863,27 @@ cmd_stack_diff() { local df="${line#*|}" desired_df["$svc"]="$df" done < <(printf '%s' "$cfg_json" | jq -r '.services | to_entries[] | "\(.key)|\(.value.build.dockerfile // "")"') - else - # try via project tools container jq - while IFS= read -r line; do - local svc="${line%%|*}" - local img="${line#*|}" - desired_img["$svc"]="$img" - done < <(_tools_exec "printf %q \"$cfg_json\" | jq -r '.services | to_entries[] | \"\\(.key)|\\(.value.image // \\\"\\\")\"' " 2>/dev/null || true) + elif _server_tools_has jq; then + # Fallback: parse via project tools container jq through stdin (no shell re-quoting of JSON payload). + local ctr + ctr="$(_project_tools_container_running || true)" + if [[ -n "$ctr" ]]; then + while IFS= read -r line; do + local svc="${line%%|*}" + local img="${line#*|}" + desired_img["$svc"]="$img" + done < <(printf '%s' "$cfg_json" | docker exec -i "$ctr" jq -r '.services | to_entries[] | "\(.key)|\(.value.image // "")"' 2>/dev/null || true) + while IFS= read -r line; do + local svc="${line%%|*}" + local ctx="${line#*|}" + desired_ctx["$svc"]="$ctx" + done < <(printf '%s' "$cfg_json" | docker exec -i "$ctr" jq -r '.services | to_entries[] | "\(.key)|\(.value.build.context // "")"' 2>/dev/null || true) + while IFS= read -r line; do + local svc="${line%%|*}" + local df="${line#*|}" + desired_df["$svc"]="$df" + done < <(printf '%s' "$cfg_json" | docker exec -i "$ctr" jq -r '.services | to_entries[] | "\(.key)|\(.value.build.dockerfile // "")"' 2>/dev/null || true) + fi fi fi @@ -3134,98 +2984,6 @@ cmd_stack_diff() { printf " - Use: lds stack diff --config (to print resolved compose config)\n" } -# domain export: dump nginx vhosts (and a few inferred properties) to JSON -cmd_domain_export() { - local out="" pretty=1 - while [[ "${1:-}" ]]; do - case "$1" in - --out) - out="${2:-}" - shift 2 - ;; - --compact) - pretty=0 - shift - ;; - *) break ;; - esac - done - - local dir_ng="$DIR/configuration/nginx" - [[ -d "$dir_ng" ]] || die "nginx vhost dir not found: $dir_ng" - - local tmp - tmp="$(mktemp)" - printf '{ "version": 1, "exported_at": "%s", "root": "%s", "domains": [' "$(date -Iseconds)" "$DIR" >"$tmp" - - local first=1 f dom typ upstream cmb - shopt -s nullglob - for f in "$dir_ng"/*.conf; do - dom="$(basename -- "$f" .conf)" - typ="static" - upstream="" - cmb="" - if grep -q 'fastcgi_pass' "$f"; then - typ="php" - upstream="$(grep -Eo 'fastcgi_pass[[:space:]]+[^;]+' "$f" | awk '{print $2}' | head -n1 || true)" - elif grep -q 'proxy_pass' "$f"; then - typ="proxy" - upstream="$(grep -m1 -Eo 'proxy_pass[[:space:]]+http[s]?://[^;]+' "$f" | awk '{print $2}' | head -n1 || true)" - fi - cmb="$(grep -m1 -Eo 'client_max_body_size[[:space:]]+[^;]+' "$f" | awk '{print $2}' | head -n1 || true)" - - ((first)) || printf ',' >>"$tmp" - first=0 - # include raw conf as base64 so import can restore exactly - local b64 - b64="$(base64 -w0 <"$f" 2>/dev/null || base64 <"$f" | tr -d '\n')" - printf '\n{ "domain": "%s", "type": "%s", "upstream": "%s", "client_max_body_size": "%s", "files": { "nginx_conf": "%s" } }' \ - "$dom" "$typ" "$upstream" "$cmb" "$b64" >>"$tmp" - done - shopt -u nullglob - printf '\n] }\n' >>"$tmp" - - if [[ -n "$out" ]]; then - if ((pretty)) && has_tool jq; then - cat "$tmp" | jq . >"$out" - else - cat "$tmp" >"$out" - fi - ok "[ok] domain export: $out\n" - else - if ((pretty)) && has_tool jq; then - cat "$tmp" | jq . - else - cat "$tmp" - fi - fi - rm -f "$tmp" -} - -# domain import: restore nginx vhosts from JSON created by domain export -cmd_domain_import() { - local in="${1:-}" - [[ -n "$in" ]] || die "domain import " - [[ -r "$in" ]] || die "cannot read: $in" - - local dir_ng="$DIR/configuration/nginx" - mkdir -p "$dir_ng" 2>/dev/null || true - - need_tool jq "required for domain import" - - local count=0 - while IFS= read -r dom; do - local b64 - b64="$(jq -r --arg d "$dom" '.domains[] | select(.domain==$d) | .files.nginx_conf' "$in")" - [[ -n "$b64" && "$b64" != "null" ]] || continue - printf '%s' "$b64" | base64 -d >"$dir_ng/$dom.conf" - count=$((count + 1)) - done < <(jq -r '.domains[].domain' "$in") - - ok "[ok] domain import: restored $count nginx vhost(s) into $dir_ng\n" - warn "[info] Next steps: run 'lds stack restart nginx' (or 'lds stack up') to apply changes.\n" -} - # support trace: quick end-to-end trace for a domain cmd_support_trace() { local dom="${1:-}" @@ -3282,7 +3040,7 @@ cmd_support_trace() { printf "\n%b[Recent nginx logs]%b\n" "$DIM" "$NC" docker_compose logs --no-color --tail 120 nginx 2>/dev/null | text_grep -i "$dom" || docker_compose logs --no-color --tail 120 nginx 2>/dev/null || true - printf "\n%bDone.%b If this still looks wrong, run: lds doctor run | lds diag tls %s\n" "$GREEN" "$NC" "$dom" + printf "\n%bDone.%b If this still looks wrong, run: lds diag tls %s\n" "$GREEN" "$NC" "$dom" } ############################################################################### @@ -3304,12 +3062,11 @@ cmd_stack() { exec) cmd_exec "$@" ;; events) cmd_events "$@" ;; clean) cmd_clean "$@" ;; - verify) cmd_verify "$@" ;; config) cmd_config "$@" ;; diff) cmd_stack_diff "$@" ;; http) cmd_http "$@" ;; *) - die "stack " + die "stack " ;; esac } @@ -3319,16 +3076,12 @@ cmd_domain() { local sub="${1:-}" shift || true case "${sub,,}" in - "" | help | -h | --help) die "domain " ;; + "" | help | -h | --help) die "domain " ;; add) cmd_host add "$@" ;; rm | remove | del | delete) cmd_host rm "$@" ;; ls | list) cmd_host list "$@" ;; - export) cmd_domain_export "$@" ;; - import) cmd_domain_import "$@" ;; - check) cmd_check upstream "$@" ;; - nginx) cmd_nginx "$@" ;; *) - die "domain " + die "domain " ;; esac } @@ -3455,7 +3208,7 @@ cmd_help() { cat <<'MD' # LocalDevStack (lds) — Command Reference -## Stack (compose + runtime) +## Stack (compose) - `lds stack up` *(aliases: `up`)* - `lds stack start` *(aliases: `start`)* - `lds stack down [--volumes --yes]` *(aliases: `down`, `stop`)* @@ -3466,7 +3219,6 @@ cmd_help() { - `lds stack exec [cmd…]` *(alias: `exec`)* - `lds stack events [--since ]` *(alias: `events`)* - `lds stack clean --yes [--volumes]` *(alias: `clean`)* -- `lds stack verify` *(alias: `verify`)* - `lds stack diff [--config] [--json]` *(shows desired vs running images)* @@ -3474,8 +3226,6 @@ cmd_help() { - `lds domain add …` - `lds domain rm …` - `lds domain ls` -- `lds domain check ` -- `lds domain nginx ` Legacy alias: `lds host …` → same subcommands as `domain`. @@ -3500,12 +3250,6 @@ Legacy alias: `lds host …` → same subcommands as `domain`. - `lds config env-used` - `lds config validate` -## Doctor -- `lds doctor` / `lds doctor run` -- `lds doctor lint` -- `lds doctor scan-logs` -- `lds doctor fix` - ## Support - `lds support open support trace ` @@ -3515,9 +3259,8 @@ Legacy alias: `lds host …` → same subcommands as `domain`. Shortcuts: `open`, `bundle`, `notify`, `ui` map to `support …`. -## Secrets / Env (senv) +## Secrets (senv) - `lds secrets …` -- `lds env …` ## Tools (project server-tools container) - `lds tools sh` @@ -3530,11 +3273,7 @@ Shortcuts: `open`, `bundle`, `notify`, `ui` map to `support …`. ## Runner (ad‑hoc Dockerfile runner) - `lds run` *(default: build+start only)* / `lds run shell` *(build+start+enter)* / `lds run *` *(same as default)* (+ `ps|logs|stop|rm|open` and flags: `--publish|-p`, `--no-keepalive`, `--mount`, `--sock`, `--tag`, `--name`) -## Runtime awareness -- `lds runtime php|node` - ## Other -- `lds nginx ` - `lds rebuild [all|]` - `lds core [domain]` @@ -3551,18 +3290,18 @@ MD cat < - clean|verify Alias of: stack <...> + clean Alias of: stack <...> ${CYAN}Domain (vhosts + routing):${NC} - domain add|rm|ls|export|import|check|nginx - host add|rm|list|check|nginx Legacy alias of: domain <...> + domain add|rm|ls + host add|rm|list Legacy alias of: domain <...> ${CYAN}Certificates (TLS):${NC} cert status|regen|diagnose @@ -3575,12 +3314,6 @@ ${CYAN}Diagnostics:${NC} ${CYAN}Config:${NC} config show|services|profiles|env-used|validate -${CYAN}Doctor:${NC} - doctor [run] Full environment checks - doctor lint shellcheck (inside project server-tools container) - doctor scan-logs [pattern] log scan (recent logs) - doctor fix Safe auto-fixes (when available) - ${CYAN}Support:${NC} support open support trace @@ -3589,9 +3322,8 @@ ${CYAN}Support:${NC} support ui open|bundle|notify|ui Shortcuts → support <...> -${CYAN}Secrets / Env:${NC} +${CYAN}Secrets:${NC} secrets - env ${CYAN}Tools (project server-tools container):${NC} tools sh|exec|file @@ -3603,8 +3335,6 @@ ${CYAN}Runner (ad-hoc Dockerfile runner):${NC} run [ps|logs|stop|rm|open] [--publish|-p A:B] [--no-keepalive] [--mount HOST[:CONT]] [--sock] ${CYAN}Other:${NC} - runtime php|node - nginx rebuild [all|] core [domain] From 4b21b6c33e8613f8f1a97821bfc8d0126a8824a6 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Sat, 14 Mar 2026 20:58:14 +0600 Subject: [PATCH 44/48] Update lds --- lds | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lds b/lds index 922a497..b4928c5 100755 --- a/lds +++ b/lds @@ -1877,6 +1877,9 @@ cmd_rebuild() { _pick_targets_interactive() { compose_services_load all_svcs=("${__COMPOSE_SVCS[@]}") + if ((${#all_svcs[@]})); then + mapfile -t all_svcs < <(printf '%s\n' "${all_svcs[@]}" | LC_ALL=C sort -f -u) + fi [[ ${#all_svcs[@]} -gt 0 ]] || die "No services found (docker compose config --services failed?)" echo From 8fe745dad20aaab6b635c7d203caac5374287c4b Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Mon, 16 Mar 2026 10:48:02 +0600 Subject: [PATCH 45/48] lds split --- docker/compose/companion.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/compose/companion.yaml b/docker/compose/companion.yaml index 5ba4b8d..b355e0b 100644 --- a/docker/compose/companion.yaml +++ b/docker/compose/companion.yaml @@ -71,8 +71,8 @@ services: - TZ=${TZ:-} - MP_MAX_MESSAGES=5000 - MP_DATABASE=/data/mailpit.db - - MP_SMTP_TLS_CERT=/certs/local.pem - - MP_SMTP_TLS_KEY=/certs/local-key.pem + - MP_SMTP_TLS_CERT=/certs/lds-server.pem + - MP_SMTP_TLS_KEY=/certs/lds-server-key.pem - MP_SMTP_REQUIRE_STARTTLS=true - MP_SMTP_AUTH_ACCEPT_ANY=1 volumes: From 311eb6df645169c1b046e9a4a25c3b4973ab49d4 Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Tue, 17 Mar 2026 21:47:10 +0600 Subject: [PATCH 46/48] update node --- docker/compose/http.yaml | 2 +- docker/dockerfiles/node.Dockerfile | 2 +- logs/.gitignore | 1 + logs/node/.gitignore | 2 ++ 4 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 logs/node/.gitignore diff --git a/docker/compose/http.yaml b/docker/compose/http.yaml index de16a44..55779e6 100644 --- a/docker/compose/http.yaml +++ b/docker/compose/http.yaml @@ -11,7 +11,7 @@ services: - "${HTTPS_PORT:-443}:443" volumes: - "${PROJECT_DIR:-./../../../application}:/app" - - lds_nginx_host:/etc/nginx/conf.d:ro + - lds_nginx_host:/etc/nginx/conf.d - lds_ssl_keys:/etc/mkcert:ro - lds_ssl_roots:/etc/share/rootCA:ro - lds_fpm_sock:/run/php-fpm:ro diff --git a/docker/dockerfiles/node.Dockerfile b/docker/dockerfiles/node.Dockerfile index 197d5c6..6df4b73 100644 --- a/docker/dockerfiles/node.Dockerfile +++ b/docker/dockerfiles/node.Dockerfile @@ -17,7 +17,7 @@ ARG NODE_GLOBAL_VERSIONED ENV PATH="/usr/local/bin:/usr/bin:/bin:/usr/games:$PATH" \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \ - NPM_CONFIG_CACHE=/tmp/.npm-cache \ + NPM_CONFIG_CACHE=/home/${USERNAME}/.npm \ GIT_CREDENTIAL_STORE=/home/${USERNAME}/.git-credentials ADD https://raw.githubusercontent.com/infocyph/Scriptomatic/master/bash/node-cli-setup.sh /usr/local/bin/cli-setup.sh diff --git a/logs/.gitignore b/logs/.gitignore index 1b0dd2d..c8eca25 100755 --- a/logs/.gitignore +++ b/logs/.gitignore @@ -14,4 +14,5 @@ !redis !php-fpm !olds +!node !.gitignore \ No newline at end of file diff --git a/logs/node/.gitignore b/logs/node/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/logs/node/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file From a8d8030cb47dbe572effc4fbebdc3a41057f9ecd Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Tue, 17 Mar 2026 22:06:22 +0600 Subject: [PATCH 47/48] Update http.yaml --- docker/compose/http.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker/compose/http.yaml b/docker/compose/http.yaml index 55779e6..6091018 100644 --- a/docker/compose/http.yaml +++ b/docker/compose/http.yaml @@ -29,8 +29,6 @@ services: hostname: apache image: infocyph/apache:latest restart: always - profiles: - - apache environment: - TZ=${TZ:-} volumes: From 70c18b9d404dfe116ee51ad8521906d68d530b2c Mon Sep 17 00:00:00 2001 From: "A. B. M. Mahmudul Hasan" Date: Thu, 19 Mar 2026 22:08:35 +0600 Subject: [PATCH 48/48] core --- docker/compose/companion.yaml | 2 ++ docker/dockerfiles/node.Dockerfile | 1 + lds | 17 +++++++++++------ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docker/compose/companion.yaml b/docker/compose/companion.yaml index b355e0b..c13f216 100644 --- a/docker/compose/companion.yaml +++ b/docker/compose/companion.yaml @@ -15,6 +15,8 @@ services: - lds_ssl_keys:/etc/mkcert - lds_fpm_pools:/etc/share/vhosts/fpm - ../../configuration/compose:/etc/share/vhosts/docker-compose + - ../../configuration/scheduler/cron-jobs:/etc/share/scheduler/cron-jobs + - ../../configuration/scheduler/supervisor:/etc/share/scheduler/supervisor - ../../configuration/sops/config:/etc/share/sops/config - ../../configuration/sops/global:/etc/share/sops/global - ../../configuration/sops/keys:/etc/share/sops/keys diff --git a/docker/dockerfiles/node.Dockerfile b/docker/dockerfiles/node.Dockerfile index 6df4b73..016b1ed 100644 --- a/docker/dockerfiles/node.Dockerfile +++ b/docker/dockerfiles/node.Dockerfile @@ -28,5 +28,6 @@ RUN apk add --no-cache bash && \ USER ${USERNAME} RUN sudo /usr/local/bin/git-default WORKDIR /app +EXPOSE 3000 ENTRYPOINT ["/usr/local/bin/node-entry"] CMD [] \ No newline at end of file diff --git a/lds b/lds index b4928c5..a3d69b7 100755 --- a/lds +++ b/lds @@ -2130,13 +2130,17 @@ cmd_core() { tools_ctr="$(_project_tools_container_running || true)" [[ -n "$tools_ctr" ]] || die "server-tools container is not running for project: $(lds_project)" - local info container - info="$(docker exec "$tools_ctr" domain-which "$target" 2>/dev/null)" || die "Unknown domain: $target" - IFS=$' \t' read -r _ container _ _ <<<"$info" + local app container wd + app="$(docker exec "$tools_ctr" domain-which --app --quiet "$target" 2>/dev/null)" || die "Unknown domain: $target" + container="$(docker exec "$tools_ctr" domain-which --container --quiet "$target" 2>/dev/null)" || die "No container resolved for: $target" + wd="$(docker exec "$tools_ctr" domain-which --docroot --quiet "$target" 2>/dev/null)" || true [[ -n "${container:-}" ]] || die "No container resolved for: $target" - # Always open project root. Do not follow inferred/public docroot. - local wd="/app" + # Node apps should always land at /app. Others follow resolved docroot. + if [[ "${app:-}" == "node" ]]; then + wd="/app" + fi + [[ -n "${wd:-}" ]] || wd="/app" docker exec -it "$container" bash -lc "cd \"$wd\" 2>/dev/null || cd /app 2>/dev/null || cd /; exec bash" return 0 @@ -2516,7 +2520,8 @@ run_start() { done if [[ "$keepalive" == 1 ]]; then - args+=(--entrypoint sh "$tag" -c "trap : TERM INT; sleep infinity & wait") + # Keepalive mode replaces the image command; disable image healthcheck to avoid false "unhealthy". + args+=(--no-healthcheck --entrypoint sh "$tag" -c "trap : TERM INT; sleep infinity & wait") else args+=("$tag") fi