From c05a846dd0d0f364e756066457d6c4092efa8a23 Mon Sep 17 00:00:00 2001 From: Clay Loveless Date: Wed, 1 Apr 2026 22:32:58 -0400 Subject: [PATCH 1/3] chore: update gitignore --- .gitignore | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 4f8dace..caf96c2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,8 +35,7 @@ target/ **/*.rs.bk # Generated man pages and shell completions (keep .gitkeep) -dist/share/man/man1/*.1 -dist/share/completions/* +dist/ !dist/share/**/.gitkeep # Benchmark results (keep .gitkeep) @@ -59,3 +58,5 @@ scratch scratch/ .private-journal/ +PRIVATE_MEMORY.md +.claude/ From 3f7dda411843088ce6e16f947c47101240c2a124 Mon Sep 17 00:00:00 2001 From: Clay Loveless Date: Wed, 1 Apr 2026 22:34:23 -0400 Subject: [PATCH 2/3] chore: update template settings --- .repo.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.repo.yml b/.repo.yml index daa2bb5..7e0fc3e 100644 --- a/.repo.yml +++ b/.repo.yml @@ -7,7 +7,7 @@ categories: copyright_name: Clay Loveless copyright_year: '2026' edition: '2024' -has_agents_md: true +has_agents_md: false has_attestations: true has_benchmarks: false has_claude: true @@ -25,8 +25,7 @@ has_inquire: false has_issue_templates: true has_jsonl_logging: true has_mcp_server: true -has_md: true -has_md_strict: false +has_md: false has_opentelemetry: false has_pr_templates: true has_releases: true From 1b6901b014646cf2669e2466478d3ce0fb9c74c1 Mon Sep 17 00:00:00 2001 From: Clay Loveless Date: Wed, 1 Apr 2026 23:17:52 -0400 Subject: [PATCH 3/3] refactor(config): drop bito-lint config names and simplify discovery Remove bito-lint.* config file discovery. Config search now short-circuits on first match per directory instead of accumulating and merging multiple files. Precedence: .config/bito. > .bito. > bito. Also removes unused clap::Parser import in main.rs and updates docs, examples, and tests to match. --- deny.toml => .config/deny.toml | 0 .config/rustfmt.toml | 2 + .github/actions/setup-cargo-tools/action.yml | 2 +- .github/dependabot.yml | 12 ++ .github/workflows/ci.yml | 5 +- .justfile | 5 +- .repo.yml | 5 +- .rustfmt.toml | 2 - Cargo.lock | 16 +- README.md | 9 +- THIRD-PARTY-NOTICES | 4 +- config/bito.toml.example | 6 +- config/bito.yaml.example | 6 +- crates/bito-core/src/config.rs | 162 +++++++------------ crates/bito/Cargo.toml | 2 +- crates/bito/src/lib.rs | 17 +- crates/bito/src/main.rs | 9 +- crates/bito/tests/cli.rs | 8 +- crates/bito/tests/config_integration.rs | 103 +++++++----- docs/README.md | 9 +- rust-toolchain.toml | 2 +- xtask/Cargo.toml | 2 +- 22 files changed, 198 insertions(+), 190 deletions(-) rename deny.toml => .config/deny.toml (100%) create mode 100644 .config/rustfmt.toml delete mode 100644 .rustfmt.toml diff --git a/deny.toml b/.config/deny.toml similarity index 100% rename from deny.toml rename to .config/deny.toml diff --git a/.config/rustfmt.toml b/.config/rustfmt.toml new file mode 100644 index 0000000..f3e454b --- /dev/null +++ b/.config/rustfmt.toml @@ -0,0 +1,2 @@ +edition = "2024" +style_edition = "2024" diff --git a/.github/actions/setup-cargo-tools/action.yml b/.github/actions/setup-cargo-tools/action.yml index c2d6fdb..cfbe688 100644 --- a/.github/actions/setup-cargo-tools/action.yml +++ b/.github/actions/setup-cargo-tools/action.yml @@ -50,7 +50,7 @@ runs: - name: Install cargo-binstall if: steps.cache-cargo-tools.outputs.cache-hit != 'true' && inputs.binstall-tools != '' - uses: cargo-bins/cargo-binstall@113a77a4ce971c41332f2129c3d995df993cf746 # v1.17.8 + uses: cargo-bins/cargo-binstall@0b24824336e2b3800b0f89d9e08b2c08bfa3dcdd # v1.17.9 - name: Install tools via binstall if: steps.cache-cargo-tools.outputs.cache-hit != 'true' && inputs.binstall-tools != '' diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 43bf67e..d189c5f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -31,3 +31,15 @@ updates: labels: - "dependencies" - "github-actions" + + # npm dependencies + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + # Disable automatic PRs - we use the issues workflow instead + open-pull-requests-limit: 0 + labels: + - "dependencies" + - "npm" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26fcb0f..12efb6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: toolchain: stable - name: rustfmt - run: cargo fmt --check + run: cargo fmt --check -- --config-path .config/rustfmt.toml - name: clippy run: cargo clippy --all-targets --all-features -- -D warnings @@ -81,6 +81,9 @@ jobs: - name: Check dependencies uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2.0.15 + with: + command: check + command-arguments: --config .config/deny.toml msrv: name: msrv diff --git a/.justfile b/.justfile index 1ac5a87..d5c3db4 100644 --- a/.justfile +++ b/.justfile @@ -97,7 +97,7 @@ bootstrap: echo " target/debug/bito --help" fmt: - cargo fmt --all + cargo fmt --all -- --config-path .config/rustfmt.toml clippy: cargo +{{toolchain}} clippy --all-targets --all-features --message-format=short -- -D warnings @@ -108,7 +108,7 @@ fix: # Check dependencies for security advisories and license compliance deny: - cargo deny check + cargo deny check --config .config/deny.toml test: cargo nextest run @@ -164,7 +164,6 @@ mdfix *files='': - # Add a new crate to the workspace add-crate *ARGS: scripts/add-crate {{ARGS}} diff --git a/.repo.yml b/.repo.yml index 7e0fc3e..6c3b66b 100644 --- a/.repo.yml +++ b/.repo.yml @@ -1,5 +1,5 @@ # Changes here will be overwritten by Copier -_commit: v1.0.0 +_commit: v1.2.0 _src_path: gh:claylo/claylo-rs categories: - command-line-utilities @@ -31,7 +31,6 @@ has_pr_templates: true has_releases: true has_roadmap_votes: false has_security_md: true -has_site: false has_yamlfmt: false has_yamllint: false hook_system: none @@ -41,7 +40,7 @@ license: lint_level: strict msrv: 1.89.0 owner: claylo -pinned_dev_toolchain: 1.94.0 +pinned_dev_toolchain: 1.94.1 preset: standard project_description: Quality gate tooling for building-in-the-open artifacts project_name: bito diff --git a/.rustfmt.toml b/.rustfmt.toml deleted file mode 100644 index 1b7b387..0000000 --- a/.rustfmt.toml +++ /dev/null @@ -1,2 +0,0 @@ -# We use rustfmt's default settings to format the source code -# This file intentionally exists so rustfmt stops searching parent directories for config. diff --git a/Cargo.lock b/Cargo.lock index a9cf646..15e0624 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -349,9 +349,9 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clap_mangen" -version = "0.2.31" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ea63a92086df93893164221ad4f24142086d535b3a0957b9b9bea2dc86301" +checksum = "d82842b45bf9f6a3be090dd860095ac30728042c08e0d6261ca7259b5d850f07" dependencies = [ "clap", "roff", @@ -1140,9 +1140,9 @@ checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "rmcp" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6b9d2f0efe2258b23767f1f9e0054cfbcac9c2d6f81a031214143096d7864f" +checksum = "2231b2c085b371c01bc90c0e6c1cab8834711b6394533375bdbf870b0166d419" dependencies = [ "async-trait", "base64", @@ -1162,9 +1162,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9d95d7ed26ad8306352b0d5f05b593222b272790564589790d210aa15caa9e" +checksum = "36ea0e100fadf81be85d7ff70f86cd805c7572601d4ab2946207f36540854b43" dependencies = [ "darling", "proc-macro2", @@ -1194,9 +1194,9 @@ dependencies = [ [[package]] name = "roff" -version = "0.2.2" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" +checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189" [[package]] name = "rustix" diff --git a/README.md b/README.md index 1cf212d..bbeefc2 100644 --- a/README.md +++ b/README.md @@ -182,11 +182,10 @@ This exposes seven tools: `analyze_writing`, `count_tokens`, `check_readability` Drop a config file in your project and it takes effect automatically: -1. `.bito.toml` (or `.yaml`, `.json`) in the current directory or any parent -2. `bito.toml` (without dot prefix) in the current directory or any parent -3. `~/.config/bito/config.toml` (user-wide defaults) - -For backward compatibility, `.bito-lint.*` config file names are also discovered. +1. `.config/bito.toml` (or `.yaml`, `.json`) in the current directory or any parent +2. `.bito.toml` in the current directory or any parent +3. `bito.toml` (without dot prefix) in the current directory or any parent +4. `~/.config/bito/config.toml` (user-wide defaults) Closer files win. All formats (TOML, YAML, JSON) work interchangeably. diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index b22e7d3..e2589ee 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -1,7 +1,7 @@ Third-Party Notices =================== -bito-lint incorporates code from the following projects: +bito incorporates code from the following projects: Rust_Grammar @@ -9,7 +9,7 @@ Rust_Grammar https://github.com/Eeman1113/rust_grammar Portions of the grammar checking, passive voice detection, and dictionary -modules (crates/bito-lint-core/src/grammar/, crates/bito-lint-core/src/dictionaries/) +modules (crates/bito-core/src/grammar/, crates/bito-core/src/dictionaries/) are adapted from Rust_Grammar. MIT License diff --git a/config/bito.toml.example b/config/bito.toml.example index b90af7f..d95a625 100644 --- a/config/bito.toml.example +++ b/config/bito.toml.example @@ -1,13 +1,13 @@ # bito Configuration (TOML format) # # Copy this file to one of the following locations: +# - .config/bito.toml (in your project directory) # - .bito.toml (in your project directory) # - bito.toml (in your project directory, without dot prefix) # - ~/.config/bito/config.toml (user-wide config) # -# For backward compatibility, .bito-lint.* names are also discovered. -# -# When multiple files exist in the same directory, all are merged. +# The first matching file wins; the search stops at the closest +# directory containing a match. # # Supported formats: TOML, YAML, JSON # (use the appropriate extension: .toml, .yaml, .yml, .json) diff --git a/config/bito.yaml.example b/config/bito.yaml.example index 0cdf7f3..f77e588 100644 --- a/config/bito.yaml.example +++ b/config/bito.yaml.example @@ -1,13 +1,13 @@ # bito Configuration (YAML format) # # Copy this file to one of the following locations: +# - .config/bito.yaml (in your project directory) # - .bito.yaml (in your project directory) # - bito.yaml (in your project directory, without dot prefix) # - ~/.config/bito/config.yaml (user-wide config) # -# For backward compatibility, .bito-lint.* names are also discovered. -# -# When multiple files exist in the same directory, all are merged. +# The first matching file wins; the search stops at the closest +# directory containing a match. # # Supported formats: TOML, YAML, JSON # (use the appropriate extension: .toml, .yaml, .yml, .json) diff --git a/crates/bito-core/src/config.rs b/crates/bito-core/src/config.rs index 7f2c511..fc4f0a2 100644 --- a/crates/bito-core/src/config.rs +++ b/crates/bito-core/src/config.rs @@ -13,16 +13,15 @@ //! - JSON (`.json`) //! //! # Config file locations (in order of precedence, highest first): -//! - `bito-lint.` in current directory or any parent -//! - `.bito-lint.` in current directory or any parent -//! - `bito.` in current directory or any parent +//! - `.config/bito.` in current directory or any parent //! - `.bito.` in current directory or any parent +//! - `bito.` in current directory or any parent //! - `~/.config/bito/config.` (user config) //! //! Where `` is one of: `toml`, `yaml`, `yml`, `json` //! -//! When multiple files exist in the same directory, all are merged via figment. -//! Later extensions override earlier: toml < yaml < yml < json. +//! The first matching file wins within a directory; the search stops at the +//! closest directory containing a match. //! //! # Example //! ```no_run @@ -320,9 +319,6 @@ const CONFIG_EXTENSIONS: &[&str] = &["toml", "yaml", "yml", "json"]; /// Application name for XDG directory lookup and config file names. const APP_NAME: &str = "bito"; -/// Application names to search for config files (in precedence order, lowest first). -const APP_NAMES: &[&str] = &["bito", "bito-lint"]; - /// Builder for loading configuration from multiple sources. #[derive(Debug, Default)] pub struct ConfigLoader { @@ -458,39 +454,31 @@ impl ConfigLoader { /// Find project config files by walking up from the given directory. /// - /// Returns all matching config files from the closest directory that has any - /// match, ordered low-to-high precedence: `bito` names before `bito-lint` - /// names, dotfiles before regular files within each app name. + /// Returns the highest-precedence config file from the closest directory + /// that has a match. Precedence: `.config/bito.` > `.bito.` > `bito.`. fn find_project_configs(&self, start: &Utf8Path) -> Vec { let mut current = Some(start.to_path_buf()); while let Some(dir) = current { - let mut found = Vec::new(); - - // Search order (low→high precedence, figment merges last-wins): - // 1. .bito.{toml,yaml,yml,json} - // 2. bito.{toml,yaml,yml,json} - // 3. .bito-lint.{toml,yaml,yml,json} - // 4. bito-lint.{toml,yaml,yml,json} - for app_name in APP_NAMES { - // Dotfiles first (lower precedence within same app name) - for ext in CONFIG_EXTENSIONS { - let dotfile = dir.join(format!(".{app_name}.{ext}")); - if dotfile.is_file() { - found.push(dotfile); - } + // Check for config files in this directory (try each extension) + for ext in CONFIG_EXTENSIONS { + // Try .config/ directory first (.config/bito.toml) + let dotconfig = dir.join(format!(".config/{APP_NAME}.{ext}")); + if dotconfig.is_file() { + return vec![dotconfig]; } - // Regular files (higher precedence within same app name) - for ext in CONFIG_EXTENSIONS { - let regular = dir.join(format!("{app_name}.{ext}")); - if regular.is_file() { - found.push(regular); - } + + // Then dotfile (.bito.toml) + let dotfile = dir.join(format!(".{APP_NAME}.{ext}")); + if dotfile.is_file() { + return vec![dotfile]; } - } - if !found.is_empty() { - return found; + // Then regular name (bito.toml) + let regular = dir.join(format!("{APP_NAME}.{ext}")); + if regular.is_file() { + return vec![regular]; + } } // Check for boundary marker AFTER checking config files, @@ -615,7 +603,7 @@ mod tests { fs::write( &config_path, r#"log_level = "debug" -log_dir = "/tmp/bito-lint" +log_dir = "/tmp/bito" "#, ) .unwrap(); @@ -632,7 +620,7 @@ log_dir = "/tmp/bito-lint" assert_eq!(config.log_level, LogLevel::Debug); assert_eq!( config.log_dir.as_ref().map(|dir| dir.as_str()), - Some("/tmp/bito-lint") + Some("/tmp/bito") ); } @@ -669,7 +657,7 @@ log_dir = "/tmp/bito-lint" fs::create_dir_all(&sub_dir).unwrap(); // Create config in project root - let config_path = project_dir.join(".bito-lint.toml"); + let config_path = project_dir.join(".bito.toml"); fs::write(&config_path, r#"log_level = "debug""#).unwrap(); // Convert to Utf8PathBuf for API call @@ -687,6 +675,36 @@ log_dir = "/tmp/bito-lint" assert!(!sources.project_files.is_empty()); } + #[test] + fn test_dotconfig_directory_takes_precedence() { + let tmp = TempDir::new().unwrap(); + let project_dir = tmp.path().join("project"); + let dotconfig_dir = project_dir.join(".config"); + fs::create_dir_all(&dotconfig_dir).unwrap(); + + // Create both .config/project.toml and .project.toml + fs::write(dotconfig_dir.join("bito.toml"), r#"log_level = "debug""#).unwrap(); + fs::write(project_dir.join(".bito.toml"), r#"log_level = "warn""#).unwrap(); + + let project_dir = Utf8PathBuf::try_from(project_dir).unwrap(); + + let (config, sources) = ConfigLoader::new() + .with_user_config(false) + .without_boundary_marker() + .with_project_search(&project_dir) + .load() + .unwrap(); + + // .config/ should win over dotfile + assert_eq!(config.log_level, LogLevel::Debug); + assert!( + sources + .project_files + .iter() + .any(|p| p.as_str().contains(".config/")) + ); + } + #[test] fn test_boundary_marker_stops_search() { let tmp = TempDir::new().unwrap(); @@ -698,7 +716,7 @@ log_dir = "/tmp/bito-lint" fs::create_dir_all(&work).unwrap(); // Config in parent (should NOT be found due to .git boundary) - fs::write(parent.join(".bito-lint.toml"), r#"log_level = "warn""#).unwrap(); + fs::write(parent.join(".bito.toml"), r#"log_level = "warn""#).unwrap(); // .git marker in child fs::create_dir(child.join(".git")).unwrap(); @@ -724,7 +742,7 @@ log_dir = "/tmp/bito-lint" let tmp = TempDir::new().unwrap(); // Project config - let project_config = tmp.path().join(".bito-lint.toml"); + let project_config = tmp.path().join(".bito.toml"); fs::write(&project_config, r#"log_level = "warn""#).unwrap(); // Explicit override @@ -983,70 +1001,6 @@ rules: assert!(!sources.project_files.is_empty()); } - #[test] - fn bito_lint_overrides_bito_config() { - let tmp = TempDir::new().unwrap(); - // .bito.toml sets debug - fs::write(tmp.path().join(".bito.toml"), r#"log_level = "debug""#).unwrap(); - // .bito-lint.toml sets warn — should win - fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "warn""#).unwrap(); - - let tmp_path = Utf8PathBuf::try_from(tmp.path().to_path_buf()).unwrap(); - - let (config, sources) = ConfigLoader::new() - .with_user_config(false) - .without_boundary_marker() - .with_project_search(&tmp_path) - .load() - .unwrap(); - - assert_eq!(config.log_level, LogLevel::Warn); - assert_eq!(sources.project_files.len(), 2); - } - - #[test] - fn bito_and_bito_lint_merge() { - let tmp = TempDir::new().unwrap(); - // .bito.toml sets dialect - fs::write(tmp.path().join(".bito.toml"), "dialect = \"en-gb\"\n").unwrap(); - // .bito-lint.toml sets log_level — both should be present - fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "warn""#).unwrap(); - - let tmp_path = Utf8PathBuf::try_from(tmp.path().to_path_buf()).unwrap(); - - let (config, _sources) = ConfigLoader::new() - .with_user_config(false) - .without_boundary_marker() - .with_project_search(&tmp_path) - .load() - .unwrap(); - - // Both values merged - assert_eq!(config.log_level, LogLevel::Warn); - assert_eq!(config.dialect, Some(Dialect::EnGb)); - } - - #[test] - fn dotfile_before_regular_same_app_name() { - let tmp = TempDir::new().unwrap(); - // .bito-lint.toml sets debug (lower precedence — dotfile) - fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "debug""#).unwrap(); - // bito-lint.toml sets error (higher precedence — regular) - fs::write(tmp.path().join("bito-lint.toml"), r#"log_level = "error""#).unwrap(); - - let tmp_path = Utf8PathBuf::try_from(tmp.path().to_path_buf()).unwrap(); - - let (config, sources) = ConfigLoader::new() - .with_user_config(false) - .without_boundary_marker() - .with_project_search(&tmp_path) - .load() - .unwrap(); - - assert_eq!(config.log_level, LogLevel::Error); - assert_eq!(sources.project_files.len(), 2); - } - #[test] fn only_closest_directory_contributes() { let tmp = TempDir::new().unwrap(); @@ -1057,7 +1011,7 @@ rules: // Config in parent fs::write(parent.join(".bito.toml"), r#"log_level = "warn""#).unwrap(); // Config in child (closer) — only this dir should contribute - fs::write(child.join(".bito-lint.toml"), r#"log_level = "error""#).unwrap(); + fs::write(child.join(".bito.toml"), r#"log_level = "error""#).unwrap(); let child_path = Utf8PathBuf::try_from(child).unwrap(); diff --git a/crates/bito/Cargo.toml b/crates/bito/Cargo.toml index e5d892b..6ce2ccb 100644 --- a/crates/bito/Cargo.toml +++ b/crates/bito/Cargo.toml @@ -48,7 +48,7 @@ clap = { version = "4.6", features = ["derive"] } anyhow = "1.0" tracing = "0.1" tokio = { version = "1.50", features = ["rt-multi-thread", "macros"], optional = true } -rmcp = { version = "1.2", features = ["server", "transport-io", "macros"], optional = true } +rmcp = { version = "1.3", features = ["server", "transport-io", "macros"], optional = true } schemars = { version = "1.2", optional = true } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/crates/bito/src/lib.rs b/crates/bito/src/lib.rs index e5687ba..dc4bd8f 100644 --- a/crates/bito/src/lib.rs +++ b/crates/bito/src/lib.rs @@ -61,6 +61,7 @@ ENVIRONMENT VARIABLES: #[command(about = "Quality gate tooling for building-in-the-open artifacts", long_about = None)] #[command(version, arg_required_else_help = true)] #[command(after_help = ENV_HELP)] +#[command(disable_help_flag = true)] pub struct Cli { /// The subcommand to execute. #[command(subcommand)] @@ -128,7 +129,19 @@ pub enum Commands { Serve(commands::serve::ServeArgs), } -/// Returns the clap command for documentation generation +/// Returns the clap command for documentation generation. +/// +/// Adds a custom `-h`/`--help` flag using `HelpShort` so both render +/// the compact single-line format. This is done at the Command level +/// (not as a struct field) because clap's derive treats `HelpShort` +/// as a value-less exit action that conflicts with struct population. pub fn command() -> clap::Command { - Cli::command() + Cli::command().arg( + clap::Arg::new("help") + .short('h') + .long("help") + .help("Print help") + .global(true) + .action(clap::ArgAction::HelpShort), + ) } diff --git a/crates/bito/src/main.rs b/crates/bito/src/main.rs index 2681a99..0af08cf 100644 --- a/crates/bito/src/main.rs +++ b/crates/bito/src/main.rs @@ -4,13 +4,16 @@ use anyhow::Context; use bito::{Cli, Commands, commands}; use bito_core::config::ConfigLoader; -use clap::Parser; +use clap::FromArgMatches; use tracing::debug; use bito_core::observability; -fn main() -> anyhow::Result<()> { - let cli = Cli::parse(); +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::from_arg_matches(&bito::command().get_matches()) + .expect("clap mismatch between Cli derive and command()"); + cli.color.apply(); if cli.version_only { diff --git a/crates/bito/tests/cli.rs b/crates/bito/tests/cli.rs index 6003d4c..5dcf337 100644 --- a/crates/bito/tests/cli.rs +++ b/crates/bito/tests/cli.rs @@ -337,7 +337,7 @@ fn lint_with_config_rules_runs_checks() { let dir = tempfile::tempdir().unwrap(); // Create a config file with rules - let config_path = dir.path().join(".bito-lint.yaml"); + let config_path = dir.path().join(".bito.yaml"); std::fs::write( &config_path, r#" @@ -374,7 +374,7 @@ rules: fn lint_no_match_skips_cleanly() { let dir = tempfile::tempdir().unwrap(); - let config_path = dir.path().join(".bito-lint.yaml"); + let config_path = dir.path().join(".bito.yaml"); std::fs::write( &config_path, "rules:\n - paths: [\"docs/**/*.md\"]\n checks:\n readability:\n max_grade: 20\n", @@ -400,7 +400,7 @@ fn lint_no_match_skips_cleanly() { fn lint_json_output_has_pass_field() { let dir = tempfile::tempdir().unwrap(); - let config_path = dir.path().join(".bito-lint.yaml"); + let config_path = dir.path().join(".bito.yaml"); std::fs::write( &config_path, r#" @@ -438,7 +438,7 @@ rules: fn lint_with_tokens_budget() { let dir = tempfile::tempdir().unwrap(); - let config_path = dir.path().join(".bito-lint.yaml"); + let config_path = dir.path().join(".bito.yaml"); std::fs::write( &config_path, r#" diff --git a/crates/bito/tests/config_integration.rs b/crates/bito/tests/config_integration.rs index 7750a70..6270a3e 100644 --- a/crates/bito/tests/config_integration.rs +++ b/crates/bito/tests/config_integration.rs @@ -52,7 +52,7 @@ fn runs_without_config_file() { #[test] fn discovers_dotfile_config_in_current_dir() { let tmp = TempDir::new().unwrap(); - let config_path = tmp.path().join(".bito-lint.toml"); + let config_path = tmp.path().join(".bito.toml"); fs::write(&config_path, r#"log_level = "debug""#).unwrap(); let json = info_json(tmp.path()); @@ -60,7 +60,7 @@ fn discovers_dotfile_config_in_current_dir() { assert_eq!(json["config"]["log_level"], "debug"); let reported = json["config"]["config_file"].as_str().unwrap(); assert!( - reported.ends_with(".bito-lint.toml"), + reported.ends_with(".bito.toml"), "should report dotfile: {reported}" ); } @@ -68,7 +68,7 @@ fn discovers_dotfile_config_in_current_dir() { #[test] fn discovers_regular_config_in_current_dir() { let tmp = TempDir::new().unwrap(); - let config_path = tmp.path().join("bito-lint.toml"); + let config_path = tmp.path().join("bito.toml"); fs::write(&config_path, r#"log_level = "warn""#).unwrap(); let json = info_json(tmp.path()); @@ -76,7 +76,7 @@ fn discovers_regular_config_in_current_dir() { assert_eq!(json["config"]["log_level"], "warn"); let reported = json["config"]["config_file"].as_str().unwrap(); assert!( - reported.ends_with("bito-lint.toml"), + reported.ends_with("bito.toml"), "should report regular config: {reported}" ); } @@ -88,7 +88,7 @@ fn discovers_config_in_parent_directory() { fs::create_dir_all(&sub_dir).unwrap(); // Config in root, run from nested/deep - fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "debug""#).unwrap(); + fs::write(tmp.path().join(".bito.toml"), r#"log_level = "debug""#).unwrap(); let json = info_json(&sub_dir); @@ -100,18 +100,53 @@ fn discovers_config_in_parent_directory() { } #[test] -fn regular_name_overrides_dotfile() { +fn discovers_dotconfig_directory_config() { let tmp = TempDir::new().unwrap(); + let dotconfig = tmp.path().join(".config"); + fs::create_dir_all(&dotconfig).unwrap(); + fs::write(dotconfig.join("bito.toml"), r#"log_level = "debug""#).unwrap(); - // Both configs exist — regular file (higher precedence) should win - fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "debug""#).unwrap(); - fs::write(tmp.path().join("bito-lint.toml"), r#"log_level = "error""#).unwrap(); + let json = info_json(tmp.path()); + + assert_eq!(json["config"]["log_level"], "debug"); + let reported = json["config"]["config_file"].as_str().unwrap(); + assert!( + reported.contains(".config/"), + "should report .config/ path: {reported}" + ); +} + +#[test] +fn dotconfig_takes_precedence_over_dotfile() { + let tmp = TempDir::new().unwrap(); + let dotconfig = tmp.path().join(".config"); + fs::create_dir_all(&dotconfig).unwrap(); + + // .config/ gets debug, dotfile gets error — .config/ should win + fs::write(dotconfig.join("bito.toml"), r#"log_level = "debug""#).unwrap(); + fs::write(tmp.path().join(".bito.toml"), r#"log_level = "error""#).unwrap(); let json = info_json(tmp.path()); assert_eq!( - json["config"]["log_level"], "error", - "regular file should override dotfile" + json["config"]["log_level"], "debug", + ".config/ should win over dotfile" + ); +} + +#[test] +fn dotfile_takes_precedence_over_regular_name() { + let tmp = TempDir::new().unwrap(); + + // Both configs exist — dotfile is checked first and wins + fs::write(tmp.path().join(".bito.toml"), r#"log_level = "debug""#).unwrap(); + fs::write(tmp.path().join("bito.toml"), r#"log_level = "error""#).unwrap(); + + let json = info_json(tmp.path()); + + assert_eq!( + json["config"]["log_level"], "debug", + "dotfile should win over regular name" ); } @@ -122,7 +157,7 @@ fn regular_name_overrides_dotfile() { #[test] fn parses_toml_config() { let tmp = TempDir::new().unwrap(); - fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "warn""#).unwrap(); + fs::write(tmp.path().join(".bito.toml"), r#"log_level = "warn""#).unwrap(); let json = info_json(tmp.path()); assert_eq!(json["config"]["log_level"], "warn"); @@ -131,7 +166,7 @@ fn parses_toml_config() { #[test] fn parses_yaml_config() { let tmp = TempDir::new().unwrap(); - fs::write(tmp.path().join(".bito-lint.yaml"), "log_level: warn\n").unwrap(); + fs::write(tmp.path().join(".bito.yaml"), "log_level: warn\n").unwrap(); let json = info_json(tmp.path()); assert_eq!(json["config"]["log_level"], "warn"); @@ -140,7 +175,7 @@ fn parses_yaml_config() { #[test] fn parses_yml_config() { let tmp = TempDir::new().unwrap(); - fs::write(tmp.path().join(".bito-lint.yml"), "log_level: debug\n").unwrap(); + fs::write(tmp.path().join(".bito.yml"), "log_level: debug\n").unwrap(); let json = info_json(tmp.path()); assert_eq!(json["config"]["log_level"], "debug"); @@ -149,11 +184,7 @@ fn parses_yml_config() { #[test] fn parses_json_config() { let tmp = TempDir::new().unwrap(); - fs::write( - tmp.path().join(".bito-lint.json"), - r#"{"log_level": "error"}"#, - ) - .unwrap(); + fs::write(tmp.path().join(".bito.json"), r#"{"log_level": "error"}"#).unwrap(); let json = info_json(tmp.path()); assert_eq!(json["config"]["log_level"], "error"); @@ -170,8 +201,8 @@ fn closer_config_takes_precedence() { fs::create_dir_all(&sub_dir).unwrap(); // Parent config (error) vs child config (debug) — child should win - fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "error""#).unwrap(); - fs::write(sub_dir.join(".bito-lint.toml"), r#"log_level = "debug""#).unwrap(); + fs::write(tmp.path().join(".bito.toml"), r#"log_level = "error""#).unwrap(); + fs::write(sub_dir.join(".bito.toml"), r#"log_level = "debug""#).unwrap(); let json = info_json(&sub_dir); @@ -182,17 +213,17 @@ fn closer_config_takes_precedence() { } #[test] -fn later_extension_overrides_earlier_in_same_directory() { +fn first_extension_wins_in_same_directory() { let tmp = TempDir::new().unwrap(); - // Both dotfiles exist — YAML comes after TOML in merge order, so it wins - fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "debug""#).unwrap(); - fs::write(tmp.path().join(".bito-lint.yaml"), "log_level: error\n").unwrap(); + // Both dotfiles exist — TOML is checked first and wins + fs::write(tmp.path().join(".bito.toml"), r#"log_level = "debug""#).unwrap(); + fs::write(tmp.path().join(".bito.yaml"), "log_level: error\n").unwrap(); let json = info_json(tmp.path()); assert_eq!( - json["config"]["log_level"], "error", - "later extension (YAML) should override earlier (TOML) in merge" + json["config"]["log_level"], "debug", + "first extension (TOML) should win" ); } @@ -201,7 +232,7 @@ fn explicit_config_overrides_discovered() { let tmp = TempDir::new().unwrap(); // Project config sets debug - fs::write(tmp.path().join(".bito-lint.toml"), r#"log_level = "debug""#).unwrap(); + fs::write(tmp.path().join(".bito.toml"), r#"log_level = "debug""#).unwrap(); // Explicit config sets error let explicit = tmp.path().join("override.toml"); @@ -239,11 +270,7 @@ fn explicit_config_overrides_discovered() { #[test] fn invalid_toml_config_shows_error() { let tmp = TempDir::new().unwrap(); - fs::write( - tmp.path().join(".bito-lint.toml"), - "this is not valid toml [[[", - ) - .unwrap(); + fs::write(tmp.path().join(".bito.toml"), "this is not valid toml [[[").unwrap(); cmd() .args(["-C", tmp.path().to_str().unwrap(), "info"]) @@ -256,7 +283,7 @@ fn invalid_toml_config_shows_error() { fn invalid_yaml_config_shows_error() { let tmp = TempDir::new().unwrap(); fs::write( - tmp.path().join(".bito-lint.yaml"), + tmp.path().join(".bito.yaml"), "invalid:\n yaml\n content:\n[broken", ) .unwrap(); @@ -270,7 +297,7 @@ fn invalid_yaml_config_shows_error() { #[test] fn invalid_json_config_shows_error() { let tmp = TempDir::new().unwrap(); - fs::write(tmp.path().join(".bito-lint.json"), "{not valid json}").unwrap(); + fs::write(tmp.path().join(".bito.json"), "{not valid json}").unwrap(); cmd() .args(["-C", tmp.path().to_str().unwrap(), "info"]) @@ -283,7 +310,7 @@ fn unknown_config_field_is_ignored() { // Figment ignores unknown fields by default with serde let tmp = TempDir::new().unwrap(); fs::write( - tmp.path().join(".bito-lint.toml"), + tmp.path().join(".bito.toml"), "log_level = \"info\"\nunknown_field = \"should be ignored\"\nanother_unknown = 42\n", ) .unwrap(); @@ -307,7 +334,7 @@ fn git_boundary_stops_config_search() { fs::create_dir_all(&src).unwrap(); // Config in parent (outside repo) - fs::write(parent.join(".bito-lint.toml"), r#"log_level = "error""#).unwrap(); + fs::write(parent.join(".bito.toml"), r#"log_level = "error""#).unwrap(); // .git directory marks repo boundary fs::create_dir(repo.join(".git")).unwrap(); @@ -334,7 +361,7 @@ fn config_in_same_dir_as_git_is_found() { // .git and config in same directory fs::create_dir(repo.join(".git")).unwrap(); - fs::write(repo.join(".bito-lint.toml"), r#"log_level = "debug""#).unwrap(); + fs::write(repo.join(".bito.toml"), r#"log_level = "debug""#).unwrap(); // Running from src/ should find the repo config let json = info_json(&src); diff --git a/docs/README.md b/docs/README.md index bba2681..ab6b36f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -231,13 +231,12 @@ bito searches for config files in this order: 1. **Explicit** -- `--config ` flag 2. **Project** -- walk up from current directory, stopping at `.git`: - - `.bito.toml`, `.bito.yaml`, `.bito.yml`, `.bito.json` - - `bito.toml`, `bito.yaml`, `bito.yml`, `bito.json` - - `.bito-lint.toml`, `.bito-lint.yaml`, `.bito-lint.yml`, `.bito-lint.json` (backward compat) - - `bito-lint.toml`, `bito-lint.yaml`, `bito-lint.yml`, `bito-lint.json` (backward compat) + - `.config/bito.{toml,yaml,yml,json}` + - `.bito.{toml,yaml,yml,json}` + - `bito.{toml,yaml,yml,json}` 3. **User** -- `~/.config/bito/config.{toml,yaml,yml,json}` -When multiple project-level config files exist in the same directory, all are merged. Later files in the precedence list override earlier ones, so `bito.yaml` overrides `.bito.yaml` for any shared keys. +The first matching file wins within a directory; the search stops at the closest directory containing a match. Precedence (highest to lowest): CLI flags > environment variables > explicit config > project config > user config > defaults. diff --git a/rust-toolchain.toml b/rust-toolchain.toml index ac97e2d..59b2874 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] # version we build with -channel = "1.94.0" +channel = "1.94.1" profile = "minimal" components = ["clippy", "rustfmt"] diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 9ed4ddb..c212e3a 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -10,5 +10,5 @@ homepage.workspace = true [dependencies] clap = { version = "4.6", features = ["derive"] } clap_complete = "4.6" -clap_mangen = "0.2" +clap_mangen = "0.3" bito = { path = "../crates/bito", features = ["mcp"] }