9 Commits
Author SHA1 Message Date
rootiest f6ead99f48 feat(help): header-driven --help for development functions, plus the wiring guard
bd-pull, branch, check_fish_deps, claude-docs, claude-pr, fast, poke, qr
and parur take the standard call site. fish-deps instead routes -h/--help
to its existing __fish_deps_help, which previously answered only via the
unknown-subcommand path, printing "Unknown subcommand: --help" and
exiting 1.

This completes the 40 conversions and lands
test_every_user_facing_function_has_help -- acceptance criterion 6, the
check that fails when a new user-facing function ships without help. It
was developed red against the pre-conversion tree and reported exactly
the 40 names of the design's CONVERT table:

    bd-pull bkg branch check_fish_deps claude-docs claude-pr cleanup
    fast fc fish-deps fzf-update gip gip4 gip6 hist lD ld limine-edit
    lock lsr lss lstree lt ltr lx parur poke ports qr sbver screensleep
    split spwin steam-dl sudo-toggle swapstat tab tmux-clean upgrade
    wake-lock

each as "<name>: no -h/--help handling and not in $__help_exempt", with
the guard exiting 1. The count fell 40 -> 32 -> 24 -> 17 -> 10 -> 0
across the conversion commits. It is committed green so every commit on
this branch passes the suite.

$__help_exempt is the single machine-readable exemption record; a second
assertion fails if an exempt name stops being a published function, so a
rename or deletion upstream cannot silently exempt nothing.
2026-09-07 20:09:30 -04:00
rootiest 3d826c5407 feat(help): header-driven --help for terminal and session functions
spwin, tab, hist, bkg, wake-lock and fc take the standard first-arg-only
call site, so `wake-lock rsync --help` still runs rsync.

split takes --help only: its own ARGUMENTS documents -h as --horizontal,
so a literal test replaces the renderer's own flag check. Verified that
`split -h` still selects a horizontal split and never prints help.
2026-09-07 20:07:25 -04:00
rootiest 3399149cba feat(help): header-driven --help for system-info functions
gip, gip4, gip6, ports, swapstat, sbver and steam-dl. All ignored $argv,
so --help previously ran the query or the launcher. sbver keeps its own
--brief flag: only argv[1] is inspected and --brief is not a help flag,
so the body still runs for it.
2026-09-07 20:06:23 -04:00
rootiest 71574b5030 feat(help): header-driven --help for listing presets
lD, lsr, lss, lstree, lt, ltr, lx and ld. None shadows a real binary, so
there is no disabled-fallback contract to honour, and `eza --help`
documented eza rather than the preset that is these functions' entire
content. First-arg-only interception leaves `lt -la --help` passing
through to eza untouched.
2026-09-07 20:05:15 -04:00
rootiest 037588ecf6 fix(help): stop eight functions executing on --help
These eight ignore $argv entirely, so passing --help ran the command
instead of describing it. The new check shadows every external binary
they reach with a recording stub on PATH and fails if anything is
invoked. Before the fix it reported, verbatim:

    cleanup --help EXECUTED: pacman -Qtdq
    fzf-update --help EXECUTED: git clone --depth 1 https://github.com/junegunn/fzf.git /tmp/.../.fzf
    limine-edit --help EXECUTED: sudoedit /boot/limine.conf sudo limine-enroll-config sudo limine-mkinitcpio sudo sbctl sign-all
    lock --help EXECUTED: loginctl lock-session
    screensleep --help EXECUTED: busctl --user call org.kde.kglobalaccel ... invokeShortcut s Turn Off Screen
    sudo-toggle --help EXECUTED: sudo stat -c %s /etc/sudoers.d/nofail-toggle sudo tee /etc/sudoers.d/nofail-toggle
    tmux-clean --help EXECUTED: tmux list-sessions -F #{session_name} #{session_attached}
    upgrade --help EXECUTED: paru -Syu --noconfirm

cleanup's log line is the read that precedes `sudo pacman -Rns $orphans`,
which the stub suppressed by returning no orphans; on a real machine with
orphans present the removal ran.

Each now answers --help from its own comment header. The call site is the
first statement of the body, above the C4 guard, so help stays reachable
when the component is disabled and nothing side-effecting runs first.
2026-09-07 20:04:02 -04:00
rootiest b424b26700 test(help): pin the renderer's degraded-path contract
A missing or label-less header must still print and exit 0. Returning 1
would hand control back to the caller's body, which for upgrade(1) is a
full system upgrade.

