diff --git a/.chezmoiroot b/.chezmoiroot new file mode 100644 index 0000000..73d74c2 --- /dev/null +++ b/.chezmoiroot @@ -0,0 +1 @@ +chezmoi diff --git a/.github/workflows/shell-quality.yml b/.github/workflows/shell-quality.yml new file mode 100644 index 0000000..a8e38e0 --- /dev/null +++ b/.github/workflows/shell-quality.yml @@ -0,0 +1,32 @@ +name: Shell Quality + +on: + push: + branches: + - main + pull_request: + +jobs: + shell-quality: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install ShellCheck + run: sudo apt-get update && sudo apt-get install -y shellcheck + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + + - name: Install shfmt + run: | + go install mvdan.cc/sh/v3/cmd/shfmt@latest + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + + - name: Lint shell + run: | + chmod +x ./scripts/lint-shell.sh + ./scripts/lint-shell.sh diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 99ca118..0000000 --- a/.gitignore +++ /dev/null @@ -1,38 +0,0 @@ -.cache/ -local/ -*.swp -*.swo -*.env -*.key -*.pem -*.p12 -*.der -*.asc -*.gpg -*.kdb* -*.sqlite -*.db -.ssh/ -.gnupg/ -.aws/ -.azure/ -.kube/ -.docker/ -.keychain/ -.local/share/keyrings/ -.mozilla/ -.config/chromium/ -.config/google-chrome/ -.config/BraveSoftware/ -.config/VSCodium/ -.config/Code/ -.config/discord/ -.config/slack/ -.config/pulse/ -.config/pipewire/ -.config/systemd/ -.config/dconf/ -.config/fontconfig/ -.config/gtk-*/ -.config/xdg-desktop-portal*/ -.config/user-dirs.* diff --git a/README.md b/README.md index 96bf7ed..60aed84 100644 --- a/README.md +++ b/README.md @@ -1 +1,92 @@ -# dotfiles +# Ubuntu 24.04 Developer Bootstrap (WSL2 + native) + +Reproducible, idempotent bootstrap for Ubuntu 24.04 that installs a full developer distro and applies dotfiles via chezmoi. + +## One-liner install + +Replace `` with your GitHub repo URL: + +```bash +git clone ~/.dotfiles && ~/.dotfiles/bootstrap/install.sh +``` + +## WSL2 systemd requirement (mandatory for Podman rootless) + +Podman rootless socket activation uses `systemctl --user`, which requires systemd. If running in WSL2: + +1. Edit `/etc/wsl.conf`: + +``` +[boot] +systemd=true +``` + +2. From Windows, run: + +```powershell +wsl.exe --shutdown +``` + +3. Re-open your WSL distro and rerun the installer. + +Microsoft Learn: https://learn.microsoft.com/windows/wsl/systemd + +## VS Code on WSL (recommended) + +- Install VS Code on Windows (not inside WSL). +- During install, enable "Add to PATH". +- Install the "Remote Development" extension pack (or at minimum the WSL extension). +- From WSL, run: `code .` — this opens the folder in VS Code and installs the VS Code Server in WSL automatically. + +## VS Code on Ubuntu (optional) + +Native Linux VS Code is opt-in. Set the environment variable to install via the official Microsoft apt repo: + +```bash +INSTALL_VSCODE_LINUX=1 ./bootstrap/install.sh +``` + +## Dotfiles with chezmoi + +- Source state lives in `chezmoi/` (enabled via `.chezmoiroot`). +- The installer runs `chezmoi init --apply ` on first run, then `chezmoi apply` on subsequent runs. +- Daily operations: + - `chezmoi diff` + - `chezmoi apply` + - `chezmoi update` + +## Shell quality + +- Lint: `./scripts/lint-shell.sh` +- Format: `./scripts/format-shell.sh` +- Prereqs: + - ShellCheck is installed by the bootstrap (or via `sudo apt-get install -y shellcheck`). + - shfmt via Go: `go install mvdan.cc/sh/v3/cmd/shfmt@latest` + +## What this installs + +- Apt packages: zsh, git, curl, ca-certificates, fzf, ripgrep, fd-find (+ fd symlink), eza, zoxide, podman, podman-compose +- From official sources/releases: GitHub CLI (apt repo), Oh-My-Zsh, Powerlevel10k theme, zsh-autosuggestions, zsh-syntax-highlighting, mise, lazygit, lazydocker, try, Neovim +- Toolchains via mise: Rust (cargo), Go, .NET SDK, Node.js LTS, Bun + +## Usage + +- Run full bootstrap: + ```bash + ./bootstrap/install.sh + ``` +- Run doctor checks only: + ```bash + ./bootstrap/steps/90-doctor.sh + ``` + +## Notes & assumptions + +- Ubuntu 24.04 only. Other distros/versions are rejected by preflight. +- WSL best practice: keep repos inside the Linux filesystem (e.g. `~/src`), not `/mnt/c`. +- Dotfiles are managed by chezmoi; re-running the installer reapplies the repo state. +- After opening a new shell, run `p10k configure` to personalize the Powerlevel10k prompt. +- Neovim and lazygit use GitHub “latest release” assets resolved at install time. +- `zsh-syntax-highlighting` must remain last in the plugins list; this repo enforces that ordering. +- The mise installer URL is assumed to be `https://mise.run` (official). Update it if upstream changes. +- For deterministic toolchains, pin exact versions in `mise/mise.toml`. A `mise.lock` can be generated and committed using mise tooling when needed. diff --git a/bootstrap/install.sh b/bootstrap/install.sh new file mode 100644 index 0000000..819349a --- /dev/null +++ b/bootstrap/install.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +BOOTSTRAP_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +export BOOTSTRAP_ROOT + +# shellcheck source=bootstrap/lib/common.sh +. "${BOOTSTRAP_ROOT}/bootstrap/lib/common.sh" + +log "Starting bootstrap" + +steps=( + "00-preflight.sh" + "10-apt.sh" + "20-zsh-ohmyzsh.sh" + "30-mise.sh" + "40-podman.sh" + "50-tools-github.sh" + "70-vscode.sh" + "60-dotfiles.sh" + "90-doctor.sh" +) + +for step in "${steps[@]}"; do + log "Running ${step}" + bash "${BOOTSTRAP_ROOT}/bootstrap/steps/${step}" + log "Completed ${step}" + echo +done + +log "Bootstrap finished" diff --git a/bootstrap/lib/common.sh b/bootstrap/lib/common.sh new file mode 100644 index 0000000..73a426e --- /dev/null +++ b/bootstrap/lib/common.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +log() { + printf '[INFO] %s\n' "$*" +} + +warn() { + printf '[WARN] %s\n' "$*" >&2 +} + +die() { + printf '[ERROR] %s\n' "$*" >&2 + exit 1 +} + +have_cmd() { + command -v "$1" >/dev/null 2>&1 +} + +need_cmd() { + have_cmd "$1" || die "Missing command: $1" +} + +is_wsl() { + grep -qi microsoft /proc/version 2>/dev/null || [ -n "${WSL_INTEROP:-}" ] || [ -n "${WSL_DISTRO_NAME:-}" ] +} + +is_ubuntu() { + [ -r /etc/os-release ] || return 1 + # shellcheck disable=SC1091 + . /etc/os-release + [ "${ID:-}" = "ubuntu" ] +} + +ubuntu_version_id() { + [ -r /etc/os-release ] || return 1 + # shellcheck disable=SC1091 + . /etc/os-release + printf '%s' "${VERSION_ID:-}" +} + +download_to_tmp() { + local url tmp + url="$1" + tmp="$(mktemp)" + log "Downloading ${url}" + curl -fsSL "${url}" -o "${tmp}" + printf '%s' "${tmp}" +} + +link_file() { + local src dest + src="$1" + dest="$2" + + if [ -L "${dest}" ] && [ "$(readlink "${dest}")" = "${src}" ]; then + log "Link exists: ${dest}" + return 0 + fi + + if [ -e "${dest}" ]; then + warn "Skip existing path: ${dest}" + return 0 + fi + + mkdir -p "$(dirname "${dest}")" + ln -s "${src}" "${dest}" + log "Linked ${dest} -> ${src}" +} diff --git a/bootstrap/steps/00-preflight.sh b/bootstrap/steps/00-preflight.sh new file mode 100644 index 0000000..fd47398 --- /dev/null +++ b/bootstrap/steps/00-preflight.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +BOOTSTRAP_ROOT="${BOOTSTRAP_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +# shellcheck source=bootstrap/lib/common.sh +. "${BOOTSTRAP_ROOT}/bootstrap/lib/common.sh" + +if [ "${EUID:-$(id -u)}" -eq 0 ]; then + die "Run this installer as a regular user (not root)." +fi + +need_cmd sudo + +if ! is_ubuntu; then + die "This installer supports Ubuntu only." +fi + +version_id="$(ubuntu_version_id)" +if [ "${version_id}" != "24.04" ]; then + die "This installer targets Ubuntu 24.04. Detected: ${version_id}." +fi + +if is_wsl; then + if [ ! -d /run/systemd/system ]; then + cat >&2 <<'EOM' +[ERROR] WSL detected, but systemd is not running. +Enable systemd in /etc/wsl.conf: + +[boot] +systemd=true + +Then run from Windows: + wsl.exe --shutdown + +Re-open your WSL distro and rerun the installer. +EOM + exit 1 + fi +fi + +log "Preflight OK" diff --git a/bootstrap/steps/10-apt.sh b/bootstrap/steps/10-apt.sh new file mode 100644 index 0000000..e67f5ff --- /dev/null +++ b/bootstrap/steps/10-apt.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +BOOTSTRAP_ROOT="${BOOTSTRAP_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +# shellcheck source=bootstrap/lib/common.sh +. "${BOOTSTRAP_ROOT}/bootstrap/lib/common.sh" + +packages=( + zsh + git + curl + ca-certificates + fzf + ripgrep + fd-find + eza + zoxide + shellcheck + podman + podman-compose +) + +log "Updating apt metadata" +sudo apt-get update + +log "Installing apt packages" +sudo apt-get install -y "${packages[@]}" + +if have_cmd fdfind && ! have_cmd fd; then + mkdir -p "${HOME}/.local/bin" + ln -sf "$(command -v fdfind)" "${HOME}/.local/bin/fd" + log "Linked fd -> fdfind in ~/.local/bin" +fi diff --git a/bootstrap/steps/20-zsh-ohmyzsh.sh b/bootstrap/steps/20-zsh-ohmyzsh.sh new file mode 100644 index 0000000..3c95aba --- /dev/null +++ b/bootstrap/steps/20-zsh-ohmyzsh.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +BOOTSTRAP_ROOT="${BOOTSTRAP_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +# shellcheck source=bootstrap/lib/common.sh +. "${BOOTSTRAP_ROOT}/bootstrap/lib/common.sh" + +ohmyzsh_dir="${HOME}/.oh-my-zsh" + +if [ ! -d "${ohmyzsh_dir}" ]; then + install_url="https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh" + tmpfile="$(download_to_tmp "${install_url}")" + + RUNZSH=no CHSH=no KEEP_ZSHRC=yes bash "${tmpfile}" + rm -f "${tmpfile}" + + log "Oh-My-Zsh installed" +else + log "Oh-My-Zsh already installed" +fi + +ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}" +mkdir -p "${ZSH_CUSTOM}/themes" "${ZSH_CUSTOM}/plugins" + +install_or_update_repo() { + local repo_dir repo_url clone_depth + repo_dir="$1" + repo_url="$2" + clone_depth="$3" + + if [ -d "${repo_dir}/.git" ]; then + log "Updating ${repo_dir}" + git -C "${repo_dir}" pull --ff-only + return 0 + fi + + if [ -e "${repo_dir}" ]; then + warn "${repo_dir} exists and is not a git repo; skipping" + return 0 + fi + + log "Cloning ${repo_url}" + if [ -n "${clone_depth}" ]; then + git clone --depth=1 "${repo_url}" "${repo_dir}" + else + git clone "${repo_url}" "${repo_dir}" + fi +} + +install_or_update_repo \ + "${ZSH_CUSTOM}/themes/powerlevel10k" \ + "https://github.com/romkatv/powerlevel10k.git" \ + "depth" + +install_or_update_repo \ + "${ZSH_CUSTOM}/plugins/zsh-autosuggestions" \ + "https://github.com/zsh-users/zsh-autosuggestions" \ + "" + +install_or_update_repo \ + "${ZSH_CUSTOM}/plugins/zsh-syntax-highlighting" \ + "https://github.com/zsh-users/zsh-syntax-highlighting.git" \ + "" diff --git a/bootstrap/steps/30-mise.sh b/bootstrap/steps/30-mise.sh new file mode 100644 index 0000000..7d3bc3e --- /dev/null +++ b/bootstrap/steps/30-mise.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +BOOTSTRAP_ROOT="${BOOTSTRAP_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +# shellcheck source=bootstrap/lib/common.sh +. "${BOOTSTRAP_ROOT}/bootstrap/lib/common.sh" + +mise_url="https://mise.run" + +if ! have_cmd mise; then + tmpfile="$(download_to_tmp "${mise_url}")" + bash "${tmpfile}" + rm -f "${tmpfile}" +fi + +if ! have_cmd mise && [ -x "${HOME}/.local/bin/mise" ]; then + export PATH="${HOME}/.local/bin:${PATH}" +fi + +if ! have_cmd mise; then + die "mise installation failed" +fi + +config_src="${BOOTSTRAP_ROOT}/mise/mise.toml" +config_dest="${HOME}/.config/mise/config.toml" +link_file "${config_src}" "${config_dest}" + +log "Installing mise toolchains" +MISE_CONFIG_FILE="${config_src}" mise install diff --git a/bootstrap/steps/40-podman.sh b/bootstrap/steps/40-podman.sh new file mode 100644 index 0000000..e2762fc --- /dev/null +++ b/bootstrap/steps/40-podman.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +BOOTSTRAP_ROOT="${BOOTSTRAP_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +# shellcheck source=bootstrap/lib/common.sh +. "${BOOTSTRAP_ROOT}/bootstrap/lib/common.sh" + +need_cmd podman +need_cmd systemctl + +missing_deps=() + +if ! have_cmd newuidmap || ! have_cmd newgidmap; then + missing_deps+=(uidmap) +fi + +if ! have_cmd slirp4netns; then + missing_deps+=(slirp4netns) +fi + +if ! have_cmd fuse-overlayfs; then + missing_deps+=(fuse-overlayfs) +fi + +if [ "${#missing_deps[@]}" -gt 0 ]; then + log "Installing Podman rootless dependencies: ${missing_deps[*]}" + sudo apt-get install -y "${missing_deps[@]}" +fi + +log "Enabling Podman user socket" +if ! systemctl --user enable --now podman.socket; then + die "Failed to enable podman.socket (systemd user session required)." +fi + +log "Podman rootless socket enabled" diff --git a/bootstrap/steps/50-tools-github.sh b/bootstrap/steps/50-tools-github.sh new file mode 100644 index 0000000..7df1440 --- /dev/null +++ b/bootstrap/steps/50-tools-github.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +BOOTSTRAP_ROOT="${BOOTSTRAP_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +# shellcheck source=bootstrap/lib/common.sh +. "${BOOTSTRAP_ROOT}/bootstrap/lib/common.sh" + +need_cmd curl +need_cmd tar + +arch="$(uname -m)" +case "${arch}" in + x86_64) + lazygit_arch="x86_64" + nvim_arch="x86_64" + ;; + aarch64 | arm64) + lazygit_arch="arm64" + nvim_arch="arm64" + ;; + *) + die "Unsupported architecture: ${arch}" + ;; +esac + +install_gh() { + if have_cmd gh; then + log "gh already installed" + return 0 + fi + + log "Setting up GitHub CLI apt repository" + sudo mkdir -p /usr/share/keyrings + + if [ ! -f /usr/share/keyrings/githubcli-archive-keyring.gpg ]; then + key_url="https://cli.github.com/packages/githubcli-archive-keyring.gpg" + tmpfile="$(download_to_tmp "${key_url}")" + sudo install -m 0644 "${tmpfile}" /usr/share/keyrings/githubcli-archive-keyring.gpg + rm -f "${tmpfile}" + fi + + list_file="/etc/apt/sources.list.d/github-cli.list" + repo_line="deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" + + if [ ! -f "${list_file}" ] || ! grep -q "cli.github.com/packages" "${list_file}"; then + echo "${repo_line}" | sudo tee "${list_file}" >/dev/null + fi + + sudo apt-get update + sudo apt-get install -y gh +} + +latest_github_tag() { + local repo tag + repo="$1" + tag="$(curl -fsSLI "https://github.com/${repo}/releases/latest" | tr -d '\r' | awk -F'/' '/^location:/I {print $NF}')" + if [ -z "${tag}" ]; then + return 1 + fi + printf '%s' "${tag}" +} + +install_lazygit() { + if have_cmd lazygit; then + log "lazygit already installed" + return 0 + fi + + tag="$(latest_github_tag "jesseduffield/lazygit")" || die "Unable to resolve lazygit release tag" + if [ -z "${tag}" ]; then + die "Unable to resolve lazygit release tag" + fi + version="${tag#v}" + asset="lazygit_${version}_Linux_${lazygit_arch}.tar.gz" + url="https://github.com/jesseduffield/lazygit/releases/download/${tag}/${asset}" + + tmpfile="$(download_to_tmp "${url}")" + tmpdir="$(mktemp -d)" + tar -xzf "${tmpfile}" -C "${tmpdir}" + mkdir -p "${HOME}/.local/bin" + install -m 0755 "${tmpdir}/lazygit" "${HOME}/.local/bin/lazygit" + rm -rf "${tmpfile}" "${tmpdir}" +} + +install_lazydocker() { + if have_cmd lazydocker; then + log "lazydocker already installed" + return 0 + fi + + script_url="https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh" + tmpfile="$(download_to_tmp "${script_url}")" + bash "${tmpfile}" + rm -f "${tmpfile}" +} + +install_try() { + if [ -x "${HOME}/.local/bin/try" ]; then + log "try already installed" + return 0 + fi + + script_url="https://raw.githubusercontent.com/binpash/try/master/try" + tmpfile="$(download_to_tmp "${script_url}")" + mkdir -p "${HOME}/.local/bin" + install -m 0755 "${tmpfile}" "${HOME}/.local/bin/try" + rm -f "${tmpfile}" +} + +resolve_nvim_url() { + local base + base="https://github.com/neovim/neovim/releases/latest/download" + + if [ "${nvim_arch}" = "x86_64" ]; then + candidates=( + "nvim-linux-x86_64.tar.gz" + "nvim-linux64.tar.gz" + "nvim-linux-x86_64.appimage" + "nvim.appimage" + ) + else + candidates=( + "nvim-linux-arm64.tar.gz" + "nvim-linux-aarch64.tar.gz" + ) + fi + + for asset in "${candidates[@]}"; do + url="${base}/${asset}" + if curl -fsSLI "${url}" >/dev/null 2>&1; then + printf '%s' "${url}" + return 0 + fi + done + + return 1 +} + +install_neovim() { + if have_cmd nvim; then + log "neovim already installed" + return 0 + fi + + url="$(resolve_nvim_url)" || die "Unable to resolve Neovim release asset" + tmpfile="$(download_to_tmp "${url}")" + mkdir -p "${HOME}/.local/bin" + + case "${url}" in + *.appimage) + install -m 0755 "${tmpfile}" "${HOME}/.local/bin/nvim" + rm -f "${tmpfile}" + ;; + *.tar.gz) + tmpdir="$(mktemp -d)" + tar -xzf "${tmpfile}" -C "${tmpdir}" + nvim_path="$(find "${tmpdir}" -type f -path '*/bin/nvim' | head -n 1)" + if [ -z "${nvim_path}" ]; then + rm -rf "${tmpfile}" "${tmpdir}" + die "Neovim binary not found in release archive" + fi + install -m 0755 "${nvim_path}" "${HOME}/.local/bin/nvim" + rm -rf "${tmpfile}" "${tmpdir}" + ;; + *) + rm -f "${tmpfile}" + die "Unsupported Neovim asset type" + ;; + esac +} + +install_gh +install_lazygit +install_lazydocker +install_try +install_neovim diff --git a/bootstrap/steps/60-dotfiles.sh b/bootstrap/steps/60-dotfiles.sh new file mode 100644 index 0000000..18291c0 --- /dev/null +++ b/bootstrap/steps/60-dotfiles.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +BOOTSTRAP_ROOT="${BOOTSTRAP_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +# shellcheck source=bootstrap/lib/common.sh +. "${BOOTSTRAP_ROOT}/bootstrap/lib/common.sh" + +if ! have_cmd chezmoi; then + install_url="https://get.chezmoi.io" + tmpfile="$(download_to_tmp "${install_url}")" + mkdir -p "${HOME}/.local/bin" + bash "${tmpfile}" -- -b "${HOME}/.local/bin" + rm -f "${tmpfile}" +fi + +export PATH="${HOME}/.local/bin:${PATH}" +if ! have_cmd chezmoi; then + die "chezmoi installation failed" +fi + +nvim_dir="${HOME}/.config/nvim" +if [ ! -d "${nvim_dir}" ]; then + log "Cloning AstroNvim" + git clone --depth 1 https://github.com/AstroNvim/AstroNvim "${nvim_dir}" +else + if [ ! -d "${nvim_dir}/.git" ]; then + warn "${nvim_dir} exists and is not a git repo; skipping AstroNvim clone" + fi +fi + +chezmoi_state_dir="${HOME}/.local/share/chezmoi" +if [ -d "${chezmoi_state_dir}" ]; then + log "Applying chezmoi dotfiles" + chezmoi apply +else + log "Initializing chezmoi" + chezmoi init --apply "${BOOTSTRAP_ROOT}" +fi diff --git a/bootstrap/steps/70-vscode.sh b/bootstrap/steps/70-vscode.sh new file mode 100644 index 0000000..47866be --- /dev/null +++ b/bootstrap/steps/70-vscode.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +BOOTSTRAP_ROOT="${BOOTSTRAP_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +# shellcheck source=bootstrap/lib/common.sh +. "${BOOTSTRAP_ROOT}/bootstrap/lib/common.sh" + +if is_wsl; then + log "WSL detected; ensuring VS Code Server dependencies" + sudo apt-get update + sudo apt-get install -y wget ca-certificates + log "WSL VS Code Server dependencies installed" + exit 0 +fi + +if [ "${INSTALL_VSCODE_LINUX:-0}" != "1" ]; then + log "INSTALL_VSCODE_LINUX not set; skipping VS Code install" + exit 0 +fi + +need_cmd sudo + +log "Installing VS Code (native Linux) via Microsoft apt repo" +sudo apt-get update +sudo apt-get install -y wget gpg ca-certificates + +key_url="https://packages.microsoft.com/keys/microsoft.asc" +tmpfile="$(download_to_tmp "${key_url}")" +tmpgpg="${tmpfile}.gpg" + +gpg --dearmor -o "${tmpgpg}" "${tmpfile}" +sudo install -d -m 0755 /etc/apt/keyrings +sudo install -m 0644 "${tmpgpg}" /etc/apt/keyrings/microsoft.gpg +rm -f "${tmpfile}" "${tmpgpg}" + +arch="$(dpkg --print-architecture)" +sudo tee /etc/apt/sources.list.d/vscode.sources >/dev/null </dev/null 2>&1; then + rootless="$(podman info --format '{{.Host.Security.Rootless}}')" + log "Podman rootless: ${rootless}" + else + warn "Podman info unavailable" + fi +fi + +if is_wsl; then + if have_cmd code; then + log "OK: code (WSL remote)" + else + warn "VS Code not detected in WSL" + cat >&2 <<'EOM' +Install VS Code on Windows (not inside WSL). +During install, enable "Add to PATH". +Install the "Remote Development" extension pack (or at minimum the WSL extension). +Then in WSL run: code . +This will download/install the VS Code Server automatically. +EOM + fi +fi diff --git a/chezmoi/dot_config/lazydocker/config.yml b/chezmoi/dot_config/lazydocker/config.yml new file mode 100644 index 0000000..b8e86dc --- /dev/null +++ b/chezmoi/dot_config/lazydocker/config.yml @@ -0,0 +1 @@ +# Lazydocker uses DOCKER_HOST from the shell environment (Podman socket). diff --git a/chezmoi/dot_config/nvim/lua/user/init.lua b/chezmoi/dot_config/nvim/lua/user/init.lua new file mode 100644 index 0000000..6846aa2 --- /dev/null +++ b/chezmoi/dot_config/nvim/lua/user/init.lua @@ -0,0 +1,3 @@ +return { + -- Minimal user layer for AstroNvim. +} diff --git a/config/zsh/.oh-my-zsh/custom/aliases.zsh b/chezmoi/dot_config/zsh/aliases.zsh similarity index 100% rename from config/zsh/.oh-my-zsh/custom/aliases.zsh rename to chezmoi/dot_config/zsh/aliases.zsh diff --git a/chezmoi/dot_gitconfig b/chezmoi/dot_gitconfig new file mode 100644 index 0000000..7c5e187 --- /dev/null +++ b/chezmoi/dot_gitconfig @@ -0,0 +1,2 @@ +[color] + ui = auto diff --git a/chezmoi/dot_zshenv b/chezmoi/dot_zshenv new file mode 100644 index 0000000..d59f7bb --- /dev/null +++ b/chezmoi/dot_zshenv @@ -0,0 +1,8 @@ +export PATH="$HOME/.local/bin:$PATH" + +export DOCKER_HOST="unix://${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/podman/podman.sock" +export PODMAN_COMPOSE_PROVIDER="podman-compose" + +if command -v mise >/dev/null 2>&1; then + eval "$(mise activate zsh)" +fi diff --git a/chezmoi/dot_zshrc b/chezmoi/dot_zshrc new file mode 100644 index 0000000..7220b13 --- /dev/null +++ b/chezmoi/dot_zshrc @@ -0,0 +1,20 @@ +export ZSH="$HOME/.oh-my-zsh" + +ZSH_THEME="powerlevel10k/powerlevel10k" +plugins=(git zsh-autosuggestions zsh-syntax-highlighting) + +if [ -f "$ZSH/oh-my-zsh.sh" ]; then + source "$ZSH/oh-my-zsh.sh" +fi + +if command -v zoxide >/dev/null 2>&1; then + eval "$(zoxide init zsh)" +fi + +if command -v fdfind >/dev/null 2>&1 && ! command -v fd >/dev/null 2>&1; then + alias fd='fdfind' +fi + +if [ -f "$HOME/.config/zsh/aliases.zsh" ]; then + source "$HOME/.config/zsh/aliases.zsh" +fi diff --git a/config/zsh/.zshrc b/config/zsh/.zshrc deleted file mode 100644 index af036b6..0000000 --- a/config/zsh/.zshrc +++ /dev/null @@ -1,119 +0,0 @@ -# Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc. -# Initialization code that may require console input (password prompts, [y/n] -# confirmations, etc.) must go above this block; everything else may go below. -if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then - source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" -fi - -# If you come from bash you might have to change your $PATH. -# export PATH=$HOME/bin:$HOME/.local/bin:/usr/local/bin:$PATH - -# Path to your Oh My Zsh installation. -export ZSH="$HOME/.oh-my-zsh" - -# Set name of the theme to load --- if set to "random", it will -# load a random theme each time Oh My Zsh is loaded, in which case, -# to know which specific one was loaded, run: echo $RANDOM_THEME -# See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes -ZSH_THEME="powerlevel10k/powerlevel10k" - -# Set list of themes to pick from when loading at random -# Setting this variable when ZSH_THEME=random will cause zsh to load -# a theme from this variable instead of looking in $ZSH/themes/ -# If set to an empty array, this variable will have no effect. -# ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" ) - -# Uncomment the following line to use case-sensitive completion. -# CASE_SENSITIVE="true" - -# Uncomment the following line to use hyphen-insensitive completion. -# Case-sensitive completion must be off. _ and - will be interchangeable. -# HYPHEN_INSENSITIVE="true" - -# Uncomment one of the following lines to change the auto-update behavior -# zstyle ':omz:update' mode disabled # disable automatic updates -# zstyle ':omz:update' mode auto # update automatically without asking -# zstyle ':omz:update' mode reminder # just remind me to update when it's time - -# Uncomment the following line to change how often to auto-update (in days). -# zstyle ':omz:update' frequency 13 - -# Uncomment the following line if pasting URLs and other text is messed up. -# DISABLE_MAGIC_FUNCTIONS="true" - -# Uncomment the following line to disable colors in ls. -# DISABLE_LS_COLORS="true" - -# Uncomment the following line to disable auto-setting terminal title. -# DISABLE_AUTO_TITLE="true" - -# Uncomment the following line to enable command auto-correction. -# ENABLE_CORRECTION="true" - -# Uncomment the following line to display red dots whilst waiting for completion. -# You can also set it to another string to have that shown instead of the default red dots. -# e.g. COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f" -# Caution: this setting can cause issues with multiline prompts in zsh < 5.7.1 (see #5765) -# COMPLETION_WAITING_DOTS="true" - -# Uncomment the following line if you want to disable marking untracked files -# under VCS as dirty. This makes repository status check for large repositories -# much, much faster. -# DISABLE_UNTRACKED_FILES_DIRTY="true" - -# Uncomment the following line if you want to change the command execution time -# stamp shown in the history command output. -# You can set one of the optional three formats: -# "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd" -# or set a custom format using the strftime function format specifications, -# see 'man strftime' for details. -# HIST_STAMPS="mm/dd/yyyy" - -# Would you like to use another custom folder than $ZSH/custom? -# ZSH_CUSTOM=/path/to/new-custom-folder - -# Which plugins would you like to load? -# Standard plugins can be found in $ZSH/plugins/ -# Custom plugins may be added to $ZSH_CUSTOM/plugins/ -# Example format: plugins=(rails git textmate ruby lighthouse) -# Add wisely, as too many plugins slow down shell startup. -plugins=(git zsh-autosuggestions zsh-syntax-highlighting) - -source $ZSH/oh-my-zsh.sh - -# User configuration - -# export MANPATH="/usr/local/man:$MANPATH" - -# You may need to manually set your language environment -# export LANG=en_US.UTF-8 - -# Preferred editor for local and remote sessions -# if [[ -n $SSH_CONNECTION ]]; then -# export EDITOR='vim' -# else -# export EDITOR='nvim' -# fi - -# Compilation flags -# export ARCHFLAGS="-arch $(uname -m)" - -# Set personal aliases, overriding those provided by Oh My Zsh libs, -# plugins, and themes. Aliases can be placed here, though Oh My Zsh -# users are encouraged to define aliases within a top-level file in -# the $ZSH_CUSTOM folder, with .zsh extension. Examples: -# - $ZSH_CUSTOM/aliases.zsh -# - $ZSH_CUSTOM/macos.zsh -# For a full list of active aliases, run `alias`. -# -# Example aliases -# alias zshconfig="mate ~/.zshrc" -# alias ohmyzsh="mate ~/.oh-my-zsh" - -# To customize prompt, run `p10k configure` or edit ~/.p10k.zsh. -[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh -source /home/w0rldx/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh - -export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm -[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion diff --git a/dotfiles/git/.gitconfig b/dotfiles/git/.gitconfig new file mode 100644 index 0000000..7c5e187 --- /dev/null +++ b/dotfiles/git/.gitconfig @@ -0,0 +1,2 @@ +[color] + ui = auto diff --git a/dotfiles/lazydocker/config.yml b/dotfiles/lazydocker/config.yml new file mode 100644 index 0000000..b8e86dc --- /dev/null +++ b/dotfiles/lazydocker/config.yml @@ -0,0 +1 @@ +# Lazydocker uses DOCKER_HOST from the shell environment (Podman socket). diff --git a/dotfiles/nvim/lua/user/init.lua b/dotfiles/nvim/lua/user/init.lua new file mode 100644 index 0000000..6846aa2 --- /dev/null +++ b/dotfiles/nvim/lua/user/init.lua @@ -0,0 +1,3 @@ +return { + -- Minimal user layer for AstroNvim. +} diff --git a/dotfiles/zsh/.zshenv b/dotfiles/zsh/.zshenv new file mode 100644 index 0000000..d59f7bb --- /dev/null +++ b/dotfiles/zsh/.zshenv @@ -0,0 +1,8 @@ +export PATH="$HOME/.local/bin:$PATH" + +export DOCKER_HOST="unix://${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/podman/podman.sock" +export PODMAN_COMPOSE_PROVIDER="podman-compose" + +if command -v mise >/dev/null 2>&1; then + eval "$(mise activate zsh)" +fi diff --git a/dotfiles/zsh/.zshrc b/dotfiles/zsh/.zshrc new file mode 100644 index 0000000..7220b13 --- /dev/null +++ b/dotfiles/zsh/.zshrc @@ -0,0 +1,20 @@ +export ZSH="$HOME/.oh-my-zsh" + +ZSH_THEME="powerlevel10k/powerlevel10k" +plugins=(git zsh-autosuggestions zsh-syntax-highlighting) + +if [ -f "$ZSH/oh-my-zsh.sh" ]; then + source "$ZSH/oh-my-zsh.sh" +fi + +if command -v zoxide >/dev/null 2>&1; then + eval "$(zoxide init zsh)" +fi + +if command -v fdfind >/dev/null 2>&1 && ! command -v fd >/dev/null 2>&1; then + alias fd='fdfind' +fi + +if [ -f "$HOME/.config/zsh/aliases.zsh" ]; then + source "$HOME/.config/zsh/aliases.zsh" +fi diff --git a/dotfiles/zsh/aliases.zsh b/dotfiles/zsh/aliases.zsh new file mode 100644 index 0000000..04fcb56 --- /dev/null +++ b/dotfiles/zsh/aliases.zsh @@ -0,0 +1,8 @@ +# eza statt ls +alias ls='eza --icons=auto --group-directories-first' +alias ll='eza -lh --icons=auto --group-directories-first' +alias la='eza -lha --icons=auto --group-directories-first' +alias l='eza -lh --icons=auto --group-directories-first' + +# optional: Baumansicht +alias lt='eza --tree --level=2 --icons=auto' diff --git a/mise/mise.toml b/mise/mise.toml new file mode 100644 index 0000000..0e495c0 --- /dev/null +++ b/mise/mise.toml @@ -0,0 +1,6 @@ +[tools] +node = "lts" +go = "latest" +rust = "latest" +dotnet = "latest" +bun = "latest" diff --git a/scripts/format-shell.sh b/scripts/format-shell.sh new file mode 100644 index 0000000..6fa0e4c --- /dev/null +++ b/scripts/format-shell.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +if ! command -v shfmt >/dev/null 2>&1; then + echo "shfmt not found. Install with: go install mvdan.cc/sh/v3/cmd/shfmt@latest" >&2 + exit 1 +fi + +files=() +if command -v rg >/dev/null 2>&1; then + mapfile -t files < <(rg --files -g '*.sh' bootstrap scripts) +else + mapfile -t files < <(find bootstrap scripts -type f -name '*.sh') +fi + +if [ "${#files[@]}" -eq 0 ]; then + echo "No shell scripts found." + exit 0 +fi + +shfmt -w -i 2 -ci "${files[@]}" diff --git a/scripts/lint-shell.sh b/scripts/lint-shell.sh new file mode 100644 index 0000000..f9306c2 --- /dev/null +++ b/scripts/lint-shell.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +if ! command -v shellcheck >/dev/null 2>&1; then + echo "shellcheck not found. Install via apt (e.g. sudo apt-get install -y shellcheck)." >&2 + exit 1 +fi + +if ! command -v shfmt >/dev/null 2>&1; then + echo "shfmt not found. Install with: go install mvdan.cc/sh/v3/cmd/shfmt@latest" >&2 + exit 1 +fi + +files=() +if command -v rg >/dev/null 2>&1; then + mapfile -t files < <(rg --files -g '*.sh' bootstrap scripts) +else + mapfile -t files < <(find bootstrap scripts -type f -name '*.sh') +fi + +if [ "${#files[@]}" -eq 0 ]; then + echo "No shell scripts found." + exit 0 +fi + +shellcheck "${files[@]}" +shfmt -d -i 2 -ci "${files[@]}"