1use clap::CommandFactory;
7use clap_complete::Shell;
8use std::io::{self, Write};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct CompletionOutput {
13 pub instructions: String,
15 pub script: String,
17}
18
19#[must_use]
21pub fn render_completion_instructions(shell: Shell, bin_name: &str) -> String {
22 match shell {
23 Shell::Bash => format!(
24 "# Shell completion for {bin_name}\n#\n# To enable completions, add this to your shell config:\n#\n# source <({bin_name} completions bash)\n\n"
25 ),
26 Shell::Zsh => format!(
27 "# Shell completion for {bin_name}\n#\n# To enable completions, add this to your shell config:\n#\n# {bin_name} completions zsh > ~/.zsh/completions/_{bin_name}\n# # Ensure fpath includes ~/.zsh/completions\n\n"
28 ),
29 Shell::Fish => format!(
30 "# Shell completion for {bin_name}\n#\n# To enable completions, add this to your shell config:\n#\n# {bin_name} completions fish | source\n\n"
31 ),
32 Shell::PowerShell => format!(
33 "# Shell completion for {bin_name}\n#\n# To enable completions, add this to your shell config:\n#\n# {bin_name} completions powershell | Out-String | Invoke-Expression\n\n"
34 ),
35 Shell::Elvish => format!(
36 "# Shell completion for {bin_name}\n#\n# To enable completions, add this to your shell config:\n#\n# {bin_name} completions elvish | eval\n\n"
37 ),
38 other => format!(
39 "# Shell completion for {bin_name}\n#\n# To enable completions, add this to your shell config:\n#\n# {bin_name} completions {other}\n\n"
40 ),
41 }
42}
43
44#[must_use]
46pub fn render_completion<T: CommandFactory>(shell: Shell) -> CompletionOutput {
47 render_completion_from_command(shell, T::command())
48}
49
50#[must_use]
52pub fn render_completion_from_command(
53 shell: Shell,
54 mut command: clap::Command,
55) -> CompletionOutput {
56 let bin_name = command.get_name().to_string();
57 let mut buffer = Vec::new();
58
59 clap_complete::generate(shell, &mut command, bin_name.clone(), &mut buffer);
60
61 CompletionOutput {
62 instructions: render_completion_instructions(shell, &bin_name),
63 script: String::from_utf8_lossy(&buffer).into_owned(),
64 }
65}
66
67pub fn write_completion(mut writer: impl Write, output: &CompletionOutput) -> io::Result<()> {
73 writer.write_all(output.instructions.as_bytes())?;
74 writer.write_all(output.script.as_bytes())
75}
76
77pub fn generate_completions<T: CommandFactory>(shell: Shell) -> io::Result<()> {
106 generate_completions_from_command(shell, T::command())
107}
108
109pub fn generate_completions_from_command(shell: Shell, command: clap::Command) -> io::Result<()> {
116 let output = render_completion_from_command(shell, command);
117 write_completion(io::stdout(), &output)
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::agent::apply_agent_surface;
124 use crate::{
125 AgentCapability, AgentModeContext, AgentSurfaceSpec, CommandSelector, FlagSelector,
126 LicenseType, RepoInfo, ToolSpec,
127 };
128 use clap::{Arg, Parser, Subcommand};
129
130 #[derive(Parser)]
131 #[command(name = "test-cli")]
132 struct TestCli {
133 #[command(subcommand)]
134 command: TestCommands,
135 }
136
137 #[derive(Subcommand)]
138 enum TestCommands {
139 Version,
140 Test { arg: String },
141 }
142
143 const QUERY_COMMAND: CommandSelector = CommandSelector::new(&["query"]);
144 const QUERY_LIMIT_FLAG: FlagSelector = FlagSelector::new(&["query"], "limit");
145 const QUERY_CAPABILITY: AgentCapability = AgentCapability::new(
146 "query-posts",
147 "Read paginated post records",
148 &[QUERY_COMMAND],
149 &[QUERY_LIMIT_FLAG],
150 );
151 const AGENT_SURFACE: AgentSurfaceSpec = AgentSurfaceSpec::new(&[QUERY_CAPABILITY]);
152
153 fn agent_spec() -> ToolSpec {
154 ToolSpec::new(
155 "test-cli",
156 "Test CLI",
157 "1.2.3",
158 LicenseType::MIT,
159 RepoInfo::new("owner", "repo"),
160 true,
161 false,
162 )
163 .with_agent_surface(&AGENT_SURFACE)
164 }
165
166 #[test]
167 fn test_generate_completions_bash() {
168 let _ = generate_completions::<TestCli>(Shell::Bash);
170 }
171
172 #[test]
173 fn test_generate_completions_zsh() {
174 let _ = generate_completions::<TestCli>(Shell::Zsh);
175 }
176
177 #[test]
178 fn test_generate_completions_fish() {
179 let _ = generate_completions::<TestCli>(Shell::Fish);
180 }
181
182 #[test]
183 fn test_generate_completions_elvish() {
184 let _ = generate_completions::<TestCli>(Shell::Elvish);
185 }
186
187 #[test]
188 fn test_generate_completions_powershell() {
189 let _ = generate_completions::<TestCli>(Shell::PowerShell);
190 }
191
192 #[test]
193 fn test_all_shells_generate_without_panic() {
194 let shells = vec![
195 Shell::Bash,
196 Shell::Zsh,
197 Shell::Fish,
198 Shell::Elvish,
199 Shell::PowerShell,
200 ];
201
202 for shell in shells {
203 let _ = generate_completions::<TestCli>(shell);
204 }
205 }
206
207 #[test]
208 fn render_completion_separates_instructions_from_script() {
209 let output = render_completion::<TestCli>(Shell::Bash);
210
211 assert!(
212 output
213 .instructions
214 .contains("source <(test-cli completions bash)")
215 );
216 assert!(output.script.contains("complete"));
217 }
218
219 #[test]
220 fn agent_surface_redaction_completion_helper_omits_hidden_entries() {
221 let mut command = clap::Command::new("test-cli")
222 .subcommand(
223 clap::Command::new("query")
224 .arg(Arg::new("limit").long("limit"))
225 .arg(Arg::new("secret").long("secret")),
226 )
227 .subcommand(clap::Command::new("admin"));
228
229 apply_agent_surface(
230 &mut command,
231 &agent_spec(),
232 &AgentModeContext { active: true },
233 );
234
235 let output = render_completion_from_command(Shell::Bash, command);
236
237 assert!(output.script.contains("query"));
238 assert!(!output.script.contains("admin"));
239 assert!(!output.script.contains("--secret"));
240 }
241}