Both fixtures reach the renderer's bottom `count $out -eq 0` fallback,
not the unreadable-source branch. Mutating that fallback to `return 1`
turns the test red with:

    headerless executed its body despite --help
    malformed executed its body despite --help

The mutation was reverted before this commit.
2026-09-07 19:59:50 -04:00
rootiest 9077d9837e feat(help): add __fish_help_header runtime renderer
Parses a function's own man-page comment header at call time and prints
it as a help menu on stdout, so the documentation that already generates
Section 5 of the manual becomes reachable from the shell.

Reads the .fish source rather than the generated docs/fish-config.md, so
it cannot go stale between a header edit and a docs rebuild. Walks
backwards from the `function` line to collect the header, which resolves
multi-header files (fish-deps, gi, y) without reimplementing
manualtools._block_identity.

Returns 1 only when argv[1] is not a help flag; every other path prints
and returns 0. A return of 1 hands control back to the caller's body.

Nothing calls it yet.
2026-09-07 14:59:05 -04:00
rootiest 8a2731d411 revert: untrack the spec
AGENTS/ is gitignored upstream; a tracked file there pollutes
git diff main..work, which is what the user reads when choosing
which job to merge. Spec stays on disk, browsable, out of history.
2026-09-07 07:22:51 -04:00
rootiest 21a02fd0e4 docs(specs): add header-driven --help design
Design for making the man-page-style comment header above each function
reachable at runtime via -h/--help, instead of hand-writing 40 more menus.

One renderer (__fish_help_header) parses the .fish source at call time and
prints the header to stdout; each call site is a single line. Audits all 79
published functions that lack help today into 35 shadow/pass-through exempt,
4 prompt-hook exempt, and 40 to convert, with a one-line reason each.

Corrects the baseline counts (109 published functions, 30 with menus, 79
without) and records two pre-existing defects found while auditing.

