a simple shell script to setup my development environment
#!/bin/bash
set -e
# --- Constants ---
CLOUDFLARED_DIR="$HOME/.cloudflared"
CONFIG_DIR="$HOME/.config/code-server"
LOG_DIR="$HOME/tmp"
BASHRC="$HOME/.bashrc"
mkdir -p "$LOG_DIR"
# --- Utility Functions ---
error_exit() {
echo -e "\n[!] ERROR: $1" >&2
exit 1
}
confirm() {
# prompts go to stderr by default with read -p
read -p "$1 [y/N]: " response
case "$response" in
[yY][eE][sS]|[yY]) return 0 ;;
*) return 1 ;;
esac
}
get_input() {
local prompt="$1"
local default="$2"
local var_name="$3"
read -p "$prompt [$default]: " input
if [ -z "$input" ]; then
eval $var_name=\"$default\"
else
eval $var_name=\"$input\"
fi
}
# Run cloudflared commands without reading the potentially broken config.yml
cf_cmd() {
cloudflared --config /dev/null "$@" 2> >(grep -v "Configuration file /dev/null was empty" >&2)
}
add_to_path() {
local new_path="$1"
if [[ ":$PATH:" != *":$new_path:"* ]]; then
export PATH="$new_path:$PATH"
if ! grep -q "export PATH=\"$new_path:\$PATH\"" "$BASHRC"; then
echo "export PATH=\"$new_path:\$PATH\"" >> "$BASHRC"
fi
fi
}
# --- Install Functions ---
install_dependencies() {
echo -e "\n=========================================="
echo " Dependency Installation"
echo "=========================================="
# Bun
if confirm "Install/Update Bun?"; then
echo " -> Installing Bun..."
curl -fsSL https://bun.sh/install | bash || echo "Bun install failed, continuing..."
export BUN_INSTALL="$HOME/.bun"
add_to_path "$BUN_INSTALL/bin"
fi
# NVM & Node
if confirm "Install/Update NVM & Node (LTS)?"; then
echo " -> Detecting latest NVM version..."
local LATEST_NVM=$(curl -s https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep -oP '"tag_name": "\K[^"]+')
echo " -> Latest NVM: $LATEST_NVM"
echo " -> Installing NVM..."
export NVM_DIR="$HOME/.nvm"
curl -o- "https://raw.githubusercontent.com/nvm-sh/nvm/${LATEST_NVM}/install.sh" | bash || echo "NVM install failed"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
echo " -> Installing Node LTS..."
nvm install --lts || echo "Node install failed"
fi
# Go
if confirm "Install/Update Go?"; then
echo " -> Detecting latest Go version..."
local LATEST_GO=$(curl -s https://go.dev/VERSION?m=text | head -n 1)
echo " -> Latest version: $LATEST_GO"
local GO_DIR="$HOME/.local/go"
echo " -> Installing $LATEST_GO..."
rm -rf "$GO_DIR"
wget -q --show-progress "https://go.dev/dl/${LATEST_GO}.linux-amd64.tar.gz" -O /tmp/go.tar.gz
mkdir -p "$HOME/.local"
tar -C "$HOME/.local" -xzf /tmp/go.tar.gz
rm /tmp/go.tar.gz
add_to_path "$GO_DIR/bin"
echo " -> Go installed to $GO_DIR"
fi
}
# --- Configuration Functions ---
configure_code_server() {
echo -e "\n--- Code Server Configuration ---" >&2
get_input "Code-Server Password" "12890000" CS_PASSWORD
get_input "Code-Server Port" "8080" CS_PORT
mkdir -p "$CONFIG_DIR"
cat > "$CONFIG_DIR/config.yaml" <<EOF
bind-addr: 127.0.0.1:$CS_PORT
auth: password
password: $CS_PASSWORD
cert: false
EOF
echo " -> Code-Server configured." >&2
}
get_tunnel_id() {
local tunnel_name="$1"
local tid=""
# Use cf_cmd to ignore broken config.yml
if cf_cmd tunnel list | grep -q "$tunnel_name"; then
tid=$(cf_cmd tunnel list | grep "$tunnel_name" | awk "{print \$1}")
fi
if [ -z "$tid" ]; then
echo " -> Tunnel \"$tunnel_name\" not found." >&2
if confirm "Create new tunnel \"$tunnel_name\"?"; then
CREATE_OUTPUT=$(cf_cmd tunnel create "$tunnel_name" 2>&1)
if [ $? -ne 0 ]; then
echo "Failed to create tunnel: $CREATE_OUTPUT" >&2
exit 1
fi
tid=$(echo "$CREATE_OUTPUT" | grep -oE "[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}" | head -n 1)
else
echo "Tunnel ID required to proceed." >&2
exit 1
fi
fi
echo "$tid"
}
update_ingress() {
local tunnel_name="$1"
local tunnel_id="$2"
local force_dns="$3"
echo -e "\n--- Ingress Configuration (Domains & Ports) ---"
# We build the config in memory first
local config_content="tunnel: $tunnel_id
credentials-file: $CLOUDFLARED_DIR/$tunnel_id.json
ingress:"
# 1. Main Code Server Ingress
get_input "Code-Server Domain" "code.zulfikar.site" CS_DOMAIN
get_input "Code-Server Local Port" "$CS_PORT" CS_PORT
config_content="$config_content
- hostname: $CS_DOMAIN
service: http://localhost:$CS_PORT"
# Route DNS for main domain
echo " -> Routing DNS for $CS_DOMAIN..."
if [ "$force_dns" = "true" ]; then
cf_cmd tunnel route dns -f "$tunnel_name" "$CS_DOMAIN" || true
else
cf_cmd tunnel route dns "$tunnel_name" "$CS_DOMAIN" || true
fi
# 2. Additional Dev Ports
while true; do
echo -e "\n--- Add Dev Tunnel? ---"
if ! confirm "Add a dev server/port mapping?"; then
break
fi
get_input "Dev Domain (e.g., dev.zulfikar.site)" "dev.zulfikar.site" DEV_DOMAIN
get_input "Dev Local Port (e.g., 3000)" "3000" DEV_PORT
config_content="$config_content
- hostname: $DEV_DOMAIN
service: http://localhost:$DEV_PORT"
echo " -> Routing DNS for $DEV_DOMAIN..."
if [ "$force_dns" = "true" ]; then
cf_cmd tunnel route dns -f "$tunnel_name" "$DEV_DOMAIN" || true
else
cf_cmd tunnel route dns "$tunnel_name" "$DEV_DOMAIN" || true
fi
done
# 3. Catch-all
config_content="$config_content
- service: http_status:404"
# Write Config
echo "$config_content" > "$CLOUDFLARED_DIR/config.yml"
echo " -> Ingress configuration written to $CLOUDFLARED_DIR/config.yml"
}
restart_services() {
echo -e "\n--- Restarting Services ---"
# Kill old
pkill -f "node .*code-server" || true
pkill -f "cloudflared .*tunnel run" || true
# Kill port holders
if [ ! -z "$CS_PORT" ]; then fuser -k ${CS_PORT}/tcp || true; fi
rm -f ~/.local/share/code-server/code-server-ipc.sock
sleep 1
# Start Code Server
echo " -> Starting code-server..."
setsid nohup code-server > "$LOG_DIR/code-server.log" 2>&1 &
# Wait for code-server (to avoid 1033)
echo " -> Waiting for code-server to start on port $CS_PORT..."
local retries=0
while ! curl -s "http://127.0.0.1:$CS_PORT" > /dev/null; do
sleep 1
retries=$((retries+1))
if [ $retries -gt 15 ]; then
echo "[!] Warning: code-server taking too long to start. Check $LOG_DIR/code-server.log"
break
fi
done
# Start Cloudflared
echo " -> Starting cloudflared..."
# We must NOT use cf_cmd here because we DO want to read the real config file now!
setsid nohup cloudflared --config "$CLOUDFLARED_DIR/config.yml" tunnel run > "$LOG_DIR/cloudflared.log" 2>&1 &
echo " -> Services restarted."
}
# --- Main Logic ---
echo "=========================================="
echo " Interactive Setup Wizard"
echo "=========================================="
# 1. Dependency Phase
if confirm "Do you want to manage/install dependencies (Bun, Go, Node)?"; then
install_dependencies
fi
# 2. Operation Mode
echo -e "\nChoose Operation Mode:"
echo "1) Full Setup (Clean Slate: New Tunnel + Config + Restart)"
echo "2) Update Configuration Only (Add ports, Change domains, Restart)"
echo "3) Skip Setup (Exit)"
get_input "Selection" "1" MODE
case "$MODE" in
1)
FORCE_DNS="true"
configure_code_server
get_input "Tunnel Name" "code-server" TUNNEL_NAME
# Cleanup
if cf_cmd tunnel list | grep -q "$TUNNEL_NAME"; then
echo " -> Existing tunnel \"$TUNNEL_NAME\" found."
if confirm "FORCE delete existing tunnel and start fresh?"; then
cf_cmd tunnel delete -f "$TUNNEL_NAME" || true
fi
fi
TUNNEL_ID=$(get_tunnel_id "$TUNNEL_NAME")
# Check if ID is empty (safety)
if [ -z "$TUNNEL_ID" ] || [ "${#TUNNEL_ID}" -lt 10 ]; then
echo "[!] Error: Invalid Tunnel ID captured: $TUNNEL_ID"
exit 1
fi
update_ingress "$TUNNEL_NAME" "$TUNNEL_ID" "$FORCE_DNS"
restart_services
;;
2)
get_input "Force update DNS routes? (Use if domain is same)" "y" FORCE_INP
[[ "$FORCE_INP" =~ ^[yY] ]] && FORCE_DNS="true" || FORCE_DNS="false"
if confirm "Update Code-Server Config (Password/Port)?"; then
configure_code_server
else
CS_PORT="8080" # Default if not updating
fi
get_input "Tunnel Name" "code-server" TUNNEL_NAME
TUNNEL_ID=$(get_tunnel_id "$TUNNEL_NAME")
# Check if ID is empty (safety)
if [ -z "$TUNNEL_ID" ] || [ "${#TUNNEL_ID}" -lt 10 ]; then
echo "[!] Error: Invalid Tunnel ID captured: $TUNNEL_ID"
exit 1
fi
update_ingress "$TUNNEL_NAME" "$TUNNEL_ID" "$FORCE_DNS"
if confirm "Restart Services to apply changes?"; then
restart_services
fi
;;
*)
echo "Exiting..."
exit 0
;;
esac
echo -e "\n[+] Done! Logs are in $LOG_DIR"
echo "[+] Access your code-server at your domain."