tftio_cli_common/
completions.rs1use 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(buffer).expect("clap_complete output must be valid UTF-8"),
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<()> {
101 generate_completions_from_command(shell, T::command())
102}
103
104pub fn generate_completions_from_command(shell: Shell, command: clap::Command) -> io::Result<()> {
106 let output = render_completion_from_command(shell, command);
107 write_completion(io::stdout(), &output)
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::agent::apply_agent_surface;
114 use crate::{
115 AgentCapability, AgentModeContext, AgentSurfaceSpec, CommandSelector, FlagSelector,
116 LicenseType, RepoInfo, ToolSpec,
117 };
118 use clap::{Arg, Parser, Subcommand};
119
120 #[derive(Parser)]
121 #[command(name = "test-cli")]
122 struct TestCli {
123 #[command(subcommand)]
124 command: TestCommands,
125 }
126
127 #[derive(Subcommand)]
128 enum TestCommands {
129 Version,
130 Test { arg: String },
131 }
132
133 const QUERY_COMMAND: CommandSelector = CommandSelector::new(&["query"]);
134 const QUERY_LIMIT_FLAG: FlagSelector = FlagSelector::new(&["query"], "limit");
135 const QUERY_CAPABILITY: AgentCapability = AgentCapability::new(
136 "query-posts",
137 "Read paginated post records",
138 &[QUERY_COMMAND],
139 &[QUERY_LIMIT_FLAG],
140 );
141 const AGENT_SURFACE: AgentSurfaceSpec = AgentSurfaceSpec::new(&[QUERY_CAPABILITY]);
142
143 fn agent_spec() -> ToolSpec {
144 ToolSpec::new(
145 "test-cli",
146 "Test CLI",
147 "1.2.3",
148 LicenseType::MIT,
149 RepoInfo::new("owner", "repo"),
150 true,
151 false,
152 )
153 .with_agent_surface(&AGENT_SURFACE)
154 }
155
156 #[test]
157 fn test_generate_completions_bash() {
158 let _ = generate_completions::<TestCli>(Shell::Bash);
160 }
161
162 #[test]
163 fn test_generate_completions_zsh() {
164 let _ = generate_completions::<TestCli>(Shell::Zsh);
165 }
166
167 #[test]
168 fn test_generate_completions_fish() {
169 let _ = generate_completions::<TestCli>(Shell::Fish);
170 }
171
172 #[test]
173 fn test_generate_completions_elvish() {
174 let _ = generate_completions::<TestCli>(Shell::Elvish);
175 }
176
177 #[test]
178 fn test_generate_completions_powershell() {
179 let _ = generate_completions::<TestCli>(Shell::PowerShell);
180 }
181
182 #[test]
183 fn test_all_shells_generate_without_panic() {
184 let shells = vec![
185 Shell::Bash,
186 Shell::Zsh,
187 Shell::Fish,
188 Shell::Elvish,
189 Shell::PowerShell,
190 ];
191
192 for shell in shells {
193 let _ = generate_completions::<TestCli>(shell);
194 }
195 }
196
197 #[test]
198 fn render_completion_separates_instructions_from_script() {
199 let output = render_completion::<TestCli>(Shell::Bash);
200
201 assert!(
202 output
203 .instructions
204 .contains("source <(test-cli completions bash)")
205 );
206 assert!(output.script.contains("complete"));
207 }
208
209 #[test]
210 fn agent_surface_redaction_completion_helper_omits_hidden_entries() {
211 let mut command = clap::Command::new("test-cli")
212 .subcommand(
213 clap::Command::new("query")
214 .arg(Arg::new("limit").long("limit"))
215 .arg(Arg::new("secret").long("secret")),
216 )
217 .subcommand(clap::Command::new("admin"));
218
219 apply_agent_surface(
220 &mut command,
221 &agent_spec(),
222 &AgentModeContext { active: true },
223 );
224
225 let output = render_completion_from_command(Shell::Bash, command);
226
227 assert!(output.script.contains("query"));
228 assert!(!output.script.contains("admin"));
229 assert!(!output.script.contains("--secret"));
230 }
231}