Hmbown/CodeWhale · error · std::io::Error

gh not found

Error message

gh not found

What it means

`upload_gist` shares a file as a GitHub Gist by shelling out to the `gh` CLI. When the `gh` executable cannot be located on the system, the spawned command construction fails with `NotFound` and this "gh not found" error string is propagated to the share flow.

Solutions

  1. Install the GitHub CLI: e.g. `brew install gh`, `apt install gh`, `winget install GitHub.cli`, or see https://cli.github.com.
  2. Verify installation with `gh --version` in the same shell/environment codewhale runs in (PATH must contain it).
  3. If `gh` is installed via a non-default prefix, add its directory to PATH (e.g. `export PATH="$HOME/.local/bin:$PATH"`).
  4. Authenticate once installed: `gh auth login` — the share step also needs a valid GitHub session.

Example fix

// before: error at share time
let output = tokio::task::spawn_blocking(move || {
    let mut cmd = crate::dependencies::Gh::command()
        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "gh not found"))?;
// after (user fix, terminal)
$ brew install gh && gh auth login
$ codewhale   # share again
Defensive patterns

Strategy: fallback

Validate before calling

// caller pre-check before invoking share
fn gh_available() -> bool {
    std::process::Command::new("gh").arg("--version").output()
        .map(|o| o.status.success()).unwrap_or(false)
}
if !gh_available() { eprintln!("Install the GitHub CLI (gh) first."); }

Type guard

fn gh_binary_present() -> bool {
    std::env::var_os("PATH")
        .map(|p| std::env::split_paths(&p).any(|d| {
            #[cfg(unix)] { d.join("gh").is_file() }
            #[cfg(windows)] { d.join("gh.exe").is_file() }
        }))
        .unwrap_or(false)
}

Try / catch

match perform_share(path).await {
    Err(msg) if msg.contains("gh not found") => {
        eprintln!("gh CLI missing. Install it (https://cli.github.com), run `gh auth login`, then retry.");
    }
    Err(msg) => eprintln!("share failed: {msg}"),
    Ok(url) => println!("shared: {url}"),
}

Prevention

When it happens

Trigger: Invoking the project share command (`perform_share` -> `upload_gist`) on a machine where `Gh::command()` cannot find the `gh` binary in PATH.

Common situations: GitHub CLI not installed at all; `gh` installed via a version manager whose PATH is not active in the environment running codewhale; running inside a container or service without the CLI.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/51f3c67fda436881. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/commands/groups/project/share.rs:163

}

/// Write HTML to a secure temp file and keep it alive for upload.
fn write_temp_html(html: &str) -> Result<tempfile::NamedTempFile, String> {
    let mut tmp = tempfile::Builder::new()
        .prefix("codewhale-share-")
        .suffix(".html")
        .tempfile()
        .map_err(|e| format!("{e}"))?;
    tmp.write_all(html.as_bytes()).map_err(|e| format!("{e}"))?;
    Ok(tmp)
}

/// Upload a file as a GitHub Gist using the `gh` CLI.
async fn upload_gist(path: &Path) -> Result<String, String> {
    let path_owned = path.to_path_buf();
    let output = tokio::task::spawn_blocking(move || {
        let mut cmd = crate::dependencies::Gh::command()
            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "gh not found"))?;
        cmd.args([
            "gist",
            "create",
            "--public",
            path_owned.to_string_lossy().as_ref(),
            "--filename",
            "session-export.html",
            "--desc",
            "codewhale Session Export",
        ])
        .output()
    })
    .await
    .map_err(|join_err| format!("gh gist create panicked: {join_err}"))?
    .map_err(|e| format!("Failed to run `gh gist create`: {e}"))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);

View on GitHub (pinned to 73e0f67d83)