dotcli: Support nested folder commands.

This commit is contained in:
2026-07-05 17:52:03 -04:00
parent 0d6f0d2fc9
commit 8ce46a7b98
5 changed files with 62 additions and 10 deletions

View File

@@ -17,8 +17,15 @@ This is pure prefactoring: no KDE-specific behavior is introduced here.
## Acceptance criteria ## Acceptance criteria
- [ ] `dot help` lists a subcommand that lives at `commands/<name>/<name>.fish` - [x] `dot help` lists a subcommand that lives at `commands/<name>/<name>.fish`
- [ ] `dot <name>` sources and dispatches to `commands/<name>/<name>.fish`'s `_dot_<name>` function - [x] `dot <name>` sources and dispatches to `commands/<name>/<name>.fish`'s `_dot_<name>` function
- [ ] Tab-completion (`__dot_custom_subcommands`) lists a nested-directory subcommand - [x] Tab-completion (`__dot_custom_subcommands`) lists a nested-directory subcommand
- [ ] Existing flat-file subcommands (`dot install`) are still discovered and dispatched correctly - [x] Existing flat-file subcommands (`dot install`) are still discovered and dispatched correctly
- [ ] `tests/dot.fish` covers a nested-directory dummy command dispatching correctly, alongside the existing flat-file dispatch case - [x] `tests/dot.fish` covers a nested-directory dummy command dispatching correctly, alongside the existing flat-file dispatch case
## Implementation Notes
- The dispatch check in `dot.fish` tries the flat file first, then falls back to `commands/<name>/<name>.fish` — a flat file always wins if both somehow exist for the same name.
- The nested-directory scan requires the file basename to match its containing directory's name (`commands/foo/foo.fish`), not just any `.fish` file one level deep — this matches the acceptance criteria's exact convention and avoids misclassifying a stray companion file (e.g. a `.py` helper) as its own subcommand.
- Tab-completion's nested-directory listing was verified manually (sourcing `completions/dot.fish` and calling `__dot_custom_subcommands` directly) rather than via an automated test — `tests/dot.fish` has no existing infrastructure for testing completions at all, even for pre-existing flat commands, so adding one here would be out of scope for this prefactoring task.
- Updated `CLAUDE.md`'s "Architecture" and "Adding a subcommand" sections to document the new nested-directory convention, since it previously only described the flat-file dispatch contract.

View File

@@ -62,6 +62,7 @@ shared: fish only autoloads a function from a file named after that function,
so a helper defined inside `dot.fish` would be undefined if tab-completion so a helper defined inside `dot.fish` would be undefined if tab-completion
ran before `dot` had ever been sourced in the session. Keep both copies in ran before `dot` had ever been sourced in the session. Keep both copies in
sync when the listing logic changes. sync when the listing logic changes.
Both copies also glob one directory level deeper, matching `~/.config/dot/commands/<name>/<name>.fish`, so a subcommand's companion file (e.g. a Python helper) can live alongside it in its own directory.
`dot init`: `dot init`:
@@ -76,7 +77,9 @@ sync when the listing logic changes.
### Adding a subcommand ### Adding a subcommand
Beyond `init`, `dot` looks for `~/.config/dot/commands/<name>.fish`, sources Beyond `init`, `dot` looks for `~/.config/dot/commands/<name>.fish`, sources
it, and calls `_dot_<name>`. These files are deliberately kept out of it, and calls `_dot_<name>`.
A subcommand needing a companion file can instead live nested one level deeper, as `~/.config/dot/commands/<name>/<name>.fish` — both layouts dispatch identically.
These files are deliberately kept out of
`~/.config/fish/functions/` (fish's autoload path) so they never become `~/.config/fish/functions/` (fish's autoload path) so they never become
independently invokable top-level commands or clutter tab-completion outside independently invokable top-level commands or clutter tab-completion outside
of `dot` itself. of `dot` itself.

View File

