Skip to main content

construct/commands/
install.rs

1//! `construct install` — unified post-build install flow.
2//!
3//! First slice: the `--sidecars-only` path is the only implementation. It
4//! materializes the embedded `install-sidecars.{sh,bat}` scripts into a
5//! temporary directory and runs them. This gives users a cross-platform
6//! `construct install --sidecars-only` entry point without having to know
7//! whether their checkout has the scripts or not (e.g. a `cargo install
8//! kumiho-construct`-only user has no `scripts/` directory available).
9//!
10//! The full install flow (prerequisite checks, build, onboard, dashboard
11//! launch) will migrate from `install.sh` / `setup.bat` into this module over
12//! time. Today those scripts remain canonical for a full install.
13
14use anyhow::{Context, Result, anyhow};
15use std::io::Write;
16use std::path::{Path, PathBuf};
17use std::process::Command;
18
19/// The POSIX sidecar installer, embedded at compile time.
20const SIDECARS_SH: &str = include_str!("../../scripts/install-sidecars.sh");
21
22/// The Windows sidecar installer, embedded at compile time.
23const SIDECARS_BAT: &str = include_str!("../../scripts/install-sidecars.bat");
24
25/// Options for `construct install`.
26#[derive(Debug, Default, Clone)]
27pub struct InstallOptions {
28    /// Install only the Python MCP sidecars (Kumiho + Operator).
29    pub sidecars_only: bool,
30    /// Skip installing the Kumiho sidecar.
31    pub skip_kumiho: bool,
32    /// Skip installing the Operator sidecar.
33    pub skip_operator: bool,
34    /// Print what would be done without executing.
35    pub dry_run: bool,
36    /// Optional explicit Python interpreter (passed to the sidecar script).
37    pub python: Option<String>,
38}
39
40/// Run the install command with the given options.
41pub async fn run(opts: InstallOptions) -> Result<()> {
42    if !opts.sidecars_only {
43        return Err(anyhow!(
44            "Full install is not yet implemented as a Rust subcommand.\n\
45             Use one of:\n  \
46               construct install --sidecars-only    # install Kumiho + Operator Python MCP sidecars\n  \
47               ./install.sh                         # full POSIX install (source build + sidecars + onboard)\n  \
48               setup.bat                            # full Windows install"
49        ));
50    }
51    run_sidecars(&opts).await
52}
53
54/// Install the Kumiho + Operator Python MCP sidecars by materializing and
55/// invoking the bundled script for the current platform.
56async fn run_sidecars(opts: &InstallOptions) -> Result<()> {
57    let tmp = tempdir_for_scripts()?;
58    let (script_path, mut cmd) = if cfg!(windows) {
59        let path = tmp.join("install-sidecars.bat");
60        write_atomic(&path, SIDECARS_BAT)?;
61        let mut c = Command::new("cmd");
62        c.arg("/C").arg(&path);
63        (path, c)
64    } else {
65        let path = tmp.join("install-sidecars.sh");
66        write_atomic(&path, SIDECARS_SH)?;
67        make_executable(&path)?;
68        let mut c = Command::new("bash");
69        c.arg(&path);
70        (path, c)
71    };
72
73    if opts.skip_kumiho {
74        cmd.arg("--skip-kumiho");
75    }
76    if opts.skip_operator {
77        cmd.arg("--skip-operator");
78    }
79    if opts.dry_run && !cfg!(windows) {
80        // .bat does not yet implement --dry-run; leave off on Windows.
81        cmd.arg("--dry-run");
82    }
83    if let Some(py) = &opts.python {
84        cmd.arg("--python").arg(py);
85    }
86
87    eprintln!("==> construct install --sidecars-only");
88    eprintln!("    script: {}", script_path.display());
89
90    let status = cmd
91        .status()
92        .with_context(|| format!("failed to invoke {}", script_path.display()))?;
93
94    if !status.success() {
95        let code = status.code().unwrap_or(-1);
96        return Err(anyhow!(
97            "sidecar installer exited with status {code}. \
98             See ~/.construct/logs/ and docs/setup-guides/kumiho-operator-setup.md for troubleshooting."
99        ));
100    }
101
102    Ok(())
103}
104
105/// Create a fresh temp directory under the system temp root.
106fn tempdir_for_scripts() -> Result<PathBuf> {
107    let base = std::env::temp_dir().join(format!("construct-install-{}", std::process::id()));
108    std::fs::create_dir_all(&base)
109        .with_context(|| format!("creating temp dir {}", base.display()))?;
110    Ok(base)
111}
112
113/// Write file contents atomically (best-effort; just write-through on Windows).
114fn write_atomic(path: &Path, contents: &str) -> Result<()> {
115    let mut f =
116        std::fs::File::create(path).with_context(|| format!("creating {}", path.display()))?;
117    f.write_all(contents.as_bytes())
118        .with_context(|| format!("writing {}", path.display()))?;
119    f.flush().ok();
120    Ok(())
121}
122
123#[cfg(unix)]
124fn make_executable(path: &Path) -> Result<()> {
125    use std::os::unix::fs::PermissionsExt;
126    let mut perm = std::fs::metadata(path)?.permissions();
127    perm.set_mode(0o755);
128    std::fs::set_permissions(path, perm).with_context(|| format!("chmod +x {}", path.display()))?;
129    Ok(())
130}
131
132#[cfg(not(unix))]
133fn make_executable(_path: &Path) -> Result<()> {
134    Ok(())
135}