Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 76 additions & 1 deletion src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,35 @@ pub async fn prepare_services_start(services: Vec<ServiceData>) -> 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

# 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
Expand Down Expand Up @@ -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),
Expand All @@ -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);
}
Expand Down Expand Up @@ -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<Vec<String>, 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)
}
3 changes: 2 additions & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
70 changes: 61 additions & 9 deletions src-tauri/src/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ pub async fn get_services_in_services(settings: &AppSettings) -> Result<Vec<Serv
.copied()
.unwrap_or(false);

// Get Node version from settings or config file
let node_version = settings.node_versions
.as_ref()
.and_then(|node_map| node_map.get(&full_name).cloned())
.or_else(|| {
config.get("nodeVersion")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
});

services.push(ServiceData {
name: service_name.clone(),
path: path.to_string_lossy().to_string(),
Expand All @@ -67,6 +77,7 @@ pub async fn get_services_in_services(settings: &AppSettings) -> Result<Vec<Serv
category: "services".to_string(),
config,
start_command: "sh .start.run.sh".to_string(),
node_version,
});
}
Err(e) => {
Expand Down Expand Up @@ -109,6 +120,9 @@ pub fn get_services_in_justo(settings: &AppSettings) -> Vec<ServiceData> {
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(),
Expand All @@ -122,6 +136,9 @@ pub fn get_services_in_justo(settings: &AppSettings) -> Vec<ServiceData> {
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()),
},
]
}
Expand All @@ -147,6 +164,9 @@ pub fn get_services_in_delivery(settings: &AppSettings) -> Vec<ServiceData> {
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(),
Expand All @@ -160,6 +180,9 @@ pub fn get_services_in_delivery(settings: &AppSettings) -> Vec<ServiceData> {
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()),
},
]
}
Expand All @@ -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()])
Expand All @@ -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
Expand All @@ -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())
Expand Down Expand Up @@ -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) => {
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pub struct ServiceData {
pub category: String, // "services", "justo", "delivery"
pub config: HashMap<String, serde_json::Value>,
pub start_command: String,
pub node_version: Option<String>, // Node.js version to use (e.g., "18.17.0", "20.10.0")
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand All @@ -58,6 +59,8 @@ pub struct AppSettings {
pub delivery_path: Option<String>,
#[serde(rename = "onServices")]
pub on_services: Option<HashMap<String, bool>>,
#[serde(rename = "nodeVersions")]
pub node_versions: Option<HashMap<String, String>>, // service.full_name -> node version
}

#[derive(Debug, Serialize)]
Expand Down
Loading