construct/commands/
install.rs1use anyhow::{Context, Result, anyhow};
15use std::io::Write;
16use std::path::{Path, PathBuf};
17use std::process::Command;
18
19const SIDECARS_SH: &str = include_str!("../../scripts/install-sidecars.sh");
21
22const SIDECARS_BAT: &str = include_str!("../../scripts/install-sidecars.bat");
24
25#[derive(Debug, Default, Clone)]
27pub struct InstallOptions {
28 pub sidecars_only: bool,
30 pub skip_kumiho: bool,
32 pub skip_operator: bool,
34 pub dry_run: bool,
36 pub python: Option<String>,
38}
39
40pub 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
54async 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 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
105fn 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
113fn 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}