Skip to main content

databricks_tui/
cli.rs

1use anyhow::{Context, Result};
2use serde_json::Value;
3use tokio::process::Command;
4
5/// Profile names from ~/.databrickscfg, in file order.
6pub fn list_profiles() -> Vec<String> {
7    let Some(home) = std::env::var_os("HOME") else {
8        return Vec::new();
9    };
10    let path = std::path::Path::new(&home).join(".databrickscfg");
11    let Ok(content) = std::fs::read_to_string(path) else {
12        return Vec::new();
13    };
14    content
15        .lines()
16        .filter_map(|line| {
17            let line = line.trim();
18            line.strip_prefix('[')
19                .and_then(|rest| rest.strip_suffix(']'))
20                .map(str::to_string)
21        })
22        .filter(|name| !name.starts_with("__"))
23        .collect()
24}
25
26#[derive(Clone)]
27pub struct DatabricksCli {
28    profile: Option<String>,
29}
30
31impl DatabricksCli {
32    pub fn new(profile: Option<String>) -> Self {
33        Self { profile }
34    }
35
36    pub async fn run(&self, args: &[&str]) -> Result<Value> {
37        let mut cmd = Command::new("databricks");
38        // Run from a neutral directory: inside a bundle folder the CLI
39        // resolves typed commands against the bundle's workspace but raw
40        // `api` calls against the default profile, splitting the app
41        // across two workspaces. Neutral cwd keeps auth consistent.
42        cmd.current_dir("/");
43        cmd.arg("--output").arg("json");
44        if let Some(p) = &self.profile {
45            cmd.arg("--profile").arg(p);
46        }
47        cmd.args(args);
48
49        let out = cmd
50            .output()
51            .await
52            .context("failed to run databricks CLI — is it installed?")?;
53
54        if !out.status.success() {
55            let stderr = String::from_utf8_lossy(&out.stderr);
56            anyhow::bail!("databricks CLI error: {}", stderr.trim());
57        }
58
59        let stdout = String::from_utf8_lossy(&out.stdout);
60        serde_json::from_str(&stdout).context("failed to parse CLI JSON output")
61    }
62
63    /// Runs a command whose stdout is plain text, not JSON —
64    /// e.g. `fs cat` on a file inside a volume.
65    pub async fn run_raw(&self, args: &[&str]) -> Result<String> {
66        let mut cmd = Command::new("databricks");
67        cmd.current_dir("/");
68        if let Some(p) = &self.profile {
69            cmd.arg("--profile").arg(p);
70        }
71        cmd.args(args);
72
73        let out = cmd
74            .output()
75            .await
76            .context("failed to run databricks CLI — is it installed?")?;
77
78        if !out.status.success() {
79            let stderr = String::from_utf8_lossy(&out.stderr);
80            anyhow::bail!("databricks CLI error: {}", stderr.trim());
81        }
82        Ok(String::from_utf8_lossy(&out.stdout).to_string())
83    }
84
85    /// Runs a mutating command where success is all that matters —
86    /// start/stop/run-now often print nothing or non-JSON on success.
87    pub async fn run_action(&self, args: &[&str]) -> Result<()> {
88        let mut cmd = Command::new("databricks");
89        cmd.current_dir("/");
90        if let Some(p) = &self.profile {
91            cmd.arg("--profile").arg(p);
92        }
93        cmd.args(args);
94
95        let out = cmd
96            .output()
97            .await
98            .context("failed to run databricks CLI — is it installed?")?;
99
100        if !out.status.success() {
101            let stderr = String::from_utf8_lossy(&out.stderr);
102            anyhow::bail!("databricks CLI error: {}", stderr.trim());
103        }
104        Ok(())
105    }
106}