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
// Spawns agent-runner.ts (compiled), communicates via stdio NDJSON
// Sidecar lifecycle management (Deno-first, Node.js fallback)
// Spawns agent-runner-deno.ts (or agent-runner.mjs), communicates via stdio NDJSON
use serde::{Deserialize, Serialize};
use std::io::{BufRead, BufReader, Write};
@ -18,6 +18,11 @@ pub struct AgentQueryOptions {
pub resume_session_id: Option<String>,
}
struct SidecarCommand {
program: String,
args: Vec<String>,
}
pub struct SidecarManager {
child: Arc<Mutex<Option<Child>>>,
stdin_writer: Arc<Mutex<Option<Box<dyn Write + Send>>>>,
@ -39,13 +44,13 @@ impl SidecarManager {
return Err("Sidecar already running".to_string());
}
// Resolve sidecar binary path relative to the app
let sidecar_path = Self::resolve_sidecar_path(app)?;
// Resolve sidecar command (Deno-first, Node.js fallback)
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")
.arg(&sidecar_path)
let mut child = Command::new(&cmd.program)
.args(&cmd.args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
@ -184,35 +189,63 @@ impl SidecarManager {
*self.ready.lock().unwrap()
}
fn resolve_sidecar_path(app: &AppHandle) -> Result<std::path::PathBuf, String> {
// In dev mode, use the sidecar source directory
// In production, the built sidecar is bundled with the app
fn resolve_sidecar_command(app: &AppHandle) -> Result<SidecarCommand, String> {
let resource_dir = app
.path()
.resource_dir()
.map_err(|e| format!("Failed to get resource dir: {e}"))?;
let prod_path = resource_dir.join("sidecar").join("dist").join("agent-runner.mjs");
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"))
let dev_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.join("sidecar")
.join("dist")
.join("agent-runner.mjs");
.to_path_buf();
if dev_path.exists() {
return Ok(dev_path);
// Try Deno first (runs TypeScript directly, no build step needed)
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!(
"Sidecar not found at {} or {}",
prod_path.display(),
dev_path.display()
"Sidecar not found. Checked Deno ({}, {}) and Node.js ({}, {})",
deno_paths[0].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"
],
"dialog": true,
"pubkey": ""
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEJCRkZEMERDMTUwMzY5MjIKUldRaWFRTVYzTkQvdTYwRDh6YStaSE9rWUZYYkRGd3UvVUcydE1IQVdTM29uNTRPTlpjQmFqVFEK"
}
},
"bundle": {
@ -43,6 +43,10 @@
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [
"../sidecar/agent-runner-deno.ts",
"../sidecar/dist/agent-runner.mjs"
],
"category": "DeveloperTool",
"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.",