Note: AGENTS/ is gitignored upstream, so this file is force-added.
2026-09-07 03:06:30 -04:00
42 changed files with 486 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# __fish_help_header <name> [args...]
#
# DESCRIPTION
# Prints <name>'s man-page comment header as a help menu on stdout.
# Intended as the first statement of a user-facing function's body:
#
# __fish_help_header (status current-function) $argv; and return 0
#
# Returns 1 -- printing nothing -- ONLY when args[1] is not a help flag.
# Every other outcome, including an unreadable or headerless source
# file, prints something and returns 0. That asymmetry is load-bearing:
# a return of 1 means "run the real body", and the real body of upgrade
# is `paru -Syu --noconfirm`. A parse failure must never return 1.
#
# Only args[1] is inspected, never the whole list. wake-lock, bkg,
# split and spwin take a command to run as their arguments, so
# scanning all of $argv would make `wake-lock rsync --help` print
# wake-lock's own help instead of running rsync.
#
# The header is read from the caller's source at call time rather than
# from the generated manual, so it cannot go stale between a header
# edit and a docs rebuild.
#
# ARGUMENTS
# name The calling function's name, from (status current-function)
# args... The caller's $argv, forwarded verbatim
#
# EXIT STATUS
# 0 Help was printed, including the degraded fallback
# 1 args[1] is not -h/--help; the caller should carry on
#
# EXAMPLE
# __fish_help_header (status current-function) $argv; and return 0
#
# NOTES
# Section labels are those of the manual SSOT parser in
# docs/manualtools.py. CATEGORY, COMPONENT and DEPENDENCIES are build
# metadata and are suppressed; SYNOPSIS renders as USAGE and EXAMPLE as
# EXAMPLES.
function __fish_help_header --argument-names name
# First argument only -- see DESCRIPTION.
contains -- "$argv[2]" -h --help; or return 1
set -l c_ttl (set_color --bold)
set -l c_sec (set_color --bold brblue)
set -l c_rst (set_color normal)
set -l miss " No documentation header found. Try: help config $name"
set -l file (functions -D -- $name 2>/dev/null)
if not test -f "$file"
# Quoted: set_color yields an EMPTY LIST under TERM=dumb, and an
# unquoted empty list in a concatenation annihilates the whole
# word -- the title line would silently vanish wherever colour is
# off, which is exactly where a test would be reading it.
echo "$c_ttl$name$c_rst"
echo $miss
return 0
end
# Collect the contiguous comment run directly above `function <name>`,
# walking backwards. This resolves multi-header files (fish-deps, gi,
# y) without reimplementing manualtools._block_identity, and is more
# accurate at runtime: in dops.fish it finds the header above
# `function docker` rather than attributing it to the file stem.
# One blank separator line is tolerated -- sponge_filter_secrets.fish
# is the only file that has one, and JOB-BRIEF-FINDINGS.md records it
# so this skip is not mistaken for dead code.
set -l lines (string split \n -- (command cat $file))
set -l pat '^\s*function\s+'(string escape --style=regex -- $name)'(\s|$)'
set -l start 0
for i in (seq (count $lines))
if string match -qr -- $pat $lines[$i]
set start $i
break
end
end
set -l header
if test $start -gt 1
set -l j (math $start - 1)
if test -z (string trim -- "$lines[$j]")
set j (math $j - 1)
end
while test $j -ge 1; and string match -q '#*' -- $lines[$j]
set -p header $lines[$j]
set j (math $j - 1)
end
end
# Render. Comment lines before the first `# LABEL` -- the copyright
# preamble -- carry no label and are dropped, matching
# manualtools._header_blocks.
set -l skip CATEGORY COMPONENT DEPENDENCIES
set -l label ""
set -l out
for line in $header
set -l m (string match -r -- '^#\s+([A-Z][A-Z ]*[A-Z])\s*$' $line)
if set -q m[2]
set label $m[2]
contains -- $label $skip; and continue
set -l shown (string replace SYNOPSIS USAGE -- $label)
set shown (string replace EXAMPLE EXAMPLES -- $shown)
# One blank line before a heading, never two: the header's own
# `#` separator has usually already emitted one.
if set -q out[1]; and test -n (string trim -- "$out[-1]")
set -a out ""
end
set -a out "$c_sec$shown$c_rst"
continue
end
test -n "$label"; or continue
contains -- $label $skip; and continue
set -l body (string sub -s 2 -- $line)
if string match -q ' *' -- $body
set -a out " "(string sub -s 4 -- $body)
else
set -a out (string trim -- $body)
end
end
# Trim the trailing blank separator, mirroring
# manualtools._trailing_blanks.
while set -q out[-1]; and test -z (string trim -- "$out[-1]")
set -e out[-1]
end
echo "$c_ttl$name$c_rst"
if test (count $out) -eq 0
echo $miss
else
# out[1] is always a heading -- a body line cannot precede the
# first label -- so this blank is never doubled.
echo ""
printf '%s\n' $out
end
return 0
end
+2
View File
@@ -23,6 +23,8 @@
# bd-pull myuser/myproject
# bd-pull rootiest/fish-config
function bd-pull --description 'Pull new Gitea issues into local Beads and link them'
__fish_help_header (status current-function) $argv; and return 0
if not set -q argv[1]; echo "Need repo owner/name"; return 1; end
if not set -q GITEA_TOKEN; echo "\$GITEA_TOKEN not set"; return 1; end
+2
View File
@@ -23,6 +23,8 @@
# EXAMPLE
# bkg firefox
function bkg --description 'Execute bkg'
__fish_help_header (status current-function) $argv; and return 0
# Check if a command was provided as an argument.
if test -z "$argv[1]"
set -l c_head (set_color --bold cyan)
+2
View File
@@ -21,6 +21,8 @@
# EXAMPLE
# branch feature/new-ui
function branch --description 'Switch to or create a git branch'
__fish_help_header (status current-function) $argv; and return 0
if not git rev-parse --is-inside-work-tree >/dev/null 2>&1
echo "Not a git repo."
return 1
+2
View File
@@ -14,5 +14,7 @@
# EXAMPLE
# check_fish_deps
function check_fish_deps --description 'Check all fish-related dependencies'
__fish_help_header (status current-function) $argv; and return 0
fish-deps status
end
+2
View File
@@ -15,5 +15,7 @@
# EXAMPLE
# claude-docs
function claude-docs --description 'Claude-code: Sync README with recent changes'
__fish_help_header (status current-function) $argv; and return 0
claude "Analyze the recent changes and update the README.md to ensure all features, setup instructions, and examples are 100% accurate. Prune any obsolete information."
end
+2
View File
@@ -15,5 +15,7 @@
# EXAMPLE
# claude-pr
function claude-pr --description 'Claude-code: New branch, commit, push, and PR'
__fish_help_header (status current-function) $argv; and return 0
claude "Act as a senior engineer. Execute this sequence: 1. Create a new git branch (kebab-case). 2. Stage changes and write a Conventional Commit message. 3. Self-verify the changes by running relevant build/test commands or linting. 4. Push to remote. 5. Create a PR to 'main' including a summary of changes and a 'Manual Verification' section containing a Markdown checklist (- [ ]) of specific, bite-sized steps required to manually verify the functionality."
end
+2
View File
@@ -14,6 +14,8 @@
# EXAMPLE
# cleanup
function cleanup --description 'Log orphans to ~/.removed_orphans and remove them'
__fish_help_header (status current-function) $argv; and return 0
set -l orphans (pacman -Qtdq)
if test -n "$orphans"
echo "📝 Logging orphans to ~/.removed_orphans..."
+2
View File
@@ -36,6 +36,8 @@
# EXAMPLE
# fast
function fast --description 'Placeholder for future fast utility'
__fish_help_header (status current-function) $argv; and return 0
# ANSI Escape Codes (Standard 16-color palette)
set -l bold "\e[1m"
set -l italic "\e[3m"
+2
View File
@@ -24,6 +24,8 @@
# fc
# fc git
function fc --description 'Edit and execute the last command (Bash-style fc)'
__fish_help_header (status current-function) $argv; and return 0
set -l tmpfile (mktemp /tmp/fish_fc.XXXXXX).fish
if count $argv >/dev/null
+7
View File
@@ -66,6 +66,13 @@ function fish-deps --description 'Manage fish shell dependencies'
_fish_deps_status
case install
_fish_deps_install $flags
case -h --help
# Reuse the existing menu rather than the header renderer: it
# is richer, and it is already the text the unknown-subcommand
# path prints. Previously --help fell into `case '*'` and
# exited 1 with "Unknown subcommand: --help".
__fish_deps_help
return 0
case update
_fish_deps_update
case sync
+2
View File
@@ -14,6 +14,8 @@
# EXAMPLE
# fzf-update
function fzf-update --description 'Install or upgrade fzf from git HEAD'
__fish_help_header (status current-function) $argv; and return 0
if test -d ~/.fzf
echo "Updating fzf..."
git -C ~/.fzf pull --ff-only
+2
View File
@@ -14,6 +14,8 @@
# EXAMPLE
# gip
function gip --description 'Show all public IP addresses'
__fish_help_header (status current-function) $argv; and return 0
echo -n "IPv4: "
curl -4 -s --max-time 2 https://icanhazip.com || echo "Not detected"
echo -n "IPv6: "
+2
View File
@@ -13,5 +13,7 @@
# EXAMPLE
# gip4
function gip4 --wraps='curl' --description 'Get public IPv4 address'
__fish_help_header (status current-function) $argv; and return 0
curl -4 -s https://icanhazip.com
end
+2
View File
@@ -21,6 +21,8 @@
# EXAMPLE
# gip6
function gip6 --description 'Get public IPv6 address'
__fish_help_header (status current-function) $argv; and return 0
# Use -6 to force IPv6 and --fail to catch network errors
set -l ip (curl -6 -s --fail https://icanhazip.com 2>/dev/null)
+2
View File
@@ -17,6 +17,8 @@
# EXAMPLE
# hist
function hist --description 'Search fish history and put it in the prompt'
__fish_help_header (status current-function) $argv; and return 0
# Opinionated guard (C4): integrations disabled
if not __fish_config_op_enabled (status current-function)
set -l c_err (set_color red)
+2
View File
@@ -17,6 +17,8 @@
# EXAMPLE
# lD ~/projects
function lD --description 'List directories only'
__fish_help_header (status current-function) $argv; and return 0
if which eza >/dev/null 2>&1
eza --only-dirs --long --icons --color=auto --hyperlink $argv
else if which lsd >/dev/null 2>&1
+2
View File
@@ -17,6 +17,8 @@
# EXAMPLE
# ld
function ld --description 'Run lazydocker on the current Docker context'
__fish_help_header (status current-function) $argv; and return 0
if not type -q docker
echo "ld: docker is not installed" >&2
return 1
+2
View File
@@ -16,6 +16,8 @@
# EXAMPLE
# limine-edit
function limine-edit --description 'Safely edit and re-verify Limine configuration'
__fish_help_header (status current-function) $argv; and return 0
# 1. Open the config with sudoedit
sudoedit /boot/limine.conf
+2
View File
@@ -13,5 +13,7 @@
# EXAMPLE
# lock
function lock --wraps='loginctl' --description 'alias lock=loginctl'
__fish_help_header (status current-function) $argv; and return 0
loginctl lock-session
end
+2
View File
@@ -17,6 +17,8 @@
# EXAMPLE
# lsr ~/projects
function lsr --description 'Reversed time-sorted listing'
__fish_help_header (status current-function) $argv; and return 0
if which eza >/dev/null 2>&1
eza --oneline --sort=modified --reverse --icons --color=auto --hyperlink $argv
else if which lsd >/dev/null 2>&1
+2
View File
@@ -17,6 +17,8 @@
# EXAMPLE
# lss ~/downloads
function lss --description 'Size-sorted listing'
__fish_help_header (status current-function) $argv; and return 0
if which eza >/dev/null 2>&1
eza --oneline --long --all --sort=size --icons --color=auto --hyperlink --color-scale=size --color-scale-mode=gradient $argv
else if which lsd >/dev/null 2>&1
+2
View File
@@ -17,6 +17,8 @@
# EXAMPLE
# lstree ~/projects/myapp
function lstree --description 'Full recursive tree listing'
__fish_help_header (status current-function) $argv; and return 0
if which eza >/dev/null 2>&1
eza --tree --icons --color=auto --hyperlink=auto $argv
else if which lsd >/dev/null 2>&1
+2
View File
@@ -17,6 +17,8 @@
# EXAMPLE
# lt ~/projects
function lt --description 'Tree listing, depth 2'
__fish_help_header (status current-function) $argv; and return 0
if which eza >/dev/null 2>&1
eza --tree --level=2 --icons --color=auto --hyperlink $argv
else if which lsd >/dev/null 2>&1
+2
View File
@@ -18,6 +18,8 @@
# EXAMPLE
# ltr ~/projects
function ltr --description 'Reversed time-sorted listing'
__fish_help_header (status current-function) $argv; and return 0
if which eza >/dev/null 2>&1
eza --long --all --sort=modified --icons --hyperlink --color=auto --color-scale=age --color-scale-mode=gradient $argv
else if which lsd >/dev/null 2>&1
+2
View File
@@ -17,6 +17,8 @@
# EXAMPLE
# lx ~/projects
function lx --description 'Extension-sorted listing'
__fish_help_header (status current-function) $argv; and return 0
if which eza >/dev/null 2>&1
eza --long --all --sort=extension --icons --color=auto --hyperlink $argv
else if which lsd >/dev/null 2>&1
+2
View File
@@ -19,6 +19,8 @@
# EXAMPLE
# parur
function parur --description 'Interactively search and remove an installed package using fzf'
__fish_help_header (status current-function) $argv; and return 0
set -l aur ""
if type -q paru
set aur paru
+2
View File
@@ -21,6 +21,8 @@
# EXAMPLE
# poke ~/projects/new/src/main.fish
function poke --description 'touch with automatic parent directory creation'
__fish_help_header (status current-function) $argv; and return 0
if test (count $argv) -eq 0
echo (set_color red)"poke: no file specified"(set_color normal) >&2
return 1
+2
View File
@@ -14,5 +14,7 @@
# EXAMPLE
# ports
function ports --wraps='sudo' --description 'Show active network listeners'
__fish_help_header (status current-function) $argv; and return 0
sudo lsof -iTCP -sTCP:LISTEN -P -n
end
+2
View File
@@ -19,6 +19,8 @@
# qr "https://example.com"
# echo "hello" | qr
function qr --description 'Generate a QR code from text or pipe'
__fish_help_header (status current-function) $argv; and return 0
if type -q qrencode
if set -q argv[1]
echo $argv | qrencode -t utf8
+2
View File
@@ -24,6 +24,8 @@
# sbver
# sbver --brief
function sbver --description 'Verifies Secure Boot status of EFI binaries using sbctl'
__fish_help_header (status current-function) $argv; and return 0
if not type -q sbctl
echo "Error: 'sbctl' is not installed."
return 1
+2
View File
@@ -14,6 +14,8 @@
# EXAMPLE
# screensleep
function screensleep --description 'Turn off the display using KDE PowerDevil'
__fish_help_header (status current-function) $argv; and return 0
# Optional: 1-second delay to ensure no keystrokes wake it immediately
sleep 1
busctl --user call \
+4
View File
@@ -29,6 +29,10 @@
# split
# split -v nvim README.md
function split --description 'Run a command in a new terminal split'
# -h is --horizontal here (see this function's own ARGUMENTS),
# so only the long form may reach the renderer.
test "$argv[1]" = --help; and __fish_help_header (status current-function) --help; and return 0
# Opinionated guard (C4): integrations disabled
if not __fish_config_op_enabled (status current-function)
set -l c_err (set_color red)
+2
View File
@@ -24,6 +24,8 @@
# EXAMPLE
# spwin
function spwin --wraps='~/.config/kitty/spawn-window.sh' --description 'spawn window in kitty or wezterm'
__fish_help_header (status current-function) $argv; and return 0
# Opinionated guard (C4): integrations disabled
if not __fish_config_op_enabled (status current-function)
set -l c_err (set_color red)
+2
View File
@@ -14,6 +14,8 @@
# EXAMPLE
# steam-dl
function steam-dl --description 'Run Steam while inhibiting system sleep'
__fish_help_header (status current-function) $argv; and return 0
echo "Inhibiting sleep while Steam downloads..."
systemd-inhibit --why="Active Download" --who="User" --what=idle:sleep steam
end
+2
View File
@@ -19,6 +19,8 @@
# EXAMPLE
# sudo-toggle
function sudo-toggle --description 'Toggle sudo password requirement on/off'
__fish_help_header (status current-function) $argv; and return 0
# Check the file size using sudo stat to see if our bypass rule is active
set -l file_size (sudo stat -c %s /etc/sudoers.d/nofail-toggle 2>/dev/null)
+2
View File
@@ -15,6 +15,8 @@
# EXAMPLE
# swapstat
function swapstat --description 'View colorized zRAM and swappiness status'
__fish_help_header (status current-function) $argv; and return 0
set -l swappiness (sysctl -n vm.swappiness)
set -l zdata (zramctl --bytes --noheadings --output DATA,TOTAL /dev/zram0 2>/dev/null)
+2
View File
@@ -25,6 +25,8 @@
# EXAMPLE
# tab
function tab --description 'Spawn a new tab in the current terminal'
__fish_help_header (status current-function) $argv; and return 0
# Opinionated guard (C4): integrations disabled
if not __fish_config_op_enabled (status current-function)
set -l c_err (set_color red)
+2
View File
@@ -14,6 +14,8 @@
# EXAMPLE
# tmux-clean
function tmux-clean --description 'Kill all tmux sessions except the current one'
__fish_help_header (status current-function) $argv; and return 0
# Get a list of all session names that are NOT currently attached
set sessions (tmux list-sessions -F '#{session_name} #{session_attached}' | string match -rv ' 1$' | string split -f1 ' ')
+2
View File
@@ -21,6 +21,8 @@
# EXAMPLE
# upgrade
function upgrade --description 'Full system upgrade via paru or yay'
__fish_help_header (status current-function) $argv; and return 0
# Opinionated guard (C4): integrations disabled
if not __fish_config_op_enabled (status current-function)
set -l c_err (set_color red)
+2
View File
@@ -22,6 +22,8 @@
# EXAMPLE
# wake-lock rsync -avz src/ dest/
function wake-lock --description 'Run a command while inhibiting system sleep'
__fish_help_header (status current-function) $argv; and return 0
if test (count $argv) -eq 0
set -l c_head (set_color --bold cyan)
set -l c_cmd (set_color --bold)
+258
View File
@@ -97,6 +97,264 @@ function test_vault_dir_honors_override
test "$got" = /tmp/vault-override-check
end
# ── Header-driven --help ─────────────────────────────────────────────
# Helper: run `<fn> $argv` in a throwaway fish that can see both $dir and
# the loaded session's function path, so a fixture function can call the
# real __fish_help_header. Paths here are mktemp -d output, never spaced.
function _help_probe --argument-names dir
env TERM=dumb fish --no-config -c \
"set -g fish_function_path $dir $fish_function_path; $argv[2..]"
end
function test_help_renderer
set -l tmp (mktemp -d)
printf '%s\n' \
'# Copyright (C) 2026 Rootiest' \
'' \
'# CATEGORY' \
'# 99-fixture' \
'#' \
'# SYNOPSIS' \
'# fixturefn [options]' \
'#' \
'# DESCRIPTION' \
'# First paragraph.' \
'#' \
'# Second paragraph.' \
'#' \
'# ARGUMENTS' \
'# -x Do the thing' \
'# more Indented continuation' \
'#' \
'# EXAMPLE' \
'# fixturefn -x' \
'function fixturefn' \
' __fish_help_header (status current-function) $argv; and return 0' \
' echo RAN-BODY' \
'end' >$tmp/fixturefn.fish
set -l out (_help_probe $tmp 'fixturefn --help')
set -l code $status
set -l text (string join \n $out)
rm -rf $tmp
set -l failed 0
if test $code -ne 0
echo " renderer exited $code, expected 0"
set failed 1
end
if contains -- RAN-BODY $out
echo " body executed despite --help"
set failed 1
end
if not contains -- USAGE $out
echo " missing USAGE heading (SYNOPSIS should render as USAGE)"
set failed 1
end
if contains -- CATEGORY $out
echo " CATEGORY leaked into the menu"
set failed 1
end
if not string match -q '* more Indented continuation*' -- $text
echo " nested ARGUMENTS indentation lost"
set failed 1
end
# Index-based, not a glob: fish's `string match` glob `*` does not
# span newlines, so a pattern straddling two lines silently never
# matches and the assertion would pass for the wrong reason.
set -l i (contains -i -- " First paragraph." $out)
if test -z "$i"
echo " DESCRIPTION body missing entirely"
set failed 1
else
# Indices hoisted: a command substitution inside a quoted index
# ("$out[(math ...)]") is a fish parse error, not an expansion.
set -l gap (math $i + 1)
set -l nxt (math $i + 2)
if test -n "$out[$gap]"
echo " multi-paragraph DESCRIPTION lost its blank line"
set failed 1
else if test "$out[$nxt]" != " Second paragraph."
echo " second paragraph missing after the blank"
set failed 1
end
end
test $failed -eq 0
end
function test_help_renderer_degrades_safely
# The renderer must return 1 ONLY when argv[1] is not a help flag.
# A missing or label-less header must still print and exit 0, because
# returning 1 hands control back to the caller's body -- and the body
# of upgrade(1) is `paru -Syu --noconfirm`.
set -l tmp (mktemp -d)
printf '%s\n' \
'function headerless' \
' __fish_help_header (status current-function) $argv; and return 0' \
" touch $tmp/BODY-RAN" \
'end' >$tmp/headerless.fish
# A comment run carrying no `# LABEL` line at all.
printf '%s\n' \
'# just an ordinary comment, no labels here' \
'function malformed' \
' __fish_help_header (status current-function) $argv; and return 0' \
" touch $tmp/BODY-RAN" \
'end' >$tmp/malformed.fish
set -l failed 0
for fn in headerless malformed
set -l out (_help_probe $tmp "$fn --help")
set -l code $status
if test $code -ne 0
echo " $fn --help exited $code, expected 0"
set failed 1
end
if test (count $out) -eq 0
echo " $fn --help printed nothing"
set failed 1
end
if not contains -- $fn $out
echo " $fn --help did not name the function"
set failed 1
end
if test -e $tmp/BODY-RAN
echo " $fn executed its body despite --help"
set failed 1
rm -f $tmp/BODY-RAN
end
end
# The inverse: no help flag must return 1 and let the body run.
_help_probe $tmp headerless >/dev/null 2>&1
if not test -e $tmp/BODY-RAN
echo " body did NOT run when no help flag was passed"
set failed 1
end
rm -rf $tmp
# Explicit, never a trailing `if`: standing gotcha #5 -- an if with no
# branch taken resolves $status to 0 and the test would pass silently.
test $failed -eq 0
end
function test_help_never_executes_destructive_path
# These eight ignore $argv entirely, so before the header-driven help
# landed, `upgrade --help` ran `paru -Syu --noconfirm`. The check has
# to prove --help does NOT reach the destructive path *without* ever
# running it: every external binary the eight can reach is shadowed by
# a recording stub on PATH, and the recorder must stay empty.
#
# WARNING: a silent pass here means a MISSING STUB, not success. If a
# function shows neither an EXECUTED line nor its own help, its
# command is absent from the stub list below -- add it. A test that
# cannot fail proves nothing about a body that runs sudo pacman -Rns.
set -l root (realpath (dirname (status filename))/..)
set -l tmp (mktemp -d)
mkdir -p $tmp/bin
set -l log $tmp/invoked.log
touch $log
for b in sudo pacman paru yay loginctl busctl tmux systemd-inhibit \
sudoedit limine-enroll-config limine-mkinitcpio sbctl git fzf steam
printf '#!/bin/sh\necho "$(basename "$0") $*" >> %s\n' $log >$tmp/bin/$b
chmod +x $tmp/bin/$b
end
set -l failed 0
for fn in cleanup fzf-update limine-edit lock screensleep sudo-toggle \
tmux-clean upgrade
set -l out (env TERM=dumb PATH="$tmp/bin:$PATH" HOME=$tmp \
fish --no-config -c \
"set -g fish_function_path $root/functions $fish_function_path
$fn --help" 2>/dev/null)
set -l code $status
if test $code -ne 0
echo " $fn --help exited $code, expected 0"
set failed 1
end
if not contains -- $fn $out
echo " $fn --help did not print its own help"
set failed 1
end
set -l ran (string trim -- (command cat $log))
if test -n "$ran"
echo " $fn --help EXECUTED: $ran"
set failed 1
end
echo -n "" >$log
end
rm -rf $tmp
test $failed -eq 0
end
# Functions published in the manual that are exempt from the -h/--help
# rule. Rationale per entry: AGENTS/specs/2026-09-07-header-driven-help-design.md
# §4. This array is the ONLY machine-readable copy of the exempt set.
#
# EXEMPT-A -- shadows a same-named binary, or forwards $argv to one named
# tool that owns its own --help. Intercepting would hide that tool's help,
# and for the C1-guarded shadows it also breaks the disabled-fallback
# contract, where the bare tool is supposed to answer.
set -g __help_exempt \
agy antigravity-ide bash cat cdi cffetch cheat claude clone clonet \
config-toggle copy docker du dusize fast-cli ffetch gitui gitup jr \
joplin less ls mkdir mv paste ping rawfish rg rm search ssh top \
view yt-dlp
# EXEMPT-B -- invoked by fish, never typed by a user.
set -a __help_exempt fish_prompt fish_right_prompt fish_mode_prompt \
sponge_filter_secrets
function test_every_user_facing_function_has_help
set -l root (realpath (dirname (status filename))/..)
set -l failed 0
set -l published
for f in $root/functions/*.fish
set -l lines (string split \n -- (command cat $f))
# Published == carries a `# CATEGORY` block, matching
# manualtools.parse_functions.
contains -- "# CATEGORY" (string trim -- $lines); or continue
# Resolve the real defined name; the file stem can disagree
# (dops.fish defines `docker` -- see JOB-BRIEF-FINDINGS.md §1).
set -l name (string match -rg '^\s*function\s+(\S+)' -- $lines)[1]
test -n "$name"; or continue
set name (string trim -c "'\"" -- $name)
string match -q '_*' -- $name; and continue
set -a published $name
contains -- $name $__help_exempt; and continue
# Body == everything from the `function` line down, comment lines
# dropped, so a header that merely mentions --help cannot pass.
set -l body
set -l in_body 0
for l in $lines
test $in_body -eq 1; or string match -qr '^\s*function\s' -- $l; and set in_body 1
test $in_body -eq 1; or continue
string match -qr '^\s*#' -- $l; and continue
set -a body $l
end
if not string match -qr -- '__fish_help_header|_flag_help|h/help|--help' \
(string join \n -- $body)
echo " $name: no -h/--help handling and not in \$__help_exempt"
set failed 1
end
end
# Guard against a stale exempt list: every exempt name must still be a
# published function. Catches renames and deletions.
for e in $__help_exempt
if not contains -- $e $published
echo " \$__help_exempt lists '$e', which is no longer published"
set failed 1
end
end
test $failed -eq 0
end
function functional_test_main
set -l names (functions -a | string match 'test_*' | sort)
set -l failed 0