Compare commits

...

8 Commits

Author SHA1 Message Date
renovate[bot]
d218bb4ed8 chore(deps): update simek/yarn-lock-changes action (v0.14.0 → v0.14.1)
Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-08 02:00:00 +00:00
a18c16b0b9 fix(shell): harden shared.sh and dfm for set -euo pipefail
Use ${VAR:-} defaults in shared.sh to prevent set -u failures on
unset variables (DOTFILES, ZSH_CUSTOM_COMPLETION_PATH, FPATH).
Export DOTFILES/BREWFILE/HOSTFILES in dfm so sourced scripts see them.
2026-02-08 01:12:39 +02:00
785a8e8eb7 fix(exports): prevent set -e abort when optional files are missing
Replace `[ -f ] && source` with `if/then/fi` for conditional source
lines so the file returns 0 even when optional exports files don't
exist. Also use `${VAR:-}` for XDG defaults to avoid set -u failures.
2026-02-08 01:11:55 +02:00
1cda859999 docs(claude): expand CLAUDE.md with msgr, dfm commands, gotchas, and hooks 2026-02-08 00:31:33 +02:00
bc69560da4 feat(claude): add shfmt/vendor hooks and shell-validate skill 2026-02-08 00:26:19 +02:00
2ee9407a43 feat(dfm): add 6 install commands and reorder install all into tiers 2026-02-07 23:41:51 +02:00
765c2fce72 test(dfm): expand bats tests from 1 to 16
Add tests for menu output of all sections (install, helpers, docs,
dotfiles, check, scripts, tests), routing of invalid input, install
menu completeness for all 19 entries, and check arch/host commands.
2026-02-07 23:20:02 +02:00
88eceaf194 fix(dfm): restrict cheat-databases glob to .sh files only
The install-cheat-* glob was matching .md documentation files, causing
errors when bash tried to execute them.
2026-02-07 22:45:08 +02:00
8 changed files with 311 additions and 20 deletions

26
.claude/settings.json Normal file
View File

@@ -0,0 +1,26 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "fp=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$fp\" ] && case \"$fp\" in */fzf-tmux|*/yarn.lock|*/.yarn/*) echo \"BLOCKED: $fp is a vendor/lock file — do not edit directly\" >&2; exit 2;; esac; exit 0"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "fp=$(cat | jq -r '.tool_input.file_path // empty') && [ -n \"$fp\" ] && [ -f \"$fp\" ] && case \"$fp\" in *.sh|*/bin/*) head -1 \"$fp\" | grep -qE '^#!.*(ba)?sh' && command -v shfmt > /dev/null && shfmt -i 2 -bn -ci -sr -fn -w \"$fp\";; esac; exit 0"
}
]
}
]
}
}

View File

@@ -0,0 +1,37 @@
---
name: shell-validate
description: Validate shell scripts after editing. Apply when writing or modifying any shell script in local/bin/ or scripts/.
user-invocable: false
allowed-tools: Bash, Read, Grep
---
After editing any shell script in `local/bin/`, `scripts/`, or `config/` (files with a `#!` shebang or `# shellcheck shell=` directive), validate it:
## 1. Determine the shell
- `/bin/sh` or `#!/usr/bin/env sh` shebang -> POSIX, use `sh -n`
- `/bin/bash` or `#!/usr/bin/env bash` shebang -> Bash, use `bash -n`
- `# shellcheck shell=bash` directive (no shebang) -> use `bash -n`
- `# shellcheck shell=sh` directive (no shebang) -> use `sh -n`
- No shebang and no directive -> default to `bash -n`
## 2. Syntax check
Run the appropriate syntax checker:
```bash
bash -n <file> # for bash scripts
sh -n <file> # for POSIX sh scripts
```
If syntax check fails, fix the issue before proceeding.
## 3. ShellCheck
Run `shellcheck <file>`. The project `.shellcheckrc` already disables SC2039, SC2166, SC2154, SC1091, SC2174, SC2016. Only report and fix warnings that are NOT in that exclude list.
## Key files to never validate (not shell scripts)
- `local/bin/fzf-tmux` (vendor file)
- `*.md` files
- `*.bats` test files (Bats, not plain shell)

View File

