construct/sidecars/
mod.rs1mod install;
9pub mod python;
10
11pub use install::{SidecarInstallOptions, install_sidecars};
12
13use anyhow::Result;
14use std::path::PathBuf;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SidecarStatus {
19 Ready,
20 Missing,
21}
22
23pub fn construct_root() -> Result<PathBuf> {
25 let home = directories::UserDirs::new()
26 .map(|u| u.home_dir().to_path_buf())
27 .ok_or_else(|| anyhow::anyhow!("could not determine home directory"))?;
28 Ok(home.join(".construct"))
29}
30
31pub fn kumiho_launcher_path() -> Result<PathBuf> {
33 Ok(construct_root()?.join("kumiho").join("run_kumiho_mcp.py"))
34}
35
36pub fn operator_launcher_path() -> Result<PathBuf> {
38 Ok(construct_root()?
39 .join("operator_mcp")
40 .join("run_operator_mcp.py"))
41}
42
43pub fn status(sidecar: Sidecar) -> SidecarStatus {
45 let Ok(root) = construct_root() else {
46 return SidecarStatus::Missing;
47 };
48 let (dir, launcher) = match sidecar {
49 Sidecar::Kumiho => (root.join("kumiho"), "run_kumiho_mcp.py"),
50 Sidecar::Operator => (root.join("operator_mcp"), "run_operator_mcp.py"),
51 };
52 let interp = if cfg!(windows) {
53 dir.join("venv").join("Scripts").join("python.exe")
54 } else {
55 dir.join("venv").join("bin").join("python3")
56 };
57 if interp.exists() && dir.join(launcher).exists() {
58 SidecarStatus::Ready
59 } else {
60 SidecarStatus::Missing
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum Sidecar {
66 Kumiho,
67 Operator,
68}
69
70pub async fn ensure_sidecars_ready(interactive: bool) -> Result<()> {
76 let kumiho = status(Sidecar::Kumiho);
77 let operator = status(Sidecar::Operator);
78 if kumiho == SidecarStatus::Ready && operator == SidecarStatus::Ready {
79 return Ok(());
80 }
81
82 if interactive && !prompt_install(kumiho, operator)? {
83 anyhow::bail!(
84 "sidecars not installed; re-run with `construct install --sidecars-only` when ready"
85 );
86 }
87
88 install_sidecars(&SidecarInstallOptions {
89 skip_kumiho: kumiho == SidecarStatus::Ready,
90 skip_operator: operator == SidecarStatus::Ready,
91 ..Default::default()
92 })
93 .await
94}
95
96fn prompt_install(kumiho: SidecarStatus, operator: SidecarStatus) -> Result<bool> {
97 use std::io::{BufRead, Write};
98 let mut missing = Vec::new();
99 if kumiho == SidecarStatus::Missing {
100 missing.push("Kumiho");
101 }
102 if operator == SidecarStatus::Missing {
103 missing.push("Operator");
104 }
105 eprintln!(
106 "==> Construct needs to install the {} MCP sidecar{} (one-time, ~60s).",
107 missing.join(" + "),
108 if missing.len() == 1 { "" } else { "s" }
109 );
110 eprint!(" Install now? [Y/n] ");
111 std::io::stderr().flush().ok();
112
113 let stdin = std::io::stdin();
114 let mut line = String::new();
115 stdin.lock().read_line(&mut line)?;
116 let ans = line.trim().to_lowercase();
117 Ok(ans.is_empty() || ans == "y" || ans == "yes")
118}