Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ Supports **multiple domains** and local TLS.
> - use a DNS that resolves to your machine, **or**
> - use `*.localhost` (no hosts entry required in many setups)

---

## Prerequisites (Docker)

Install docker on your system first. If you already have docker installed, you can skip this step.
- It is recommended to use [Docker Engine](https://docs.docker.com/engine/install/).
- If Docker Engine not supported in your OS, use [Docker Desktop](https://docs.docker.com/desktop/) (although you can also install this on linux as well).

## Supported Project Languages

- PHP
- NodeJs

---

## Quickstart
Expand Down Expand Up @@ -60,8 +63,6 @@ lds setup domain
sudo lds certificate install
```

---

## CLI help (built-in “man”)

```bash
Expand Down
234 changes: 217 additions & 17 deletions lds
Original file line number Diff line number Diff line change
Expand Up @@ -208,18 +208,40 @@ logv() { ((VERBOSE)) && printf "%b[%s]%b %s\n" "$CYAN" "${1:-info}" "$NC" "${2:-
logq() { printf "%b[%s]%b %s\n" "$CYAN" "${1:-info}" "$NC" "${2:-}" >&2; }

# Unified prompt helper (used by env_init + profiles)
tty_readline() {
# Robust prompt/read across Linux/macOS/WSL/Windows Git Bash.
# Prefer stdin when it is a TTY (normal interactive use). If stdin is not a TTY,
# fall back to /dev/tty when available.
local __var_name="$1" __prompt="$2" __line

if [[ -t 0 ]]; then
# Interactive: show prompt on stderr (so it is never swallowed) and read stdin.
printf '%s' "$__prompt" >&2
IFS= read -r __line || return 1
elif [[ -r /dev/tty ]]; then
# Non-interactive stdin (piped) but we still have a controlling terminal.
printf '%s' "$__prompt" > /dev/tty
IFS= read -r __line < /dev/tty || return 1
else
return 1
fi

printf -v "$__var_name" '%s' "$__line"
}

read_default() {
local prompt=$1 default=$2 input
read -rp "$(printf '%b%s [default: %s]:%b ' "$CYAN" "$prompt" "$default" "$NC")" input
tty_readline input "$(printf '%b%s [default: %s]:%b ' "$CYAN" "$prompt" "$default" "$NC")" || return 1
printf '%s' "${input:-$default}"
}

ask_yes() {
local prompt="$1" ans
read -rp "$(printf "%b%s (y/n): %b" "$BLUE" "$prompt" "$NC")" ans
tty_readline ans "$(printf '%b%s (y/n): %b' "$BLUE" "$prompt" "$NC")" || return 1
[[ "${ans,,}" == "y" ]]
}


# ── dotenv quoting: only quote when needed (spaces, tabs, #, quotes, leading/trailing whitespace) ──
env_quote() {
# Wrap in double-quotes and escape backslash + double-quote + newlines
Expand Down Expand Up @@ -423,15 +445,15 @@ modify_profiles() {
# ─────────────────────────────────────────────────────────────────────────────

declare -A SERVICES=(
[ELASTICSEARCH]="elasticsearch"
[POSTGRESQL]="postgresql"
[MYSQL]="mysql"
[MARIADB]="mariadb"
[ELASTICSEARCH]="elasticsearch"
[MONGODB]="mongodb"
[REDIS]="redis"
[POSTGRESQL]="postgresql"
)

declare -a SERVICE_ORDER=(ELASTICSEARCH MYSQL MARIADB MONGODB REDIS POSTGRESQL)
declare -a SERVICE_ORDER=(POSTGRESQL MYSQL MARIADB ELASTICSEARCH MONGODB REDIS)

declare -A PROFILE_ENV=(
[elasticsearch]="ELASTICSEARCH_VERSION=9.2.4 ELASTICSEARCH_PORT=9200"
Expand Down Expand Up @@ -463,15 +485,95 @@ flush_profiles() {
done
}

process_service() {
# ── setup menu (selection-first) ──────────────────────────────────────────────

setup_menu_print() {
# Print menu to stderr to avoid stdout buffering in some Windows wrappers.
{
printf "\n%bSetup profiles%b (will replace previous configuration, if exists):\n\n" "$CYAN" "$NC"
local i=1 key slug
for key in "${SERVICE_ORDER[@]}"; do
slug="${SERVICES[$key]}"
printf " %2d) %-12s (%s)\n" "$i" "$key" "$slug"
i=$((i+1))
done
printf "\n a) ALL\n"
printf " n) NONE / Back\n\n"
} >&2
}

# Parse user selection into indices or ALL/NONE (prints one token per line)
setup_menu_parse() {
local input="${1//[[:space:]]/}"
[[ -n "$input" ]] || return 1
input="${input//;/,}"

echo "$input" | tr ',' '\n' | awk '
BEGIN { ok=1 }
/^[0-9]+-[0-9]+$/ {
split($0,a,"-")
if (a[1] > a[2]) { t=a[1]; a[1]=a[2]; a[2]=t }
for (i=a[1]; i<=a[2]; i++) print i
next
}
/^[0-9]+$/ { print $0; next }
/^[aA]$/ { print "ALL"; next }
/^[nN]$/ { print "NONE"; next }
{ ok=0 }
END { if (!ok) exit 2 }
'
}

# Outputs: newline-separated service KEYS from SERVICE_ORDER (e.g. MYSQL, REDIS)
setup_choose_services() {
local ans parsed
while :; do
setup_menu_print
tty_readline ans "Select (e.g. 1,3,5 or 2-4 or a): " || return 1

if ! parsed="$(setup_menu_parse "$ans" 2>/dev/null)"; then
printf "%bInvalid selection.%b Try again.\n" "$YELLOW" "$NC"
continue
fi

if grep -qx "NONE" <<<"$parsed"; then
return 1
fi

if grep -qx "ALL" <<<"$parsed"; then
printf "%s\n" "${SERVICE_ORDER[@]}"
return 0
fi

# Indices -> keys (de-dupe, preserve order)
local -A seen=()
local out=()
local idx key
while IFS= read -r idx; do
[[ "$idx" =~ ^[0-9]+$ ]] || continue
(( idx >= 1 && idx <= ${#SERVICE_ORDER[@]} )) || continue
key="${SERVICE_ORDER[idx-1]}"
[[ -n "${seen[$key]:-}" ]] && continue
seen[$key]=1
out+=("$key")
done <<<"$parsed"

if ((${#out[@]}==0)); then
printf "%bNo valid items selected.%b\n" "$YELLOW" "$NC"
continue
fi

printf "%s\n" "${out[@]}"
return 0
done
}

setup_service() {
local service="$1"
printf "\n%b→ %s%b\n" "$YELLOW" "$service" "$NC"
ask_yes "Enable $service?" || {
printf "%bSkipping %s%b\n" "$RED" "$service" "$NC"
return
}
local profile="${SERVICES[$service]:-}"
[[ -n "$profile" ]] || die "Unknown service: $service"

local profile="${SERVICES[$service]}"
printf "\n%b→ %s%b\n" "$YELLOW" "$service" "$NC"
queue_profile "$profile"

printf "%bEnter value(s) for %s:%b\n" "$BLUE" "$service" "$NC"
Expand All @@ -484,13 +586,26 @@ process_service() {
}

process_all() {
local selected
if ! selected="$(setup_choose_services)"; then
printf "\n%bSetup cancelled.%b\n" "$YELLOW" "$NC"
return 0
fi

printf "\n%bWill configure:%b\n" "$CYAN" "$NC"
while IFS= read -r svc; do
printf " - %s (%s)\n" "$svc" "${SERVICES[$svc]}"
done <<<"$selected"
echo

local svc
for svc in "${SERVICE_ORDER[@]}"; do
process_service "$svc"
done
while IFS= read -r svc; do
setup_service "$svc"
done <<<"$selected"

flush_envs
flush_profiles
printf "\n%b✅ All services configured!%b\n" "$GREEN" "$NC"
printf "\n%b✅ Selected services configured!%b\n" "$GREEN" "$NC"
}

###############################################################################
Expand Down Expand Up @@ -675,7 +790,87 @@ ca_plan() {
esac
}


is_windows_shell() {
[[ "${OSTYPE:-}" =~ (msys|cygwin) ]] || [[ -n "${WORKDIR_WIN:-}" ]]
}

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."
}

install_ca_windows() {
need_windows_tools

local src_ca="$DIR/configuration/rootCA/rootCA.pem"
[[ -r "$src_ca" ]] || die "certificate not found: $src_ca"

local win_ca
win_ca="$(cygpath -w "$src_ca")"

printf "%bInstalling root CA into Windows trust store (CurrentUser\\Root)…%b\n" "$CYAN" "$NC"

powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "
\$ErrorActionPreference = 'Stop'
\$path = '$win_ca'
\$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(\$path)

\$store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root','CurrentUser')
\$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)

\$exists = \$store.Certificates | Where-Object { \$_.Thumbprint -eq \$cert.Thumbprint }
if (-not \$exists) { \$store.Add(\$cert) }

\$store.Close()
" >/dev/null 2>&1 || die "Windows certificate install failed (PowerShell import)."

printf "%bRoot CA installed on Windows%b (CurrentUser\\Root)\n" "$GREEN" "$NC"
printf "%bNote:%b restart browsers if they still show trust errors.\n" "$YELLOW" "$NC"
}

uninstall_ca_windows() {
need_windows_tools

local src_ca="$DIR/configuration/rootCA/rootCA.pem"
[[ -r "$src_ca" ]] || die "certificate not found: $src_ca"

local win_ca
win_ca="$(cygpath -w "$src_ca")"

printf "%bUninstalling root CA from Windows trust store (CurrentUser\\Root)…%b\n" "$CYAN" "$NC"

local removed
removed="$(powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "
\$ErrorActionPreference = 'Stop'
\$path = '$win_ca'
\$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(\$path)
\$thumb = \$cert.Thumbprint

\$store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root','CurrentUser')
\$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)

\$matches = @(\$store.Certificates | Where-Object { \$_.Thumbprint -eq \$thumb })
foreach (\$c in \$matches) { \$store.Remove(\$c) }

\$store.Close()
[string]\$matches.Count
" 2>/dev/null || true)"

removed="${removed//[$'\r\n\t ']/}"
if [[ "${removed:-0}" =~ ^[0-9]+$ ]] && ((removed > 0)); then
printf "%bRoot CA uninstalled on Windows%b (removed %s cert)\n" "$GREEN" "$NC" "$removed"
else
printf "%bRoot CA already absent on Windows%b (no matching cert)\n" "$YELLOW" "$NC"
fi
}

install_ca() {
if is_windows_shell; then
install_ca_windows
return 0
fi

local src_ca="$DIR/configuration/rootCA/rootCA.pem"
[[ ${EUID:-$(id -u)} -eq 0 ]] || die "certificate install requires sudo"
[[ -r "$src_ca" ]] || die "certificate not found: $src_ca"
Expand Down Expand Up @@ -770,6 +965,11 @@ install_ca() {
}

uninstall_ca() {
if is_windows_shell; then
uninstall_ca_windows
return 0
fi

[[ ${EUID:-$(id -u)} -eq 0 ]] || die "certificate uninstall requires sudo"

local all=0
Expand Down Expand Up @@ -1333,7 +1533,7 @@ core_pick_domain() {
local ans idx
while true; do
printf "%bDomain #%b " "$GREEN" "$NC" >&2
IFS= read -r ans || return 130
tty_readline ans "" || return 130
ans="${ans//[[:space:]]/}"
[[ -n "$ans" ]] || continue

Expand Down