Skip to content

feat: add zenodo completion install command#65

Merged
ran-codes merged 4 commits intomainfrom
feat/completion-install
Feb 20, 2026
Merged

feat: add zenodo completion install command#65
ran-codes merged 4 commits intomainfrom
feat/completion-install

Conversation

@ran-codes
Copy link
Copy Markdown
Owner

@ran-codes ran-codes commented Feb 20, 2026

Summary

  • Adds zenodo completion install subcommand that auto-configures shell completions
  • Auto-detects shell (or accepts --shell flag)
  • Shows what will be modified and asks for confirmation before writing
  • Supports bash, zsh, fish, and powershell
  • Fish handled specially (writes completion file directly instead of appending source line)
  • Detects if completions are already installed and skips if so
  • Uses the absolute path to the running executable, so it works both during local dev (./zenodo.exe) and after Scoop install (zenodo)

ELI5

Right now you can press Tab to autocomplete flags, but only if you manually edit your shell config file. This adds a one-time zenodo completion install command that does that setup for you — it figures out your shell, shows you what it'll change, and asks before doing it.

How to test

1. Build

go build -o zenodo.exe ./cmd/zenodo

2. Install completions

In PowerShell, run:

./zenodo.exe completion install --shell powershell

You should see something like:

Shell:   powershell
File:    C:\Users\you\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1
Append:  & "D:\GitHub\zenodo-cli\zenodo.exe" completion powershell | Out-String | Invoke-Expression

Proceed? [y/N]

Type y and hit Enter.

3. Restart PowerShell

Close the window and open a new one.

4. Try tab completion

Type each of these and press Tab — you should see options autocomplete:

# Autocomplete subcommands
./zenodo.exe <Tab>
# Should cycle through: access, communities, completion, config, licenses, records, version

# Autocomplete nested subcommands
./zenodo.exe records <Tab>
# Should cycle through: get, list, search, versions

# Autocomplete flags
./zenodo.exe records list --<Tab>
# Should cycle through: --authored, --uploaded, --community, --output, etc.

5. Run again — should say already installed

./zenodo.exe completion install --shell powershell
# Should print: Completions already installed in ...

Closes #63

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 20, 2026 20:47
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a zenodo completion install subcommand that automates shell completion setup by detecting the user's shell (or accepting a --shell flag), showing what will be modified, and appending the appropriate completion source line to the shell's profile file after user confirmation. This eliminates the manual step of editing shell configuration files that most users skip, making tab completion more accessible.

Changes:

  • Added zenodo completion install subcommand with shell auto-detection
  • Implemented shell-specific profile configuration for bash, zsh, fish, and powershell
  • Added confirmation prompts before modifying shell configuration files
  • Fish shell handled specially by writing completion file directly instead of sourcing

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +97 to +111
fmt.Printf("Completions already installed in %s\n", profilePath)
return nil
}

// Show what we'll do and ask for confirmation.
fmt.Printf("Shell: %s\n", shell)
fmt.Printf("File: %s\n", profilePath)
fmt.Printf("Append: %s\n", sourceLine)
fmt.Print("\nProceed? [y/N] ")

reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "y" && answer != "yes" {
fmt.Println("Cancelled.")
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational messages should be written to stderr for consistency with the rest of the codebase. Other commands consistently use fmt.Fprintf(os.Stderr, ...) for user-facing informational messages (see config.go:50, deposit.go:48, records.go:72). This allows users to redirect actual output separately from status messages.

Suggested change
fmt.Printf("Completions already installed in %s\n", profilePath)
return nil
}
// Show what we'll do and ask for confirmation.
fmt.Printf("Shell: %s\n", shell)
fmt.Printf("File: %s\n", profilePath)
fmt.Printf("Append: %s\n", sourceLine)
fmt.Print("\nProceed? [y/N] ")
reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "y" && answer != "yes" {
fmt.Println("Cancelled.")
fmt.Fprintf(os.Stderr, "Completions already installed in %s\n", profilePath)
return nil
}
// Show what we'll do and ask for confirmation.
fmt.Fprintf(os.Stderr, "Shell: %s\n", shell)
fmt.Fprintf(os.Stderr, "File: %s\n", profilePath)
fmt.Fprintf(os.Stderr, "Append: %s\n", sourceLine)
fmt.Fprint(os.Stderr, "\nProceed? [y/N] ")
reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "y" && answer != "yes" {
fmt.Fprintln(os.Stderr, "Cancelled.")

Copilot uses AI. Check for mistakes.
fmt.Printf("Shell: %s\n", shell)
fmt.Printf("File: %s\n", profilePath)
fmt.Printf("Append: %s\n", sourceLine)
fmt.Print("\nProceed? [y/N] ")
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The confirmation prompt message should be written to stderr for consistency. See deposit.go:294 where the confirm() helper writes the prompt to stderr. This follows the convention of sending user prompts to stderr while reserving stdout for actual program output.

Suggested change
fmt.Print("\nProceed? [y/N] ")
fmt.Fprint(os.Stderr, "\nProceed? [y/N] ")

Copilot uses AI. Check for mistakes.
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "y" && answer != "yes" {
fmt.Println("Cancelled.")
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "Cancelled." message should be written to stderr for consistency. Throughout the codebase, status messages like this are written to stderr (see deposit.go:188). This allows users to distinguish program output from status messages.

Suggested change
fmt.Println("Cancelled.")
fmt.Fprintln(os.Stderr, "Cancelled.")

Copilot uses AI. Check for mistakes.
return fmt.Errorf("writing to %s: %w", profilePath, err)
}

fmt.Printf("\nDone! Restart your shell or run:\n source %s\n", profilePath)
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The success message should be written to stderr for consistency. Throughout the codebase, informational messages to the user are written to stderr (see config.go:50, deposit.go:129). This ensures that stdout remains clean for program output that might be piped to other commands.

Suggested change
fmt.Printf("\nDone! Restart your shell or run:\n source %s\n", profilePath)
fmt.Fprintf(os.Stderr, "\nDone! Restart your shell or run:\n source %s\n", profilePath)

Copilot uses AI. Check for mistakes.

switch shell {
case "bash":
return filepath.Join(home, ".bashrc"),
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On macOS, login shells typically source .bash_profile while non-login shells source .bashrc. Many macOS users don't have a .bashrc or it's not sourced by default. Consider checking for .bash_profile on macOS (when runtime.GOOS == "darwin") as the primary location, falling back to .bashrc if it doesn't exist. Alternatively, append to both files if they exist, or provide guidance to users about which file was modified.

Suggested change
return filepath.Join(home, ".bashrc"),
bashProfile := filepath.Join(home, ".bash_profile")
bashRC := filepath.Join(home, ".bashrc")
// On macOS, login shells typically source .bash_profile, while non-login shells
// source .bashrc. Prefer .bash_profile when it exists, falling back to .bashrc.
if runtime.GOOS == "darwin" {
if _, err := os.Stat(bashProfile); err == nil {
return bashProfile,
`eval "$(zenodo completion bash)"`,
nil
}
// If .bash_profile doesn't exist, fall back to .bashrc (which may or may not exist).
return bashRC,
`eval "$(zenodo completion bash)"`,
nil
}
// Non-macOS: keep using .bashrc as before.
return bashRC,

Copilot uses AI. Check for mistakes.
Comment on lines +221 to +251
fmt.Printf("Fish completions already installed at %s\n", path)
return nil
}

fmt.Printf("Write fish completions to %s\n", path)
fmt.Print("Proceed? [y/N] ")

reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "y" && answer != "yes" {
fmt.Println("Cancelled.")
return nil
}

// Ensure directory exists.
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Errorf("creating directory: %w", err)
}

f, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating %s: %w", path, err)
}
defer f.Close()

if err := rootCmd.GenFishCompletion(f, true); err != nil {
return fmt.Errorf("generating fish completions: %w", err)
}

fmt.Printf("\nDone! Completions will be loaded automatically in new fish sessions.\n")
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All user-facing messages in this function should be written to stderr for consistency. The codebase consistently uses fmt.Fprintf(os.Stderr, ...) for informational messages (see config.go:50, deposit.go:48, records.go:72).

Copilot uses AI. Check for mistakes.
Comment on lines +196 to +203
for _, ps := range []string{"pwsh", "powershell"} {
out, err := exec.Command(ps, "-NoProfile", "-Command", "echo $PROFILE").Output()
if err == nil {
p := strings.TrimSpace(string(out))
if p != "" {
return p
}
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PowerShell command execution could hang indefinitely if PowerShell has issues. Consider adding a timeout using exec.CommandContext with a context that has a deadline (e.g., 5 seconds). This prevents the command from hanging indefinitely if PowerShell is unresponsive.

Copilot uses AI. Check for mistakes.

// Check if already installed.
if alreadyInstalled(profilePath, sourceLine) {
fmt.Printf("Completions already installed in %s\n", profilePath)
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "already installed" message should be written to stderr for consistency. Other informational messages in the codebase are consistently written to stderr (see config.go:50, deposit.go:48).

Suggested change
fmt.Printf("Completions already installed in %s\n", profilePath)
fmt.Fprintf(os.Stderr, "Completions already installed in %s\n", profilePath)

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +113
reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "y" && answer != "yes" {
fmt.Println("Cancelled.")
return nil
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extracting this confirmation prompt logic into a reusable helper function to reduce duplication. The same pattern is repeated in installFishCompletion at lines 228-233, and similar logic exists in deposit.go:293-299 (currently commented out). A shared helper would improve maintainability and ensure consistent behavior across commands.

Copilot uses AI. Check for mistakes.
ran-codes and others added 3 commits February 20, 2026 15:55
The source line in the shell profile now uses the full path to the
executable so it works regardless of PATH or working directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users install via Scoop so zenodo is on PATH. The absolute exe path
was wrong since it would break when the binary moves.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolves the running executable's absolute path so completions work
both during local dev (./zenodo.exe) and after Scoop install.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ran-codes ran-codes merged commit 8022f14 into main Feb 20, 2026
1 check passed
@ran-codes ran-codes deleted the feat/completion-install branch February 20, 2026 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add zenodo completion install command for automatic shell setup

2 participants