Skip to content
Closed
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
20 changes: 20 additions & 0 deletions .shellcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# .shellcheckrc — repo-wide ShellCheck config so author-time, CI, and editor
# (bashls / nvim) all lint identically. Previously the only ShellCheck settings were
# the `-x` flag the audit passes and per-file `# shellcheck` directives scattered in
# the scripts; an editor opening a script saw none of that and could disagree with CI.
# This centralises the cross-cutting bits. File-SPECIFIC suppressions (e.g. the
# intentional SC2016 in test-core.sh / bench-core.sh, where `$` in single quotes is
# expanded by a zsh CHILD, not this bash parent) deliberately stay inline, next to the
# code that needs them — they are not global truths and must not be hidden here.

# Follow `source`/`.`-d files so `-x` works from any working directory, and resolve a
# sourced path relative to the script being checked (SCRIPTDIR) — this is what lets the
# gate scripts' `source "${BASH_SOURCE[0]%/*}/lib/common.sh"` resolve in an editor
# without each file needing an inline `# shellcheck source=` directive.
external-sources=true
source-path=SCRIPTDIR
source-path=scripts

# Lint as bash: every runnable script here is `#!/usr/bin/env bash`, and the sourced
# libraries (scripts/lib/*.sh) carry no shebang, so pin the dialect explicitly.
shell=bash
28 changes: 27 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@ commit (`git tag -a vX.Y.Z -m vX.Y.Z`).

### Added

- `core.version` — a human-readable SemVer stamp vendored into every OS repo, plus a
`core-version` verb that reads it, so you can tell WHICH Core a given OS repo carries
from inside it (the subtree squash records the commit; this records the version).
`scripts/sync-core.sh` prints it on fan-out and the audit asserts it is well-formed.
- `core-doctor` — the shell counterpart to nvim's `:checkhealth gerrrt`: a scannable
report of which modern-CLI tools Core detected on this box and which integrations are
live, including the RESOLVED binary names (`fd`/`fdfind`, `bat`/`batcat`) and the
detected package manager. Read-only.
- `up -n`/`--dry-run` — list the packages that WOULD upgrade and exit, touching nothing
(the non-destructive inspect the count-only nudge didn't offer).
- `make audit-changed` (`audit-core.sh --changed`) — scope the audit to what your local
git diff touches, via the SAME `scripts/ci-classify.sh` CI uses; fails safe to the
full run when the diff can't be resolved.
- First-party completions for `fif`, `fbr`, `core-version`, and `core-doctor`, and a
`core.version`/`up --dry-run`-aware `_up`; the completion-parity test now covers them.
- `.shellcheckrc` — repo-wide ShellCheck config (`external-sources`, `source-path`,
`shell=bash`) so author-time, CI, and editor lint identically.
- `zsh/ui.zsh` — shared terminal-UX primitives (`_core_err`/`_core_warn`/`_core_ok`/
`_core_hint`/`_core_usage`/`_core_confirm`/`_core_spin`), gum-aware with a plain
fallback on every helper. Loads right after `tools` in the canonical chain and is
Expand Down Expand Up @@ -78,6 +95,8 @@ commit (`git tag -a vX.Y.Z -m vX.Y.Z`).

### Changed

- `scripts/setup.sh` provisions `luacheck` via `luarocks` (no clean mise source) and
emits precise, actionable install hints — closing the last manual onboarding gap.
- Defensive confirms on impactful interactive actions: `please` now previews the exact
`sudo …` line and confirms before eval'ing it as root (and refuses with no previous
command); `up` pre-confirms `Apply updates with <mgr>?` before touching the system
Expand Down Expand Up @@ -111,8 +130,15 @@ commit (`git tag -a vX.Y.Z -m vX.Y.Z`).
upstream breaking change — or a compromised tag — out to all nine machines on the
next install; installs now fetch exactly the pinned commit.

### Fixed
### Fixed

