返回 CodeWhale
cli.rs
根目录 / crates / tui / src / tools / github / cli.rs
1 //! Command construction: every `gh` and `git` process this tool runs.
2 //!
3 //! Nothing above this file assembles an argv or resolves a binary path, so
4 //! "what did the tool actually shell out to" has exactly one answer.
5
6 use std::process::Command;
7
8 use serde_json::Value;
9
10 use crate::dependencies::ExternalTool;
11 use crate::tools::spec::{ToolContext, ToolError};
12
13 const DEFAULT_GH: &str = "/opt/homebrew/bin/gh";
14 const FALLBACK_GH_PATHS: &[&str] = &[
15 "/usr/bin/gh", // Linux system package manager
16 "/usr/local/bin/gh", // macOS Intel Homebrew / manual install
17 "/home/linuxbrew/.linuxbrew/bin/gh", // Linux Homebrew (official prefix)
18 "/opt/homebrew/bin/gh", // macOS Apple Silicon Homebrew
19 ];
20
21 fn gh_bin() -> String {
22 if let Ok(bin) = std::env::var("CODEWHALE_GH_BIN").or_else(|_| std::env::var("DEEPSEEK_GH_BIN"))
23 {
24 return bin;
25 }
26 for path in FALLBACK_GH_PATHS {
27 if std::path::Path::new(path).is_file() {
28 return path.to_string();
29 }
30 }
31 DEFAULT_GH.to_string()
32 }
33
34 pub(super) fn run_gh_text(context: &ToolContext, args: &[&str]) -> Result<String, ToolError> {
35 let out = Command::new(gh_bin())
36 .args(args)
37 .current_dir(&context.workspace)
38 .output()
39 .map_err(|e| {
40 if e.kind() == std::io::ErrorKind::NotFound {
41 ToolError::not_available("gh CLI not found; install it or set DEEPSEEK_GH_BIN")
42 } else {
43 ToolError::execution_failed(format!("failed to run gh: {e}"))
44 }
45 })?;
46 if !out.status.success() {
47 return Err(ToolError::execution_failed(format!(
48 "gh {} failed: {}",
49 args.join(" "),
50 String::from_utf8_lossy(&out.stderr).trim()
51 )));
52 }
53 Ok(String::from_utf8_lossy(&out.stdout).to_string())
54 }
55
56 pub(super) fn run_gh_json(context: &ToolContext, args: &[&str]) -> Result<Value, ToolError> {
57 let text = run_gh_text(context, args)?;
58 serde_json::from_str(&text).map_err(|e| ToolError::execution_failed(e.to_string()))
59 }
60
61 pub(super) fn ensure_github_repo(context: &ToolContext) -> Result<(), ToolError> {
62 let out = crate::dependencies::Git::output(
63 &["rev-parse", "--is-inside-work-tree"],
64 &context.workspace,
65 )
66 .map_err(|e| ToolError::execution_failed(format!("failed to run git: {e}")))?;
67 if out.status.success() {
68 Ok(())
69 } else {
70 Err(ToolError::not_available(
71 "current workspace is not a git repository",
72 ))
73 }
74 }
75
76 pub(super) fn git_status_porcelain(context: &ToolContext) -> Result<String, ToolError> {
77 let out = crate::dependencies::Git::output(&["status", "--porcelain"], &context.workspace)
78 .map_err(|e| ToolError::execution_failed(format!("failed to run git status: {e}")))?;
79 Ok(String::from_utf8_lossy(&out.stdout).to_string())
80 }
81
81 lines RUST