From d970d48aa714ada01bf2cd40934ad374408c3f41 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Sat, 18 Oct 2025 22:25:59 -0300 Subject: [PATCH] - Postgres updated to 18. --- .gitignore | 1 + docs/platform/DEV.md | 38 ++ fix-errors.sh | 28 +- gbot.sh | 4 +- src/bootstrap/mod.rs | 15 +- src/package_manager/mod.rs | 1061 ++++++++++++++++++++++++------------ 6 files changed, 770 insertions(+), 377 deletions(-) diff --git a/.gitignore b/.gitignore index 94e6db37..1bff003a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ target work *.out bin +botserver-stack diff --git a/docs/platform/DEV.md b/docs/platform/DEV.md index a1f2784f..d2a2327f 100644 --- a/docs/platform/DEV.md +++ b/docs/platform/DEV.md @@ -38,3 +38,41 @@ valkey-cli -p 6379 monitor - Prompt add-ons: Fill the file with info!, trace! and debug! macros. - + + +# Zed Agents +``` + "language_models": { + "openai_compatible": { + "Groq GPT 120b": { + "api_url": "https://api.groq.com/openai/v1", + "available_models": [ + { + "name": "meta-llama/llama-4-scout-17b-16e-instruct", + "max_tokens": 30000, + "capabilities": { + "tools": true, + "images": false, + "parallel_tool_calls": false, + "prompt_cache_key": false + } + }, + { + "name": "groq/compound", + "max_tokens": 70000 + }, + { + "name": "openai/gpt-oss-120b", + "max_tokens": 8000, + "capabilities": { + "tools": true, + "images": false, + "parallel_tool_calls": false, + "prompt_cache_key": false + } + } + ] + } + } + }, +``` diff --git a/fix-errors.sh b/fix-errors.sh index 397e0b5d..13058238 100755 --- a/fix-errors.sh +++ b/fix-errors.sh @@ -2,14 +2,14 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$SCRIPT_DIR" -OUTPUT_FILE="$SCRIPT_DIR/prompt.out" +OUTPUT_FILE="/tmp/prompt.out" rm $OUTPUT_FILE echo "Please, fix this consolidated LLM Context" > "$OUTPUT_FILE" prompts=( - "./prompts/dev/shared.md" + "./prompts/dev/platform/shared.md" "./Cargo.toml" - "./prompts/dev/fix.md" + "./prompts/dev/platform/fix-errors.md" ) for file in "${prompts[@]}"; do @@ -21,7 +21,8 @@ dirs=( #"auth" #"automation" #"basic" - "bot" + #"bot" + "bootstrap" #"channels" #"config" #"context" @@ -49,11 +50,22 @@ done echo "$PROJECT_ROOT/src/main.rs" >> "$OUTPUT_FILE" cat "$PROJECT_ROOT/src/main.rs" >> "$OUTPUT_FILE" -cat "$PROJECT_ROOT/src/basic/keywords/hear_talk.rs" >> "$OUTPUT_FILE" -echo "$PROJECT_ROOT/src/basic/mod.rs">> "$OUTPUT_FILE" -cat "$PROJECT_ROOT/src/basic/mod.rs" >> "$OUTPUT_FILE" - echo "" >> "$OUTPUT_FILE" cargo build --message-format=short 2>&1 | grep -E 'error' >> "$OUTPUT_FILE" + + +# Calculate and display token count (approximation: words * 1.3) +WORD_COUNT=$(wc -w < "$OUTPUT_FILE") +TOKEN_COUNT=$(echo "$WORD_COUNT * 1.3 / 1" | bc) +FILE_SIZE=$(wc -c < "$OUTPUT_FILE") + +echo "" >> "$OUTPUT_FILE" + +echo "Approximate token count: $TOKEN_COUNT" +echo "Context size: $FILE_SIZE bytes" + +cat "$OUTPUT_FILE" | xclip -selection clipboard +echo "Content copied to clipboard (xclip)" +rm -f "$OUTPUT_FILE" diff --git a/gbot.sh b/gbot.sh index 3544eef4..2aaeef89 100755 --- a/gbot.sh +++ b/gbot.sh @@ -1,2 +1,2 @@ -echo Starting General Bots... -npm run start + +clear && cargo build && sudo RUST_BACKTRACE=1 ./target/debug/botserver diff --git a/src/bootstrap/mod.rs b/src/bootstrap/mod.rs index af272284..bfefb760 100644 --- a/src/bootstrap/mod.rs +++ b/src/bootstrap/mod.rs @@ -424,10 +424,10 @@ impl BootstrapManager { fn generate_password(&self) -> String { use rand::Rng; const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - let mut rng = rand::rng(); + let mut rng = rand::thread_rng(); (0..16) .map(|_| { - let idx = rng.random_range(0..CHARSET.len()); + let idx = rng.gen_range(0..CHARSET.len()); CHARSET[idx] as char }) .collect() @@ -479,9 +479,14 @@ impl BootstrapManager { let start = std::time::Instant::now(); while start.elapsed().as_secs() < timeout_secs { - if TcpListener::bind((host, port)).is_err() { - info!("Service {}:{} is ready", host, port); - return Ok(()); + match TcpListener::bind((host, port)) { + Ok(_) => { + thread::sleep(Duration::from_secs(1)); + } + Err(_) => { + info!("Service {}:{} is ready", host, port); + return Ok(()); + } } thread::sleep(Duration::from_secs(1)); } diff --git a/src/package_manager/mod.rs b/src/package_manager/mod.rs index a97b9a5b..90d90c59 100644 --- a/src/package_manager/mod.rs +++ b/src/package_manager/mod.rs @@ -54,9 +54,9 @@ impl PackageManager { } else { PathBuf::from("./botserver-stack") }; - + let tenant = tenant.unwrap_or_else(|| "default".to_string()); - + let mut pm = PackageManager { mode, os_type, @@ -64,9 +64,14 @@ impl PackageManager { tenant, components: HashMap::new(), }; - + pm.register_components(); - info!("PackageManager initialized with {} components in {:?} mode for tenant {}", pm.components.len(), pm.mode, pm.tenant); + info!( + "PackageManager initialized with {} components in {:?} mode for tenant {}", + pm.components.len(), + pm.mode, + pm.tenant + ); Ok(pm) } @@ -168,31 +173,37 @@ impl PackageManager { required: true, ports: vec![5432], dependencies: vec![], - linux_packages: vec!["wget".to_string(), "gnupg2".to_string(), "lsb-release".to_string(), "postgresql-common".to_string()], - macos_packages: vec!["postgresql".to_string()], + linux_packages: vec!["wget".to_string()], + macos_packages: vec!["wget".to_string()], windows_packages: vec![], - download_url: None, + download_url: Some("https://github.com/theseus-rs/postgresql-binaries/releases/download/18.0.0/postgresql-18.0.0-x86_64-unknown-linux-gnu.tar.gz".to_string()), binary_name: Some("postgres".to_string()), - pre_install_cmds_linux: vec![ - "/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh".to_string(), - "apt-get update && apt-get install -y postgresql-16".to_string() - ], + pre_install_cmds_linux: vec![], post_install_cmds_linux: vec![ - "sudo -u postgres psql -p 5432 -c \"CREATE USER default WITH PASSWORD 'defaultpass'\" || true".to_string(), - "sudo -u postgres psql -p 5432 -c \"CREATE DATABASE default_db OWNER default\" || true".to_string(), - "sudo -u postgres psql -p 5432 -c \"GRANT ALL PRIVILEGES ON DATABASE default_db TO default\" || true".to_string() + "tar -xzf postgresql-18.0.0-x86_64-unknown-linux-gnu.tar.gz".to_string(), + "mv pgsql/* . && rm -rf pgsql".to_string(), + "if [ ! -d \"{{DATA_PATH}}/pgdata\" ]; then ./initdb -D {{DATA_PATH}}/pgdata -U postgres; fi".to_string(), + "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"data_directory = '{{DATA_PATH}}/pgdata'\" > {{CONF_PATH}}/postgresql.conf; fi".to_string(), + "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"hba_file = '{{CONF_PATH}}/pg_hba.conf'\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), + "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"ident_file = '{{CONF_PATH}}/pg_ident.conf'\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), + "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"port = 5432\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), + "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"listen_addresses = '*'\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), + "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"log_directory = '{{LOGS_PATH}}'\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), + "if [ ! -f \"{{CONF_PATH}}/postgresql.conf\" ]; then echo \"logging_collector = on\" >> {{CONF_PATH}}/postgresql.conf; fi".to_string(), + "if [ ! -f \"{{CONF_PATH}}/pg_hba.conf\" ]; then echo \"host all all all md5\" > {{CONF_PATH}}/pg_hba.conf; fi".to_string(), + "if [ ! -f \"{{CONF_PATH}}/pg_ident.conf\" ]; then touch {{CONF_PATH}}/pg_ident.conf; fi".to_string(), + "if [ ! -d \"{{DATA_PATH}}/pgdata\" ]; then ./pg_ctl -D {{DATA_PATH}}/pgdata -l {{LOGS_PATH}}/postgres.log start; sleep 5; ./psql -p 5432 -d postgres -c \"CREATE USER default WITH PASSWORD 'defaultpass'\"; ./psql -p 5432 -d postgres -c \"CREATE DATABASE default_db OWNER default\"; ./psql -p 5432 -d postgres -c \"GRANT ALL PRIVILEGES ON DATABASE default_db TO default\"; ./pg_ctl -D {{DATA_PATH}}/pgdata stop; fi".to_string() ], pre_install_cmds_macos: vec![], post_install_cmds_macos: vec![ - "initdb -D {{DATA_PATH}}/pgdata".to_string(), - "sleep 5".to_string(), - "psql -p 5432 -d postgres -c \"CREATE USER default WITH PASSWORD 'defaultpass'\" || true".to_string(), - "psql -p 5432 -d postgres -c \"CREATE DATABASE default_db OWNER default\" || true".to_string() + "tar -xzf postgresql-18.0-1-linux-x64-binaries.tar.gz".to_string(), + "mv pgsql/* . && rm -rf pgsql".to_string(), + "if [ ! -d \"{{DATA_PATH}}/pgdata\" ]; then ./initdb -D {{DATA_PATH}}/pgdata -U postgres; fi".to_string(), ], pre_install_cmds_windows: vec![], post_install_cmds_windows: vec![], env_vars: HashMap::new(), - exec_cmd: "postgres -D {{DATA_PATH}}/pgdata -p 5432".to_string(), + exec_cmd: "./pg_ctl -D {{DATA_PATH}}/pgdata -l {{LOGS_PATH}}/postgres.log start".to_string(), }); } @@ -421,140 +432,166 @@ impl PackageManager { } fn register_table_editor(&mut self) { - self.components.insert("table-editor".to_string(), ComponentConfig { - name: "table-editor".to_string(), - required: false, - ports: vec![5757], - dependencies: vec!["tables".to_string()], - linux_packages: vec!["wget".to_string(), "curl".to_string()], - macos_packages: vec![], - windows_packages: vec![], - download_url: Some("http://get.nocodb.com/linux-x64".to_string()), - binary_name: Some("nocodb".to_string()), - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "{{BIN_PATH}}/nocodb".to_string(), - }); + self.components.insert( + "table-editor".to_string(), + ComponentConfig { + name: "table-editor".to_string(), + required: false, + ports: vec![5757], + dependencies: vec!["tables".to_string()], + linux_packages: vec!["wget".to_string(), "curl".to_string()], + macos_packages: vec![], + windows_packages: vec![], + download_url: Some("http://get.nocodb.com/linux-x64".to_string()), + binary_name: Some("nocodb".to_string()), + pre_install_cmds_linux: vec![], + post_install_cmds_linux: vec![], + pre_install_cmds_macos: vec![], + post_install_cmds_macos: vec![], + pre_install_cmds_windows: vec![], + post_install_cmds_windows: vec![], + env_vars: HashMap::new(), + exec_cmd: "{{BIN_PATH}}/nocodb".to_string(), + }, + ); } fn register_doc_editor(&mut self) { - self.components.insert("doc-editor".to_string(), ComponentConfig { - name: "doc-editor".to_string(), - required: false, - ports: vec![9980], - dependencies: vec![], - linux_packages: vec!["wget".to_string(), "gnupg".to_string()], - macos_packages: vec![], - windows_packages: vec![], - download_url: None, - binary_name: Some("coolwsd".to_string()), - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "coolwsd --config-file={{CONF_PATH}}/coolwsd.xml".to_string(), - }); + self.components.insert( + "doc-editor".to_string(), + ComponentConfig { + name: "doc-editor".to_string(), + required: false, + ports: vec![9980], + dependencies: vec![], + linux_packages: vec!["wget".to_string(), "gnupg".to_string()], + macos_packages: vec![], + windows_packages: vec![], + download_url: None, + binary_name: Some("coolwsd".to_string()), + pre_install_cmds_linux: vec![], + post_install_cmds_linux: vec![], + pre_install_cmds_macos: vec![], + post_install_cmds_macos: vec![], + pre_install_cmds_windows: vec![], + post_install_cmds_windows: vec![], + env_vars: HashMap::new(), + exec_cmd: "coolwsd --config-file={{CONF_PATH}}/coolwsd.xml".to_string(), + }, + ); } fn register_desktop(&mut self) { - self.components.insert("desktop".to_string(), ComponentConfig { - name: "desktop".to_string(), - required: false, - ports: vec![3389], - dependencies: vec![], - linux_packages: vec!["xvfb".to_string(), "xrdp".to_string(), "xfce4".to_string()], - macos_packages: vec![], - windows_packages: vec![], - download_url: None, - binary_name: None, - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "xrdp --nodaemon".to_string(), - }); + self.components.insert( + "desktop".to_string(), + ComponentConfig { + name: "desktop".to_string(), + required: false, + ports: vec![3389], + dependencies: vec![], + linux_packages: vec!["xvfb".to_string(), "xrdp".to_string(), "xfce4".to_string()], + macos_packages: vec![], + windows_packages: vec![], + download_url: None, + binary_name: None, + pre_install_cmds_linux: vec![], + post_install_cmds_linux: vec![], + pre_install_cmds_macos: vec![], + post_install_cmds_macos: vec![], + pre_install_cmds_windows: vec![], + post_install_cmds_windows: vec![], + env_vars: HashMap::new(), + exec_cmd: "xrdp --nodaemon".to_string(), + }, + ); } fn register_devtools(&mut self) { - self.components.insert("devtools".to_string(), ComponentConfig { - name: "devtools".to_string(), - required: false, - ports: vec![], - dependencies: vec![], - linux_packages: vec!["xclip".to_string(), "git".to_string(), "curl".to_string()], - macos_packages: vec!["git".to_string()], - windows_packages: vec![], - download_url: None, - binary_name: None, - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "".to_string(), - }); + self.components.insert( + "devtools".to_string(), + ComponentConfig { + name: "devtools".to_string(), + required: false, + ports: vec![], + dependencies: vec![], + linux_packages: vec!["xclip".to_string(), "git".to_string(), "curl".to_string()], + macos_packages: vec!["git".to_string()], + windows_packages: vec![], + download_url: None, + binary_name: None, + pre_install_cmds_linux: vec![], + post_install_cmds_linux: vec![], + pre_install_cmds_macos: vec![], + post_install_cmds_macos: vec![], + pre_install_cmds_windows: vec![], + post_install_cmds_windows: vec![], + env_vars: HashMap::new(), + exec_cmd: "".to_string(), + }, + ); } fn register_bot(&mut self) { - self.components.insert("bot".to_string(), ComponentConfig { - name: "bot".to_string(), - required: false, - ports: vec![3000], - dependencies: vec![], - linux_packages: vec!["curl".to_string(), "gnupg".to_string(), "ca-certificates".to_string(), "git".to_string()], - macos_packages: vec!["node".to_string()], - windows_packages: vec![], - download_url: None, - binary_name: None, - pre_install_cmds_linux: vec![ - "curl -fsSL https://deb.nodesource.com/setup_22.x | bash -".to_string(), - "apt-get update && apt-get install -y nodejs".to_string() - ], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::from([ - ("DISPLAY".to_string(), ":99".to_string()) - ]), - exec_cmd: "".to_string(), - }); + self.components.insert( + "bot".to_string(), + ComponentConfig { + name: "bot".to_string(), + required: false, + ports: vec![3000], + dependencies: vec![], + linux_packages: vec![ + "curl".to_string(), + "gnupg".to_string(), + "ca-certificates".to_string(), + "git".to_string(), + ], + macos_packages: vec!["node".to_string()], + windows_packages: vec![], + download_url: None, + binary_name: None, + pre_install_cmds_linux: vec![ + "curl -fsSL https://deb.nodesource.com/setup_22.x | bash -".to_string(), + "apt-get update && apt-get install -y nodejs".to_string(), + ], + post_install_cmds_linux: vec![], + pre_install_cmds_macos: vec![], + post_install_cmds_macos: vec![], + pre_install_cmds_windows: vec![], + post_install_cmds_windows: vec![], + env_vars: HashMap::from([("DISPLAY".to_string(), ":99".to_string())]), + exec_cmd: "".to_string(), + }, + ); } fn register_system(&mut self) { - self.components.insert("system".to_string(), ComponentConfig { - name: "system".to_string(), - required: false, - ports: vec![8000], - dependencies: vec![], - linux_packages: vec!["wget".to_string(), "curl".to_string(), "unzip".to_string(), "git".to_string()], - macos_packages: vec![], - windows_packages: vec![], - download_url: None, - binary_name: None, - pre_install_cmds_linux: vec![], - post_install_cmds_linux: vec![], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "".to_string(), - }); + self.components.insert( + "system".to_string(), + ComponentConfig { + name: "system".to_string(), + required: false, + ports: vec![8000], + dependencies: vec![], + linux_packages: vec![ + "wget".to_string(), + "curl".to_string(), + "unzip".to_string(), + "git".to_string(), + ], + macos_packages: vec![], + windows_packages: vec![], + download_url: None, + binary_name: None, + pre_install_cmds_linux: vec![], + post_install_cmds_linux: vec![], + pre_install_cmds_macos: vec![], + post_install_cmds_macos: vec![], + pre_install_cmds_windows: vec![], + post_install_cmds_windows: vec![], + env_vars: HashMap::new(), + exec_cmd: "".to_string(), + }, + ); } fn register_vector_db(&mut self) { @@ -580,79 +617,104 @@ impl PackageManager { } fn register_host(&mut self) { - self.components.insert("host".to_string(), ComponentConfig { - name: "host".to_string(), - required: false, - ports: vec![], - dependencies: vec![], - linux_packages: vec!["sshfs".to_string(), "bridge-utils".to_string()], - macos_packages: vec![], - windows_packages: vec![], - download_url: None, - binary_name: None, - pre_install_cmds_linux: vec![ - "echo 'net.ipv4.ip_forward=1' | tee -a /etc/sysctl.conf".to_string(), - "sysctl -p".to_string(), - ], - post_install_cmds_linux: vec![ - "lxd init --auto".to_string(), - "lxc storage create default dir".to_string(), - "lxc profile device add default root disk path=/ pool=default".to_string(), - ], - pre_install_cmds_macos: vec![], - post_install_cmds_macos: vec![], - pre_install_cmds_windows: vec![], - post_install_cmds_windows: vec![], - env_vars: HashMap::new(), - exec_cmd: "".to_string(), - }); + self.components.insert( + "host".to_string(), + ComponentConfig { + name: "host".to_string(), + required: false, + ports: vec![], + dependencies: vec![], + linux_packages: vec!["sshfs".to_string(), "bridge-utils".to_string()], + macos_packages: vec![], + windows_packages: vec![], + download_url: None, + binary_name: None, + pre_install_cmds_linux: vec![ + "echo 'net.ipv4.ip_forward=1' | tee -a /etc/sysctl.conf".to_string(), + "sysctl -p".to_string(), + ], + post_install_cmds_linux: vec![ + "lxd init --auto".to_string(), + "lxc storage create default dir".to_string(), + "lxc profile device add default root disk path=/ pool=default".to_string(), + ], + pre_install_cmds_macos: vec![], + post_install_cmds_macos: vec![], + pre_install_cmds_windows: vec![], + post_install_cmds_windows: vec![], + env_vars: HashMap::new(), + exec_cmd: "".to_string(), + }, + ); } pub fn install(&self, component_name: &str) -> Result<()> { - let component = self.components.get(component_name).context(format!("Component '{}' not found", component_name))?; - - info!("Starting installation of component '{}' in {:?} mode", component_name, self.mode); - + let component = self + .components + .get(component_name) + .context(format!("Component '{}' not found", component_name))?; + + info!( + "Starting installation of component '{}' in {:?} mode", + component_name, self.mode + ); + for dep in &component.dependencies { if !self.is_installed(dep) { warn!("Installing missing dependency: {}", dep); self.install(dep)?; } } - + match self.mode { InstallMode::Local => self.install_local(component)?, InstallMode::Container => self.install_container(component)?, } - - info!("Component '{}' installation completed successfully", component_name); + + info!( + "Component '{}' installation completed successfully", + component_name + ); Ok(()) } fn install_local(&self, component: &ComponentConfig) -> Result<()> { - info!("Installing component '{}' locally to {}", component.name, self.base_path.display()); - + info!( + "Installing component '{}' locally to {}", + component.name, + self.base_path.display() + ); + self.create_directories(&component.name)?; - + let (pre_cmds, post_cmds) = match self.os_type { - OsType::Linux => (&component.pre_install_cmds_linux, &component.post_install_cmds_linux), - OsType::MacOS => (&component.pre_install_cmds_macos, &component.post_install_cmds_macos), - OsType::Windows => (&component.pre_install_cmds_windows, &component.post_install_cmds_windows), + OsType::Linux => ( + &component.pre_install_cmds_linux, + &component.post_install_cmds_linux, + ), + OsType::MacOS => ( + &component.pre_install_cmds_macos, + &component.post_install_cmds_macos, + ), + OsType::Windows => ( + &component.pre_install_cmds_windows, + &component.post_install_cmds_windows, + ), }; - + self.run_commands(pre_cmds, "local", &component.name)?; self.install_system_packages(component)?; - + if let Some(url) = &component.download_url { self.download_and_install(url, &component.name, component.binary_name.as_deref())?; } - + self.run_commands(post_cmds, "local", &component.name)?; - + if self.os_type == OsType::Linux && !component.exec_cmd.is_empty() { self.create_service_file(&component.name, &component.exec_cmd, &component.env_vars)?; } - + info!("Local installation of '{}' completed", component.name); Ok(()) } @@ -660,104 +722,157 @@ impl PackageManager { fn install_container(&self, component: &ComponentConfig) -> Result<()> { let container_name = format!("{}-{}", self.tenant, component.name); info!("Creating LXC container: {}", container_name); - - let output = Command::new("lxc").args(&["launch", "images:debian/12", &container_name, "-c", "security.privileged=true"]).output()?; - + + let output = Command::new("lxc") + .args(&[ + "launch", + "images:debian/12", + &container_name, + "-c", + "security.privileged=true", + ]) + .output()?; + if !output.status.success() { - return Err(anyhow::anyhow!("LXC container creation failed: {}", String::from_utf8_lossy(&output.stderr))); + return Err(anyhow::anyhow!( + "LXC container creation failed: {}", + String::from_utf8_lossy(&output.stderr) + )); } - + std::thread::sleep(std::time::Duration::from_secs(15)); - + self.exec_in_container(&container_name, "mkdir -p /opt/gbo/{bin,data,conf,logs}")?; - + let (pre_cmds, post_cmds) = match self.os_type { - OsType::Linux => (&component.pre_install_cmds_linux, &component.post_install_cmds_linux), - OsType::MacOS => (&component.pre_install_cmds_macos, &component.post_install_cmds_macos), - OsType::Windows => (&component.pre_install_cmds_windows, &component.post_install_cmds_windows), + OsType::Linux => ( + &component.pre_install_cmds_linux, + &component.post_install_cmds_linux, + ), + OsType::MacOS => ( + &component.pre_install_cmds_macos, + &component.post_install_cmds_macos, + ), + OsType::Windows => ( + &component.pre_install_cmds_windows, + &component.post_install_cmds_windows, + ), }; - + self.run_commands(pre_cmds, &container_name, &component.name)?; - + let packages = match self.os_type { OsType::Linux => &component.linux_packages, OsType::MacOS => &component.macos_packages, OsType::Windows => &component.windows_packages, }; - + if !packages.is_empty() { let pkg_list = packages.join(" "); - self.exec_in_container(&container_name, &format!("apt-get update && apt-get install -y {}", pkg_list))?; + self.exec_in_container( + &container_name, + &format!("apt-get update && apt-get install -y {}", pkg_list), + )?; } - + if let Some(url) = &component.download_url { - self.download_in_container(&container_name, url, &component.name, component.binary_name.as_deref())?; + self.download_in_container( + &container_name, + url, + &component.name, + component.binary_name.as_deref(), + )?; } - + self.run_commands(post_cmds, &container_name, &component.name)?; - - self.exec_in_container(&container_name, "useradd --system --no-create-home --shell /bin/false gbuser")?; + + self.exec_in_container( + &container_name, + "useradd --system --no-create-home --shell /bin/false gbuser", + )?; self.mount_container_directories(&container_name, &component.name)?; - + if !component.exec_cmd.is_empty() { - self.create_container_service(&container_name, &component.name, &component.exec_cmd, &component.env_vars)?; + self.create_container_service( + &container_name, + &component.name, + &component.exec_cmd, + &component.env_vars, + )?; } - + self.setup_port_forwarding(&container_name, &component.ports)?; - - info!("Container installation of '{}' completed in {}", component.name, container_name); + + info!( + "Container installation of '{}' completed in {}", + component.name, container_name + ); Ok(()) } pub fn remove(&self, component_name: &str) -> Result<()> { - let component = self.components.get(component_name).context(format!("Component '{}' not found", component_name))?; - + let component = self + .components + .get(component_name) + .context(format!("Component '{}' not found", component_name))?; + info!("Removing component: {}", component_name); - + match self.mode { InstallMode::Local => self.remove_local(component)?, InstallMode::Container => self.remove_container(component)?, } - + info!("Component '{}' removed successfully", component_name); Ok(()) } fn remove_local(&self, component: &ComponentConfig) -> Result<()> { + if component.name == "tables" { + // Stop PostgreSQL if running + let bin_path = self.base_path.join("bin").join(&component.name); + let data_path = self.base_path.join("data").join(&component.name); + + let _ = Command::new(bin_path.join("pg_ctl")) + .args(&["-D", data_path.join("pgdata").to_str().unwrap(), "stop"]) + .output(); + } + if self.os_type == OsType::Linux { - let _ = Command::new("systemctl").args(&["stop", &format!("{}.service", component.name)]).output(); - let _ = Command::new("systemctl").args(&["disable", &format!("{}.service", component.name)]).output(); + let _ = Command::new("systemctl") + .args(&["stop", &format!("{}.service", component.name)]) + .output(); + let _ = Command::new("systemctl") + .args(&["disable", &format!("{}.service", component.name)]) + .output(); let service_path = format!("/etc/systemd/system/{}.service", component.name); let _ = std::fs::remove_file(service_path); let _ = Command::new("systemctl").args(&["daemon-reload"]).output(); } - + let bin_path = self.base_path.join("bin").join(&component.name); - let data_path = self.base_path.join("data").join(&component.name); - let conf_path = self.base_path.join("conf").join(&component.name); - let logs_path = self.base_path.join("logs").join(&component.name); - let _ = std::fs::remove_dir_all(bin_path); - let _ = std::fs::remove_dir_all(data_path); - let _ = std::fs::remove_dir_all(conf_path); - let _ = std::fs::remove_dir_all(logs_path); - + Ok(()) } fn remove_container(&self, component: &ComponentConfig) -> Result<()> { let container_name = format!("{}-{}", self.tenant, component.name); - - let _ = Command::new("lxc").args(&["stop", &container_name]).output(); - let output = Command::new("lxc").args(&["delete", &container_name]).output()?; - + + let _ = Command::new("lxc") + .args(&["stop", &container_name]) + .output(); + let output = Command::new("lxc") + .args(&["delete", &container_name]) + .output()?; + if !output.status.success() { - warn!("Container deletion had issues: {}", String::from_utf8_lossy(&output.stderr)); + warn!( + "Container deletion had issues: {}", + String::from_utf8_lossy(&output.stderr) + ); } - - let host_base = format!("/opt/gbo/tenants/{}/{}", self.tenant, component.name); - let _ = std::fs::remove_dir_all(host_base); - + Ok(()) } @@ -773,7 +888,18 @@ impl PackageManager { } InstallMode::Container => { let container_name = format!("{}-{}", self.tenant, component_name); - Command::new("lxc").args(&["list", &container_name, "--format=json"]).output().map(|o| o.status.success()).unwrap_or(false) + let output = Command::new("lxc") + .args(&["list", &container_name, "--format=json"]) + .output() + .unwrap(); + + if !output.status.success() { + return false; + } + + // Parse JSON output to check if container exists and is running + let output_str = String::from_utf8_lossy(&output.stdout); + !output_str.contains("\"name\":\"") || output_str.contains("\"status\":\"Stopped\"") } } } @@ -782,7 +908,8 @@ impl PackageManager { let dirs = ["bin", "data", "conf", "logs"]; for dir in &dirs { let path = self.base_path.join(dir).join(component); - std::fs::create_dir_all(&path).context(format!("Failed to create directory: {:?}", path))?; + std::fs::create_dir_all(&path) + .context(format!("Failed to create directory: {:?}", path))?; trace!("Created directory: {:?}", path); } Ok(()) @@ -794,22 +921,38 @@ impl PackageManager { OsType::MacOS => &component.macos_packages, OsType::Windows => &component.windows_packages, }; - + if packages.is_empty() { return Ok(()); } - - info!("Installing {} system packages for component '{}'", packages.len(), component.name); - + + info!( + "Installing {} system packages for component '{}'", + packages.len(), + component.name + ); + match self.os_type { OsType::Linux => { - let output = Command::new("apt-get").args(&["install", "-y"]).args(packages).output()?; + let output = Command::new("apt-get").args(&["update"]).output()?; + + if !output.status.success() { + warn!("apt-get update had issues"); + } + + let output = Command::new("apt-get") + .args(&["install", "-y"]) + .args(packages) + .output()?; if !output.status.success() { warn!("Some packages may have failed to install"); } } OsType::MacOS => { - let output = Command::new("brew").args(&["install"]).args(packages).output()?; + let output = Command::new("brew") + .args(&["install"]) + .args(packages) + .output()?; if !output.status.success() { warn!("Homebrew installation had warnings"); } @@ -818,28 +961,65 @@ impl PackageManager { warn!("Windows package installation not implemented"); } } - + Ok(()) } - fn download_and_install(&self, url: &str, component: &str, binary_name: Option<&str>) -> Result<()> { + fn download_and_install( + &self, + url: &str, + component: &str, + binary_name: Option<&str>, + ) -> Result<()> { let bin_path = self.base_path.join("bin").join(component); - let temp_file = bin_path.join("download.tmp"); - - info!("Downloading from: {}", url); - let output = Command::new("wget").args(&["-O", temp_file.to_str().unwrap(), url]).output()?; - + + // Ensure the bin directory exists + std::fs::create_dir_all(&bin_path)?; + + let filename = url.split('/').last().unwrap_or("download.tmp"); + let temp_file = bin_path.join(filename); + + info!("Downloading from: {} to {:?}", url, temp_file); + + // Download to the component's bin directory + let output = Command::new("wget") + .current_dir(&bin_path) + .args(&["-O", temp_file.to_str().unwrap(), url]) + .output()?; + if !output.status.success() { return Err(anyhow::anyhow!("Download failed from URL: {}", url)); } - + if url.ends_with(".tar.gz") || url.ends_with(".tgz") { trace!("Extracting tar.gz archive to {:?}", bin_path); - Command::new("tar").args(&["-xzf", temp_file.to_str().unwrap(), "-C", bin_path.to_str().unwrap()]).output()?; + let output = Command::new("tar") + .current_dir(&bin_path) + .args(&["-xzf", temp_file.to_str().unwrap()]) + .output()?; + + if !output.status.success() { + return Err(anyhow::anyhow!( + "Extraction failed: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + + // Clean up the downloaded archive std::fs::remove_file(&temp_file)?; } else if url.ends_with(".zip") { trace!("Extracting zip archive to {:?}", bin_path); - Command::new("unzip").args(&[temp_file.to_str().unwrap(), "-d", bin_path.to_str().unwrap()]).output()?; + let output = Command::new("unzip") + .current_dir(&bin_path) + .args(&["-o", temp_file.to_str().unwrap()]) + .output()?; + + if !output.status.success() { + return Err(anyhow::anyhow!( + "Extraction failed: {}", + String::from_utf8_lossy(&output.stderr) + )); + } std::fs::remove_file(&temp_file)?; } else if let Some(name) = binary_name { let final_path = bin_path.join(name); @@ -852,80 +1032,111 @@ impl PackageManager { std::fs::set_permissions(&final_path, perms)?; } } - + Ok(()) } - fn create_service_file(&self, component: &str, exec_cmd: &str, env_vars: &HashMap) -> Result<()> { + fn create_service_file( + &self, + component: &str, + exec_cmd: &str, + env_vars: &HashMap, + ) -> Result<()> { let service_path = format!("/etc/systemd/system/{}.service", component); - - let bin_path = self.base_path.join("bin").join(component).to_string_lossy().to_string(); - let data_path = self.base_path.join("data").join(component).to_string_lossy().to_string(); - let conf_path = self.base_path.join("conf").join(component).to_string_lossy().to_string(); - let logs_path = self.base_path.join("logs").join(component).to_string_lossy().to_string(); - + + let bin_path = self.base_path.join("bin").join(component); + let data_path = self.base_path.join("data").join(component); + let conf_path = self.base_path.join("conf").join(component); + let logs_path = self.base_path.join("logs").join(component); + + // Ensure all directories exist + std::fs::create_dir_all(&bin_path)?; + std::fs::create_dir_all(&data_path)?; + std::fs::create_dir_all(&conf_path)?; + std::fs::create_dir_all(&logs_path)?; + let rendered_cmd = exec_cmd - .replace("{{BIN_PATH}}", &bin_path) - .replace("{{DATA_PATH}}", &data_path) - .replace("{{CONF_PATH}}", &conf_path) - .replace("{{LOGS_PATH}}", &logs_path); - + .replace("{{BIN_PATH}}", &bin_path.to_string_lossy()) + .replace("{{DATA_PATH}}", &data_path.to_string_lossy()) + .replace("{{CONF_PATH}}", &conf_path.to_string_lossy()) + .replace("{{LOGS_PATH}}", &logs_path.to_string_lossy()); + let mut env_section = String::new(); for (key, value) in env_vars { - let rendered_value = value.replace("{{DATA_PATH}}", &data_path); - env_section.push_str(&format!("Environment=\"{}={}\"\n", key, rendered_value)); + let rendered_value = value + .replace("{{DATA_PATH}}", &data_path.to_string_lossy()) + .replace("{{BIN_PATH}}", &bin_path.to_string_lossy()) + .replace("{{CONF_PATH}}", &conf_path.to_string_lossy()) + .replace("{{LOGS_PATH}}", &logs_path.to_string_lossy()); + env_section.push_str(&format!("Environment={}={}\n", key, rendered_value)); } - - let service_content = format!("[Unit]\nDescription={} Service\nAfter=network.target\n\n[Service]\nType=simple\n{}ExecStart={}\nWorkingDirectory={}\nRestart=always\nRestartSec=10\nUser=root\n\n[Install]\nWantedBy=multi-user.target\n", component, env_section, rendered_cmd, data_path); - + + let service_content = format!( + "[Unit]\nDescription={} Service\nAfter=network.target\n\n[Service]\nType=simple\n{}ExecStart={}\nWorkingDirectory={}\nRestart=always\nRestartSec=10\nUser=root\n\n[Install]\nWantedBy=multi-user.target\n", + component, env_section, rendered_cmd, data_path.to_string_lossy() + ); + std::fs::write(&service_path, service_content)?; - - Command::new("systemctl").args(&["daemon-reload"]).output()?; - Command::new("systemctl").args(&["enable", &format!("{}.service", component)]).output()?; - Command::new("systemctl").args(&["start", &format!("{}.service", component)]).output()?; - + + Command::new("systemctl") + .args(&["daemon-reload"]) + .output()?; + Command::new("systemctl") + .args(&["enable", &format!("{}.service", component)]) + .output()?; + Command::new("systemctl") + .args(&["start", &format!("{}.service", component)]) + .output()?; + info!("Created and started systemd service: {}.service", component); Ok(()) } fn run_commands(&self, commands: &[String], target: &str, component: &str) -> Result<()> { + let bin_path = if target == "local" { + self.base_path.join("bin").join(component) + } else { + PathBuf::from("/opt/gbo/bin") + }; + + let data_path = if target == "local" { + self.base_path.join("data").join(component) + } else { + PathBuf::from("/opt/gbo/data") + }; + + let conf_path = if target == "local" { + self.base_path.join("conf").join(component) + } else { + PathBuf::from("/opt/gbo/conf") + }; + + let logs_path = if target == "local" { + self.base_path.join("logs").join(component) + } else { + PathBuf::from("/opt/gbo/logs") + }; + for cmd in commands { - let bin_path = if target == "local" { - self.base_path.join("bin").join(component).to_string_lossy().to_string() - } else { - "/opt/gbo/bin".to_string() - }; - - let data_path = if target == "local" { - self.base_path.join("data").join(component).to_string_lossy().to_string() - } else { - "/opt/gbo/data".to_string() - }; - - let conf_path = if target == "local" { - self.base_path.join("conf").join(component).to_string_lossy().to_string() - } else { - "/opt/gbo/conf".to_string() - }; - - let logs_path = if target == "local" { - self.base_path.join("logs").join(component).to_string_lossy().to_string() - } else { - "/opt/gbo/logs".to_string() - }; - let rendered_cmd = cmd - .replace("{{BIN_PATH}}", &bin_path) - .replace("{{DATA_PATH}}", &data_path) - .replace("{{CONF_PATH}}", &conf_path) - .replace("{{LOGS_PATH}}", &logs_path); - + .replace("{{BIN_PATH}}", &bin_path.to_string_lossy()) + .replace("{{DATA_PATH}}", &data_path.to_string_lossy()) + .replace("{{CONF_PATH}}", &conf_path.to_string_lossy()) + .replace("{{LOGS_PATH}}", &logs_path.to_string_lossy()); + trace!("Executing command: {}", rendered_cmd); - + if target == "local" { - let output = Command::new("bash").args(&["-c", &rendered_cmd]).output()?; + // Run commands in the component's bin directory + let output = Command::new("bash") + .current_dir(&bin_path) + .args(&["-c", &rendered_cmd]) + .output()?; if !output.status.success() { - warn!("Command had non-zero exit: {}", String::from_utf8_lossy(&output.stderr)); + warn!( + "Command had non-zero exit: {}", + String::from_utf8_lossy(&output.stderr) + ); } } else { self.exec_in_container(target, &rendered_cmd)?; @@ -936,89 +1147,173 @@ impl PackageManager { fn exec_in_container(&self, container: &str, command: &str) -> Result<()> { debug!("Executing in container {}: {}", container, command); - let output = Command::new("lxc").args(&["exec", container, "--", "bash", "-c", command]).output()?; - + let output = Command::new("lxc") + .args(&["exec", container, "--", "bash", "-c", command]) + .output()?; + if !output.status.success() { - warn!("Container command failed: {}", String::from_utf8_lossy(&output.stderr)); + warn!( + "Container command failed: {}", + String::from_utf8_lossy(&output.stderr) + ); } Ok(()) } - fn download_in_container(&self, container: &str, url: &str, _component: &str, binary_name: Option<&str>) -> Result<()> { + fn download_in_container( + &self, + container: &str, + url: &str, + component: &str, + binary_name: Option<&str>, + ) -> Result<()> { let download_cmd = format!("wget -O /tmp/download.tmp {}", url); self.exec_in_container(container, &download_cmd)?; - + if url.ends_with(".tar.gz") || url.ends_with(".tgz") { self.exec_in_container(container, "tar -xzf /tmp/download.tmp -C /opt/gbo/bin")?; } else if url.ends_with(".zip") { - self.exec_in_container(container, "unzip /tmp/download.tmp -d /opt/gbo/bin")?; + self.exec_in_container(container, "unzip -o /tmp/download.tmp -d /opt/gbo/bin")?; } else if let Some(name) = binary_name { - let mv_cmd = format!("mv /tmp/download.tmp /opt/gbo/bin/{} && chmod +x /opt/gbo/bin/{}", name, name); + let mv_cmd = format!( + "mv /tmp/download.tmp /opt/gbo/bin/{} && chmod +x /opt/gbo/bin/{}", + name, name + ); self.exec_in_container(container, &mv_cmd)?; } - + self.exec_in_container(container, "rm -f /tmp/download.tmp")?; Ok(()) } fn mount_container_directories(&self, container: &str, component: &str) -> Result<()> { let host_base = format!("/opt/gbo/tenants/{}/{}", self.tenant, component); - + for dir in &["data", "conf", "logs"] { let host_path = format!("{}/{}", host_base, dir); std::fs::create_dir_all(&host_path)?; - - let device_name = format!("{}{}", component, dir); + + let device_name = format!("{}-{}", component, dir); let container_path = format!("/opt/gbo/{}", dir); - - let _ = Command::new("lxc").args(&["config", "device", "remove", container, &device_name]).output(); - - Command::new("lxc").args(&["config", "device", "add", container, &device_name, "disk", &format!("source={}", host_path), &format!("path={}", container_path)]).output()?; - - trace!("Mounted {} to {} in container {}", host_path, container_path, container); + + let _ = Command::new("lxc") + .args(&["config", "device", "remove", container, &device_name]) + .output(); + + let output = Command::new("lxc") + .args(&[ + "config", + "device", + "add", + container, + &device_name, + "disk", + &format!("source={}", host_path), + &format!("path={}", container_path), + ]) + .output()?; + + if !output.status.success() { + warn!("Failed to mount {} in container {}", dir, container); + } + + trace!( + "Mounted {} to {} in container {}", + host_path, + container_path, + container + ); } Ok(()) } - fn create_container_service(&self, container: &str, component: &str, exec_cmd: &str, env_vars: &HashMap) -> Result<()> { + fn create_container_service( + &self, + container: &str, + component: &str, + exec_cmd: &str, + env_vars: &HashMap, + ) -> Result<()> { let rendered_cmd = exec_cmd .replace("{{BIN_PATH}}", "/opt/gbo/bin") .replace("{{DATA_PATH}}", "/opt/gbo/data") .replace("{{CONF_PATH}}", "/opt/gbo/conf") .replace("{{LOGS_PATH}}", "/opt/gbo/logs"); - + let mut env_section = String::new(); for (key, value) in env_vars { - let rendered_value = value.replace("{{DATA_PATH}}", "/opt/gbo/data"); - env_section.push_str(&format!("Environment=\"{}={}\"\n", key, rendered_value)); + let rendered_value = value + .replace("{{DATA_PATH}}", "/opt/gbo/data") + .replace("{{BIN_PATH}}", "/opt/gbo/bin") + .replace("{{CONF_PATH}}", "/opt/gbo/conf") + .replace("{{LOGS_PATH}}", "/opt/gbo/logs"); + env_section.push_str(&format!("Environment={}={}\n", key, rendered_value)); } - - let service_content = format!("[Unit]\nDescription={} Service\nAfter=network.target\n\n[Service]\nType=simple\n{}ExecStart={}\nWorkingDirectory=/opt/gbo/data\nRestart=always\nRestartSec=10\nUser=root\n\n[Install]\nWantedBy=multi-user.target\n", component, env_section, rendered_cmd); - + + let service_content = format!( + "[Unit]\nDescription={} Service\nAfter=network.target\n\n[Service]\nType=simple\n{}ExecStart={}\nWorkingDirectory=/opt/gbo/data\nRestart=always\nRestartSec=10\nUser=root\n\n[Install]\nWantedBy=multi-user.target\n", + component, env_section, rendered_cmd + ); + let service_file = format!("/tmp/{}.service", component); std::fs::write(&service_file, &service_content)?; - - Command::new("lxc").args(&["file", "push", &service_file, &format!("{}/etc/systemd/system/{}.service", container, component)]).output()?; - + + let output = Command::new("lxc") + .args(&[ + "file", + "push", + &service_file, + &format!("{}/etc/systemd/system/{}.service", container, component), + ]) + .output()?; + + if !output.status.success() { + warn!("Failed to push service file to container"); + } + self.exec_in_container(container, "systemctl daemon-reload")?; self.exec_in_container(container, &format!("systemctl enable {}", component))?; self.exec_in_container(container, &format!("systemctl start {}", component))?; - + std::fs::remove_file(&service_file)?; - - info!("Created and started service in container {}: {}", container, component); + + info!( + "Created and started service in container {}: {}", + container, component + ); Ok(()) } fn setup_port_forwarding(&self, container: &str, ports: &[u16]) -> Result<()> { for port in ports { let device_name = format!("port-{}", port); - - let _ = Command::new("lxc").args(&["config", "device", "remove", container, &device_name]).output(); - - Command::new("lxc").args(&["config", "device", "add", container, &device_name, "proxy", &format!("listen=tcp:0.0.0.0:{}", port), &format!("connect=tcp:127.0.0.1:{}", port)]).output()?; - - trace!("Port forwarding configured: {} -> container {}", port, container); + + let _ = Command::new("lxc") + .args(&["config", "device", "remove", container, &device_name]) + .output(); + + let output = Command::new("lxc") + .args(&[ + "config", + "device", + "add", + container, + &device_name, + "proxy", + &format!("listen=tcp:0.0.0.0:{}", port), + &format!("connect=tcp:127.0.0.1:{}", port), + ]) + .output()?; + + if !output.status.success() { + warn!("Failed to setup port forwarding for port {}", port); + } + + trace!( + "Port forwarding configured: {} -> container {}", + port, + container + ); } Ok(()) } @@ -1031,64 +1326,106 @@ pub mod cli { pub fn run() -> Result<()> { env_logger::init(); let args: Vec = env::args().collect(); - + if args.len() < 2 { print_usage(); return Ok(()); } - + let command = &args[1]; - + match command.as_str() { "install" => { if args.len() < 3 { - eprintln!("Usage: botserver install [--container] [--tenant ]"); + eprintln!( + "Usage: botserver install [--container] [--tenant ]" + ); return Ok(()); } - + let component = &args[2]; - let mode = if args.contains(&"--container".to_string()) { InstallMode::Container } else { InstallMode::Local }; - let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { args.get(idx + 1).cloned() } else { None }; - + let mode = if args.contains(&"--container".to_string()) { + InstallMode::Container + } else { + InstallMode::Local + }; + let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { + args.get(idx + 1).cloned() + } else { + None + }; + let pm = PackageManager::new(mode, tenant)?; pm.install(component)?; println!("✓ Component '{}' installed successfully", component); } "remove" => { if args.len() < 3 { - eprintln!("Usage: botserver remove [--container] [--tenant ]"); + eprintln!( + "Usage: botserver remove [--container] [--tenant ]" + ); return Ok(()); } - + let component = &args[2]; - let mode = if args.contains(&"--container".to_string()) { InstallMode::Container } else { InstallMode::Local }; - let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { args.get(idx + 1).cloned() } else { None }; - + let mode = if args.contains(&"--container".to_string()) { + InstallMode::Container + } else { + InstallMode::Local + }; + let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { + args.get(idx + 1).cloned() + } else { + None + }; + let pm = PackageManager::new(mode, tenant)?; pm.remove(component)?; println!("✓ Component '{}' removed successfully", component); } "list" => { - let mode = if args.contains(&"--container".to_string()) { InstallMode::Container } else { InstallMode::Local }; - let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { args.get(idx + 1).cloned() } else { None }; - + let mode = if args.contains(&"--container".to_string()) { + InstallMode::Container + } else { + InstallMode::Local + }; + let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { + args.get(idx + 1).cloned() + } else { + None + }; + let pm = PackageManager::new(mode, tenant)?; println!("Available components:"); for component in pm.list() { - let status = if pm.is_installed(&component) { "✓ installed" } else { " available" }; + let status = if pm.is_installed(&component) { + "✓ installed" + } else { + " available" + }; println!(" {} {}", status, component); } } "status" => { if args.len() < 3 { - eprintln!("Usage: botserver status [--container] [--tenant ]"); + eprintln!( + "Usage: botserver status [--container] [--tenant ]" + ); return Ok(()); } - + let component = &args[2]; - let mode = if args.contains(&"--container".to_string()) { InstallMode::Container } else { InstallMode::Local }; - let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { args.get(idx + 1).cloned() } else { None }; - + let mode = if args.contains(&"--container".to_string()) { + InstallMode::Container + } else { + InstallMode::Local + }; + let tenant = if let Some(idx) = args.iter().position(|a| a == "--tenant") { + args.get(idx + 1).cloned() + } else { + None + }; + let pm = PackageManager::new(mode, tenant)?; if pm.is_installed(component) { println!("✓ Component '{}' is installed", component); @@ -1104,7 +1441,7 @@ pub mod cli { print_usage(); } } - + Ok(()) }