#!/usr/bin/env bash set -Eeuo pipefail export DEBIAN_FRONTEND=noninteractive LOG_FILE="${HOME}/init-$(date +%F_%H-%M-%S).log" STEP_NO=0 exec > >(tee -a "$LOG_FILE") exec 2>&1 on_error() { local exit_code=$? local line_no="$1" local cmd="$2" echo echo "❌ [ERROR] 초기 세팅 중 오류가 발생했습니다." echo " - line : ${line_no}" echo " - command : ${cmd}" echo " - exit code : ${exit_code}" echo " - log file : ${LOG_FILE}" exit "$exit_code" } trap 'on_error "${LINENO}" "${BASH_COMMAND}"' ERR step() { STEP_NO=$((STEP_NO + 1)) echo echo "==================================================" echo "[STEP ${STEP_NO}] $1" echo "==================================================" } done_msg() { echo "✅ 완료: $1" } info() { echo "ℹ️ $1" } latest_tag() { curl -fsSL "https://api.github.com/repos/$1/releases/latest" \ | grep '"tag_name":' | cut -d '"' -f 4 } append_if_missing() { local file="$1" local line="$2" mkdir -p "$(dirname "$file")" touch "$file" if ! grep -Fqx "$line" "$file"; then echo "$line" >> "$file" fi } require_sudo() { if [[ "${EUID}" -eq 0 ]]; then return 0 fi info "sudo 권한을 비대화형으로 확인합니다..." if sudo -n true 2>/dev/null; then done_msg "sudo 권한 확인" else echo "❌ 현재 계정은 passwordless sudo(NOPASSWD) 상태가 아닙니다." echo " - 스크립트에서 비밀번호 프롬프트 없이 실행하려면 sudoers에 NOPASSWD 설정이 필요합니다." echo " - 확인 명령: sudo -l" echo " - 설정 예시: sudo visudo" echo " ${USER} ALL=(ALL) NOPASSWD: ALL" exit 1 fi } detect_env() { ARCH="$(uname -m)" KERNEL="$(uname -r)" if [[ "$ARCH" != "x86_64" ]]; then echo "❌ 이 스크립트는 x86_64 전용입니다. (현재 아키텍처: ${ARCH})" exit 1 fi if [[ -r /etc/os-release ]]; then . /etc/os-release OS_ID="${ID:-unknown}" OS_VERSION="${VERSION_ID:-unknown}" OS_PRETTY="${PRETTY_NAME:-unknown}" else OS_ID="unknown" OS_VERSION="unknown" OS_PRETTY="unknown" fi if ! command -v apt-get >/dev/null 2>&1; then echo "❌ 이 스크립트는 apt 기반(Debian/Ubuntu 계열) 배포판 전용입니다. (현재: ${OS_PRETTY})" exit 1 fi } print_env() { step "현재 환경 정보" info " - OS : ${OS_PRETTY} (${OS_ID} ${OS_VERSION})" info " - 커널 : ${KERNEL}" info " - 아키텍처 : ${ARCH}" info " - 사용자 : ${USER} (HOME=${HOME})" done_msg "현재 환경 정보" } ask_yn() { local prompt="$1" default="$2" reply hint if [[ "$default" == "y" ]]; then hint="[Y/n]" else hint="[y/N]" fi printf '%s %s: ' "$prompt" "$hint" > /dev/tty 2>/dev/null || true read -r reply < /dev/tty 2>/dev/null || reply="" reply="${reply:-$default}" case "${reply,,}" in y|yes) return 0 ;; *) return 1 ;; esac } prompt_config() { step "설치/설정 항목 선택" info "각 항목에서 Enter 를 누르면 대괄호 안의 기본값이 적용됩니다." echo printf '로케일을 입력하세요 [ko_KR.UTF-8]: ' > /dev/tty 2>/dev/null || true read -r LOCALE < /dev/tty 2>/dev/null || LOCALE="" LOCALE="${LOCALE:-ko_KR.UTF-8}" printf '타임존을 입력하세요 [Asia/Seoul]: ' > /dev/tty 2>/dev/null || true read -r TIMEZONE < /dev/tty 2>/dev/null || TIMEZONE="" TIMEZONE="${TIMEZONE:-Asia/Seoul}" if ask_yn "Starship 프롬프트를 설치/설정할까요?" "y"; then DO_STARSHIP="y"; else DO_STARSHIP="n"; fi if ask_yn "Git 전역 설정을 적용할까요?" "y"; then DO_GIT="y"; else DO_GIT="n"; fi if ask_yn "Docker(도커 + compose plugin + 그룹 권한)를 설치/설정할까요?" "y"; then DO_DOCKER="y"; else DO_DOCKER="n"; fi if ask_yn "Rust(cargo) 를 설치할까요?" "y"; then DO_RUST="y"; else DO_RUST="n"; fi if ask_yn "lsd 를 설치하고 ls alias 를 설정할까요?" "y"; then DO_LSD="y"; else DO_LSD="n"; fi if ask_yn "Neovim + NvChad 을 설치할까요?" "y"; then DO_NEOVIM="y"; else DO_NEOVIM="n"; fi if ask_yn "Yazi 를 설치할까요?" "y"; then DO_YAZI="y"; else DO_YAZI="n"; fi if ask_yn "fzf 를 설치할까요?" "y"; then DO_FZF="y"; else DO_FZF="n"; fi if ask_yn "ripgrep 를 설치할까요?" "y"; then DO_RIPGREP="y"; else DO_RIPGREP="n"; fi if ask_yn "mise 를 설치할까요?" "y"; then DO_MISE="y"; else DO_MISE="n"; fi echo info "선택한 구성으로 진행합니다:" info " - 로케일 : ${LOCALE}" info " - 타임존 : ${TIMEZONE}" info " - Starship : ${DO_STARSHIP}" info " - Git 전역 설정 : ${DO_GIT}" info " - Docker : ${DO_DOCKER}" info " - Rust(cargo) : ${DO_RUST}" info " - lsd (+alias) : ${DO_LSD}" info " - Neovim + NvChad : ${DO_NEOVIM}" info " - Yazi : ${DO_YAZI}" info " - fzf : ${DO_FZF}" info " - ripgrep : ${DO_RIPGREP}" info " - mise : ${DO_MISE}" done_msg "설치/설정 항목 선택" } install_starship() { step "Starship 설치" curl -sS https://starship.rs/install.sh | sudo sh -s -- -y append_if_missing "${HOME}/.bashrc" 'eval "$(starship init bash)"' done_msg "Starship 설치" } configure_lsd_alias() { step "lsd alias 설정" append_if_missing "${HOME}/.bashrc" 'alias ls="lsd"' done_msg "lsd alias 설정" } configure_starship() { step "Starship 설정 파일 생성" mkdir -p "${HOME}/.config" cat << 'EOF' > "${HOME}/.config/starship.toml" format="[STARSHIP](fg:red) $all" [time] disabled = false format = '🕙[\[$time\]]($style) ' time_format = '%T' utc_time_offset = 'local' EOF done_msg "Starship 설정 파일 생성" } install_neovim() { step "Neovim 설치 (공식 tarball)" local tarball="/tmp/nvim-linux-x86_64.tar.gz" local install_dir="/opt/nvim-linux-x86_64" curl -L -o "${tarball}" \ https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.tar.gz sudo rm -rf "${install_dir}" sudo tar -C /opt -xzf "${tarball}" rm -f "${tarball}" sudo ln -sf "${install_dir}/bin/nvim" /usr/local/bin/nvim append_if_missing "${HOME}/.bashrc" 'alias vi="nvim"' done_msg "Neovim 설치" step "NvChad starter 설치" if [[ -d "${HOME}/.config/nvim" ]]; then info "${HOME}/.config/nvim 가 이미 존재하여 NvChad 설치를 건너뜁니다." else git clone https://github.com/NvChad/starter "${HOME}/.config/nvim" rm -rf "${HOME}/.config/nvim/.git" fi done_msg "NvChad starter 설치" } install_yazi() { step "Yazi 설치 (릴리즈 zip)" sudo apt-get install -y unzip local zip="/tmp/yazi.zip" local tmpdir tmpdir="$(mktemp -d)" curl -L -o "${zip}" \ https://github.com/sxyazi/yazi/releases/latest/download/yazi-x86_64-unknown-linux-musl.zip unzip -q "${zip}" -d "${tmpdir}" sudo mv "${tmpdir}"/*/{ya,yazi} /usr/local/bin/ rm -rf "${tmpdir}" "${zip}" done_msg "Yazi 설치" step "Yazi 셸 함수(y) 설정" if ! grep -Fq 'function y()' "${HOME}/.bashrc" 2>/dev/null; then cat << 'EOF' >> "${HOME}/.bashrc" function y() { local tmp="$(mktemp -t "yazi-cwd.XXXXXX")" cwd command yazi "$@" --cwd-file="$tmp" IFS= read -r -d '' cwd < "$tmp" [ "$cwd" != "$PWD" ] && [ -d "$cwd" ] && builtin cd -- "$cwd" command rm -f -- "$tmp" } EOF fi done_msg "Yazi 셸 함수(y) 설정" step "Yazi keymap 설정 파일 생성" mkdir -p "${HOME}/.config/yazi" cat << 'EOF' > "${HOME}/.config/yazi/keymap.toml" [[mgr.prepend_keymap]] on = "" run = "open --interactive" desc = "Open selected files interactively" EOF done_msg "Yazi keymap 설정 파일 생성" } install_fzf() { step "fzf 설치 (릴리즈 tarball, latest)" local tag version tag="$(latest_tag junegunn/fzf)" version="${tag#v}" local tarball="/tmp/fzf-${version}-linux_amd64.tar.gz" local tmpdir tmpdir="$(mktemp -d)" curl -L -o "${tarball}" \ "https://github.com/junegunn/fzf/releases/download/${tag}/fzf-${version}-linux_amd64.tar.gz" tar -C "${tmpdir}" -xzf "${tarball}" sudo mv "${tmpdir}/fzf" /usr/local/bin/ rm -rf "${tmpdir}" "${tarball}" append_if_missing "${HOME}/.bashrc" 'eval "$(fzf --bash)"' done_msg "fzf 설치 (${version})" } install_rust() { step "Rust(cargo) 설치 (rustup)" if command -v rustup >/dev/null 2>&1; then info "rustup 이 이미 설치되어 있어 설치를 건너뜁니다." else curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y fi if [[ -f "${HOME}/.cargo/env" ]]; then . "${HOME}/.cargo/env" fi if command -v rustup >/dev/null 2>&1; then rustup self update rustup update stable fi append_if_missing "${HOME}/.bashrc" '. "$HOME/.cargo/env"' done_msg "Rust(cargo) 설치" } install_lsd() { step "lsd 설치 (릴리즈 tarball, latest)" local tag tag="$(latest_tag lsd-rs/lsd)" local tarball="/tmp/lsd-${tag}.tar.gz" local tmpdir tmpdir="$(mktemp -d)" curl -L -o "${tarball}" \ "https://github.com/lsd-rs/lsd/releases/download/${tag}/lsd-${tag}-x86_64-unknown-linux-musl.tar.gz" tar -C "${tmpdir}" -xzf "${tarball}" sudo mv "${tmpdir}"/*/lsd /usr/local/bin/ rm -rf "${tmpdir}" "${tarball}" done_msg "lsd 설치 (${tag})" } install_ripgrep() { step "ripgrep 설치 (릴리즈 tarball, latest)" local tag tag="$(latest_tag BurntSushi/ripgrep)" local tarball="/tmp/ripgrep-${tag}.tar.gz" local tmpdir tmpdir="$(mktemp -d)" curl -L -o "${tarball}" \ "https://github.com/BurntSushi/ripgrep/releases/download/${tag}/ripgrep-${tag}-x86_64-unknown-linux-musl.tar.gz" tar -C "${tmpdir}" -xzf "${tarball}" sudo mv "${tmpdir}"/*/rg /usr/local/bin/ rm -rf "${tmpdir}" "${tarball}" done_msg "ripgrep 설치 (${tag})" } install_mise() { step "mise 설치 (릴리즈 tarball, latest)" local tag tag="$(latest_tag jdx/mise)" local tarball="/tmp/mise-${tag}.tar.gz" local tmpdir tmpdir="$(mktemp -d)" curl -L -o "${tarball}" \ "https://github.com/jdx/mise/releases/download/${tag}/mise-${tag}-linux-x64-musl.tar.gz" tar -C "${tmpdir}" -xzf "${tarball}" sudo mv "${tmpdir}/mise/bin/mise" /usr/local/bin/mise rm -rf "${tmpdir}" "${tarball}" append_if_missing "${HOME}/.bashrc" 'eval "$(mise activate bash)"' done_msg "mise 설치 (${tag})" } install_docker() { step "Docker 설치 (공식 apt 저장소)" sudo apt-get install -y ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc local codename codename="$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")" sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null < "${HOME}/.docker/config.json" { "psFormat": "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" } EOF done_msg "Docker CLI 출력 포맷 설정" } configure_git() { step "Git 전역 설정" git config --global credential.helper store git config --global init.defaultBranch main git config --global core.editor nvim done_msg "Git 전역 설정" } main() { info "서버 초기 세팅을 시작합니다." info "로그 파일: ${LOG_FILE}" detect_env print_env prompt_config require_sudo step "로케일 설정 (${LOCALE})" sudo locale-gen "${LOCALE}" sudo update-locale LANG="${LOCALE}" LC_ALL="${LOCALE}" done_msg "로케일 설정 (${LOCALE})" step "타임존 설정 (${TIMEZONE})" sudo timedatectl set-timezone "${TIMEZONE}" done_msg "타임존 설정 (${TIMEZONE})" step "APT 패키지 목록 업데이트" sudo apt-get update done_msg "APT 패키지 목록 업데이트" step "needrestart 패키지 제거" sudo apt-get remove -y needrestart done_msg "needrestart 패키지 제거" step "설치된 패키지 업그레이드" sudo apt-get upgrade -y done_msg "설치된 패키지 업그레이드" step "기본 패키지 설치 (curl ssh net-tools build-essential pkg-config libssl-dev)" sudo apt-get install -y curl ssh net-tools build-essential pkg-config libssl-dev done_msg "기본 패키지 설치" if [[ "$DO_STARSHIP" == "y" ]]; then install_starship configure_starship fi if [[ "$DO_GIT" == "y" ]]; then configure_git; fi if [[ "$DO_DOCKER" == "y" ]]; then install_docker step "현재 사용자에게 docker 그룹 권한 추가" sudo usermod -aG docker "$USER" info "docker 그룹 권한은 보통 재로그인 후 반영됩니다." info "스크립트 안에서 newgrp docker 는 새 셸을 열어 흐름을 꼬이게 할 수 있어 생략합니다." done_msg "docker 그룹 권한 추가" configure_docker_ps_format fi if [[ "$DO_RUST" == "y" ]]; then install_rust; fi if [[ "$DO_LSD" == "y" ]]; then install_lsd configure_lsd_alias fi if [[ "$DO_NEOVIM" == "y" ]]; then install_neovim; fi if [[ "$DO_YAZI" == "y" ]]; then install_yazi; fi if [[ "$DO_FZF" == "y" ]]; then install_fzf; fi if [[ "$DO_RIPGREP" == "y" ]]; then install_ripgrep; fi if [[ "$DO_MISE" == "y" ]]; then install_mise; fi echo echo "🎉 서버 초기 세팅이 완료되었습니다." echo "📄 로그 파일: ${LOG_FILE}" echo "👉 변경사항(.bashrc alias/함수, PATH, docker 그룹 권한 등)을 적용하려면 다시 로그인하세요." } main "$@"