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
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ just rollback [id] # Rollback to previous state
just rollback [id] --with-brew # Also rollback packages
just rollback [id] --dry-run # Preview changes
just licenses # Manage application licenses
just manifest ... # Manage config manifest (add, remove, list)
just secrets ... # Secrets management (init, backup, restore)
```

Expand All @@ -70,6 +71,7 @@ just secrets ... # Secrets management (init, backup, restore)
```
~/.dotfiles/
├── config/ # Base configurations
│ ├── manifest # Declares what gets linked where
│ ├── ghostty/
│ ├── git/
│ ├── gnupg/
Expand All @@ -86,7 +88,8 @@ just secrets ... # Secrets management (init, backup, restore)
│ │ ├── core/
│ │ │ ├── Brewfile
│ │ │ ├── bundle.conf
│ │ │ └── setup.sh
│ │ │ ├── setup.sh
│ │ │ └── manifest # (optional) Config links
│ │ ├── develop/
│ │ ├── personal/
│ │ ├── work/
Expand All @@ -106,6 +109,7 @@ just secrets ... # Secrets management (init, backup, restore)
│ ├── secrets-init # Initialize encryption
│ ├── licenses # License manager (Python)
│ ├── daemons # Daemon/agent manager (Python)
│ ├── manifest # Manifest management (add/remove/list)
│ └── platform # Platform operations (configure/link)
├── secrets/ # Encrypted secrets (age)
├── loaded/ # Symlinks to active bundles (for glob discovery)
Expand Down Expand Up @@ -164,6 +168,19 @@ apply_config_overrides "$BUNDLE_DIR"
chmod +x platforms/macos/bundles/mybundle/setup.sh
```

6. (Optional) Add a `manifest` file for config linking instead of hardcoding in `setup.sh`:

```
# Link individual files
myconfig/settings.toml -> $HOME/.config/myapp/settings.toml

# Link entire directories (trailing /)
myconfig/ -> $HOME/.config/myapp/