@@ -73,6 +73,19 @@ end" >$HOME/.config/dot/commands/mark.fish
dot mark >/dev/null 2>&1 dot mark >/dev/null 2>&1
@test "dispatches to a command file under ~/.config/dot/commands/" (cat $marker) = marked @test "dispatches to a command file under ~/.config/dot/commands/" (cat $marker) = marked
# --- dispatches to a nested commands/<name>/<name>.fish, same as a flat file
set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1
mkdir -p $HOME/.config/dot/commands/nested
set -l nested_marker (mktemp)
echo "function _dot_nested
echo nested-marked >$nested_marker
end" >$HOME/.config/dot/commands/nested/nested.fish
dot nested >/dev/null 2>&1
@test "dispatches to a nested commands/<name>/<name>.fish" (cat $nested_marker) = nested-marked
# --- dot help --- # --- dot help ---
set -gx HOME (mktemp -d) set -gx HOME (mktemp -d)
dot init --url $remote >/dev/null 2>&1 dot init --url $remote >/dev/null 2>&1
@@ -93,6 +106,14 @@ end" >$HOME/.config/dot/commands/mark.fish
set -l help_with_custom (dot help) set -l help_with_custom (dot help)
@test "dot help lists custom commands found under ~/.config/dot/commands/" (string match -q '*mark*' -- $help_with_custom; echo $status) -eq 0 @test "dot help lists custom commands found under ~/.config/dot/commands/" (string match -q '*mark*' -- $help_with_custom; echo $status) -eq 0
mkdir -p $HOME/.config/dot/commands/nested
echo "function _dot_nested
echo nested
end" >$HOME/.config/dot/commands/nested/nested.fish
set -l help_with_nested (dot help)
@test "dot help lists a nested-directory subcommand" (string match -q '*nested*' -- $help_with_nested; echo $status) -eq 0
# --- dot install --- # --- dot install ---
# pacman and sudo are faked out via a bin dir prepended to PATH: sudo just # pacman and sudo are faked out via a bin dir prepended to PATH: sudo just
# execs its arguments, and pacman logs each invocation to $PACMAN_LOG (one # execs its arguments, and pacman logs each invocation to $PACMAN_LOG (one

View File

@@ -2,6 +2,13 @@ function __dot_custom_subcommands
echo init echo init
echo help echo help
path basename $HOME/.config/dot/commands/*.fish 2>/dev/null | path change-extension '' path basename $HOME/.config/dot/commands/*.fish 2>/dev/null | path change-extension ''
for d in $HOME/.config/dot/commands/*/
test -d $d; or continue
set -l name (path basename $d)
test -f $d$name.fish; or continue
echo $name
end
end end
complete -c dot -n __fish_use_subcommand -a "(__dot_custom_subcommands)" complete -c dot -n __fish_use_subcommand -a "(__dot_custom_subcommands)"

View File

@@ -14,11 +14,18 @@ function dot --wraps=git --description 'Manage dotfiles via a bare repo checked
set -l commands_dir $HOME/.config/dot/commands set -l commands_dir $HOME/.config/dot/commands
set -l command_file $commands_dir/$argv[1].fish set -l command_file $commands_dir/$argv[1].fish
set -l nested_command_file $commands_dir/$argv[1]/$argv[1].fish
if test -n "$argv[1]" -a -f "$command_file" if test -n "$argv[1]"
source $command_file if test -f "$command_file"
_dot_$argv[1] $argv[2..-1] source $command_file
return $status _dot_$argv[1] $argv[2..-1]
return $status
else if test -f "$nested_command_file"
source $nested_command_file
_dot_$argv[1] $argv[2..-1]
return $status
end
end end
git --git-dir=$dotfiles_dir --work-tree=$HOME $argv git --git-dir=$dotfiles_dir --work-tree=$HOME $argv
@@ -109,6 +116,13 @@ Commands:
echo " "(path basename $f | path change-extension '') echo " "(path basename $f | path change-extension '')
end end
for d in $HOME/.config/dot/commands/*/
test -d $d; or continue
set -l name (path basename $d)
test -f $d$name.fish; or continue
echo " $name"
end
echo " echo "
Run 'dot <command> help' for flags on a specific command. Run 'dot <command> help' for flags on a specific command.