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
26pub struct DatabricksCli {
27    profile: Option<String>,
28}
29
30impl DatabricksCli {
31    pub fn new(profile: Option<String>) -> Self {
32        Self { profile }
33    }
34
35    pub async fn run(&self, args: &[&str]) -> Result<Value> {
36        let mut cmd = Command::new("databricks");
37        // Run from a neutral directory: inside a bundle folder the CLI
38        // resolves typed commands against the bundle's workspace but raw
39        // `api` calls against the default profile, splitting the app
40        // across two workspaces. Neutral cwd keeps auth consistent.
41        cmd.current_dir("/");
42        cmd.arg("--output").arg("json");
43        if let Some(p) = &self.profile {
44            cmd.arg("--profile").arg(p);
45        }
46        cmd.args(args);
47
48        let out = cmd
49            .output()
50            .await
51            .context("failed to run databricks CLI — is it installed?")?;
52
53        if !out.status.success() {
54            let stderr = String::from_utf8_lossy(&out.stderr);
55            anyhow::bail!("databricks CLI error: {}", stderr.trim());
56        }
57
58        let stdout = String::from_utf8_lossy(&out.stdout);
59        serde_json::from_str(&stdout).context("failed to parse CLI JSON output")
60    }
61
62    /// Runs a command whose stdout is plain text, not JSON —
63    /// e.g. `fs cat` on a file inside a volume.
64    pub async fn run_raw(&self, args: &[&str]) -> Result<String> {
65        let mut cmd = Command::new("databricks");
66        cmd.current_dir("/");
67        if let Some(p) = &self.profile {
68            cmd.arg("--profile").arg(p);
69        }
70        cmd.args(args);
71
72        let out = cmd
73            .output()
74            .await
75            .context("failed to run databricks CLI — is it installed?")?;
76
77        if !out.status.success() {
78            let stderr = String::from_utf8_lossy(&out.stderr);
79            anyhow::bail!("databricks CLI error: {}", stderr.trim());
80        }
81        Ok(String::from_utf8_lossy(&out.stdout).to_string())
82    }
83
84    /// Runs a mutating command where success is all that matters —
85    /// start/stop/run-now often print nothing or non-JSON on success.
86    pub async fn run_action(&self, args: &[&str]) -> Result<()> {
87        let mut cmd = Command::new("databricks");
88        cmd.current_dir("/");
89        if let Some(p) = &self.profile {
90            cmd.arg("--profile").arg(p);
91        }
92        cmd.args(args);
93
94        let out = cmd
95            .output()
96            .await
97            .context("failed to run databricks CLI — is it installed?")?;
98
99        if !out.status.success() {
100            let stderr = String::from_utf8_lossy(&out.stderr);
101            anyhow::bail!("databricks CLI error: {}", stderr.trim());
102        }
103        Ok(())
104    }
105}