use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
use anyhow::{Context, Result, bail};
use clap::{Args, Subcommand};
use crate::embed::{
COLLECTOR_FILENAME, COLLECTOR_YAML, COMPOSE_FILENAME, COMPOSE_YAML, DEFAULT_DEST_REL,
DOC_FILENAME, LANGFUSE_DOC,
};
#[derive(Subcommand, Debug)]
pub enum Action {
Init(InitArgs),
Up(DestArgs),
Down(DownArgs),
Status(DestArgs),
}
#[derive(Args, Debug)]
pub struct InitArgs {
#[arg(long, default_value = DEFAULT_DEST_REL)]
pub dest: PathBuf,
#[arg(long)]
pub force: bool,
}
#[derive(Args, Debug)]
pub struct DestArgs {
#[arg(long, default_value = DEFAULT_DEST_REL)]
pub dest: PathBuf,
}
#[derive(Args, Debug)]
pub struct DownArgs {
#[arg(long, default_value = DEFAULT_DEST_REL)]
pub dest: PathBuf,
#[arg(long)]
pub volumes: bool,
}
pub fn run(action: Action) -> Result<()> {
match action {
Action::Init(args) => init(args),
Action::Up(args) => up(&args),
Action::Down(args) => down(&args),
Action::Status(args) => status(&args),
}
}
fn init(args: InitArgs) -> Result<()> {
let dest = args.dest;
fs::create_dir_all(&dest)
.with_context(|| format!("creating destination directory {}", dest.display()))?;
let entries: [(&str, &str); 3] = [
(COMPOSE_FILENAME, COMPOSE_YAML),
(COLLECTOR_FILENAME, COLLECTOR_YAML),
(DOC_FILENAME, LANGFUSE_DOC),
];
if !args.force {
for (name, _) in entries {
let path = dest.join(name);
if path.exists() {
bail!(
"refusing to overwrite existing file {} (rerun with --force to replace)",
path.display()
);
}
}
}
for (name, contents) in entries {
let path = dest.join(name);
fs::write(&path, contents).with_context(|| format!("writing {}", path.display()))?;
println!("wrote {}", path.display());
}
println!();
println!(
"next: `agent-sdk local-langfuse up --dest {}` to start the stack",
dest.display()
);
Ok(())
}
fn up(args: &DestArgs) -> Result<()> {
let compose_path = compose_path(&args.dest)?;
run_docker_compose(&compose_path, ["up", "-d"])
}
fn down(args: &DownArgs) -> Result<()> {
let compose_path = compose_path(&args.dest)?;
if args.volumes {
run_docker_compose(&compose_path, ["down", "-v"])
} else {
run_docker_compose(&compose_path, ["down"])
}
}
fn status(args: &DestArgs) -> Result<()> {
let compose_path = compose_path(&args.dest)?;
run_docker_compose(&compose_path, ["ps"])
}
fn compose_path(dest: &Path) -> Result<PathBuf> {
let path = dest.join(COMPOSE_FILENAME);
if !path.exists() {
bail!(
"compose file not found at {}; run `agent-sdk local-langfuse init` first",
path.display()
);
}
Ok(path)
}
fn run_docker_compose<I, S>(compose_path: &Path, args: I) -> Result<()>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut cmd = Command::new("docker");
cmd.arg("compose").arg("-f").arg(compose_path);
for a in args {
cmd.arg(a);
}
let status: ExitStatus = cmd
.status()
.context("spawning `docker` (is Docker installed and on PATH?)")?;
if !status.success() {
bail!("`docker compose` exited with {status}");
}
Ok(())
}