mod http;
use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::Parser;
use crate::cli::configure_windows_console_utf8;
use crate::config::Config;
pub use http::{RuntimeApiOptions, run_http_server};
#[derive(Parser, Debug)]
#[command(
name = "zagens-runtime",
about = "DeepSeek runtime HTTP/SSE sidecar (Zagens desktop; no TUI)",
version
)]
pub struct RuntimeServeCli {
#[arg(short, long)]
pub config: Option<PathBuf>,
#[arg(long)]
pub profile: Option<String>,
#[arg(short, long)]
pub workspace: Option<PathBuf>,
#[arg(long, default_value = "127.0.0.1")]
pub host: String,
#[arg(long, default_value_t = 7878)]
pub port: u16,
#[arg(long, default_value_t = 8)]
pub workers: usize,
#[arg(long = "cors-origin", value_name = "URL")]
pub cors_origin: Vec<String>,
#[arg(long = "auth-token", value_name = "TOKEN")]
pub auth_token: Option<String>,
#[arg(short, long)]
pub verbose: bool,
}
pub fn resolve_cors_origins(config: &Config, flag_origins: &[String]) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut push = |raw: &str| {
let trimmed = raw.trim();
if trimmed.is_empty() {
return;
}
if !out.iter().any(|existing| existing == trimmed) {
out.push(trimmed.to_string());
}
};
for o in flag_origins {
push(o);
}
if let Ok(env_value) = std::env::var("DEEPSEEK_CORS_ORIGINS") {
for piece in env_value.split(',') {
push(piece);
}
}
if let Some(rt) = &config.runtime_api
&& let Some(list) = &rt.cors_origins
{
for o in list {
push(o);
}
}
out
}
fn load_config(cli: &RuntimeServeCli) -> Result<Config> {
let profile = cli
.profile
.clone()
.or_else(|| std::env::var("DEEPSEEK_PROFILE").ok());
Config::load(cli.config.clone(), profile.as_deref()).context("load config")
}
fn resolve_workspace(cli: &RuntimeServeCli) -> PathBuf {
if let Some(ws) = cli.workspace.clone() {
return ws;
}
if let Some(docs) = dirs::document_dir() {
let zagens = docs.join("Zagens");
if zagens.is_dir() {
return zagens;
}
if std::fs::create_dir_all(&zagens).is_ok() {
return zagens;
}
}
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
pub async fn run(cli: RuntimeServeCli) -> Result<()> {
configure_windows_console_utf8();
crate::logging::set_verbose(cli.verbose || crate::logging::env_requests_verbose_logging());
if cli.host != "127.0.0.1" && cli.host != "localhost" {
eprintln!(
"⚠ deepseek-runtime is binding to {} (not localhost).\n\
The runtime API will be reachable from other machines on the network.\n\
Make sure you have set --auth-token (or DEEPSEEK_RUNTIME_TOKEN) and\n\
configured restrictive CORS origins via --cors-origin or config.toml.",
cli.host,
);
}
let _ = crate::config::ensure_config_file_exists(cli.config.clone());
let config = load_config(&cli)?;
let workspace = resolve_workspace(&cli);
crate::symbol_index::warmup_if_needed(&workspace);
let skills_dir = config.skills_dir();
tokio::spawn(async move {
if let Err(e) = crate::skills::install_system_skills(&skills_dir) {
crate::logging::warn(format!("Failed to install system skills: {e}"));
}
});
let cors_origins = resolve_cors_origins(&config, &cli.cors_origin);
run_http_server(
config,
workspace,
RuntimeApiOptions {
host: cli.host,
port: cli.port,
workers: cli.workers.clamp(1, 16),
cors_origins,
auth_token: cli.auth_token,
},
)
.await
}
pub async fn run_or_exit(cli: RuntimeServeCli) -> ! {
match run(cli).await {
Ok(()) => {
eprintln!("[deepseek-runtime] server shut down cleanly, exiting");
std::process::exit(0);
}
Err(e) => {
eprintln!("[deepseek-runtime] fatal: {:#}", e);
std::process::exit(1);
}
}
}
pub async fn run_from_args<I, T>(args: I) -> !
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
let cli = RuntimeServeCli::parse_from(args);
run_or_exit(cli).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_cors_dedupes() {
let config = Config::default();
let out = resolve_cors_origins(&config, &["http://a".into(), "http://a".into()]);
assert_eq!(out, vec!["http://a".to_string()]);
}
}