Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Project Instructions

- When repairing symlinks for dotfiles in this project use @scripts/stow.sh

## Git Workflow

- Never commit directly to main. Always create a branch, commit there, push, and open a PR if one doesn't already exist for that branch.
- Use conventional commit messages. Body should be in list format and not overly verbose.
- Use `scripts/stow.sh` for dotfile deployment and symlink repair.
- Run `scripts/check.sh` before committing implementation changes.
- Keep application configuration portable. Put OS package names and system
provisioning only under `ansible/`.
- Never commit secrets, resolved 1Password references, access tokens, or private
keys.
125 changes: 88 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,67 +1,118 @@
# dotfiles

Simple GNU Stow-based dotfiles with idempotent setup.
Portable application packages and configuration, deployed with GNU Stow.

## Layout
This repository has a deliberately narrow job:

- Top-level directories (excluding `hosts`, `scripts`, `.git*`, and `.claude`) are base stow packages.
- Package contents mirror `$HOME` exactly.
- Per-host overrides live under `hosts/<hostname>/<package>` and are stowed after base packages.
1. Track a small profile of applications and command-line tools.
2. Map that profile to the current operating system's package names.
3. Link portable application configuration into the user's home directory.

Current base packages:
Full Fedora workstation provisioning—including repositories, drivers, desktop
policy, and system tuning—belongs in a separate repository.

- `git` -> `~/.config/git/config`
- `starship` -> `~/.config/starship.toml`
## Managed configuration

Example:
The default Stow deployment includes Git, Ghostty, Neovim/LazyVim, Bash, tmux,
and Starship. Package contents mirror paths relative to `$HOME`. The managed
Bash startup file loads additive fragments from `~/.bashrc.d`. If an existing
`~/.bashrc` already loads that directory, deployment preserves it; otherwise
Stow stops instead of replacing personal shell startup commands.

```text
hosts/<hostname>/hypr/.config/hypr/...
The Fedora package profile installs the core command-line applications and
utilities available from the configured DNF repositories on mutable Fedora
installations. Ghostty and Starship configuration is tracked here, but their
third-party repository or binary setup is intentionally left to workstation
provisioning.

## Bootstrap

Preview the package transaction and dotfile deployment:

```bash
./bootstrap.sh --dry-run
```

## Usage
On a fresh machine without GNU Stow, this lists the dotfile packages that will
be deployed; after Stow is installed, the same command also checks exact link
changes and conflicts.

Check that GNU Stow is installed:
Install missing packages and deploy all configured dotfiles:

```sh
stow --version
```bash
./bootstrap.sh
```

Dry-run first:
The package step is idempotent: the Fedora adapter checks installed RPMs and
invokes DNF only for missing packages. The Stow step always uses `--restow` so
it also repairs managed symlinks.

```sh
./scripts/stow.sh base -n
./scripts/stow.sh host -n
Run either half independently:

```bash
./bootstrap.sh --packages-only
./bootstrap.sh --dotfiles-only
```

Stow all base packages:
Never run these scripts with `sudo`; the Fedora adapter requests elevation only
for the DNF transaction.

```sh
./scripts/stow.sh base
## Package tracking

List the portable identifiers and their Fedora package mapping:

```bash
./scripts/install-packages.sh --provider fedora --list
```

Stow host-specific packages for this machine:
The package data is split into:

```sh
./scripts/stow.sh host
- `ansible/packages/profile.txt`: provider-neutral application identifiers.
- `ansible/packages/providers/fedora.txt`: Fedora package names.
- `ansible/package-providers/fedora.sh`: Fedora detection, planning, and
installation behavior.

To add another tested provider, add its mapping and an executable adapter with
the same `detect`, `plan`, and `install` interface. The shared profile and
Stow packages do not change.

## Dotfiles

Preview or deploy the explicit default package list:

```bash
./scripts/stow.sh base --dry-run
./scripts/stow.sh base
```

Stow a specific host's packages:
Deploy selected portable packages:

```sh
./scripts/stow.sh host <hostname>
```bash
./scripts/stow.sh base git nvim
```

For low-level troubleshooting, this is the equivalent shape for one host
package:
Host-specific configuration remains available when needed:

```sh
stow -t "$HOME" --restow --no-folding -d hosts/<hostname> <package>
```bash
./scripts/stow.sh host
./scripts/stow.sh host nexus-unbound
```

## Notes
`stow-packages.txt` is the source of truth for the default deployment. Keeping
that allowlist separate prevents infrastructure and documentation directories
from being mistaken for dotfile packages.

## Git authentication and signing

- Keep only source-of-truth files here; avoid generated artifacts.
- Host packages should only contain overrides, so the base packages stay portable.
- Use `scripts/stow.sh` for normal refreshes and symlink repairs so base and
host package discovery stays consistent.
Ordinary commits are unsigned so unattended tools do not block on 1Password.
Use `git cis` for an explicitly signed personal commit. GitHub HTTPS credentials
are delegated to `gh auth git-credential`; credentials and private keys are
never stored in this repository.

## Validation

Run the repository checks before committing implementation changes:

```bash
./scripts/check.sh
```
11 changes: 11 additions & 0 deletions ansible/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Package manifests

This directory is intentionally limited to operating-system-specific package
data. `packages/profile.txt` contains stable application identifiers, while
`packages/providers/<provider>.txt` maps those identifiers to concrete package
names.

The package-provider adapters also live here because they perform the elevated
package transaction. The unprivileged dispatcher lives under `scripts/`. Full
system provisioning, repository configuration, drivers, and workstation policy
belong in the separate Fedora setup repository.
74 changes: 74 additions & 0 deletions ansible/package-providers/fedora.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
set -euo pipefail

die() {
printf 'Error: %s\n' "$*" >&2
exit 1
}

is_supported() {
[[ "$(uname -s)" == Linux ]] || return 1
[[ -r /etc/os-release ]] || return 1

(
# shellcheck source=/dev/null
source /etc/os-release
[[ "${ID:-}" == fedora ]]
) || return 1

[[ ! -e /run/ostree-booted ]] || return 1
command -v dnf > /dev/null 2>&1 && command -v rpm > /dev/null 2>&1
}

collect_missing() {
missing_packages=()
local package
for package in "$@"; do
[[ "$package" =~ ^[A-Za-z0-9][A-Za-z0-9+_.:-]*$ ]] ||
die "invalid Fedora package name: $package"
rpm -q -- "$package" > /dev/null 2>&1 || missing_packages+=("$package")
done
}

print_plan() {
collect_missing "$@"
if [[ ${#missing_packages[@]} -eq 0 ]]; then
printf 'All requested Fedora packages are already installed.\n'
return
fi

printf 'Would install %d missing Fedora package(s):\n' "${#missing_packages[@]}"
printf ' sudo dnf install --assumeyes'
printf ' %q' "${missing_packages[@]}"
printf '\n'
}

install_packages() {
collect_missing "$@"
if [[ ${#missing_packages[@]} -eq 0 ]]; then
printf 'All requested Fedora packages are already installed.\n'
return
fi

command -v sudo > /dev/null 2>&1 || die "sudo is required to install Fedora packages"
sudo dnf install --assumeyes "${missing_packages[@]}"
Comment thread
mark-groves marked this conversation as resolved.
}

case "${1:-}" in
detect)
is_supported
;;
plan)
shift
is_supported || die "this provider requires a mutable Fedora installation with dnf"
print_plan "$@"
;;
install)
shift
is_supported || die "this provider requires a mutable Fedora installation with dnf"
install_packages "$@"
;;
*)
die "provider usage: $(basename "$0") <detect|plan|install> [package ...]"
;;
esac
28 changes: 28 additions & 0 deletions ansible/packages/profile.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Stable application identifiers for the default portable command-line profile.
git
git-lfs
github-cli
c-compiler
openssh-client
curl
tree-sitter-cli
nodejs
npm
go
rust
cargo
rust-analyzer
neovim
Comment thread
mark-groves marked this conversation as resolved.
stow
tmux
direnv
ripgrep
fd
fzf
bat
jq
yq
unzip
shellcheck
shfmt
clipboard
28 changes: 28 additions & 0 deletions ansible/packages/providers/fedora.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# logical-id Fedora package name
git git
git-lfs git-lfs
github-cli gh
c-compiler gcc
openssh-client openssh-clients
curl curl
tree-sitter-cli tree-sitter-cli
nodejs nodejs22-bin
npm nodejs22-npm-bin
go golang
rust rust
cargo cargo
rust-analyzer rust-analyzer
neovim neovim
stow stow
tmux tmux
direnv direnv
ripgrep ripgrep
fd fd-find
fzf fzf
bat bat
jq jq
yq yq
unzip unzip
shellcheck ShellCheck
shfmt shfmt
clipboard wl-clipboard
71 changes: 71 additions & 0 deletions bootstrap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
cat << 'EOF'
Usage: bootstrap.sh [options]

Install the portable package profile, then deploy its dotfiles.

Options:
-n, --dry-run Preview package and Stow changes
--provider NAME Override automatic package-provider detection
--packages-only Install packages without deploying dotfiles
--dotfiles-only Deploy dotfiles without installing packages
-h, --help Show this help message
EOF
}

die() {
printf 'Error: %s\n' "$*" >&2
exit 1
}

root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
dry_run=false
packages_only=false
dotfiles_only=false
provider=""

while [[ $# -gt 0 ]]; do
case "$1" in
-n | --dry-run) dry_run=true ;;
--provider)
shift
[[ $# -gt 0 ]] || die "--provider requires a name"
provider="$1"
;;
--provider=*) provider="${1#*=}" ;;
--packages-only) packages_only=true ;;
--dotfiles-only) dotfiles_only=true ;;
-h | --help)
usage
exit 0
;;
*) die "unknown option: $1" ;;
esac
shift
done

"$packages_only" && "$dotfiles_only" &&
die "--packages-only and --dotfiles-only cannot be used together"
[[ ${EUID:-$(id -u)} -ne 0 ]] || die "run bootstrap as your normal user, not root"

if ! "$dotfiles_only"; then
package_args=()
"$dry_run" && package_args+=(--dry-run)
[[ -z "$provider" ]] || package_args+=(--provider "$provider")
"$root/scripts/install-packages.sh" "${package_args[@]}"
fi

if ! "$packages_only"; then
if "$dry_run" && ! command -v stow > /dev/null 2>&1; then
printf 'GNU Stow is not installed; the package plan above includes it.\n'
printf 'Would deploy these dotfile packages after installation:\n'
"$root/scripts/stow.sh" list | sed 's/^/ /'
else
stow_args=(base)
"$dry_run" && stow_args+=(--dry-run)
"$root/scripts/stow.sh" "${stow_args[@]}"
fi
fi
10 changes: 10 additions & 0 deletions ghostty/.config/ghostty/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Follow the desktop's light/dark appearance automatically.
theme = dark:Catppuccin Frappe,light:Catppuccin Latte

font-size = 12
window-padding-x = 8
window-padding-y = 6
window-padding-balance = true

# Protect terminals that still have a running process.
confirm-close-surface = true
Loading