@@ -31,7 +31,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Yarn Lock Changes
uses: Simek/yarn-lock-changes@c7543145aaafdd8fc925cea5d85b2bd5a73091f8 # v0.14.0
uses: Simek/yarn-lock-changes@59f47ee499424d2c2437c5aebf863b5c6d50a5bc # v0.14.1
with:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -34,6 +34,8 @@ yarn install
yarn lint # Run biome + prettier + editorconfig-checker
yarn lint:biome # Biome only
yarn lint:ec # EditorConfig checker only
yarn lint:md-table # Markdown table formatting check
yarn fix:md-table # Auto-fix markdown tables
# Formatting
yarn fix:biome # Autofix with biome (JS/TS/JSON/MD)
@@ -74,12 +76,24 @@ which loads:
Zsh additionally uses **antidote** (in `tools/antidote/`)
for plugin management and **oh-my-posh** for the prompt.
### msgr — Messaging Helper
`local/bin/msgr` provides colored output functions (`msgr msg`,
`msgr run`, `msgr yay`, `msgr err`, `msgr warn`). Sourced by `dfm`
and most scripts in `local/bin/`.
### dfm — Dotfiles Manager
`local/bin/dfm` is the main management script. Key commands:
- `dfm install all` — install everything (called during `./install`)
- `dfm install all` — install everything in tiered stages
- `dfm brew install` / `dfm brew update` — Homebrew management
- `dfm apt upkeep` — APT package maintenance (Debian/Ubuntu)
- `dfm dotfiles fmt` / `dfm dotfiles shfmt` — format configs/scripts
- `dfm helpers <name>` — inspect aliases, colors, env, functions, path
- `dfm docs all` — regenerate documentation under `docs/`
- `dfm check arch` / `dfm check host` — system info
- `dfm scripts` — run scripts from `scripts/` (discovered via `@description` tags)
- `dfm tests` — test visualization helpers
### Submodules
@@ -118,6 +132,26 @@ SC2039 (POSIX `local`), SC2166 (`-o` in test),
SC2154 (unassigned variables), SC1091 (source following),
SC2174 (mkdir -p -m), SC2016 (single-quote expressions).
## Gotchas
- **POSIX scripts**: `x-ssh-audit`, `x-codeql`, `x-until-error`,
`x-until-success`, `x-ssl-expiry-date` use `/bin/sh`.
Validate with `sh -n`, not `bash -n`.
- **Vendor file**: `local/bin/fzf-tmux` is vendored from
junegunn/fzf — do not modify.
- **Fish config**: `config/fish/` has its own config chain
(`config.fish`, `exports.fish`, `alias.fish`) plus 80+ functions.
- **Python**: Two scripts (`x-compare-versions.py`,
`x-git-largest-files.py`) linted by Ruff (config in `pyproject.toml`).
## Claude Code Configuration
- **Hooks** (`.claude/settings.json`):
- *PreToolUse*: Blocks edits to `fzf-tmux`, `yarn.lock`, `.yarn/`
- *PostToolUse*: Auto-runs `shfmt` on shell scripts after Edit/Write
- **Skills** (`.claude/skills/`):
- `shell-validate`: Auto-validates shell scripts (syntax + shellcheck)
## Package Manager
Yarn (v4.12.0) is the package manager. Do not use npm.

View File

