feat(v2): Deno-first sidecar with Node.js fallback and signing key

Refactor SidecarManager to use SidecarCommand struct abstracting
runtime choice. resolve_sidecar_command() prefers Deno (runs TS
directly, no build step), falls back to Node.js if deno not in PATH.
Both runners bundled in tauri.conf.json resources. Set auto-update
signing pubkey in updater config.
This commit is contained in:
Hibryda 2026-03-06 15:42:26 +01:00
parent 035d4186fa
commit a2bc8838b4
2 changed files with 63 additions and 26 deletions

View file

@ -1,5 +1,5 @@
// Node.js sidecar lifecycle management // Sidecar lifecycle management (Deno-first, Node.js fallback)
// Spawns agent-runner.ts (compiled), communicates via stdio NDJSON // Spawns agent-runner-deno.ts (or agent-runner.mjs), communicates via stdio NDJSON
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::io::{BufRead, BufReader, Write}; use std::io::{BufRead, BufReader, Write};
@ -18,6 +18,11 @@ pub struct AgentQueryOptions {
pub resume_session_id: Option<String>, pub resume_session_id: Option<String>,
} }
struct SidecarCommand {
program: String,
args: Vec<String>,
}
pub struct SidecarManager { pub struct SidecarManager {
child: Arc<Mutex<Option<Child>>>, child: Arc<Mutex<Option<Child>>>,
stdin_writer: Arc<Mutex<Option<Box<dyn Write + Send>>>>, stdin_writer: Arc<Mutex<Option<Box<dyn Write + Send>>>>,
@ -39,13 +44,13 @@ impl SidecarManager {
return Err("Sidecar already running".to_string()); return Err("Sidecar already running".to_string());
} }
// Resolve sidecar binary path relative to the app // Resolve sidecar command (Deno-first, Node.js fallback)
let sidecar_path = Self::resolve_sidecar_path(app)?; let cmd = Self::resolve_sidecar_command(app)?;
log::info!("Starting sidecar: node {}", sidecar_path.display()); log::info!("Starting sidecar: {} {}", cmd.program, cmd.args.join(" "));
let mut child = Command::new("node") let mut child = Command::new(&cmd.program)
.arg(&sidecar_path) .args(&cmd.args)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
@ -184,35 +189,63 @@ impl SidecarManager {
*self.ready.lock().unwrap() *self.ready.lock().unwrap()
} }
fn resolve_sidecar_path(app: &AppHandle) -> Result<std::path::PathBuf, String> { fn resolve_sidecar_command(app: &AppHandle) -> Result<SidecarCommand, String> {
// In dev mode, use the sidecar source directory
// In production, the built sidecar is bundled with the app
let resource_dir = app let resource_dir = app
.path() .path()
.resource_dir() .resource_dir()
.map_err(|e| format!("Failed to get resource dir: {e}"))?; .map_err(|e| format!("Failed to get resource dir: {e}"))?;
let prod_path = resource_dir.join("sidecar").join("dist").join("agent-runner.mjs"); let dev_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
if prod_path.exists() {
return Ok(prod_path);
}
// Dev fallback: look relative to the Cargo project root
let dev_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent() .parent()
.unwrap() .unwrap()
.join("sidecar") .to_path_buf();
.join("dist")
.join("agent-runner.mjs");
if dev_path.exists() { // Try Deno first (runs TypeScript directly, no build step needed)
return Ok(dev_path); let deno_paths = [
resource_dir.join("sidecar").join("agent-runner-deno.ts"),
dev_root.join("sidecar").join("agent-runner-deno.ts"),
];
for path in &deno_paths {
if path.exists() {
// Check if deno is available
if Command::new("deno").arg("--version").output().is_ok() {
return Ok(SidecarCommand {
program: "deno".to_string(),
args: vec![
"run".to_string(),
"--allow-run".to_string(),
"--allow-env".to_string(),
"--allow-read".to_string(),
path.to_string_lossy().to_string(),
],
});
}
log::warn!("Deno sidecar found at {} but deno not in PATH, falling back to Node.js", path.display());
}
}
// Fallback to Node.js
let node_paths = [
resource_dir.join("sidecar").join("dist").join("agent-runner.mjs"),
dev_root.join("sidecar").join("dist").join("agent-runner.mjs"),
];
for path in &node_paths {
if path.exists() {
return Ok(SidecarCommand {
program: "node".to_string(),
args: vec![path.to_string_lossy().to_string()],
});
}
} }
Err(format!( Err(format!(
"Sidecar not found at {} or {}", "Sidecar not found. Checked Deno ({}, {}) and Node.js ({}, {})",
prod_path.display(), deno_paths[0].display(),
dev_path.display() deno_paths[1].display(),
node_paths[0].display(),
node_paths[1].display(),
)) ))
} }
} }

View file

@ -30,7 +30,7 @@
"https://github.com/DexterFromLab/BTerminal/releases/latest/download/latest.json" "https://github.com/DexterFromLab/BTerminal/releases/latest/download/latest.json"
], ],
"dialog": true, "dialog": true,
"pubkey": "" "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEJCRkZEMERDMTUwMzY5MjIKUldRaWFRTVYzTkQvdTYwRDh6YStaSE9rWUZYYkRGd3UvVUcydE1IQVdTM29uNTRPTlpjQmFqVFEK"
} }
}, },
"bundle": { "bundle": {
@ -43,6 +43,10 @@
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
], ],
"resources": [
"../sidecar/agent-runner-deno.ts",
"../sidecar/dist/agent-runner.mjs"
],
"category": "DeveloperTool", "category": "DeveloperTool",
"shortDescription": "Multi-session Claude agent dashboard", "shortDescription": "Multi-session Claude agent dashboard",
"longDescription": "BTerminal is a terminal emulator with integrated Claude AI agent sessions, SSH management, and a tiling pane layout. Built with Tauri, Svelte 5, and xterm.js.", "longDescription": "BTerminal is a terminal emulator with integrated Claude AI agent sessions, SSH management, and a tiling pane layout. Built with Tauri, Svelte 5, and xterm.js.",