use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use gaze_proxy::adapters::{AnthropicAdapter, GeminiAdapter, OpenAiAdapter};
use gaze_proxy::daemon::{self, AdapterConfig, DaemonConfig, DaemonPaths};
use gaze_proxy::ProxyConfig;
use url::Url;
use crate::clean_overrides::CleanOverrides;
use crate::error::CliError;
use crate::pipeline::build::resolve_pipeline;
pub(crate) struct ServeArgs {
pub(crate) bind: SocketAddr,
pub(crate) upstream_openai: Url,
pub(crate) upstream_anthropic: Url,
pub(crate) upstream_gemini: Url,
pub(crate) policy: Option<PathBuf>,
pub(crate) rulepack: String,
pub(crate) session_ttl: String,
pub(crate) foreground_daemon: bool,
#[cfg(feature = "dashboard")]
pub(crate) dashboard: super::proxy_dashboard::DashboardArgs,
}
pub(crate) struct StartArgs {
pub(crate) bind: Option<SocketAddr>,
pub(crate) upstream_openai: Option<Url>,
pub(crate) upstream_anthropic: Option<Url>,
pub(crate) upstream_gemini: Option<Url>,
pub(crate) policy: Option<PathBuf>,
pub(crate) rulepack: Option<String>,
pub(crate) session_ttl: Option<String>,
#[cfg(feature = "dashboard")]
pub(crate) dashboard: super::proxy_dashboard::DashboardArgs,
}
pub(crate) struct StopArgs {
pub(crate) force: bool,
pub(crate) timeout: String,
}
pub(crate) struct RestartArgs {
pub(crate) force: bool,
pub(crate) timeout: String,
}
pub(crate) fn serve(args: ServeArgs) -> Result<(), CliError> {
let paths = DaemonPaths::resolve().map_err(map_proxy)?;
if args.foreground_daemon {
daemon::init_foreground_daemon(&paths, args.bind).map_err(map_proxy)?;
}
let mut config = proxy_config(
args.bind,
args.upstream_openai,
args.upstream_anthropic,
args.upstream_gemini,
);
config.session_ttl = parse_duration(&args.session_ttl)?;
#[cfg(feature = "dashboard")]
let mut _dashboard_launch = None;
#[cfg(feature = "dashboard")]
{
use super::proxy_dashboard::{self, DashboardDecision};
match proxy_dashboard::decide(&args.dashboard) {
DashboardDecision::Off => {}
DashboardDecision::Disabled(reason) => {
eprintln!("gaze dashboard disabled: {reason}");
}
DashboardDecision::Enable(plan) => match proxy_dashboard::activate(plan) {
Ok(active) => {
config = config.with_inspection(active.producer);
_dashboard_launch = Some(active.launch);
eprintln!("gaze dashboard active");
}
Err(reason) => eprintln!("gaze dashboard disabled: {reason}"),
},
}
}
let (pipeline, locale_chain, dictionaries) = build_pipeline(args.policy, &args.rulepack)?;
config = config
.with_locale_chain(locale_chain)
.with_dictionaries(dictionaries);
let runtime = tokio::runtime::Runtime::new()
.map_err(|err| CliError::ProxyDetail(format!("runtime: {err}")))?;
runtime
.block_on(gaze_proxy::serve(config, Arc::new(pipeline)))
.map_err(map_proxy)
}
pub(crate) fn start(args: StartArgs) -> Result<(), CliError> {
let paths = DaemonPaths::resolve().map_err(map_proxy)?;
let mut config = daemon::read_or_default_config(&paths).map_err(map_proxy)?;
#[cfg(feature = "dashboard")]
let dashboard_args = args.dashboard.clone();
apply_start_overrides(&mut config, args);
let start_options = daemon::StartOptions::new(paths.clone(), config.clone());
#[cfg(feature = "dashboard")]
let start_options =
start_options.with_extra_args(super::proxy_dashboard::relay_extra_args(&dashboard_args));
let pid = daemon::start(start_options).map_err(map_proxy)?;
println!(
"gaze-proxy started (pid={pid}, bind={}, log={})",
config.bind,
paths.log_file.display()
);
Ok(())
}
pub(crate) fn stop(args: StopArgs) -> Result<(), CliError> {
let paths = DaemonPaths::resolve().map_err(map_proxy)?;
daemon::stop(daemon::StopOptions::new(
paths,
parse_duration(&args.timeout)?,
args.force,
))
.map_err(map_proxy)?;
println!("gaze-proxy stopped");
Ok(())
}
pub(crate) fn status() -> Result<(), CliError> {
let paths = DaemonPaths::resolve().map_err(map_proxy)?;
match daemon::status(&paths).map_err(map_proxy)? {
Some(status) if status.running => {
println!(
"gaze-proxy running (pid={}, bind={})",
status.pid,
status.bind.unwrap_or_else(|| "unknown".to_string())
);
let config = daemon::read_or_default_config(&paths).map_err(map_proxy)?;
println!(" adapters: openai -> {}", config.adapters.openai.upstream);
println!(
" anthropic -> {}",
config.adapters.anthropic.upstream
);
println!(" gemini -> {}", config.adapters.gemini.upstream);
}
Some(status) => {
println!("gaze-proxy not running (stale pidfile pid={})", status.pid);
}
None => println!("gaze-proxy not running"),
}
Ok(())
}
pub(crate) fn logs(follow: bool) -> Result<(), CliError> {
let paths = DaemonPaths::resolve().map_err(map_proxy)?;
daemon::logs(&paths, follow).map_err(map_proxy)
}
pub(crate) fn restart(args: RestartArgs) -> Result<(), CliError> {
let paths = DaemonPaths::resolve().map_err(map_proxy)?;
let config = daemon::read_or_default_config(&paths).map_err(map_proxy)?;
let pid = daemon::restart(
daemon::StartOptions::new(paths.clone(), config.clone()),
parse_duration(&args.timeout)?,
args.force,
)
.map_err(map_proxy)?;
println!(
"gaze-proxy restarted (pid={pid}, bind={}, log={})",
config.bind,
paths.log_file.display()
);
Ok(())
}
pub(crate) fn install_launchd() -> Result<(), CliError> {
Err(CliError::ProxyDetail(
"launchd integration is reserved for v0.8.x; use `gaze proxy start`".to_string(),
))
}
pub(crate) fn uninstall_launchd() -> Result<(), CliError> {
Err(CliError::ProxyDetail(
"launchd integration is reserved for v0.8.x; use `gaze proxy stop`".to_string(),
))
}
pub(crate) fn install_systemd_user() -> Result<(), CliError> {
Err(CliError::ProxyDetail(
"systemd user integration is reserved for v0.8.x; use `gaze proxy start`".to_string(),
))
}
pub(crate) fn uninstall_systemd_user() -> Result<(), CliError> {
Err(CliError::ProxyDetail(
"systemd user integration is reserved for v0.8.x; use `gaze proxy stop`".to_string(),
))
}
fn build_pipeline(
policy: Option<PathBuf>,
rulepack: &str,
) -> Result<(gaze::Pipeline, gaze::LocaleChain, gaze::DictionaryBundle), CliError> {
if let Some(path) = policy {
let resolved = resolve_pipeline(
Some(&path),
&CleanOverrides::default(),
&[],
None,
None,
None,
)?;
return Ok((
resolved.pipeline,
resolved.locale_chain,
resolved.dictionaries,
));
}
let mut config = gaze_assembly::CorePipelineConfig::new();
if rulepack != "core" {
config = config.with_bundled_rulepack(rulepack);
}
let core = config
.build()
.map_err(|err| CliError::ProxyDetail(format!("pipeline: {err}")))?;
let locale_chain = core.locale_chain().clone();
Ok((
core.into_pipeline(),
locale_chain,
gaze::DictionaryBundle::default(),
))
}
fn proxy_config(
bind: SocketAddr,
upstream_openai: Url,
upstream_anthropic: Url,
upstream_gemini: Url,
) -> ProxyConfig {
ProxyConfig::new(
bind,
vec![
Arc::new(OpenAiAdapter::new(upstream_openai)),
Arc::new(AnthropicAdapter::new(upstream_anthropic)),
Arc::new(GeminiAdapter::new(upstream_gemini)),
],
)
}
fn apply_start_overrides(config: &mut DaemonConfig, args: StartArgs) {
if let Some(bind) = args.bind {
config.bind = bind;
}
if let Some(policy) = args.policy {
config.policy = Some(policy);
}
if let Some(rulepack) = args.rulepack {
config.rulepack = Some(rulepack);
}
if let Some(session_ttl) = args.session_ttl {
config.session_ttl = session_ttl;
}
config.adapters = AdapterConfig::new(
args.upstream_openai
.unwrap_or_else(|| config.adapters.openai.upstream.clone()),
args.upstream_anthropic
.unwrap_or_else(|| config.adapters.anthropic.upstream.clone()),
args.upstream_gemini
.unwrap_or_else(|| config.adapters.gemini.upstream.clone()),
);
}
pub(crate) fn parse_duration(input: &str) -> Result<Duration, CliError> {
let trimmed = input.trim();
let parse_number = |suffix: &str| {
trimmed
.strip_suffix(suffix)
.and_then(|value| value.parse::<u64>().ok())
};
if let Some(minutes) = parse_number("m") {
Ok(Duration::from_secs(minutes * 60))
} else if let Some(seconds) = parse_number("s") {
Ok(Duration::from_secs(seconds))
} else if let Some(hours) = parse_number("h") {
Ok(Duration::from_secs(hours * 60 * 60))
} else if let Ok(seconds) = trimmed.parse::<u64>() {
Ok(Duration::from_secs(seconds))
} else {
Err(CliError::ProxyDetail(format!(
"invalid duration `{input}`; use Ns, Nm, Nh, or seconds"
)))
}
}
fn map_proxy(err: gaze_proxy::ProxyError) -> CliError {
CliError::ProxyDetail(err.to_string())
}