@@ -4,15 +4,15 @@
# Set XDG directories if not already set
# https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
[ -z "$XDG_CONFIG_HOME" ] && export XDG_CONFIG_HOME="$HOME/.config"
[ -z "$XDG_DATA_HOME" ] && export XDG_DATA_HOME="$HOME/.local/share"
[ -z "$XDG_CACHE_HOME" ] && export XDG_CACHE_HOME="$HOME/.cache"
[ -z "$XDG_STATE_HOME" ] && export XDG_STATE_HOME="$HOME/.local/state"
[ -z "$XDG_BIN_HOME" ] && export XDG_BIN_HOME="$HOME/.local/bin"
[ -z "$XDG_RUNTIME_DIR" ] && export XDG_RUNTIME_DIR="$HOME/.local/run"
[ -z "${XDG_CONFIG_HOME:-}" ] && export XDG_CONFIG_HOME="$HOME/.config"
[ -z "${XDG_DATA_HOME:-}" ] && export XDG_DATA_HOME="$HOME/.local/share"
[ -z "${XDG_CACHE_HOME:-}" ] && export XDG_CACHE_HOME="$HOME/.cache"
[ -z "${XDG_STATE_HOME:-}" ] && export XDG_STATE_HOME="$HOME/.local/state"
[ -z "${XDG_BIN_HOME:-}" ] && export XDG_BIN_HOME="$HOME/.local/bin"
[ -z "${XDG_RUNTIME_DIR:-}" ] && export XDG_RUNTIME_DIR="$HOME/.local/run"
# if DOTFILES is not set, set it to the default location
[ -z "$DOTFILES" ] && export DOTFILES="$HOME/.dotfiles"
[ -z "${DOTFILES:-}" ] && export DOTFILES="$HOME/.dotfiles"
export PATH="$XDG_BIN_HOME:$DOTFILES/local/bin:$XDG_DATA_HOME/bob/nvim-bin:$XDG_DATA_HOME/cargo/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
@@ -448,9 +448,9 @@ export ZSHZ_DATA="$XDG_STATE_HOME/z"
export CHEAT_USE_FZF=true
export SQLITE_HISTORY="${XDG_CACHE_HOME}/sqlite_history"
[ -f "$XDG_CONFIG_HOME/exports-secret" ] && source "$XDG_CONFIG_HOME/exports-secret"
[ -f "$XDG_CONFIG_HOME/exports-local" ] && source "$XDG_CONFIG_HOME/exports-local"
if [ -f "$XDG_CONFIG_HOME/exports-secret" ]; then source "$XDG_CONFIG_HOME/exports-secret"; fi
if [ -f "$XDG_CONFIG_HOME/exports-local" ]; then source "$XDG_CONFIG_HOME/exports-local"; fi
# shellcheck source=./exports-lakka
[ -f "$XDG_CONFIG_HOME/exports-$(hostname)" ] && source "$XDG_CONFIG_HOME/exports-$(hostname)"
if [ -f "$XDG_CONFIG_HOME/exports-$(hostname)" ]; then source "$XDG_CONFIG_HOME/exports-$(hostname)"; fi
# shellcheck source=./exports-lakka-secret
[ -f "$XDG_CONFIG_HOME/exports-$(hostname)-secret" ] && source "$XDG_CONFIG_HOME/exports-$(hostname)-secret"
if [ -f "$XDG_CONFIG_HOME/exports-$(hostname)-secret" ]; then source "$XDG_CONFIG_HOME/exports-$(hostname)-secret"; fi

View File

@@ -5,7 +5,7 @@
# shellcheck shell=bash
# Defaults
[[ -z "$DOTFILES" ]] && export DOTFILES="$HOME/.dotfiles"
[[ -z "${DOTFILES:-}" ]] && export DOTFILES="$HOME/.dotfiles"
DOTFILES_CURRENT_SHELL=$(basename "$SHELL")
export DOTFILES_CURRENT_SHELL
@@ -76,9 +76,9 @@ x-path-prepend "$DOTFILES/local/bin"
x-path-prepend "$XDG_BIN_HOME"
# Custom completion paths
[[ -z "$ZSH_CUSTOM_COMPLETION_PATH" ]] && export ZSH_CUSTOM_COMPLETION_PATH="$XDG_CONFIG_HOME/zsh/completion"
[[ -z "${ZSH_CUSTOM_COMPLETION_PATH:-}" ]] && export ZSH_CUSTOM_COMPLETION_PATH="$XDG_CONFIG_HOME/zsh/completion"
x-dc "$ZSH_CUSTOM_COMPLETION_PATH"
export FPATH="$ZSH_CUSTOM_COMPLETION_PATH:$FPATH"
export FPATH="$ZSH_CUSTOM_COMPLETION_PATH:${FPATH:-}"
if ! declare -f msg > /dev/null; then
# Function to print messages if VERBOSE is enabled

View File

