From ae93ed255705ab5eef75ecf5dcd075f2cf438f16 Mon Sep 17 00:00:00 2001 From: SUN Yizhe Date: Wed, 28 Oct 2026 11:39:38 +0900 Subject: [PATCH] Add clap shell completions and prepare v0.3.0 --- .github/workflows/ci.yml | 4 + CHANGELOG.md | 6 ++ CONTRIBUTING.md | 2 +- Cargo.lock | 44 ++++++++++- Cargo.toml | 6 +- Makefile | 4 + README.md | 37 ++++++++-- docs/RELEASING.md | 4 +- src/cli.rs | 155 ++++++++++++++++++++++++++------------- tests/cli.rs | 97 ++++++++++++++++++++++++ xtask/src/main.rs | 8 ++ 11 files changed, 303 insertions(+), 64 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52224ec..5043742 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,10 @@ jobs: cmp "$CARGO_TARGET_DIR/release/codex-commit" "$executable" "$executable" --help "$executable" --version + "$executable" --completions zsh > "$RUNNER_TEMP/expected-completion" + cmp "$RUNNER_TEMP/expected-completion" "$staging/usr/local/share/zsh/site-functions/_codex-commit" + test -s "$staging/usr/local/share/bash-completion/completions/codex-commit" + test -s "$staging/usr/local/share/fish/vendor_completions.d/codex-commit.fish" lint: name: Lint diff --git a/CHANGELOG.md b/CHANGELOG.md index cb4ed2f..c294447 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.3.0 + +- Use clap for argument parsing and generate shell completions from the same command definition. +- Add `--completions` for zsh, bash, fish, PowerShell, and Elvish without requiring Git or Codex. +- Install zsh, bash, and fish completions automatically through Homebrew and `make install`; document setup for Cargo and prebuilt downloads. + ## 0.2.2 - Find unpublished GitHub releases through authenticated release listings and bind asset validation and publication to the release ID, avoiding draft-by-tag API lookup failures. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbe03c8..cac9aee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ cargo publish -p codex-commit --dry-run --locked `make test` runs `cargo test --workspace --locked`, including the Rust mock Codex executable, disposable local Git remotes, real terminal signal handling, and release-helper validation. Tests do not need Codex login or network push credentials. CI also checks installation on Linux, Apple Silicon macOS, and Intel macOS. Put manual-test artifacts in one scratch directory and remove them after testing. -The runtime has no third-party Rust dependencies. Test and private `xtask` dependencies are locked in `Cargo.lock`; use Cargo to update and verify them rather than adding a second language or package manager to the tooling. +The CLI uses `clap` and `clap_complete` to keep parsing, help, and shell completions in one command definition. Runtime, test, and private `xtask` dependencies are locked in `Cargo.lock`; use Cargo to update and verify them rather than adding a second language or package manager to the tooling. Create a branch and open a pull request against `main`. The protected branch requires passing CI before merging. Describe the user-visible change and how you verified it; include only sanitized output. Do not commit credentials or private repository content. diff --git a/Cargo.lock b/Cargo.lock index 057484e..049bc62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + [[package]] name = "bitflags" version = "2.13.2" @@ -35,10 +41,46 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_complete" +version = "4.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037e2a1a92236d0aff7e845093f64661d6df4c02c9fcc61a60e9e1d736fa392f" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + [[package]] name = "codex-commit" -version = "0.2.2" +version = "0.3.0" dependencies = [ + "clap", + "clap_complete", "nix", "serde_json", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index dd662ba..d3ef31c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codex-commit" -version = "0.2.2" +version = "0.3.0" edition = "2024" rust-version = "1.85" description = "Commit Git changes with Codex Luna at Max reasoning, with optional push" @@ -17,6 +17,10 @@ include = ["Cargo.toml", "Cargo.lock", "src/**", "tests/**/*.rs", "README.md", " members = ["xtask"] resolver = "3" +[dependencies] +clap = { version = "4.6.7", default-features = false, features = ["std", "help", "usage", "error-context"] } +clap_complete = "4.6.11" + [dev-dependencies] nix = { version = "0.31.3", features = ["fs", "ioctl", "process", "signal", "term"] } serde_json = "1.0.154" diff --git a/Makefile b/Makefile index 421299b..79c0454 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,10 @@ release: install: release install -d "$(DESTDIR)$(PREFIX)/bin" install -m 755 "$(CARGO_TARGET_DIR)/release/codex-commit" "$(DESTDIR)$(PREFIX)/bin/codex-commit" + install -d "$(DESTDIR)$(PREFIX)/share/zsh/site-functions" "$(DESTDIR)$(PREFIX)/share/bash-completion/completions" "$(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d" + "$(CARGO_TARGET_DIR)/release/codex-commit" --completions zsh > "$(DESTDIR)$(PREFIX)/share/zsh/site-functions/_codex-commit" + "$(CARGO_TARGET_DIR)/release/codex-commit" --completions bash > "$(DESTDIR)$(PREFIX)/share/bash-completion/completions/codex-commit" + "$(CARGO_TARGET_DIR)/release/codex-commit" --completions fish > "$(DESTDIR)$(PREFIX)/share/fish/vendor_completions.d/codex-commit.fish" check: cargo fmt --all -- --check diff --git a/README.md b/README.md index 93e6194..80c345f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ One command to inspect and commit your existing Git changes with **GPT-6 Luna at codex-commit ``` -Each run creates a saved Codex CLI session. The Rust executable has no third-party Rust dependencies and supports macOS and Linux. +Each task run creates a saved Codex CLI session. The Rust executable supports macOS and Linux and uses clap for argument parsing and shell completions. ## Install @@ -39,18 +39,18 @@ Cargo compiles the published source for your machine and normally installs into ### Prebuilt macOS downloads -Download the matching archive and `SHA256SUMS` from [v0.2.2](https://github.com/syz51/codex-commit/releases/tag/v0.2.2): +Download the matching archive and `SHA256SUMS` from [v0.3.0](https://github.com/syz51/codex-commit/releases/tag/v0.3.0): | Mac | Archive | | --- | --- | -| Apple Silicon (M-series) | [`codex-commit-v0.2.2-aarch64-apple-darwin.tar.gz`](https://github.com/syz51/codex-commit/releases/download/v0.2.2/codex-commit-v0.2.2-aarch64-apple-darwin.tar.gz) | -| Intel | [`codex-commit-v0.2.2-x86_64-apple-darwin.tar.gz`](https://github.com/syz51/codex-commit/releases/download/v0.2.2/codex-commit-v0.2.2-x86_64-apple-darwin.tar.gz) | +| Apple Silicon (M-series) | [`codex-commit-v0.3.0-aarch64-apple-darwin.tar.gz`](https://github.com/syz51/codex-commit/releases/download/v0.3.0/codex-commit-v0.3.0-aarch64-apple-darwin.tar.gz) | +| Intel | [`codex-commit-v0.3.0-x86_64-apple-darwin.tar.gz`](https://github.com/syz51/codex-commit/releases/download/v0.3.0/codex-commit-v0.3.0-x86_64-apple-darwin.tar.gz) | For example, on Apple Silicon: ```sh -release=https://github.com/syz51/codex-commit/releases/download/v0.2.2 -archive=codex-commit-v0.2.2-aarch64-apple-darwin.tar.gz +release=https://github.com/syz51/codex-commit/releases/download/v0.3.0 +archive=codex-commit-v0.3.0-aarch64-apple-darwin.tar.gz curl -fLO "$release/$archive" curl -fLO "$release/SHA256SUMS" grep " $archive\$" SHA256SUMS | shasum -a 256 -c - @@ -69,7 +69,28 @@ cd codex-commit cargo install --path . --locked ``` -Alternatively, `make install` builds a release executable and installs into `~/.local/bin`. `PREFIX` and `DESTDIR` are supported for custom locations and packaging. No Codex or shell configuration is changed. +Alternatively, `make install` builds a release executable and installs into `~/.local/bin`, with zsh, bash, and fish completions under `~/.local/share`. `PREFIX` and `DESTDIR` are supported for custom locations and packaging. Shell startup files and Codex configuration are not changed. + +### Shell completions + +Homebrew installs zsh, bash, and fish completions automatically. Zsh must have Homebrew's `share/zsh/site-functions` on its `fpath` before `compinit`; see [Homebrew's shell setup](https://docs.brew.sh/Shell-Completion#configuring-completions-in-zsh). + +For Cargo or prebuilt installs, generate zsh completions into a directory on your `fpath`: + +```sh +mkdir -p ~/.local/share/zsh/site-functions +codex-commit --completions zsh > ~/.local/share/zsh/site-functions/_codex-commit +``` + +Add the following to `~/.zshrc` before your existing `compinit` call or shell framework initialization. If neither initializes completions, also add `autoload -Uz compinit && compinit` afterward: + +```zsh +fpath=(~/.local/share/zsh/site-functions $fpath) +``` + +Start a new shell after setup. Regenerate the file after upgrading a Cargo or prebuilt install; `make install` regenerates it automatically. Flags include descriptions, and `-C` completes directories. + +`codex-commit --completions SHELL` also supports `bash`, `fish`, `powershell`, and `elvish`. It prints the script to stdout without requiring Git, a repository, or Codex. Install generated files using your shell's completion setup. ## Use @@ -108,7 +129,7 @@ make check # rustfmt and Clippy make test # Rust workspace unit, integration, terminal, and packaging tests ``` -Development and release tooling use the pinned Rust 1.99.1 toolchain. Runtime code has no third-party Rust dependencies; tests and the private `xtask` release helper use development dependencies recorded in `Cargo.lock`. Run `cargo test --workspace --locked` directly if you do not use Make. +Development and release tooling use the pinned Rust 1.99.1 toolchain. Runtime code uses `clap` and `clap_complete`; all dependencies, including tests and the private `xtask` release helper, are recorded in `Cargo.lock`. Run `cargo test --workspace --locked` directly if you do not use Make. CI tests native Linux, Apple Silicon macOS, and Intel macOS, including installation. Automated Rust tests use a mock Codex executable, real disposable Git repositories, and real terminal signals; they do not call a model or require credentials. Release builds execute and check each native Mac binary before packaging with `cargo xtask`. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 0694326..b1bf755 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -21,7 +21,7 @@ The tag must match the Cargo version and point to a commit reachable from `main` The `Release` workflow checks the exact tagged commit's latest required GitHub Actions check results before any publication. It then: 1. Builds on native Apple Silicon and Intel runners, checks Mach-O architecture, and executes each binary's help and version commands. -2. Uses the Rust `xtask` helper to package each executable, `LICENSE`, and `README.md`, create `SHA256SUMS`, and upload a GitHub release draft. +2. Uses the Rust `xtask` helper to package each executable, `LICENSE`, and `README.md`, create `SHA256SUMS`, and upload a GitHub release draft. Each executable generates its own completion scripts with `--completions SHELL`; no separate completion download is required. 3. Compiles and checks the Cargo package before requesting short-lived crates.io credentials through GitHub OIDC. It publishes the exact tagged source. 4. Downloads the public crate, checks its registry checksum and embedded Git commit, installs it from crates.io, and runs the Rust CLI integration tests against that installed executable, including terminal cancellation. 5. Publishes the GitHub release only after Cargo verification succeeds, then verifies release immutability and all asset attestations. GitHub creates release attestations asynchronously, so verification retries for up to 30 attempts with five-second delays and fails if verification still cannot succeed. Publication is serialized across versions, and an older version cannot replace a newer version as Latest. @@ -42,7 +42,7 @@ The [Homebrew tap](https://github.com/syz51/homebrew-tap) checks for a new publi The tap verifies the immutable upstream release and its checksums, generates the formula with the released Rust helper, and opens an update PR using a short-lived GitHub App installation token. Normal PR events run native Mac CI. Auto-merge waits for both Apple Silicon and Intel installation, system-Git, style, and audit checks. Branch protection remains enforced; [manually dispatched checks do not qualify as required PR checks](https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/troubleshooting-required-status-checks#checks-from-some-workflow-jobs-are-not-evaluated). -After that PR merges, users receive the update through `brew update` and `brew upgrade syz51/tap/codex-commit`. A source push without a new version tag runs CI but does not publish a new package. +After that PR merges, users receive the update through `brew update` and `brew upgrade syz51/tap/codex-commit`. The formula generates and installs zsh, bash, and fish completions from the installed executable and tests the installed files. A source push without a new version tag runs CI but does not publish a new package. ## One-time configuration diff --git a/src/cli.rs b/src/cli.rs index 65b76d5..292da20 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,24 +1,34 @@ use std::ffi::{OsStr, OsString}; use std::io::{self, Write}; -use std::os::unix::ffi::{OsStrExt, OsStringExt}; +use std::os::unix::ffi::OsStringExt; use std::os::unix::process::CommandExt; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -const HELP: &str = - "Usage: codex-commit [-C DIR] [--push] [--dry-run] [--interactive] [--] [INSTRUCTIONS...] - -Inspect and commit changes with Codex, using GPT-6 Luna and Max reasoning. - - -C DIR Run in this Git repository (default: current directory) - --push Also push the current branch after committing - --dry-run Print the task and settings without launching Codex - --interactive Use Codex's interactive terminal with permission prompts - -h, --help Show this help - -V, --version Show version - -Progress and command output stream to the terminal. Press Ctrl-C to interrupt. -The default unattended mode gives Codex full local command access."; +fn cli() -> clap::Command { + use clap::{Arg, ArgAction, ValueHint, value_parser}; + clap::Command::new("codex-commit") + .version(env!("CARGO_PKG_VERSION")) + .about("Inspect and commit changes with Codex, using GPT-6 Luna and Max reasoning.") + .after_help("Options precede optional instructions. Use -- before instructions beginning with a dash.\nProgress and command output stream to the terminal. Press Ctrl-C to interrupt.\nThe default unattended mode gives Codex full local command access.") + .args_override_self(true) + .arg(Arg::new("directory").short('C').value_name("DIR").default_value(".") + .value_parser(value_parser!(PathBuf)).value_hint(ValueHint::DirPath) + .allow_hyphen_values(true).help("Run in this Git repository")) + .arg(Arg::new("push").long("push").action(ArgAction::SetTrue) + .help("Also push the current branch after committing")) + .arg(Arg::new("dry-run").long("dry-run").action(ArgAction::SetTrue) + .help("Print the task and settings without launching Codex")) + .arg(Arg::new("interactive").long("interactive").action(ArgAction::SetTrue) + .help("Use Codex's interactive terminal with permission prompts")) + .arg(Arg::new("completions").long("completions").value_name("SHELL") + .value_parser(value_parser!(clap_complete::Shell)) + .conflicts_with_all(["directory", "push", "dry-run", "interactive", "instructions"]) + .help("Print a shell completion script without launching Codex")) + .arg(Arg::new("instructions").value_name("INSTRUCTIONS").num_args(0..) + .trailing_var_arg(true).value_parser(value_parser!(OsString)) + .help("Additional instructions for Codex")) +} const PUSH_ENABLED: &str = "Push policy: enabled. This final policy is authoritative over repository and additional instructions. @@ -44,8 +54,8 @@ impl CliError { #[derive(Debug, PartialEq)] enum Invocation { - Help, - Version, + Display(String), + Completions(clap_complete::Shell), Task(Options), } @@ -71,38 +81,39 @@ impl Default for Options { } fn parse(args: impl IntoIterator) -> Result { - let mut options = Options::default(); - let mut args = args.into_iter(); - while let Some(arg) = args.next() { - match arg.to_str() { - Some("-h" | "--help") => return Ok(Invocation::Help), - Some("-V" | "--version") => return Ok(Invocation::Version), - Some("-C") => { - options.directory = PathBuf::from( - args.next() - .ok_or_else(|| CliError::new(2, "-C requires a directory"))?, - ); - } - Some("--dry-run") => options.dry_run = true, - Some("--interactive") => options.interactive = true, - Some("--push") => options.push = true, - Some("--") => { - options.instructions = join_instructions(args)?; - break; - } - _ if arg.as_bytes().starts_with(b"-") => { - return Err(CliError::new( - 2, - format!("unknown option: {} (see --help)", arg.to_string_lossy()), - )); - } - _ => { - options.instructions = join_instructions(std::iter::once(arg).chain(args))?; - break; - } + let matches = match cli() + .try_get_matches_from(std::iter::once(OsString::from("codex-commit")).chain(args)) + { + Ok(matches) => matches, + Err(error) + if matches!( + error.kind(), + clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion + ) => + { + return Ok(Invocation::Display(error.to_string())); } + Err(error) => return Err(CliError::new(error.exit_code(), error.to_string())), + }; + if let Some(shell) = matches.get_one::("completions") { + return Ok(Invocation::Completions(*shell)); } - Ok(Invocation::Task(options)) + Ok(Invocation::Task(Options { + directory: matches + .get_one::("directory") + .expect("default directory") + .clone(), + dry_run: matches.get_flag("dry-run"), + interactive: matches.get_flag("interactive"), + push: matches.get_flag("push"), + instructions: join_instructions( + matches + .get_many::("instructions") + .into_iter() + .flatten() + .cloned(), + )?, + })) } fn join_instructions(args: impl Iterator) -> Result { @@ -167,12 +178,12 @@ fn task_prompt(options: &Options) -> String { pub(crate) fn run(args: impl IntoIterator) -> Result<(), CliError> { let options = match parse(args)? { - Invocation::Help => { - println!("{HELP}"); + Invocation::Display(output) => { + print!("{output}"); return Ok(()); } - Invocation::Version => { - println!("codex-commit {}", env!("CARGO_PKG_VERSION")); + Invocation::Completions(shell) => { + clap_complete::generate(shell, &mut cli(), "codex-commit", &mut io::stdout()); return Ok(()); } Invocation::Task(options) => options, @@ -286,6 +297,48 @@ mod tests { assert_eq!(without_final_newline(b"path\n\n".to_vec()), b"path\n"); } + #[test] + fn clap_definition_is_consistent() { + cli().debug_assert(); + } + + #[test] + fn delimiter_preserves_dash_leading_instructions() { + let Invocation::Task(options) = parse(arguments(&["--", "--push", "only README"])) + .unwrap_or_else(|error| panic!("{}", error.message)) + else { + panic!("expected task"); + }; + assert!(!options.push); + assert_eq!(options.instructions, "--push only README"); + } + + #[test] + fn invalid_instruction_bytes_are_rejected_at_boundary() { + let error = parse([OsString::from_vec(vec![0xff])]).expect_err("invalid UTF-8"); + assert_eq!(error.code, 2); + assert_eq!( + error.message, + "additional instructions must be valid UTF-8 text" + ); + } + + #[test] + fn completions_require_a_shell_and_exclude_task_options() { + for args in [ + &["--completions"][..], + &["--completions", "invalid"], + &["--completions", "zsh", "--push"], + &["-C", ".", "--completions", "zsh"], + &["--completions", "zsh", "instructions"], + ] { + assert_eq!( + parse(arguments(args)).expect_err("invalid invocation").code, + 2 + ); + } + } + #[test] fn push_permission_is_the_final_policy() { let mut options = Options { diff --git a/tests/cli.rs b/tests/cli.rs index d3e6378..7d38fe3 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -464,6 +464,103 @@ fn dry_run_needs_no_codex_and_does_not_change_git() { assert_eq!(fixture.git(&["rev-parse", "HEAD"]), head); } +#[test] +fn completions_need_no_git_repository_or_external_tools() { + let fixture = Fixture::new(); + let empty = fixture.empty_bin("empty-completion-bin"); + for shell in ["bash", "zsh", "fish", "powershell", "elvish"] { + let output = fixture + .command(cli()) + .env("PATH", &empty) + .current_dir(&fixture.root) + .args(["--completions", shell]) + .bounded_output() + .unwrap(); + assert_code(&output, 0); + assert!(output.stderr.is_empty()); + let script = text(&output.stdout); + assert!(script.contains("codex-commit")); + assert!( + script.contains("push"), + "missing push completion for {shell}" + ); + if shell == "zsh" { + assert!(script.starts_with("#compdef codex-commit")); + assert!(script.contains(":DIR:_files -/")); + } + } + assert!(!fixture.log.exists()); +} + +#[test] +fn generated_zsh_script_completes_flags_and_only_directories_in_real_zle() { + if Command::new("zsh").arg("--version").output().is_err() { + eprintln!("zsh is not installed; skipping shell integration"); + return; + } + let fixture = Fixture::new(); + let generated = fixture + .command(cli()) + .args(["--completions", "zsh"]) + .bounded_output() + .unwrap(); + assert_code(&generated, 0); + fs::create_dir(fixture.root.join("completion-directory")).unwrap(); + fs::write(fixture.root.join("completion-decoy"), "file").unwrap(); + let config = fixture.root.join("completion.zsh"); + fs::write( + &config, + format!( + r#" + autoload -Uz compinit + compinit -D + {} + bindkey -e + PS1='READY>' + emit_buffer() {{ print -r -- "RESULT:$BUFFER"; }} + zle -N emit_buffer + bindkey '^X' emit_buffer + "#, + text(&generated.stdout) + ), + ) + .unwrap(); + let output = fixture + .command("zsh") + .current_dir(&fixture.root) + .args([ + "-f", + "-c", + r#" + zmodload zsh/zpty + zmodload zsh/zselect + zpty -b shell zsh -f + trap 'zpty -d shell' EXIT + zpty -w shell "source ${(q)1}" + wait_for() { + local captured='' chunk + for attempt in {1..60}; do + if zpty -r shell chunk; then captured+=$chunk; fi + if [[ $captured == *$1* ]]; then return 0; fi + zselect -t 5 + done + print -ru2 -- "Missing $1 in: $captured" + return 1 + } + wait_for 'READY>' || exit 1 + zpty -w -n shell $'codex-commit --pu\t\x18' + wait_for 'RESULT:codex-commit --push ' || exit 1 + zpty -w -n shell $'\x15codex-commit -C completion-d\t\x18' + wait_for 'RESULT:codex-commit -C completion-directory/' || exit 1 + "#, + "zsh", + config.to_str().unwrap(), + ]) + .bounded_output() + .unwrap(); + assert_code(&output, 0); +} + #[test] fn help_and_version_need_no_external_tools() { let fixture = Fixture::new(); diff --git a/xtask/src/main.rs b/xtask/src/main.rs index fb6f77b..bc8a73d 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -350,6 +350,7 @@ fn render_formula(version: &str, contents: &str) -> Result { def install bin.install "codex-commit" + generate_completions_from_executable(bin/"codex-commit", "--completions") end def caveats @@ -363,6 +364,10 @@ fn render_formula(version: &str, contents: &str) -> Result { test do assert_match "codex-commit {version}", shell_output("#{{bin}}/codex-commit --version") assert_match "Usage:", shell_output("#{{bin}}/codex-commit --help") + assert_match "#compdef codex-commit", (zsh_completion/"_codex-commit").read + assert_match "--push", (zsh_completion/"_codex-commit").read + assert_path_exists bash_completion/"codex-commit" + assert_path_exists fish_completion/"codex-commit.fish" with_env(PATH: "/usr/bin:/bin") do system "git", "init", "-b", "main" output = shell_output("#{{bin}}/codex-commit --dry-run") @@ -480,6 +485,9 @@ mod tests { assert!(formula.find("depends_on :macos").unwrap() < formula.find("on_macos do").unwrap()); assert!(formula.contains("with_env(PATH: \"/usr/bin:/bin\")")); assert!(formula.contains("assert_match \"Push: disabled\"")); + assert!(formula.contains( + "generate_completions_from_executable(bin/\"codex-commit\", \"--completions\")" + )); } #[cfg(unix)]