commit ac7e575957897c119529d56b2914b1648b74713a Author: Travis Herbranson Date: Thu Apr 23 12:15:26 2026 -0400 Initial scaffold: cli_modern Ansible role for Fish + Starship diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ecbd142 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.retry +*.pyc +__pycache__/ +.vagrant/ +*.log +.env +vault_password diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1448b5b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,57 @@ +# CLI Standardization + +Ansible project to deploy a modern CLI environment (Fish + Starship + modern coreutils) +to Arch and Ubuntu/Debian hosts. + +## What it installs + +- **Fish shell** 4.x (PPA on Ubuntu, pacman on Arch) +- **Starship** prompt (official binary installer) +- **fzf** — fuzzy finder +- **zoxide** — smarter cd +- **bat** — cat with syntax highlighting +- **eza** — modern ls +- **fd** — modern find +- **ripgrep** — fast grep +- **Fisher** plugin manager + plugins + +## Dev workflow + +```bash +# Lint +ansible-lint playbook.yml + +# Syntax check +ansible-playbook playbook.yml --syntax-check + +# Dry run +ansible-playbook playbook.yml -i inventory.yml --check --diff + +# Deploy +ansible-playbook playbook.yml -i inventory.yml +ansible-playbook playbook.yml -i inventory.yml --limit manjaro-dev +``` + +## Role structure + +``` +roles/cli_modern/ +├── defaults/main.yml # All configurable variables +├── tasks/ +│ ├── main.yml # Task dispatcher +│ ├── packages-archlinux.yml # Arch/Manjaro packages +│ ├── packages-debian.yml # Ubuntu/Debian packages + Fish PPA +│ ├── starship.yml # Starship binary install +│ ├── fisher.yml # Fisher + plugin sync +│ ├── config.yml # Deploy config templates +│ └── default-shell.yml # Set Fish as login shell +└── templates/ + ├── config.fish.j2 # Fish shell config + ├── starship.toml.j2 # Starship prompt config + └── fish_plugins.j2 # Fisher plugin list +``` + +## Configuration + +All options are in `roles/cli_modern/defaults/main.yml`. Override per-host +in `host_vars/` or per-group in `group_vars/`. diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..3b9ab94 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,9 @@ +[defaults] +inventory = inventory.yml +roles_path = roles +host_key_checking = false +retry_files_enabled = false + +[privilege_escalation] +become = true +become_method = sudo diff --git a/inventory.yml b/inventory.yml new file mode 100644 index 0000000..967b285 --- /dev/null +++ b/inventory.yml @@ -0,0 +1,28 @@ +--- +# CLI Standardization inventory +# Add target hosts here. Group by OS family for role routing. +# +# Example: +# +# all: +# children: +# arch: +# hosts: +# manjaro-dev: +# ansible_host: 192.168.1.100 +# ansible_user: herby +# debian: +# hosts: +# proxmox-01: +# ansible_host: 192.168.1.10 +# ansible_user: root +# ubuntu-nas: +# ansible_host: 192.168.1.20 +# ansible_user: herby + +all: + children: + arch: + hosts: {} + debian: + hosts: {} diff --git a/playbook.yml b/playbook.yml new file mode 100644 index 0000000..ee9230e --- /dev/null +++ b/playbook.yml @@ -0,0 +1,15 @@ +--- +# CLI Standardization — deploy modern shell tooling to target hosts. +# +# Usage: +# ansible-playbook playbook.yml -i inventory.yml +# ansible-playbook playbook.yml -i inventory.yml --limit proxmox +# ansible-playbook playbook.yml -i inventory.yml --tags cli + +- name: Deploy modern CLI environment + hosts: all + gather_facts: true + tags: [cli] + + roles: + - role: cli_modern diff --git a/roles/cli_modern/defaults/main.yml b/roles/cli_modern/defaults/main.yml new file mode 100644 index 0000000..f52729a --- /dev/null +++ b/roles/cli_modern/defaults/main.yml @@ -0,0 +1,53 @@ +--- +# cli_modern role defaults + +# User to configure (shell, dotfiles, etc.) +cli_modern_user: "{{ ansible_user }}" +cli_modern_user_group: "{{ ansible_user }}" +cli_modern_user_home: "/home/{{ cli_modern_user }}" + +# Set Fish as the default login shell +cli_modern_set_default_shell: true + +# Fish shell +cli_modern_install_fish: true +# Minimum Fish version required (Ubuntu PPA added if system package is older) +cli_modern_fish_min_version: "4.0" + +# Starship prompt +cli_modern_install_starship: true +# Install method: "binary" uses the official install script, "package" uses system repos +cli_modern_starship_install_method: "binary" + +# fzf — fuzzy finder +cli_modern_install_fzf: true + +# zoxide — smarter cd +cli_modern_install_zoxide: true + +# bat — cat with syntax highlighting +cli_modern_install_bat: true + +# eza — modern ls replacement +cli_modern_install_eza: true + +# fd — modern find replacement +cli_modern_install_fd: true + +# ripgrep — fast grep +cli_modern_install_ripgrep: true + +# Fisher plugin manager — auto-installed on first Fish launch +cli_modern_install_fisher: true + +# Fisher plugins to install +cli_modern_fisher_plugins: + - "jorgebucaran/fisher" + - "PatrickF1/fzf.fish" + - "jethrokuan/z" + - "meaningful-ooo/sponge" + - "jorgebucaran/autopair.fish" + +# Deploy config templates +cli_modern_deploy_fish_config: true +cli_modern_deploy_starship_config: true diff --git a/roles/cli_modern/tasks/config.yml b/roles/cli_modern/tasks/config.yml new file mode 100644 index 0000000..869bef9 --- /dev/null +++ b/roles/cli_modern/tasks/config.yml @@ -0,0 +1,39 @@ +--- +# Deploy Fish and Starship configuration files. + +- name: Ensure Fish config directory exists + ansible.builtin.file: + path: "{{ cli_modern_user_home }}/.config/fish/conf.d" + state: directory + owner: "{{ cli_modern_user }}" + group: "{{ cli_modern_user_group }}" + mode: "0755" + +- name: Ensure Starship config directory exists + ansible.builtin.file: + path: "{{ cli_modern_user_home }}/.config" + state: directory + owner: "{{ cli_modern_user }}" + group: "{{ cli_modern_user_group }}" + mode: "0755" + when: cli_modern_deploy_starship_config + +- name: Deploy Fish config + ansible.builtin.template: + src: config.fish.j2 + dest: "{{ cli_modern_user_home }}/.config/fish/config.fish" + owner: "{{ cli_modern_user }}" + group: "{{ cli_modern_user_group }}" + mode: "0644" + backup: true + when: cli_modern_deploy_fish_config + +- name: Deploy Starship config + ansible.builtin.template: + src: starship.toml.j2 + dest: "{{ cli_modern_user_home }}/.config/starship.toml" + owner: "{{ cli_modern_user }}" + group: "{{ cli_modern_user_group }}" + mode: "0644" + backup: true + when: cli_modern_deploy_starship_config diff --git a/roles/cli_modern/tasks/default-shell.yml b/roles/cli_modern/tasks/default-shell.yml new file mode 100644 index 0000000..f3f5057 --- /dev/null +++ b/roles/cli_modern/tasks/default-shell.yml @@ -0,0 +1,20 @@ +--- +# Set Fish as the default login shell for the target user. + +- name: Get Fish binary path + ansible.builtin.command: which fish + register: cli_modern_fish_path + changed_when: false + +- name: Ensure Fish is in /etc/shells + become: true + ansible.builtin.lineinfile: + path: /etc/shells + line: "{{ cli_modern_fish_path.stdout }}" + state: present + +- name: Set Fish as default shell for {{ cli_modern_user }} + become: true + ansible.builtin.user: + name: "{{ cli_modern_user }}" + shell: "{{ cli_modern_fish_path.stdout }}" diff --git a/roles/cli_modern/tasks/fisher.yml b/roles/cli_modern/tasks/fisher.yml new file mode 100644 index 0000000..f778678 --- /dev/null +++ b/roles/cli_modern/tasks/fisher.yml @@ -0,0 +1,46 @@ +--- +# Install Fisher plugin manager and configured plugins. +# Fisher bootstraps itself on first run — we just need the fish_plugins file. + +- name: Ensure Fish config directory exists + ansible.builtin.file: + path: "{{ cli_modern_user_home }}/.config/fish" + state: directory + owner: "{{ cli_modern_user }}" + group: "{{ cli_modern_user_group }}" + mode: "0755" + +- name: Ensure Fish functions directory exists + ansible.builtin.file: + path: "{{ cli_modern_user_home }}/.config/fish/functions" + state: directory + owner: "{{ cli_modern_user }}" + group: "{{ cli_modern_user_group }}" + mode: "0755" + +- name: Download Fisher + ansible.builtin.get_url: + url: https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish + dest: "{{ cli_modern_user_home }}/.config/fish/functions/fisher.fish" + owner: "{{ cli_modern_user }}" + group: "{{ cli_modern_user_group }}" + mode: "0644" + +- name: Deploy fish_plugins list + ansible.builtin.template: + src: fish_plugins.j2 + dest: "{{ cli_modern_user_home }}/.config/fish/fish_plugins" + owner: "{{ cli_modern_user }}" + group: "{{ cli_modern_user_group }}" + mode: "0644" + +- name: Install Fisher plugins + become: true + become_user: "{{ cli_modern_user }}" + ansible.builtin.command: > + fish -c "fisher update" + args: + chdir: "{{ cli_modern_user_home }}" + register: cli_modern_fisher_install + changed_when: "'Installing' in cli_modern_fisher_install.stdout or 'Updating' in cli_modern_fisher_install.stdout" + failed_when: false diff --git a/roles/cli_modern/tasks/main.yml b/roles/cli_modern/tasks/main.yml new file mode 100644 index 0000000..a6296dc --- /dev/null +++ b/roles/cli_modern/tasks/main.yml @@ -0,0 +1,26 @@ +--- +# cli_modern — main task dispatcher +# Routes to OS-specific package installation, then shared config tasks. + +- name: Gather OS family facts + ansible.builtin.setup: + filter: ansible_os_family + when: ansible_os_family is not defined + +- name: Include OS-specific package tasks + ansible.builtin.include_tasks: "packages-{{ ansible_os_family | lower }}.yml" + +- name: Install Starship via official script + ansible.builtin.include_tasks: starship.yml + when: cli_modern_install_starship + +- name: Install Fisher and plugins + ansible.builtin.include_tasks: fisher.yml + when: cli_modern_install_fisher + +- name: Deploy shell configuration + ansible.builtin.include_tasks: config.yml + +- name: Set Fish as default shell + ansible.builtin.include_tasks: default-shell.yml + when: cli_modern_set_default_shell diff --git a/roles/cli_modern/tasks/packages-archlinux.yml b/roles/cli_modern/tasks/packages-archlinux.yml new file mode 100644 index 0000000..d3d45e6 --- /dev/null +++ b/roles/cli_modern/tasks/packages-archlinux.yml @@ -0,0 +1,22 @@ +--- +# Arch Linux / Manjaro package installation +# All tools are in the official repos or AUR — no PPA needed. + +- name: Install CLI tools (pacman) + become: true + community.general.pacman: + name: "{{ cli_modern_arch_packages }}" + state: present + update_cache: true + vars: + cli_modern_arch_packages: >- + {{ + (['fish'] if cli_modern_install_fish else []) + + (['fzf'] if cli_modern_install_fzf else []) + + (['zoxide'] if cli_modern_install_zoxide else []) + + (['bat'] if cli_modern_install_bat else []) + + (['eza'] if cli_modern_install_eza else []) + + (['fd'] if cli_modern_install_fd else []) + + (['ripgrep'] if cli_modern_install_ripgrep else []) + }} + when: cli_modern_arch_packages | length > 0 diff --git a/roles/cli_modern/tasks/packages-debian.yml b/roles/cli_modern/tasks/packages-debian.yml new file mode 100644 index 0000000..4f96734 --- /dev/null +++ b/roles/cli_modern/tasks/packages-debian.yml @@ -0,0 +1,89 @@ +--- +# Debian / Ubuntu package installation +# Adds the Fish shell PPA to get 4.x on older Ubuntu releases. + +- name: Install prerequisites for PPA management + become: true + ansible.builtin.apt: + name: + - software-properties-common + - apt-transport-https + - gnupg + - curl + state: present + update_cache: true + +# --- Fish PPA (Ubuntu only, needed for 4.x on < 24.10) --- + +- name: Check current Fish version + ansible.builtin.command: fish --version + register: cli_modern_fish_version_check + changed_when: false + failed_when: false + +- name: Extract Fish version number + ansible.builtin.set_fact: + cli_modern_fish_current_ver: >- + {{ cli_modern_fish_version_check.stdout + | default('') + | regex_search('(\d+\.\d+)', '\1') + | first + | default('0') }} + +- name: Determine if Fish PPA is needed + ansible.builtin.set_fact: + cli_modern_need_fish_ppa: >- + {{ cli_modern_install_fish and ( + cli_modern_fish_version_check.rc != 0 or + cli_modern_fish_current_ver is version(cli_modern_fish_min_version, '<') + ) }} + +- name: Add Fish shell PPA + become: true + ansible.builtin.apt_repository: + repo: "ppa:fish-shell/release-4" + state: present + update_cache: true + when: cli_modern_need_fish_ppa | bool + +# --- Install packages --- + +- name: Install CLI tools (apt) + become: true + ansible.builtin.apt: + name: "{{ cli_modern_deb_packages }}" + state: present + update_cache: true + vars: + cli_modern_deb_packages: >- + {{ + (['fish'] if cli_modern_install_fish else []) + + (['fzf'] if cli_modern_install_fzf else []) + + (['zoxide'] if cli_modern_install_zoxide else []) + + (['bat'] if cli_modern_install_bat else []) + + (['eza'] if cli_modern_install_eza else []) + + (['fd-find'] if cli_modern_install_fd else []) + + (['ripgrep'] if cli_modern_install_ripgrep else []) + }} + when: cli_modern_deb_packages | length > 0 + +# On Debian/Ubuntu, fd is packaged as fd-find with binary named fdfind. +# Create a symlink so "fd" works everywhere. +- name: Symlink fdfind to fd + become: true + ansible.builtin.file: + src: /usr/bin/fdfind + dest: /usr/local/bin/fd + state: link + when: cli_modern_install_fd + failed_when: false + +# On older Ubuntu, bat is packaged as batcat. +- name: Symlink batcat to bat + become: true + ansible.builtin.file: + src: /usr/bin/batcat + dest: /usr/local/bin/bat + state: link + when: cli_modern_install_bat + failed_when: false diff --git a/roles/cli_modern/tasks/starship.yml b/roles/cli_modern/tasks/starship.yml new file mode 100644 index 0000000..b1f3a72 --- /dev/null +++ b/roles/cli_modern/tasks/starship.yml @@ -0,0 +1,29 @@ +--- +# Install Starship prompt via official install script. +# This method works on both Arch and Debian without repo differences. + +- name: Check if Starship is already installed + ansible.builtin.command: starship --version + register: cli_modern_starship_check + changed_when: false + failed_when: false + +- name: Download Starship install script + ansible.builtin.get_url: + url: https://starship.rs/install.sh + dest: /tmp/starship-install.sh + mode: "0755" + when: cli_modern_starship_check.rc != 0 + +- name: Install Starship via official installer + become: true + ansible.builtin.command: /tmp/starship-install.sh --yes + args: + creates: /usr/local/bin/starship + when: cli_modern_starship_check.rc != 0 + +- name: Clean up Starship installer + ansible.builtin.file: + path: /tmp/starship-install.sh + state: absent + when: cli_modern_starship_check.rc != 0 diff --git a/roles/cli_modern/templates/config.fish.j2 b/roles/cli_modern/templates/config.fish.j2 new file mode 100644 index 0000000..5e439b8 --- /dev/null +++ b/roles/cli_modern/templates/config.fish.j2 @@ -0,0 +1,57 @@ +# {{ ansible_managed }} +# Fish shell configuration — deployed by cli_modern role + +if status is-interactive + + # ── Prompt ────────────────────────────────────────────── +{% if cli_modern_install_starship %} + starship init fish | source +{% endif %} + + # ── Navigation ────────────────────────────────────────── +{% if cli_modern_install_zoxide %} + zoxide init fish | source +{% endif %} + +{% if cli_modern_install_fzf %} + # fzf.fish plugin handles keybindings (Ctrl+R history, Ctrl+F files) + # Set fzf default options + set -gx FZF_DEFAULT_OPTS "--height 40% --layout=reverse --border --info=inline" +{% endif %} + + # ── Aliases ───────────────────────────────────────────── +{% if cli_modern_install_eza %} + alias ls "eza --icons --group-directories-first" + alias ll "eza -la --icons --group-directories-first --git" + alias lt "eza --tree --level=2 --icons" +{% endif %} + +{% if cli_modern_install_bat %} + alias cat "bat --paging=never" + alias catp "bat" +{% endif %} + + # Git shortcuts + alias gs "git status" + alias gd "git diff" + alias gl "git log --oneline -20" + alias gp "git pull" + + # Docker shortcuts + alias dc "docker compose" + alias dps "docker ps --format 'table {{ '{{' }}.Names{{ '}}' }}\t{{ '{{' }}.Status{{ '}}' }}\t{{ '{{' }}.Ports{{ '}}' }}'" + alias dlogs "docker compose logs -f" + + # Safety nets + alias rm "rm -i" + alias mv "mv -i" + alias cp "cp -i" + + # ── Environment ───────────────────────────────────────── + set -gx EDITOR vim + set -gx VISUAL vim + + # Suppress Fish greeting + set -g fish_greeting + +end diff --git a/roles/cli_modern/templates/fish_plugins.j2 b/roles/cli_modern/templates/fish_plugins.j2 new file mode 100644 index 0000000..36758d7 --- /dev/null +++ b/roles/cli_modern/templates/fish_plugins.j2 @@ -0,0 +1,6 @@ +# {{ ansible_managed }} +# Fisher plugins — managed by cli_modern role +# Run `fisher update` to sync after changes. +{% for plugin in cli_modern_fisher_plugins %} +{{ plugin }} +{% endfor %} diff --git a/roles/cli_modern/templates/starship.toml.j2 b/roles/cli_modern/templates/starship.toml.j2 new file mode 100644 index 0000000..d9d688e --- /dev/null +++ b/roles/cli_modern/templates/starship.toml.j2 @@ -0,0 +1,98 @@ +# {{ ansible_managed }} +# Starship prompt configuration — deployed by cli_modern role +# Docs: https://starship.rs/config/ + +# Timeout for commands that starship runs (ms) +command_timeout = 1000 + +# Insert blank line between prompts +add_newline = true + +# ── Prompt Format ─────────────────────────────────────────── +format = """ +$username\ +$hostname\ +$directory\ +$git_branch\ +$git_status\ +$python\ +$golang\ +$nodejs\ +$docker_context\ +$cmd_duration\ +$line_break\ +$character""" + +# ── Character ─────────────────────────────────────────────── +[character] +success_symbol = "[❯](bold green)" +error_symbol = "[❯](bold red)" +vimcmd_symbol = "[❮](bold green)" + +# ── Directory ─────────────────────────────────────────────── +[directory] +truncation_length = 4 +truncation_symbol = "…/" +truncate_to_repo = true +style = "bold cyan" + +# ── Git ───────────────────────────────────────────────────── +[git_branch] +format = "[$symbol$branch(:$remote_branch)]($style) " +symbol = " " +style = "bold purple" + +[git_status] +format = '([\[$all_status$ahead_behind\]]($style) )' +style = "bold red" +conflicted = "=" +ahead = "⇡${count}" +behind = "⇣${count}" +diverged = "⇕⇡${ahead_count}⇣${behind_count}" +untracked = "?${count}" +stashed = "*" +modified = "!${count}" +staged = "+${count}" +deleted = "✘${count}" + +# ── Languages ─────────────────────────────────────────────── +[python] +format = '[${symbol}${pyenv_prefix}(${version} )(\($virtualenv\) )]($style)' +symbol = " " +style = "yellow" + +[golang] +format = "[$symbol($version )]($style)" +symbol = " " +style = "bold cyan" + +[nodejs] +format = "[$symbol($version )]($style)" +symbol = " " +style = "bold green" + +# ── Docker ────────────────────────────────────────────────── +[docker_context] +format = "[$symbol$context]($style) " +symbol = " " +style = "blue bold" +only_with_files = true + +# ── Command Duration ──────────────────────────────────────── +[cmd_duration] +min_time = 2_000 +format = "[$duration]($style) " +style = "bold yellow" + +# ── Username & Hostname (show on SSH sessions only) ───────── +[username] +show_always = false +format = "[$user]($style)@" +style_user = "bold green" +style_root = "bold red" + +[hostname] +ssh_only = true +format = "[$ssh_symbol$hostname]($style) " +ssh_symbol = "🌐 " +style = "bold green"