1use crate::ui;
7use dialoguer::{theme::ColorfulTheme, Confirm, MultiSelect};
8use std::process::Command;
9use which::which;
10
11#[derive(Debug, Clone)]
16struct DetectedEnv {
17 docker: Option<String>,
18 docker_compose: Option<String>,
19 python: Option<String>,
20 node: Option<String>,
21 bun: Option<String>,
22 npm: Option<String>,
23 rust: Option<String>,
24}
25
26impl DetectedEnv {
27 fn detect() -> Self {
28 DetectedEnv {
29 docker: version_of(&["docker", "--version"], "Docker version "),
30 docker_compose: version_of(
31 &["docker", "compose", "version"],
32 "Docker Compose version ",
33 )
34 .or_else(|| version_of(&["docker-compose", "--version"], "docker-compose version ")),
35 python: version_of(&["python3", "--version"], "Python ")
36 .or_else(|| version_of(&["python", "--version"], "Python ")),
37 node: version_of(&["node", "--version"], "v"),
38 bun: version_of(&["bun", "--version"], ""),
39 npm: version_of(&["npm", "--version"], ""),
40 rust: version_of(&["rustc", "--version"], "rustc "),
41 }
42 }
43
44 fn has_js_package_manager(&self) -> bool {
45 self.bun.is_some() || self.npm.is_some()
46 }
47}
48
49fn version_of(cmd: &[&str], strip_prefix: &str) -> Option<String> {
51 if which(cmd[0]).is_err() {
52 return None;
53 }
54 let output = Command::new(cmd[0]).args(&cmd[1..]).output().ok()?;
55 let raw = String::from_utf8_lossy(&output.stdout);
56 let first_line = raw.lines().next()?;
57 Some(
58 first_line
59 .trim()
60 .trim_start_matches(strip_prefix)
61 .split_whitespace()
62 .next()
63 .unwrap_or(first_line.trim())
64 .to_string(),
65 )
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73enum Sdk {
74 TypeScript,
75 Python,
76 Rust,
77}
78
79impl Sdk {
80 fn label(&self) -> &str {
81 match self {
82 Sdk::TypeScript => "TypeScript / JavaScript",
83 Sdk::Python => "Python",
84 Sdk::Rust => "Rust",
85 }
86 }
87}
88
89pub fn run() -> anyhow::Result<()> {
94 ui::print_header();
95
96 ui::step("Detecting your environment...");
97 let env = DetectedEnv::detect();
98
99 if let Some(v) = &env.docker {
101 ui::kv_ok("Docker", v);
102 } else {
103 ui::kv_missing("Docker");
104 }
105 if let Some(v) = &env.python {
106 ui::kv_ok("Python", v);
107 } else {
108 ui::kv_missing("Python");
109 }
110 if let Some(v) = &env.node {
111 ui::kv_ok("Node.js", v);
112 } else {
113 ui::kv_missing("Node.js");
114 }
115 if let Some(v) = &env.bun {
116 ui::kv_ok("Bun", v);
117 } else {
118 ui::kv_missing("Bun");
119 }
120 if let Some(v) = &env.rust {
121 ui::kv_ok("Rust", v);
122 } else {
123 ui::kv_missing("Rust");
124 }
125
126 let mut available_sdks: Vec<Sdk> = vec![];
128 let mut defaults: Vec<bool> = vec![];
129
130 if env.has_js_package_manager() {
131 available_sdks.push(Sdk::TypeScript);
132 defaults.push(true);
133 }
134 if env.python.is_some() {
135 available_sdks.push(Sdk::Python);
136 defaults.push(true);
137 }
138 if env.rust.is_some() {
139 available_sdks.push(Sdk::Rust);
140 defaults.push(false); }
142
143 if available_sdks.is_empty() {
144 ui::warn("No supported language runtimes found.");
145 ui::info("Install Python, Node.js, or Bun, then run `cellstate setup` again.");
146 return Ok(());
147 }
148
149 ui::step("What are you building with?");
151 let sdk_labels: Vec<&str> = available_sdks.iter().map(|s| s.label()).collect();
152 let selections = MultiSelect::with_theme(&ColorfulTheme::default())
153 .with_prompt("Select languages (space to toggle, enter to confirm)")
154 .items(&sdk_labels)
155 .defaults(&defaults)
156 .interact()?;
157
158 let chosen_sdks: Vec<Sdk> = selections.iter().map(|&i| available_sdks[i]).collect();
159
160 if chosen_sdks.is_empty() {
161 ui::warn("Nothing selected — skipping SDK installation.");
162 } else {
163 install_sdks(&chosen_sdks, &env)?;
164 }
165
166 if env.docker.is_some() && env.docker_compose.is_some() {
168 println!();
169 let start = Confirm::with_theme(&ColorfulTheme::default())
170 .with_prompt("Start a local CELLSTATE server?")
171 .default(true)
172 .interact()?;
173
174 if start {
175 super::start::run()?;
176 }
177 } else {
178 ui::warn("Docker not found — skipping local server.");
179 ui::info("Install Docker, then run `cellstate start` to launch a local server.");
180 ui::info("Or use the managed cloud: https://cellstate.batterypack.dev");
181 }
182
183 ui::success_box(
185 "CELLSTATE is ready.",
186 &[
187 "Docs: https://cellstate.batterypack.dev/docs",
188 "Discord: https://discord.gg/cellstate",
189 "Issues: https://github.com/TheFreeBatteryFactory/CellState/issues",
190 ],
191 );
192
193 Ok(())
194}
195
196fn install_sdks(sdks: &[Sdk], env: &DetectedEnv) -> anyhow::Result<()> {
201 ui::step("Installing SDKs...");
202
203 for sdk in sdks {
204 match sdk {
205 Sdk::TypeScript => install_ts(env)?,
206 Sdk::Python => install_python(env)?,
207 Sdk::Rust => install_rust()?,
208 }
209 }
210
211 Ok(())
212}
213
214fn install_ts(env: &DetectedEnv) -> anyhow::Result<()> {
215 let pb = ui::spinner("Installing @cellstate/sdk...");
216
217 let (cmd, args) = if env.bun.is_some() {
219 ("bun", vec!["add", "@cellstate/sdk"])
220 } else {
221 ("npm", vec!["install", "@cellstate/sdk"])
222 };
223
224 let result = Command::new(cmd).args(&args).output();
225
226 match result {
227 Ok(output) if output.status.success() => {
228 ui::finish_spinner(pb, "@cellstate/sdk installed");
229 }
230 Ok(output) => {
231 let stderr = String::from_utf8_lossy(&output.stderr);
232 ui::fail_spinner(pb, &format!("npm install failed: {}", stderr.trim()));
233 }
234 Err(e) => {
235 ui::fail_spinner(pb, &format!("Could not run npm: {e}"));
236 }
237 }
238
239 Ok(())
240}
241
242fn install_python(env: &DetectedEnv) -> anyhow::Result<()> {
243 let pb = ui::spinner("Installing cellstate (Python SDK)...");
244
245 let (cmd, args): (&str, Vec<&str>) = if which("uv").is_ok() {
247 ("uv", vec!["add", "cellstate"])
248 } else if which("poetry").is_ok() {
249 ("poetry", vec!["add", "cellstate"])
250 } else {
251 let pip = if env.python.is_some() && which("pip3").is_ok() {
252 "pip3"
253 } else {
254 "pip"
255 };
256 (pip, vec!["install", "cellstate"])
257 };
258
259 let result = Command::new(cmd).args(&args).output();
260
261 match result {
262 Ok(output) if output.status.success() => {
263 ui::finish_spinner(pb, "cellstate (Python SDK) installed");
264 }
265 Ok(output) => {
266 let stderr = String::from_utf8_lossy(&output.stderr);
267 ui::fail_spinner(pb, &format!("pip install failed: {}", stderr.trim()));
268 }
269 Err(e) => {
270 ui::fail_spinner(pb, &format!("Could not run pip: {e}"));
271 }
272 }
273
274 Ok(())
275}
276
277fn install_rust() -> anyhow::Result<()> {
278 let pb = ui::spinner("Adding cellstate to Cargo.toml...");
279
280 let result = Command::new("cargo").args(["add", "cellstate"]).output();
281
282 match result {
283 Ok(output) if output.status.success() => {
284 ui::finish_spinner(pb, "cellstate crate added");
285 }
286 Ok(output) => {
287 let stderr = String::from_utf8_lossy(&output.stderr);
288 ui::fail_spinner(pb, &format!("cargo add failed: {}", stderr.trim()));
289 }
290 Err(e) => {
291 ui::fail_spinner(pb, &format!("Could not run cargo: {e}"));
292 }
293 }
294
295 Ok(())
296}