fix(desktop): restore managed Node server startup (#348)

## Summary
- revert the Bun standalone desktop packaging path and restore the
server's original `dist/bin.js` bootstrap flow
- add a managed Node runtime for Electron and Tauri that downloads only
the current platform/arch artifact into `~/.config/codenomad`
- update desktop startup and packaging scripts so packaged apps use the
managed runtime consistently, and clean up Electron's expected
navigation-abort log noise

## Testing
- npm run typecheck --workspace @neuralnomads/codenomad-electron-app
- cargo check
- npm run build --workspace @neuralnomads/codenomad
- npm run build:mac --workspace @neuralnomads/codenomad-electron-app
- launch
`packages/electron-app/release/mac-arm64/CodeNomad.app/Contents/MacOS/CodeNomad`
and verify the packaged server reaches ready with the managed Node
runtime
This commit is contained in:
Shantur Rathore
2026-04-26 13:20:47 +01:00
committed by GitHub
parent e708c565ef
commit fd57bd11a6
28 changed files with 1962 additions and 2348 deletions

View File

@@ -1,3 +1,4 @@
use crate::managed_node::ensure_managed_node_binary;
use dirs::home_dir;
use parking_lot::Mutex;
use regex::Regex;
@@ -136,10 +137,6 @@ fn workspace_root() -> Option<PathBuf> {
})
}
fn launch_cwd() -> Option<PathBuf> {
std::env::current_dir().ok()
}
const SESSION_COOKIE_NAME_PREFIX: &str = "codenomad_session";
const CLI_STOP_GRACE_SECS: u64 = 30;
@@ -628,19 +625,16 @@ impl CliProcessManager {
log_line("development mode: will prefer tsx + source if present");
}
let cwd = launch_cwd();
let cwd = workspace_root();
if let Some(ref c) = cwd {
log_line(&format!("using cwd={}", c.display()));
}
let use_user_shell = supports_user_shell();
if resolution.runner == Runner::Tsx
&& !use_user_shell
&& which::which(&resolution.node_binary).is_err()
{
if !use_user_shell && which::which(&resolution.node_binary).is_err() {
return Err(anyhow::anyhow!(
"Node binary '{}' not found. CodeNomad development mode requires Node.js installed on the system, or set NODE_BINARY to a valid runtime path.",
"Node binary '{}' not found. CodeNomad desktop currently requires Node.js installed on the system, or set NODE_BINARY to a valid runtime path.",
resolution.node_binary
));
}
@@ -649,17 +643,13 @@ impl CliProcessManager {
log_line("spawning via user shell");
ShellCommandType::UserShell(build_shell_command_string(&resolution, &args)?)
} else {
log_line(if resolution.runner == Runner::Standalone {
"spawning directly with standalone executable"
log_line(if resolution.runner == Runner::Tsx {
"spawning directly with node + tsx"
} else {
"spawning directly with node"
});
ShellCommandType::Direct(DirectCommand {
program: if resolution.runner == Runner::Standalone {
resolution.entry.clone()
} else {
resolution.node_binary.clone()
},
program: resolution.node_binary.clone(),
args: resolution.runner_args(&args),
})
};
@@ -669,13 +659,11 @@ impl CliProcessManager {
log_line(&format!("spawn command: {} {:?}", cmd.shell, cmd.args));
let mut c = Command::new(&cmd.shell);
c.args(&cmd.args)
.env("ELECTRON_RUN_AS_NODE", "1")
.env_remove("npm_config_prefix")
.env_remove("NPM_CONFIG_PREFIX")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if resolution.runner != Runner::Standalone {
c.env("ELECTRON_RUN_AS_NODE", "1");
}
configure_spawn(&mut c);
if let Some(ref cwd) = cwd {
c.current_dir(cwd);
@@ -688,11 +676,9 @@ impl CliProcessManager {
log_line(&format!("spawn command: {} {:?}", cmd.program, cmd.args));
let mut c = Command::new(&cmd.program);
c.args(&cmd.args)
.env("ELECTRON_RUN_AS_NODE", "1")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if resolution.runner != Runner::Standalone {
c.env("ELECTRON_RUN_AS_NODE", "1");
}
configure_spawn(&mut c);
if let Some(ref cwd) = cwd {
c.current_dir(cwd);
@@ -943,7 +929,7 @@ impl CliProcessManager {
let mut locked = status.lock();
if locked.error.is_none() {
locked.error = Some(format!(
"Node binary '{}' not found in the desktop shell environment. CodeNomad development mode requires Node.js installed on the system, or set NODE_BINARY to a valid runtime path.",
"Node binary '{}' not found in the desktop shell environment. CodeNomad desktop currently requires Node.js installed on the system, or set NODE_BINARY to a valid runtime path.",
node_binary.trim()
));
}
@@ -1062,19 +1048,19 @@ struct CliEntry {
runner: Runner,
runner_path: Option<String>,
node_binary: String,
node_args: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Runner {
Standalone,
Node,
Tsx,
}
impl CliEntry {
fn resolve(app: &AppHandle, dev: bool) -> anyhow::Result<Self> {
let node_binary = std::env::var("NODE_BINARY").unwrap_or_else(|_| "node".to_string());
if dev {
let node_binary = std::env::var("NODE_BINARY").unwrap_or_else(|_| "node".to_string());
if let Some(tsx_path) = resolve_tsx(app) {
if let Some(entry) = resolve_dev_entry(app) {
return Ok(Self {
@@ -1082,22 +1068,24 @@ impl CliEntry {
runner: Runner::Tsx,
runner_path: Some(tsx_path),
node_binary,
node_args: Vec::new(),
});
}
}
}
if let Some(entry) = resolve_standalone_entry(app) {
if let Some(entry) = resolve_prod_entry(app) {
return Ok(Self {
entry,
runner: Runner::Standalone,
runner: Runner::Node,
runner_path: None,
node_binary: String::new(),
node_binary: ensure_managed_node_binary(app)?,
node_args: vec!["--experimental-specifier-resolution=node".to_string()],
});
}
Err(anyhow::anyhow!(
"Unable to locate the packaged CodeNomad standalone server. Please rebuild the desktop bundle."
"Unable to locate the packaged CodeNomad server entrypoint (dist/bin.js). Please rebuild the desktop bundle."
))
}
@@ -1151,11 +1139,10 @@ impl CliEntry {
}
fn runner_args(&self, cli_args: &[String]) -> Vec<String> {
if self.runner == Runner::Standalone {
return cli_args.to_vec();
}
let mut args = VecDeque::new();
for arg in &self.node_args {
args.push_back(arg.clone());
}
if self.runner == Runner::Tsx {
if let Some(path) = &self.runner_path {
args.push_back(path.clone());
@@ -1227,37 +1214,24 @@ fn resolve_dev_entry(_app: &AppHandle) -> Option<String> {
first_existing(candidates)
}
fn resolve_standalone_entry(_app: &AppHandle) -> Option<String> {
let executable_name = if cfg!(windows) {
"codenomad-server.exe"
} else {
"codenomad-server"
};
fn resolve_prod_entry(_app: &AppHandle) -> Option<String> {
let base = workspace_root();
let mut candidates = vec![base
.as_ref()
.map(|p| p.join("packages/server/dist").join(executable_name))];
.map(|p| p.join("packages/server/dist/bin.js"))];
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
candidates.push(Some(
dir.join("resources/server/dist").join(executable_name),
));
candidates.push(Some(dir.join("resources/server/dist/bin.js")));
let resources = dir.join("../Resources");
candidates.push(Some(resources.join("server/dist").join(executable_name)));
candidates.push(Some(
resources
.join("resources/server/dist")
.join(executable_name),
));
candidates.push(Some(resources.join("server/dist/bin.js")));
candidates.push(Some(resources.join("resources/server/dist/bin.js")));
let linux_resource_roots = [dir.join("../lib/CodeNomad"), dir.join("../lib/codenomad")];
for root in linux_resource_roots {
candidates.push(Some(root.join("server/dist").join(executable_name)));
candidates.push(Some(
root.join("resources/server/dist").join(executable_name),
));
candidates.push(Some(root.join("server/dist/bin.js")));
candidates.push(Some(root.join("resources/server/dist/bin.js")));
}
}
}
@@ -1271,31 +1245,37 @@ fn build_shell_command_string(
) -> anyhow::Result<ShellCommand> {
let shell = default_shell();
let mut quoted: Vec<String> = Vec::new();
let command = if entry.runner == Runner::Standalone {
quoted.push(shell_escape(&entry.entry));
for arg in cli_args {
quoted.push(shell_escape(arg));
}
format!("exec {}", quoted.join(" "))
} else {
quoted.push(shell_escape(&entry.node_binary));
for arg in entry.runner_args(cli_args) {
quoted.push(shell_escape(&arg));
}
format!(
"if command -v {} >/dev/null 2>&1; then ELECTRON_RUN_AS_NODE=1 exec {}; else printf '%s%s\\n' '{}' {} >&2; exit 127; fi",
shell_escape(&entry.node_binary),
quoted.join(" "),
MISSING_NODE_PREFIX,
shell_escape(&entry.node_binary),
)
};
quoted.push(shell_escape(&entry.node_binary));
for arg in entry.runner_args(cli_args) {
quoted.push(shell_escape(&arg));
}
let command = format!(
"if [ -x {} ] || command -v {} >/dev/null 2>&1; then ELECTRON_RUN_AS_NODE=1 exec {}; else printf '%s%s\\n' '{}' {}; exit 127; fi",
shell_escape(&entry.node_binary),
shell_escape(&entry.node_binary),
quoted.join(" "),
MISSING_NODE_PREFIX,
shell_escape(&entry.node_binary),
);
let wrapped_command = wrap_command_for_shell(&command, &shell);
let args = build_shell_args(&shell, &wrapped_command);
log_line(&format!("user shell command: {} {:?}", shell, args));
Ok(ShellCommand { shell, args })
}
fn default_shell() -> String {
if let Ok(shell) = std::env::var("SHELL") {
if !shell.trim().is_empty() {
return shell;
}
}
if cfg!(target_os = "macos") {
"/bin/zsh".to_string()
} else {
"/bin/bash".to_string()
}
}
fn wrap_command_for_shell(command: &str, shell: &str) -> String {
let shell_name = std::path::Path::new(shell)
.file_name()
@@ -1320,19 +1300,6 @@ fn wrap_command_for_shell(command: &str, shell: &str) -> String {
command.to_string()
}
fn default_shell() -> String {
if let Ok(shell) = std::env::var("SHELL") {
if !shell.trim().is_empty() {
return shell;
}
}
if cfg!(target_os = "macos") {
"/bin/zsh".to_string()
} else {
"/bin/bash".to_string()
}
}
fn shell_escape(input: &str) -> String {
if input.is_empty() {
"''".to_string()
@@ -1354,8 +1321,8 @@ fn build_shell_args(shell: &str, command: &str) -> Vec<String> {
.unwrap_or("")
.to_lowercase();
if shell_name.contains("zsh") {
vec!["-l".into(), "-i".into(), "-c".into(), command.into()]
if shell_name.contains("zsh") || shell_name.contains("bash") {
vec!["-i".into(), "-l".into(), "-c".into(), command.into()]
} else {
vec!["-l".into(), "-c".into(), command.into()]
}