- fzf / fzf-tab previews hardcoded `bat`/`eza`, so every preview pane printed
"command not found" on Debian/Ubuntu (bat ships as `batcat`) and on any box without
eza. Previews now resolve `$BAT_BIN` with a `cat`/`ls` fallback, and a new audit
section (`fzf preview binary resolution`) locks it so the regression can't recur.
- `fif`, `fbr`, and the Alt-Z zoxide-jump widget assumed `fzf`/`rg`/`git`/`zoxide`
were present; they now degrade in Core's voice (`_core_err`/`_core_hint`) like `fcd`,
instead of a raw "command not found".
- Removed leaked `</content>`/`</invoke>` template artifacts from the end of this
changelog — the exact bug class the new markdown gate now catches.
- Restored non-executable mode (`100644`) on the twelve `zsh/*.zsh` modules. They
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# pre-commit call the same scripts/audit-core.sh, so `make audit` == green CI.
# ──────────────────────────────────────────────────────────────────────────────
.DEFAULT_GOAL := help
.PHONY: help setup audit test bench lint sync sync-dry hooks update-hooks update-plugins update-nvim-plugins check-pins
.PHONY: help setup audit audit-changed test bench lint sync sync-dry hooks update-hooks update-plugins update-nvim-plugins check-pins

help: ## Show this help
@echo "dotfiles-core — make targets:"
Expand All @@ -20,6 +20,9 @@ setup: ## One-command dev bootstrap (pre-commit hooks + version doctor + audit)
audit: ## Run the full Core audit (manifest, exec-bits, syntax, lint, behavioral) — the one gate
@./scripts/audit-core.sh

audit-changed: ## Audit only what your git diff touches (fast dev loop; same classifier as CI)
@./scripts/audit-core.sh --changed

