diff --git a/completions/bash/am.bash b/completions/bash/am.bash index cb5bda84..bc82bffa 100644 --- a/completions/bash/am.bash +++ b/completions/bash/am.bash @@ -25,9 +25,6 @@ _am() { am,help) cmd="am__subcmd__help" ;; - am,hook) - cmd="am__subcmd__hook" - ;; am,import) cmd="am__subcmd__import" ;; @@ -40,9 +37,6 @@ _am() { am,profile) cmd="am__subcmd__profile" ;; - am,reload) - cmd="am__subcmd__reload" - ;; am,remove) cmd="am__subcmd__remove" ;; @@ -55,6 +49,9 @@ _am() { am,status) cmd="am__subcmd__status" ;; + am,sync) + cmd="am__subcmd__sync" + ;; am,trust) cmd="am__subcmd__trust" ;; @@ -76,9 +73,6 @@ _am() { am__subcmd__help,help) cmd="am__subcmd__help__subcmd__help" ;; - am__subcmd__help,hook) - cmd="am__subcmd__help__subcmd__hook" - ;; am__subcmd__help,import) cmd="am__subcmd__help__subcmd__import" ;; @@ -91,9 +85,6 @@ _am() { am__subcmd__help,profile) cmd="am__subcmd__help__subcmd__profile" ;; - am__subcmd__help,reload) - cmd="am__subcmd__help__subcmd__reload" - ;; am__subcmd__help,remove) cmd="am__subcmd__help__subcmd__remove" ;; @@ -106,6 +97,9 @@ _am() { am__subcmd__help,status) cmd="am__subcmd__help__subcmd__status" ;; + am__subcmd__help,sync) + cmd="am__subcmd__help__subcmd__sync" + ;; am__subcmd__help,trust) cmd="am__subcmd__help__subcmd__trust" ;; @@ -167,7 +161,7 @@ _am() { case "${cmd}" in am) - opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload help" + opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -229,7 +223,7 @@ _am() { return 0 ;; am__subcmd__help) - opts="add remove ls status profile init setup use tui export import share trust untrust hook reload help" + opts="add remove ls status profile init setup use tui export import share trust untrust sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -284,20 +278,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__hook) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__help__subcmd__import) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -410,7 +390,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__reload) + am__subcmd__help__subcmd__remove) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -424,7 +404,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__remove) + am__subcmd__help__subcmd__setup) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -438,7 +418,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__setup) + am__subcmd__help__subcmd__share) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -452,7 +432,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__share) + am__subcmd__help__subcmd__status) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -466,7 +446,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__status) + am__subcmd__help__subcmd__sync) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -536,20 +516,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__hook) - opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__import) opts="-l -g -p -b -y -h -V --local --global --profile --all --base64 --yes --trust --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then @@ -762,20 +728,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__reload) - opts="-h -V --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__remove) opts="-p -l -g -h -V --profile --local --global --sub --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then @@ -852,6 +804,20 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + am__subcmd__sync) + opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; am__subcmd__trust) opts="-h -V --help --version" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then diff --git a/completions/fish/am.fish b/completions/fish/am.fish index c55a2fe1..6b47fc01 100644 --- a/completions/fish/am.fish +++ b/completions/fish/am.fish @@ -40,8 +40,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases fr complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -125,28 +124,25 @@ complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print v complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/completions/powershell/_am.ps1 b/completions/powershell/_am.ps1 index 88304628..94c27c54 100644 --- a/completions/powershell/_am.ps1 +++ b/completions/powershell/_am.ps1 @@ -39,8 +39,7 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [CompletionResult]::new('share', 'share', [CompletionResultType]::ParameterValue, 'Generate a share command for posting aliases to a pastebin service') [CompletionResult]::new('trust', 'trust', [CompletionResultType]::ParameterValue, 'Review and trust the project .aliases file in the current directory') [CompletionResult]::new('untrust', 'untrust', [CompletionResultType]::ParameterValue, 'Remove trust for the project .aliases file in the current directory') - [CompletionResult]::new('hook', 'hook', [CompletionResultType]::ParameterValue, 'Internal: called by the cd hook to load/unload project aliases') - [CompletionResult]::new('reload', 'reload', [CompletionResultType]::ParameterValue, 'Internal: called by the am wrapper to reload profile aliases after switching') + [CompletionResult]::new('sync', 'sync', [CompletionResultType]::ParameterValue, 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)') [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break } @@ -261,7 +260,7 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') break } - 'am;hook' { + 'am;sync' { [CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') [CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') @@ -270,13 +269,6 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') break } - 'am;reload' { - [CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help') - [CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version') - [CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version') - break - } 'am;help' { [CompletionResult]::new('add', 'add', [CompletionResultType]::ParameterValue, 'Add a new alias') [CompletionResult]::new('remove', 'remove', [CompletionResultType]::ParameterValue, 'Remove an alias') @@ -292,8 +284,7 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [CompletionResult]::new('share', 'share', [CompletionResultType]::ParameterValue, 'Generate a share command for posting aliases to a pastebin service') [CompletionResult]::new('trust', 'trust', [CompletionResultType]::ParameterValue, 'Review and trust the project .aliases file in the current directory') [CompletionResult]::new('untrust', 'untrust', [CompletionResultType]::ParameterValue, 'Remove trust for the project .aliases file in the current directory') - [CompletionResult]::new('hook', 'hook', [CompletionResultType]::ParameterValue, 'Internal: called by the cd hook to load/unload project aliases') - [CompletionResult]::new('reload', 'reload', [CompletionResultType]::ParameterValue, 'Internal: called by the am wrapper to reload profile aliases after switching') + [CompletionResult]::new('sync', 'sync', [CompletionResultType]::ParameterValue, 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)') [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break } @@ -355,10 +346,7 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { 'am;help;untrust' { break } - 'am;help;hook' { - break - } - 'am;help;reload' { + 'am;help;sync' { break } 'am;help;help' { diff --git a/completions/zsh/_am b/completions/zsh/_am index d3bcd4fe..e9299d8c 100644 --- a/completions/zsh/_am +++ b/completions/zsh/_am @@ -293,7 +293,7 @@ _arguments "${_arguments_options[@]}" : \ '--version[Print version]' \ && ret=0 ;; -(hook) +(sync) _arguments "${_arguments_options[@]}" : \ '-q[Suppress info and warning messages (still unloads/loads aliases)]' \ '--quiet[Suppress info and warning messages (still unloads/loads aliases)]' \ @@ -304,15 +304,6 @@ _arguments "${_arguments_options[@]}" : \ ':shell:(bash brush fish powershell zsh)' \ && ret=0 ;; -(reload) -_arguments "${_arguments_options[@]}" : \ -'-h[Print help]' \ -'--help[Print help]' \ -'-V[Print version]' \ -'--version[Print version]' \ -':shell:(bash brush fish powershell zsh)' \ -&& ret=0 -;; (help) _arguments "${_arguments_options[@]}" : \ ":: :_am__subcmd__help_commands" \ @@ -409,11 +400,7 @@ _arguments "${_arguments_options[@]}" : \ _arguments "${_arguments_options[@]}" : \ && ret=0 ;; -(hook) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(reload) +(sync) _arguments "${_arguments_options[@]}" : \ && ret=0 ;; @@ -447,8 +434,7 @@ _am_commands() { 'share:Generate a share command for posting aliases to a pastebin service' \ 'trust:Review and trust the project .aliases file in the current directory' \ 'untrust:Remove trust for the project .aliases file in the current directory' \ -'hook:Internal\: called by the cd hook to load/unload project aliases' \ -'reload:Internal\: called by the am wrapper to reload profile aliases after switching' \ +'sync:Internal\: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'am commands' commands "$@" @@ -480,8 +466,7 @@ _am__subcmd__help_commands() { 'share:Generate a share command for posting aliases to a pastebin service' \ 'trust:Review and trust the project .aliases file in the current directory' \ 'untrust:Remove trust for the project .aliases file in the current directory' \ -'hook:Internal\: called by the cd hook to load/unload project aliases' \ -'reload:Internal\: called by the am wrapper to reload profile aliases after switching' \ +'sync:Internal\: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'am help commands' commands "$@" @@ -501,11 +486,6 @@ _am__subcmd__help__subcmd__help_commands() { local commands; commands=() _describe -t commands 'am help help commands' commands "$@" } -(( $+functions[_am__subcmd__help__subcmd__hook_commands] )) || -_am__subcmd__help__subcmd__hook_commands() { - local commands; commands=() - _describe -t commands 'am help hook commands' commands "$@" -} (( $+functions[_am__subcmd__help__subcmd__import_commands] )) || _am__subcmd__help__subcmd__import_commands() { local commands; commands=() @@ -551,11 +531,6 @@ _am__subcmd__help__subcmd__profile__subcmd__use_commands() { local commands; commands=() _describe -t commands 'am help profile use commands' commands "$@" } -(( $+functions[_am__subcmd__help__subcmd__reload_commands] )) || -_am__subcmd__help__subcmd__reload_commands() { - local commands; commands=() - _describe -t commands 'am help reload commands' commands "$@" -} (( $+functions[_am__subcmd__help__subcmd__remove_commands] )) || _am__subcmd__help__subcmd__remove_commands() { local commands; commands=() @@ -576,6 +551,11 @@ _am__subcmd__help__subcmd__status_commands() { local commands; commands=() _describe -t commands 'am help status commands' commands "$@" } +(( $+functions[_am__subcmd__help__subcmd__sync_commands] )) || +_am__subcmd__help__subcmd__sync_commands() { + local commands; commands=() + _describe -t commands 'am help sync commands' commands "$@" +} (( $+functions[_am__subcmd__help__subcmd__trust_commands] )) || _am__subcmd__help__subcmd__trust_commands() { local commands; commands=() @@ -596,11 +576,6 @@ _am__subcmd__help__subcmd__use_commands() { local commands; commands=() _describe -t commands 'am help use commands' commands "$@" } -(( $+functions[_am__subcmd__hook_commands] )) || -_am__subcmd__hook_commands() { - local commands; commands=() - _describe -t commands 'am hook commands' commands "$@" -} (( $+functions[_am__subcmd__import_commands] )) || _am__subcmd__import_commands() { local commands; commands=() @@ -683,11 +658,6 @@ _am__subcmd__profile__subcmd__use_commands() { local commands; commands=() _describe -t commands 'am profile use commands' commands "$@" } -(( $+functions[_am__subcmd__reload_commands] )) || -_am__subcmd__reload_commands() { - local commands; commands=() - _describe -t commands 'am reload commands' commands "$@" -} (( $+functions[_am__subcmd__remove_commands] )) || _am__subcmd__remove_commands() { local commands; commands=() @@ -708,6 +678,11 @@ _am__subcmd__status_commands() { local commands; commands=() _describe -t commands 'am status commands' commands "$@" } +(( $+functions[_am__subcmd__sync_commands] )) || +_am__subcmd__sync_commands() { + local commands; commands=() + _describe -t commands 'am sync commands' commands "$@" +} (( $+functions[_am__subcmd__trust_commands] )) || _am__subcmd__trust_commands() { local commands; commands=() diff --git a/crates/am-tui/src/tree.rs b/crates/am-tui/src/tree.rs index fbe6f864..c3342d6c 100644 --- a/crates/am-tui/src/tree.rs +++ b/crates/am-tui/src/tree.rs @@ -130,7 +130,7 @@ fn emit_trie_children( fn collect_prog_names(subcommands: &amoxide::SubcommandSet) -> Vec { let mut names = std::collections::BTreeSet::new(); - for key in subcommands.keys() { + for key in subcommands.as_ref().keys() { if let Some(prog) = key.split(':').next() { names.insert(prog.to_string()); } @@ -597,6 +597,7 @@ mod tests { fn global_subcommand(mut self, key: &str, longs: &[&str]) -> Self { self.config .subcommands + .as_mut() .insert(key.into(), longs.iter().map(|s| s.to_string()).collect()); self } diff --git a/crates/am-tui/src/update/mod.rs b/crates/am-tui/src/update/mod.rs index 2ed73842..d2b3edf2 100644 --- a/crates/am-tui/src/update/mod.rs +++ b/crates/am-tui/src/update/mod.rs @@ -826,6 +826,7 @@ mod tests { .app_model .config .subcommands + .as_mut() .insert("jj:ab".into(), vec!["abandon".into()]); model.rebuild_tree(); let idx = model @@ -1007,13 +1008,18 @@ mod tests { }); update(&mut model, TuiMessage::TextInputConfirm); assert_eq!(model.mode, Mode::Normal); - assert!(model.app_model.config.subcommands.contains_key("jj:ab")); + assert!(model + .app_model + .config + .subcommands + .as_ref() + .contains_key("jj:ab")); } fn make_subcmd_model(keys: &[(&str, &[&str])]) -> TuiModel { let mut config = amoxide::Config::default(); for (key, longs) in keys { - config.subcommands.insert( + config.subcommands.as_mut().insert( key.to_string(), longs.iter().map(|s| s.to_string()).collect(), ); @@ -1057,7 +1063,12 @@ mod tests { .unwrap(); model.cursor = idx; update(&mut model, TuiMessage::DeleteItem); - assert!(!model.app_model.config.subcommands.contains_key("jj:ab")); + assert!(!model + .app_model + .config + .subcommands + .as_ref() + .contains_key("jj:ab")); } #[test] diff --git a/crates/am-tui/src/update/profile_actions.rs b/crates/am-tui/src/update/profile_actions.rs index bb04cfc9..a3059bc2 100644 --- a/crates/am-tui/src/update/profile_actions.rs +++ b/crates/am-tui/src/update/profile_actions.rs @@ -49,6 +49,7 @@ pub fn handle(model: &mut TuiModel, msg: TuiMessage) { let keys_to_remove: Vec = { let lib_target = derive_target_from_cursor(model); get_subcommand_set(model, &lib_target) + .as_ref() .keys() .filter(|k| k.starts_with(&prefix)) .cloned() @@ -75,6 +76,7 @@ pub fn handle(model: &mut TuiModel, msg: TuiMessage) { let keys_to_remove: Vec = { let lib_target = derive_target_from_cursor(model); get_subcommand_set(model, &lib_target) + .as_ref() .keys() .filter(|k| k.starts_with(&prog_prefix)) .cloned() diff --git a/crates/am-tui/src/update/transfer.rs b/crates/am-tui/src/update/transfer.rs index 351dc31d..f46b6751 100644 --- a/crates/am-tui/src/update/transfer.rs +++ b/crates/am-tui/src/update/transfer.rs @@ -216,16 +216,21 @@ pub(super) fn is_same_source(id: &AliasId, dest: &MoveDestination) -> bool { pub(super) fn alias_exists_at_dest(model: &TuiModel, id: &AliasId, dest: &MoveDestination) -> bool { match id { AliasId::Subcommand { key, .. } => match dest { - MoveDestination::Global => model.app_model.config.subcommands.contains_key(key), + MoveDestination::Global => model + .app_model + .config + .subcommands + .as_ref() + .contains_key(key), MoveDestination::Project => model .app_model .project_aliases() - .is_some_and(|p| p.subcommands.contains_key(key)), + .is_some_and(|p| p.subcommands.as_ref().contains_key(key)), MoveDestination::Profile(name) => model .app_model .profile_config() .get_profile_by_name(name) - .is_some_and(|p| p.subcommands.contains_key(key)), + .is_some_and(|p| p.subcommands.as_ref().contains_key(key)), }, _ => { let alias_name_str = match id { diff --git a/crates/am-tui/src/view.rs b/crates/am-tui/src/view.rs index 768c2fb5..c0a3e677 100644 --- a/crates/am-tui/src/view.rs +++ b/crates/am-tui/src/view.rs @@ -866,6 +866,7 @@ mod subcommand_render { let mut config = Config::default(); config .subcommands + .as_mut() .insert("jj:ab".into(), vec!["abandon".into()]); let app = amoxide::update::AppModel::new(config, ProfileConfig::default()); let mut model = TuiModel::new().unwrap(); diff --git a/crates/am/src/alias.rs b/crates/am/src/alias.rs index 948f9899..51405f10 100644 --- a/crates/am/src/alias.rs +++ b/crates/am/src/alias.rs @@ -101,14 +101,14 @@ impl AliasSet { } } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(untagged)] pub enum TomlAlias { Command(String), Detailed(AliasDetail), } -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] pub struct AliasDetail { pub command: String, pub description: Option, diff --git a/crates/am/src/app_model.rs b/crates/am/src/app_model.rs index fa78fdae..d1d621b2 100644 --- a/crates/am/src/app_model.rs +++ b/crates/am/src/app_model.rs @@ -262,7 +262,7 @@ impl AppModel { let path = self.project_path_or_create(); let mut project = self.project_aliases().cloned().unwrap_or_default(); for (key, longs) in subcommands { - project.subcommands.insert(key, longs); + project.subcommands.as_mut().insert(key, longs); } project.save(&path)?; let hash = compute_file_hash(&path)?; diff --git a/crates/am/src/bin/am.rs b/crates/am/src/bin/am.rs index 06699f12..7cd01920 100644 --- a/crates/am/src/bin/am.rs +++ b/crates/am/src/bin/am.rs @@ -34,7 +34,7 @@ fn setup_logging() { fn main() -> anyhow::Result<()> { // Guard against recursive invocation during alias scanning. // When `zsh -i -c alias` is spawned to enumerate existing shell aliases it - // sources the user's startup files, which call `am hook` (or `am init`). + // sources the user's startup files, which call `am sync` (or `am init`). // If those calls were allowed to run normally they could trigger another // scan, causing infinite recursion. Exiting here makes `eval "$(...)"` a // no-op, which is safe. @@ -46,10 +46,7 @@ fn main() -> anyhow::Result<()> { let mut model = AppModel::default(); // Don't log for commands whose stdout is eval'd by the shell - if !matches!( - &cli.command, - Commands::Init { .. } | Commands::Hook { .. } | Commands::Reload { .. } - ) { + if !matches!(&cli.command, Commands::Init { .. } | Commands::Sync { .. }) { setup_logging(); } @@ -219,7 +216,7 @@ fn main() -> anyhow::Result<()> { .ok_or_else(|| anyhow::anyhow!("Profile '{name}' not found"))?; if !profile.is_empty() { let alias_count = profile.aliases.iter().count(); - let subcmd_count = profile.subcommands.len(); + let subcmd_count = profile.subcommands.as_ref().len(); let question = match (alias_count, subcmd_count) { (a, 0) => format!( "Profile '{name}' has {a} alias{}. Remove?", @@ -335,7 +332,7 @@ fn main() -> anyhow::Result<()> { let cmd = alias_value.command(); println!(" {:width$} \u{2192} {cmd}", name, width = max_name_len); } - let subcmd_groups = amoxide::subcommand::group_by_program(&project.subcommands); + let subcmd_groups = project.subcommands.group_by_program(); if !subcmd_groups.is_empty() { println!(); for (program, entries) in &subcmd_groups { @@ -371,7 +368,7 @@ fn main() -> anyhow::Result<()> { if answer == Answer::Yes { let result = update(&mut model, Message::Trust)?; execute_effects(&mut model, &result.effects)?; - // The shell wrapper calls `am hook` after this, which loads + // The shell wrapper calls `am sync` after this, which loads // the aliases and shows the load message. } else { let result = update(&mut model, Message::Untrust { forget: false })?; @@ -385,8 +382,7 @@ fn main() -> anyhow::Result<()> { return Ok(()); } Commands::Init { shell, force } => Message::InitShell(shell.clone(), *force), - Commands::Hook { shell, quiet } => Message::Hook(shell.clone(), *quiet), - Commands::Reload { shell } => Message::Reload(shell.clone()), + Commands::Sync { shell, quiet } => Message::Sync(shell.clone(), *quiet), }; let result = update(&mut model, message)?; diff --git a/crates/am/src/cli.rs b/crates/am/src/cli.rs index c74bc8be..7a94a8ad 100644 --- a/crates/am/src/cli.rs +++ b/crates/am/src/cli.rs @@ -128,18 +128,15 @@ pub enum Commands { forget: bool, }, - /// Internal: called by the cd hook to load/unload project aliases + /// Internal: compute and emit the minimal shell ops to sync the shell with + /// the effective merged alias state (global + profile + project). #[command(hide = true)] - Hook { - /// Suppress info and warning messages (still unloads/loads aliases) + Sync { + /// Suppress info and warning messages (still unloads/loads aliases). #[arg(short, long)] quiet: bool, shell: Shell, }, - - /// Internal: called by the am wrapper to reload profile aliases after switching - #[command(hide = true)] - Reload { shell: Shell }, } #[derive(Subcommand)] diff --git a/crates/am/src/config.rs b/crates/am/src/config.rs index c3b1b2dd..a50b9681 100644 --- a/crates/am/src/config.rs +++ b/crates/am/src/config.rs @@ -86,11 +86,12 @@ impl Config { } pub fn add_subcommand(&mut self, key: String, long_subcommands: Vec) { - self.subcommands.insert(key, long_subcommands); + self.subcommands.as_mut().insert(key, long_subcommands); } pub fn remove_subcommand(&mut self, key: &str) -> crate::Result<()> { self.subcommands + .as_mut() .remove(key) .ok_or_else(|| anyhow::anyhow!("Subcommand alias '{key}' not found"))?; Ok(()) @@ -164,19 +165,20 @@ mod tests { let mut config = Config::default(); config .subcommands + .as_mut() .insert("jj:ab".into(), vec!["abandon".into()]); config.save_to(dir.path()).unwrap(); let loaded = Config::load_from(dir.path()).unwrap(); - assert_eq!(loaded.subcommands.len(), 1); - assert_eq!(loaded.subcommands["jj:ab"], vec!["abandon"]); + assert_eq!(loaded.subcommands.as_ref().len(), 1); + assert_eq!(loaded.subcommands.as_ref()["jj:ab"], vec!["abandon"]); } #[test] fn test_add_and_remove_subcommand() { let mut config = Config::default(); config.add_subcommand("jj:ab".into(), vec!["abandon".into()]); - assert_eq!(config.subcommands.len(), 1); + assert_eq!(config.subcommands.as_ref().len(), 1); config.remove_subcommand("jj:ab").unwrap(); assert!(config.subcommands.is_empty()); diff --git a/crates/am/src/display.rs b/crates/am/src/display.rs index cdec6d24..fa629d74 100644 --- a/crates/am/src/display.rs +++ b/crates/am/src/display.rs @@ -23,7 +23,7 @@ fn render_items( aliases: &AliasSet, subcommands: &crate::subcommand::SubcommandSet, ) { - let subcmd_groups = crate::subcommand::group_by_program(subcommands); + let subcmd_groups = subcommands.group_by_program(); // Chain aliases then subcommand groups into one peekable stream. // Each "item" is rendered with ├─ unless peek() returns None (last item). @@ -594,8 +594,9 @@ mod tests { let config: ProfileConfig = ProfileConfig::default(); let mut subs = SubcommandSet::new(); - subs.insert("jj:ab".into(), vec!["abandon".into()]); - subs.insert("jj:b:l".into(), vec!["branch".into(), "list".into()]); + subs.as_mut().insert("jj:ab".into(), vec!["abandon".into()]); + subs.as_mut() + .insert("jj:b:l".into(), vec!["branch".into(), "list".into()]); let output = render_listing(&AliasSet::default(), &subs, &config, &[], None, None); assert!(output.contains("jj (subcommands)")); diff --git a/crates/am/src/env_vars.rs b/crates/am/src/env_vars.rs index 16c36f88..f42ba1b8 100644 --- a/crates/am/src/env_vars.rs +++ b/crates/am/src/env_vars.rs @@ -1,23 +1,15 @@ -/// Tracks globally-loaded alias names (global + active-profile aliases). -/// Value: comma-separated list, e.g. `"gs,ll"`. +/// Tracks effective alias state in the shell (aliases + subcommand wrappers). +/// Value: comma-separated entries of `name|short_hash`, enabling per-entry +/// change detection on sync. pub const AM_ALIASES: &str = "_AM_ALIASES"; -/// Tracks project-level aliases loaded by the cd hook. -/// Value: comma-separated entries of `name|short_hash`, e.g. `"b|a132b21,t|1241ab1"`. -/// The short hash is the first 7 hex chars of the BLAKE3 hash of the alias value, -/// enabling per-alias change detection on reload. -pub const AM_PROJECT_ALIASES: &str = "_AM_PROJECT_ALIASES"; +/// Tracks effective subcommand-key state in the shell. +/// Value: comma-separated entries of `name|short_hash`. +pub const AM_SUBCOMMANDS: &str = "_AM_SUBCOMMANDS"; /// Path of the `.aliases` file currently in scope, used to suppress -/// duplicate hook messages when navigating into subdirectories. +/// duplicate warnings when navigating into subdirectories. pub const AM_PROJECT_PATH: &str = "_AM_PROJECT_PATH"; -/// Set during `zsh -i -c alias` alias-detection scans to prevent recursive -/// `am` invocations from triggering another scan. When present, the `am` -/// binary exits immediately with no output so that `eval "$(...)"` in shell -/// startup scripts is a no-op. +/// Set during shell-scanning to prevent recursive am invocation. pub const AM_DETECTING_ALIASES: &str = "_AM_DETECTING_ALIASES"; - -/// Legacy tracking variable replaced by `AM_ALIASES`. Unset on startup so -/// that old installations do not leave stale state. -pub const AM_PROFILE_ALIASES_LEGACY: &str = "_AM_PROFILE_ALIASES"; diff --git a/crates/am/src/exchange.rs b/crates/am/src/exchange.rs index e5ca6bf3..4c989a1b 100644 --- a/crates/am/src/exchange.rs +++ b/crates/am/src/exchange.rs @@ -54,15 +54,15 @@ impl ExportAll { pub fn flatten_subcommands(&self) -> SubcommandSet { let mut result = SubcommandSet::new(); for (k, v) in &self.global_subcommands { - result.insert(k.clone(), v.clone()); + result.as_mut().insert(k.clone(), v.clone()); } for profile in &self.profiles { for (k, v) in &profile.subcommands { - result.insert(k.clone(), v.clone()); + result.as_mut().insert(k.clone(), v.clone()); } } for (k, v) in &self.local_subcommands { - result.insert(k.clone(), v.clone()); + result.as_mut().insert(k.clone(), v.clone()); } result } @@ -103,9 +103,11 @@ pub fn subcommand_merge_check( let mut new_subcommands = SubcommandSet::new(); let mut conflicts = Vec::new(); for (key, incoming_longs) in incoming { - match current.get(key) { + match current.as_ref().get(key) { None => { - new_subcommands.insert(key.clone(), incoming_longs.clone()); + new_subcommands + .as_mut() + .insert(key.clone(), incoming_longs.clone()); } Some(existing_longs) => { if existing_longs != incoming_longs { @@ -183,7 +185,7 @@ pub fn render_import_summary_subcommands( scope_name: &str, result: &SubcommandMergeResult, ) -> String { - let total = result.new_subcommands.len() + result.conflicts.len(); + let total = result.new_subcommands.as_ref().len() + result.conflicts.len(); let mut output = format!("Importing subcommands into \"{scope_name}\" ({total} entries)\n"); if !result.new_subcommands.is_empty() { @@ -872,25 +874,27 @@ mod tests { let mut export = ExportAll::default(); export .global_subcommands + .as_mut() .insert("jj:ab".into(), vec!["abandon".into()]); export.profiles.push(Profile { name: "vcs".into(), aliases: AliasSet::default(), subcommands: { let mut s = SubcommandSet::new(); - s.insert("jj:d".into(), vec!["diff".into()]); + s.as_mut().insert("jj:d".into(), vec!["diff".into()]); s }, }); export .local_subcommands + .as_mut() .insert("git:psh".into(), vec!["push".into()]); let flat = export.flatten_subcommands(); - assert_eq!(flat.len(), 3); - assert!(flat.contains_key("jj:ab")); - assert!(flat.contains_key("jj:d")); - assert!(flat.contains_key("git:psh")); + assert_eq!(flat.as_ref().len(), 3); + assert!(flat.as_ref().contains_key("jj:ab")); + assert!(flat.as_ref().contains_key("jj:d")); + assert!(flat.as_ref().contains_key("git:psh")); } #[test] @@ -898,14 +902,15 @@ mod tests { let mut export = ExportAll::default(); export .global_subcommands + .as_mut() .insert("jj:ab".into(), vec!["abandon".into()]); - export.local_subcommands.insert( + export.local_subcommands.as_mut().insert( "jj:ab".into(), vec!["abandon", "!"].into_iter().map(String::from).collect(), ); let flat = export.flatten_subcommands(); - assert_eq!(flat["jj:ab"], vec!["abandon", "!"]); + assert_eq!(flat.as_ref()["jj:ab"], vec!["abandon", "!"]); } // ─── subcommand_merge_check ────────────────────────────────────────── @@ -914,19 +919,23 @@ mod tests { fn test_merge_check_new_entries() { let current = SubcommandSet::new(); let mut incoming = SubcommandSet::new(); - incoming.insert("jj:ab".into(), vec!["abandon".into()]); + incoming + .as_mut() + .insert("jj:ab".into(), vec!["abandon".into()]); let result = subcommand_merge_check(¤t, &incoming); - assert_eq!(result.new_subcommands.len(), 1); + assert_eq!(result.new_subcommands.as_ref().len(), 1); assert!(result.conflicts.is_empty()); } #[test] fn test_merge_check_conflict() { let mut current = SubcommandSet::new(); - current.insert("jj:ab".into(), vec!["abandon".into()]); + current + .as_mut() + .insert("jj:ab".into(), vec!["abandon".into()]); let mut incoming = SubcommandSet::new(); - incoming.insert( + incoming.as_mut().insert( "jj:ab".into(), vec!["abandon", "--detach"] .into_iter() @@ -943,9 +952,13 @@ mod tests { #[test] fn test_merge_check_identical_entry_skipped() { let mut current = SubcommandSet::new(); - current.insert("jj:ab".into(), vec!["abandon".into()]); + current + .as_mut() + .insert("jj:ab".into(), vec!["abandon".into()]); let mut incoming = SubcommandSet::new(); - incoming.insert("jj:ab".into(), vec!["abandon".into()]); + incoming + .as_mut() + .insert("jj:ab".into(), vec!["abandon".into()]); let result = subcommand_merge_check(¤t, &incoming); assert!(result.new_subcommands.is_empty()); @@ -993,7 +1006,9 @@ mod tests { #[test] fn test_render_import_summary_subcommands_new_only() { let mut new_subcommands = SubcommandSet::new(); - new_subcommands.insert("jj:ab".into(), vec!["abandon".into()]); + new_subcommands + .as_mut() + .insert("jj:ab".into(), vec!["abandon".into()]); let result = SubcommandMergeResult { new_subcommands, conflicts: vec![], @@ -1028,6 +1043,7 @@ mod tests { let mut export = ExportAll::default(); export .global_subcommands + .as_mut() .insert("jj:\x1Bab".into(), vec!["abandon".into()]); let findings = scan_suspicious(&export); assert_eq!(findings.len(), 1); @@ -1040,6 +1056,7 @@ mod tests { let mut export = ExportAll::default(); export .local_subcommands + .as_mut() .insert("jj:ab".into(), vec!["aban\x07don".into()]); let findings = scan_suspicious(&export); assert_eq!(findings.len(), 1); @@ -1055,7 +1072,8 @@ mod tests { aliases: AliasSet::default(), subcommands: { let mut s = SubcommandSet::new(); - s.insert("jj:ab".into(), vec!["aban\x1Bdon".into()]); + s.as_mut() + .insert("jj:ab".into(), vec!["aban\x1Bdon".into()]); s }, }], @@ -1074,6 +1092,7 @@ mod tests { let mut export = ExportAll::default(); export .global_subcommands + .as_mut() .insert("jj:ab".into(), vec!["abandon".into()]); assert!(!export.is_empty()); } @@ -1083,6 +1102,7 @@ mod tests { let mut export = ExportAll::default(); export .local_subcommands + .as_mut() .insert("git:psh".into(), vec!["push".into()]); assert!(!export.is_empty()); } diff --git a/crates/am/src/hook.rs b/crates/am/src/hook.rs deleted file mode 100644 index 91f0ad52..00000000 --- a/crates/am/src/hook.rs +++ /dev/null @@ -1,968 +0,0 @@ -use std::collections::BTreeMap; -use std::path::Path; - -use crate::env_vars; -use crate::project::ProjectAliases; -use crate::security::{SecurityConfig, TrustStatus}; -use crate::shell::ShellContext; -use crate::trust::{ - compute_file_hash, compute_short_hash, render_load_message, render_unload_message, -}; - -/// Parse `_AM_PROJECT_ALIASES` value: `"name|hash,name|hash,..."` into a map. -/// Falls back to name-only format (no `|`) for backward compat during upgrade. -fn parse_prev_aliases(raw: Option<&str>) -> BTreeMap> { - let mut map = BTreeMap::new(); - let Some(s) = raw.filter(|s| !s.is_empty()) else { - return map; - }; - for entry in s.split(',') { - if let Some((name, hash)) = entry.split_once('|') { - map.insert(name.to_string(), Some(hash.to_string())); - } else { - // Backward compat: name without hash — always triggers reload - map.insert(entry.to_string(), None); - } - } - map -} - -/// Compute a short content hash for a regular alias. -/// -/// Hashes the command string which determines the shell-visible behaviour. -fn alias_content_hash(alias: &crate::alias::TomlAlias) -> String { - compute_short_hash(alias.command().as_bytes()) -} - -/// Generate shell code for the cd hook. -/// -/// `ctx.cwd` — the current working directory to search for `.aliases`. -/// `previous_aliases` — comma-separated alias entries from `_AM_PROJECT_ALIASES` env var. -pub fn generate_hook(ctx: &ShellContext, previous_aliases: Option<&str>) -> crate::Result { - let mut security = SecurityConfig::load().unwrap_or_default(); - let prev_project_path = std::env::var(env_vars::AM_PROJECT_PATH).ok(); - let (output, _changed) = generate_hook_with_security( - ctx, - previous_aliases, - prev_project_path.as_deref(), - &mut security, - false, - )?; - Ok(output) -} - -/// Generate shell code for the cd hook with explicit security config. -/// -/// Returns `(shell_code, security_changed)` — `security_changed` is true -/// when a tamper was detected and `security_config` was mutated in memory. -/// -/// When `quiet` is true, info and warning echo messages are suppressed -/// (alias loading/unloading still happens). -/// -/// `prev_project_path` — the value of `_AM_PROJECT_PATH` from the shell -/// environment, used to suppress duplicate warnings. Pass `None` to treat -/// the env var as unset (e.g. in tests). -pub fn generate_hook_with_security( - ctx: &ShellContext, - previous_aliases: Option<&str>, - prev_project_path: Option<&str>, - security_config: &mut SecurityConfig, - quiet: bool, -) -> crate::Result<(String, bool)> { - let shell_impl = ctx.shell.clone().as_shell( - ctx.cfg, - ctx.external_functions.clone(), - ctx.external_aliases.clone(), - ); - let cwd = ctx.cwd; - let mut lines: Vec = Vec::new(); - let mut security_changed = false; - - let prev = parse_prev_aliases(previous_aliases); - - // `prev_project_path` tracks which .aliases file was last seen, to avoid - // repeating warnings. It is passed in explicitly rather than read from the - // environment so that callers (e.g. tests) can control it independently. - - // Helper: unalias only shell-level names (no `:` — subcommand keys like `c:l` - // are tracked for change detection but are not themselves shell functions). - let unload_prev_names: Vec = - prev.keys().filter(|n| !n.contains(':')).cloned().collect(); - let unload_prev = |lines: &mut Vec| { - for name in &unload_prev_names { - lines.push(shell_impl.unalias(name)); - } - }; - - let project_path = ProjectAliases::find_path(cwd)?; - - match project_path { - Some(path) => { - let hash = compute_file_hash(&path)?; - let status = security_config.check(&path, &hash); - - // Only show info/warning messages when: - // - not in quiet mode - // - the .aliases file is directly in cwd (not inherited from parent) - // - we haven't already shown a message for this exact file - let is_direct = path.parent().is_some_and(|p| p == cwd); - let already_seen = prev_project_path.is_some_and(|p| Path::new(p) == path); - let show_messages = !quiet && is_direct && !already_seen; - - match status { - TrustStatus::Trusted => { - let project = ProjectAliases::load(&path)?; - if !project.aliases.is_empty() || !project.subcommands.is_empty() { - let subcmd_groups = - crate::subcommand::group_by_program(&project.subcommands); - let subcmd_program_names: Vec = - subcmd_groups.keys().cloned().collect(); - - // Build current alias map: name -> short content hash - let mut current: BTreeMap = BTreeMap::new(); - - for (alias_name, alias_value) in project.aliases.iter() { - current.insert( - alias_name.as_ref().to_string(), - alias_content_hash(alias_value), - ); - } - - // Subcommand program names (shell-level wrapper function) - for program in &subcmd_program_names { - let entries_str: String = project - .subcommands - .iter() - .filter(|(k, _)| k.starts_with(&format!("{program}:"))) - .map(|(k, v)| format!("{k}={}", v.join(","))) - .collect::>() - .join(";"); - current.insert( - program.clone(), - compute_short_hash(entries_str.as_bytes()), - ); - } - - // Individual subcommand keys for fine-grained tracking - for (key, longs) in project.subcommands.iter() { - current.insert( - key.clone(), - compute_short_hash(longs.join(",").as_bytes()), - ); - } - - // Compute diff against previous state - let mut removed: Vec = Vec::new(); - let mut added: Vec = Vec::new(); - let mut changed: Vec = Vec::new(); - - for name in prev.keys() { - if !current.contains_key(name) { - removed.push(name.clone()); - } - } - for (name, hash) in ¤t { - match prev.get(name) { - None => added.push(name.clone()), - Some(prev_hash) => { - // If prev had no hash (backward compat) or hash - // differs -> changed - if prev_hash.as_deref() != Some(hash.as_str()) { - changed.push(name.clone()); - } - } - } - } - - // If nothing changed at all, skip entirely - if removed.is_empty() && added.is_empty() && changed.is_empty() { - return Ok((String::new(), false)); - } - - let is_fresh_load = prev.is_empty(); - - // 1. Unload removed + changed (not unchanged!) - for name in removed.iter().chain(changed.iter()) { - if !name.contains(':') { - lines.push(shell_impl.unalias(name)); - } - } - - // 2. Show messages - if show_messages { - if is_fresh_load { - // Full load message (same as cd-into-project) - for line in - render_load_message(&project.aliases, &project.subcommands) - .lines() - { - lines.push(shell_impl.echo(line)); - } - } else { - // Incremental change summary - let mut parts = Vec::new(); - if !added.is_empty() { - parts.push(format!("{} added", added.len())); - } - if !changed.is_empty() { - parts.push(format!("{} updated", changed.len())); - } - if !removed.is_empty() { - parts.push(format!("{} removed", removed.len())); - } - lines.push( - shell_impl.echo(&format!( - "am: .aliases changed ({})", - parts.join(", ") - )), - ); - } - } - - let programs_set: std::collections::BTreeSet<&str> = - subcmd_groups.keys().map(|s| s.as_str()).collect(); - - if is_fresh_load { - // 3a. Fresh load: emit all aliases - for (alias_name, alias_value) in project.aliases.iter() { - let name = alias_name.as_ref(); - if !programs_set.contains(name) { - lines.push(shell_impl.alias(&alias_value.as_entry(name))); - } - } - - // All subcommand wrappers - for (program, entries) in &subcmd_groups { - let base_cmd = project - .aliases - .iter() - .find(|(n, _)| n.as_ref() == program.as_str()) - .map(|(_, v)| v.command().to_string()) - .unwrap_or_else(|| format!("command {program}")); - lines.push( - shell_impl.subcommand_wrapper(program, &base_cmd, entries), - ); - } - } else { - // 3b. Incremental: only load added + changed aliases - for (alias_name, alias_value) in project.aliases.iter() { - let name = alias_name.as_ref(); - if !programs_set.contains(name) - && (added.contains(&name.to_string()) - || changed.contains(&name.to_string())) - { - lines.push(shell_impl.alias(&alias_value.as_entry(name))); - } - } - - // Reload subcommand wrappers if any subcmd was - // added/changed/removed - let subcmd_changed = added - .iter() - .chain(changed.iter()) - .chain(removed.iter()) - .any(|n| n.contains(':') || subcmd_program_names.contains(n)); - if subcmd_changed { - for (program, entries) in &subcmd_groups { - let base_cmd = project - .aliases - .iter() - .find(|(n, _)| n.as_ref() == program.as_str()) - .map(|(_, v)| v.command().to_string()) - .unwrap_or_else(|| format!("command {program}")); - lines.push( - shell_impl.subcommand_wrapper(program, &base_cmd, entries), - ); - } - } - } - - // 4. Update tracking env var with name|hash format - let tracking: Vec = current - .iter() - .map(|(name, hash)| format!("{name}|{hash}")) - .collect(); - lines.push( - shell_impl.set_env(env_vars::AM_PROJECT_ALIASES, &tracking.join(",")), - ); - } - } - TrustStatus::Unknown => { - unload_prev(&mut lines); - if show_messages { - lines.push(shell_impl.echo( - "am: .aliases found but not trusted. Run 'am trust' to review and allow.", - )); - } - } - TrustStatus::Untrusted => { - unload_prev(&mut lines); - } - TrustStatus::Tampered => { - unload_prev(&mut lines); - security_changed = true; - if show_messages { - lines.push(shell_impl.echo( - "am: .aliases was modified since last trusted. Run 'am trust' to review and allow.", - )); - } - } - } - - // For non-trusted states: track the path to avoid repeating warnings, - // and clear the alias tracking env var. - if !matches!(status, TrustStatus::Trusted) { - lines.push( - shell_impl.set_env(env_vars::AM_PROJECT_PATH, &path.display().to_string()), - ); - if !prev.is_empty() { - lines.push(shell_impl.unset_env(env_vars::AM_PROJECT_ALIASES)); - } - } else if prev_project_path.is_some() { - lines.push(shell_impl.unset_env(env_vars::AM_PROJECT_PATH)); - } - } - None => { - if !prev.is_empty() { - unload_prev(&mut lines); - if !quiet { - let prev_names: Vec<&str> = - unload_prev_names.iter().map(|s| s.as_str()).collect(); - lines.push(shell_impl.echo(&render_unload_message(&prev_names))); - } - lines.push(shell_impl.unset_env(env_vars::AM_PROJECT_ALIASES)); - } - if prev_project_path.is_some() { - lines.push(shell_impl.unset_env(env_vars::AM_PROJECT_PATH)); - } - } - } - - Ok((lines.join("\n"), security_changed)) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::shell::Shell; - use crate::trust::compute_short_hash; - use std::path::{Path, PathBuf}; - - /// Extract the `_AM_PROJECT_ALIASES` value from generated shell code. - fn extract_prev_aliases(output: &str, shell: &Shell) -> Option { - let prefix = match shell { - Shell::Fish => "set -gx _AM_PROJECT_ALIASES \"", - _ => "export _AM_PROJECT_ALIASES=\"", - }; - output - .lines() - .find(|l| l.contains("_AM_PROJECT_ALIASES")) - .and_then(|l| { - let start = l.find(prefix).map(|i| i + prefix.len())?; - let end = l[start..].find('"').map(|i| start + i)?; - Some(l[start..end].to_string()) - }) - } - - /// Builder for hook test fixtures. - struct TestBed { - dir: tempfile::TempDir, - aliases_content: Option, - subdirs: Vec, - security: SecurityConfig, - } - - impl TestBed { - fn new() -> Self { - Self { - dir: tempfile::tempdir().unwrap(), - aliases_content: None, - subdirs: Vec::new(), - security: SecurityConfig::default(), - } - } - - fn with_aliases(mut self, content: &str) -> Self { - self.aliases_content = Some(content.to_string()); - self - } - - fn with_subdir(mut self, rel_path: &str) -> Self { - self.subdirs.push(PathBuf::from(rel_path)); - self - } - - fn with_security_trusted(mut self) -> Self { - let path = self.aliases_path(); - if let Some(content) = &self.aliases_content { - std::fs::write(&path, content).unwrap(); - } - let hash = compute_file_hash(&path).unwrap(); - self.security.trust(&path, &hash); - self - } - - fn with_security_untrusted(mut self) -> Self { - self.security.untrust(&self.aliases_path()); - self - } - - fn with_security_tampered(mut self) -> Self { - self.security.trust(&self.aliases_path(), "wrong_hash"); - self - } - - fn setup(self) -> SetupTestBed { - let aliases_path = self.dir.path().join(".aliases"); - if let Some(content) = &self.aliases_content { - std::fs::write(&aliases_path, content).unwrap(); - } - for sub in &self.subdirs { - std::fs::create_dir_all(self.dir.path().join(sub)).unwrap(); - } - SetupTestBed { - dir: self.dir, - security: self.security, - } - } - - fn aliases_path(&self) -> PathBuf { - self.dir.path().join(".aliases") - } - } - - struct SetupTestBed { - dir: tempfile::TempDir, - security: SecurityConfig, - } - - impl SetupTestBed { - fn root(&self) -> PathBuf { - self.dir.path().to_path_buf() - } - - fn subdir(&self, rel_path: &str) -> PathBuf { - self.dir.path().join(rel_path) - } - - fn run(&mut self, shell: &Shell, cwd: &Path, prev: Option<&str>) -> (String, bool) { - use crate::config::ShellsTomlConfig; - let cfg = ShellsTomlConfig::default(); - let ctx = ShellContext { - shell, - cfg: &cfg, - cwd, - external_functions: Default::default(), - external_aliases: Default::default(), - }; - generate_hook_with_security(&ctx, prev, None, &mut self.security, false).unwrap() - } - - /// Update the .aliases content and re-trust. - fn update_aliases(&mut self, content: &str) { - let path = self.dir.path().join(".aliases"); - std::fs::write(&path, content).unwrap(); - let hash = compute_file_hash(&path).unwrap(); - self.security.trust(&path, &hash); - } - } - - // ─── Basic hook behavior ──────────────────────────────────────── - - #[test] - fn test_hook_with_aliases_file() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\nt = \"make test\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, None); - assert!(output.contains("alias b \"make build\"")); - assert!(output.contains("alias t \"make test\"")); - assert!(output.contains(env_vars::AM_PROJECT_ALIASES)); - } - - #[test] - fn test_hook_unloads_previous_aliases() { - let mut t = TestBed::new().setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, Some("old1,old2")); - assert!(output.contains("functions -e old1")); - assert!(output.contains("functions -e old2")); - assert!(output.contains("set -e _AM_PROJECT_ALIASES")); - } - - #[test] - fn test_hook_no_aliases_no_previous() { - let mut t = TestBed::new().setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, None); - assert!(output.is_empty()); - } - - #[test] - fn test_hook_transitions_between_projects() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nnew1 = \"echo new\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, Some("old1,old2")); - assert!(output.contains("functions -e old1")); - assert!(output.contains("functions -e old2")); - assert!(output.contains("alias new1 \"echo new\"")); - let new1_hash = compute_short_hash(b"echo new"); - assert!( - output.contains(&format!("\"new1|{new1_hash}\"")), - "expected new1|hash in env var, got: {output}" - ); - } - - #[test] - fn test_hook_zsh_output() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Zsh, &cwd, Some("old")); - assert!(output.contains("unset -f old")); - assert!(output.contains("alias b=\"make build\"")); - assert!(output.contains("export _AM_PROJECT_ALIASES=")); - } - - #[test] - fn test_hook_picks_up_added_alias() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, None); - assert!(output.contains("alias b \"make build\"")); - assert!(!output.contains("alias t")); - - // Extract prev from first run to feed back as realistic input - let prev = extract_prev_aliases(&output, &Shell::Fish); - - t.update_aliases("[aliases]\nb = \"make build\"\nt = \"make test\"\n"); - - let (output, _) = t.run(&Shell::Fish, &cwd, prev.as_deref()); - // b is unchanged so should NOT be unloaded or reloaded - assert!( - !output.contains("functions -e b"), - "unchanged alias b should not be unloaded, got: {output}" - ); - assert!( - !output.contains("alias b \"make build\""), - "unchanged alias b should not be reloaded, got: {output}" - ); - // t is newly added - assert!(output.contains("alias t \"make test\"")); - // Env var now contains both with hashes - let b_hash = compute_short_hash(b"make build"); - let t_hash = compute_short_hash(b"make test"); - assert!( - output.contains(&format!("b|{b_hash},t|{t_hash}")), - "expected b|hash,t|hash in env var, got: {output}" - ); - } - - #[test] - fn test_hook_picks_up_removed_alias() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\nt = \"make test\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, None); - assert!(output.contains("alias b")); - assert!(output.contains("alias t")); - - // Extract prev from first run - let prev = extract_prev_aliases(&output, &Shell::Fish); - - t.update_aliases("[aliases]\nb = \"make build\"\n"); - - let (output, _) = t.run(&Shell::Fish, &cwd, prev.as_deref()); - // b is unchanged: should NOT be unloaded or reloaded - assert!( - !output.contains("functions -e b"), - "unchanged alias b should not be unloaded, got: {output}" - ); - assert!( - !output.contains("alias b \"make build\""), - "unchanged alias b should not be reloaded, got: {output}" - ); - // t is removed: should be unloaded - assert!( - output.contains("functions -e t"), - "removed alias t should be unloaded, got: {output}" - ); - assert!(!output.contains("alias t \"make test\"")); - // Env var only contains b now - let b_hash = compute_short_hash(b"make build"); - assert!( - output.contains(&format!("\"b|{b_hash}\"")), - "expected b|hash in env var, got: {output}" - ); - } - - #[test] - fn test_hook_bash_output() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Bash, &cwd, Some("old")); - assert!(output.contains("unset -f old")); - assert!(output.contains("alias b=\"make build\"")); - assert!(output.contains("export _AM_PROJECT_ALIASES=")); - } - - #[test] - fn test_hook_loads_aliases_from_parent_directory() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\nt = \"make test\"\n") - .with_subdir("src/deep") - .with_security_trusted() - .setup(); - - let sub = t.subdir("src/deep"); - let (output, _) = t.run(&Shell::Fish, &sub, None); - assert!( - output.contains("alias b \"make build\""), - "should load aliases from parent .aliases, got: {output}" - ); - assert!(output.contains("alias t \"make test\"")); - assert!(output.contains(env_vars::AM_PROJECT_ALIASES)); - } - - // ─── Trust-gated hook tests ───────────────────────────────────── - - #[test] - fn test_hook_trusted_shows_load_message() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, changed) = t.run(&Shell::Fish, &cwd, None); - assert!(!changed); - assert!(output.contains("alias b \"make build\"")); - assert!(output.contains("am: loaded .aliases")); - } - - #[test] - fn test_hook_unknown_shows_warning() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .setup(); - - let cwd = t.root(); - let (output, changed) = t.run(&Shell::Fish, &cwd, None); - assert!(!changed); - assert!(!output.contains("alias b")); - assert!(output.contains("am: .aliases found but not trusted")); - assert!(output.contains("am trust")); - } - - #[test] - fn test_hook_untrusted_silent() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_untrusted() - .setup(); - - let cwd = t.root(); - let (output, changed) = t.run(&Shell::Fish, &cwd, None); - assert!(!changed); - assert!(!output.contains("alias b")); - assert!(!output.contains("am:")); - } - - #[test] - fn test_hook_tampered_shows_loud_warning() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_tampered() - .setup(); - - let cwd = t.root(); - let (output, changed) = t.run(&Shell::Fish, &cwd, None); - assert!(changed); - assert!(!output.contains("alias b")); - assert!(output.contains("modified since last trusted")); - } - - #[test] - fn test_hook_unload_shows_message() { - let mut t = TestBed::new().setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, Some("old1,old2")); - assert!(output.contains("functions -e old1")); - assert!(output.contains("functions -e old2")); - assert!(output.contains("am: unloaded .aliases")); - } - - // ─── Subdirectory behavior ────────────────────────────────────── - - #[test] - fn test_hook_subdirectory_no_warning_for_parent_aliases() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_subdir("src") - .setup(); - - let sub = t.subdir("src"); - let (output, _) = t.run(&Shell::Fish, &sub, None); - assert!( - !output.contains("am:"), - "should not show warning for parent .aliases, got: {output}" - ); - } - - #[test] - fn test_hook_subdirectory_trusted_loads_silently() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_subdir("src") - .with_security_trusted() - .setup(); - - let sub = t.subdir("src"); - let (output, _) = t.run(&Shell::Fish, &sub, None); - assert!(output.contains("alias b \"make build\"")); - assert!( - !output.contains("am: loaded"), - "should not show load message for parent .aliases, got: {output}" - ); - } - - #[test] - fn test_hook_picks_up_new_subcommand_added_to_existing_program() { - // Regression: when a second subcommand is added under the same program (e.g. c:t after - // c:l), the hook was incorrectly skipping the reload because the set of *program names* - // hadn't changed ("c" was already in _AM_PROJECT_ALIASES). The wrapper function must - // be regenerated whenever the file content changes. - let mut t = TestBed::new() - .with_aliases("[subcommands]\n\"c:l\" = [\"clippy\"]\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - - // First run: load c:l, c wrapper is emitted - let (output, _) = t.run(&Shell::Fish, &cwd, None); - assert!( - output.contains("function c"), - "first run should emit c wrapper" - ); - assert!(output.contains("clippy")); - - // Extract prev from first run for realistic change detection - let prev = extract_prev_aliases(&output, &Shell::Fish); - - // Add c:t — the .aliases file changes, but program name `c` stays the same - t.update_aliases("[subcommands]\n\"c:l\" = [\"clippy\"]\n\"c:t\" = [\"test\"]\n"); - - // Second run: prev has c|hash and c:l|hash, but file has new content - let (output, _) = t.run(&Shell::Fish, &cwd, prev.as_deref()); - assert!( - output.contains("function c"), - "hook must re-emit c wrapper after new subcommand added, got: {output}" - ); - assert!(output.contains("test"), "updated wrapper must include c:t"); - assert!( - output.contains("clippy"), - "updated wrapper must still include c:l" - ); - } - - #[test] - fn test_hook_with_project_subcommands() { - let mut t = TestBed::new() - .with_aliases( - "[aliases]\nb = \"make build\"\n\n[subcommands]\n\"jj:ab\" = [\"abandon\"]\n", - ) - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Bash, &cwd, None); - assert!(output.contains("alias b=\"make build\"")); - assert!(output.contains("jj() {")); - assert!(output.contains("ab) shift; command jj abandon")); - } - - // ─── Per-alias content hashing ───────────────────────────────── - - #[test] - fn test_parse_prev_aliases_new_format() { - let map = parse_prev_aliases(Some("b|abc1234,t|def5678")); - assert_eq!(map.len(), 2); - assert_eq!(map["b"], Some("abc1234".to_string())); - assert_eq!(map["t"], Some("def5678".to_string())); - } - - #[test] - fn test_parse_prev_aliases_old_format_backward_compat() { - let map = parse_prev_aliases(Some("b,t")); - assert_eq!(map.len(), 2); - assert_eq!(map["b"], None); - assert_eq!(map["t"], None); - } - - #[test] - fn test_parse_prev_aliases_empty() { - assert!(parse_prev_aliases(None).is_empty()); - assert!(parse_prev_aliases(Some("")).is_empty()); - } - - #[test] - fn test_parse_prev_aliases_mixed_format() { - let map = parse_prev_aliases(Some("b|abc1234,t,gs|fed9876")); - assert_eq!(map.len(), 3); - assert_eq!(map["b"], Some("abc1234".to_string())); - assert_eq!(map["t"], None); - assert_eq!(map["gs"], Some("fed9876".to_string())); - } - - #[test] - fn test_alias_content_hash_deterministic() { - let alias = crate::TomlAlias::Command("make build".to_string()); - let h1 = alias_content_hash(&alias); - let h2 = alias_content_hash(&alias); - assert_eq!(h1, h2); - assert_eq!(h1.len(), 7); - } - - #[test] - fn test_alias_content_hash_different_commands() { - let a = crate::TomlAlias::Command("make build".to_string()); - let b = crate::TomlAlias::Command("cargo build".to_string()); - assert_ne!(alias_content_hash(&a), alias_content_hash(&b)); - } - - #[test] - fn test_hook_reloads_when_alias_value_changes() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, None); - assert!(output.contains("alias b \"make build\"")); - - // Extract prev from first run - let prev = extract_prev_aliases(&output, &Shell::Fish); - - // Update alias value (same name, different command) and re-trust - t.update_aliases("[aliases]\nb = \"cargo build\"\n"); - - // Hook with prev — same name "b" but different command - let (output, _) = t.run(&Shell::Fish, &cwd, prev.as_deref()); - assert!( - output.contains("alias b \"cargo build\""), - "hook must reload when alias value changes, got: {output}" - ); - // Old value should be unloaded first - assert!( - output.contains("functions -e b"), - "changed alias should be unloaded before reload, got: {output}" - ); - } - - #[test] - fn test_hook_skips_unchanged_aliases() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\nt = \"make test\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, None); - - // Extract prev from first run - let prev = extract_prev_aliases(&output, &Shell::Fish); - - // Re-run with same content — should skip entirely - let (output, _) = t.run(&Shell::Fish, &cwd, prev.as_deref()); - assert!( - output.is_empty(), - "unchanged aliases should produce no output, got: {output}" - ); - } - - #[test] - fn test_hook_incremental_message_on_change() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\nt = \"make test\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - let (output, _) = t.run(&Shell::Fish, &cwd, None); - // Fresh load shows full message - assert!(output.contains("am: loaded .aliases")); - - let prev = extract_prev_aliases(&output, &Shell::Fish); - - // Change t, add x, remove nothing - t.update_aliases("[aliases]\nb = \"make build\"\nt = \"make test --all\"\nx = \"exit\"\n"); - - let (output, _) = t.run(&Shell::Fish, &cwd, prev.as_deref()); - // Incremental message instead of full load - assert!( - output.contains("am: .aliases changed"), - "should show incremental change message, got: {output}" - ); - assert!( - !output.contains("am: loaded .aliases"), - "should not show full load message on incremental change, got: {output}" - ); - } - - #[test] - fn test_hook_backward_compat_old_format_triggers_full_reload() { - let mut t = TestBed::new() - .with_aliases("[aliases]\nb = \"make build\"\n") - .with_security_trusted() - .setup(); - - let cwd = t.root(); - // Old format: no hashes - let (output, _) = t.run(&Shell::Fish, &cwd, Some("b")); - // Should treat b as "changed" (prev hash is None) and reload - assert!( - output.contains("alias b \"make build\""), - "backward compat: old format should trigger reload, got: {output}" - ); - } - - #[test] - fn test_extract_prev_aliases_fish() { - let output = "set -gx _AM_PROJECT_ALIASES \"b|abc1234,t|def5678\""; - let prev = extract_prev_aliases(output, &Shell::Fish); - assert_eq!(prev, Some("b|abc1234,t|def5678".to_string())); - } - - #[test] - fn test_extract_prev_aliases_bash() { - let output = "export _AM_PROJECT_ALIASES=\"b|abc1234,t|def5678\""; - let prev = extract_prev_aliases(output, &Shell::Bash); - assert_eq!(prev, Some("b|abc1234,t|def5678".to_string())); - } -} diff --git a/crates/am/src/import_export.rs b/crates/am/src/import_export.rs index ce4ba170..212590f0 100644 --- a/crates/am/src/import_export.rs +++ b/crates/am/src/import_export.rs @@ -481,11 +481,13 @@ pub fn prompt_merge_subcommands( if apply_overwrites { for conflict in &merge.conflicts { - accepted.insert(conflict.key.clone(), conflict.incoming.clone()); + accepted + .as_mut() + .insert(conflict.key.clone(), conflict.incoming.clone()); } } - let imported = accepted.len(); + let imported = accepted.as_ref().len(); let skipped = if apply_overwrites { 0 } else { n }; eprintln!( "\u{2713} Imported {imported} subcommand aliases into \"{scope}\" ({skipped} skipped)" @@ -493,7 +495,7 @@ pub fn prompt_merge_subcommands( } else { eprintln!( "\u{2713} Imported {} subcommand aliases into \"{scope}\"", - accepted.len() + accepted.as_ref().len() ); } diff --git a/crates/am/src/init.rs b/crates/am/src/init.rs index bcfe71fc..80b1214b 100644 --- a/crates/am/src/init.rs +++ b/crates/am/src/init.rs @@ -1,6 +1,5 @@ -use crate::env_vars; use crate::shell::{Shell, ShellContext}; -use crate::subcommand::{group_by_program, SubcommandSet}; +use crate::subcommand::SubcommandSet; use crate::AliasSet; const WRAPPER_BASH: &str = include_str!("shell_wrappers/wrapper.bash"); @@ -27,184 +26,38 @@ pub fn generate_init( profile_aliases: &AliasSet, subcommands: &SubcommandSet, ) -> String { + use crate::precedence::Precedence; + let shell_impl = ctx.shell.clone().as_shell( ctx.cfg, ctx.external_functions.clone(), ctx.external_aliases.clone(), ); - let mut lines: Vec = Vec::new(); - let mut all_names: Vec = Vec::new(); - - // Determine which program names have subcommand wrappers - let subcmd_groups = group_by_program(subcommands); - let programs_with_wrappers: std::collections::BTreeSet<&str> = - subcmd_groups.keys().map(|s| s.as_str()).collect(); - - // Emit global aliases (skip those absorbed by subcommand wrappers) - for (alias_name, alias_value) in global_aliases.iter() { - let name = alias_name.as_ref(); - if !programs_with_wrappers.contains(name) { - lines.push(shell_impl.alias(&alias_value.as_entry(name))); - } - all_names.push(name.to_string()); - } - - // Emit profile aliases (skip those absorbed by subcommand wrappers) - for (alias_name, alias_value) in profile_aliases.iter() { - let name = alias_name.as_ref(); - if !programs_with_wrappers.contains(name) { - lines.push(shell_impl.alias(&alias_value.as_entry(name))); - } - all_names.push(name.to_string()); - } - - // Emit subcommand wrappers - for (program, entries) in &subcmd_groups { - // Determine base command: alias value if regular alias exists, else "command " - let all_aliases = global_aliases.iter().chain(profile_aliases.iter()); - let base_cmd = all_aliases - .filter(|(n, _)| n.as_ref() == program.as_str()) - .map(|(_, v)| v.command().to_string()) - .last() - .unwrap_or_else(|| format!("command {program}")); - - lines.push(shell_impl.subcommand_wrapper(program, &base_cmd, entries)); - all_names.push(program.to_string()); - } - // Track all loaded aliases (global + profile + subcommand wrappers) for reload cleanup - if !all_names.is_empty() { - all_names.sort(); - all_names.dedup(); - lines.push(shell_impl.set_env(env_vars::AM_ALIASES, &all_names.join(","))); - } - // Clean up legacy tracking var from older versions - lines.push(shell_impl.unset_env(env_vars::AM_PROFILE_ALIASES_LEGACY)); - - // Wrapper function - lines.push(String::new()); - lines.push(am_wrapper(ctx.shell)); - - // cd hook for project aliases - lines.push(String::new()); - lines.push(cd_hook_setup(ctx.shell)); - - // Shell completions - lines.push(String::new()); - lines.push(completions(ctx.shell)); - - lines.join("\n") -} - -/// Like [`generate_init`] but prepends force-cleanup lines for `prev_names`. -/// Each name is unloaded using all possible shell forms before the normal init runs. -/// Intended for testing; production code reads prev_names from env vars in `update.rs`. -pub fn generate_force_init( - ctx: &ShellContext, - global_aliases: &AliasSet, - profile_aliases: &AliasSet, - subcommands: &SubcommandSet, - prev_names: &[String], -) -> String { - let shell_impl = ctx - .shell - .clone() - .as_shell(ctx.cfg, Default::default(), Default::default()); - let mut output = String::new(); - for name in prev_names { - output.push_str(&shell_impl.force_unalias(name)); + // Split subcommands back into global/profile buckets. Callers today pass + // a single merged SubcommandSet — for init, global = config.subcommands + // and profile = resolved from active profiles. Since both are already + // merged upstream we simply pass the full set as "profile" to keep the + // engine's precedence order intact (global vs profile tier is invisible + // on init — there is no shell state yet). + let diff = Precedence::new() + .with_global(global_aliases, &SubcommandSet::new()) + .with_profiles(profile_aliases, subcommands) + .resolve(); + + let mut output = diff.render(shell_impl.as_ref()); + + // Wrapper function + cd hook + completions. + if !output.is_empty() { output.push('\n'); } - // Clear project-alias tracking so __am_hook reloads them fresh. - output.push_str(&shell_impl.unset_env(crate::env_vars::AM_PROJECT_ALIASES)); + output.push_str(&am_wrapper(ctx.shell)); output.push('\n'); - output.push_str(&shell_impl.unset_env(crate::env_vars::AM_PROJECT_PATH)); + output.push_str(&cd_hook_setup(ctx.shell)); output.push('\n'); - output.push_str(&generate_init( - ctx, - global_aliases, - profile_aliases, - subcommands, - )); - output -} - -/// Generate shell code to reload all aliases (global + profile) after a mutation. -/// Unloads old aliases, loads new ones, updates the tracking env var. -pub fn generate_reload( - ctx: &ShellContext, - global_aliases: &AliasSet, - profile_aliases: &AliasSet, - subcommands: &SubcommandSet, - previous_aliases: Option<&str>, -) -> String { - let shell_impl = ctx.shell.clone().as_shell( - ctx.cfg, - ctx.external_functions.clone(), - ctx.external_aliases.clone(), - ); - let mut lines: Vec = Vec::new(); - - // Unload all previously tracked aliases - let prev: Vec<&str> = previous_aliases - .filter(|s| !s.is_empty()) - .map(|s| s.split(',').collect()) - .unwrap_or_default(); - - for alias_name in &prev { - lines.push(shell_impl.unalias(alias_name)); - } - - // Determine which program names have subcommand wrappers - let subcmd_groups = group_by_program(subcommands); - let programs_with_wrappers: std::collections::BTreeSet<&str> = - subcmd_groups.keys().map(|s| s.as_str()).collect(); - - // Load global + profile aliases (skip those absorbed by subcommand wrappers) - let mut all_names: Vec = Vec::new(); - - for (alias_name, alias_value) in global_aliases.iter() { - let name = alias_name.as_ref(); - if !programs_with_wrappers.contains(name) { - lines.push(shell_impl.alias(&alias_value.as_entry(name))); - } - all_names.push(name.to_string()); - } - - for (alias_name, alias_value) in profile_aliases.iter() { - let name = alias_name.as_ref(); - if !programs_with_wrappers.contains(name) { - lines.push(shell_impl.alias(&alias_value.as_entry(name))); - } - all_names.push(name.to_string()); - } - - // Emit subcommand wrappers - for (program, entries) in &subcmd_groups { - // Determine base command: alias value if regular alias exists, else "command " - let all_aliases = global_aliases.iter().chain(profile_aliases.iter()); - let base_cmd = all_aliases - .filter(|(n, _)| n.as_ref() == program.as_str()) - .map(|(_, v)| v.command().to_string()) - .last() - .unwrap_or_else(|| format!("command {program}")); - - lines.push(shell_impl.subcommand_wrapper(program, &base_cmd, entries)); - all_names.push(program.to_string()); - } - - // Update tracking - if all_names.is_empty() { - if !prev.is_empty() { - lines.push(shell_impl.unset_env(env_vars::AM_ALIASES)); - } - } else { - all_names.sort(); - all_names.dedup(); - lines.push(shell_impl.set_env(env_vars::AM_ALIASES, &all_names.join(","))); - } + output.push_str(&completions(ctx.shell)); - lines.join("\n") + output } fn shell_script(template: &str, shell: &Shell) -> String { @@ -270,6 +123,7 @@ fn powershell_completions() -> String { mod tests { use super::*; use crate::config::ShellsTomlConfig; + use crate::env_vars; use crate::shell::ShellContext; use crate::subcommand::SubcommandSet; use crate::{AliasName, TomlAlias}; @@ -289,7 +143,7 @@ mod tests { fn test_subcommands() -> SubcommandSet { let mut subs = SubcommandSet::new(); - subs.insert("jj:ab".into(), vec!["abandon".into()]); + subs.as_mut().insert("jj:ab".into(), vec!["abandon".into()]); subs } @@ -315,8 +169,8 @@ mod tests { &aliases, &SubcommandSet::new(), ); - assert!(output.contains("alias gs \"git status\"")); - assert!(output.contains("alias ll \"ls -lha\"")); + assert!(output.contains("function gs\n git status $argv\nend")); + assert!(output.contains("function ll\n ls -lha $argv\nend")); } #[test] @@ -341,9 +195,7 @@ mod tests { &SubcommandSet::new(), ); assert!(output.contains("function am --wraps=am")); - assert!(output.contains("am reload fish")); - assert!(output.contains("--local")); - assert!(output.contains("am hook fish")); + assert!(output.contains("am sync fish")); } #[test] @@ -356,7 +208,7 @@ mod tests { &SubcommandSet::new(), ); assert!(output.contains("--on-variable PWD")); - assert!(output.contains("am hook fish")); + assert!(output.contains("am sync fish")); } #[test] @@ -382,9 +234,7 @@ mod tests { &SubcommandSet::new(), ); assert!(output.contains("am()")); - assert!(output.contains("am reload zsh")); - assert!(output.contains("--local")); - assert!(output.contains("am hook zsh")); + assert!(output.contains("am sync zsh")); } #[test] @@ -397,7 +247,7 @@ mod tests { &SubcommandSet::new(), ); assert!(output.contains("chpwd_functions")); - assert!(output.contains("am hook zsh")); + assert!(output.contains("am sync zsh")); } #[test] @@ -412,64 +262,6 @@ mod tests { assert!(!output.contains(env_vars::AM_ALIASES)); } - #[test] - fn test_reload_unloads_old_and_loads_new() { - let aliases = test_aliases(); - let output = generate_reload( - &default_ctx(&Shell::Fish), - &AliasSet::default(), - &aliases, - &SubcommandSet::new(), - Some("old1,old2"), - ); - assert!(output.contains("functions -e old1")); - assert!(output.contains("functions -e old2")); - assert!(output.contains("alias gs \"git status\"")); - assert!(output.contains("alias ll \"ls -lha\"")); - assert!(output.contains(env_vars::AM_ALIASES)); - } - - #[test] - fn test_reload_zsh_unloads_with_unset_f() { - let aliases = test_aliases(); - let output = generate_reload( - &default_ctx(&Shell::Zsh), - &AliasSet::default(), - &aliases, - &SubcommandSet::new(), - Some("old1"), - ); - assert!(output.contains("unset -f old1")); - assert!(output.contains("alias gs=\"git status\"")); - } - - #[test] - fn test_reload_no_previous() { - let aliases = test_aliases(); - let output = generate_reload( - &default_ctx(&Shell::Fish), - &AliasSet::default(), - &aliases, - &SubcommandSet::new(), - None, - ); - assert!(!output.contains("functions -e")); - assert!(output.contains("alias gs")); - } - - #[test] - fn test_reload_to_empty_clears_tracking() { - let output = generate_reload( - &default_ctx(&Shell::Fish), - &AliasSet::default(), - &AliasSet::default(), - &SubcommandSet::new(), - Some("old1"), - ); - assert!(output.contains("functions -e old1")); - assert!(output.contains("set -e _AM_ALIASES")); - } - #[test] fn test_init_includes_global_aliases() { let mut globals = AliasSet::default(); @@ -483,7 +275,7 @@ mod tests { &AliasSet::default(), &SubcommandSet::new(), ); - assert!(output.contains("alias ll \"ls -lha\"")); + assert!(output.contains("function ll\n ls -lha $argv\nend")); } #[test] @@ -508,24 +300,6 @@ mod tests { ); } - #[test] - fn test_reload_includes_globals() { - let mut globals = AliasSet::default(); - globals.insert( - "ll".into(), - crate::TomlAlias::Command("ls -lha".to_string()), - ); - let output = generate_reload( - &default_ctx(&Shell::Fish), - &globals, - &AliasSet::default(), - &SubcommandSet::new(), - Some("old"), - ); - assert!(output.contains("functions -e old")); - assert!(output.contains("alias ll \"ls -lha\"")); - } - #[test] fn test_bash_init_contains_aliases() { let aliases = test_aliases(); @@ -549,9 +323,7 @@ mod tests { &SubcommandSet::new(), ); assert!(output.contains("am()")); - assert!(output.contains("am reload bash")); - assert!(output.contains("--local")); - assert!(output.contains("am hook bash")); + assert!(output.contains("am sync bash")); } #[test] @@ -566,21 +338,7 @@ mod tests { assert!(output.contains("PROMPT_COMMAND")); assert!(output.contains("__am_hook")); assert!(output.contains("__am_prev_dir")); - assert!(output.contains("am hook bash")); - } - - #[test] - fn test_reload_bash_unloads_with_unset_f() { - let aliases = test_aliases(); - let output = generate_reload( - &default_ctx(&Shell::Bash), - &AliasSet::default(), - &aliases, - &SubcommandSet::new(), - Some("old1"), - ); - assert!(output.contains("unset -f old1")); - assert!(output.contains("alias gs=\"git status\"")); + assert!(output.contains("am sync bash")); } #[test] @@ -667,26 +425,16 @@ mod tests { } #[test] - fn test_fish_reload_with_abbr_unloads_via_abbr_erase() { - use crate::config::{FishConfig, ShellsTomlConfig}; - let cfg = ShellsTomlConfig { - fish: Some(FishConfig { use_abbr: true }), - }; - let cwd = std::path::Path::new("/tmp"); - let ctx = ShellContext { - shell: &Shell::Fish, - cfg: &cfg, - cwd, - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let output = generate_reload( - &ctx, - &AliasSet::default(), - &AliasSet::default(), - &SubcommandSet::new(), - Some("old1"), + fn init_delegates_alias_emission_to_precedence() { + // init output must match PrecedenceDiff::render output for the same inputs. + let aliases = test_aliases(); + let ctx = default_ctx(&Shell::Fish); + let output = generate_init(&ctx, &AliasSet::default(), &aliases, &SubcommandSet::new()); + // Everything should be in _AM_ALIASES with name|hash format (not bare names). + let gs_hash = crate::trust::compute_short_hash(b"git status"); + assert!( + output.contains(&format!("gs|{gs_hash}")), + "init must use name|hash format in _AM_ALIASES, got: {output}" ); - assert!(output.contains("abbr --erase old1")); } } diff --git a/crates/am/src/lib.rs b/crates/am/src/lib.rs index 89509ee9..425e23a6 100644 --- a/crates/am/src/lib.rs +++ b/crates/am/src/lib.rs @@ -8,10 +8,10 @@ pub mod display; pub mod effects; pub mod env_vars; pub mod exchange; -pub mod hook; pub mod import_export; pub mod init; pub mod messages; +pub mod precedence; pub mod profile; pub mod project; pub mod prompt; diff --git a/crates/am/src/messages.rs b/crates/am/src/messages.rs index a62d5e7a..5ae2d0cc 100644 --- a/crates/am/src/messages.rs +++ b/crates/am/src/messages.rs @@ -35,8 +35,7 @@ pub enum Message { raw: bool, }, InitShell(Shell, bool), - Hook(Shell, bool), - Reload(Shell), + Sync(Shell, bool), ToggleProfiles(Vec), UseProfilesAt(Vec, usize), diff --git a/crates/am/src/precedence/diff.rs b/crates/am/src/precedence/diff.rs new file mode 100644 index 00000000..47e99b7d --- /dev/null +++ b/crates/am/src/precedence/diff.rs @@ -0,0 +1,208 @@ +use crate::alias::TomlAlias; +use crate::env_vars; +use crate::shell::ShellAdapter; +use crate::subcommand::SubcommandEntry; + +use super::env_state::{AliasWithHash, AliasWithHashList}; + +#[derive(Debug, Clone, PartialEq)] +pub enum EntryKind { + Alias(TomlAlias), + SubcommandWrapper { + program: String, + entries: Vec, + base_cmd: Option, + }, + /// Per-key subcommand entry tracked in `_AM_SUBCOMMANDS` for fine-grained + /// change detection. Never emitted as shell code — the program-level + /// `SubcommandWrapper` is the shell-visible unit. + SubcommandKey { + longs: Vec, + }, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct EffectiveEntry { + pub name: String, + pub kind: EntryKind, + pub hash: String, +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct PrecedenceDiff { + pub added: Vec, + pub changed: Vec, + pub removed: Vec, + pub unchanged: Vec, +} + +/// Build a change-summary line like +/// `" — N verb1: a, b | M verb2: c"`. +/// +/// Empty sections are skipped; returns `None` when every section is empty so +/// callers can stay silent. Shared between [`PrecedenceDiff::change_summary`] +/// (fixed head, shell-state diff verbs) and the profile-toggle message in +/// `update.rs` (dynamic head, shadow-aware verbs). +pub(crate) fn format_change_summary(head: &str, sections: &[(&str, &[&str])]) -> Option { + let parts: Vec = sections + .iter() + .filter(|(_, names)| !names.is_empty()) + .map(|(verb, names)| format!("{} {verb}: {}", names.len(), names.join(", "))) + .collect(); + if parts.is_empty() { + None + } else { + Some(format!("{head} — {}", parts.join(" | "))) + } +} + +impl PrecedenceDiff { + /// Human-readable summary of what changed, suitable for echoing to the + /// user (e.g. `"am: aliases changed — 1 loaded: b | 1 unloaded: t"`). + /// + /// Returns `None` when nothing changed so callers can stay silent. + pub fn change_summary(&self) -> Option { + let added: Vec<&str> = self.added.iter().map(|e| e.name.as_str()).collect(); + let changed: Vec<&str> = self.changed.iter().map(|e| e.name.as_str()).collect(); + let removed: Vec<&str> = self.removed.iter().map(|s| s.as_str()).collect(); + format_change_summary( + "am: aliases changed", + &[ + ("loaded", &added), + ("updated", &changed), + ("unloaded", &removed), + ], + ) + } + + /// Render this diff into shell code using the given adapter. + /// + /// Emission order: + /// 1. unload (removed + changed) — skipping subcommand-key names + /// (they're tracking-only, not shell functions) + /// 2. load (added + changed) + /// 3. set `_AM_ALIASES` / `_AM_SUBCOMMANDS` to the union of added + + /// changed + unchanged + pub fn render(&self, shell: &dyn ShellAdapter) -> String { + let mut lines: Vec = Vec::new(); + + // 1. Unload + for name in &self.removed { + if name.contains(':') { + continue; + } + lines.push(shell.unalias(name)); + } + for entry in &self.changed { + if matches!(entry.kind, EntryKind::SubcommandKey { .. }) { + continue; + } + if entry.name.contains(':') { + continue; + } + lines.push(shell.unalias(&entry.name)); + } + + // 2. Load (added + changed) + for entry in self.added.iter().chain(self.changed.iter()) { + match &entry.kind { + EntryKind::Alias(alias) => { + lines.push(shell.alias(&alias.as_entry(&entry.name))); + } + EntryKind::SubcommandWrapper { + program, + entries, + base_cmd, + } => { + let cmd = base_cmd + .clone() + .unwrap_or_else(|| format!("command {program}")); + lines.push(shell.subcommand_wrapper(program, &cmd, entries)); + } + EntryKind::SubcommandKey { .. } => {} + } + } + + // 3. Update tracking env vars + let mut alias_list = AliasWithHashList::new(); + let mut sub_list = AliasWithHashList::new(); + for e in self + .added + .iter() + .chain(self.changed.iter()) + .chain(self.unchanged.iter()) + { + let entry = AliasWithHash::new(&e.name, Some(e.hash.clone())); + match &e.kind { + EntryKind::SubcommandKey { .. } => sub_list.push(entry), + _ => alias_list.push(entry), + } + } + + if !alias_list.is_empty() { + lines.push(shell.set_env(env_vars::AM_ALIASES, &alias_list.to_string())); + } + if !sub_list.is_empty() { + lines.push(shell.set_env(env_vars::AM_SUBCOMMANDS, &sub_list.to_string())); + } + + lines.join("\n") + } +} + +#[cfg(test)] +mod tests { + use super::super::engine::Precedence; + use super::*; + use crate::alias::{AliasName, AliasSet}; + use crate::config::ShellsTomlConfig; + use crate::shell::Shell; + use crate::subcommand::SubcommandSet; + + fn aset(pairs: &[(&str, &str)]) -> AliasSet { + let mut s = AliasSet::default(); + for (n, c) in pairs { + s.insert(AliasName::from(*n), TomlAlias::Command((*c).into())); + } + s + } + + #[test] + fn render_emits_unloads_then_loads_then_env() { + let cfg = ShellsTomlConfig::default(); + let shell = Shell::Fish.as_shell(&cfg, Default::default(), Default::default()); + + // Previous shell state: `b|0000000,gone|aaa` ; new effective: `b|make build`. + let project = aset(&[("b", "make build")]); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .with_shell_state_from_env(Some("b|0000000,gone|aaa"), None) + .resolve(); + + let out = diff.render(shell.as_ref()); + assert!( + out.contains("functions -e gone"), + "gone must be unloaded: {out}" + ); + assert!( + out.contains("functions -e b"), + "changed b must be unloaded: {out}" + ); + assert!( + out.contains("function b\n make build $argv\nend"), + "b must be reloaded: {out}" + ); + // env-var update must be the last section + let env_pos = out.find("_AM_ALIASES").expect("env update missing"); + let fn_pos = out.find("function b").unwrap(); + assert!(env_pos > fn_pos, "env update must come after loads"); + } + + #[test] + fn render_empty_diff_produces_empty_string() { + let cfg = ShellsTomlConfig::default(); + let shell = Shell::Fish.as_shell(&cfg, Default::default(), Default::default()); + let out = PrecedenceDiff::default().render(shell.as_ref()); + assert!(out.is_empty()); + } +} diff --git a/crates/am/src/precedence/engine.rs b/crates/am/src/precedence/engine.rs new file mode 100644 index 00000000..8445b839 --- /dev/null +++ b/crates/am/src/precedence/engine.rs @@ -0,0 +1,653 @@ +use std::collections::{BTreeMap, BTreeSet, HashSet}; + +use crate::alias::{AliasSet, TomlAlias}; +use crate::subcommand::SubcommandSet; + +use super::diff::{EffectiveEntry, EntryKind, PrecedenceDiff}; +use super::env_state::AliasWithHashList; + +#[derive(Debug, Default)] +pub struct Precedence { + global_aliases: AliasSet, + global_subcommands: SubcommandSet, + profile_aliases: AliasSet, + profile_subcommands: SubcommandSet, + project_aliases: AliasSet, + project_subcommands: SubcommandSet, + shell_alias_state: BTreeMap>, + shell_subcmd_state: BTreeMap>, + external_functions: HashSet, + external_aliases: HashSet, +} + +impl Precedence { + pub fn new() -> Self { + Self::default() + } + + pub fn with_global(mut self, aliases: &AliasSet, subs: &SubcommandSet) -> Self { + self.global_aliases = aliases.clone(); + self.global_subcommands = subs.clone(); + self + } + + pub fn with_profiles(mut self, aliases: &AliasSet, subs: &SubcommandSet) -> Self { + self.profile_aliases = aliases.clone(); + self.profile_subcommands = subs.clone(); + self + } + + pub fn with_project(mut self, aliases: &AliasSet, subs: &SubcommandSet) -> Self { + self.project_aliases = aliases.clone(); + self.project_subcommands = subs.clone(); + self + } + + pub fn with_shell_state_from_env( + mut self, + aliases: Option<&str>, + subcommands: Option<&str>, + ) -> Self { + self.shell_alias_state = Self::parse_state(aliases); + self.shell_subcmd_state = Self::parse_state(subcommands); + self + } + + pub fn with_shell_state_from_introspection( + mut self, + functions: &HashSet, + aliases: &HashSet, + ) -> Self { + for name in functions.iter().chain(aliases.iter()) { + self.shell_alias_state.entry(name.clone()).or_insert(None); + } + self.external_functions = functions.clone(); + self.external_aliases = aliases.clone(); + self + } + + /// Internal: merged alias set keyed by shell-visible name, + /// with project > profile > global precedence. + fn merged_aliases(&self) -> BTreeMap { + let mut out = BTreeMap::new(); + for layer in [ + &self.global_aliases, + &self.profile_aliases, + &self.project_aliases, + ] { + for (name, alias) in layer.iter() { + out.insert(name.as_ref().to_string(), alias.clone()); + } + } + out + } + + /// Internal: merged subcommand set keyed by full "program:seg:..." key, + /// with project > profile > global precedence. + fn merged_subcommands(&self) -> SubcommandSet { + let mut out = SubcommandSet::new(); + for layer in [ + &self.global_subcommands, + &self.profile_subcommands, + &self.project_subcommands, + ] { + for (k, v) in layer { + out.as_mut().insert(k.clone(), v.clone()); + } + } + out + } + + fn alias_hash(alias: &TomlAlias) -> String { + crate::trust::compute_short_hash(alias.command().as_bytes()) + } + + fn subcmd_program_hash(program: &str, subs: &SubcommandSet) -> String { + let entries_str: String = subs + .as_ref() + .iter() + .filter(|(k, _)| k.starts_with(&format!("{program}:"))) + .map(|(k, v)| format!("{k}={}", v.join(","))) + .collect::>() + .join(";"); + crate::trust::compute_short_hash(entries_str.as_bytes()) + } + + fn subcmd_key_hash(longs: &[String]) -> String { + crate::trust::compute_short_hash(longs.join(",").as_bytes()) + } + + fn parse_state(raw: Option<&str>) -> BTreeMap> { + AliasWithHashList::parse(raw) + .into_iter() + .map(|e| (e.name().to_string(), e.hash().map(String::from))) + .collect() + } + + #[cfg(test)] + fn merged_aliases_for_test(&self) -> BTreeMap { + self.merged_aliases() + } + + #[cfg(test)] + pub(crate) fn alias_hash_for_test(alias: &TomlAlias) -> String { + Self::alias_hash(alias) + } + + #[cfg(test)] + pub(crate) fn subcmd_program_hash_for_test(program: &str, subs: &SubcommandSet) -> String { + Self::subcmd_program_hash(program, subs) + } + + #[cfg(test)] + pub(crate) fn subcmd_key_hash_for_test(longs: &[String]) -> String { + Self::subcmd_key_hash(longs) + } + + #[cfg(test)] + pub(crate) fn shell_alias_state_for_test(&self) -> &BTreeMap> { + &self.shell_alias_state + } + + #[cfg(test)] + pub(crate) fn shell_subcmd_state_for_test(&self) -> &BTreeMap> { + &self.shell_subcmd_state + } + + pub fn resolve(self) -> PrecedenceDiff { + let merged_aliases = self.merged_aliases(); + let merged_subcommands = self.merged_subcommands(); + let subcmd_groups = merged_subcommands.group_by_program(); + let program_names: BTreeSet = subcmd_groups.keys().cloned().collect(); + + let mut effective: BTreeMap = BTreeMap::new(); + + // Regular aliases — skip names absorbed by a subcommand wrapper. + for (name, alias) in merged_aliases.iter() { + if program_names.contains(name) { + continue; + } + let hash = Self::alias_hash(alias); + effective.insert( + name.clone(), + EffectiveEntry { + name: name.clone(), + kind: EntryKind::Alias(alias.clone()), + hash, + }, + ); + } + + // Subcommand wrappers (one entry per program). + for (program, entries) in &subcmd_groups { + let base_cmd = merged_aliases.get(program).map(|a| a.command().to_string()); + let hash = Self::subcmd_program_hash(program, &merged_subcommands); + effective.insert( + program.clone(), + EffectiveEntry { + name: program.clone(), + kind: EntryKind::SubcommandWrapper { + program: program.clone(), + entries: entries.clone(), + base_cmd, + }, + hash, + }, + ); + } + + // Per-key subcommand tracking for `_AM_SUBCOMMANDS`. + let mut effective_subkeys: BTreeMap = BTreeMap::new(); + for (key, longs) in &merged_subcommands { + let hash = Self::subcmd_key_hash(longs); + effective_subkeys.insert( + key.clone(), + EffectiveEntry { + name: key.clone(), + kind: EntryKind::SubcommandKey { + longs: longs.clone(), + }, + hash, + }, + ); + } + + let mut diff = PrecedenceDiff::default(); + + // --- Regular + wrapper diff against shell_alias_state --- + for name in self.shell_alias_state.keys() { + if !effective.contains_key(name) { + diff.removed.push(name.clone()); + } + } + for (name, entry) in effective { + match self.shell_alias_state.get(&name) { + None => diff.added.push(entry), + Some(prev) if prev.as_deref() == Some(entry.hash.as_str()) => { + diff.unchanged.push(entry) + } + Some(_) => diff.changed.push(entry), + } + } + + // --- Per-key subcommand diff against shell_subcmd_state --- + // + // The program-level wrapper already lives in `effective`/`diff` above. + // Here we additionally track individual keys so they appear in + // `_AM_SUBCOMMANDS` with fine-grained hashes. + for name in self.shell_subcmd_state.keys() { + // A program-level entry (no ':') is tracked in shell_alias_state, not here. + if !name.contains(':') { + continue; + } + if !effective_subkeys.contains_key(name) { + diff.removed.push(name.clone()); + } + } + for (name, entry) in effective_subkeys { + match self.shell_subcmd_state.get(&name) { + None => diff.added.push(entry), + Some(prev) if prev.as_deref() == Some(entry.hash.as_str()) => { + diff.unchanged.push(entry) + } + Some(_) => diff.changed.push(entry), + } + } + + diff + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::alias::AliasName; + + #[test] + fn empty_inputs_produce_empty_diff() { + let diff = Precedence::new().resolve(); + assert_eq!(diff, PrecedenceDiff::default()); + } + + fn aset(pairs: &[(&str, &str)]) -> AliasSet { + let mut s = AliasSet::default(); + for (n, c) in pairs { + s.insert(AliasName::from(*n), TomlAlias::Command((*c).into())); + } + s + } + + #[test] + fn merge_project_overrides_profile_overrides_global() { + let global = aset(&[("ll", "ls -lha"), ("t", "global-t")]); + let profile = aset(&[("gs", "git status"), ("t", "profile-t")]); + let project = aset(&[("b", "make build"), ("t", "project-t")]); + + let p = Precedence::new() + .with_global(&global, &SubcommandSet::new()) + .with_profiles(&profile, &SubcommandSet::new()) + .with_project(&project, &SubcommandSet::new()); + + let merged = p.merged_aliases_for_test(); + assert_eq!(merged.get("ll").unwrap().command(), "ls -lha"); + assert_eq!(merged.get("gs").unwrap().command(), "git status"); + assert_eq!(merged.get("b").unwrap().command(), "make build"); + assert_eq!(merged.get("t").unwrap().command(), "project-t"); + } + + #[test] + fn merge_without_project_falls_back_to_profile() { + let global = aset(&[("t", "global-t")]); + let profile = aset(&[("t", "profile-t")]); + let p = Precedence::new() + .with_global(&global, &SubcommandSet::new()) + .with_profiles(&profile, &SubcommandSet::new()); + let merged = p.merged_aliases_for_test(); + assert_eq!(merged.get("t").unwrap().command(), "profile-t"); + } + + #[test] + fn hash_alias_stable_and_differs_by_command() { + let a = TomlAlias::Command("make build".into()); + let b = TomlAlias::Command("cargo build".into()); + let h_a = Precedence::alias_hash_for_test(&a); + let h_b = Precedence::alias_hash_for_test(&b); + assert_eq!(h_a.len(), 7); + assert_ne!(h_a, h_b); + assert_eq!(h_a, Precedence::alias_hash_for_test(&a)); + } + + #[test] + fn hash_subcmd_program_includes_all_entries_under_it() { + let mut a = SubcommandSet::new(); + a.as_mut().insert("jj:ab".into(), vec!["abandon".into()]); + let mut b = a.clone(); + b.as_mut() + .insert("jj:bl".into(), vec!["branch".into(), "list".into()]); + + let h_a = Precedence::subcmd_program_hash_for_test("jj", &a); + let h_b = Precedence::subcmd_program_hash_for_test("jj", &b); + assert_eq!(h_a.len(), 7); + assert_ne!(h_a, h_b, "adding jj:bl must change jj program hash"); + } + + #[test] + fn hash_subcmd_key_hashes_long_subcommands() { + let key_hash = Precedence::subcmd_key_hash_for_test(&["branch".into(), "list".into()]); + assert_eq!(key_hash.len(), 7); + assert_eq!( + key_hash, + Precedence::subcmd_key_hash_for_test(&["branch".into(), "list".into()]) + ); + } + + #[test] + fn parse_shell_state_new_format() { + let p = Precedence::new().with_shell_state_from_env(Some("b|abc1234,t|def5678"), None); + let aliases = p.shell_alias_state_for_test(); + assert_eq!(aliases.get("b"), Some(&Some("abc1234".into()))); + assert_eq!(aliases.get("t"), Some(&Some("def5678".into()))); + } + + #[test] + fn parse_shell_state_old_name_only_format_treated_as_unknown() { + let p = Precedence::new().with_shell_state_from_env(Some("b,t"), None); + let aliases = p.shell_alias_state_for_test(); + assert_eq!(aliases.get("b"), Some(&None)); + assert_eq!(aliases.get("t"), Some(&None)); + } + + #[test] + fn parse_shell_state_empty_and_none() { + let p1 = Precedence::new().with_shell_state_from_env(None, None); + assert!(p1.shell_alias_state_for_test().is_empty()); + let p2 = Precedence::new().with_shell_state_from_env(Some(""), None); + assert!(p2.shell_alias_state_for_test().is_empty()); + } + + #[test] + fn parse_shell_state_mixed_format() { + let p = Precedence::new().with_shell_state_from_env(Some("b|abc1234,t,gs|fed9876"), None); + let aliases = p.shell_alias_state_for_test(); + assert_eq!(aliases.get("b"), Some(&Some("abc1234".into()))); + assert_eq!(aliases.get("t"), Some(&None)); + assert_eq!(aliases.get("gs"), Some(&Some("fed9876".into()))); + } + + #[test] + fn parse_shell_state_subcommands_stored_separately() { + let p = Precedence::new() + .with_shell_state_from_env(Some("b|aaa0000"), Some("jj|bbb1111,jj:ab|ccc2222")); + assert!(p.shell_alias_state_for_test().contains_key("b")); + let subs = p.shell_subcmd_state_for_test(); + assert_eq!(subs.get("jj"), Some(&Some("bbb1111".into()))); + assert_eq!(subs.get("jj:ab"), Some(&Some("ccc2222".into()))); + } + + fn find<'a>(v: &'a [EffectiveEntry], name: &str) -> Option<&'a EffectiveEntry> { + v.iter().find(|e| e.name == name) + } + + fn cmd_of(entry: &EffectiveEntry) -> &str { + match &entry.kind { + EntryKind::Alias(a) => a.command(), + _ => panic!("expected Alias, got {:?}", entry.kind), + } + } + + #[test] + fn resolve_fresh_load_everything_added() { + let global = aset(&[("ll", "ls -lha")]); + let profile = aset(&[("gs", "git status")]); + let project = aset(&[("b", "make build")]); + let diff = Precedence::new() + .with_global(&global, &SubcommandSet::new()) + .with_profiles(&profile, &SubcommandSet::new()) + .with_project(&project, &SubcommandSet::new()) + .resolve(); + let added_names: BTreeSet<_> = diff.added.iter().map(|e| e.name.as_str()).collect(); + assert_eq!(added_names, BTreeSet::from(["ll", "gs", "b"]),); + assert!(diff.changed.is_empty()); + assert!(diff.removed.is_empty()); + assert!(diff.unchanged.is_empty()); + } + + #[test] + fn resolve_unchanged_when_hashes_match() { + let project = aset(&[("b", "make build")]); + let hash = Precedence::alias_hash_for_test(&TomlAlias::Command("make build".into())); + let prev = format!("b|{hash}"); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .with_shell_state_from_env(Some(&prev), None) + .resolve(); + assert!(diff.added.is_empty()); + assert!(diff.changed.is_empty()); + assert!(diff.removed.is_empty()); + assert_eq!(diff.unchanged.len(), 1); + assert_eq!(diff.unchanged[0].name, "b"); + } + + #[test] + fn resolve_changed_when_hash_differs() { + let project = aset(&[("b", "cargo build")]); + let prev = "b|0000000"; // obviously not the real hash + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .with_shell_state_from_env(Some(prev), None) + .resolve(); + assert_eq!(diff.changed.len(), 1); + assert_eq!(cmd_of(&diff.changed[0]), "cargo build"); + assert!(diff.added.is_empty()); + assert!(diff.removed.is_empty()); + } + + #[test] + fn resolve_backward_compat_bare_name_triggers_reload() { + let project = aset(&[("b", "make build")]); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .with_shell_state_from_env(Some("b"), None) // old format + .resolve(); + assert_eq!(diff.changed.len(), 1); + assert_eq!(diff.changed[0].name, "b"); + } + + #[test] + fn resolve_removed_when_no_layer_contains_name() { + let diff = Precedence::new() + .with_shell_state_from_env(Some("gone|abc1234"), None) + .resolve(); + assert_eq!(diff.removed, vec!["gone".to_string()]); + } + + #[test] + fn resolve_shadow_restoration_via_changed_entry() { + // Previous session: project 't' shadowed profile 't'. Now project layer is + // gone (we left the project directory). Effective 't' reverts to profile. + // The stored hash was the project's; the new effective hash is the profile's. + // This must be detected as Changed -> the shell reloads with the profile value. + let profile = aset(&[("t", "profile-t")]); + let project_hash = Precedence::alias_hash_for_test(&TomlAlias::Command("project-t".into())); + let prev = format!("t|{project_hash}"); + let diff = Precedence::new() + .with_profiles(&profile, &SubcommandSet::new()) + .with_shell_state_from_env(Some(&prev), None) + .resolve(); + assert_eq!( + diff.changed.len(), + 1, + "shadow restoration must emit a reload" + ); + assert_eq!(cmd_of(&diff.changed[0]), "profile-t"); + assert!(diff.removed.is_empty()); + } + + fn subset(pairs: &[(&str, &[&str])]) -> SubcommandSet { + let mut s = SubcommandSet::new(); + for (k, longs) in pairs { + s.as_mut() + .insert((*k).into(), longs.iter().map(|x| (*x).into()).collect()); + } + s + } + + #[test] + fn resolve_subcommand_fresh_load_emits_wrapper() { + let project_subs = subset(&[("jj:ab", &["abandon"])]); + let diff = Precedence::new() + .with_project(&AliasSet::default(), &project_subs) + .resolve(); + let wrapper = find(&diff.added, "jj").expect("expected jj wrapper in added"); + match &wrapper.kind { + EntryKind::SubcommandWrapper { + program, + entries, + base_cmd, + } => { + assert_eq!(program, "jj"); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].short_subcommands, vec!["ab"]); + assert_eq!(entries[0].long_subcommands, vec!["abandon"]); + assert!(base_cmd.is_none()); + } + other => panic!("expected SubcommandWrapper, got {other:?}"), + } + // per-key entry also added (for env-var tracking) + let key = find(&diff.added, "jj:ab").expect("expected per-key entry"); + assert!(matches!(key.kind, EntryKind::SubcommandKey { .. })); + } + + #[test] + fn resolve_subcommand_base_cmd_from_regular_alias_same_name() { + let aliases = aset(&[("jj", "just-a-joke")]); + let subs = subset(&[("jj:ab", &["abandon"])]); + let diff = Precedence::new().with_project(&aliases, &subs).resolve(); + let wrapper = find(&diff.added, "jj").unwrap(); + match &wrapper.kind { + EntryKind::SubcommandWrapper { base_cmd, .. } => { + assert_eq!(base_cmd.as_deref(), Some("just-a-joke")); + } + _ => panic!(), + } + // Only one entry named "jj" — the wrapper, which absorbs the alias. + let jj_hits = diff.added.iter().filter(|e| e.name == "jj").count(); + assert_eq!(jj_hits, 1, "only the wrapper entry should represent 'jj'"); + } + + #[test] + fn resolve_subcommand_different_keys_coexist_across_layers() { + let profile_subs = subset(&[("jj:ab", &["abandon"])]); + let project_subs = subset(&[("jj:b:l", &["branch", "list"])]); + let diff = Precedence::new() + .with_profiles(&AliasSet::default(), &profile_subs) + .with_project(&AliasSet::default(), &project_subs) + .resolve(); + let wrapper = find(&diff.added, "jj").unwrap(); + match &wrapper.kind { + EntryKind::SubcommandWrapper { entries, .. } => { + let keys: BTreeSet<_> = entries.iter().map(|e| e.to_key()).collect(); + assert_eq!(keys, BTreeSet::from(["jj:ab".into(), "jj:b:l".into()])); + } + _ => panic!(), + } + } + + #[test] + fn resolve_subcommand_project_key_overrides_profile_same_key() { + let profile_subs = subset(&[("jj:ab", &["abandon"])]); + let project_subs = subset(&[("jj:ab", &["abandon-force"])]); + let diff = Precedence::new() + .with_profiles(&AliasSet::default(), &profile_subs) + .with_project(&AliasSet::default(), &project_subs) + .resolve(); + let wrapper = find(&diff.added, "jj").unwrap(); + match &wrapper.kind { + EntryKind::SubcommandWrapper { entries, .. } => { + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].long_subcommands, vec!["abandon-force"]); + } + _ => panic!(), + } + } + + #[test] + fn resolve_subcommand_unchanged_when_program_hash_matches() { + let subs = subset(&[("jj:ab", &["abandon"])]); + let merged = subs.clone(); + let program_hash = Precedence::subcmd_program_hash_for_test("jj", &merged); + let key_hash = Precedence::subcmd_key_hash_for_test(&["abandon".into()]); + let prev_aliases = format!("jj|{program_hash}"); + let prev_subs = format!("jj:ab|{key_hash}"); + let diff = Precedence::new() + .with_project(&AliasSet::default(), &subs) + .with_shell_state_from_env(Some(&prev_aliases), Some(&prev_subs)) + .resolve(); + assert!(diff.added.is_empty(), "got added: {:?}", diff.added); + assert!(diff.changed.is_empty(), "got changed: {:?}", diff.changed); + assert!(diff.removed.is_empty(), "got removed: {:?}", diff.removed); + assert_eq!( + diff.unchanged.len(), + 2, + "jj wrapper + jj:ab key both unchanged" + ); + } + + #[test] + fn introspection_adds_names_with_unknown_hash() { + let mut fns = HashSet::new(); + fns.insert("gs".to_string()); + let mut aliases = HashSet::new(); + aliases.insert("ll".to_string()); + let p = Precedence::new() + .with_shell_state_from_env(Some("b|abc1234"), None) + .with_shell_state_from_introspection(&fns, &aliases); + let state = p.shell_alias_state_for_test(); + assert_eq!(state.get("b"), Some(&Some("abc1234".into()))); + assert_eq!(state.get("gs"), Some(&None)); + assert_eq!(state.get("ll"), Some(&None)); + } + + #[test] + fn introspection_does_not_overwrite_known_hashes() { + let mut fns = HashSet::new(); + fns.insert("b".to_string()); + let p = Precedence::new() + .with_shell_state_from_env(Some("b|abc1234"), None) + .with_shell_state_from_introspection(&fns, &HashSet::new()); + assert_eq!( + p.shell_alias_state_for_test().get("b"), + Some(&Some("abc1234".into())) + ); + } + + #[test] + fn resolve_subcommand_regenerates_wrapper_when_entry_added() { + // Previous: only jj:ab was tracked. Now jj:bl is added too. + // The program hash changes -> wrapper must be in `changed`. + let subs_before = subset(&[("jj:ab", &["abandon"])]); + let program_hash_before = Precedence::subcmd_program_hash_for_test("jj", &subs_before); + let key_hash_ab = Precedence::subcmd_key_hash_for_test(&["abandon".into()]); + let prev_aliases = format!("jj|{program_hash_before}"); + let prev_subs = format!("jj:ab|{key_hash_ab}"); + + let subs_after = subset(&[("jj:ab", &["abandon"]), ("jj:bl", &["branch", "list"])]); + let diff = Precedence::new() + .with_project(&AliasSet::default(), &subs_after) + .with_shell_state_from_env(Some(&prev_aliases), Some(&prev_subs)) + .resolve(); + assert!( + find(&diff.changed, "jj").is_some(), + "wrapper must be regenerated" + ); + assert!( + find(&diff.added, "jj:bl").is_some(), + "new key must be added" + ); + // jj:ab itself unchanged + assert!( + find(&diff.unchanged, "jj:ab").is_some(), + "jj:ab entry itself is unchanged" + ); + } +} diff --git a/crates/am/src/precedence/env_state.rs b/crates/am/src/precedence/env_state.rs new file mode 100644 index 00000000..d1a262ef --- /dev/null +++ b/crates/am/src/precedence/env_state.rs @@ -0,0 +1,193 @@ +use std::fmt; + +/// One entry in the `_AM_ALIASES` / `_AM_SUBCOMMANDS` env var, in the +/// `"name|hash"` format (or a legacy bare `"name"` with no hash). +/// +/// `hash = None` means the shell reloaded from an older amoxide that only +/// tracked names; the diff treats such entries as "always differs" so they +/// get reloaded on the next sync. +#[derive(Debug, Clone, PartialEq)] +pub struct AliasWithHash { + name: String, + hash: Option, +} + +impl AliasWithHash { + pub fn new(name: impl Into, hash: Option) -> Self { + Self { + name: name.into(), + hash, + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn hash(&self) -> Option<&str> { + self.hash.as_deref() + } + + /// Parse one `"name|hash"` (or bare `"name"`) token. Returns `None` when + /// the name segment is empty — callers skip such entries silently. + pub fn parse(token: &str) -> Option { + match token.split_once('|') { + Some((name, hash)) if !name.is_empty() => Some(Self { + name: name.to_string(), + hash: Some(hash.to_string()), + }), + Some(_) => None, // empty name before '|' + None if token.is_empty() => None, + None => Some(Self { + name: token.to_string(), + hash: None, + }), + } + } +} + +impl fmt::Display for AliasWithHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.hash { + Some(h) => write!(f, "{}|{}", self.name, h), + None => write!(f, "{}", self.name), + } + } +} + +/// A comma-separated list of [`AliasWithHash`] entries — the on-the-wire +/// format of `_AM_ALIASES` and `_AM_SUBCOMMANDS`. +/// +/// Owns round-trip parsing and rendering so no other module has to know +/// about the `"name|hash,name|hash,..."` layout. +#[derive(Debug, Default, Clone, PartialEq)] +pub struct AliasWithHashList(Vec); + +impl AliasWithHashList { + pub fn new() -> Self { + Self::default() + } + + pub fn push(&mut self, entry: AliasWithHash) { + self.0.push(entry); + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn iter(&self) -> std::slice::Iter<'_, AliasWithHash> { + self.0.iter() + } + + /// Parse an `_AM_ALIASES` / `_AM_SUBCOMMANDS` value. `None` or empty + /// string yields an empty list; malformed tokens are skipped. + pub fn parse(raw: Option<&str>) -> Self { + let Some(s) = raw.filter(|s| !s.is_empty()) else { + return Self::new(); + }; + Self(s.split(',').filter_map(AliasWithHash::parse).collect()) + } +} + +impl fmt::Display for AliasWithHashList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, entry) in self.0.iter().enumerate() { + if i > 0 { + f.write_str(",")?; + } + write!(f, "{entry}")?; + } + Ok(()) + } +} + +impl FromIterator for AliasWithHashList { + fn from_iter>(iter: I) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl<'a> IntoIterator for &'a AliasWithHashList { + type Item = &'a AliasWithHash; + type IntoIter = std::slice::Iter<'a, AliasWithHash>; + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl IntoIterator for AliasWithHashList { + type Item = AliasWithHash; + type IntoIter = std::vec::IntoIter; + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn alias_with_hash_parse_new_format() { + let e = AliasWithHash::parse("b|abc1234").unwrap(); + assert_eq!(e.name(), "b"); + assert_eq!(e.hash(), Some("abc1234")); + } + + #[test] + fn alias_with_hash_parse_bare_name() { + let e = AliasWithHash::parse("t").unwrap(); + assert_eq!(e.name(), "t"); + assert_eq!(e.hash(), None); + } + + #[test] + fn alias_with_hash_parse_empty_returns_none() { + assert!(AliasWithHash::parse("").is_none()); + assert!(AliasWithHash::parse("|abc").is_none()); + } + + #[test] + fn alias_with_hash_display_roundtrip() { + assert_eq!( + AliasWithHash::new("b", Some("abc1234".into())).to_string(), + "b|abc1234" + ); + assert_eq!(AliasWithHash::new("t", None).to_string(), "t"); + } + + #[test] + fn alias_with_hash_list_parse_and_render() { + let list = AliasWithHashList::parse(Some("b|abc1234,t|def5678")); + assert_eq!(list.iter().count(), 2); + assert_eq!(list.to_string(), "b|abc1234,t|def5678"); + } + + #[test] + fn alias_with_hash_list_parse_mixed_format() { + let list = AliasWithHashList::parse(Some("b|abc1234,t,gs|fed9876")); + let names: Vec<&str> = list.iter().map(|e| e.name()).collect(); + assert_eq!(names, vec!["b", "t", "gs"]); + assert_eq!(list.iter().nth(1).unwrap().hash(), None); + } + + #[test] + fn alias_with_hash_list_parse_empty_and_none() { + assert!(AliasWithHashList::parse(None).is_empty()); + assert!(AliasWithHashList::parse(Some("")).is_empty()); + } + + #[test] + fn alias_with_hash_list_parse_skips_malformed_tokens() { + // Leading empty token and "|xxx" token get dropped silently. + let list = AliasWithHashList::parse(Some(",|xxx,b|abc1234")); + let names: Vec<&str> = list.iter().map(|e| e.name()).collect(); + assert_eq!(names, vec!["b"]); + } + + #[test] + fn alias_with_hash_list_display_empty() { + assert_eq!(AliasWithHashList::new().to_string(), ""); + } +} diff --git a/crates/am/src/precedence/mod.rs b/crates/am/src/precedence/mod.rs new file mode 100644 index 00000000..1d6d4160 --- /dev/null +++ b/crates/am/src/precedence/mod.rs @@ -0,0 +1,17 @@ +//! Precedence resolution: merge global/profile/project alias layers against +//! the current shell's loaded state to produce a diff that tells the shell +//! exactly what to load, reload, or unload. +//! +//! Split into three submodules: +//! * `env_state` — `_AM_ALIASES` / `_AM_SUBCOMMANDS` wire format. +//! * `diff` — the `PrecedenceDiff` output and how it renders to shell code. +//! * `engine` — the `Precedence` builder and `resolve()` logic. + +mod diff; +mod engine; +mod env_state; + +pub(crate) use diff::format_change_summary; +pub use diff::{EffectiveEntry, EntryKind, PrecedenceDiff}; +pub use engine::Precedence; +pub use env_state::{AliasWithHash, AliasWithHashList}; diff --git a/crates/am/src/profile.rs b/crates/am/src/profile.rs index b8fe8345..3c49b715 100644 --- a/crates/am/src/profile.rs +++ b/crates/am/src/profile.rs @@ -4,7 +4,7 @@ use log::info; use serde::{Deserialize, Serialize}; use crate::dirs::config_dir; -use crate::subcommand::{group_by_program, SubcommandSet}; +use crate::subcommand::SubcommandSet; use crate::{AliasDetail, AliasName, AliasSet, Result, TomlAlias}; /// A collection of aliases (regular and/or subcommand) that can report its @@ -79,7 +79,7 @@ impl ProfileConfig { for name in profile_names { if let Some(profile) = self.get_profile_by_name(name.as_ref()) { for (key, values) in &profile.subcommands { - resolved.insert(key.clone(), values.clone()); + resolved.as_mut().insert(key.clone(), values.clone()); } } } @@ -256,11 +256,12 @@ impl Profile { } pub fn add_subcommand(&mut self, key: String, long_subcommands: Vec) { - self.subcommands.insert(key, long_subcommands); + self.subcommands.as_mut().insert(key, long_subcommands); } pub fn remove_subcommand(&mut self, key: &str) -> Result<()> { self.subcommands + .as_mut() .remove(key) .ok_or_else(|| anyhow::anyhow!("Subcommand alias '{key}' not found"))?; Ok(()) @@ -273,7 +274,7 @@ impl AliasCollection for Profile { } fn len(&self) -> usize { - self.aliases.len() + self.subcommands.len() + self.aliases.len() + self.subcommands.as_ref().len() } fn short_list(&self) -> String { @@ -283,7 +284,7 @@ impl AliasCollection for Profile { .map(|(k, _)| k.as_ref().to_string()) .collect(); - let groups = group_by_program(&self.subcommands); + let groups = self.subcommands.group_by_program(); for (program, entries) in &groups { // Each entry's short_subcommands are space-joined; entries within // the same program are comma-separated. diff --git a/crates/am/src/project.rs b/crates/am/src/project.rs index c5f14fc8..e7fffd37 100644 --- a/crates/am/src/project.rs +++ b/crates/am/src/project.rs @@ -125,11 +125,11 @@ impl ProjectAliases { } pub fn add_subcommand(&mut self, key: String, long_subcommands: Vec) { - self.subcommands.insert(key, long_subcommands); + self.subcommands.as_mut().insert(key, long_subcommands); } pub fn remove_subcommand(&mut self, key: &str) -> crate::Result<()> { - self.subcommands.remove(key).ok_or_else(|| { + self.subcommands.as_mut().remove(key).ok_or_else(|| { anyhow::anyhow!("Subcommand alias '{key}' not found in {ALIASES_FILE}") })?; Ok(()) @@ -238,11 +238,12 @@ mod tests { let mut project = ProjectAliases::default(); project .subcommands + .as_mut() .insert("jj:ab".into(), vec!["abandon".into()]); project.save(&path).unwrap(); let loaded = ProjectAliases::load(&path).unwrap(); - assert_eq!(loaded.subcommands.len(), 1); - assert_eq!(loaded.subcommands["jj:ab"], vec!["abandon"]); + assert_eq!(loaded.subcommands.as_ref().len(), 1); + assert_eq!(loaded.subcommands.as_ref()["jj:ab"], vec!["abandon"]); } } diff --git a/crates/am/src/shell/fish.rs b/crates/am/src/shell/fish.rs index cacc3d62..b9df718e 100644 --- a/crates/am/src/shell/fish.rs +++ b/crates/am/src/shell/fish.rs @@ -127,13 +127,51 @@ impl ShellAdapter for Fish { } fn alias(&self, entry: &AliasEntry) -> String { + // Emit a plain `function` and register completion inheritance via a + // separate `complete -c NAME --wraps CMD` call, rather than going + // through fish's `alias` builtin. Reasons: + // - fish's `alias` puts `--wraps=` directly in the + // function signature, which shows up in `type NAME` output and + // stacks on redefinition (the `--wraps=` entries accumulate). + // - `function NAME` alone has a clean signature. + // - `complete -c NAME --wraps` registers inheritance in the + // completion system, which the prelude's `complete -e -c NAME` + // wipes before each emission, so no stacking. + // - We wrap the first whitespace-separated token of the command — + // the actual wrapped binary — rather than the full argv, which + // is what fish's completion system expects. + let prelude = format!( + "functions -e {name}\ncomplete -e -c {name}", + name = entry.name + ); + let wrap_target = entry + .command + .split_whitespace() + .next() + .filter(|s| !s.is_empty()); + let wraps_line = wrap_target.map(|w| { + format!( + "\ncomplete -c {name} --wraps {cmd}", + name = entry.name, + cmd = quote_cmd(w), + ) + }); if !entry.raw && has_template_args(entry.command) { let body = substitute_fish(entry.command); - format!("function {}\n {}\nend", entry.name, body) + format!( + "{prelude}\nfunction {name}\n {body}\nend{wraps}", + name = entry.name, + wraps = wraps_line.as_deref().unwrap_or(""), + ) } else if self.use_abbr { format!("abbr --add {} {}", entry.name, quote_cmd(entry.command)) } else { - format!("alias {} {}", entry.name, quote_cmd(entry.command)) + format!( + "{prelude}\nfunction {name}\n {cmd} $argv\nend{wraps}", + name = entry.name, + cmd = entry.command, + wraps = wraps_line.as_deref().unwrap_or(""), + ) } } @@ -240,11 +278,11 @@ mod tests { fn test_fish_simple_alias() { assert_eq!( Fish::default().alias(&simple("h", "'echo hello'")), - "alias h 'echo hello'" + "functions -e h\ncomplete -e -c h\nfunction h\n 'echo hello' $argv\nend\ncomplete -c h --wraps \"'echo\"" ); assert_eq!( Fish::default().alias(&simple("h", "echo hello")), - "alias h \"echo hello\"" + "functions -e h\ncomplete -e -c h\nfunction h\n echo hello $argv\nend\ncomplete -c h --wraps \"echo\"" ); } @@ -252,11 +290,11 @@ mod tests { fn test_fish_parameterized() { assert_eq!( Fish::default().alias(&simple("cmf", "cm feat: {{@}}")), - "function cmf\n cm feat: $argv\nend" + "functions -e cmf\ncomplete -e -c cmf\nfunction cmf\n cm feat: $argv\nend\ncomplete -c cmf --wraps \"cm\"" ); assert_eq!( Fish::default().alias(&simple("x", "echo {{1}} and {{2}}")), - "function x\n echo $argv[1] and $argv[2]\nend" + "functions -e x\ncomplete -e -c x\nfunction x\n echo $argv[1] and $argv[2]\nend\ncomplete -c x --wraps \"echo\"" ); } @@ -264,7 +302,7 @@ mod tests { fn test_fish_raw_skips_templates() { assert_eq!( Fish::default().alias(&raw("my-awk", "awk '{print {{1}}}'")), - "alias my-awk \"awk '{print {{1}}}'\"" + "functions -e my-awk\ncomplete -e -c my-awk\nfunction my-awk\n awk '{print {{1}}}' $argv\nend\ncomplete -c my-awk --wraps \"awk\"" ); } @@ -412,7 +450,7 @@ mod tests { let fish = Fish { use_abbr: true }; assert_eq!( fish.alias(&simple("cmf", "cm feat: {{@}}")), - "function cmf\n cm feat: $argv\nend" + "functions -e cmf\ncomplete -e -c cmf\nfunction cmf\n cm feat: $argv\nend\ncomplete -c cmf --wraps \"cm\"" ); } diff --git a/crates/am/src/shell_wrappers/hook.bash b/crates/am/src/shell_wrappers/hook.bash index 34620d83..249b05e5 100644 --- a/crates/am/src/shell_wrappers/hook.bash +++ b/crates/am/src/shell_wrappers/hook.bash @@ -1,9 +1,9 @@ -# am cd hook: track directory changes and reload project aliases +# am cd hook: sync project aliases on directory change __am_hook() { local previous_exit_status=$? if [[ "${__am_prev_dir:-}" != "$PWD" ]]; then __am_prev_dir="$PWD" - eval "$(command am hook __SHELL__)" + eval "$(command am sync __SHELL__)" fi return $previous_exit_status } diff --git a/crates/am/src/shell_wrappers/hook.fish b/crates/am/src/shell_wrappers/hook.fish index 03bc5366..c79aafed 100644 --- a/crates/am/src/shell_wrappers/hook.fish +++ b/crates/am/src/shell_wrappers/hook.fish @@ -1,5 +1,5 @@ # am cd hook function __am_hook --on-variable PWD - am hook __SHELL__ | source + am sync __SHELL__ | source end -__am_hook \ No newline at end of file +__am_hook diff --git a/crates/am/src/shell_wrappers/hook.ps1 b/crates/am/src/shell_wrappers/hook.ps1 index 9b52aba1..54050a3f 100644 --- a/crates/am/src/shell_wrappers/hook.ps1 +++ b/crates/am/src/shell_wrappers/hook.ps1 @@ -1,11 +1,11 @@ -# am cd hook: track directory changes and reload project aliases +# am cd hook: sync project aliases on directory change $env:__AM_LAST_DIR = $PWD.Path $__am_original_prompt = $function:prompt function global:prompt { if ($PWD.Path -ne $env:__AM_LAST_DIR) { $env:__AM_LAST_DIR = $PWD.Path $amBin = (Get-Command -CommandType Application am | Select-Object -First 1).Source - $hookCode = (& $amBin hook __SHELL__) -join "`r`n" + $hookCode = (& $amBin sync __SHELL__) -join "`r`n" if ($hookCode) { Invoke-Command -ScriptBlock ([scriptblock]::Create($hookCode)) -NoNewScope } } if ($__am_original_prompt) { & $__am_original_prompt } else { "PS $($PWD.Path)> " } diff --git a/crates/am/src/shell_wrappers/hook.zsh b/crates/am/src/shell_wrappers/hook.zsh index 0516dbc5..90c50441 100644 --- a/crates/am/src/shell_wrappers/hook.zsh +++ b/crates/am/src/shell_wrappers/hook.zsh @@ -1,4 +1,4 @@ # am cd hook -__am_hook() { eval "$(am hook __SHELL__)"; } +__am_hook() { eval "$(am sync __SHELL__)"; } chpwd_functions+=(__am_hook) -__am_hook \ No newline at end of file +__am_hook diff --git a/crates/am/src/shell_wrappers/wrapper.bash b/crates/am/src/shell_wrappers/wrapper.bash index 93670ced..6234fc14 100644 --- a/crates/am/src/shell_wrappers/wrapper.bash +++ b/crates/am/src/shell_wrappers/wrapper.bash @@ -3,21 +3,14 @@ am() { local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - tui|t) eval "$(command am reload __SHELL__)"; eval "$(command am hook __SHELL__)"; return ;; - esac - case "$1" in - use|u) eval "$(command am reload __SHELL__)"; return ;; - esac - case "$1:$2" in - profile:use|p:use|profile:u|p:u|profile:add|p:add|profile:a|p:a|profile:remove|p:remove|profile:r|p:r) eval "$(command am reload __SHELL__)" ;; - esac - case "$1" in - add|a|remove|r) - case "$*" in - *\ -l\ *|*\ --local\ *|*\ -l|*\ --local) eval "$(command am hook __SHELL__)" ;; - *) eval "$(command am reload __SHELL__)" ;; + add|a|remove|r|trust|tui|t) + eval "$(command am sync __SHELL__)" ;; + use|u|untrust) + eval "$(command am sync --quiet __SHELL__)" ;; + profile|p) + case "$2" in + use|u) eval "$(command am sync --quiet __SHELL__)" ;; + add|a|remove|r) eval "$(command am sync __SHELL__)" ;; esac ;; - trust) eval "$(command am hook __SHELL__)" ;; - untrust) eval "$(command am hook --quiet __SHELL__)" ;; esac } diff --git a/crates/am/src/shell_wrappers/wrapper.fish b/crates/am/src/shell_wrappers/wrapper.fish index d9cd6783..aa96d0c3 100644 --- a/crates/am/src/shell_wrappers/wrapper.fish +++ b/crates/am/src/shell_wrappers/wrapper.fish @@ -1,37 +1,21 @@ -# am wrapper: reload aliases after mutations +# am wrapper: sync after mutations function am --wraps=am command am $argv set -l am_status $status if test $am_status -ne 0 return $am_status end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload __SHELL__ | source - command am hook __SHELL__ | source - return + switch "$argv[1]" + case add a remove r trust tui t + command am sync __SHELL__ | source + case use u untrust + command am sync --quiet __SHELL__ | source + case profile p + switch "$argv[2]" + case use u + command am sync --quiet __SHELL__ | source + case add a remove r + command am sync __SHELL__ | source + end end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload __SHELL__ | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload __SHELL__ | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook __SHELL__ | source - else - # profile/global alias change → reload - command am reload __SHELL__ | source - end - else if test "$argv[1]" = trust - command am hook __SHELL__ | source - else if test "$argv[1]" = untrust - command am hook --quiet __SHELL__ | source - end -end \ No newline at end of file +end diff --git a/crates/am/src/shell_wrappers/wrapper.ps1 b/crates/am/src/shell_wrappers/wrapper.ps1 index 7b476750..effada5f 100644 --- a/crates/am/src/shell_wrappers/wrapper.ps1 +++ b/crates/am/src/shell_wrappers/wrapper.ps1 @@ -1,46 +1,30 @@ -# am wrapper: reload aliases after mutations +# am wrapper: sync after mutations function am { $amBin = (Get-Command -CommandType Application am | Select-Object -First 1).Source & $amBin @args if ($LASTEXITCODE -ne 0) { return } - # tui — always reload - if ($args.Count -ge 1 -and $args[0] -in 'tui', 't') { - $out = (& $amBin reload __SHELL__) -join "`r`n" + if ($args.Count -lt 1) { return } + $first = $args[0] + $second = if ($args.Count -ge 2) { $args[1] } else { $null } + + $runSync = { + $out = (& $amBin sync __SHELL__) -join "`r`n" if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - $out = (& $amBin hook __SHELL__) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - return } - # top-level use — reload - if ($args.Count -ge 1 -and $args[0] -in 'use', 'u') { - $out = (& $amBin reload __SHELL__) -join "`r`n" + $runSyncQuiet = { + $out = (& $amBin sync --quiet __SHELL__) -join "`r`n" if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - return - } - # profile mutation — reload - if ($args.Count -ge 1 -and $args[0] -in 'profile', 'p') { - if ($args.Count -ge 2 -and $args[1] -in 'use', 'u', 'add', 'a', 'remove', 'r') { - $out = (& $amBin reload __SHELL__) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - } } - # alias mutation — reload - elseif ($args.Count -ge 1 -and $args[0] -in 'add', 'a', 'remove', 'r') { - if ($args -contains '-l' -or $args -contains '--local') { - $out = (& $amBin hook __SHELL__) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - } else { - $out = (& $amBin reload __SHELL__) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } + + if ($first -in 'add', 'a', 'remove', 'r', 'trust', 'tui', 't') { + & $runSync + } elseif ($first -in 'use', 'u', 'untrust') { + & $runSyncQuiet + } elseif ($first -in 'profile', 'p') { + if ($second -in 'use', 'u') { + & $runSyncQuiet + } elseif ($second -in 'add', 'a', 'remove', 'r') { + & $runSync } } - # trust/untrust — reload project aliases - elseif ($args.Count -ge 1 -and $args[0] -eq 'trust') { - $out = (& $amBin hook __SHELL__) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - } - elseif ($args.Count -ge 1 -and $args[0] -eq 'untrust') { - $out = (& $amBin hook --quiet __SHELL__) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - } } diff --git a/crates/am/src/shell_wrappers/wrapper.zsh b/crates/am/src/shell_wrappers/wrapper.zsh index e5daec0d..6234fc14 100644 --- a/crates/am/src/shell_wrappers/wrapper.zsh +++ b/crates/am/src/shell_wrappers/wrapper.zsh @@ -3,21 +3,14 @@ am() { local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - tui|t) eval "$(command am reload __SHELL__)"; eval "$(command am hook __SHELL__)"; return ;; - esac - case "$1" in - use|u) eval "$(command am reload __SHELL__)"; return ;; - esac - case "$1:$2" in - profile:use|p:use|profile:u|p:u|profile:add|p:add|profile:a|p:a|profile:remove|p:remove|profile:r|p:r) eval "$(command am reload __SHELL__)" ;; - esac - case "$1" in - add|a|remove|r) - case "$*" in - *\ -l\ *|*\ --local\ *|*\ -l|*\ --local) eval "$(command am hook __SHELL__)" ;; - *) eval "$(command am reload __SHELL__)" ;; + add|a|remove|r|trust|tui|t) + eval "$(command am sync __SHELL__)" ;; + use|u|untrust) + eval "$(command am sync --quiet __SHELL__)" ;; + profile|p) + case "$2" in + use|u) eval "$(command am sync --quiet __SHELL__)" ;; + add|a|remove|r) eval "$(command am sync __SHELL__)" ;; esac ;; - trust) eval "$(command am hook __SHELL__)" ;; - untrust) eval "$(command am hook --quiet __SHELL__)" ;; esac -} \ No newline at end of file +} diff --git a/crates/am/src/subcommand.rs b/crates/am/src/subcommand.rs index 7dec96c5..8c61c115 100644 --- a/crates/am/src/subcommand.rs +++ b/crates/am/src/subcommand.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use anyhow::anyhow; use log::warn; +use serde::{Deserialize, Serialize}; /// Validates that a name is a safe shell identifier. /// @@ -86,23 +87,79 @@ impl SubcommandEntry { } /// Storage type for subcommand aliases. Key is the full colon-joined string -/// (e.g., "jj:b:l"), value is the Vec of long subcommands (e.g., ["branch", "list"]). -pub type SubcommandSet = BTreeMap>; - -/// Group subcommand entries by program name. -pub fn group_by_program(set: &SubcommandSet) -> BTreeMap> { - let mut groups: BTreeMap> = BTreeMap::new(); - for (key, values) in set { - match SubcommandEntry::parse_key(key, values.clone()) { - Ok(entry) => { - groups.entry(entry.program.clone()).or_default().push(entry); - } - Err(e) => { - warn!("Skipping invalid subcommand alias '{key}': {e}"); +/// (e.g., `"jj:b:l"`), value is the Vec of long subcommands (e.g., +/// `["branch", "list"]`). +/// +/// Wraps `BTreeMap>` as a newtype so the API is explicit +/// and the serde boundary is transparent (preserves `[subcommands]` TOML +/// layout). +#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)] +#[serde(transparent)] +pub struct SubcommandSet(BTreeMap>); + +impl AsRef>> for SubcommandSet { + fn as_ref(&self) -> &BTreeMap> { + &self.0 + } +} + +impl AsMut>> for SubcommandSet { + fn as_mut(&mut self) -> &mut BTreeMap> { + &mut self.0 + } +} + +impl SubcommandSet { + pub fn new() -> Self { + Self::default() + } + + /// Kept as a method so `#[serde(skip_serializing_if = "SubcommandSet::is_empty")]` + /// can reference it directly. All other access should go through `AsRef`/`AsMut`. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Group the entries by program name. Each entry whose key fails to parse + /// (mismatched short/long count, empty segment, etc.) is skipped with a + /// warning. Returns a `BTreeMap>` so callers + /// can iterate per-program. + pub fn group_by_program(&self) -> BTreeMap> { + let mut groups: BTreeMap> = BTreeMap::new(); + for (key, values) in self { + match SubcommandEntry::parse_key(key, values.clone()) { + Ok(entry) => { + groups.entry(entry.program.clone()).or_default().push(entry); + } + Err(e) => { + warn!("Skipping invalid subcommand alias '{key}': {e}"); + } } } + groups + } +} + +impl<'a> IntoIterator for &'a SubcommandSet { + type Item = (&'a String, &'a Vec); + type IntoIter = std::collections::btree_map::Iter<'a, String, Vec>; + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl IntoIterator for SubcommandSet { + type Item = (String, Vec); + type IntoIter = std::collections::btree_map::IntoIter>; + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl FromIterator<(String, Vec)> for SubcommandSet { + fn from_iter)>>(iter: I) -> Self { + Self(BTreeMap::from_iter(iter)) } - groups } #[cfg(test)] @@ -220,11 +277,13 @@ mod tests { #[test] fn group_by_program_groups_correctly() { let mut set = SubcommandSet::new(); - set.insert("jj:ab".into(), vec!["abandon".into()]); - set.insert("jj:b:l".into(), vec!["branch".into(), "list".into()]); - set.insert("git:co".into(), vec!["checkout".into()]); + set.as_mut().insert("jj:ab".into(), vec!["abandon".into()]); + set.as_mut() + .insert("jj:b:l".into(), vec!["branch".into(), "list".into()]); + set.as_mut() + .insert("git:co".into(), vec!["checkout".into()]); - let groups = group_by_program(&set); + let groups = set.group_by_program(); assert_eq!(groups.len(), 2); assert_eq!(groups["jj"].len(), 2); assert_eq!(groups["git"].len(), 1); @@ -233,22 +292,86 @@ mod tests { #[test] fn group_by_program_empty() { let set = SubcommandSet::new(); - let groups = group_by_program(&set); + let groups = set.group_by_program(); assert!(groups.is_empty()); } #[test] fn group_by_program_skips_invalid_entries() { let mut set = SubcommandSet::new(); - set.insert("jj:ab".into(), vec!["abandon".into()]); + set.as_mut().insert("jj:ab".into(), vec!["abandon".into()]); // mismatched counts — invalid - set.insert("jj:b:l".into(), vec!["branch".into()]); + set.as_mut().insert("jj:b:l".into(), vec!["branch".into()]); // no colon — invalid - set.insert("bad".into(), vec!["whatever".into()]); + set.as_mut().insert("bad".into(), vec!["whatever".into()]); - let groups = group_by_program(&set); + let groups = set.group_by_program(); assert_eq!(groups.len(), 1); assert_eq!(groups["jj"].len(), 1); assert_eq!(groups["jj"][0].short_subcommands, vec!["ab"]); } + + // --- SubcommandSet newtype API --- + + #[test] + fn subcommandset_basic_ops_via_as_ref_as_mut() { + let mut set = SubcommandSet::new(); + assert!(set.is_empty()); + + set.as_mut().insert("jj:ab".into(), vec!["abandon".into()]); + assert_eq!(set.as_ref().len(), 1); + assert!(set.as_ref().contains_key("jj:ab")); + assert_eq!( + set.as_ref().get("jj:ab"), + Some(&vec!["abandon".to_string()]) + ); + + let removed = set.as_mut().remove("jj:ab"); + assert_eq!(removed, Some(vec!["abandon".to_string()])); + assert!(set.is_empty()); + } + + #[test] + fn subcommandset_iteration_via_into_iterator() { + let set: SubcommandSet = [ + ("a:x".to_string(), vec!["one".to_string()]), + ("b:y".to_string(), vec!["two".to_string()]), + ] + .into_iter() + .collect(); + + // IntoIterator for &SubcommandSet lets for-loops work directly. + let keys: Vec<&str> = (&set).into_iter().map(|(k, _)| k.as_str()).collect(); + assert_eq!(keys, vec!["a:x", "b:y"]); + + // Owning IntoIterator yields (String, Vec). + let owned: Vec<(String, Vec)> = set.into_iter().collect(); + assert_eq!(owned.len(), 2); + } + + #[test] + fn subcommandset_serde_transparent() { + let set: SubcommandSet = [ + ("jj:ab".to_string(), vec!["abandon".to_string()]), + ( + "jj:b:l".to_string(), + vec!["branch".to_string(), "list".to_string()], + ), + ] + .into_iter() + .collect(); + + // Serializes as a plain map (not as a tuple-struct wrapper). + #[derive(serde::Serialize, serde::Deserialize)] + struct Wrapper { + subcommands: SubcommandSet, + } + let toml_str = toml::to_string(&Wrapper { subcommands: set }).unwrap(); + assert!(toml_str.contains("[subcommands]")); + assert!(toml_str.contains("\"jj:ab\" = [\"abandon\"]")); + + let parsed: Wrapper = toml::from_str(&toml_str).unwrap(); + assert_eq!(parsed.subcommands.as_ref().len(), 2); + assert_eq!(parsed.subcommands.as_ref()["jj:ab"], vec!["abandon"]); + } } diff --git a/crates/am/src/trust.rs b/crates/am/src/trust.rs index b6f12901..8311abcb 100644 --- a/crates/am/src/trust.rs +++ b/crates/am/src/trust.rs @@ -79,7 +79,7 @@ pub fn render_load_message( lines.push(format!(" {padded} \u{2192} {cmd}")); } - let subcmd_groups = crate::subcommand::group_by_program(subcommands); + let subcmd_groups = subcommands.group_by_program(); for (program, entries) in &subcmd_groups { lines.push(format!(" {program} (subcommands):")); for entry in entries { @@ -92,11 +92,6 @@ pub fn render_load_message( lines.join("\n") } -/// Render the "unloaded" info message shown when leaving a trusted directory. -pub fn render_unload_message(alias_names: &[&str]) -> String { - format!("am: unloaded .aliases: {}", alias_names.join(", ")) -} - #[cfg(test)] mod tests { use super::*; @@ -192,18 +187,6 @@ mod tests { assert!(msg.contains("cargo build")); } - #[test] - fn render_unload_message_comma_separated() { - let msg = render_unload_message(&["b", "t", "cb"]); - assert_eq!(msg, "am: unloaded .aliases: b, t, cb"); - } - - #[test] - fn render_unload_message_single() { - let msg = render_unload_message(&["b"]); - assert_eq!(msg, "am: unloaded .aliases: b"); - } - #[test] fn project_trust_path_returns_path_for_all_variants() { let path = PathBuf::from("/project/.aliases"); diff --git a/crates/am/src/update.rs b/crates/am/src/update.rs index 86623464..63ed1a81 100644 --- a/crates/am/src/update.rs +++ b/crates/am/src/update.rs @@ -3,8 +3,8 @@ pub use crate::app_model::AppModel; use crate::display::render_listing; use crate::effects::Effect; use crate::env_vars; -use crate::init::{generate_init, generate_reload}; -use crate::profile::AliasCollection; +use crate::init::generate_init; +use crate::precedence::{format_change_summary, Precedence}; use crate::project::ProjectAliases; use crate::shell::bash; use crate::shell::zsh; @@ -306,7 +306,7 @@ pub fn update(model: &mut AppModel, message: Message) -> Result match target { AliasTarget::Global => { - model.config.subcommands.remove(&original_key); + model.config.subcommands.as_mut().remove(&original_key); model.config.add_subcommand(new_key, long_subcommands); Ok(UpdateResult::effect(Effect::SaveConfig)) } @@ -324,7 +324,7 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Result { let profile = resolve_profile_mut(model, &target)?; - profile.subcommands.remove(&original_key); + profile.subcommands.as_mut().remove(&original_key); profile.add_subcommand(new_key, long_subcommands); Ok(UpdateResult::effect(Effect::SaveProfiles)) } @@ -361,7 +361,7 @@ pub fn update(model: &mut AppModel, message: Message) -> Result keys .iter() - .filter_map(|k| Some((k.clone(), subs.get(k)?.clone()))) + .filter_map(|k| Some((k.clone(), subs.as_ref().get(k)?.clone()))) .collect(), None => vec![], } @@ -415,7 +415,7 @@ pub fn update(model: &mut AppModel, message: Message) -> Result keys .iter() - .filter_map(|k| Some((k.clone(), subs.get(k)?.clone()))) + .filter_map(|k| Some((k.clone(), subs.as_ref().get(k)?.clone()))) .collect(), None => vec![], } @@ -447,7 +447,7 @@ pub fn update(model: &mut AppModel, message: Message) -> Result { for (key, _) in &pairs { - model.config.subcommands.remove(key); + model.config.subcommands.as_mut().remove(key); } } AliasTarget::Local => {} // handled via effects below @@ -455,7 +455,7 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Result Result (zsh::scan_external_functions(), zsh::scan_external_aliases()), @@ -591,23 +591,51 @@ pub fn update(model: &mut AppModel, message: Message) -> Result = prev_global - .split(',') - .chain(prev_project.split(',')) - .filter(|s| !s.is_empty()) - .collect(); - for name in all_prev { + let prev_global = std::env::var(env_vars::AM_ALIASES).ok(); + + // Per-key subcommand entries (containing ':') are tracking-only, + // not shell functions. Program-level wrapper names (no ':') are + // picked up from prev_global because PrecedenceDiff::render writes + // them there alongside regular aliases. + let mut names: std::collections::BTreeSet = + crate::precedence::AliasWithHashList::parse(prev_global.as_deref()) + .iter() + .map(|e| e.name()) + .filter(|n| !n.contains(':')) + .map(String::from) + .collect(); + + // Union with shell introspection for bash/zsh. + match shell { + Shell::Zsh => { + for n in zsh::scan_external_functions().iter() { + names.insert(n.clone()); + } + for n in zsh::scan_external_aliases().iter() { + names.insert(n.clone()); + } + } + Shell::Bash => { + for n in bash::scan_external_functions().iter() { + names.insert(n.clone()); + } + for n in bash::scan_external_aliases().iter() { + names.insert(n.clone()); + } + } + _ => {} + } + + for name in &names { output.push_str(&shell_impl.force_unalias(name)); output.push('\n'); } - // Clear project-alias tracking so __am_hook reloads them fresh - // instead of assuming they're still loaded. - output.push_str(&shell_impl.unset_env(env_vars::AM_PROJECT_ALIASES)); - output.push('\n'); output.push_str(&shell_impl.unset_env(env_vars::AM_PROJECT_PATH)); output.push('\n'); + output.push_str(&shell_impl.unset_env(env_vars::AM_SUBCOMMANDS)); + output.push('\n'); + output.push_str(&shell_impl.unset_env(env_vars::AM_ALIASES)); + output.push('\n'); } output.push_str(&generate_init( @@ -619,60 +647,125 @@ pub fn update(model: &mut AppModel, message: Message) -> Result { - let resolved = model + Message::Sync(shell, quiet) => { + let prev_aliases = std::env::var(env_vars::AM_ALIASES).ok(); + let prev_subs = std::env::var(env_vars::AM_SUBCOMMANDS).ok(); + let prev_project_path = std::env::var(env_vars::AM_PROJECT_PATH).ok(); + + let shell_cfg = model.config.shell.clone(); + let cwd = model.cwd.clone(); + let shell_impl = + shell + .clone() + .as_shell(&shell_cfg, Default::default(), Default::default()); + + let resolved_aliases = model .profile_config() .resolve_active_aliases(&model.session.active_profiles); let resolved_subs = model .profile_config() .resolve_active_subcommands(&model.session.active_profiles); - let mut all_subs = model.config.subcommands.clone(); - for (k, v) in resolved_subs { - all_subs.insert(k, v); - } - let prev = std::env::var(env_vars::AM_ALIASES).ok(); - let ctx = ShellContext { - shell: &shell, - cfg: &model.config.shell, - cwd: &model.cwd, - external_functions: Default::default(), - external_aliases: Default::default(), + + // Resolve project state. `project_path` is the `.aliases` file path + // (regardless of trust); `include_project` is true only when trusted. + let project_path = model.project_trust().map(|t| t.path().to_path_buf()); + let is_direct = project_path + .as_deref() + .is_some_and(|p| p.parent().is_some_and(|pp| pp == cwd)); + let already_seen_path = match (prev_project_path.as_deref(), project_path.as_deref()) { + (Some(prev), Some(cur)) => std::path::Path::new(prev) == cur, + _ => false, }; - let output = generate_reload( - &ctx, - &model.config.aliases, - &resolved, - &all_subs, - prev.as_deref(), - ); - if !output.is_empty() { - print!("{output}"); + let show_warn = !quiet && is_direct && !already_seen_path; + + let mut lines: Vec = Vec::new(); + let mut security_changed = false; + let mut include_project = false; + match model.project_trust() { + Some(crate::trust::ProjectTrust::Trusted(..)) => { + include_project = true; + } + Some(crate::trust::ProjectTrust::Unknown(_)) => { + if show_warn { + lines.push(shell_impl.echo( + "am: .aliases found but not trusted. Run 'am trust' to review and allow.", + )); + } + } + Some(crate::trust::ProjectTrust::Tampered(_)) => { + security_changed = true; + if show_warn { + lines.push(shell_impl.echo( + "am: .aliases was modified since last trusted. Run 'am trust' to review and allow.", + )); + } + } + Some(crate::trust::ProjectTrust::Untrusted(_)) | None => {} } - Ok(UpdateResult::done()) - } - Message::Hook(shell, quiet) => { - let prev = std::env::var(env_vars::AM_PROJECT_ALIASES).ok(); - let prev_project_path = std::env::var(env_vars::AM_PROJECT_PATH).ok(); - let shell_cfg = model.config.shell.clone(); - let cwd = model.cwd.clone(); - let ctx = ShellContext { - shell: &shell, - cfg: &shell_cfg, - cwd: &cwd, - external_functions: Default::default(), - external_aliases: Default::default(), + + let (project_aliases, project_subs) = if include_project { + model.project_alias_set_and_subcommands() + } else { + ( + crate::AliasSet::default(), + crate::subcommand::SubcommandSet::new(), + ) }; - let (output, security_changed) = crate::hook::generate_hook_with_security( - &ctx, - prev.as_deref(), - prev_project_path.as_deref(), - model.security_config_mut(), - quiet, - ) - .map_err(|e| UpdateError::Other(e.to_string()))?; - if !output.is_empty() { - print!("{output}"); + + // Fresh project load: we're directly in a trusted project we haven't + // seen before (or cd'd into a different project). Triggers the full + // listing instead of the compact incremental summary. + let is_fresh_project_load = include_project && is_direct && !already_seen_path; + + let diff = Precedence::new() + .with_global(&model.config.aliases, &model.config.subcommands) + .with_profiles(&resolved_aliases, &resolved_subs) + .with_project(&project_aliases, &project_subs) + .with_shell_state_from_env(prev_aliases.as_deref(), prev_subs.as_deref()) + .resolve(); + + // ── Human-readable messaging ──────────────────────── + if !quiet { + if is_fresh_project_load { + for line in + crate::trust::render_load_message(&project_aliases, &project_subs).lines() + { + lines.push(shell_impl.echo(line)); + } + } else if let Some(msg) = diff.change_summary() { + lines.push(shell_impl.echo(&msg)); + } + } + + let rendered = diff.render(shell_impl.as_ref()); + if !rendered.is_empty() { + lines.push(rendered); } + + // ── _AM_PROJECT_PATH bookkeeping ─────────────────── + // Always track the current project path (regardless of trust) so + // subsequent syncs can detect "first time in this project". + let current_path_str = project_path.as_ref().map(|p| p.display().to_string()); + match (prev_project_path.as_deref(), current_path_str.as_deref()) { + (Some(prev), Some(cur)) if prev == cur => {} + (_, Some(cur)) => { + lines.push(shell_impl.set_env(env_vars::AM_PROJECT_PATH, cur)); + } + (Some(_), None) => { + lines.push(shell_impl.unset_env(env_vars::AM_PROJECT_PATH)); + } + (None, None) => {} + } + + let joined = lines + .into_iter() + .filter(|l| !l.is_empty()) + .collect::>() + .join("\n"); + if !joined.is_empty() { + print!("{joined}"); + } + if security_changed { Ok(UpdateResult::effect(Effect::SaveSecurity)) } else { @@ -686,25 +779,17 @@ pub fn update(model: &mut AppModel, message: Message) -> Result Result Result Result std::collections::BTreeSet { + let Some(project) = model.project_aliases() else { + return std::collections::BTreeSet::new(); + }; + let mut set: std::collections::BTreeSet = project + .aliases + .iter() + .map(|(n, _)| n.as_ref().to_string()) + .collect(); + set.extend(project.subcommands.as_ref().keys().cloned()); + set +} + +/// Profile items (regular alias names + subcommand keys). +fn profile_items(profile: &Profile) -> Vec { + let mut items: Vec = profile + .aliases + .iter() + .map(|(n, _)| n.as_ref().to_string()) + .collect(); + items.extend(profile.subcommands.as_ref().keys().cloned()); + items +} + +/// Build the user-facing message for a profile activation/deactivation, +/// highlighting which of the profile's aliases are shadowed by the project's +/// `.aliases`. `activated = false` means the profile is being deactivated. +fn profile_toggle_message( + name: &str, + activated: bool, + position: Option, + profile_aliases: &[String], + project_names: &std::collections::BTreeSet, +) -> String { + if profile_aliases.is_empty() { + let action = if activated { + "activated" + } else { + "deactivated" + }; + return match position { + Some(pos) => format!("am: profile {name} {action} at position {pos}, 0 aliases"), + None => format!("am: profile {name} {action}, 0 aliases"), + }; + } + + let (unshadowed, shadowed): (Vec<&str>, Vec<&str>) = profile_aliases + .iter() + .map(|s| s.as_str()) + .partition(|n| !project_names.contains(*n)); + + let head = match (activated, position) { + (true, Some(pos)) => format!("am: profile {name} activated at position {pos}"), + (true, None) => format!("am: profile {name} activated"), + (false, _) => format!("am: profile {name} deactivated"), + }; + + let (primary_verb, secondary_verb) = if activated { + ("loaded", "shadowed by .aliases") + } else { + ("unloaded", "kept by .aliases") + }; + + // "All shadowed" / "all kept" is a special-case phrasing only the profile + // path uses — sync never has this shape. Keep it inline. + if unshadowed.is_empty() && !shadowed.is_empty() { + return format!( + "{head} — all {} {secondary_verb}: {}", + shadowed.len(), + shadowed.join(", ") + ); + } + + format_change_summary( + &head, + &[(primary_verb, &unshadowed), (secondary_verb, &shadowed)], + ) + .expect("profile_aliases non-empty but produced no sections") +} + fn resolve_profile<'a>( model: &'a AppModel, target: &AliasTarget, @@ -1041,6 +1205,7 @@ mod tests { model .config .subcommands + .as_mut() .insert("jj:ab".into(), vec!["abandon".into()]); let result = update( &mut model, @@ -1052,9 +1217,9 @@ mod tests { }, ) .unwrap(); - assert!(!model.config.subcommands.contains_key("jj:ab")); + assert!(!model.config.subcommands.as_ref().contains_key("jj:ab")); assert_eq!( - model.config.subcommands.get("jj:a"), + model.config.subcommands.as_ref().get("jj:a"), Some(&vec!["abandon".to_string()]) ); assert!(result @@ -1069,6 +1234,7 @@ mod tests { model .config .subcommands + .as_mut() .insert("jj:ab".into(), vec!["abandon".into()]); model.profile_config_mut().add_profile("rust").unwrap(); let _ = update( @@ -1082,11 +1248,11 @@ mod tests { .unwrap(); let profile = model.profile_config().get_profile_by_name("rust").unwrap(); assert_eq!( - profile.subcommands.get("jj:ab"), + profile.subcommands.as_ref().get("jj:ab"), Some(&vec!["abandon".to_string()]) ); // Source preserved - assert!(model.config.subcommands.contains_key("jj:ab")); + assert!(model.config.subcommands.as_ref().contains_key("jj:ab")); } #[test] @@ -1095,6 +1261,7 @@ mod tests { model .config .subcommands + .as_mut() .insert("jj:ab".into(), vec!["abandon".into()]); model.profile_config_mut().add_profile("rust").unwrap(); let _ = update( @@ -1106,9 +1273,9 @@ mod tests { }, ) .unwrap(); - assert!(!model.config.subcommands.contains_key("jj:ab")); + assert!(!model.config.subcommands.as_ref().contains_key("jj:ab")); let profile = model.profile_config().get_profile_by_name("rust").unwrap(); - assert!(profile.subcommands.contains_key("jj:ab")); + assert!(profile.subcommands.as_ref().contains_key("jj:ab")); } #[test] @@ -1543,8 +1710,8 @@ mod tests { ) .unwrap(); - assert_eq!(model.config.subcommands.len(), 1); - assert_eq!(model.config.subcommands["jj:ab"], vec!["abandon"]); + assert_eq!(model.config.subcommands.as_ref().len(), 1); + assert_eq!(model.config.subcommands.as_ref()["jj:ab"], vec!["abandon"]); assert_eq!(result.effects, vec![Effect::SaveConfig]); } @@ -1605,7 +1772,7 @@ mod tests { assert_eq!(result.effects, vec![Effect::SaveProfiles]); let profile = model.profile_config().get_profile_by_name("rust").unwrap(); - assert_eq!(profile.subcommands.len(), 1); + assert_eq!(profile.subcommands.as_ref().len(), 1); } #[test] diff --git a/crates/am/tests/e2e.rs b/crates/am/tests/e2e.rs index 21207170..50638c13 100644 --- a/crates/am/tests/e2e.rs +++ b/crates/am/tests/e2e.rs @@ -8,17 +8,17 @@ /// CI primes the shell environment before invoking this suite. use std::process::Command; -/// Proof that the `_AM_DETECTING_ALIASES` guard works: when `am hook` is invoked +/// Proof that the `_AM_DETECTING_ALIASES` guard works: when `am sync` is invoked /// while the env var is set (as it happens during alias scanning), the binary must /// exit cleanly with no stdout so that `eval "$(...)"` in shell startup scripts /// is a no-op — preventing infinite recursion. /// -/// Without the guard `am hook zsh` in a directory containing an `.aliases` file +/// Without the guard `am sync zsh` in a directory containing an `.aliases` file /// outputs shell code (at minimum a trust warning), which would be eval'd by the /// child zsh and could trigger another scan cycle. #[test] #[ignore = "e2e: requires the am binary and a zsh installation"] -fn am_hook_is_silent_when_am_detecting_aliases_guard_is_active() { +fn am_sync_is_silent_when_am_detecting_aliases_guard_is_active() { let dir = tempfile::tempdir().expect("failed to create temp dir"); std::fs::write( dir.path().join(".aliases"), @@ -27,9 +27,8 @@ fn am_hook_is_silent_when_am_detecting_aliases_guard_is_active() { .unwrap(); let output = Command::new(env!("CARGO_BIN_EXE_am")) - .args(["hook", "zsh"]) + .args(["sync", "zsh"]) .env("_AM_DETECTING_ALIASES", "1") - .env_remove("_AM_PROJECT_ALIASES") .env_remove("_AM_PROJECT_PATH") .current_dir(dir.path()) .output() @@ -43,7 +42,7 @@ fn am_hook_is_silent_when_am_detecting_aliases_guard_is_active() { let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.is_empty(), - "am hook must produce no stdout when _AM_DETECTING_ALIASES is set\n\ + "am sync must produce no stdout when _AM_DETECTING_ALIASES is set\n\ (any output would be eval'd by the shell and could cause recursion)\n\ got: {stdout:?}" ); diff --git a/crates/am/tests/snapshots.rs b/crates/am/tests/snapshots.rs index bb3d7995..4c16e99f 100644 --- a/crates/am/tests/snapshots.rs +++ b/crates/am/tests/snapshots.rs @@ -1,16 +1,13 @@ use amoxide::alias::{AliasConflict, MergeResult}; -use amoxide::config::{FishConfig, ShellsTomlConfig}; +use amoxide::config::ShellsTomlConfig; use amoxide::display::{render_listing, render_profiles}; use amoxide::exchange::{ render_import_summary, render_suspicious_warning, ExportAll, SuspiciousAlias, }; -use amoxide::hook::generate_hook_with_security; -use amoxide::init::{generate_force_init, generate_init, generate_reload}; +use amoxide::init::generate_init; use amoxide::project::ProjectAliases; -use amoxide::security::SecurityConfig; use amoxide::shell::{Shell, ShellContext}; use amoxide::subcommand::SubcommandSet; -use amoxide::trust::compute_file_hash; use amoxide::{AliasName, AliasSet, ProfileConfig, TomlAlias}; static DEFAULT_CFG: std::sync::LazyLock = @@ -212,8 +209,12 @@ fn snapshot_init_fish_deep_chain() { fn snapshot_init_fish_with_simple_subcommands() { let globals = aliases(&[("gs", "git status")]); let mut subcommands = SubcommandSet::new(); - subcommands.insert("jj:ab".into(), vec!["abandon".into()]); - subcommands.insert("jj:new".into(), vec!["new --no-edit".into()]); + subcommands + .as_mut() + .insert("jj:ab".into(), vec!["abandon".into()]); + subcommands + .as_mut() + .insert("jj:new".into(), vec!["new --no-edit".into()]); let output = generate_init( &default_ctx(&Shell::Fish), &globals, @@ -226,17 +227,23 @@ fn snapshot_init_fish_with_simple_subcommands() { #[test] fn snapshot_init_bash_with_kubectl_subcommands() { let mut subcommands = SubcommandSet::new(); - subcommands.insert("kubectl:get:po".into(), vec!["get".into(), "pods".into()]); - subcommands.insert( + subcommands + .as_mut() + .insert("kubectl:get:po".into(), vec!["get".into(), "pods".into()]); + subcommands.as_mut().insert( "kubectl:get:svc".into(), vec!["get".into(), "services".into()], ); - subcommands.insert("kubectl:apply:f".into(), vec!["apply".into(), "-f".into()]); - subcommands.insert( + subcommands + .as_mut() + .insert("kubectl:apply:f".into(), vec!["apply".into(), "-f".into()]); + subcommands.as_mut().insert( "kubectl:rollout:status".into(), vec!["rollout".into(), "status".into()], ); - subcommands.insert("kubectl:logs:f".into(), vec!["logs".into(), "-f".into()]); + subcommands + .as_mut() + .insert("kubectl:logs:f".into(), vec!["logs".into(), "-f".into()]); let output = generate_init( &default_ctx(&Shell::Bash), &AliasSet::default(), @@ -246,126 +253,6 @@ fn snapshot_init_bash_with_kubectl_subcommands() { insta::assert_snapshot!(output); } -// ═══════════════════════════════════════════════════════════════════════ -// Reload snapshots -// ═══════════════════════════════════════════════════════════════════════ - -#[test] -fn snapshot_reload_fish_switch_profile() { - let config = git_conventional_config(); - let resolved = config.resolve_active_aliases(&["git", "git-conventional"]); - let output = generate_reload( - &default_ctx(&Shell::Fish), - &AliasSet::default(), - &resolved, - &SubcommandSet::new(), - Some("gs,cm"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_zsh_switch_profile() { - let config = git_conventional_config(); - let resolved = config.resolve_active_aliases(&["git", "git-conventional"]); - let output = generate_reload( - &default_ctx(&Shell::Zsh), - &AliasSet::default(), - &resolved, - &SubcommandSet::new(), - Some("gs,cm"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_powershell_switch_profile() { - let config = git_conventional_config(); - let resolved = config.resolve_active_aliases(&["git-conventional"]); - let output = generate_reload( - &default_ctx(&Shell::Powershell), - &AliasSet::default(), - &resolved, - &SubcommandSet::new(), - Some("gs,cm"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_bash_switch_profile() { - let config = git_conventional_config(); - let resolved = config.resolve_active_aliases(&["git", "git-conventional"]); - let output = generate_reload( - &default_ctx(&Shell::Bash), - &AliasSet::default(), - &resolved, - &SubcommandSet::new(), - Some("gs,cm"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_fish_after_global_add() { - // Simulates: user had profile aliases loaded, then adds a global alias - let globals = aliases(&[("ll", "ls -lha")]); - let config = git_conventional_config(); - let resolved = config.resolve_active_aliases(&["git", "git-conventional"]); - let output = generate_reload( - &default_ctx(&Shell::Fish), - &globals, - &resolved, - &SubcommandSet::new(), - Some("cm,cmf,gs"), // previously tracked aliases - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_fish_globals_only_no_profile() { - // No active profile, only globals - let globals = aliases(&[("ll", "ls -lha"), ("gs", "git status")]); - let output = generate_reload( - &default_ctx(&Shell::Fish), - &globals, - &AliasSet::default(), - &SubcommandSet::new(), - Some("old"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_zsh_after_global_add() { - let globals = aliases(&[("ll", "ls -lha")]); - let config = git_conventional_config(); - let resolved = config.resolve_active_aliases(&["git", "git-conventional"]); - let output = generate_reload( - &default_ctx(&Shell::Zsh), - &globals, - &resolved, - &SubcommandSet::new(), - Some("cm,cmf,gs"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_bash_after_global_add() { - let globals = aliases(&[("ll", "ls -lha")]); - let config = git_conventional_config(); - let resolved = config.resolve_active_aliases(&["git", "git-conventional"]); - let output = generate_reload( - &default_ctx(&Shell::Bash), - &globals, - &resolved, - &SubcommandSet::new(), - Some("cm,cmf,gs"), - ); - insta::assert_snapshot!(output); -} - #[test] fn snapshot_init_fish_globals_and_multi_profile() { // Full scenario: globals + multiple active profiles @@ -381,255 +268,6 @@ fn snapshot_init_fish_globals_and_multi_profile() { insta::assert_snapshot!(output); } -#[test] -fn snapshot_reload_after_profile_removed() { - // Scenario: rust profile had its own aliases, then was removed from active set - // Now only git's aliases should remain - let config = profiles(indoc! {r#" - [[profiles]] - name = "git" - [profiles.aliases] - gs = "git status" - cm = "git commit -sm" - - [[profiles]] - name = "rust" - [profiles.aliases] - ct = "cargo test" - "#}); - // rust is no longer in the active set - let resolved = config.resolve_active_aliases(&["git"]); - // Previously tracked: cm,ct,gs (git's + rust's aliases were all loaded) - let output = generate_reload( - &default_ctx(&Shell::Fish), - &AliasSet::default(), - &resolved, - &SubcommandSet::new(), - Some("cm,ct,gs"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_after_parent_profile_removed() { - // Scenario: git was removed, git-conventional is now standalone - let config = profiles(indoc! {r#" - [[profiles]] - name = "git-conventional" - [profiles.aliases] - cmf = "cm feat: {{@}}" - "#}); - // git was removed, only git-conventional active - let resolved = config.resolve_active_aliases(&["git-conventional"]); - // Previously tracked: cm,cmf,gs (git's + git-conventional's were loaded) - let output = generate_reload( - &default_ctx(&Shell::Fish), - &AliasSet::default(), - &resolved, - &SubcommandSet::new(), - Some("cm,cmf,gs"), - ); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_reload_after_active_set_changed() { - // Scenario: previously had git+rust active, now changed to base+rust - let config = profiles(indoc! {r#" - [[profiles]] - name = "base" - [profiles.aliases] - ll = "ls -lha" - - [[profiles]] - name = "git" - [profiles.aliases] - gs = "git status" - - [[profiles]] - name = "rust" - [profiles.aliases] - ct = "cargo test" - "#}); - let resolved = config.resolve_active_aliases(&["base", "rust"]); - // Previously tracked: ct,gs (from rust + git) - // Now should have: ct,ll (from rust + base) - let output = generate_reload( - &default_ctx(&Shell::Fish), - &AliasSet::default(), - &resolved, - &SubcommandSet::new(), - Some("ct,gs"), - ); - insta::assert_snapshot!(output); -} - -// ═══════════════════════════════════════════════════════════════════════ -// Hook snapshots -// ═══════════════════════════════════════════════════════════════════════ - -#[test] -fn snapshot_hook_fish_with_aliases() { - let dir = tempfile::tempdir().unwrap(); - let aliases_path = dir.path().join(".aliases"); - fs::write( - &aliases_path, - indoc! {r#" - [aliases] - t = "cargo test" - b = "cargo build" - "#}, - ) - .unwrap(); - - let mut security = SecurityConfig::default(); - let hash = compute_file_hash(&aliases_path).unwrap(); - security.trust(&aliases_path, &hash); - - let ctx = ShellContext { - shell: &Shell::Fish, - cfg: &DEFAULT_CFG, - cwd: dir.path(), - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let (output, _) = generate_hook_with_security(&ctx, None, None, &mut security, false).unwrap(); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_hook_zsh_with_aliases() { - let dir = tempfile::tempdir().unwrap(); - let aliases_path = dir.path().join(".aliases"); - fs::write( - &aliases_path, - indoc! {r#" - [aliases] - t = "cargo test" - b = "cargo build" - "#}, - ) - .unwrap(); - - let mut security = SecurityConfig::default(); - let hash = compute_file_hash(&aliases_path).unwrap(); - security.trust(&aliases_path, &hash); - - let ctx = ShellContext { - shell: &Shell::Zsh, - cfg: &DEFAULT_CFG, - cwd: dir.path(), - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let (output, _) = generate_hook_with_security(&ctx, None, None, &mut security, false).unwrap(); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_hook_powershell_with_aliases() { - let dir = tempfile::tempdir().unwrap(); - let aliases_path = dir.path().join(".aliases"); - fs::write( - &aliases_path, - indoc! {r#" - [aliases] - t = "cargo test" - b = "cargo build" - "#}, - ) - .unwrap(); - - let mut security = SecurityConfig::default(); - let hash = compute_file_hash(&aliases_path).unwrap(); - security.trust(&aliases_path, &hash); - - let ctx = ShellContext { - shell: &Shell::Powershell, - cfg: &DEFAULT_CFG, - cwd: dir.path(), - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let (output, _) = generate_hook_with_security(&ctx, None, None, &mut security, false).unwrap(); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_hook_bash_with_aliases() { - let dir = tempfile::tempdir().unwrap(); - let aliases_path = dir.path().join(".aliases"); - fs::write( - &aliases_path, - indoc! {r#" - [aliases] - t = "cargo test" - b = "cargo build" - "#}, - ) - .unwrap(); - - let mut security = SecurityConfig::default(); - let hash = compute_file_hash(&aliases_path).unwrap(); - security.trust(&aliases_path, &hash); - - let ctx = ShellContext { - shell: &Shell::Bash, - cfg: &DEFAULT_CFG, - cwd: dir.path(), - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let (output, _) = generate_hook_with_security(&ctx, None, None, &mut security, false).unwrap(); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_hook_fish_transition() { - let dir = tempfile::tempdir().unwrap(); - let aliases_path = dir.path().join(".aliases"); - fs::write( - &aliases_path, - indoc! {r#" - [aliases] - t = "make test" - "#}, - ) - .unwrap(); - - let mut security = SecurityConfig::default(); - let hash = compute_file_hash(&aliases_path).unwrap(); - security.trust(&aliases_path, &hash); - - let ctx = ShellContext { - shell: &Shell::Fish, - cfg: &DEFAULT_CFG, - cwd: dir.path(), - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let (output, _) = - generate_hook_with_security(&ctx, Some("old_a,old_b"), None, &mut security, false).unwrap(); - insta::assert_snapshot!(output); -} - -#[test] -fn snapshot_hook_fish_leaving_project() { - let dir = tempfile::tempdir().unwrap(); - // No .aliases file - let mut security = SecurityConfig::default(); - let ctx = ShellContext { - shell: &Shell::Fish, - cfg: &DEFAULT_CFG, - cwd: dir.path(), - external_functions: Default::default(), - external_aliases: Default::default(), - }; - let (output, _) = - generate_hook_with_security(&ctx, Some("old_a,old_b"), None, &mut security, false).unwrap(); - insta::assert_snapshot!(output); -} - // ═══════════════════════════════════════════════════════════════════════ // Display snapshots // ═══════════════════════════════════════════════════════════════════════ @@ -1043,72 +681,188 @@ fn snapshot_share_help() { insta::assert_snapshot!(output); } -fn abbr_ctx(shell: &Shell) -> ShellContext<'_> { - static ABBR_CFG: std::sync::LazyLock = - std::sync::LazyLock::new(|| ShellsTomlConfig { - fish: Some(FishConfig { use_abbr: true }), - }); - ShellContext { - shell, - cfg: &ABBR_CFG, - cwd: std::path::Path::new("/tmp"), - external_functions: Default::default(), - external_aliases: Default::default(), - } +#[test] +fn sync_fresh_load_emits_aliases_and_env_var() { + use amoxide::precedence::Precedence; + let aliases = aliases(&[("gs", "git status")]); + let diff = Precedence::new() + .with_profiles(&aliases, &SubcommandSet::new()) + .resolve(); + let shell = Shell::Fish.as_shell(&Default::default(), Default::default(), Default::default()); + let out = diff.render(shell.as_ref()); + assert!(out.contains("function gs\n git status $argv\nend")); + assert!(out.contains("_AM_ALIASES")); + assert!(out.contains("gs|")); } #[test] -fn snapshot_init_fish_force_with_tracked_aliases() { - // Simulates: _AM_ALIASES=gs,ll is set, user runs am init --force fish. - // force_unalias must emit both cleanup forms for each tracked name, - // followed by the normal init output. - let prev_names = vec!["gs".to_string(), "ll".to_string()]; - let output = generate_force_init( - &default_ctx(&Shell::Fish), - &aliases(&[("gs", "git status"), ("ll", "ls -lha")]), - &AliasSet::default(), - &SubcommandSet::new(), - &prev_names, +fn sync_tampered_returns_save_security_effect_and_excludes_project() { + use amoxide::app_model::AppModel; + use amoxide::messages::Message; + use amoxide::shell::Shell; + use amoxide::update::update; + + let dir = tempfile::tempdir().unwrap(); + let aliases_path = dir.path().join(".aliases"); + fs::write(&aliases_path, "[aliases]\nt = \"cargo test\"\n").unwrap(); + + let mut sec = amoxide::security::SecurityConfig::default(); + // Trust with a wrong hash to force tamper. + sec.trust(&aliases_path, "wrong_hash"); + let mut model = AppModel::new_with_security( + amoxide::Config::default(), + amoxide::ProfileConfig::default(), + sec, + ) + .with_cwd(dir.path().to_path_buf()); + + // Smoke test: we don't care about stdout, just the effect list. + let res = update(&mut model, Message::Sync(Shell::Fish, true)).unwrap(); + assert!( + res.effects + .iter() + .any(|e| matches!(e, amoxide::Effect::SaveSecurity)), + "tampered file must trigger SaveSecurity effect" ); +} + +#[cfg(feature = "test-util")] +#[test] +fn init_force_unloads_introspected_names_with_hash_suffix_stripped() { + use amoxide::app_model::AppModel; + use amoxide::messages::Message; + use amoxide::shell::Shell; + use amoxide::update::update; + + // Simulate a prior session where _AM_ALIASES held name|hash entries. + // The force init must emit `unalias name` (no `|hash` suffix). + std::env::set_var("_AM_ALIASES", "b|abc1234,t|def5678"); + + let dir = tempfile::tempdir().unwrap(); + let mut model = AppModel::load_from(dir.path().to_path_buf()); + + // The real coverage is the snapshot added in Task 15. Here we just + // assert the handler completes without panicking. + let res = update(&mut model, Message::InitShell(Shell::Fish, true)); + assert!(res.is_ok()); + + std::env::remove_var("_AM_ALIASES"); +} + +// ═══════════════════════════════════════════════════════════════════════ +// Sync snapshots — cover behaviors that snapshot_hook_* and +// snapshot_reload_* used to exercise, now through the Precedence Engine. +// ═══════════════════════════════════════════════════════════════════════ + +#[test] +fn snapshot_sync_fish_fresh_load_project_only() { + use amoxide::precedence::Precedence; + let project = aliases(&[("b", "cargo build"), ("t", "cargo test")]); + let shell = Shell::Fish.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .resolve(); + let output = diff.render(shell.as_ref()); insta::assert_snapshot!(output); } #[test] -fn snapshot_init_fish_abbr_force_with_tracked_aliases() { - // Same but with use_abbr = true — cleanup must still emit both forms. - let prev_names = vec!["gs".to_string()]; - let output = generate_force_init( - &abbr_ctx(&Shell::Fish), - &aliases(&[("gs", "git status")]), - &AliasSet::default(), - &SubcommandSet::new(), - &prev_names, - ); +fn snapshot_sync_bash_fresh_load_project_only() { + use amoxide::precedence::Precedence; + let project = aliases(&[("b", "cargo build")]); + let shell = Shell::Bash.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .resolve(); + let output = diff.render(shell.as_ref()); insta::assert_snapshot!(output); } #[test] -fn snapshot_init_fish_force_no_previous() { - // --force with no tracked aliases: no cleanup lines, just normal init. - let output = generate_force_init( - &default_ctx(&Shell::Fish), - &aliases(&[("gs", "git status")]), - &AliasSet::default(), - &SubcommandSet::new(), - &[], - ); +fn snapshot_sync_zsh_fresh_load_project_only() { + use amoxide::precedence::Precedence; + let project = aliases(&[("b", "cargo build")]); + let shell = Shell::Zsh.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .resolve(); + let output = diff.render(shell.as_ref()); insta::assert_snapshot!(output); } #[test] -fn snapshot_init_bash_force_with_tracked_aliases() { - let prev_names = vec!["gs".to_string()]; - let output = generate_force_init( - &default_ctx(&Shell::Bash), - &aliases(&[("gs", "git status")]), - &AliasSet::default(), - &SubcommandSet::new(), - &prev_names, - ); +fn snapshot_sync_powershell_fresh_load_project_only() { + use amoxide::precedence::Precedence; + let project = aliases(&[("b", "cargo build")]); + let shell = + Shell::Powershell.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .resolve(); + let output = diff.render(shell.as_ref()); + insta::assert_snapshot!(output); +} + +#[test] +fn snapshot_sync_fish_transition_to_new_project() { + use amoxide::precedence::Precedence; + let project = aliases(&[("new1", "echo new")]); + let prev_hash = amoxide::trust::compute_short_hash(b"echo old"); + let prev = format!("old1|{prev_hash}"); + let shell = Shell::Fish.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .with_shell_state_from_env(Some(&prev), None) + .resolve(); + let output = diff.render(shell.as_ref()); + insta::assert_snapshot!(output); +} + +#[test] +fn snapshot_sync_fish_leaving_project_with_shadow_restoration() { + use amoxide::precedence::Precedence; + // Previously the project shadowed a profile alias `t`. Now we've left the + // project directory — effective `t` reverts to the profile value. The + // stored hash was the project's; new effective hash is the profile's. + // The engine must re-emit the profile `t` (shadow restoration). + let profile = aliases(&[("t", "cargo test"), ("ll", "ls -lha")]); + let project_hash = amoxide::trust::compute_short_hash(b"cargo test --release"); + let ll_hash = amoxide::trust::compute_short_hash(b"ls -lha"); + let prev = format!("t|{project_hash},b|aaa1111,ll|{ll_hash}"); + let shell = Shell::Fish.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_profiles(&profile, &SubcommandSet::new()) + .with_shell_state_from_env(Some(&prev), None) + .resolve(); + let output = diff.render(shell.as_ref()); + insta::assert_snapshot!(output); +} + +#[test] +fn snapshot_sync_fish_incremental_one_alias_updated() { + use amoxide::precedence::Precedence; + let project = aliases(&[("b", "cargo build --release"), ("t", "cargo test")]); + let old_b_hash = amoxide::trust::compute_short_hash(b"cargo build"); + let t_hash = amoxide::trust::compute_short_hash(b"cargo test"); + let prev = format!("b|{old_b_hash},t|{t_hash}"); + let shell = Shell::Fish.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_project(&project, &SubcommandSet::new()) + .with_shell_state_from_env(Some(&prev), None) + .resolve(); + let output = diff.render(shell.as_ref()); + insta::assert_snapshot!(output); +} + +#[test] +fn snapshot_sync_bash_subcommand_wrapper_fresh_load() { + use amoxide::precedence::Precedence; + let mut subs = SubcommandSet::new(); + subs.as_mut().insert("jj:ab".into(), vec!["abandon".into()]); + let shell = Shell::Bash.as_shell(&Default::default(), Default::default(), Default::default()); + let diff = Precedence::new() + .with_project(&AliasSet::default(), &subs) + .resolve(); + let output = diff.render(shell.as_ref()); insta::assert_snapshot!(output); } diff --git a/crates/am/tests/snapshots/snapshots__snapshot_hook_bash_with_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_hook_bash_with_aliases.snap deleted file mode 100644 index cf55a960..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_hook_bash_with_aliases.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -assertion_line: 584 -expression: output ---- -printf '%s\n' 'am: loaded .aliases' -printf '%s\n' ' b → cargo build' -printf '%s\n' ' t → cargo test' -alias b="cargo build" -alias t="cargo test" -export _AM_PROJECT_ALIASES="b|b58de66,t|ab61de4" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_leaving_project.snap b/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_leaving_project.snap deleted file mode 100644 index 6306434d..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_leaving_project.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e old_a -functions -e old_b -echo 'am: unloaded .aliases: old_a, old_b' -set -e _AM_PROJECT_ALIASES diff --git a/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_transition.snap b/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_transition.snap deleted file mode 100644 index e844994e..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_transition.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -assertion_line: 613 -expression: output ---- -functions -e old_a -functions -e old_b -echo 'am: .aliases changed (1 added, 2 removed)' -alias t "make test" -set -gx _AM_PROJECT_ALIASES "t|7f7c42c" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_with_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_with_aliases.snap deleted file mode 100644 index ee0788a5..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_hook_fish_with_aliases.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -assertion_line: 497 -expression: output ---- -echo 'am: loaded .aliases' -echo ' b → cargo build' -echo ' t → cargo test' -alias b "cargo build" -alias t "cargo test" -set -gx _AM_PROJECT_ALIASES "b|b58de66,t|ab61de4" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_hook_powershell_with_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_hook_powershell_with_aliases.snap deleted file mode 100644 index c7a9a1ae..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_hook_powershell_with_aliases.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -assertion_line: 555 -expression: output ---- -Write-Host 'am: loaded .aliases' -Write-Host ' b → cargo build' -Write-Host ' t → cargo test' -function global:b { cargo build @args } -function global:t { cargo test @args } -$env:_AM_PROJECT_ALIASES = "b|b58de66,t|ab61de4" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_hook_zsh_with_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_hook_zsh_with_aliases.snap deleted file mode 100644 index de331c60..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_hook_zsh_with_aliases.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -assertion_line: 526 -expression: output ---- -printf '%s\n' 'am: loaded .aliases' -printf '%s\n' ' b → cargo build' -printf '%s\n' ' t → cargo test' -alias b="cargo build" -alias t="cargo test" -export _AM_PROJECT_ALIASES="b|b58de66,t|ab61de4" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_force_with_tracked_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_force_with_tracked_aliases.snap deleted file mode 100644 index 7b09232e..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_force_with_tracked_aliases.snap +++ /dev/null @@ -1,985 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -unalias gs 2>/dev/null; unset -f gs 2>/dev/null -unset _AM_PROJECT_ALIASES -unset _AM_PROJECT_PATH -alias gs="git status" -export _AM_ALIASES="gs" -unset _AM_PROFILE_ALIASES - -am() { - command am "$@" - local am_status=$? - if [[ $am_status -ne 0 ]]; then return $am_status; fi - case "$1" in - tui|t) eval "$(command am reload bash)"; eval "$(command am hook bash)"; return ;; - esac - case "$1" in - use|u) eval "$(command am reload bash)"; return ;; - esac - case "$1:$2" in - profile:use|p:use|profile:u|p:u|profile:add|p:add|profile:a|p:a|profile:remove|p:remove|profile:r|p:r) eval "$(command am reload bash)" ;; - esac - case "$1" in - add|a|remove|r) - case "$*" in - *\ -l\ *|*\ --local\ *|*\ -l|*\ --local) eval "$(command am hook bash)" ;; - *) eval "$(command am reload bash)" ;; - esac ;; - trust) eval "$(command am hook bash)" ;; - untrust) eval "$(command am hook --quiet bash)" ;; - esac -} - - -# am cd hook: track directory changes and reload project aliases -__am_hook() { - local previous_exit_status=$? - if [[ "${__am_prev_dir:-}" != "$PWD" ]]; then - __am_prev_dir="$PWD" - eval "$(command am hook bash)" - fi - return $previous_exit_status -} -if [[ "$(declare -p PROMPT_COMMAND 2>&1)" == "declare -a"* ]]; then - case " ${PROMPT_COMMAND[*]} " in - *" __am_hook "*) ;; - *) PROMPT_COMMAND=(__am_hook "${PROMPT_COMMAND[@]}") ;; - esac -else - case ";${PROMPT_COMMAND:-};" in - *";__am_hook;"*) ;; - *) PROMPT_COMMAND="__am_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}" ;; - esac -fi -__am_hook - - -_am() { - local i cur prev opts cmd - COMPREPLY=() - if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then - cur="$2" - else - cur="${COMP_WORDS[COMP_CWORD]}" - fi - prev="$3" - cmd="" - opts="" - - for i in "${COMP_WORDS[@]:0:COMP_CWORD}" - do - case "${cmd},${i}" in - ",$1") - cmd="am" - ;; - am,add) - cmd="am__subcmd__add" - ;; - am,export) - cmd="am__subcmd__export" - ;; - am,help) - cmd="am__subcmd__help" - ;; - am,hook) - cmd="am__subcmd__hook" - ;; - am,import) - cmd="am__subcmd__import" - ;; - am,init) - cmd="am__subcmd__init" - ;; - am,ls) - cmd="am__subcmd__ls" - ;; - am,profile) - cmd="am__subcmd__profile" - ;; - am,reload) - cmd="am__subcmd__reload" - ;; - am,remove) - cmd="am__subcmd__remove" - ;; - am,setup) - cmd="am__subcmd__setup" - ;; - am,share) - cmd="am__subcmd__share" - ;; - am,status) - cmd="am__subcmd__status" - ;; - am,trust) - cmd="am__subcmd__trust" - ;; - am,tui) - cmd="am__subcmd__tui" - ;; - am,untrust) - cmd="am__subcmd__untrust" - ;; - am,use) - cmd="am__subcmd__use" - ;; - am__subcmd__help,add) - cmd="am__subcmd__help__subcmd__add" - ;; - am__subcmd__help,export) - cmd="am__subcmd__help__subcmd__export" - ;; - am__subcmd__help,help) - cmd="am__subcmd__help__subcmd__help" - ;; - am__subcmd__help,hook) - cmd="am__subcmd__help__subcmd__hook" - ;; - am__subcmd__help,import) - cmd="am__subcmd__help__subcmd__import" - ;; - am__subcmd__help,init) - cmd="am__subcmd__help__subcmd__init" - ;; - am__subcmd__help,ls) - cmd="am__subcmd__help__subcmd__ls" - ;; - am__subcmd__help,profile) - cmd="am__subcmd__help__subcmd__profile" - ;; - am__subcmd__help,reload) - cmd="am__subcmd__help__subcmd__reload" - ;; - am__subcmd__help,remove) - cmd="am__subcmd__help__subcmd__remove" - ;; - am__subcmd__help,setup) - cmd="am__subcmd__help__subcmd__setup" - ;; - am__subcmd__help,share) - cmd="am__subcmd__help__subcmd__share" - ;; - am__subcmd__help,status) - cmd="am__subcmd__help__subcmd__status" - ;; - am__subcmd__help,trust) - cmd="am__subcmd__help__subcmd__trust" - ;; - am__subcmd__help,tui) - cmd="am__subcmd__help__subcmd__tui" - ;; - am__subcmd__help,untrust) - cmd="am__subcmd__help__subcmd__untrust" - ;; - am__subcmd__help,use) - cmd="am__subcmd__help__subcmd__use" - ;; - am__subcmd__help__subcmd__profile,add) - cmd="am__subcmd__help__subcmd__profile__subcmd__add" - ;; - am__subcmd__help__subcmd__profile,list) - cmd="am__subcmd__help__subcmd__profile__subcmd__list" - ;; - am__subcmd__help__subcmd__profile,remove) - cmd="am__subcmd__help__subcmd__profile__subcmd__remove" - ;; - am__subcmd__help__subcmd__profile,use) - cmd="am__subcmd__help__subcmd__profile__subcmd__use" - ;; - am__subcmd__profile,add) - cmd="am__subcmd__profile__subcmd__add" - ;; - am__subcmd__profile,help) - cmd="am__subcmd__profile__subcmd__help" - ;; - am__subcmd__profile,list) - cmd="am__subcmd__profile__subcmd__list" - ;; - am__subcmd__profile,remove) - cmd="am__subcmd__profile__subcmd__remove" - ;; - am__subcmd__profile,use) - cmd="am__subcmd__profile__subcmd__use" - ;; - am__subcmd__profile__subcmd__help,add) - cmd="am__subcmd__profile__subcmd__help__subcmd__add" - ;; - am__subcmd__profile__subcmd__help,help) - cmd="am__subcmd__profile__subcmd__help__subcmd__help" - ;; - am__subcmd__profile__subcmd__help,list) - cmd="am__subcmd__profile__subcmd__help__subcmd__list" - ;; - am__subcmd__profile__subcmd__help,remove) - cmd="am__subcmd__profile__subcmd__help__subcmd__remove" - ;; - am__subcmd__profile__subcmd__help,use) - cmd="am__subcmd__profile__subcmd__help__subcmd__use" - ;; - *) - ;; - esac - done - - case "${cmd}" in - am) - opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__add) - opts="-p -l -g -h -V --profile --local --global --raw --sub --help --version [COMMAND]..." - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --profile) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -p) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --sub) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__export) - opts="-l -g -p -b -h -V --local --global --profile --all --base64 --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --profile) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -p) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help) - opts="add remove ls status profile init setup use tui export import share trust untrust hook reload help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__add) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__export) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__help) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__hook) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__import) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__init) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__ls) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__profile) - opts="add use remove list" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__profile__subcmd__add) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__profile__subcmd__list) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__profile__subcmd__remove) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__profile__subcmd__use) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__reload) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__remove) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__setup) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__share) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__status) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__trust) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__tui) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__untrust) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__help__subcmd__use) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__hook) - opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__import) - opts="-l -g -p -b -y -h -V --local --global --profile --all --base64 --yes --trust --help --version " - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --profile) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -p) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__init) - opts="-f -h -V --force --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__ls) - opts="-u -h -V --used --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile) - opts="-h -V --help --version add use remove list help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__add) - opts="-h -V --help --version " - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__help) - opts="add use remove list help" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__help__subcmd__add) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__help__subcmd__help) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__help__subcmd__list) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__help__subcmd__remove) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__help__subcmd__use) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__list) - opts="-u -h -V --used --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__remove) - opts="-f -h -V --force --help --version " - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__profile__subcmd__use) - opts="-n -i -h -V --priority --inverse --help --version [NAMES]..." - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --priority) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -n) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__reload) - opts="-h -V --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__remove) - opts="-p -l -g -h -V --profile --local --global --sub --help --version " - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --profile) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -p) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - --sub) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__setup) - opts="-h -V --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__share) - opts="-l -g -p -h -V --local --global --profile --all --termbin --paste-rs --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --profile) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -p) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__status) - opts="-h -V --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__trust) - opts="-h -V --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__tui) - opts="-h -V --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__untrust) - opts="-f -h -V --forget --help --version" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - am__subcmd__use) - opts="-n -i -h -V --priority --inverse --help --version [NAMES]..." - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - --priority) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - -n) - COMPREPLY=($(compgen -f "${cur}")) - return 0 - ;; - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; - esac -} - -if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then - complete -F _am -o nosort -o bashdefault -o default am -else - complete -F _am -o bashdefault -o default am -fi diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap index 57593af5..3857644b 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_simple_profile.snap @@ -4,40 +4,30 @@ expression: output --- alias gs="git status" alias ll="ls -lha" -export _AM_ALIASES="gs,ll" -unset _AM_PROFILE_ALIASES - +export _AM_ALIASES="gs|22db469,ll|619d266" am() { command am "$@" local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - tui|t) eval "$(command am reload bash)"; eval "$(command am hook bash)"; return ;; - esac - case "$1" in - use|u) eval "$(command am reload bash)"; return ;; - esac - case "$1:$2" in - profile:use|p:use|profile:u|p:u|profile:add|p:add|profile:a|p:a|profile:remove|p:remove|profile:r|p:r) eval "$(command am reload bash)" ;; - esac - case "$1" in - add|a|remove|r) - case "$*" in - *\ -l\ *|*\ --local\ *|*\ -l|*\ --local) eval "$(command am hook bash)" ;; - *) eval "$(command am reload bash)" ;; + add|a|remove|r|trust|tui|t) + eval "$(command am sync bash)" ;; + use|u|untrust) + eval "$(command am sync --quiet bash)" ;; + profile|p) + case "$2" in + use|u) eval "$(command am sync --quiet bash)" ;; + add|a|remove|r) eval "$(command am sync bash)" ;; esac ;; - trust) eval "$(command am hook bash)" ;; - untrust) eval "$(command am hook --quiet bash)" ;; esac } - -# am cd hook: track directory changes and reload project aliases +# am cd hook: sync project aliases on directory change __am_hook() { local previous_exit_status=$? if [[ "${__am_prev_dir:-}" != "$PWD" ]]; then __am_prev_dir="$PWD" - eval "$(command am hook bash)" + eval "$(command am sync bash)" fi return $previous_exit_status } @@ -54,7 +44,6 @@ else fi __am_hook - _am() { local i cur prev opts cmd COMPREPLY=() @@ -82,9 +71,6 @@ _am() { am,help) cmd="am__subcmd__help" ;; - am,hook) - cmd="am__subcmd__hook" - ;; am,import) cmd="am__subcmd__import" ;; @@ -97,9 +83,6 @@ _am() { am,profile) cmd="am__subcmd__profile" ;; - am,reload) - cmd="am__subcmd__reload" - ;; am,remove) cmd="am__subcmd__remove" ;; @@ -112,6 +95,9 @@ _am() { am,status) cmd="am__subcmd__status" ;; + am,sync) + cmd="am__subcmd__sync" + ;; am,trust) cmd="am__subcmd__trust" ;; @@ -133,9 +119,6 @@ _am() { am__subcmd__help,help) cmd="am__subcmd__help__subcmd__help" ;; - am__subcmd__help,hook) - cmd="am__subcmd__help__subcmd__hook" - ;; am__subcmd__help,import) cmd="am__subcmd__help__subcmd__import" ;; @@ -148,9 +131,6 @@ _am() { am__subcmd__help,profile) cmd="am__subcmd__help__subcmd__profile" ;; - am__subcmd__help,reload) - cmd="am__subcmd__help__subcmd__reload" - ;; am__subcmd__help,remove) cmd="am__subcmd__help__subcmd__remove" ;; @@ -163,6 +143,9 @@ _am() { am__subcmd__help,status) cmd="am__subcmd__help__subcmd__status" ;; + am__subcmd__help,sync) + cmd="am__subcmd__help__subcmd__sync" + ;; am__subcmd__help,trust) cmd="am__subcmd__help__subcmd__trust" ;; @@ -224,7 +207,7 @@ _am() { case "${cmd}" in am) - opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload help" + opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -286,7 +269,7 @@ _am() { return 0 ;; am__subcmd__help) - opts="add remove ls status profile init setup use tui export import share trust untrust hook reload help" + opts="add remove ls status profile init setup use tui export import share trust untrust sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -341,20 +324,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__hook) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__help__subcmd__import) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -467,7 +436,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__reload) + am__subcmd__help__subcmd__remove) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -481,7 +450,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__remove) + am__subcmd__help__subcmd__setup) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -495,7 +464,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__setup) + am__subcmd__help__subcmd__share) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -509,7 +478,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__share) + am__subcmd__help__subcmd__status) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -523,7 +492,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__status) + am__subcmd__help__subcmd__sync) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -593,20 +562,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__hook) - opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__import) opts="-l -g -p -b -y -h -V --local --global --profile --all --base64 --yes --trust --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then @@ -819,20 +774,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__reload) - opts="-h -V --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__remove) opts="-p -l -g -h -V --profile --local --global --sub --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then @@ -909,6 +850,20 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + am__subcmd__sync) + opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; am__subcmd__trust) opts="-h -V --help --version" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap index 0af7e3d0..0264d221 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_bash_with_kubectl_subcommands.snap @@ -32,40 +32,31 @@ kubectl() { *) command kubectl "$@" ;; esac } -export _AM_ALIASES="kubectl" -unset _AM_PROFILE_ALIASES - +export _AM_ALIASES="kubectl|e597aec" +export _AM_SUBCOMMANDS="kubectl:apply:f|ce5fb2d,kubectl:get:po|98de999,kubectl:get:svc|15a309b,kubectl:logs:f|1b79d5f,kubectl:rollout:status|da7ff14" am() { command am "$@" local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - tui|t) eval "$(command am reload bash)"; eval "$(command am hook bash)"; return ;; - esac - case "$1" in - use|u) eval "$(command am reload bash)"; return ;; - esac - case "$1:$2" in - profile:use|p:use|profile:u|p:u|profile:add|p:add|profile:a|p:a|profile:remove|p:remove|profile:r|p:r) eval "$(command am reload bash)" ;; - esac - case "$1" in - add|a|remove|r) - case "$*" in - *\ -l\ *|*\ --local\ *|*\ -l|*\ --local) eval "$(command am hook bash)" ;; - *) eval "$(command am reload bash)" ;; + add|a|remove|r|trust|tui|t) + eval "$(command am sync bash)" ;; + use|u|untrust) + eval "$(command am sync --quiet bash)" ;; + profile|p) + case "$2" in + use|u) eval "$(command am sync --quiet bash)" ;; + add|a|remove|r) eval "$(command am sync bash)" ;; esac ;; - trust) eval "$(command am hook bash)" ;; - untrust) eval "$(command am hook --quiet bash)" ;; esac } - -# am cd hook: track directory changes and reload project aliases +# am cd hook: sync project aliases on directory change __am_hook() { local previous_exit_status=$? if [[ "${__am_prev_dir:-}" != "$PWD" ]]; then __am_prev_dir="$PWD" - eval "$(command am hook bash)" + eval "$(command am sync bash)" fi return $previous_exit_status } @@ -82,7 +73,6 @@ else fi __am_hook - _am() { local i cur prev opts cmd COMPREPLY=() @@ -110,9 +100,6 @@ _am() { am,help) cmd="am__subcmd__help" ;; - am,hook) - cmd="am__subcmd__hook" - ;; am,import) cmd="am__subcmd__import" ;; @@ -125,9 +112,6 @@ _am() { am,profile) cmd="am__subcmd__profile" ;; - am,reload) - cmd="am__subcmd__reload" - ;; am,remove) cmd="am__subcmd__remove" ;; @@ -140,6 +124,9 @@ _am() { am,status) cmd="am__subcmd__status" ;; + am,sync) + cmd="am__subcmd__sync" + ;; am,trust) cmd="am__subcmd__trust" ;; @@ -161,9 +148,6 @@ _am() { am__subcmd__help,help) cmd="am__subcmd__help__subcmd__help" ;; - am__subcmd__help,hook) - cmd="am__subcmd__help__subcmd__hook" - ;; am__subcmd__help,import) cmd="am__subcmd__help__subcmd__import" ;; @@ -176,9 +160,6 @@ _am() { am__subcmd__help,profile) cmd="am__subcmd__help__subcmd__profile" ;; - am__subcmd__help,reload) - cmd="am__subcmd__help__subcmd__reload" - ;; am__subcmd__help,remove) cmd="am__subcmd__help__subcmd__remove" ;; @@ -191,6 +172,9 @@ _am() { am__subcmd__help,status) cmd="am__subcmd__help__subcmd__status" ;; + am__subcmd__help,sync) + cmd="am__subcmd__help__subcmd__sync" + ;; am__subcmd__help,trust) cmd="am__subcmd__help__subcmd__trust" ;; @@ -252,7 +236,7 @@ _am() { case "${cmd}" in am) - opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust hook reload help" + opts="-h -V --help --version add remove ls status profile init setup use tui export import share trust untrust sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -314,7 +298,7 @@ _am() { return 0 ;; am__subcmd__help) - opts="add remove ls status profile init setup use tui export import share trust untrust hook reload help" + opts="add remove ls status profile init setup use tui export import share trust untrust sync help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -369,20 +353,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__hook) - opts="" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__help__subcmd__import) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then @@ -495,7 +465,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__reload) + am__subcmd__help__subcmd__remove) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -509,7 +479,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__remove) + am__subcmd__help__subcmd__setup) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -523,7 +493,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__setup) + am__subcmd__help__subcmd__share) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -537,7 +507,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__share) + am__subcmd__help__subcmd__status) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -551,7 +521,7 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__help__subcmd__status) + am__subcmd__help__subcmd__sync) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) @@ -621,20 +591,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__hook) - opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__import) opts="-l -g -p -b -y -h -V --local --global --profile --all --base64 --yes --trust --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then @@ -847,20 +803,6 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - am__subcmd__reload) - opts="-h -V --help --version bash brush fish powershell zsh" - if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - fi - case "${prev}" in - *) - COMPREPLY=() - ;; - esac - COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) - return 0 - ;; am__subcmd__remove) opts="-p -l -g -h -V --profile --local --global --sub --help --version " if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then @@ -937,6 +879,20 @@ _am() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; + am__subcmd__sync) + opts="-q -h -V --quiet --help --version bash brush fish powershell zsh" + if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + fi + case "${prev}" in + *) + COMPREPLY=() + ;; + esac + COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) + return 0 + ;; am__subcmd__trust) opts="-h -V --help --version" if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_abbr_force_with_tracked_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_abbr_force_with_tracked_aliases.snap deleted file mode 100644 index 5b04ef5f..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_abbr_force_with_tracked_aliases.snap +++ /dev/null @@ -1,209 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e gs -abbr --query gs; and abbr --erase gs -set -e _AM_PROJECT_ALIASES -set -e _AM_PROJECT_PATH -abbr --add gs "git status" -set -gx _AM_ALIASES "gs" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations -function am --wraps=am - command am $argv - set -l am_status $status - if test $am_status -ne 0 - return $am_status - end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source - end -end - -# am cd hook -function __am_hook --on-variable PWD - am hook fish | source -end -__am_hook - -# Print an optspec for argparse to handle cmd's options that are independent of any subcommand. -function __fish_am_global_optspecs - string join \n h/help V/version -end - -function __fish_am_needs_command - # Figure out if the current invocation already has a command. - set -l cmd (commandline -opc) - set -e cmd[1] - argparse -s (__fish_am_global_optspecs) -- $cmd 2>/dev/null - or return - if set -q argv[1] - # Also print the command, so this can be used to figure out what it is. - echo $argv[1] - return 1 - end - return 0 -end - -function __fish_am_using_subcommand - set -l cmd (__fish_am_needs_command) - test -z "$cmd" - and return 1 - contains -- $cmd[1] $argv -end - -complete -c am -n "__fish_am_needs_command" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_needs_command" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_needs_command" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_needs_command" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_needs_command" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_needs_command" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_needs_command" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_needs_command" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_needs_command" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_needs_command" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_needs_command" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r -complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r -complete -c am -n "__fish_am_using_subcommand add" -s l -l local -d 'Add to the project\'s .aliases file instead of a profile' -complete -c am -n "__fish_am_using_subcommand add" -s g -l global -d 'Add as a global alias (always loaded, independent of profile)' -complete -c am -n "__fish_am_using_subcommand add" -l raw -d 'Disable {{N}} template detection (treat command as literal)' -complete -c am -n "__fish_am_using_subcommand add" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand add" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand remove" -s p -l profile -d 'Profile to remove the alias from (defaults to active profile)' -r -complete -c am -n "__fish_am_using_subcommand remove" -l sub -d 'Subcommand path segments to complete the key (e.g. --sub b --sub l removes jj:b:l)' -r -complete -c am -n "__fish_am_using_subcommand remove" -s l -l local -d 'Remove from the project\'s .aliases file instead of a profile' -complete -c am -n "__fish_am_using_subcommand remove" -s g -l global -d 'Remove a global alias' -complete -c am -n "__fish_am_using_subcommand remove" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand remove" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand ls" -s u -l used -d 'Show only active profiles and loaded project aliases' -complete -c am -n "__fish_am_using_subcommand ls" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand ls" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand status" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand status" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "list" -d 'List all profiles' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from add" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from add" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s n -l priority -d 'Activate at specific priority position (1-based). Repositions if already active' -r -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s i -l inverse -d 'Reverse the processing order (first listed = highest priority)' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s f -l force -d 'Skip confirmation prompt' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s u -l used -d 'Show only active profiles and loaded project aliases' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "list" -d 'List all profiles' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand init" -s f -l force -d 'Force re-initialisation: unload all previously tracked aliases (both alias and function forms) before re-loading. Use after config changes such as toggling `use_abbr`' -complete -c am -n "__fish_am_using_subcommand init" -s h -l help -d 'Print help (see more with \'--help\')' -complete -c am -n "__fish_am_using_subcommand init" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand setup" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand setup" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand use" -s n -l priority -d 'Activate at specific priority position (1-based). Repositions if already active' -r -complete -c am -n "__fish_am_using_subcommand use" -s i -l inverse -d 'Reverse the processing order (first listed = highest priority)' -complete -c am -n "__fish_am_using_subcommand use" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand use" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand tui" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand tui" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand export" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand export" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand export" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand export" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand export" -s b -l base64 -d 'Encode output as base64' -complete -c am -n "__fish_am_using_subcommand export" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand export" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand import" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand import" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand import" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand import" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand import" -s b -l base64 -d 'Decode base64 input before parsing' -complete -c am -n "__fish_am_using_subcommand import" -s y -l yes -d 'Skip all confirmation prompts' -complete -c am -n "__fish_am_using_subcommand import" -l trust -d 'DANGER: Skip safety checks for suspicious content (escape sequences). Only use for your own exports. Never trust external input blindly — it can carry invisible escape sequences that hide malicious commands' -complete -c am -n "__fish_am_using_subcommand import" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand import" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand share" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand share" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand share" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand share" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand share" -l termbin -d 'Generate command for termbin.com (netcat)' -complete -c am -n "__fish_am_using_subcommand share" -l paste-rs -d 'Generate command for paste.rs (curl)' -complete -c am -n "__fish_am_using_subcommand share" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand share" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand trust" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' -complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "list" -d 'List all profiles' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap index 50a8c386..f0822361 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_deep_chain.snap @@ -2,53 +2,50 @@ source: crates/am/tests/snapshots.rs expression: output --- -alias ct "cargo test" -alias gs "git status" -alias ll "ls -lha" -set -gx _AM_ALIASES "ct,gs,ll" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations +functions -e ct +complete -e -c ct +function ct + cargo test $argv +end +complete -c ct --wraps "cargo" +functions -e gs +complete -e -c gs +function gs + git status $argv +end +complete -c gs --wraps "git" +functions -e ll +complete -e -c ll +function ll + ls -lha $argv +end +complete -c ll --wraps "ls" +set -gx _AM_ALIASES "ct|ab61de4,gs|22db469,ll|619d266" +# am wrapper: sync after mutations function am --wraps=am command am $argv set -l am_status $status if test $am_status -ne 0 return $am_status end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source + switch "$argv[1]" + case add a remove r trust tui t + command am sync fish | source + case use u untrust + command am sync --quiet fish | source + case profile p + switch "$argv[2]" + case use u + command am sync --quiet fish | source + case add a remove r + command am sync fish | source + end end end # am cd hook function __am_hook --on-variable PWD - am hook fish | source + am sync fish | source end __am_hook @@ -94,8 +91,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases fr complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -179,28 +175,25 @@ complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print v complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_no_previous.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_no_previous.snap deleted file mode 100644 index f86e7bf4..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_no_previous.snap +++ /dev/null @@ -1,207 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -set -e _AM_PROJECT_ALIASES -set -e _AM_PROJECT_PATH -alias gs "git status" -set -gx _AM_ALIASES "gs" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations -function am --wraps=am - command am $argv - set -l am_status $status - if test $am_status -ne 0 - return $am_status - end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source - end -end - -# am cd hook -function __am_hook --on-variable PWD - am hook fish | source -end -__am_hook - -# Print an optspec for argparse to handle cmd's options that are independent of any subcommand. -function __fish_am_global_optspecs - string join \n h/help V/version -end - -function __fish_am_needs_command - # Figure out if the current invocation already has a command. - set -l cmd (commandline -opc) - set -e cmd[1] - argparse -s (__fish_am_global_optspecs) -- $cmd 2>/dev/null - or return - if set -q argv[1] - # Also print the command, so this can be used to figure out what it is. - echo $argv[1] - return 1 - end - return 0 -end - -function __fish_am_using_subcommand - set -l cmd (__fish_am_needs_command) - test -z "$cmd" - and return 1 - contains -- $cmd[1] $argv -end - -complete -c am -n "__fish_am_needs_command" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_needs_command" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_needs_command" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_needs_command" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_needs_command" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_needs_command" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_needs_command" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_needs_command" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_needs_command" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_needs_command" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_needs_command" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r -complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r -complete -c am -n "__fish_am_using_subcommand add" -s l -l local -d 'Add to the project\'s .aliases file instead of a profile' -complete -c am -n "__fish_am_using_subcommand add" -s g -l global -d 'Add as a global alias (always loaded, independent of profile)' -complete -c am -n "__fish_am_using_subcommand add" -l raw -d 'Disable {{N}} template detection (treat command as literal)' -complete -c am -n "__fish_am_using_subcommand add" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand add" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand remove" -s p -l profile -d 'Profile to remove the alias from (defaults to active profile)' -r -complete -c am -n "__fish_am_using_subcommand remove" -l sub -d 'Subcommand path segments to complete the key (e.g. --sub b --sub l removes jj:b:l)' -r -complete -c am -n "__fish_am_using_subcommand remove" -s l -l local -d 'Remove from the project\'s .aliases file instead of a profile' -complete -c am -n "__fish_am_using_subcommand remove" -s g -l global -d 'Remove a global alias' -complete -c am -n "__fish_am_using_subcommand remove" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand remove" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand ls" -s u -l used -d 'Show only active profiles and loaded project aliases' -complete -c am -n "__fish_am_using_subcommand ls" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand ls" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand status" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand status" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "list" -d 'List all profiles' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from add" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from add" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s n -l priority -d 'Activate at specific priority position (1-based). Repositions if already active' -r -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s i -l inverse -d 'Reverse the processing order (first listed = highest priority)' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s f -l force -d 'Skip confirmation prompt' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s u -l used -d 'Show only active profiles and loaded project aliases' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "list" -d 'List all profiles' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand init" -s f -l force -d 'Force re-initialisation: unload all previously tracked aliases (both alias and function forms) before re-loading. Use after config changes such as toggling `use_abbr`' -complete -c am -n "__fish_am_using_subcommand init" -s h -l help -d 'Print help (see more with \'--help\')' -complete -c am -n "__fish_am_using_subcommand init" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand setup" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand setup" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand use" -s n -l priority -d 'Activate at specific priority position (1-based). Repositions if already active' -r -complete -c am -n "__fish_am_using_subcommand use" -s i -l inverse -d 'Reverse the processing order (first listed = highest priority)' -complete -c am -n "__fish_am_using_subcommand use" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand use" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand tui" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand tui" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand export" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand export" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand export" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand export" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand export" -s b -l base64 -d 'Encode output as base64' -complete -c am -n "__fish_am_using_subcommand export" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand export" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand import" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand import" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand import" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand import" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand import" -s b -l base64 -d 'Decode base64 input before parsing' -complete -c am -n "__fish_am_using_subcommand import" -s y -l yes -d 'Skip all confirmation prompts' -complete -c am -n "__fish_am_using_subcommand import" -l trust -d 'DANGER: Skip safety checks for suspicious content (escape sequences). Only use for your own exports. Never trust external input blindly — it can carry invisible escape sequences that hide malicious commands' -complete -c am -n "__fish_am_using_subcommand import" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand import" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand share" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand share" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand share" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand share" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand share" -l termbin -d 'Generate command for termbin.com (netcat)' -complete -c am -n "__fish_am_using_subcommand share" -l paste-rs -d 'Generate command for paste.rs (curl)' -complete -c am -n "__fish_am_using_subcommand share" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand share" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand trust" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' -complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "list" -d 'List all profiles' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_with_tracked_aliases.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_with_tracked_aliases.snap deleted file mode 100644 index 3817b4ba..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_force_with_tracked_aliases.snap +++ /dev/null @@ -1,212 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e gs -abbr --query gs; and abbr --erase gs -functions -e ll -abbr --query ll; and abbr --erase ll -set -e _AM_PROJECT_ALIASES -set -e _AM_PROJECT_PATH -alias gs "git status" -alias ll "ls -lha" -set -gx _AM_ALIASES "gs,ll" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations -function am --wraps=am - command am $argv - set -l am_status $status - if test $am_status -ne 0 - return $am_status - end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source - end -end - -# am cd hook -function __am_hook --on-variable PWD - am hook fish | source -end -__am_hook - -# Print an optspec for argparse to handle cmd's options that are independent of any subcommand. -function __fish_am_global_optspecs - string join \n h/help V/version -end - -function __fish_am_needs_command - # Figure out if the current invocation already has a command. - set -l cmd (commandline -opc) - set -e cmd[1] - argparse -s (__fish_am_global_optspecs) -- $cmd 2>/dev/null - or return - if set -q argv[1] - # Also print the command, so this can be used to figure out what it is. - echo $argv[1] - return 1 - end - return 0 -end - -function __fish_am_using_subcommand - set -l cmd (__fish_am_needs_command) - test -z "$cmd" - and return 1 - contains -- $cmd[1] $argv -end - -complete -c am -n "__fish_am_needs_command" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_needs_command" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_needs_command" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_needs_command" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_needs_command" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_needs_command" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_needs_command" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_needs_command" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_needs_command" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_needs_command" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_needs_command" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r -complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r -complete -c am -n "__fish_am_using_subcommand add" -s l -l local -d 'Add to the project\'s .aliases file instead of a profile' -complete -c am -n "__fish_am_using_subcommand add" -s g -l global -d 'Add as a global alias (always loaded, independent of profile)' -complete -c am -n "__fish_am_using_subcommand add" -l raw -d 'Disable {{N}} template detection (treat command as literal)' -complete -c am -n "__fish_am_using_subcommand add" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand add" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand remove" -s p -l profile -d 'Profile to remove the alias from (defaults to active profile)' -r -complete -c am -n "__fish_am_using_subcommand remove" -l sub -d 'Subcommand path segments to complete the key (e.g. --sub b --sub l removes jj:b:l)' -r -complete -c am -n "__fish_am_using_subcommand remove" -s l -l local -d 'Remove from the project\'s .aliases file instead of a profile' -complete -c am -n "__fish_am_using_subcommand remove" -s g -l global -d 'Remove a global alias' -complete -c am -n "__fish_am_using_subcommand remove" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand remove" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand ls" -s u -l used -d 'Show only active profiles and loaded project aliases' -complete -c am -n "__fish_am_using_subcommand ls" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand ls" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand status" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand status" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "list" -d 'List all profiles' -complete -c am -n "__fish_am_using_subcommand profile; and not __fish_seen_subcommand_from add use remove list help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from add" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from add" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s n -l priority -d 'Activate at specific priority position (1-based). Repositions if already active' -r -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s i -l inverse -d 'Reverse the processing order (first listed = highest priority)' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from use" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s f -l force -d 'Skip confirmation prompt' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from remove" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s u -l used -d 'Show only active profiles and loaded project aliases' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from list" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "list" -d 'List all profiles' -complete -c am -n "__fish_am_using_subcommand profile; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand init" -s f -l force -d 'Force re-initialisation: unload all previously tracked aliases (both alias and function forms) before re-loading. Use after config changes such as toggling `use_abbr`' -complete -c am -n "__fish_am_using_subcommand init" -s h -l help -d 'Print help (see more with \'--help\')' -complete -c am -n "__fish_am_using_subcommand init" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand setup" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand setup" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand use" -s n -l priority -d 'Activate at specific priority position (1-based). Repositions if already active' -r -complete -c am -n "__fish_am_using_subcommand use" -s i -l inverse -d 'Reverse the processing order (first listed = highest priority)' -complete -c am -n "__fish_am_using_subcommand use" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand use" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand tui" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand tui" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand export" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand export" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand export" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand export" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand export" -s b -l base64 -d 'Encode output as base64' -complete -c am -n "__fish_am_using_subcommand export" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand export" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand import" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand import" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand import" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand import" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand import" -s b -l base64 -d 'Decode base64 input before parsing' -complete -c am -n "__fish_am_using_subcommand import" -s y -l yes -d 'Skip all confirmation prompts' -complete -c am -n "__fish_am_using_subcommand import" -l trust -d 'DANGER: Skip safety checks for suspicious content (escape sequences). Only use for your own exports. Never trust external input blindly — it can carry invisible escape sequences that hide malicious commands' -complete -c am -n "__fish_am_using_subcommand import" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand import" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand share" -s p -l profile -d 'Operate on specific profile(s) — can be repeated' -r -complete -c am -n "__fish_am_using_subcommand share" -s l -l local -d 'Operate on project-local aliases' -complete -c am -n "__fish_am_using_subcommand share" -s g -l global -d 'Operate on global aliases' -complete -c am -n "__fish_am_using_subcommand share" -l all -d 'Operate on everything (global + all profiles + local)' -complete -c am -n "__fish_am_using_subcommand share" -l termbin -d 'Generate command for termbin.com (netcat)' -complete -c am -n "__fish_am_using_subcommand share" -l paste-rs -d 'Generate command for paste.rs (curl)' -complete -c am -n "__fish_am_using_subcommand share" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand share" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand trust" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' -complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' -complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "list" -d 'List all profiles' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap index 9037a9a9..6393b261 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_globals_and_multi_profile.snap @@ -2,56 +2,56 @@ source: crates/am/tests/snapshots.rs expression: output --- -alias ll "ls -lha" -alias cm "git commit -sm" +functions -e cm +complete -e -c cm +function cm + git commit -sm $argv +end +complete -c cm --wraps "git" +functions -e cmf +complete -e -c cmf function cmf cm feat: $argv end -alias gs "git status" -set -gx _AM_ALIASES "cm,cmf,gs,ll" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations +complete -c cmf --wraps "cm" +functions -e gs +complete -e -c gs +function gs + git status $argv +end +complete -c gs --wraps "git" +functions -e ll +complete -e -c ll +function ll + ls -lha $argv +end +complete -c ll --wraps "ls" +set -gx _AM_ALIASES "cm|bed528f,cmf|9c99949,gs|22db469,ll|619d266" +# am wrapper: sync after mutations function am --wraps=am command am $argv set -l am_status $status if test $am_status -ne 0 return $am_status end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source + switch "$argv[1]" + case add a remove r trust tui t + command am sync fish | source + case use u untrust + command am sync --quiet fish | source + case profile p + switch "$argv[2]" + case use u + command am sync --quiet fish | source + case add a remove r + command am sync fish | source + end end end # am cd hook function __am_hook --on-variable PWD - am hook fish | source + am sync fish | source end __am_hook @@ -97,8 +97,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases fr complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -182,28 +181,25 @@ complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print v complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap index 6a71dc32..0a54fbe8 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_multi_profile.snap @@ -2,55 +2,50 @@ source: crates/am/tests/snapshots.rs expression: output --- -alias cm "git commit -sm" +functions -e cm +complete -e -c cm +function cm + git commit -sm $argv +end +complete -c cm --wraps "git" +functions -e cmf +complete -e -c cmf function cmf cm feat: $argv end -alias gs "git status" -set -gx _AM_ALIASES "cm,cmf,gs" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations +complete -c cmf --wraps "cm" +functions -e gs +complete -e -c gs +function gs + git status $argv +end +complete -c gs --wraps "git" +set -gx _AM_ALIASES "cm|bed528f,cmf|9c99949,gs|22db469" +# am wrapper: sync after mutations function am --wraps=am command am $argv set -l am_status $status if test $am_status -ne 0 return $am_status end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source + switch "$argv[1]" + case add a remove r trust tui t + command am sync fish | source + case use u untrust + command am sync --quiet fish | source + case profile p + switch "$argv[2]" + case use u + command am sync --quiet fish | source + case add a remove r + command am sync fish | source + end end end # am cd hook function __am_hook --on-variable PWD - am hook fish | source + am sync fish | source end __am_hook @@ -96,8 +91,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases fr complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -181,28 +175,25 @@ complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print v complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap index c57598b9..294d5f0a 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_simple_profile.snap @@ -2,52 +2,44 @@ source: crates/am/tests/snapshots.rs expression: output --- -alias gs "git status" -alias ll "ls -lha" -set -gx _AM_ALIASES "gs,ll" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations +functions -e gs +complete -e -c gs +function gs + git status $argv +end +complete -c gs --wraps "git" +functions -e ll +complete -e -c ll +function ll + ls -lha $argv +end +complete -c ll --wraps "ls" +set -gx _AM_ALIASES "gs|22db469,ll|619d266" +# am wrapper: sync after mutations function am --wraps=am command am $argv set -l am_status $status if test $am_status -ne 0 return $am_status end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source + switch "$argv[1]" + case add a remove r trust tui t + command am sync fish | source + case use u untrust + command am sync --quiet fish | source + case profile p + switch "$argv[2]" + case use u + command am sync --quiet fish | source + case add a remove r + command am sync fish | source + end end end # am cd hook function __am_hook --on-variable PWD - am hook fish | source + am sync fish | source end __am_hook @@ -93,8 +85,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases fr complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -178,28 +169,25 @@ complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print v complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap index e9726ddf..1e3176ea 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_globals.snap @@ -2,52 +2,44 @@ source: crates/am/tests/snapshots.rs expression: output --- -alias ll "ls -lha" -alias ct "cargo test" -set -gx _AM_ALIASES "ct,ll" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations +functions -e ct +complete -e -c ct +function ct + cargo test $argv +end +complete -c ct --wraps "cargo" +functions -e ll +complete -e -c ll +function ll + ls -lha $argv +end +complete -c ll --wraps "ls" +set -gx _AM_ALIASES "ct|ab61de4,ll|619d266" +# am wrapper: sync after mutations function am --wraps=am command am $argv set -l am_status $status if test $am_status -ne 0 return $am_status end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source + switch "$argv[1]" + case add a remove r trust tui t + command am sync fish | source + case use u untrust + command am sync --quiet fish | source + case profile p + switch "$argv[2]" + case use u + command am sync --quiet fish | source + case add a remove r + command am sync fish | source + end end end # am cd hook function __am_hook --on-variable PWD - am hook fish | source + am sync fish | source end __am_hook @@ -93,8 +85,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases fr complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -178,28 +169,25 @@ complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print v complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap index d48d1d7b..bde09557 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_fish_with_simple_subcommands.snap @@ -2,7 +2,12 @@ source: crates/am/tests/snapshots.rs expression: output --- -alias gs "git status" +functions -e gs +complete -e -c gs +function gs + git status $argv +end +complete -c gs --wraps "git" function jj --wraps=jj switch $argv[1] case 'ab' @@ -13,50 +18,33 @@ function jj --wraps=jj command jj $argv end end -set -gx _AM_ALIASES "gs,jj" -set -e _AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations +set -gx _AM_ALIASES "gs|22db469,jj|d8877a2" +set -gx _AM_SUBCOMMANDS "jj:ab|8296f9c,jj:new|22681a8" +# am wrapper: sync after mutations function am --wraps=am command am $argv set -l am_status $status if test $am_status -ne 0 return $am_status end - # tui may have changed anything → always reload after - if begin; test "$argv[1]" = tui; or test "$argv[1]" = t; end - command am reload fish | source - command am hook fish | source - return - end - # top-level use → reload aliases - if begin; test "$argv[1]" = use; or test "$argv[1]" = u; end - command am reload fish | source - return - end - # profile mutation → reload aliases - if begin; test "$argv[1]" = profile; or test "$argv[1]" = p; end - if begin; test "$argv[2]" = use; or test "$argv[2]" = u; or test "$argv[2]" = add; or test "$argv[2]" = a; or test "$argv[2]" = remove; or test "$argv[2]" = r; end - command am reload fish | source - end - else if begin; test "$argv[1]" = add; or test "$argv[1]" = a; or test "$argv[1]" = remove; or test "$argv[1]" = r; end - if contains -- -l $argv; or contains -- --local $argv - # local alias change → reload project aliases - command am hook fish | source - else - # profile/global alias change → reload - command am reload fish | source - end - else if test "$argv[1]" = trust - command am hook fish | source - else if test "$argv[1]" = untrust - command am hook --quiet fish | source + switch "$argv[1]" + case add a remove r trust tui t + command am sync fish | source + case use u untrust + command am sync --quiet fish | source + case profile p + switch "$argv[2]" + case use u + command am sync --quiet fish | source + case add a remove r + command am sync fish | source + end end end # am cd hook function __am_hook --on-variable PWD - am hook fish | source + am sync fish | source end __am_hook @@ -102,8 +90,7 @@ complete -c am -n "__fish_am_needs_command" -f -a "import" -d 'Import aliases fr complete -c am -n "__fish_am_needs_command" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' complete -c am -n "__fish_am_needs_command" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' complete -c am -n "__fish_am_needs_command" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_needs_command" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_needs_command" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' +complete -c am -n "__fish_am_needs_command" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' complete -c am -n "__fish_am_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand add" -s p -l profile -d 'Profile to add the alias to (defaults to active profile)' -r complete -c am -n "__fish_am_using_subcommand add" -l sub -d 'Define a subcommand alias (repeatable: --sub short long)' -r @@ -187,28 +174,25 @@ complete -c am -n "__fish_am_using_subcommand trust" -s V -l version -d 'Print v complete -c am -n "__fish_am_using_subcommand untrust" -s f -l forget -d 'Forget the path entirely (remove from security tracking instead of marking untrusted)' complete -c am -n "__fish_am_using_subcommand untrust" -s h -l help -d 'Print help' complete -c am -n "__fish_am_using_subcommand untrust" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand hook" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' -complete -c am -n "__fish_am_using_subcommand hook" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand hook" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand reload" -s h -l help -d 'Print help' -complete -c am -n "__fish_am_using_subcommand reload" -s V -l version -d 'Print version' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "add" -d 'Add a new alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "remove" -d 'Remove an alias' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "ls" -d 'List all profiles and project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "status" -d 'Check if the shell is set up correctly' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "init" -d 'Print shell init code' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "export" -d 'Export aliases to stdout as TOML' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "import" -d 'Import aliases from a URL or file' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "hook" -d 'Internal: called by the cd hook to load/unload project aliases' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "reload" -d 'Internal: called by the am wrapper to reload profile aliases after switching' -complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust hook reload help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' +complete -c am -n "__fish_am_using_subcommand sync" -s q -l quiet -d 'Suppress info and warning messages (still unloads/loads aliases)' +complete -c am -n "__fish_am_using_subcommand sync" -s h -l help -d 'Print help' +complete -c am -n "__fish_am_using_subcommand sync" -s V -l version -d 'Print version' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "add" -d 'Add a new alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "remove" -d 'Remove an alias' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "ls" -d 'List all profiles and project aliases' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "status" -d 'Check if the shell is set up correctly' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "profile" -d 'Manage profiles (defaults to listing when no subcommand given)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "init" -d 'Print shell init code' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "setup" -d 'Guided setup — adds amoxide to your shell profile' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "use" -d 'Shortcut for `am profile use` — toggle one or more profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "tui" -d 'Launch the interactive TUI for managing aliases and profiles' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "export" -d 'Export aliases to stdout as TOML' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "import" -d 'Import aliases from a URL or file' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "share" -d 'Generate a share command for posting aliases to a pastebin service' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "trust" -d 'Review and trust the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "untrust" -d 'Remove trust for the project .aliases file in the current directory' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "sync" -d 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' +complete -c am -n "__fish_am_using_subcommand help; and not __fish_seen_subcommand_from add remove ls status profile init setup use tui export import share trust untrust sync help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "add" -d 'Add a new profile' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "use" -d 'Toggle one or more profiles as active/inactive, optionally at a specific priority' complete -c am -n "__fish_am_using_subcommand help; and __fish_seen_subcommand_from profile" -f -a "remove" -d 'Remove a profile' diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap index 0a148ba0..cd465ea6 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_powershell_simple_profile.snap @@ -4,65 +4,46 @@ expression: output --- function global:gs { git status @args } function global:ll { ls -lha @args } -$env:_AM_ALIASES = "gs,ll" -Remove-Item -ErrorAction SilentlyContinue Env:_AM_PROFILE_ALIASES - -# am wrapper: reload aliases after mutations +$env:_AM_ALIASES = "gs|22db469,ll|619d266" +# am wrapper: sync after mutations function am { $amBin = (Get-Command -CommandType Application am | Select-Object -First 1).Source & $amBin @args if ($LASTEXITCODE -ne 0) { return } - # tui — always reload - if ($args.Count -ge 1 -and $args[0] -in 'tui', 't') { - $out = (& $amBin reload powershell) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - $out = (& $amBin hook powershell) -join "`r`n" + if ($args.Count -lt 1) { return } + $first = $args[0] + $second = if ($args.Count -ge 2) { $args[1] } else { $null } + + $runSync = { + $out = (& $amBin sync powershell) -join "`r`n" if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - return } - # top-level use — reload - if ($args.Count -ge 1 -and $args[0] -in 'use', 'u') { - $out = (& $amBin reload powershell) -join "`r`n" + $runSyncQuiet = { + $out = (& $amBin sync --quiet powershell) -join "`r`n" if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - return - } - # profile mutation — reload - if ($args.Count -ge 1 -and $args[0] -in 'profile', 'p') { - if ($args.Count -ge 2 -and $args[1] -in 'use', 'u', 'add', 'a', 'remove', 'r') { - $out = (& $amBin reload powershell) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - } } - # alias mutation — reload - elseif ($args.Count -ge 1 -and $args[0] -in 'add', 'a', 'remove', 'r') { - if ($args -contains '-l' -or $args -contains '--local') { - $out = (& $amBin hook powershell) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - } else { - $out = (& $amBin reload powershell) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } + + if ($first -in 'add', 'a', 'remove', 'r', 'trust', 'tui', 't') { + & $runSync + } elseif ($first -in 'use', 'u', 'untrust') { + & $runSyncQuiet + } elseif ($first -in 'profile', 'p') { + if ($second -in 'use', 'u') { + & $runSyncQuiet + } elseif ($second -in 'add', 'a', 'remove', 'r') { + & $runSync } } - # trust/untrust — reload project aliases - elseif ($args.Count -ge 1 -and $args[0] -eq 'trust') { - $out = (& $amBin hook powershell) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - } - elseif ($args.Count -ge 1 -and $args[0] -eq 'untrust') { - $out = (& $amBin hook --quiet powershell) -join "`r`n" - if ($out) { Invoke-Command -ScriptBlock ([scriptblock]::Create($out)) -NoNewScope } - } } - -# am cd hook: track directory changes and reload project aliases +# am cd hook: sync project aliases on directory change $env:__AM_LAST_DIR = $PWD.Path $__am_original_prompt = $function:prompt function global:prompt { if ($PWD.Path -ne $env:__AM_LAST_DIR) { $env:__AM_LAST_DIR = $PWD.Path $amBin = (Get-Command -CommandType Application am | Select-Object -First 1).Source - $hookCode = (& $amBin hook powershell) -join "`r`n" + $hookCode = (& $amBin sync powershell) -join "`r`n" if ($hookCode) { Invoke-Command -ScriptBlock ([scriptblock]::Create($hookCode)) -NoNewScope } } if ($__am_original_prompt) { & $__am_original_prompt } else { "PS $($PWD.Path)> " } @@ -70,7 +51,6 @@ function global:prompt { - Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) @@ -108,8 +88,7 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [System.Management.Automation.CompletionResult]::new('share', 'share', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Generate a share command for posting aliases to a pastebin service') [System.Management.Automation.CompletionResult]::new('trust', 'trust', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Review and trust the project .aliases file in the current directory') [System.Management.Automation.CompletionResult]::new('untrust', 'untrust', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Remove trust for the project .aliases file in the current directory') - [System.Management.Automation.CompletionResult]::new('hook', 'hook', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: called by the cd hook to load/unload project aliases') - [System.Management.Automation.CompletionResult]::new('reload', 'reload', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: called by the am wrapper to reload profile aliases after switching') + [System.Management.Automation.CompletionResult]::new('sync', 'sync', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)') [System.Management.Automation.CompletionResult]::new('help', 'help', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break } @@ -330,7 +309,7 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [System.Management.Automation.CompletionResult]::new('--version', '--version', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print version') break } - 'am;hook' { + 'am;sync' { [System.Management.Automation.CompletionResult]::new('-q', '-q', [System.Management.Automation.CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') [System.Management.Automation.CompletionResult]::new('--quiet', '--quiet', [System.Management.Automation.CompletionResultType]::ParameterName, 'Suppress info and warning messages (still unloads/loads aliases)') [System.Management.Automation.CompletionResult]::new('-h', '-h', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print help') @@ -339,13 +318,6 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [System.Management.Automation.CompletionResult]::new('--version', '--version', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print version') break } - 'am;reload' { - [System.Management.Automation.CompletionResult]::new('-h', '-h', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print help') - [System.Management.Automation.CompletionResult]::new('--help', '--help', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print help') - [System.Management.Automation.CompletionResult]::new('-V', '-V ', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print version') - [System.Management.Automation.CompletionResult]::new('--version', '--version', [System.Management.Automation.CompletionResultType]::ParameterName, 'Print version') - break - } 'am;help' { [System.Management.Automation.CompletionResult]::new('add', 'add', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Add a new alias') [System.Management.Automation.CompletionResult]::new('remove', 'remove', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Remove an alias') @@ -361,8 +333,7 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { [System.Management.Automation.CompletionResult]::new('share', 'share', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Generate a share command for posting aliases to a pastebin service') [System.Management.Automation.CompletionResult]::new('trust', 'trust', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Review and trust the project .aliases file in the current directory') [System.Management.Automation.CompletionResult]::new('untrust', 'untrust', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Remove trust for the project .aliases file in the current directory') - [System.Management.Automation.CompletionResult]::new('hook', 'hook', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: called by the cd hook to load/unload project aliases') - [System.Management.Automation.CompletionResult]::new('reload', 'reload', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: called by the am wrapper to reload profile aliases after switching') + [System.Management.Automation.CompletionResult]::new('sync', 'sync', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Internal: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)') [System.Management.Automation.CompletionResult]::new('help', 'help', [System.Management.Automation.CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)') break } @@ -424,10 +395,7 @@ Register-ArgumentCompleter -Native -CommandName 'am' -ScriptBlock { 'am;help;untrust' { break } - 'am;help;hook' { - break - } - 'am;help;reload' { + 'am;help;sync' { break } 'am;help;help' { diff --git a/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap index 02348629..9abeb63a 100644 --- a/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap +++ b/crates/am/tests/snapshots/snapshots__snapshot_init_zsh_simple_profile.snap @@ -4,35 +4,26 @@ expression: output --- alias gs="git status" alias ll="ls -lha" -export _AM_ALIASES="gs,ll" -unset _AM_PROFILE_ALIASES - +export _AM_ALIASES="gs|22db469,ll|619d266" am() { command am "$@" local am_status=$? if [[ $am_status -ne 0 ]]; then return $am_status; fi case "$1" in - tui|t) eval "$(command am reload zsh)"; eval "$(command am hook zsh)"; return ;; - esac - case "$1" in - use|u) eval "$(command am reload zsh)"; return ;; - esac - case "$1:$2" in - profile:use|p:use|profile:u|p:u|profile:add|p:add|profile:a|p:a|profile:remove|p:remove|profile:r|p:r) eval "$(command am reload zsh)" ;; - esac - case "$1" in - add|a|remove|r) - case "$*" in - *\ -l\ *|*\ --local\ *|*\ -l|*\ --local) eval "$(command am hook zsh)" ;; - *) eval "$(command am reload zsh)" ;; + add|a|remove|r|trust|tui|t) + eval "$(command am sync zsh)" ;; + use|u|untrust) + eval "$(command am sync --quiet zsh)" ;; + profile|p) + case "$2" in + use|u) eval "$(command am sync --quiet zsh)" ;; + add|a|remove|r) eval "$(command am sync zsh)" ;; esac ;; - trust) eval "$(command am hook zsh)" ;; - untrust) eval "$(command am hook --quiet zsh)" ;; esac } # am cd hook -__am_hook() { eval "$(am hook zsh)"; } +__am_hook() { eval "$(am sync zsh)"; } chpwd_functions+=(__am_hook) __am_hook @@ -331,7 +322,7 @@ _arguments "${_arguments_options[@]}" : \ '--version[Print version]' \ && ret=0 ;; -(hook) +(sync) _arguments "${_arguments_options[@]}" : \ '-q[Suppress info and warning messages (still unloads/loads aliases)]' \ '--quiet[Suppress info and warning messages (still unloads/loads aliases)]' \ @@ -342,15 +333,6 @@ _arguments "${_arguments_options[@]}" : \ ':shell:(bash brush fish powershell zsh)' \ && ret=0 ;; -(reload) -_arguments "${_arguments_options[@]}" : \ -'-h[Print help]' \ -'--help[Print help]' \ -'-V[Print version]' \ -'--version[Print version]' \ -':shell:(bash brush fish powershell zsh)' \ -&& ret=0 -;; (help) _arguments "${_arguments_options[@]}" : \ ":: :_am__subcmd__help_commands" \ @@ -447,11 +429,7 @@ _arguments "${_arguments_options[@]}" : \ _arguments "${_arguments_options[@]}" : \ && ret=0 ;; -(hook) -_arguments "${_arguments_options[@]}" : \ -&& ret=0 -;; -(reload) +(sync) _arguments "${_arguments_options[@]}" : \ && ret=0 ;; @@ -485,8 +463,7 @@ _am_commands() { 'share:Generate a share command for posting aliases to a pastebin service' \ 'trust:Review and trust the project .aliases file in the current directory' \ 'untrust:Remove trust for the project .aliases file in the current directory' \ -'hook:Internal\: called by the cd hook to load/unload project aliases' \ -'reload:Internal\: called by the am wrapper to reload profile aliases after switching' \ +'sync:Internal\: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'am commands' commands "$@" @@ -518,8 +495,7 @@ _am__subcmd__help_commands() { 'share:Generate a share command for posting aliases to a pastebin service' \ 'trust:Review and trust the project .aliases file in the current directory' \ 'untrust:Remove trust for the project .aliases file in the current directory' \ -'hook:Internal\: called by the cd hook to load/unload project aliases' \ -'reload:Internal\: called by the am wrapper to reload profile aliases after switching' \ +'sync:Internal\: compute and emit the minimal shell ops to sync the shell with the effective merged alias state (global + profile + project)' \ 'help:Print this message or the help of the given subcommand(s)' \ ) _describe -t commands 'am help commands' commands "$@" @@ -539,11 +515,6 @@ _am__subcmd__help__subcmd__help_commands() { local commands; commands=() _describe -t commands 'am help help commands' commands "$@" } -(( $+functions[_am__subcmd__help__subcmd__hook_commands] )) || -_am__subcmd__help__subcmd__hook_commands() { - local commands; commands=() - _describe -t commands 'am help hook commands' commands "$@" -} (( $+functions[_am__subcmd__help__subcmd__import_commands] )) || _am__subcmd__help__subcmd__import_commands() { local commands; commands=() @@ -589,11 +560,6 @@ _am__subcmd__help__subcmd__profile__subcmd__use_commands() { local commands; commands=() _describe -t commands 'am help profile use commands' commands "$@" } -(( $+functions[_am__subcmd__help__subcmd__reload_commands] )) || -_am__subcmd__help__subcmd__reload_commands() { - local commands; commands=() - _describe -t commands 'am help reload commands' commands "$@" -} (( $+functions[_am__subcmd__help__subcmd__remove_commands] )) || _am__subcmd__help__subcmd__remove_commands() { local commands; commands=() @@ -614,6 +580,11 @@ _am__subcmd__help__subcmd__status_commands() { local commands; commands=() _describe -t commands 'am help status commands' commands "$@" } +(( $+functions[_am__subcmd__help__subcmd__sync_commands] )) || +_am__subcmd__help__subcmd__sync_commands() { + local commands; commands=() + _describe -t commands 'am help sync commands' commands "$@" +} (( $+functions[_am__subcmd__help__subcmd__trust_commands] )) || _am__subcmd__help__subcmd__trust_commands() { local commands; commands=() @@ -634,11 +605,6 @@ _am__subcmd__help__subcmd__use_commands() { local commands; commands=() _describe -t commands 'am help use commands' commands "$@" } -(( $+functions[_am__subcmd__hook_commands] )) || -_am__subcmd__hook_commands() { - local commands; commands=() - _describe -t commands 'am hook commands' commands "$@" -} (( $+functions[_am__subcmd__import_commands] )) || _am__subcmd__import_commands() { local commands; commands=() @@ -721,11 +687,6 @@ _am__subcmd__profile__subcmd__use_commands() { local commands; commands=() _describe -t commands 'am profile use commands' commands "$@" } -(( $+functions[_am__subcmd__reload_commands] )) || -_am__subcmd__reload_commands() { - local commands; commands=() - _describe -t commands 'am reload commands' commands "$@" -} (( $+functions[_am__subcmd__remove_commands] )) || _am__subcmd__remove_commands() { local commands; commands=() @@ -746,6 +707,11 @@ _am__subcmd__status_commands() { local commands; commands=() _describe -t commands 'am status commands' commands "$@" } +(( $+functions[_am__subcmd__sync_commands] )) || +_am__subcmd__sync_commands() { + local commands; commands=() + _describe -t commands 'am sync commands' commands "$@" +} (( $+functions[_am__subcmd__trust_commands] )) || _am__subcmd__trust_commands() { local commands; commands=() diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_after_active_set_changed.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_after_active_set_changed.snap deleted file mode 100644 index 5c9767bb..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_after_active_set_changed.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e ct -functions -e gs -alias ct "cargo test" -alias ll "ls -lha" -set -gx _AM_ALIASES "ct,ll" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_after_parent_profile_removed.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_after_parent_profile_removed.snap deleted file mode 100644 index b6e82c3d..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_after_parent_profile_removed.snap +++ /dev/null @@ -1,11 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e cm -functions -e cmf -functions -e gs -function cmf - cm feat: $argv -end -set -gx _AM_ALIASES "cmf" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_after_profile_removed.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_after_profile_removed.snap deleted file mode 100644 index 3b4c6b93..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_after_profile_removed.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e cm -functions -e ct -functions -e gs -alias cm "git commit -sm" -alias gs "git status" -set -gx _AM_ALIASES "cm,gs" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_bash_after_global_add.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_bash_after_global_add.snap deleted file mode 100644 index bc346a2f..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_bash_after_global_add.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -unalias cm 2>/dev/null; unset -f cm 2>/dev/null -unalias cmf 2>/dev/null; unset -f cmf 2>/dev/null -unalias gs 2>/dev/null; unset -f gs 2>/dev/null -alias ll="ls -lha" -alias cm="git commit -sm" -cmf() { cm feat: "$@"; } -alias gs="git status" -export _AM_ALIASES="cm,cmf,gs,ll" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_bash_switch_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_bash_switch_profile.snap deleted file mode 100644 index a67b370f..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_bash_switch_profile.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -unalias gs 2>/dev/null; unset -f gs 2>/dev/null -unalias cm 2>/dev/null; unset -f cm 2>/dev/null -alias cm="git commit -sm" -cmf() { cm feat: "$@"; } -alias gs="git status" -export _AM_ALIASES="cm,cmf,gs" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_after_global_add.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_after_global_add.snap deleted file mode 100644 index 6a0b444c..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_after_global_add.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e cm -functions -e cmf -functions -e gs -alias ll "ls -lha" -alias cm "git commit -sm" -function cmf - cm feat: $argv -end -alias gs "git status" -set -gx _AM_ALIASES "cm,cmf,gs,ll" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_globals_only_no_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_globals_only_no_profile.snap deleted file mode 100644 index 0913485e..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_globals_only_no_profile.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e old -alias gs "git status" -alias ll "ls -lha" -set -gx _AM_ALIASES "gs,ll" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_switch_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_switch_profile.snap deleted file mode 100644 index 87a263f9..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_fish_switch_profile.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -functions -e gs -functions -e cm -alias cm "git commit -sm" -function cmf - cm feat: $argv -end -alias gs "git status" -set -gx _AM_ALIASES "cm,cmf,gs" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_powershell_switch_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_powershell_switch_profile.snap deleted file mode 100644 index ef9610c5..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_powershell_switch_profile.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -if (Test-Path Function:\gs) { Remove-Item Function:\gs } -if (Test-Path Function:\cm) { Remove-Item Function:\cm } -function global:cmf { cm feat: $args } -$env:_AM_ALIASES = "cmf" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_zsh_after_global_add.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_zsh_after_global_add.snap deleted file mode 100644 index bc346a2f..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_zsh_after_global_add.snap +++ /dev/null @@ -1,12 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -unalias cm 2>/dev/null; unset -f cm 2>/dev/null -unalias cmf 2>/dev/null; unset -f cmf 2>/dev/null -unalias gs 2>/dev/null; unset -f gs 2>/dev/null -alias ll="ls -lha" -alias cm="git commit -sm" -cmf() { cm feat: "$@"; } -alias gs="git status" -export _AM_ALIASES="cm,cmf,gs,ll" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_reload_zsh_switch_profile.snap b/crates/am/tests/snapshots/snapshots__snapshot_reload_zsh_switch_profile.snap deleted file mode 100644 index a67b370f..00000000 --- a/crates/am/tests/snapshots/snapshots__snapshot_reload_zsh_switch_profile.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/am/tests/snapshots.rs -expression: output ---- -unalias gs 2>/dev/null; unset -f gs 2>/dev/null -unalias cm 2>/dev/null; unset -f cm 2>/dev/null -alias cm="git commit -sm" -cmf() { cm feat: "$@"; } -alias gs="git status" -export _AM_ALIASES="cm,cmf,gs" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_bash_fresh_load_project_only.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_bash_fresh_load_project_only.snap new file mode 100644 index 00000000..19ae4226 --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_bash_fresh_load_project_only.snap @@ -0,0 +1,6 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +alias b="cargo build" +export _AM_ALIASES="b|b58de66" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_bash_subcommand_wrapper_fresh_load.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_bash_subcommand_wrapper_fresh_load.snap new file mode 100644 index 00000000..50a78ca5 --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_bash_subcommand_wrapper_fresh_load.snap @@ -0,0 +1,12 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +jj() { + case "$1" in + ab) shift; command jj abandon "$@" ;; + *) command jj "$@" ;; + esac +} +export _AM_ALIASES="jj|33f7a66" +export _AM_SUBCOMMANDS="jj:ab|8296f9c" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap new file mode 100644 index 00000000..9a11d91f --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_fresh_load_project_only.snap @@ -0,0 +1,17 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +functions -e b +complete -e -c b +function b + cargo build $argv +end +complete -c b --wraps "cargo" +functions -e t +complete -e -c t +function t + cargo test $argv +end +complete -c t --wraps "cargo" +set -gx _AM_ALIASES "b|b58de66,t|ab61de4" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap new file mode 100644 index 00000000..578ffeb1 --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_incremental_one_alias_updated.snap @@ -0,0 +1,12 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +functions -e b +functions -e b +complete -e -c b +function b + cargo build --release $argv +end +complete -c b --wraps "cargo" +set -gx _AM_ALIASES "b|0dc3caa,t|ab61de4" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap new file mode 100644 index 00000000..613e869e --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_leaving_project_with_shadow_restoration.snap @@ -0,0 +1,13 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +functions -e b +functions -e t +functions -e t +complete -e -c t +function t + cargo test $argv +end +complete -c t --wraps "cargo" +set -gx _AM_ALIASES "t|ab61de4,ll|619d266" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap new file mode 100644 index 00000000..0aadfe84 --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_fish_transition_to_new_project.snap @@ -0,0 +1,12 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +functions -e old1 +functions -e new1 +complete -e -c new1 +function new1 + echo new $argv +end +complete -c new1 --wraps "echo" +set -gx _AM_ALIASES "new1|4d1fcee" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_powershell_fresh_load_project_only.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_powershell_fresh_load_project_only.snap new file mode 100644 index 00000000..14d39ffa --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_powershell_fresh_load_project_only.snap @@ -0,0 +1,6 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +function global:b { cargo build @args } +$env:_AM_ALIASES = "b|b58de66" diff --git a/crates/am/tests/snapshots/snapshots__snapshot_sync_zsh_fresh_load_project_only.snap b/crates/am/tests/snapshots/snapshots__snapshot_sync_zsh_fresh_load_project_only.snap new file mode 100644 index 00000000..19ae4226 --- /dev/null +++ b/crates/am/tests/snapshots/snapshots__snapshot_sync_zsh_fresh_load_project_only.snap @@ -0,0 +1,6 @@ +--- +source: crates/am/tests/snapshots.rs +expression: output +--- +alias b="cargo build" +export _AM_ALIASES="b|b58de66" diff --git a/website/.vitepress/theme/UseCases.vue b/website/.vitepress/theme/UseCases.vue index 756c0a6f..f74e679d 100644 --- a/website/.vitepress/theme/UseCases.vue +++ b/website/.vitepress/theme/UseCases.vue @@ -65,12 +65,12 @@
every day after that
am use node
-node activated — 4 loaded: l, b, r, t
+am: profile node activated — 4 loaded: l, b, r, t
 l      # npm run lint
 t      # npm test
 
 am use rust
-rust activated — 4 loaded: l, b, r, t
+am: profile rust activated — 4 loaded: l, b, r, t
 l      # cargo clippy --locked --all-targets -- -D warnings
 t      # cargo test
@@ -126,7 +126,11 @@
every developer, automatically
cd ~/work/myproject
-project activated — 4 loaded: ci, db, deploy, ti
+am: loaded .aliases
+  ci     → just ci-check
+  db     → just db-migrate --env staging
+  deploy → just deploy --target production
+  ti     → cargo test --features integration -- --test-threads 1
 
 am ls
 📁 project (.aliases)
@@ -251,12 +255,18 @@ api-7d4f9b8c6-xk2pm    1/1     Running   0
           
switching clients — nothing to remember
cd ~/clients/client-a
-client-a activated — 4 loaded: deploy, logs, stage, tf:plan
+am: loaded .aliases
+  deploy  → ./scripts/deploy.sh --env staging
+  logs    → ssh app@client-a.internal journalctl -u api -f
+  stage   → open https://staging.client-a.internal
+  tf:plan → terraform plan -var-file=client-a.tfvars
 deploy
 
 cd ~/clients/client-b
-client-a deactivated
-client-b activated — 3 loaded: infra:plan, preview, ship
+am: loaded .aliases
+  infra:plan → terraform -chdir=infra plan
+  preview    → open https://preview.client-b.internal
+  ship       → ./scripts/ship.sh
 preview
 
 # no stale aliases. no cross-contamination.
@@ -317,8 +327,8 @@ client-b activated — 3 loaded: infra:plan, preview, ship
what's actually loaded, right now
am use git work
-git activated — 4 loaded: gl, gm, gp, gs
-work activated — 3 loaded: jira, standup, vpn
+am: profile git activated — 4 loaded: gl, gm, gp, gs
+am: profile work activated — 3 loaded: jira, standup, vpn
 
 am ls
 ├─● git (active)
diff --git a/website/.vitepress/theme/UseCasesDe.vue b/website/.vitepress/theme/UseCasesDe.vue
index 18254f13..b9ae37f6 100644
--- a/website/.vitepress/theme/UseCasesDe.vue
+++ b/website/.vitepress/theme/UseCasesDe.vue
@@ -66,12 +66,12 @@
           
danach jeden Tag
am use node
-node activated — 4 loaded: l, b, r, t
+am: profile node activated — 4 loaded: l, b, r, t
 l      # npm run lint
 t      # npm test
 
 am use rust
-rust activated — 4 loaded: l, b, r, t
+am: profile rust activated — 4 loaded: l, b, r, t
 l      # cargo clippy --locked --all-targets -- -D warnings
 t      # cargo test
@@ -129,7 +129,11 @@
für jeden Entwickler, automatisch
cd ~/work/myproject
-project activated — 4 loaded: ci, db, deploy, ti
+am: loaded .aliases
+  ci     → just ci-check
+  db     → just db-migrate --env staging
+  deploy → just deploy --target production
+  ti     → cargo test --features integration -- --test-threads 1
 
 am ls
 📁 project (.aliases)
@@ -257,12 +261,18 @@ api-7d4f9b8c6-xk2pm    1/1     Running   0
           
Kunden wechseln — nichts zu merken
cd ~/clients/client-a
-client-a activated — 4 loaded: deploy, logs, stage, tf:plan
+am: loaded .aliases
+  deploy  → ./scripts/deploy.sh --env staging
+  logs    → ssh app@client-a.internal journalctl -u api -f
+  stage   → open https://staging.client-a.internal
+  tf:plan → terraform plan -var-file=client-a.tfvars
 deploy
 
 cd ~/clients/client-b
-client-a deactivated
-client-b activated — 3 loaded: infra:plan, preview, ship
+am: loaded .aliases
+  infra:plan → terraform -chdir=infra plan
+  preview    → open https://preview.client-b.internal
+  ship       → ./scripts/ship.sh
 preview
 
 # keine veralteten Aliase. keine Vermischung.
@@ -325,8 +335,8 @@ client-b activated — 3 loaded: infra:plan, preview, ship
was gerade aktiv ist
am use git work
-git activated — 4 loaded: gl, gm, gp, gs
-work activated — 3 loaded: jira, standup, vpn
+am: profile git activated — 4 loaded: gl, gm, gp, gs
+am: profile work activated — 3 loaded: jira, standup, vpn
 
 am ls
 ├─● git (active)
diff --git a/website/de/guide/installation.md b/website/de/guide/installation.md
index 1fd1d96d..c405c7cb 100644
--- a/website/de/guide/installation.md
+++ b/website/de/guide/installation.md
@@ -57,7 +57,7 @@ Der TUI-Companion (`am-tui`) ist ein separates Paket. Optional, aber empfohlen f
 | `am status` | Prüfen, ob die Shell korrekt eingerichtet ist |
 | `am setup` | Geführte Shell-Einrichtung |
 | `am tui` | Interaktives TUI zur Alias-Verwaltung (*separate Installation*) |
-| `am hook` | Wird vom cd-Hook aufgerufen (intern) |
+| `am sync` | Wird vom Shell-Wrapper und cd-Hook zum Aliassynchronisieren aufgerufen (intern) |
 
 ::: tip
 Alle Verben haben Kurzformen: `am a` für add, `am r` für remove, `am p` für profile, `am l` für ls.
diff --git a/website/de/usage/project-aliases.md b/website/de/usage/project-aliases.md
index 3dcc2047..590138be 100644
--- a/website/de/usage/project-aliases.md
+++ b/website/de/usage/project-aliases.md
@@ -117,12 +117,15 @@ Diese Meldungen erscheinen nur beim Betreten oder Verlassen des Verzeichnisses m
 
 ## Wie es funktioniert
 
-Der `am init` Shell-Hook ruft `am hook ` bei jedem Verzeichniswechsel auf. Der Hook:
+Der `am init` Shell-Hook ruft `am sync ` bei jedem Verzeichniswechsel auf. Sync:
 
 1. Sucht vom aktuellen Verzeichnis aufwärts nach einer `.aliases`-Datei (stoppt vor `$HOME`)
 2. Prüft, ob die Datei vertrauenswürdig ist (Pfad + Hash in `security.toml`)
-3. Falls vertrauenswürdig: entlädt vorherige Projekt-Aliase und lädt die neuen
-4. Falls nicht vertrauenswürdig: zeigt eine Warnung oder bleibt still, je nach Vertrauensstatus
+3. Führt alle Ebenen mit Präzedenz zusammen — global < Profil < Projekt — und berechnet den minimalen Satz an Shell-Operationen
+4. Gibt nur die Unloads/Loads aus, die sich tatsächlich auf die Shell auswirken (unveränderte Aliase bleiben bestehen)
+5. Falls die Datei nicht vertrauenswürdig ist, zeigt eine Warnung oder bleibt still, je nach Vertrauensstatus
+
+Dadurch folgen Aliase automatisch dem Kontext — ein Wechsel in ein Rust-Projekt lädt die Rust-Aliase, ein Wechsel in ein Node-Projekt die Node-Aliase — vorausgesetzt, die jeweiligen `.aliases`-Dateien sind vertrauenswürdig. Wenn ein Projekt-Alias denselben Namen wie ein Profil-Alias hat, gewinnt der Projekt-Wert; beim Verlassen des Projekts übernimmt automatisch wieder der Profil-Wert.
 
 ## Workflow
 
diff --git a/website/de/usage/subcommand-aliases.md b/website/de/usage/subcommand-aliases.md
index 50f01397..aff2bde5 100644
--- a/website/de/usage/subcommand-aliases.md
+++ b/website/de/usage/subcommand-aliases.md
@@ -79,7 +79,7 @@ Kurzform: `am r -g jj:ab`
 
 ## Wie es funktioniert
 
-Beim Shell-Init (und bei `am reload`) generiert amoxide eine Wrapper-Funktion für jedes Programm mit Subcommand-Aliasen:
+Beim Shell-Init (und bei jedem `am sync`, ausgelöst durch `cd` oder eine `am`-Änderung) generiert amoxide eine Wrapper-Funktion für jedes Programm mit Subcommand-Aliasen:
 
 ```sh
 # generiert für jj (bash/zsh)
diff --git a/website/guide/installation.md b/website/guide/installation.md
index 39a679ef..9f838e15 100644
--- a/website/guide/installation.md
+++ b/website/guide/installation.md
@@ -57,7 +57,7 @@ The TUI companion (`am-tui`) is a separate install. It's optional but recommende
 | `am status` | Check if the shell is set up correctly |
 | `am setup` | Guided shell setup |
 | `am tui` | Interactive TUI for managing aliases and profiles (*separate install*) |
-| `am hook` | Called by the cd hook (internal) |
+| `am sync` | Called by the shell wrapper and cd hook to sync aliases (internal) |
 
 ::: tip
 All verbs have short forms: `am a` for add, `am r` for remove, `am p` for profile, `am l` for ls.
diff --git a/website/usage/project-aliases.md b/website/usage/project-aliases.md
index 8fee5981..29d9c4cb 100644
--- a/website/usage/project-aliases.md
+++ b/website/usage/project-aliases.md
@@ -117,14 +117,15 @@ These messages only appear when entering or leaving the directory containing the
 
 ## How It Works
 
-The `am init` shell hook calls `am hook ` on every directory change. The hook:
+The `am init` shell hook calls `am sync ` on every directory change. Sync:
 
 1. Walks up from the current directory looking for a `.aliases` file (stopping before `$HOME`)
 2. Checks whether the file is trusted (path + hash match in `security.toml`)
-3. If trusted: unloads any previously active project aliases and loads the new ones
-4. If not trusted: shows a warning or stays silent, depending on the trust state
+3. Merges all layers with precedence — global < profile < project — and computes the minimal set of shell operations needed
+4. Emits only the unloads/loads that actually change the shell (unchanged aliases stay put)
+5. If the file is not trusted, shows a warning or stays silent, depending on the trust state
 
-This means aliases automatically follow your context — switch to a Rust project and get Rust aliases, switch to a Node project and get Node aliases — as long as you've trusted the respective `.aliases` files.
+This means aliases automatically follow your context — switch to a Rust project and get Rust aliases, switch to a Node project and get Node aliases — as long as you've trusted the respective `.aliases` files. When a project alias shares a name with a profile alias, the project value wins; when you leave the project, the profile value takes over automatically.
 
 ## Workflow
 
diff --git a/website/usage/subcommand-aliases.md b/website/usage/subcommand-aliases.md
index 3d5ef58b..3ff15676 100644
--- a/website/usage/subcommand-aliases.md
+++ b/website/usage/subcommand-aliases.md
@@ -86,7 +86,7 @@ Short form: `am r -g jj:ab`
 
 ## How It Works
 
-At shell init (and on `am reload`), amoxide generates a wrapper function for each program that has subcommand aliases:
+At shell init (and on every `am sync` triggered by `cd` or an `am` mutation), amoxide generates a wrapper function for each program that has subcommand aliases:
 
 ```sh
 # generated for jj (bash/zsh)