# Set directory permissions
@chmod 700 $HOME/.config/myapp
```

The bundle will automatically appear in the selection menu.

## Requirements
Expand Down
66 changes: 40 additions & 26 deletions TODO.md

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions config/manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Zsh (individual files — defaults.zsh and rune.zsh are sourced, not linked)
zsh/zshrc -> $HOME/.zshrc
zsh/zshenv -> $HOME/.zshenv
zsh/zprofile -> $HOME/.zprofile

# Git
git/gitconfig -> $HOME/.gitconfig
git/gitignore -> $HOME/.gitignore

# Ghostty (entire directory: config, themes, shaders)
ghostty/ -> $HOME/.config/ghostty/

# Starship
starship/starship.toml -> $HOME/.config/starship.toml

# Mise (conf.d/ so mise use -g writes to config.toml without dirtying git)
mise/conf.d/ -> $HOME/.config/mise/conf.d/

# Atuin (shell history sync)
atuin/config.toml -> $HOME/.config/atuin/config.toml

# WezTerm
wezterm/wezterm.lua -> $HOME/.config/wezterm/wezterm.lua

# SSH
ssh/config -> $HOME/.ssh/config
@chmod 700 $HOME/.ssh

# GnuPG
gnupg/gpg-agent.conf -> $HOME/.gnupg/gpg-agent.conf
@chmod 700 $HOME/.gnupg
16 changes: 11 additions & 5 deletions config/mise/config.toml → config/mise/conf.d/defaults.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,25 @@ compile = false
MISE_DEFAULT_CONFIG_FILENAME = "mise.local.toml"

[tools]
# Languages
node = "lts"
pnpm = "latest"
python = "latest"
rust = "latest"
go = "latest"
python = "latest"
zig = "latest"
lua = "latest"
uv = "latest"
java = "latest"
ruby = "latest"
dotnet = "latest"
gradle = "latest"
cmake = "latest"
deno = "latest"
bun = "latest"
dart = "latest"
flutter = "latest"

# Tools
pnpm = "latest"
uv = "latest"
cargo-binstall = "latest"
cocogitto = "latest"
lefthook = "latest"
task = "latest"
4 changes: 4 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ licenses *args:
daemons *args:
./scripts/daemons {{args}}

# Manage config manifest (add, remove, list, chmod)
manifest *args:
./scripts/manifest {{args}}

# Dev utilities for faster testing (hidden from `just --list`).
# Uses a separate justfile + [private] recipe because:
# - `[private]` only hides recipes, not module namespaces
Expand Down
135 changes: 116 additions & 19 deletions scripts/lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,22 @@ safe_link() {
## Directory Setup

# Create standard config directories with proper permissions
# Reads @chmod directives from base manifest, also ensures .config and .ssh/sockets
ensure_config_dirs() {
mkdir -p "$HOME/.config"
mkdir -p "$HOME/.config/mise"
mkdir -p "$HOME/.config/ghostty"
mkdir -p "$HOME/.config/wezterm"
mkdir -p "$HOME/.ssh/sockets"
mkdir -p "$HOME/.gnupg"
chmod 700 "$HOME/.ssh"
chmod 700 "$HOME/.gnupg"

local manifest="$DOTFILES_DIR/config/manifest"
if [[ -f "$manifest" ]]; then
while IFS= read -r line || [[ -n "$line" ]]; do
# Strip leading/trailing whitespace
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
case "$line" in
@chmod\ *) _apply_chmod_directive "$line" ;;
esac
done < "$manifest"
fi
}

## Homebrew
Expand Down Expand Up @@ -78,28 +85,117 @@ install_brewfile() {
}
}

## Manifest-Driven Config Linking

# Apply a manifest file: link files, recurse directories, set permissions
# Usage: apply_manifest <manifest_file> <source_base_dir>
apply_manifest() {
local manifest="$1"
local base_dir="$2"

[[ ! -f "$manifest" ]] && return 0

while IFS= read -r line || [[ -n "$line" ]]; do
# Strip leading/trailing whitespace
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"

# Skip comments and blank lines
[[ -z "$line" || "$line" == \#* ]] && continue

# Permission directive
if [[ "$line" == @chmod\ * ]]; then
_apply_chmod_directive "$line"
continue
fi

# Parse: source -> destination
local src dest
src="${line%% ->*}"
dest="${line##*-> }"

# Strip whitespace from src/dest
src="${src%"${src##*[![:space:]]}"}"
dest="${dest#"${dest%%[![:space:]]*}"}"

# Expand $HOME in destination
dest="${dest/\$HOME/$HOME}"

# Directory linking (trailing /)
if [[ "$src" == */ && "$dest" == */ ]]; then
_link_directory "$base_dir/$src" "$dest"
continue
fi

# File linking
local full_src="$base_dir/$src"
[[ -f "$full_src" ]] && safe_link "$full_src" "$dest"
done < "$manifest"
}

# Recursively link all files from source directory to destination directory
# Skips .DS_Store files
# Usage: _link_directory <source_dir> <dest_dir>
_link_directory() {
local src_dir="${1%/}"
local dest_dir="${2%/}"

[[ ! -d "$src_dir" ]] && return 0

while IFS= read -r -d '' file; do
local rel_path="${file#"$src_dir"}"
# Skip .DS_Store
local basename
basename=$(basename "$file")
[[ "$basename" == ".DS_Store" ]] && continue

safe_link "$file" "$dest_dir$rel_path"
done < <(find "$src_dir" -type f -print0 2>/dev/null)
}

# Parse and apply a @chmod directive
# Usage: _apply_chmod_directive "@chmod 700 $HOME/.ssh"
_apply_chmod_directive() {
local line="$1"
local mode path

# Strip @chmod prefix
line="${line#@chmod }"
mode="${line%% *}"
path="${line#* }"

# Expand $HOME
path="${path/\$HOME/$HOME}"

mkdir -p "$path"
chmod "$mode" "$path"
}

## Config Symlinks

# Link base configs from DOTFILES_DIR/config to home
link_base_configs() {
local config_dir="$DOTFILES_DIR/config"

[[ -f "$config_dir/zsh/zshrc" ]] && safe_link "$config_dir/zsh/zshrc" "$HOME/.zshrc"
[[ -f "$config_dir/zsh/zshenv" ]] && safe_link "$config_dir/zsh/zshenv" "$HOME/.zshenv"
[[ -f "$config_dir/zsh/zprofile" ]] && safe_link "$config_dir/zsh/zprofile" "$HOME/.zprofile"
[[ -f "$config_dir/starship/starship.toml" ]] && safe_link "$config_dir/starship/starship.toml" "$HOME/.config/starship.toml"
[[ -f "$config_dir/git/gitconfig" ]] && safe_link "$config_dir/git/gitconfig" "$HOME/.gitconfig"
[[ -f "$config_dir/git/gitignore" ]] && safe_link "$config_dir/git/gitignore" "$HOME/.gitignore"
[[ -f "$config_dir/mise/config.toml" ]] && safe_link "$config_dir/mise/config.toml" "$HOME/.config/mise/config.toml"
[[ -f "$config_dir/ghostty/config" ]] && safe_link "$config_dir/ghostty/config" "$HOME/.config/ghostty/config"
[[ -f "$config_dir/wezterm/wezterm.lua" ]] && safe_link "$config_dir/wezterm/wezterm.lua" "$HOME/.config/wezterm/wezterm.lua"
[[ -f "$config_dir/ssh/config" ]] && safe_link "$config_dir/ssh/config" "$HOME/.ssh/config"
[[ -f "$config_dir/gnupg/gpg-agent.conf" ]] && safe_link "$config_dir/gnupg/gpg-agent.conf" "$HOME/.gnupg/gpg-agent.conf"
apply_manifest "$DOTFILES_DIR/config/manifest" "$DOTFILES_DIR/config"
}

# Apply bundle-specific config overrides
# Checks for bundle manifest first, falls back to legacy case-statement
apply_config_overrides() {
local bundle_dir="$1"

# Manifest-based: bundle has its own manifest file
if [[ -f "$bundle_dir/manifest" ]]; then
apply_manifest "$bundle_dir/manifest" "$DOTFILES_DIR/config"
return 0
fi

# Legacy: scan bundle config/ directory with case-statement mapping
_apply_config_overrides_legacy "$bundle_dir"
}

# Legacy config override logic for bundles without a manifest
_apply_config_overrides_legacy() {
local bundle_dir="$1"
local config_dir="$bundle_dir/config"

[[ ! -d "$config_dir" ]] && return 0
Expand All @@ -115,6 +211,7 @@ apply_config_overrides() {
git/gitconfig) dest="$HOME/.gitconfig" ;;
git/gitignore) dest="$HOME/.gitignore" ;;
mise/*) dest="$HOME/.config/mise/${rel_path#mise/}" ;;
atuin/*) dest="$HOME/.config/atuin/${rel_path#atuin/}" ;;
ghostty/*) dest="$HOME/.config/ghostty/${rel_path#ghostty/}" ;;
wezterm/*) dest="$HOME/.config/wezterm/${rel_path#wezterm/}" ;;
ssh/*) dest="$HOME/.ssh/${rel_path#ssh/}" ;;
Expand Down
Loading
Loading