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) {
101 generate_completions_from_command(shell, T::command());
102}
103
104pub fn generate_completions_from_command(shell: Shell, command: clap::Command) {
106 let output = render_completion_from_command(shell, command);
107 write_completion(io::stdout(), &output).expect("failed to write completions");
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 true,
153 )
154 .with_agent_surface(&AGENT_SURFACE)
155 }
156
157 #[test]
158 fn test_generate_completions_bash() {
159 generate_completions::<TestCli>(Shell::Bash);
161 }
162
163 #[test]
164 fn test_generate_completions_zsh() {
165 generate_completions::<TestCli>(Shell::Zsh);
166 }
167
168 #[test]
169 fn test_generate_completions_fish() {
170 generate_completions::<TestCli>(Shell::Fish);
171 }
172
173 #[test]
174 fn test_generate_completions_elvish() {
175 generate_completions::<TestCli>(Shell::Elvish);
176 }
177
178 #[test]
179 fn test_generate_completions_powershell() {
180 generate_completions::<TestCli>(Shell::PowerShell);
181 }
182
183 #[test]
184 fn test_all_shells_generate_without_panic() {
185 let shells = vec![
186 Shell::Bash,
187 Shell::Zsh,
188 Shell::Fish,
189 Shell::Elvish,
190 Shell::PowerShell,
191 ];
192
193 for shell in shells {
194 generate_completions::<TestCli>(shell);
195 }
196 }
197
198 #[test]
199 fn render_completion_separates_instructions_from_script() {
200 let output = render_completion::<TestCli>(Shell::Bash);
201
202 assert!(
203 output
204 .instructions
205 .contains("source <(test-cli completions bash)")
206 );
207 assert!(output.script.contains("complete"));
208 }
209
210 #[test]
211 fn agent_surface_redaction_completion_helper_omits_hidden_entries() {
212 let mut command = clap::Command::new("test-cli")
213 .subcommand(
214 clap::Command::new("query")
215 .arg(Arg::new("limit").long("limit"))
216 .arg(Arg::new("secret").long("secret")),
217 )
218 .subcommand(clap::Command::new("admin"));
219
220 apply_agent_surface(
221 &mut command,
222 &agent_spec(),
223 &AgentModeContext { active: true },
224 );
225
226 let output = render_completion_from_command(Shell::Bash, command);
227
228 assert!(output.script.contains("query"));
229 assert!(!output.script.contains("admin"));
230 assert!(!output.script.contains("--secret"));
231 }
232}