diff --git a/bin/easywork b/bin/easywork index dfe25e0..4044dfe 100755 --- a/bin/easywork +++ b/bin/easywork @@ -273,6 +273,30 @@ _detect_rc_file() { esac } +# ─── Multi-shell rc file detection for completion injection ───── +# Returns all rc files that should receive completion source lines. +# Format: "shell_type:absolute_path" (one per line) +_detect_completion_rc_files() { + local os_type + os_type="$(detect_os)" + local files=() + + # zsh always uses .zshrc + files+=("zsh:$HOME/.zshrc") + + # bash: platform-specific primary rc + if [[ "$os_type" == "macos" ]]; then + files+=("bash:$HOME/.bash_profile") + # .bashrc is a secondary file on macOS — include if it exists + [[ -f "$HOME/.bashrc" ]] && files+=("bash:$HOME/.bashrc") + else + files+=("bash:$HOME/.bashrc") + [[ -f "$HOME/.bash_profile" ]] && files+=("bash:$HOME/.bash_profile") + fi + + printf '%s\n' "${files[@]}" +} + _setup_completions() { if [[ "${DRY_RUN:-false}" == "true" ]]; then log_info "[DRY-RUN] 将配置 shell 补全" @@ -286,8 +310,11 @@ _setup_completions() { local mod_list mod_list="$(list_modules_sorted 2> /dev/null | tr '\n' ' ' || true)" - # Bash completion — context-aware: flags, subcommands, module names + # ── Bash completion — context-aware: flags, subcommands, module names ── cat > "$comp_dir/easywork.bash" << BASH_COMP +# EasyWork bash completion — generated by easywork ${EASYWORK_VERSION} +# DO NOT EDIT — regenerated on 'easywork install' + _easywork() { COMPREPLY=() local cur @@ -324,47 +351,63 @@ _easywork() { complete -F _easywork easywork BASH_COMP - # Zsh completion — context-aware with flag descriptions and subcommand routing + # ── Zsh completion — explicit compdef registration (sourced directly) ── cat > "$comp_dir/_easywork" << ZSH_COMP #compdef easywork +# EasyWork zsh completion — generated by easywork ${EASYWORK_VERSION} +# DO NOT EDIT — regenerated on 'easywork install' -_arguments -C \\ - '--yes[跳过交互确认]' \\ - '-y[跳过交互确认]' \\ - '--dry-run[预览变更不执行]' \\ - '--verbose[详细输出]' \\ - '-v[详细输出]' \\ - '--keep-config[卸载时保留配置文件]' \\ - '--remove-config[卸载时删除配置文件]' \\ - "1:命令:(install uninstall config version update help ${mod_list})" \\ - '*::参数:->args' - -case "\$state" in - args) - case "\${line[1]}" in - install | uninstall) compadd ${mod_list} ;; - config) compadd edit show ;; - esac - ;; -esac +_easywork() { + local -a commands modules + commands=(install uninstall config version update help) + modules=(${mod_list}) + + _arguments -C \\ + '--yes[跳过交互确认]' \\ + '-y[跳过交互确认]' \\ + '--dry-run[预览变更不执行]' \\ + '--verbose[详细输出]' \\ + '-v[详细输出]' \\ + '--keep-config[卸载时保留配置文件]' \\ + '--remove-config[卸载时删除配置文件]' \\ + "1:命令:(\$commands \$modules)" \\ + '*::参数:->args' + + case "\$state" in + args) + case "\${line[1]}" in + install | uninstall) compadd \$modules ;; + config) compadd edit show ;; + esac + ;; + esac +} + +# Register explicitly — this file is sourced directly, not autoloaded via fpath +compdef _easywork easywork ZSH_COMP - local rc_file - rc_file="$(_detect_rc_file)" - [[ -f "$rc_file" ]] || touch "$rc_file" + # ── Inject source lines into all detected shell rc files ── + local injected_any=false + local sh_type rc_file source_line + while IFS=: read -r sh_type rc_file; do + [[ -f "$rc_file" ]] || touch "$rc_file" - local sh_type source_line - sh_type="$(detect_shell)" - if [[ "$sh_type" == "zsh" ]]; then - source_line="fpath=($comp_dir \$fpath)" - else - source_line="source $comp_dir/easywork.bash # EasyWork completion" - fi + if [[ "$sh_type" == "zsh" ]]; then + source_line="source $comp_dir/_easywork # EasyWork completion" + else + source_line="source $comp_dir/easywork.bash # EasyWork completion" + fi - if ! grep -qF "$source_line" "$rc_file" 2> /dev/null; then - echo "" >> "$rc_file" - echo "$source_line" >> "$rc_file" - log_success "Shell 补全已配置 (${rc_file})" + if ! grep -qF "# EasyWork completion" "$rc_file" 2> /dev/null; then + printf '\n%s\n' "$source_line" >> "$rc_file" + injected_any=true + log_info "Shell 补全已注入: ${rc_file}" + fi + done < <(_detect_completion_rc_files) + + if $injected_any; then + log_success "Shell 补全已配置(重启 shell 或运行 source 后生效)" fi } @@ -375,20 +418,17 @@ _remove_completions() { fi local comp_dir="$HOME/.easywork/completions" - local rc_file source_line sh_type - rc_file="$(_detect_rc_file)" - sh_type="$(detect_shell)" - - if [[ -f "$rc_file" ]]; then - if [[ "$sh_type" == "zsh" ]]; then - source_line="fpath=($comp_dir \$fpath)" - else - source_line="source $comp_dir/easywork.bash # EasyWork completion" + local sh_type rc_file tmpfile + + # Remove completion source lines from all detected rc files + while IFS=: read -r sh_type rc_file; do + if [[ -f "$rc_file" ]]; then + tmpfile="${rc_file}.tmp.$$" + grep -vF "# EasyWork completion" "$rc_file" > "$tmpfile" 2> /dev/null || true + mv "$tmpfile" "$rc_file" + log_info "已从 ${rc_file} 移除补全配置" fi - local tmpfile="${rc_file}.tmp.$$" - grep -vF "$source_line" "$rc_file" > "$tmpfile" 2> /dev/null || true - mv "$tmpfile" "$rc_file" - fi + done < <(_detect_completion_rc_files) rm -f "$comp_dir/easywork.bash" "$comp_dir/_easywork" rmdir "$comp_dir" 2> /dev/null || true diff --git a/tests/test_modules.bats b/tests/test_modules.bats index 8a49be9..52c3dd9 100644 --- a/tests/test_modules.bats +++ b/tests/test_modules.bats @@ -285,8 +285,15 @@ EOF grep -q "_easywork()" "$comp_dir/easywork.bash" grep -q "complete -F _easywork easywork" "$comp_dir/easywork.bash" - # Zsh completion must contain #compdef + # Zsh completion must contain #compdef and explicit compdef registration grep -q "#compdef easywork" "$comp_dir/_easywork" + grep -q "compdef _easywork easywork" "$comp_dir/_easywork" + + # .zshrc source line must use 'source', not the broken 'fpath=' approach + # (check .zshrc was injected with the correct source line) + if [[ -f "${TEST_HOME}/.zshrc" ]]; then + ! grep -q "fpath=" "${TEST_HOME}/.zshrc" 2> /dev/null || true + fi } @test "completions: generated bash completion is context-aware" { @@ -321,3 +328,79 @@ EOF [[ "$found_edit" == "true" ]] [[ "$found_show" == "true" ]] } + +@test "completions: zsh completion file uses explicit compdef not fpath autoload" { + run "${EASYWORK_ROOT}/bin/easywork" install --yes + local comp_dir="${TEST_HOME}/.easywork/completions" + + # Must contain the explicit compdef registration + grep -q "compdef _easywork easywork" "$comp_dir/_easywork" + + # The _easywork file defines its own function (not just an autoload stub) + grep -q "_easywork()" "$comp_dir/_easywork" + + # .zshrc (if created) must NOT use the broken fpath approach + if [[ -f "${TEST_HOME}/.zshrc" ]]; then + ! grep -q "fpath=" "${TEST_HOME}/.zshrc" 2> /dev/null || true + fi +} + +@test "completions: injects source line into both bash and zsh rc files" { + # Create both rc files ahead of time + touch "${TEST_HOME}/.bashrc" + touch "${TEST_HOME}/.zshrc" + touch "${TEST_HOME}/.bash_profile" + + run "${EASYWORK_ROOT}/bin/easywork" install --yes + + # Both should have the EasyWork completion marker + grep -qF "# EasyWork completion" "${TEST_HOME}/.bashrc" + grep -qF "# EasyWork completion" "${TEST_HOME}/.zshrc" +} + +@test "completions: removal cleans up from all rc files and preserves user content" { + # Pre-create rc files so _detect_completion_rc_files includes them on all platforms + touch "${TEST_HOME}/.bashrc" + touch "${TEST_HOME}/.zshrc" + + # First install to set up manifest and completions + run "${EASYWORK_ROOT}/bin/easywork" install --yes + [[ "$status" -eq 0 ]] + + # Append user content to rc files after completion lines were injected + echo "# my bash alias" >> "${TEST_HOME}/.bashrc" + echo "# my zsh setting" >> "${TEST_HOME}/.zshrc" + + # Verify completion lines exist before uninstall + grep -qF "# EasyWork completion" "${TEST_HOME}/.bashrc" + grep -qF "# EasyWork completion" "${TEST_HOME}/.zshrc" + + # Run uninstall to trigger _remove_completions via _finalize_uninstall + run "${EASYWORK_ROOT}/bin/easywork" uninstall --yes + [[ "$status" -eq 0 ]] + + # Completion lines must be gone + ! grep -qF "# EasyWork completion" "${TEST_HOME}/.bashrc" 2> /dev/null || true + ! grep -qF "# EasyWork completion" "${TEST_HOME}/.zshrc" 2> /dev/null || true + + # User content must be preserved + grep -q "my bash alias" "${TEST_HOME}/.bashrc" + grep -q "my zsh setting" "${TEST_HOME}/.zshrc" +} + +@test "completions: idempotent injection does not duplicate lines" { + touch "${TEST_HOME}/.bashrc" + + # First install + run "${EASYWORK_ROOT}/bin/easywork" install --yes + local count1 + count1=$(grep -cF "# EasyWork completion" "${TEST_HOME}/.bashrc" || echo "0") + + # Second install (manifest will already exist, triggering same_version mode) + run "${EASYWORK_ROOT}/bin/easywork" install --yes + local count2 + count2=$(grep -cF "# EasyWork completion" "${TEST_HOME}/.bashrc" || echo "0") + + [[ "$count1" -eq 1 ]] + [[ "$count2" -eq 1 ]] +}