diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index eceb10d..e70ccad 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -31,6 +31,26 @@ pub async fn prepare_services_start(services: Vec) -> Result<(), St println!("Preparing start scripts for {} services", services.len()); for service in services { + let node_setup = if let Some(node_version) = &service.node_version { + format!( + "# Load NVM and use specific Node version +export NVM_DIR=\"$HOME/.nvm\" +[ -s \"$NVM_DIR/nvm.sh\" ] && source \"$NVM_DIR/nvm.sh\" +[ -s \"$NVM_DIR/bash_completion\" ] && source \"$NVM_DIR/bash_completion\" +if command -v nvm >/dev/null 2>&1; then + echo \"Using Node.js version: {}\" + nvm use {} 2>/dev/null || nvm install {} 2>/dev/null || echo \"Warning: Could not switch to Node.js version {}\" +else + echo \"NVM not found, using system Node.js\" +fi +echo \"Current Node.js version: $(node --version 2>/dev/null || echo 'N/A')\" +", + node_version, node_version, node_version, node_version + ) + } else { + "# Using default Node.js version\necho \"Current Node.js version: $(node --version 2>/dev/null || echo 'N/A')\"\n".to_string() + }; + let script = format!( r#"#!/bin/bash # Justo Runner V4.1 @@ -38,6 +58,8 @@ pub async fn prepare_services_start(services: Vec) -> Result<(), St # Source shell configuration to access yarn source ~/.zshrc 2>/dev/null || true +{} + export LOCAL_NETWORK_NAME=host.docker.internal export MONGO_URL=mongodb://host.docker.internal:3003/{} export KAFKA_BROKERS=host.docker.internal:30092 @@ -85,8 +107,9 @@ else echo "No process running on port $PORT" fi -exec sh start.sh +exec bash start.sh "#, + node_setup, service.config.get("dbName") .and_then(|v| v.as_str()) .unwrap_or(&service.name), @@ -99,6 +122,18 @@ exec sh start.sh let script_path = format!("{}/.start.run.sh", service.path); fs::write(&script_path, script) .map_err(|e| format!("Failed to write start script for {}: {}", service.name, e))?; + + // Make the script executable + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&script_path) + .map_err(|e| format!("Failed to get metadata for {}: {}", script_path, e))? + .permissions(); + perms.set_mode(0o755); // rwxr-xr-x + fs::set_permissions(&script_path, perms) + .map_err(|e| format!("Failed to set permissions for {}: {}", script_path, e))?; + } println!("Created start script for service: {}", service.name); } @@ -222,4 +257,44 @@ pub async fn clear_service_output(app_handle: AppHandle, service_name: String) - processes.clear_output(&service_name); Ok(()) +} + +/// Get available Node.js versions from NVM +#[tauri::command] +pub async fn get_available_node_versions() -> Result, String> { + use std::process::Command; + + // First try to get versions from NVM + let output = Command::new("zsh") + .arg("-c") + .arg("source ~/.zshrc 2>/dev/null; export NVM_DIR=\"$HOME/.nvm\"; [ -s \"$NVM_DIR/nvm.sh\" ] && source \"$NVM_DIR/nvm.sh\"; nvm list 2>/dev/null | grep -o 'v[0-9]*\\.[0-9]*\\.[0-9]*' | sed 's/^v//' | sort -V") + .output(); + + let mut versions = Vec::new(); + + if let Ok(output) = output { + if output.status.success() { + let output_str = String::from_utf8_lossy(&output.stdout); + for line in output_str.lines() { + let version = line.trim(); + if !version.is_empty() { + versions.push(version.to_string()); + } + } + } + } + + // If no versions found from NVM, add some common versions + if versions.is_empty() { + versions = vec![ + "16.20.0".to_string(), + "18.17.0".to_string(), + "18.18.0".to_string(), + "20.10.0".to_string(), + "20.11.0".to_string(), + "21.0.0".to_string(), + ]; + } + + Ok(versions) } \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d8beed2..a54a72b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -35,7 +35,8 @@ pub fn run() { ensure_services_running, stop_all_services_command, get_service_output, - clear_service_output + clear_service_output, + get_available_node_versions ]) .build(tauri::generate_context!()) .expect("error while running tauri application") diff --git a/src-tauri/src/services.rs b/src-tauri/src/services.rs index 9be3628..6d51f80 100644 --- a/src-tauri/src/services.rs +++ b/src-tauri/src/services.rs @@ -58,6 +58,16 @@ pub async fn get_services_in_services(settings: &AppSettings) -> Result Result { @@ -109,6 +120,9 @@ pub fn get_services_in_justo(settings: &AppSettings) -> Vec { category: "justo".to_string(), config: HashMap::new(), start_command: "sh start.sh".to_string(), + node_version: settings.node_versions + .as_ref() + .and_then(|node_map| node_map.get("justo.main").cloned()), }, ServiceData { name: "web".to_string(), @@ -122,6 +136,9 @@ pub fn get_services_in_justo(settings: &AppSettings) -> Vec { category: "justo".to_string(), config: HashMap::new(), start_command: "yarn start".to_string(), + node_version: settings.node_versions + .as_ref() + .and_then(|node_map| node_map.get("justo.web").cloned()), }, ] } @@ -147,6 +164,9 @@ pub fn get_services_in_delivery(settings: &AppSettings) -> Vec { category: "delivery".to_string(), config: HashMap::new(), start_command: "sh start.sh".to_string(), + node_version: settings.node_versions + .as_ref() + .and_then(|node_map| node_map.get("delivery.main").cloned()), }, ServiceData { name: "web".to_string(), @@ -160,6 +180,9 @@ pub fn get_services_in_delivery(settings: &AppSettings) -> Vec { category: "delivery".to_string(), config: HashMap::new(), start_command: "yarn start".to_string(), + node_version: settings.node_versions + .as_ref() + .and_then(|node_map| node_map.get("delivery.web").cloned()), }, ] } @@ -175,18 +198,19 @@ pub async fn start_service_with_output_capture(service: &ServiceData, processes_ // Determine the command to run based on the service category let (command, args) = match service.category.as_str() { "services" => { - // For services, run the .start.run.sh script + // For services, run the .start.run.sh script directly (it has #!/bin/bash shebang) let script_path = format!("{}/.start.run.sh", service.path); if !Path::new(&script_path).exists() { return Err(format!("Start script not found: {}", script_path)); } - ("sh", vec![".start.run.sh".to_string()]) + // Make sure the script is executable and run it directly + ("./.start.run.sh", vec![]) } "justo" | "delivery" => { // For justo and delivery, run the start command directly if service.start_command.starts_with("sh ") { let script_name = service.start_command.strip_prefix("sh ").unwrap_or("start.sh"); - ("sh", vec![script_name.to_string()]) + ("bash", vec![script_name.to_string()]) } else if service.start_command.starts_with("yarn ") { let yarn_command = service.start_command.strip_prefix("yarn ").unwrap_or("start"); ("yarn", vec![yarn_command.to_string()]) @@ -203,7 +227,11 @@ pub async fn start_service_with_output_capture(service: &ServiceData, processes_ { let mut processes = processes_state.lock().unwrap(); processes.clear_output(&service.full_name); - processes.add_output(&service.full_name, &format!("Starting {} service...\n", service.name)); + processes.add_output(&service.full_name, &format!("šŸš€ Starting {} service...\n", service.name)); + processes.add_output(&service.full_name, &format!("Directory: {}\n", service.path)); + processes.add_output(&service.full_name, &format!("Command: {} {}\n", command, args.join(" "))); + processes.add_output(&service.full_name, &format!("Port: {}\n", service.port)); + processes.add_output(&service.full_name, "Loading environment...\n"); } // Build the command string @@ -214,17 +242,28 @@ pub async fn start_service_with_output_capture(service: &ServiceData, processes_ }; // Start with tokio::process using user's login shell to load environment - // Source shell configuration files for the built app + // Source multiple shell configuration files for better compatibility and ensure we stay in the correct directory + let node_setup = if let Some(node_version) = &service.node_version { + format!( + "export NVM_DIR=\"$HOME/.nvm\"; [ -s \"$NVM_DIR/nvm.sh\" ] && . \"$NVM_DIR/nvm.sh\"; [ -s \"$NVM_DIR/bash_completion\" ] && . \"$NVM_DIR/bash_completion\"; if command -v nvm >/dev/null 2>&1; then echo 'Using Node.js version: {}'; nvm use {} 2>/dev/null || nvm install {} 2>/dev/null || echo 'Warning: Could not switch to Node.js version {}'; else echo 'NVM not found, using system Node.js'; fi", + node_version, node_version, node_version, node_version + ) + } else { + "echo 'Using default Node.js version'".to_string() + }; + + // Execute with bash login shell to properly inherit environment let shell_command = format!( - "source ~/.zshrc 2>/dev/null || true; {}", - command_string + "cd '{}' && {}", + service.path, command_string ); let mut tokio_child = TokioCommand::new("/bin/zsh") - .arg("-l") // Login shell - loads user environment + .arg("-l") // Login shell - loads user environment from .zshrc .arg("-c") .arg(&shell_command) .current_dir(&service.path) + .env("HOME", std::env::var("HOME").unwrap_or_else(|_| "/Users".to_string())) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -273,7 +312,20 @@ pub async fn start_service_with_output_capture(service: &ServiceData, processes_ match tokio_child.wait().await { Ok(exit_status) => { if let Ok(mut processes) = processes_state_clone.lock() { - processes.add_output(&service_name_clone, &format!("\nProcess exited with status: {}\n", exit_status)); + if exit_status.success() { + processes.add_output(&service_name_clone, &format!("\nāœ… Process completed successfully: {}\n", exit_status)); + } else { + processes.add_output(&service_name_clone, &format!("\nāŒ Process failed with exit status: {}\n", exit_status)); + if let Some(code) = exit_status.code() { + processes.add_output(&service_name_clone, &format!("Exit code: {}\n", code)); + match code { + 1 => processes.add_output(&service_name_clone, "Common causes: Missing dependencies, syntax errors, or permission issues\n"), + 127 => processes.add_output(&service_name_clone, "Command not found - check if the required tools (node, yarn, etc.) are installed\n"), + 130 => processes.add_output(&service_name_clone, "Process interrupted (Ctrl+C)\n"), + _ => processes.add_output(&service_name_clone, &format!("See above logs for details about exit code {}\n", code)), + } + } + } } } Err(e) => { diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 103cfdd..7377f5f 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -46,6 +46,7 @@ pub struct ServiceData { pub category: String, // "services", "justo", "delivery" pub config: HashMap, pub start_command: String, + pub node_version: Option, // Node.js version to use (e.g., "18.17.0", "20.10.0") } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -58,6 +59,8 @@ pub struct AppSettings { pub delivery_path: Option, #[serde(rename = "onServices")] pub on_services: Option>, + #[serde(rename = "nodeVersions")] + pub node_versions: Option>, // service.full_name -> node version } #[derive(Debug, Serialize)] diff --git a/src/components/NodeVersionSelector/index.tsx b/src/components/NodeVersionSelector/index.tsx new file mode 100644 index 0000000..93a5dfb --- /dev/null +++ b/src/components/NodeVersionSelector/index.tsx @@ -0,0 +1,154 @@ +import { useState, useEffect } from 'react' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Settings, Check, X } from 'lucide-react' +import { toast } from 'sonner' +import { invoke } from '@tauri-apps/api/core' + +interface NodeVersionSelectorProps { + currentVersion?: string + serviceName: string + category: string + onVersionChange: (version: string) => void +} + +export function NodeVersionSelector({ + currentVersion, + serviceName, + category, + onVersionChange, +}: NodeVersionSelectorProps) { + const [isEditing, setIsEditing] = useState(false) + const [inputValue, setInputValue] = useState(currentVersion || '') + const [availableVersions, setAvailableVersions] = useState([]) + const [showSuggestions, setShowSuggestions] = useState(false) + + useEffect(() => { + if (isEditing) { + // Load available Node versions when starting to edit + invoke('get_available_node_versions') + .then(setAvailableVersions) + .catch(console.error) + } + }, [isEditing]) + + const handleSave = () => { + const version = inputValue.trim() + if (version !== currentVersion) { + onVersionChange(version) + if (version) { + toast.success(`Node.js version set to ${version} for ${serviceName}`) + } else { + toast.success(`Using default Node.js version for ${serviceName}`) + } + } + setIsEditing(false) + setShowSuggestions(false) + } + + const handleCancel = () => { + setInputValue(currentVersion || '') + setIsEditing(false) + setShowSuggestions(false) + } + + const handleClear = () => { + setInputValue('') + onVersionChange('') + setIsEditing(false) + setShowSuggestions(false) + toast.success(`Using default Node.js version for ${serviceName}`) + } + + const filteredVersions = availableVersions.filter(version => + version.toLowerCase().includes(inputValue.toLowerCase()) + ) + + if (isEditing) { + return ( +
+ Node.js: +
+ { + setInputValue(e.target.value) + setShowSuggestions(true) + }} + onFocus={() => setShowSuggestions(true)} + placeholder="e.g. 18.17.0, 20.10.0" + className="h-7 w-32 text-xs" + onKeyDown={(e) => { + if (e.key === 'Enter') handleSave() + if (e.key === 'Escape') handleCancel() + }} + autoFocus + /> + {showSuggestions && filteredVersions.length > 0 && ( +
+ {filteredVersions.map((version) => ( + + ))} +
+ )} +
+ + + {currentVersion && ( + + )} +
+ ) + } + + return ( +
+ Node.js: + + {currentVersion || 'default'} + + +
+ ) +} \ No newline at end of file diff --git a/src/pages/Service/index.tsx b/src/pages/Service/index.tsx index 1df9547..6f6ad6d 100644 --- a/src/pages/Service/index.tsx +++ b/src/pages/Service/index.tsx @@ -14,11 +14,12 @@ import { import {useProvideCommands} from '../Layout/CommandBar/Context' import Logs from './Logs' import {openNativeTerminal} from '@/lib/openTerminal' +import {NodeVersionSelector} from '@/components/NodeVersionSelector' export default function Service() { const {serviceName, category} = useParams() const service = useServiceData(serviceName, category) - const {setServiceOn, status: allStatus, processes} = useSettings() + const {setServiceOn, setServiceNodeVersion, status: allStatus, processes} = useSettings() const [tab, setTab] = useState<'logs'>('logs') // Only logs tab now const currentStatus = allStatus[`${category}.${serviceName}`] || 'off' @@ -101,6 +102,12 @@ export default function Service() {
+ setServiceNodeVersion(category, serviceName, version)} + />