From ee55f805c751f7688d84a8ceba37ebfcc6bfbd9e Mon Sep 17 00:00:00 2001 From: "Joakim L. Engeset" Date: Wed, 23 Sep 2026 12:59:48 +0200 Subject: [PATCH] Move `config check` to a top-level `cid doctor` The report is a health check of everything cid reaches for, not a verb over the configuration file, and `doctor` is the name that job goes by elsewhere. The implementation stays in cmd/config.rs beside the row and status machinery it shares with `config print`. `config check` is removed rather than aliased: a hidden alias would be one more spelling nobody can discover from `--help`. The help, README command table and setup block, starter config, doc comments and the contributor notes now name `cid doctor`. Release: minor --- CHANGELOG.md | 2 ++ CLAUDE.md | 2 +- README.md | 7 ++++--- src/binding.rs | 6 +++--- src/cmd/config.rs | 8 ++++---- src/cmd/file.rs | 2 +- src/cmd/note.rs | 4 ++-- src/gh.rs | 2 +- src/main.rs | 39 ++++++++++++++++++++------------------- tests/cli.rs | 36 ++++++++++++++++++------------------ 10 files changed, 56 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffe7aaf..21aef77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ raised, never by hand. `inbox/migrate-to-aws.md`. Notes go under `[note] inbox`, `inbox` unless set, and the date moves into a `created:` line in the note's front matter, which `note ls` and the selector already read. +- **`cid config check` is now `cid doctor`.** The report is unchanged; the + old spelling is gone, so a setup script that runs it needs the new name. ## v0.23.0 diff --git a/CLAUDE.md b/CLAUDE.md index 65beff7..32b6236 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -156,7 +156,7 @@ Rationale for code that does is a doc comment at the site — `ScratchRow`, Prefer `Preview::Text` from data in hand — hence PR bodies from `gh pr list` rather than `gh pr view`. A `Preview::Command` must be local, bounded, built through `select::quote`, and `--no-optional-locks` if it is git. -- **A new dependency on the outside world gets a `config check` row** in +- **A new dependency on the outside world gets a `doctor` row** in `cmd/config.rs`. `Fail` only when cid is genuinely broken without it, and skip a check that repeats an earlier one. - **A new setting gets a `config print` row**, under the table it is written diff --git a/README.md b/README.md index f99cf20..4f45f4c 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ cargo install --git https://github.com/joakimen/cid cid config init # write ~/.config/cid/config.toml, then set `root` cid init # shell functions, key bindings and completions, to source cid config print # every setting, and what is in force for it -cid config check # what cid reaches for, and what is missing +cid doctor # what cid reaches for, and what is missing ``` ## Commands @@ -55,7 +55,8 @@ cid config check # what cid reaches for, and what is missing | `cid ps` | what is running, and what to kill | | `cid history` | the commands you have already run, back onto the command line | | `cid project` | builds or installs `$PWD`, whatever it turns out to be written in | -| `cid config` | write, read back and check the configuration | +| `cid config` | write and read back the configuration | +| `cid doctor` | what cid reaches for, and what is missing | | `cid stats` | what you run, how often, and how long cid takes over it | | `cid init` | the shell integration below | @@ -83,7 +84,7 @@ b = "project-build" # cid project build What a table holds is the whole of what is bound, so leaving a key out is how it stays free. `cid config actions` lists every action with whatever is bound -to it, `cid config print` lists what you have, and `cid config check` says +to it, `cid config print` lists what you have, and `cid doctor` says whether they all resolve — an action cid does not define stops `cid init` rather than emitting a shell where one key silently does nothing. diff --git a/src/binding.rs b/src/binding.rs index 2ec49a1..c6f9565 100644 --- a/src/binding.rs +++ b/src/binding.rs @@ -249,7 +249,7 @@ pub fn resolve(config: &ShellConfig) -> Result { /// /// Unlike [`resolve`], an action nobody defines is kept rather than refused: /// `cid config print` reports a line the file really has, and leaves calling -/// it broken to `cid config check`. +/// it broken to `cid doctor`. pub fn entries(table: Option<&Bindings>) -> Vec<(&str, &str)> { table .into_iter() @@ -273,7 +273,7 @@ pub struct Assignment<'a> { /// the ones that are still free is what the list is for. /// /// A line naming an action cid does not define belongs to no row here; -/// `cid config check` is where that is reported. +/// `cid doctor` is where that is reported. pub fn assignments(config: &ShellConfig) -> Vec> { let bindings = entries(config.bindings.as_ref()); let aliases = entries(config.aliases.as_ref()); @@ -531,7 +531,7 @@ mod tests { assert!(row("proc-kill").keys.is_empty()); } - /// Reporting a line that names nothing is `config check`'s job; here it + /// Reporting a line that names nothing is `doctor`'s job; here it /// would be a row for an action that does not exist. #[test] fn a_line_naming_no_action_adds_no_row() { diff --git a/src/cmd/config.rs b/src/cmd/config.rs index 8253a98..6fc730c 100644 --- a/src/cmd/config.rs +++ b/src/cmd/config.rs @@ -117,7 +117,7 @@ ignore = ["node_modules", "target"] # same configuration serves fish and any shell cid later learns to write for. # Between them the two tables below name every action there is. `cid config # actions` lists them all with whatever is bound to each, `cid config print` -# lists the keys and names you have, and `cid config check` says whether they +# lists the keys and names you have, and `cid doctor` says whether they # resolve. # # cid binds nothing on its own. The two tables below are suggestions, and @@ -602,7 +602,7 @@ fn pad(text: &str, width: usize) -> String { /// `cid config print` — every setting there is, and what is in force for it. /// /// The settings only: whether the paths they name exist, and whether the tools -/// cid shells out to are installed, is `cid config check`. +/// cid shells out to are installed, is `cid doctor`. pub fn print(ctx: &Ctx) -> Result<()> { ctx.log.info(&format!( "printing configuration from {}", @@ -1165,7 +1165,7 @@ fn files_check(ctx: &Ctx) -> Check { } } -/// Everything `config check` looks at, in the order it is reported: what the +/// Everything `doctor` looks at, in the order it is reported: what the /// configuration points at, then the programs cid shells out to, then the /// files it keeps. fn collect(ctx: &Ctx) -> Vec
{ @@ -1241,7 +1241,7 @@ fn collect(ctx: &Ctx) -> Vec
{ ] } -/// `cid config check` — look at everything cid depends on in one go and say +/// `cid doctor` — look at everything cid depends on in one go and say /// what is wrong with it. The exit status is non-zero only when something is /// genuinely broken, so it is worth putting in a setup script. /// diff --git a/src/cmd/file.rs b/src/cmd/file.rs index 4c83331..7922b97 100644 --- a/src/cmd/file.rs +++ b/src/cmd/file.rs @@ -54,7 +54,7 @@ pub fn ls(ctx: &Ctx, status: bool, missing: bool, exists: bool) -> Result<()> { /// A file with its existence marked: green tick for there, red cross for gone. /// Shared by `file ls --status` and `file prune`, and the same glyphs and -/// colour indices `config check` marks a passing and a failing row with. +/// colour indices `doctor` marks a passing and a failing row with. fn status_row(path: &str, present: bool, color: bool) -> String { let (glyph, tint) = if present { ("✓", 2) } else { ("✗", 1) }; term::paint(&format!("{glyph} {path}"), tint, color) diff --git a/src/cmd/note.rs b/src/cmd/note.rs index 249d1e5..d7d75e5 100644 --- a/src/cmd/note.rs +++ b/src/cmd/note.rs @@ -974,7 +974,7 @@ fn which(program: &str) -> Option { }) } -/// The `config check` row for the vault: where it is, how much is in it, and +/// The `doctor` row for the vault: where it is, how much is in it, and /// how much of that a listing leaves out. One row rather than two, since a root /// that resolves and holds nothing has already been reported by the count. pub(crate) fn vault_summary(ctx: &Ctx) -> Result { @@ -1001,7 +1001,7 @@ pub(crate) fn vault_summary(ctx: &Ctx) -> Result { }) } -/// What `config check` found in the vault. +/// What `doctor` found in the vault. pub(crate) struct Vault { pub root: PathBuf, /// Notes a listing shows. diff --git a/src/gh.rs b/src/gh.rs index 3671902..22ec5ae 100644 --- a/src/gh.rs +++ b/src/gh.rs @@ -1060,7 +1060,7 @@ pub fn owners() -> Result> { /// token against the host, so this catches an expired or revoked one as well as /// no login at all. /// -/// A network round trip, and therefore for `config check` alone — nothing on a +/// A network round trip, and therefore for `doctor` alone — nothing on a /// keystroke path may ask this. pub fn authenticated() -> bool { let _child = stats::in_child(); diff --git a/src/main.rs b/src/main.rs index 8170152..7c66380 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,8 @@ //! Top-level commands: `repo`, `file`, `note`, `branch`, `worktree`, `pr`, //! `ps` and `history` work with the things cid finds; `edit` opens a file //! from the directory the user is in and `project` builds it; `config` manages -//! its configuration; `init` prints shell integration. +//! its configuration and `doctor` checks what it points at; `init` prints shell +//! integration. use std::process::ExitCode; @@ -242,6 +243,21 @@ enum Command { #[command(subcommand)] command: ConfigCmd, }, + /// Check everything cid depends on and report what is wrong + /// + /// A checklist: the config file, the paths it names, the repositories + /// discovery actually finds, your editor, `git`, `gh` and whether it is + /// still logged in, fish's history file, the tracked-file list and the + /// notes vault — each a line, with what to do about the ones that are + /// wrong. Exits non-zero only when something is genuinely broken, so it is + /// worth putting in a setup script; a warning still leaves cid working. + /// + /// What each setting is set to is `config print`; a row here repeats a + /// value only where repeating it is the way out of a problem. + /// + /// The login is asked of GitHub, so this is the one command here that waits + /// on the network. + Doctor, /// What you run, how often, and how long it takes /// /// Every run appends one line to a log — the command, and how long it took @@ -264,7 +280,7 @@ enum Command { /// `[shell.aliases]`, which name actions rather than shell code. cid /// binds nothing on its own: `cid config init` writes a suggested set out /// commented, and until a table is written nothing is bound. `cid config - /// actions` lists every action there is to name, and `cid config check` + /// actions` lists every action there is to name, and `cid doctor` /// says whether yours resolve; a configuration that will not parse, /// or that names an action cid does not define, stops this command rather /// than emitting a shell where one key silently does nothing. @@ -900,7 +916,7 @@ enum ConfigCmd { /// each one runs and what that action does; there are none until the file /// names some. /// - /// Whether what the settings point at is actually there is `config check`. + /// Whether what the settings point at is actually there is `doctor`. Print, /// List every action a key binding or alias can name /// @@ -913,21 +929,6 @@ enum ConfigCmd { Actions, /// Print the configuration file path Path, - /// Check everything cid depends on and report what is wrong - /// - /// A checklist: the config file, the paths it names, the repositories - /// discovery actually finds, your editor, `git`, `gh` and whether it is - /// still logged in, fish's history file, the tracked-file list and the - /// notes vault — each a line, with what to do about the ones that are - /// wrong. Exits non-zero only when something is genuinely broken, so it is - /// worth putting in a setup script; a warning still leaves cid working. - /// - /// What each setting is set to is `config print`; a row here repeats a - /// value only where repeating it is the way out of a problem. - /// - /// The login is asked of GitHub, so this is the one command here that waits - /// on the network. - Check, } #[derive(Subcommand)] @@ -1150,12 +1151,12 @@ fn dispatch(ctx: &Ctx, command: Command) -> anyhow::Result<()> { ProjectCmd::Deps { dry_run, dump } => cmd::project::deps(ctx, dry_run, dump), ProjectCmd::Build { dry_run } => cmd::project::build(ctx, dry_run), }, + Command::Doctor => cmd::config::check(ctx), Command::Config { command } => match command { ConfigCmd::Init { force } => cmd::config::init(ctx, force), ConfigCmd::Print => cmd::config::print(ctx), ConfigCmd::Actions => cmd::config::actions(ctx), ConfigCmd::Path => cmd::config::path(ctx), - ConfigCmd::Check => cmd::config::check(ctx), }, // The tree these report on is the clap command itself, which is the // only place that knows every command there is. diff --git a/tests/cli.rs b/tests/cli.rs index ab8b904..091665b 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -571,12 +571,12 @@ fn a_broken_config_names_the_file() { } #[test] -fn config_check_passes_on_a_sound_setup() { +fn doctor_passes_on_a_sound_setup() { let sandbox = Sandbox::new(); mk_repo(&sandbox.home().join("dev/github.com"), "acme", "billing"); sandbox.write_config("[repo]\nroot = \"~/dev/github.com\"\n"); - let run = sandbox.run(&["config", "check"]); + let run = sandbox.run(&["doctor"]); run.ok(); assert!(run.stdout.contains("repo root"), "{}", run.stdout); // Discovery is really run: the count is what answers "is my root right". @@ -585,11 +585,11 @@ fn config_check_passes_on_a_sound_setup() { } #[test] -fn config_check_fails_on_a_missing_root() { +fn doctor_fails_on_a_missing_root() { let sandbox = Sandbox::new(); sandbox.write_config("[repo]\nroot = \"~/not/here\"\n"); - let run = sandbox.run(&["config", "check"]); + let run = sandbox.run(&["doctor"]); run.code(1); assert!( run.stdout @@ -602,14 +602,14 @@ fn config_check_fails_on_a_missing_root() { assert!(!run.stdout.contains('\x1b'), "colour through a pipe"); } -/// `config check` and `--color` were written in parallel, each green on its +/// `doctor` and `--color` were written in parallel, each green on its /// own branch, and the merge did not compile. Nothing tied the two together. #[test] -fn config_check_honours_the_color_flag() { +fn doctor_honours_the_color_flag() { let sandbox = Sandbox::new(); sandbox.write_config("[repo]\nroot = \"~/not/here\"\n"); - let forced = sandbox.run(&["--color", "always", "config", "check"]); + let forced = sandbox.run(&["--color", "always", "doctor"]); forced.code(1); assert!( forced.stdout.contains('\x1b'), @@ -619,11 +619,11 @@ fn config_check_honours_the_color_flag() { } #[test] -fn config_check_does_not_report_one_problem_twice() { +fn doctor_does_not_report_one_problem_twice() { let sandbox = Sandbox::new(); sandbox.write_config("[repo]\nroot = \"~/not/here\"\n"); - let run = sandbox.run(&["config", "check"]); + let run = sandbox.run(&["doctor"]); run.code(1); assert_eq!( run.stdout @@ -2225,7 +2225,7 @@ fn note_ls_says_the_archives_are_why_it_found_nothing() { /// An archive path is relative to the vault, and one that names nothing hides /// nothing — which is a setting the user meant and did not get. #[test] -fn config_check_reports_an_archive_that_is_not_there() { +fn doctor_reports_an_archive_that_is_not_there() { let sandbox = Sandbox::new(); let vault = sandbox.home().join("notes"); mk_note(&vault, "work/plan.md", "plan\n", 1_000); @@ -2234,7 +2234,7 @@ fn config_check_reports_an_archive_that_is_not_there() { vault.display().to_string() )); - let run = sandbox.run(&["config", "check"]); + let run = sandbox.run(&["doctor"]); assert!(run.stdout.contains("work/archive"), "{}", run.stdout); } @@ -2411,13 +2411,13 @@ fn note_open_falls_back_to_the_environment_editor() { ); } -/// `config check` is what a setup script runs, so a configured vault has to +/// `doctor` is what a setup script runs, so a configured vault has to /// report as found rather than as a thing cid knows nothing about. #[test] -fn config_check_counts_the_notes_in_the_vault() { +fn doctor_counts_the_notes_in_the_vault() { let sandbox = Sandbox::new(); mk_vault(&sandbox); - let run = sandbox.run(&["config", "check"]); + let run = sandbox.run(&["doctor"]); assert!(run.stdout.contains("3 notes"), "{}", run.stdout); assert!(run.stdout.contains("note editor"), "{}", run.stdout); // `note open` shells out to it to search, so the report says whether it is @@ -2620,7 +2620,7 @@ fn every_note_command_survives_a_vault_that_is_not_ascii() { sandbox.run(&["note", "open", "øvelser.md"]).ok(); sandbox.run(&["note", "new", "Løsningsforslag"]).ok(); sandbox.run(&["note", "scratch"]).ok(); - sandbox.run(&["config", "check"]); + sandbox.run(&["doctor"]); // The one that crashed. Without a terminal it stops at the selector, which // is past every byte offset that was the bug. @@ -2961,7 +2961,7 @@ fn the_shell_integration_is_counted_by_check_and_named_by_print() { let sandbox = Sandbox::new(); sandbox.write_config("[shell.aliases]\nbuild = \"project-build\"\n"); - let check = sandbox.run(&["config", "check"]); + let check = sandbox.run(&["doctor"]); let row = check .lines() .into_iter() @@ -2980,11 +2980,11 @@ fn the_shell_integration_is_counted_by_check_and_named_by_print() { } #[test] -fn config_check_fails_on_a_binding_nothing_answers_to() { +fn doctor_fails_on_a_binding_nothing_answers_to() { let sandbox = Sandbox::new(); sandbox.write_config("[shell.bindings]\nctrl-o = \"repo-jump\"\n"); - let run = sandbox.run(&["config", "check"]); + let run = sandbox.run(&["doctor"]); run.code(1); assert!( run.stdout.contains("repo-jump"),