-
Notifications
You must be signed in to change notification settings - Fork 268
feat(cli): add shell completion scaffolding #89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
LSX-s-Software
merged 23 commits into
kvcache-ai:main
from
rbalachandar:cli/shell-completion
Aug 6, 2026
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
b4d6b81
feat(cli): add shell completion scaffolding
rbalachandar 25562c4
fix(cli): harden completion output and tests
rbalachandar 6a17694
Update crates/aenv/src/commands/completion.rs
rbalachandar 81cb641
test(cli): validate completion features via Cli metadata
rbalachandar be16e95
test(cli): cover completion write branches and add error context
rbalachandar 55124bd
docs(cli): document shell completion
rbalachandar f8751d8
docs(cli): correct shell-completion install paths
rbalachandar d13bc78
feat(cli): install shell-completion loaders via the installers
rbalachandar 205223d
Update .github/workflows/ci.yml
rbalachandar 08be532
Update .github/workflows/ci.yml
rbalachandar eb4bbaf
fix(cli): harden shell-completion installer per review
rbalachandar 6145167
Update scripts/tests/verify-shell-completion.sh
rbalachandar 08622c1
Update scripts/tests/verify-shell-completion.sh
rbalachandar 72d8e69
fix(cli): address second round of review on completion installers
rbalachandar 8f196ab
refactor(cli): unify completion writes through one atomic-commit prim…
rbalachandar 5768eed
Update crates/aenv/src/commands/completion.rs
rbalachandar 2fa21b3
Update scripts/tests/verify-shell-completion.sh
rbalachandar a9078f3
Update scripts/tests/verify-shell-completion.sh
rbalachandar 8eb8c91
fix(cli): propagate ownership-marker/upgrade rewrite + fix #compdef +…
rbalachandar fc358d3
fix(cli): round 5 — compile fix, portable resolve/chown, install-side…
rbalachandar 5a03bb8
refactor(cli): split automatic completion installation
rbalachandar af6c267
test(cli): cover completion flush errors
rbalachandar a13c510
docs(cli): clarify manual completion activation
rbalachandar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,229 @@ | ||
| use anyhow::Context; | ||
| use anyhow::Result; | ||
| use clap::Args as ClapArgs; | ||
| use clap::CommandFactory; | ||
| use clap::ValueEnum; | ||
| use clap_complete::Shell as ClapShell; | ||
| use std::io::Write; | ||
|
|
||
| /// Shell to generate completion for. | ||
| /// | ||
| #[derive(Copy, Clone, Debug, ValueEnum)] | ||
| pub enum Shell { | ||
| Bash, | ||
| Zsh, | ||
| Fish, | ||
| } | ||
|
|
||
| impl From<Shell> for ClapShell { | ||
| fn from(shell: Shell) -> Self { | ||
| match shell { | ||
| Shell::Bash => ClapShell::Bash, | ||
| Shell::Zsh => ClapShell::Zsh, | ||
| Shell::Fish => ClapShell::Fish, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(ClapArgs)] | ||
| pub struct Args { | ||
| /// Shell to generate completion for. | ||
| #[arg(value_enum)] | ||
| pub shell: Shell, | ||
| } | ||
|
|
||
| /// Generate a completion script for the requested shell and write it to stdout. | ||
| pub fn run(args: Args) -> Result<()> { | ||
| write_completion(args.shell, &mut std::io::stdout().lock()) | ||
| } | ||
|
|
||
| /// Generate the completion script for `shell` and write it to `out`. | ||
| /// | ||
| /// Generation goes through an in-memory buffer first: clap_complete's | ||
| /// generators panic on write errors (`Generator::generate` calls `.expect`), | ||
| /// so writing straight to `out` would turn a closed downstream pipe into a | ||
| /// panic. The buffer cannot fail, so generation is infallible; only the | ||
| /// explicit write below can. A `BrokenPipe` there (e.g. `aenv completion bash | ||
| /// | head`) is normal and treated as success; any other error propagates with | ||
| /// context. Split out from `run` so the write branches are unit-testable. | ||
| fn write_completion<W: Write>(shell: Shell, out: &mut W) -> Result<()> { | ||
| let mut cmd = crate::Cli::command(); | ||
|
rbalachandar marked this conversation as resolved.
|
||
| let mut script = Vec::new(); | ||
| clap_complete::generate(ClapShell::from(shell), &mut cmd, "aenv", &mut script); | ||
| match out.write_all(&script).and_then(|_| out.flush()) { | ||
| Ok(()) => Ok(()), | ||
| Err(err) if err.kind() == std::io::ErrorKind::BrokenPipe => Ok(()), | ||
| Err(err) => Err(err).context("writing completion script to stdout"), | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| fn generate_for(shell: Shell) -> String { | ||
| let mut buf = Vec::new(); | ||
| write_completion(shell, &mut buf).expect("writing to a Vec cannot fail"); | ||
| String::from_utf8(buf).expect("completion output is valid UTF-8") | ||
| } | ||
|
|
||
| /// Writer that always fails with a configured error kind, for exercising | ||
| /// `write_completion`'s error branches. | ||
| struct FailingWriter { | ||
| kind: std::io::ErrorKind, | ||
| fail_on_flush: bool, | ||
|
rbalachandar marked this conversation as resolved.
|
||
| } | ||
|
|
||
| impl std::io::Write for FailingWriter { | ||
| fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { | ||
| if self.fail_on_flush { | ||
| Ok(buf.len()) | ||
| } else { | ||
| Err(std::io::Error::from(self.kind)) | ||
| } | ||
| } | ||
|
|
||
| fn flush(&mut self) -> std::io::Result<()> { | ||
| Err(std::io::Error::from(self.kind)) | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn bash_has_compdef_or_complete_f() { | ||
| let s = generate_for(Shell::Bash); | ||
| assert!( | ||
| s.contains("complete -F") || s.contains("compdef"), | ||
| "bash output should register the binary; got:\n{s}" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn zsh_has_compdef_header() { | ||
| let s = generate_for(Shell::Zsh); | ||
| assert!( | ||
| s.starts_with("#compdef"), | ||
| "zsh output should start with a #compdef header; got:\n{s}" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn fish_has_complete_calls() { | ||
| let s = generate_for(Shell::Fish); | ||
| assert!( | ||
| s.contains("complete "), | ||
| "fish output should contain `complete` invocations; got:\n{s}" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn connect_exposes_cn_alias() { | ||
| // Assert on the Command tree, not on clap_complete's generated bash | ||
| // dispatch format: the internal `aenv,cn)` / `__subcmd__` naming is an | ||
| // implementation detail a compatible clap_complete upgrade could rename | ||
| // even though completion still works. If `connect` declares `cn` as a | ||
| // visible alias, clap_complete emits it — that contract is ours. | ||
| let cmd = crate::Cli::command(); | ||
| let connect = cmd | ||
| .find_subcommand("connect") | ||
| .expect("`connect` command exists"); | ||
| assert!( | ||
| connect.get_visible_aliases().any(|a| a == "cn"), | ||
| "`connect` should declare `cn` as a visible alias" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn snapshot_exposes_create_subcommand() { | ||
| // See `connect_exposes_cn_alias`: assert on the Command tree, not on | ||
| // clap_complete's internal bash helper naming. | ||
| let cmd = crate::Cli::command(); | ||
| let snapshot = cmd | ||
| .find_subcommand("snapshot") | ||
| .expect("`snapshot` command exists"); | ||
| assert!( | ||
| snapshot.find_subcommand("create").is_some(), | ||
| "`snapshot` should expose a `create` subcommand" | ||
| ); | ||
|
rbalachandar marked this conversation as resolved.
rbalachandar marked this conversation as resolved.
rbalachandar marked this conversation as resolved.
|
||
| } | ||
|
|
||
| #[test] | ||
| fn output_arg_offers_table_and_json() { | ||
| // Assert on the argument metadata: the exact `compgen -W "table json"` | ||
| // string is clap_complete's bash formatting, which a compatible upgrade | ||
| // could change. The possible values are defined on the `--output` arg. | ||
| let cmd = crate::Cli::command(); | ||
| let list = cmd.find_subcommand("list").expect("`list` command exists"); | ||
| let output = list | ||
| .get_arguments() | ||
| .find(|a| a.get_long() == Some("output")) | ||
| .expect("`list` should declare an --output argument"); | ||
| let possible = output.get_possible_values(); | ||
| let names: Vec<&str> = possible.iter().map(|v| v.get_name()).collect(); | ||
| assert!( | ||
| names.contains(&"table") && names.contains(&"json"), | ||
| "--output should offer table and json; got {names:?}" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn write_completion_succeeds_into_buffer() { | ||
| let mut buf: Vec<u8> = Vec::new(); | ||
| write_completion(Shell::Bash, &mut buf).expect("Vec write succeeds"); | ||
| assert!( | ||
| !buf.is_empty(), | ||
| "a completion script should have been written" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn broken_pipe_is_treated_as_success() { | ||
| // `aenv completion bash | head` closes the pipe early; that must exit | ||
| // cleanly rather than error or panic. | ||
| let mut out = FailingWriter { | ||
| kind: std::io::ErrorKind::BrokenPipe, | ||
| fail_on_flush: false, | ||
| }; | ||
| write_completion(Shell::Bash, &mut out) | ||
| .expect("BrokenPipe during completion output should not error"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn broken_pipe_on_flush_is_treated_as_success() { | ||
| let mut out = FailingWriter { | ||
| kind: std::io::ErrorKind::BrokenPipe, | ||
| fail_on_flush: true, | ||
| }; | ||
| write_completion(Shell::Bash, &mut out) | ||
| .expect("BrokenPipe during completion flush should not error"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn other_io_error_propagates() { | ||
| let mut out = FailingWriter { | ||
| kind: std::io::ErrorKind::Other, | ||
| fail_on_flush: false, | ||
| }; | ||
| let err = write_completion(Shell::Bash, &mut out) | ||
| .expect_err("non-BrokenPipe I/O errors should propagate"); | ||
| assert!( | ||
| err.to_string() | ||
| .contains("writing completion script to stdout"), | ||
| "error should carry completion context; got: {err}" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn other_io_error_on_flush_propagates() { | ||
| let mut out = FailingWriter { | ||
| kind: std::io::ErrorKind::Other, | ||
| fail_on_flush: true, | ||
| }; | ||
| let err = | ||
| write_completion(Shell::Bash, &mut out).expect_err("flush errors should propagate"); | ||
| assert!( | ||
| err.to_string() | ||
| .contains("writing completion script to stdout"), | ||
| "error should carry completion context; got: {err}" | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| pub mod auth; | ||
| pub mod build; | ||
| pub mod completion; | ||
| pub mod connect; | ||
| pub mod delete; | ||
| pub mod download; | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.