test: ## Run only the behavioral tests (load-order smoke + function units)
@./scripts/test-core.sh

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ git/
local.gitconfig.example identity template — seeded by bootstrap, never tracked
nvim/ entire lazy.nvim tree: lua/gerrrt/{config,plugins,servers,utils}
core.manifest the canonical list of Core files (drives sync + audits)
core.version human-readable Core version stamp (read by `core-version`)
```

> Load order is load-bearing: `tools` inits atuin (registers its widget), `options`
Expand Down
4 changes: 4 additions & 0 deletions core.manifest
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
# OS repo (OS-native) or to dotfiles-Kali (offensive).
#
# This list is the contract. Audit scripts and the promotion checklist read it.
# ── version stamp — the human-readable Core version, vendored so an OS repo can
# report WHICH Core it carries (`core-version`); the subtree squash records the
# commit, this records the SemVer. ──────────────────────────────────────────
core.version
# ── zsh ───────────────────────────────────────────────────────────────────────
# Load order (sourced by each OS repo's .zshrc loader, in THIS order):
# tools -> ui -> options -> history -> aliases -> git -> functions -> fzf
Expand Down
1 change: 1 addition & 0 deletions core.version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.1.0-dev
105 changes: 102 additions & 3 deletions scripts/audit-core.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$HERE" || exit 1

QUIET=0
CHANGED=0 # --changed: derive the scope from the local git diff (fast dev loop)
SCOPE_EXPLICIT=0 # an explicit --scope always wins over --changed
# Scope gates the SLOW, area-specific sections so a per-area push (driven by
# scripts/ci-classify.sh) pays only for what it changed — e.g. a docs-only PR runs
# the cheap structural/config/markdown checks but skips the zsh and nvim toolchains.
Expand Down Expand Up @@ -106,11 +108,16 @@ while (($#)); do
fi
shift
_set_scope "$1"
SCOPE_EXPLICIT=1
;;
--scope=*) _set_scope "${1#*=}" ;;
--scope=*)
_set_scope "${1#*=}"
SCOPE_EXPLICIT=1
;;
--changed) CHANGED=1 ;;
-h | --help)
cat <<'EOF'
usage: audit-core.sh [-q|--quiet] [--scope LIST] [-h|--help]
usage: audit-core.sh [-q|--quiet] [--scope LIST] [--changed] [-h|--help]

THE audit button — manifest/exec-bit/syntax/lint/config/markdown/workflow/
version/behavioral checks. CI and pre-commit run this exact script.
Expand All @@ -120,6 +127,12 @@ version/behavioral checks. CI and pre-commit run this exact script.
shell, nvim, all (default), none. Cheap structural/config/
markdown/workflow/version checks always run. CI sets this from
scripts/ci-classify.sh; omit it locally to run the full audit.
--changed derive the scope from your local git diff (working tree vs HEAD,
falling back to the branch delta vs the default branch) using the
SAME scripts/ci-classify.sh CI uses — so a docs- or nvim-only edit
skips the gates it can't affect, tightening the dev loop. Fails SAFE
to the full run when the diff can't be resolved. An explicit --scope
overrides this.
-h, --help show this help and exit
EOF
exit 0
Expand All @@ -138,6 +151,49 @@ done
# shellcheck source=scripts/lib/common.sh
source "${BASH_SOURCE[0]%/*}/lib/common.sh"

# ── --changed: derive the scope from the local git diff ───────────────────────
# Reuse the EXACT classifier CI runs (scripts/ci-classify.sh) so `make audit-changed`
# narrows to the same gates a push would — one definition of path→gate, no drift. The
# changed set is the working tree vs HEAD plus untracked files; when the tree is clean
# we fall back to the branch delta vs the default branch. Anything unresolvable → the
# full run (fail-safe), matching CI's "detection miss never hides a gate" rule. An
# explicit --scope already set SCOPE_EXPLICIT and wins.
_changed_scope() {
if ! have git || ! git rev-parse --git-dir >/dev/null 2>&1; then
printf 'all'
return
fi
local files base
files="$( { git diff --name-only HEAD 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null; } | sort -u )"
if [[ -z "$files" ]]; then
for base in origin/main main origin/master master; do
git rev-parse -q --verify "$base" >/dev/null 2>&1 || continue
files="$(git diff --name-only "$base"...HEAD 2>/dev/null)"
break
done
fi
[[ -n "$files" ]] || { printf 'all'; return; } # nothing resolvable → full (safe)
local out sh nv scope=""
out="$(printf '%s\n' "$files" | "$HERE/scripts/ci-classify.sh" 2>/dev/null)"
sh="$(printf '%s\n' "$out" | sed -n 's/^shell=//p')"
nv="$(printf '%s\n' "$out" | sed -n 's/^nvim=//p')"
# The classifier must emit BOTH keys as true/false. If it errored (non-executable,
# internal failure) or produced no/garbage output, sh/nv won't be valid — fail SAFE
# to the full run rather than silently returning "none" and skipping every slow gate.
if { [[ "$sh" != true && "$sh" != false ]]; } || { [[ "$nv" != true && "$nv" != false ]]; }; then
printf 'all'
return
fi
[[ "$sh" == true ]] && scope="shell"
[[ "$nv" == true ]] && scope="${scope:+$scope,}nvim"
printf '%s' "${scope:-none}"
Comment thread
Gerrrt marked this conversation as resolved.
}
if ((CHANGED)) && ((!SCOPE_EXPLICIT)); then
_cs="$(_changed_scope)"
((QUIET)) || printf '%s== --changed → scope %s ==%s\n' "$c_blu" "$_cs" "$c_rst"
_set_scope "$_cs"
fi

# Wall-clock from here, surfaced in the summary — so a long run (the headless nvim /
# zsh legs) reads as "took Ns", not "hung", and a regression in audit cost is visible.
SECONDS=0
Expand All @@ -147,7 +203,7 @@ SECONDS=0
# manifest, must appear here (or under a META_PREFIXES dir) or section 1 flags it.
META_ALLOWLIST=(
README.md PORTING-MATRIX.md CONTRIBUTING.md CHANGELOG.md LICENSE SECURITY.md
core.manifest .gitignore .gitattributes .editorconfig .pre-commit-config.yaml .markdownlint.jsonc
core.manifest .gitignore .gitattributes .editorconfig .pre-commit-config.yaml .markdownlint.jsonc .shellcheckrc
Makefile
nvim/.luacheckrc
CODEOWNERS pull_request_template.md
Expand Down Expand Up @@ -282,6 +338,34 @@ else
skip "shellcheck (not installed)"
fi

# ── 5b. fzf preview binary resolution (regression gate) ──────────────────────
# fzf / fzf-tab previews run their command STRING in a subshell, so a LITERAL `bat`
# there printed "command not found" in every preview pane on Debian/Ubuntu — those
# distros ship bat as `batcat` — a silent breakage that fanned out to those OS repos
# with no failing gate. The fix routes previews through $BAT_BIN (tools.zsh resolves
# the real name) with a cat/ls fallback. Lock it so the bug can't recur: no uncommented
# preview line in zsh/fzf.zsh or zsh/plugins.zsh may invoke a literal bat/batcat, and
# fzf.zsh must still reference $BAT_BIN. Pure sed+grep (busybox-safe), shell-scoped.
hdr "fzf preview binary resolution"
if ((SCOPE_SHELL)); then
pv_fail=0
for f in zsh/fzf.zsh zsh/plugins.zsh; do
# Strip comments (from the first #), then flag a bare lowercase bat/batcat command
# token — $BAT_BIN (uppercase) is intentionally NOT matched, which is the point.
if sed 's/#.*//' "$f" | grep -qE '(^|[^A-Za-z_$])bat(cat)?[[:space:]]'; then
pv_fail=1
fail "literal bat/batcat in a preview command ($f) — route it through \$BAT_BIN"
fi
done
grep -q 'BAT_BIN' zsh/fzf.zsh || {
pv_fail=1
fail "zsh/fzf.zsh no longer references \$BAT_BIN (preview resolution lost)"
}
((pv_fail)) || pass "fzf/fzf-tab previews resolve \$BAT_BIN (no literal bat/batcat)"
else
skip "fzf preview resolution (out of scope)"
fi

# ── 6. config files (toml / yaml parse) ──────────────────────────────────────
# A malformed starship.toml / mise config.toml / ci.yml is still valid *text* —
# so zsh -n and shellcheck never look at it — yet it breaks every one of the 9
Expand Down Expand Up @@ -414,6 +498,21 @@ else
skip "version consistency ($VERSIONS_ENV or $PRECOMMIT_CFG unreadable)"
fi

# core.version is the human-readable Core stamp vendored into all 9 OS repos (read by
# the `core-version` verb). A missing or malformed stamp would fan out a bogus version
# everywhere, so assert it exists and is SemVer-shaped (MAJOR.MINOR.PATCH, optional
# -prerelease). Single line only — the verb and sync-core.sh both read it whole.
if [[ -r core.version ]]; then
cv="$(tr -d '[:space:]' <core.version)"
if [[ "$cv" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
pass "core.version well-formed ($cv)"
else
fail "core.version malformed ('$cv') — expected SemVer MAJOR.MINOR.PATCH[-pre]"
fi
else
fail "core.version missing — the vendored version stamp (core-version reads it)"
fi

# ── 10. behavioral tests (load-order smoke + function unit tests) ─────────────
# Static analysis above proves the modules PARSE; this proves they LOAD TOGETHER
# in canonical order and that the pure functions behave. Delegated to test-core.sh
Expand Down
7 changes: 4 additions & 3 deletions scripts/ci-classify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
# so the caller can append them straight to $GITHUB_OUTPUT.
#
# Buckets (first match per file wins):
# • infra scripts/ .github/ .claude/ core.manifest .pre-commit-config.yaml
# Makefile — cross-cutting, so force the FULL run (shell+nvim)
# • infra scripts/ .github/ .claude/ core.manifest core.version
# .pre-commit-config.yaml .shellcheckrc Makefile — cross-cutting, force
# the FULL run
# • nvim nvim/** → nvim
# • shell zsh/ bin/ maint/ tmux/ sesh/ starship/ mise/ git/ **/*.sh → shell
# • inert *.md + repo-meta dotfiles → no gate
Expand All @@ -40,7 +41,7 @@ while IFS= read -r f; do
break
fi
case "$f" in
scripts/* | .github/* | .claude/* | core.manifest | .pre-commit-config.yaml | Makefile) full ;;
scripts/* | .github/* | .claude/* | core.manifest | core.version | .pre-commit-config.yaml | .shellcheckrc | Makefile) full ;;
nvim/*) nvim=true ;;
zsh/* | bin/* | maint/* | tmux/* | sesh/* | starship/* | mise/* | git/* | *.sh) shell=true ;;
*.md | LICENSE | CODEOWNERS | .gitignore | .gitattributes | .editorconfig | .markdownlint.jsonc) ;;
Expand Down
33 changes: 31 additions & 2 deletions scripts/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ source "${BASH_SOURCE[0]%/*}/lib/common.sh"
VERSIONS="scripts/tool-versions.env"
_ver() { sed -n "s/^$1=//p" "$VERSIONS" 2>/dev/null | head -n1; }

# Dim follow-up "→" hint line (the fix-it, after a skip/warn) — one consistent voice
# for the actionable next step, mirroring zsh/ui.zsh's _core_hint in the runtime layer.
hint() { printf '%s→%s %s\n' "$c_yel" "$c_rst" "$*"; }

# ── 1. pre-commit hooks ───────────────────────────────────────────────────────
hdr "pre-commit hooks"
PRECOMMIT_VERSION="$(_ver PRECOMMIT_VERSION)"
Expand All @@ -46,8 +50,8 @@ fi
# mise.local.toml from it and let `mise install` provision the linters the audit/CI
# run. Best-effort + graceful like every other step: no mise → a hint, not a hard
# stop; a tool mise can't supply just stays missing and the doctor below flags it.
# luacheck has no clean mise source (luarocks) so it stays manual; pre-commit is
# handled in step 1. The generated file is regenerated every run (idempotent) and is
# luacheck has no clean mise source (luarocks) so step 2b below provisions it; pre-commit
# is handled in step 1. The generated file is regenerated every run (idempotent) and is
# NOT tracked — bumping a pin in tool-versions.env and re-running propagates here.
hdr "hermetic dev toolchain (mise)"
if have mise; then
Expand All @@ -73,6 +77,31 @@ else
skip "mise absent — install it (https://mise.jdx.dev) to auto-provision the pinned toolchain; the doctor below lists what to install manually"
fi

# ── 2b. luacheck via luarocks (no clean mise source) ──────────────────────────
# luacheck is a luarocks package — mise has no first-class source for it, so step 2
# can't provision it and it used to be left wholly manual ("see ci.yml"). Close that
# gap the same way CI does: when it's missing AND luarocks is present, install the
# pinned version with --local (lands in ~/.luarocks/bin). Best-effort + graceful: no
# luarocks → a precise hint, not a hard stop; the doctor below still reports the result.
hdr "luacheck (luarocks)"
LUACHECK_VERSION="$(_ver LUACHECK_VERSION)"
if have luacheck; then
pass "luacheck already installed"
elif have luarocks; then
if luarocks --local install luacheck "$LUACHECK_VERSION" >/dev/null 2>&1; then
pass "luacheck ${LUACHECK_VERSION} installed via luarocks (--local)"
# ~/.luarocks/bin is where --local drops the binary; surface it if it's not on PATH.
case ":$PATH:" in
*":$HOME/.luarocks/bin:"*) ;;
*) hint "add \$HOME/.luarocks/bin to PATH so luacheck resolves: export PATH=\"\$HOME/.luarocks/bin:\$PATH\"" ;;
esac
else
hint "luarocks present but 'luarocks --local install luacheck ${LUACHECK_VERSION}' failed — install lua + a dev toolchain, then retry"
fi
else
skip "luacheck absent and luarocks not found — install luarocks, then: luarocks --local install luacheck ${LUACHECK_VERSION}"
fi

# ── 3. version doctor (present? vs the pin) ───────────────────────────────────
hdr "tool versions (pinned in $VERSIONS)"
_doctor() { # _doctor <bin> <pinned> <version-cmd...>
Expand Down
Loading
Loading