mod complete;
mod config_check;
pub(crate) mod data;
pub(crate) mod front;
mod init;
pub(crate) mod pod;
mod progress;
mod runpod_train;
mod skill;
pub(crate) mod train;
use std::path::{Path, PathBuf};
use clap::{Args, Parser, Subcommand, ValueHint};
use clap_complete::engine::ArgValueCandidates;
use secrecy::SecretString;
use tokio::sync::OnceCell;
use self::front::Frontend;
use crate::logging::{LOG_LINES, LogBuffer, LogMode};
use crate::secrets::{Resolver, SecretError, SecretSource, VaultRef, VaultSettings, VaultSource};
#[derive(Debug, Parser)]
#[command(name = "overbrainer", version, about)]
pub struct Cli {
#[arg(short = 'C', long, global = true, default_value = ".", value_hint = ValueHint::DirPath)]
pub project_dir: PathBuf,
#[command(subcommand)]
pub command: Command,
}
#[derive(Debug, Subcommand)]
pub enum Command {
Init {
#[arg(value_hint = ValueHint::DirPath)]
dir: Option<PathBuf>,
},
Config {
#[command(subcommand)]
command: ConfigCommand,
},
Subtopics(StageArgs),
Questions(StageArgs),
Answers(StageArgs),
Split(SplitArgs),
Run,
Train(TrainArgs),
Runs {
#[command(subcommand)]
command: RunsCommand,
},
Pod {
#[command(subcommand)]
command: PodCommand,
},
Skill {
#[command(subcommand)]
command: SkillCommand,
},
Tui,
}
impl Command {
#[must_use]
pub fn log_mode(&self) -> LogMode {
match self {
Self::Tui => LogMode::Tui(LogBuffer::new(LOG_LINES)),
_ => LogMode::Stderr,
}
}
}
#[derive(Debug, Subcommand)]
pub enum PodCommand {
Ls,
Rm {
#[arg(add = ArgValueCandidates::new(complete::run_ids))]
run_id: String,
#[arg(long)]
force: bool,
},
}
#[derive(Debug, Subcommand)]
pub enum SkillCommand {
Install(SkillInstallArgs),
}
#[derive(Debug, Args)]
pub struct SkillInstallArgs {
#[arg(long, conflicts_with = "dir")]
pub global: bool,
#[arg(long, value_hint = ValueHint::DirPath)]
pub dir: Option<PathBuf>,
#[arg(long)]
pub force: bool,
}
#[derive(Debug, Default, Args)]
#[command(args_conflicts_with_subcommands = true)]
pub struct TrainArgs {
#[arg(long, add = ArgValueCandidates::new(complete::targets))]
pub target: Option<String>,
#[arg(long)]
pub keep_pod: bool,
#[command(subcommand)]
pub command: Option<TrainCommand>,
}
#[derive(Debug, Subcommand)]
pub enum TrainCommand {
Attach {
#[arg(add = ArgValueCandidates::new(complete::run_ids))]
run_id: String,
},
Cancel {
#[arg(add = ArgValueCandidates::new(complete::run_ids))]
run_id: String,
},
}
#[derive(Debug, Subcommand)]
pub enum RunsCommand {
Ls,
}
#[derive(Debug, Default, Args)]
pub struct StageArgs {
#[arg(long, add = ArgValueCandidates::new(complete::topics))]
pub topic: Option<String>,
#[arg(long)]
pub force: bool,
}
#[derive(Debug, Default, Args)]
pub struct SplitArgs {
#[arg(long, add = ArgValueCandidates::new(complete::topics))]
pub topic: Option<String>,
}
#[derive(Debug, Subcommand)]
pub enum ConfigCommand {
Check {
#[arg(long)]
resolve: bool,
},
}
pub async fn run(cli: Cli, logs: LogMode) -> anyhow::Result<()> {
let dir = &cli.project_dir;
match cli.command {
Command::Init { dir: target } => init::run(target.as_deref().unwrap_or(dir)),
Command::Config {
command: ConfigCommand::Check { resolve },
} => config_check::run(dir, resolve).await,
Command::Subtopics(args) => stage(dir, data::Command::Subtopics, &args).await,
Command::Questions(args) => stage(dir, data::Command::Questions, &args).await,
Command::Answers(args) => stage(dir, data::Command::Answers, &args).await,
Command::Split(SplitArgs { topic }) => {
let args = StageArgs {
topic,
force: false,
};
stage(dir, data::Command::Split, &args).await
},
Command::Run => {
stage(dir, data::Command::Run, &StageArgs::default()).await?;
train::after_run(dir, &Frontend::Cli).await
},
Command::Train(args) => train::run(dir, &args, &Frontend::Cli).await,
Command::Runs {
command: RunsCommand::Ls,
} => train::list(dir),
Command::Pod { command } => pod::run(dir, &command).await,
Command::Skill { command } => skill::run(dir, &command),
Command::Tui => match logs {
LogMode::Tui(buffer) => crate::tui::run(dir, buffer).await,
LogMode::Stderr => anyhow::bail!("overbrainer tui needs the TUI log mode"),
},
}
}
async fn stage(dir: &Path, command: data::Command, args: &StageArgs) -> anyhow::Result<()> {
data::run(dir, command, args, &Frontend::Cli).await
}
fn resolver() -> Resolver<LazyVault> {
Resolver::new(Some(LazyVault::default()))
}
#[derive(Default)]
struct LazyVault {
source: OnceCell<Option<VaultSource>>,
}
impl LazyVault {
async fn source(&self) -> Result<Option<&VaultSource>, SecretError> {
let source = self.source.get_or_try_init(|| async { vault() }).await?;
Ok(source.as_ref())
}
}
impl SecretSource for LazyVault {
async fn fetch(&self, reference: &VaultRef) -> Result<SecretString, SecretError> {
match self.source().await? {
Some(source) => source.fetch(reference).await,
None => Err(SecretError::VaultNotConfigured),
}
}
}
fn vault() -> Result<Option<VaultSource>, SecretError> {
let home = std::env::var_os("HOME").map(PathBuf::from);
VaultSettings::from_env(|key| std::env::var(key).ok(), home.as_deref())?
.map(|settings| VaultSource::new(&settings))
.transpose()
}