@@ -12,6 +12,7 @@
: "${DOTFILES:=$HOME/.dotfiles}"
: "${BREWFILE:=$DOTFILES/config/homebrew/Brewfile}"
: "${HOSTFILES:=$DOTFILES/hosts}"
export DOTFILES BREWFILE HOSTFILES
SCRIPT=$(basename "$0")
@@ -71,33 +72,57 @@ section_install()
MENU=(
"all:Installs everything in the correct order"
"apt-packages:Install apt packages (Debian/Ubuntu)"
"cargo:Install rust/cargo packages"
"cheat-databases:Install cheat external cheatsheet databases"
"composer:Install composer"
"dnf-packages:Install dnf packages (Fedora/RHEL)"
"fonts:Install programming fonts"
"gh:Install GitHub CLI Extensions"
"git-crypt:Install git-crypt from source"
"go:Install Go Packages"
"imagick:Install ImageMagick CLI"
"macos:Setup nice macOS defaults"
"npm-packages:Install NPM Packages"
"ntfy:Install ntfy notification tool"
"nvm-latest:Install latest lts node using nvm"
"nvm:Install Node Version Manager (nvm)"
"python-packages:Install Python packages via uv"
"xcode-cli-tools:Install Xcode CLI tools (macOS)"
"z:Install z"
)
case "$1" in
all)
msgr msg "Starting to install all and reloading configurations..."
$0 install macos
$0 install fonts
# Tier 0: Platform foundations (OS packages, build tools)
[[ "$(uname)" == "Darwin" ]] && $0 install macos
[[ "$(uname)" == "Darwin" ]] && $0 install xcode-cli-tools
command -v apt &> /dev/null && $0 install apt-packages
command -v dnf &> /dev/null && $0 install dnf-packages
# Tier 1: Package managers & fonts
$0 brew install
$0 install fonts
# Tier 2: Language packages (depend on runtimes from Tier 1)
$0 install cargo
$0 install go
$0 install composer
$0 install cheat-databases
$0 install nvm
$0 install npm-packages
$0 install python-packages
# Tier 3: Tool-dependent installers
$0 install cheat-databases
$0 install gh
$0 install git-crypt
$0 install ntfy
# Tier 4: Independent utilities
$0 install z
msgr msg "Reloading configurations again..."
# shellcheck disable=SC1091
source "$DOTFILES/config/shared.sh"
@@ -112,7 +137,7 @@ section_install()
cheat-databases)
msgr run "Installing cheat databases..."
for database in "$DOTFILES"/scripts/install-cheat-*; do
for database in "$DOTFILES"/scripts/install-cheat-*.sh; do
bash "$database" \
&& msgr run_done "Cheat: $database run"
done
@@ -184,6 +209,42 @@ section_install()
&& msgr yay "NPM Packages have been installed!"
;;
apt-packages)
msgr run "Installing apt packages..."
bash "$DOTFILES/scripts/install-apt-packages.sh" \
&& msgr yay "apt packages installed!"
;;
dnf-packages)
msgr run "Installing dnf packages..."
bash "$DOTFILES/scripts/install-dnf-packages.sh" \
&& msgr yay "dnf packages installed!"
;;
git-crypt)
msgr run "Installing git-crypt..."
bash "$DOTFILES/scripts/install-git-crypt.sh" \
&& msgr yay "git-crypt installed!"
;;
ntfy)
msgr run "Installing ntfy..."
bash "$DOTFILES/scripts/install-ntfy.sh" \
&& msgr yay "ntfy installed!"
;;
python-packages)
msgr run "Installing Python packages..."
bash "$DOTFILES/scripts/install-python-packages.sh" \
&& msgr yay "Python packages installed!"
;;
xcode-cli-tools)
msgr run "Installing Xcode CLI tools..."
bash "$DOTFILES/scripts/install-xcode-cli-tools.sh" \
&& msgr yay "Xcode CLI tools installed!"
;;
z)
msgr run "Installing z..."
bash "$DOTFILES/scripts/install-z.sh" \

View File

