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:
109
packages/tauri-app/Cargo.lock
generated
109
packages/tauri-app/Cargo.lock
generated
@@ -47,6 +47,15 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
@@ -502,6 +511,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"dirs 5.0.1",
|
||||
"flate2",
|
||||
"keepawake",
|
||||
"libc",
|
||||
"parking_lot",
|
||||
@@ -511,6 +521,8 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sha2",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
@@ -521,6 +533,7 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"which",
|
||||
"windows-sys 0.59.0",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -770,6 +783,17 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.20.2"
|
||||
@@ -1119,6 +1143,17 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -1212,6 +1247,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2228,7 +2264,10 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"libc",
|
||||
"plain",
|
||||
"redox_syscall 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2709,7 +2748,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"smallvec",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
@@ -2942,6 +2981,12 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
version = "1.8.0"
|
||||
@@ -3309,6 +3354,15 @@ dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
@@ -3389,6 +3443,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
@@ -3974,7 +4029,7 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
"raw-window-handle",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"tracing",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
@@ -4189,6 +4244,17 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.16"
|
||||
@@ -6150,6 +6216,16 @@ version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rustix 1.1.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xkeysym"
|
||||
version = "0.2.1"
|
||||
@@ -6320,12 +6396,41 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"indexmap 2.13.0",
|
||||
"memchr",
|
||||
"thiserror 2.0.18",
|
||||
"zopfli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.10.0"
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"build": "tauri build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.10.1"
|
||||
"@tauri-apps/cli": "^2.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ const serverDevInstallCommand =
|
||||
const uiDevInstallCommand =
|
||||
"npm install --workspace @codenomad/ui --include-workspace-root=false --install-strategy=nested --fund=false --audit=false"
|
||||
const serverPrepareUiCommand = "npm run prepare-ui --workspace @neuralnomads/codenomad"
|
||||
const serverStandaloneBuildCommand = "npm run build:standalone --workspace @neuralnomads/codenomad"
|
||||
|
||||
const envWithRootBin = {
|
||||
...process.env,
|
||||
@@ -78,15 +77,6 @@ function ensureServerBuild() {
|
||||
}
|
||||
}
|
||||
|
||||
function ensureStandaloneServerBuild() {
|
||||
console.log("[prebuild] building standalone server executable...")
|
||||
execSync(serverStandaloneBuildCommand, {
|
||||
cwd: workspaceRoot,
|
||||
stdio: "inherit",
|
||||
env: envWithRootBin,
|
||||
})
|
||||
}
|
||||
|
||||
function ensureUiBuild() {
|
||||
const loadingHtml = path.join(uiDist, "loading.html")
|
||||
if (fs.existsSync(loadingHtml)) {
|
||||
@@ -127,19 +117,15 @@ function ensureServerDevDependencies() {
|
||||
}
|
||||
|
||||
function ensureServerDependencies() {
|
||||
console.log("[prebuild] pruning server to production dependencies...")
|
||||
execSync("npm prune --omit=dev --ignore-scripts --workspaces=false --fund=false --audit=false", {
|
||||
if (fs.existsSync(braceExpansionPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log("[prebuild] ensuring server production dependencies...")
|
||||
execSync(serverInstallCommand, {
|
||||
cwd: serverRoot,
|
||||
stdio: "inherit",
|
||||
})
|
||||
|
||||
if (!fs.existsSync(braceExpansionPath)) {
|
||||
console.log("[prebuild] restoring missing server production dependencies...")
|
||||
execSync(serverInstallCommand, {
|
||||
cwd: serverRoot,
|
||||
stdio: "inherit",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function ensureUiDevDependencies() {
|
||||
@@ -195,11 +181,14 @@ function ensureRollupPlatformBinary() {
|
||||
function ensureEsbuildPlatformBinary() {
|
||||
const platformKey = `${process.platform}-${process.arch}`
|
||||
const platformPackages = {
|
||||
"linux-x64": "@esbuild/linux-x64",
|
||||
"linux-arm": "@esbuild/linux-arm",
|
||||
"linux-arm64": "@esbuild/linux-arm64",
|
||||
"linux-ia32": "@esbuild/linux-ia32",
|
||||
"linux-x64": "@esbuild/linux-x64",
|
||||
"darwin-arm64": "@esbuild/darwin-arm64",
|
||||
"darwin-x64": "@esbuild/darwin-x64",
|
||||
"win32-arm64": "@esbuild/win32-arm64",
|
||||
"win32-ia32": "@esbuild/win32-ia32",
|
||||
"win32-x64": "@esbuild/win32-x64",
|
||||
}
|
||||
|
||||
@@ -208,26 +197,29 @@ function ensureEsbuildPlatformBinary() {
|
||||
return
|
||||
}
|
||||
|
||||
const platformPackagePath = path.join(workspaceRoot, "node_modules", ...pkgName.split("/"))
|
||||
if (fs.existsSync(platformPackagePath)) {
|
||||
const platformPackageName = pkgName.split("/").pop()
|
||||
const platformPackagePaths = [
|
||||
path.join(serverRoot, "node_modules", "@esbuild", platformPackageName),
|
||||
path.join(workspaceRoot, "node_modules", "@esbuild", platformPackageName),
|
||||
]
|
||||
if (platformPackagePaths.some((packagePath) => fs.existsSync(packagePath))) {
|
||||
return
|
||||
}
|
||||
|
||||
let esbuildVersion = ""
|
||||
try {
|
||||
esbuildVersion = require(path.join(workspaceRoot, "node_modules", "esbuild", "package.json")).version
|
||||
} catch {
|
||||
for (const baseRoot of [serverRoot, workspaceRoot]) {
|
||||
try {
|
||||
esbuildVersion = require(path.join(workspaceRoot, "node_modules", "vite", "node_modules", "esbuild", "package.json")).version
|
||||
} catch {
|
||||
// leave version empty; fallback install will use latest compatible
|
||||
esbuildVersion = require(path.join(baseRoot, "node_modules", "esbuild", "package.json")).version
|
||||
break
|
||||
} catch (error) {
|
||||
// try the next install root; fallback install will use latest compatible
|
||||
}
|
||||
}
|
||||
|
||||
const packageSpec = esbuildVersion ? `${pkgName}@${esbuildVersion}` : pkgName
|
||||
|
||||
console.log("[prebuild] installing esbuild platform binary (optional dep workaround)...")
|
||||
execSync(`npm install ${packageSpec} --no-save --ignore-scripts --fund=false --audit=false`, {
|
||||
execSync(`npm install ${packageSpec} --no-save --ignore-scripts --package-lock=false --fund=false --audit=false`, {
|
||||
cwd: workspaceRoot,
|
||||
stdio: "inherit",
|
||||
})
|
||||
@@ -313,7 +305,6 @@ function copyUiLoadingAssets() {
|
||||
ensureRollupPlatformBinary()
|
||||
ensureEsbuildPlatformBinary()
|
||||
ensureServerBuild()
|
||||
ensureStandaloneServerBuild()
|
||||
ensureServerDependencies()
|
||||
ensureUiBuild()
|
||||
syncServerUiBundle()
|
||||
|
||||
@@ -5,16 +5,16 @@ edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.5.6", features = [] }
|
||||
tauri-build = { version = "2.5.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.10.1", features = [ "devtools"] }
|
||||
tauri = { version = "2.5.2", features = [ "devtools"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9"
|
||||
base64 = "0.22"
|
||||
rustls = { version = "0.23", features = ["ring"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["http2", "charset", "json", "stream", "rustls-tls"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "http2", "charset", "json", "stream", "rustls-tls"] }
|
||||
regex = "1"
|
||||
parking_lot = "0.12"
|
||||
anyhow = "1"
|
||||
@@ -27,6 +27,10 @@ tauri-plugin-opener = "2"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
url = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
flate2 = "1"
|
||||
sha2 = "0.10"
|
||||
tar = "0.4"
|
||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security_Cryptography", "Win32_UI_Shell", "Win32_Security", "Win32_System_JobObjects"] }
|
||||
|
||||
@@ -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()]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#[allow(dead_code)]
|
||||
mod cert_manager;
|
||||
mod cli_manager;
|
||||
mod managed_node;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux_tls;
|
||||
|
||||
|
||||
299
packages/tauri-app/src-tauri/src/managed_node.rs
Normal file
299
packages/tauri-app/src-tauri/src/managed_node.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
use anyhow::anyhow;
|
||||
use dirs::home_dir;
|
||||
use flate2::read::GzDecoder;
|
||||
use reqwest::blocking::Client;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Read};
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::thread;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tar::Archive;
|
||||
use tauri::{AppHandle, Runtime};
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
|
||||
use zip::ZipArchive;
|
||||
|
||||
const MANAGED_NODE_VERSION: &str = "v22.22.2";
|
||||
|
||||
struct NodeArtifactSpec {
|
||||
archive_name: &'static str,
|
||||
archive_root: &'static str,
|
||||
binary_relative_path: &'static str,
|
||||
}
|
||||
|
||||
pub fn ensure_managed_node_binary<R: Runtime>(app: &AppHandle<R>) -> anyhow::Result<String> {
|
||||
let runtime_root = managed_node_root()?;
|
||||
let spec = artifact_spec()?;
|
||||
let binary_path = runtime_root.join(spec.binary_relative_path);
|
||||
if binary_path.is_file() {
|
||||
return Ok(binary_path.to_string_lossy().into_owned());
|
||||
}
|
||||
|
||||
if !prompt_to_download(app) {
|
||||
return Err(anyhow!(
|
||||
"CodeNomad requires the managed Node.js runtime to start. Download was cancelled."
|
||||
));
|
||||
}
|
||||
|
||||
install_managed_node_runtime(&runtime_root, &spec)?;
|
||||
|
||||
if !binary_path.is_file() {
|
||||
return Err(anyhow!(
|
||||
"Managed Node binary missing after installation: {}",
|
||||
binary_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mut permissions = fs::metadata(&binary_path)?.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(&binary_path, permissions)?;
|
||||
}
|
||||
|
||||
Ok(binary_path.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
fn prompt_to_download<R: Runtime>(app: &AppHandle<R>) -> bool {
|
||||
let app = app.clone();
|
||||
thread::spawn(move || {
|
||||
app.dialog()
|
||||
.message(format!(
|
||||
"CodeNomad needs its managed Node.js runtime to start the server. Download {} for {}-{} into ~/.config/codenomad?",
|
||||
MANAGED_NODE_VERSION,
|
||||
platform_label(),
|
||||
rust_arch_label().unwrap_or("unknown")
|
||||
))
|
||||
.title("Download Node Runtime")
|
||||
.buttons(MessageDialogButtons::OkCancelCustom(
|
||||
"Download".into(),
|
||||
"Cancel".into(),
|
||||
))
|
||||
.kind(MessageDialogKind::Info)
|
||||
.blocking_show()
|
||||
})
|
||||
.join()
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn managed_node_root() -> anyhow::Result<PathBuf> {
|
||||
Ok(config_dir()?.join("node").join(MANAGED_NODE_VERSION).join(platform_dir_name()?))
|
||||
}
|
||||
|
||||
fn config_dir() -> anyhow::Result<PathBuf> {
|
||||
let home = home_dir().ok_or_else(|| anyhow!("Unable to resolve the user home directory."))?;
|
||||
Ok(home.join(".config").join("codenomad"))
|
||||
}
|
||||
|
||||
fn platform_dir_name() -> anyhow::Result<String> {
|
||||
Ok(format!("{}-{}", platform_label(), rust_arch_label()?))
|
||||
}
|
||||
|
||||
fn platform_label() -> &'static str {
|
||||
match std::env::consts::OS {
|
||||
"macos" => "darwin",
|
||||
"windows" => "win32",
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
fn rust_arch_label() -> anyhow::Result<&'static str> {
|
||||
match std::env::consts::ARCH {
|
||||
"x86_64" => Ok("x64"),
|
||||
"aarch64" => Ok("arm64"),
|
||||
other => Err(anyhow!("Managed Node runtime is not supported on architecture '{other}'.")),
|
||||
}
|
||||
}
|
||||
|
||||
fn artifact_spec() -> anyhow::Result<NodeArtifactSpec> {
|
||||
let arch = rust_arch_label()?;
|
||||
match (std::env::consts::OS, arch) {
|
||||
("macos", "x64") => Ok(NodeArtifactSpec {
|
||||
archive_name: "node-v22.22.2-darwin-x64.tar.gz",
|
||||
archive_root: "node-v22.22.2-darwin-x64",
|
||||
binary_relative_path: "bin/node",
|
||||
}),
|
||||
("macos", "arm64") => Ok(NodeArtifactSpec {
|
||||
archive_name: "node-v22.22.2-darwin-arm64.tar.gz",
|
||||
archive_root: "node-v22.22.2-darwin-arm64",
|
||||
binary_relative_path: "bin/node",
|
||||
}),
|
||||
("linux", "x64") => Ok(NodeArtifactSpec {
|
||||
archive_name: "node-v22.22.2-linux-x64.tar.gz",
|
||||
archive_root: "node-v22.22.2-linux-x64",
|
||||
binary_relative_path: "bin/node",
|
||||
}),
|
||||
("linux", "arm64") => Ok(NodeArtifactSpec {
|
||||
archive_name: "node-v22.22.2-linux-arm64.tar.gz",
|
||||
archive_root: "node-v22.22.2-linux-arm64",
|
||||
binary_relative_path: "bin/node",
|
||||
}),
|
||||
("windows", "x64") => Ok(NodeArtifactSpec {
|
||||
archive_name: "node-v22.22.2-win-x64.zip",
|
||||
archive_root: "node-v22.22.2-win-x64",
|
||||
binary_relative_path: "node.exe",
|
||||
}),
|
||||
("windows", "arm64") => Ok(NodeArtifactSpec {
|
||||
archive_name: "node-v22.22.2-win-arm64.zip",
|
||||
archive_root: "node-v22.22.2-win-arm64",
|
||||
binary_relative_path: "node.exe",
|
||||
}),
|
||||
(os, arch) => Err(anyhow!("Managed Node runtime is not supported on {os}-{arch}.")),
|
||||
}
|
||||
}
|
||||
|
||||
fn install_managed_node_runtime(runtime_root: &Path, spec: &NodeArtifactSpec) -> anyhow::Result<()> {
|
||||
let runtime_parent = runtime_root
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("Managed Node runtime path is invalid."))?;
|
||||
fs::create_dir_all(runtime_parent)?;
|
||||
|
||||
let temp_root = runtime_parent.join(format!(
|
||||
".download-{}-{}",
|
||||
std::process::id(),
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_millis())
|
||||
.unwrap_or(0)
|
||||
));
|
||||
|
||||
if temp_root.exists() {
|
||||
fs::remove_dir_all(&temp_root).ok();
|
||||
}
|
||||
fs::create_dir_all(&temp_root)?;
|
||||
|
||||
let archive_path = temp_root.join(spec.archive_name);
|
||||
let extract_root = temp_root.join("extract");
|
||||
fs::create_dir_all(&extract_root)?;
|
||||
|
||||
let result = (|| {
|
||||
let expected_sha = fetch_expected_sha(spec.archive_name)?;
|
||||
download_file(spec.archive_name, &archive_path)?;
|
||||
|
||||
let actual_sha = sha256_file(&archive_path)?;
|
||||
if actual_sha != expected_sha {
|
||||
return Err(anyhow!("Checksum mismatch for {}.", spec.archive_name));
|
||||
}
|
||||
|
||||
extract_archive(&archive_path, &extract_root)?;
|
||||
|
||||
let extracted_root = extract_root.join(spec.archive_root);
|
||||
let extracted_binary = extracted_root.join(spec.binary_relative_path);
|
||||
if !extracted_binary.is_file() {
|
||||
return Err(anyhow!(
|
||||
"Managed Node binary missing after extraction: {}",
|
||||
extracted_binary.display()
|
||||
));
|
||||
}
|
||||
|
||||
if runtime_root.exists() {
|
||||
fs::remove_dir_all(runtime_root)?;
|
||||
}
|
||||
fs::rename(&extracted_root, runtime_root)?;
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
fs::remove_dir_all(&temp_root).ok();
|
||||
result
|
||||
}
|
||||
|
||||
fn fetch_expected_sha(archive_name: &str) -> anyhow::Result<String> {
|
||||
let url = format!("https://nodejs.org/dist/{MANAGED_NODE_VERSION}/SHASUMS256.txt");
|
||||
let response = Client::builder()
|
||||
.build()?
|
||||
.get(url)
|
||||
.send()?
|
||||
.error_for_status()?;
|
||||
let body = response.text()?;
|
||||
|
||||
for line in body.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut parts = trimmed.split_whitespace();
|
||||
let checksum = parts.next();
|
||||
let file_name = parts.next();
|
||||
if let (Some(checksum), Some(file_name)) = (checksum, file_name) {
|
||||
if file_name == archive_name {
|
||||
return Ok(checksum.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("Unable to find checksum for {archive_name}."))
|
||||
}
|
||||
|
||||
fn download_file(archive_name: &str, destination: &Path) -> anyhow::Result<()> {
|
||||
let url = format!("https://nodejs.org/dist/{MANAGED_NODE_VERSION}/{archive_name}");
|
||||
let mut response = Client::builder()
|
||||
.build()?
|
||||
.get(url)
|
||||
.send()?
|
||||
.error_for_status()?;
|
||||
let mut output = File::create(destination)?;
|
||||
io::copy(&mut response, &mut output)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sha256_file(path: &Path) -> anyhow::Result<String> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
let mut buffer = [0_u8; 8192];
|
||||
|
||||
loop {
|
||||
let read = file.read(&mut buffer)?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
hasher.update(&buffer[..read]);
|
||||
}
|
||||
|
||||
Ok(format!("{:x}", hasher.finalize()))
|
||||
}
|
||||
|
||||
fn extract_archive(archive_path: &Path, destination: &Path) -> anyhow::Result<()> {
|
||||
if archive_path.extension().and_then(|value| value.to_str()) == Some("zip") {
|
||||
extract_zip(archive_path, destination)
|
||||
} else {
|
||||
extract_tar_gz(archive_path, destination)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_tar_gz(archive_path: &Path, destination: &Path) -> anyhow::Result<()> {
|
||||
let file = File::open(archive_path)?;
|
||||
let decoder = GzDecoder::new(file);
|
||||
let mut archive = Archive::new(decoder);
|
||||
archive.unpack(destination)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_zip(archive_path: &Path, destination: &Path) -> anyhow::Result<()> {
|
||||
let file = File::open(archive_path)?;
|
||||
let mut archive = ZipArchive::new(file)?;
|
||||
|
||||
for index in 0..archive.len() {
|
||||
let mut entry = archive.by_index(index)?;
|
||||
let relative_path = entry
|
||||
.enclosed_name()
|
||||
.map(|path| path.to_path_buf())
|
||||
.ok_or_else(|| anyhow!("Zip archive contains an invalid path."))?;
|
||||
let output_path = destination.join(relative_path);
|
||||
|
||||
if entry.is_dir() {
|
||||
fs::create_dir_all(&output_path)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(parent) = output_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let mut output = File::create(&output_path)?;
|
||||
io::copy(&mut entry, &mut output)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -43,6 +43,11 @@
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"linux": {
|
||||
"appimage": {
|
||||
"files": {
|
||||
"/usr/share/applications/ai.neuralnomads.codenomad.client.desktop": "icons/linux/ai.neuralnomads.codenomad.client.desktop"
|
||||
}
|
||||
},
|
||||
"deb": {
|
||||
"files": {
|
||||
"/usr/share/applications/ai.neuralnomads.codenomad.client.desktop": "icons/linux/ai.neuralnomads.codenomad.client.desktop",
|
||||
|
||||
Reference in New Issue
Block a user