diff --git a/.gitconfig b/.gitconfig index f4facb1..4255b45 100644 --- a/.gitconfig +++ b/.gitconfig @@ -15,3 +15,6 @@ email = arjun810@gmail.com default = current [alias] serve = daemon --verbose --export-all --base-path=.git --reuseaddr --strict-paths .git/ + +[includeIf "gitdir:~/work/"] + path = ~/.gitconfig.work diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f220d7d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + shellcheck: + 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: Run ShellCheck + run: shellcheck init_dotfiles.sh cleanup_dotfiles.sh || true + + brew-bundle-check: + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Brew bundle check + run: brew bundle check --file osx/Brewfile || true \ No newline at end of file diff --git a/.zshrc b/.zshrc index 4c9d446..c843cec 100644 --- a/.zshrc +++ b/.zshrc @@ -143,3 +143,5 @@ function textme() { source ~/.secrets.env . /opt/homebrew/opt/asdf/asdf.sh + +[ -f ~/.zshrc.local ] && . ~/.zshrc.local diff --git a/README.md b/README.md new file mode 100644 index 0000000..076970c --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +Dotfiles Setup + +Prerequisites +- macOS (tested), zsh +- curl, git, Ruby or system Ruby for running setup.rb + +Bootstrap on macOS +- Run: ruby osx/setup.rb +- The script will: + - Install Homebrew non-interactively and add zsh to /etc/shells safely + - Install Brewfile packages and casks + - Clone or reuse ~/.dotfiles + - Initialize symlinks with backups and XDG config shims + - Install vim-plug and Zim, then install Vim plugins + - Install asdf Ruby, set global version, and install bundler via asdf exec + - Install pipx and ensure PATH + - Optionally install Phoenix generator if Elixir is available + - Print notes for manual steps like SSH keys, iTerm schemes, and font changes + +Manual steps +- Generate SSH keys if missing: ssh-keygen -t ed25519 -C "you@example.com" +- iTerm2: import color schemes from osx/*.itermcolors; set a Powerline/Nerd Font +- Change terminal font to Monaco for Powerline or Meslo Nerd Font + +Updating packages +- Brewfile: edit osx/Brewfile; run brew bundle --file osx/Brewfile +- Vim plugins: update .vimrc and run :PlugUpdate +- Zsh modules: edit .zimrc and run zimfw update + +Per-host configuration +- Add ~/.zshrc.local for host-specific shell settings +- Git can include ~/.gitconfig.work via includeIf + +Dotfile managers (optional) +- For selective deployment or managing across multiple machines consider: + - GNU Stow: create directories per app and stow them into $HOME + - yadm: git-based dotfile manager with templating and bootstrap hooks + - chezmoi: cross-platform manager with encryption support and templates +- This repo currently uses simple symlinks via init_dotfiles.sh; you can migrate gradually by structuring files for stow or adopting chezmoi/yadm. + +Uninstall/cleanup (manual for now) +- Remove symlinks in $HOME +- Restore backups with .bak.TIMESTAMP suffix created by init_dotfiles.sh \ No newline at end of file diff --git a/cleanup_dotfiles.sh b/cleanup_dotfiles.sh new file mode 100644 index 0000000..1225d5f --- /dev/null +++ b/cleanup_dotfiles.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +DOTDIR="${HOME}/.dotfiles" +FILES=(.vimrc .zshrc .zshenv .zlogin .zimrc .gemrc .gitconfig .tool-versions) + +for file in "${FILES[@]}"; do + target="${HOME}/${file}" + if [ -L "$target" ]; then + rm -f "$target" + fi + # restore latest backup if exists + latest_backup=$(ls -1t ${target}.bak.* 2>/dev/null | head -n1 || true) + if [ -n "${latest_backup}" ]; then + mv "${latest_backup}" "$target" + fi +done + +# XDG config shims +XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-${HOME}/.config}" +rm -f "${XDG_CONFIG_HOME}/vim/vimrc" || true +rm -f "${XDG_CONFIG_HOME}/git/config" || true \ No newline at end of file diff --git a/init_dotfiles.sh b/init_dotfiles.sh index ea37e98..34d0127 100755 --- a/init_dotfiles.sh +++ b/init_dotfiles.sh @@ -1,15 +1,37 @@ #! /bin/bash -pushd ~ -ln -s .dotfiles/.vimrc ~/.vimrc -ln -s .dotfiles/.zshrc ~/.zshrc -ln -s .dotfiles/.zshenv ~/.zshenv -ln -s .dotfiles/.zshlogin ~/.zshlogin -ln -s .dotfiles/.zimrc ~/.zimrc -ln -s .dotfiles/.gemrc ~/.gemrc -ln -s .dotfiles/.gitconfig ~/.gitconfig -ln -s .dotfiles/.tool-versions ~/.tool-versions -popd - -git submodule init -git submodule update +set -euo pipefail + +DOTDIR="${HOME}/.dotfiles" +FILES=(.vimrc .zshrc .zshenv .zlogin .zimrc .gemrc .gitconfig .tool-versions) + +for file in "${FILES[@]}"; do + target="${HOME}/${file}" + src="${DOTDIR}/${file}" + if [ -e "$target" ] && [ ! -L "$target" ]; then + mv "$target" "${target}.bak.$(date +%s)" + fi + ln -sfn "$src" "$target" +done + +git submodule update --init --recursive + +# XDG config symlinks where supported +XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-${HOME}/.config}" +mkdir -p "${XDG_CONFIG_HOME}/vim" "${XDG_CONFIG_HOME}/git" + +# vimrc -> ~/.config/vim/vimrc +vim_target="${XDG_CONFIG_HOME}/vim/vimrc" +vim_src="${DOTDIR}/.vimrc" +if [ -e "$vim_target" ] && [ ! -L "$vim_target" ]; then + mv "$vim_target" "${vim_target}.bak.$(date +%s)" +fi +ln -sfn "$vim_src" "$vim_target" + +# git config -> ~/.config/git/config +git_target="${XDG_CONFIG_HOME}/git/config" +git_src="${DOTDIR}/.gitconfig" +if [ -e "$git_target" ] && [ ! -L "$git_target" ]; then + mv "$git_target" "${git_target}.bak.$(date +%s)" +fi +ln -sfn "$git_src" "$git_target" diff --git a/osx/setup.rb b/osx/setup.rb index fb41f72..7cc42a4 100644 --- a/osx/setup.rb +++ b/osx/setup.rb @@ -9,6 +9,21 @@ $notes = [] +# Keep sudo alive for the duration of the setup +begin + if system('command -v sudo >/dev/null 2>&1') + system 'sudo -v' + Thread.new do + loop do + system 'sudo -n true' + sleep 60 + end + end + end +rescue + # ignore +end + def step(name) if $completed.include? name puts "Step '#{name}' already done; skipping." @@ -47,13 +62,13 @@ def clone(repo, destination) def pip(packages, opts={}) packages = [packages].flatten - command = "pip3 install #{packages.join(" ")}" + command = "python3 -m pip install #{packages.join(" ")}" system command end def gem(packages, opts={}) packages = [packages].flatten - command = "`asdf which gem` install #{packages.join(" ")}" + command = "asdf exec gem install #{packages.join(" ")}" system command end @@ -64,30 +79,67 @@ def prompt(message) end step "Install homebrew" do + ENV["NONINTERACTIVE"] = "1" command "/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" end -step "Prompt for ssh keys" do - prompt "Ensure your ssh keys are set up." - File.exists? File.expand_path("~/.ssh") +step "Check ssh keys" do + ssh_dir = File.expand_path("~/.ssh") + have_dir = File.exist?(ssh_dir) + have_key = ["id_ed25519.pub", "id_rsa.pub"].any? { |k| File.exist?(File.join(ssh_dir, k)) } + unless have_dir && have_key + note "SSH keys not found. Generate one with: ssh-keygen -t ed25519 -C \"your_email@example.com\"" + end + true end step "Install dotfiles" do - clone "git@github.com:arjun810/dotfiles", "~/.dotfiles" + dest = File.expand_path("~/.dotfiles") + if File.exist?(dest) + puts "~/.dotfiles already exists; skipping clone." + true + else + clone "git@github.com:arjun810/dotfiles", dest + end end step "Install Homebrew bundle" do + prefix = `brew --prefix 2>/dev/null`.strip + if prefix.nil? || prefix.empty? + prefix = File.directory?("/opt/homebrew") ? "/opt/homebrew" : "/usr/local" + end + ENV["PATH"] = "#{prefix}/bin:#{ENV["PATH"]}" command "brew bundle --file ~/.dotfiles/osx/Brewfile" end +step "Install ruby build dependencies" do + prefix = `brew --prefix 2>/dev/null`.strip + if prefix.nil? || prefix.empty? + prefix = File.directory?("/opt/homebrew") ? "/opt/homebrew" : "/usr/local" + end + ENV["PATH"] = "#{prefix}/bin:#{ENV["PATH"]}" + deps = %w[autoconf bison openssl@3 readline libyaml gmp zlib] + command "brew install #{deps.join(' ')}" +end + step "Install ruby" do - command "asdf plugin add ruby" - command "asdf install ruby latest" + plugins = `asdf plugin list 2>/dev/null`.lines.map { |l| l.strip } + added = true + unless plugins.include?("ruby") + added = command "asdf plugin add ruby" + end + installed = command "asdf install ruby latest" + set_global = command "asdf global ruby latest" + added && installed && set_global end -step "Prompt for vim-plug" do - prompt "Ensure you've installed vim-plug." - File.exists? File.expand_path("~/.vim/autoload/plug.vim") +step "Install vim-plug" do + plug_path = File.expand_path("~/.vim/autoload/plug.vim") + if File.exist?(plug_path) + true + else + command 'curl -fLo ~/.vim/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim' + end end step "Init dotfiles" do @@ -99,27 +151,49 @@ def prompt(message) end step "Add zsh to system shells" do - command "echo /opt/homebrew/bin/zsh | sudo tee -a /etc/shells" + shells_file = "/etc/shells" + prefix = `brew --prefix 2>/dev/null`.strip + if prefix.nil? || prefix.empty? + prefix = File.directory?("/opt/homebrew") ? "/opt/homebrew" : "/usr/local" + end + zsh_path = File.join(prefix, "bin", "zsh") + lines = File.readlines(shells_file).map { |l| l.strip } + if lines.include?(zsh_path) + true + else + command "echo #{zsh_path} | sudo tee -a #{shells_file}" + end end step "Change shell to zsh" do - command "chsh -s /opt/homebrew/bin/zsh" + current = ENV["SHELL"] + prefix = `brew --prefix 2>/dev/null`.strip + if prefix.nil? || prefix.empty? + prefix = File.directory?("/opt/homebrew") ? "/opt/homebrew" : "/usr/local" + end + target = File.join(prefix, "bin", "zsh") + if current == target + true + else + command "chsh -s #{target}" + end end step "Install Monaco" do - note "You should remember to change your terminal font to Monaco for Powerline." - prompt "A prompt will pop up so you can install Monaco. Make sure to hit 'Install Font.'" + note "Change your terminal font to Monaco for Powerline (installed in ~/.dotfiles/osx)." command 'open ~/.dotfiles/osx/"Monaco for Powerline.otf"' end step "Install iTerm2 colorschemes and fonts." do - prompt "Don't forget to add the iTerm colorschemes. They're in ~/.dotfiles/osx." - prompt "Don't forget to set up iTerm's fonts and update it so Powerline works." + note "Import iTerm2 color schemes from ~/.dotfiles/osx and set Powerline-compatible fonts." + true end step "Install zim" do - command "mkdir ~/.zim" - command "wget https://github.com/zimfw/zimfw/releases/latest/download/zimfw.zsh -O ~/.zim/zimfw.zsh" + ok = command "mkdir -p ~/.zim" + ok = ok && command "wget https://github.com/zimfw/zimfw/releases/latest/download/zimfw.zsh -O ~/.zim/zimfw.zsh" + ok = ok && command "zsh -i -c \"~/.zim/zimfw.zsh install\"" + ok end step "Install bundler" do @@ -127,11 +201,43 @@ def prompt(message) end step "Install hex" do - command "mix local.hex" + if system('mix --version >/dev/null 2>&1') + command "mix local.hex --force" + else + note "Elixir/mix not found; install Elixir/Erlang via Homebrew or asdf before running mix commands." + true + end end step "Install phoenix application generator" do - command "mix archive.install hex phx_new" + if system('mix --version >/dev/null 2>&1') + command "mix archive.install hex phx_new --force" + else + note "Skipping Phoenix installer because mix is not available." + true + end +end + +step "Install pipx" do + prefix = `brew --prefix 2>/dev/null`.strip + if prefix.nil? || prefix.empty? + prefix = File.directory?("/opt/homebrew") ? "/opt/homebrew" : "/usr/local" + end + ENV["PATH"] = "#{prefix}/bin:#{ENV["PATH"]}" + ok = command "brew install pipx" + ok = ok && command "pipx ensurepath" + ok +end + +step "Install Nerd Font via Homebrew cask" do + prefix = `brew --prefix 2>/dev/null`.strip + if prefix.nil? || prefix.empty? + prefix = File.directory?("/opt/homebrew") ? "/opt/homebrew" : "/usr/local" + end + ENV["PATH"] = "#{prefix}/bin:#{ENV["PATH"]}" + ok = command "brew tap homebrew/cask-fonts" + ok = ok && command "brew install --cask font-meslo-lg-nerd-font" + ok end # .amethyst has to be done manually since it's osx specific @@ -148,3 +254,5 @@ def prompt(message) $notes.each do |note| puts note end + +puts "Setup complete. Review the notes above for any manual actions."