@@ -5,8 +5,141 @@ setup()
export DOTFILES="$PWD"
}
# ── Group 1: Main help & routing ──────────────────────────────
@test "dfm help shows usage" {
run bash local/bin/dfm help
[ "$status" -eq 0 ]
[[ "$output" == *"Usage: dfm"* ]]
}
@test "dfm with no args shows full usage with all sections" {
run bash local/bin/dfm
[ "$status" -eq 0 ]
[[ "$output" == *"Usage: dfm"* ]]
[[ "$output" == *"dfm install"* ]]
[[ "$output" == *"dfm check"* ]]
[[ "$output" == *"dfm helpers"* ]]
[[ "$output" == *"dfm docs"* ]]
[[ "$output" == *"dfm dotfiles"* ]]
[[ "$output" == *"dfm scripts"* ]]
}
@test "dfm with invalid section shows usage" {
run bash local/bin/dfm nonexistent
[ "$status" -eq 0 ]
[[ "$output" == *"Usage: dfm"* ]]
}
# ── Group 2: Install menu completeness ────────────────────────
@test "dfm install menu shows all entries" {
run bash local/bin/dfm install
[ "$status" -eq 0 ]
[[ "$output" == *"all"* ]]
[[ "$output" == *"apt-packages"* ]]
[[ "$output" == *"cargo"* ]]
[[ "$output" == *"cheat-databases"* ]]
[[ "$output" == *"composer"* ]]
[[ "$output" == *"dnf-packages"* ]]
[[ "$output" == *"fonts"* ]]
[[ "$output" == *"gh"* ]]
[[ "$output" == *"git-crypt"* ]]
[[ "$output" == *"go"* ]]
[[ "$output" == *"imagick"* ]]
[[ "$output" == *"macos"* ]]
[[ "$output" == *"npm-packages"* ]]
[[ "$output" == *"ntfy"* ]]
[[ "$output" == *"nvm-latest"* ]]
[[ "$output" == *"nvm"* ]]
[[ "$output" == *"python-packages"* ]]
[[ "$output" == *"xcode-cli-tools"* ]]
[[ "$output" == *"z"* ]]
}
@test "dfm install with invalid subcommand shows menu" {
run bash local/bin/dfm install nonexistent
[ "$status" -eq 0 ]
[[ "$output" == *"dfm install"* ]]
}
# ── Group 3: Other section menus ──────────────────────────────
@test "dfm helpers menu shows entries" {
run bash local/bin/dfm helpers
[ "$status" -eq 0 ]
[[ "$output" == *"aliases"* ]]
[[ "$output" == *"colors"* ]]
[[ "$output" == *"path"* ]]
[[ "$output" == *"env"* ]]
}
@test "dfm docs menu shows entries" {
run bash local/bin/dfm docs
[ "$status" -eq 0 ]
[[ "$output" == *"all"* ]]
[[ "$output" == *"tmux"* ]]
[[ "$output" == *"nvim"* ]]
[[ "$output" == *"wezterm"* ]]
}
@test "dfm dotfiles menu shows entries" {
run bash local/bin/dfm dotfiles
[ "$status" -eq 0 ]
[[ "$output" == *"fmt"* ]]
[[ "$output" == *"shfmt"* ]]
[[ "$output" == *"yamlfmt"* ]]
}
@test "dfm check menu shows entries" {
run bash local/bin/dfm check
[ "$status" -eq 0 ]
[[ "$output" == *"arch"* ]]
[[ "$output" == *"host"* ]]
}
@test "dfm scripts menu lists install scripts" {
run bash local/bin/dfm scripts
[ "$status" -eq 0 ]
[[ "$output" == *"cargo-packages"* ]]
[[ "$output" == *"fonts"* ]]
[[ "$output" == *"z"* ]]
}
@test "dfm tests menu shows entries" {
run bash local/bin/dfm tests
[ "$status" -eq 0 ]
[[ "$output" == *"msgr"* ]]
[[ "$output" == *"params"* ]]
}
# ── Group 4: Check commands ───────────────────────────────────
@test "dfm check arch returns current arch" {
run bash local/bin/dfm check arch
[ "$status" -eq 0 ]
[ -n "$output" ]
}
@test "dfm check arch with matching value exits 0" {
local current_arch
current_arch="$(uname)"
run bash local/bin/dfm check arch "$current_arch"
[ "$status" -eq 0 ]
}
@test "dfm check arch with non-matching value exits 1" {
run bash local/bin/dfm check arch FakeArch
[ "$status" -eq 1 ]
}
@test "dfm check host returns current hostname" {
run bash local/bin/dfm check host
[ "$status" -eq 0 ]
[ -n "$output" ]
}
@test "dfm check host with non-matching value exits 1" {
run bash local/bin/dfm check host fakehostname
[ "$status" -eq 1 ]
}