use std::path::{Path, PathBuf};
use crate::cli_outbound::{
Cli, Streamed, StreamedLine, StreamedOutcome, StreamedPoll, stderr_text,
};
use crate::opslog::{self, OpEntry, Origin};
pub mod auth;
pub mod runs;
pub mod wire;
#[cfg(test)]
mod tests;
const LOGIN_FLAG: &str = "--login";
const PROVIDER_FLAG: &str = "--provider";
const BROWSER_FLAG: &str = "--browser";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Flow {
Browser,
Device,
}
fn login_args(provider: &str, flow: Flow) -> Vec<&str> {
let mut args = vec![LOGIN_FLAG, PROVIDER_FLAG, provider];
if flow == Flow::Browser {
args.push(BROWSER_FLAG);
}
args
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LoginView {
pub lines: Vec<StreamedLine>,
pub outcome: Option<i32>,
pub fallback: Option<String>,
}
pub struct LoginRun {
streamed: Streamed,
view: LoginView,
argv: Vec<String>,
by_hand: String,
state_root: PathBuf,
ts: String,
}
pub fn by_hand(workspace: Option<&Path>, provider: &str, flow: Flow) -> String {
let args = login_args(provider, flow).join(" ");
let Some(ws) = workspace else {
return format!(
"no workspace: a sign-in belongs to one, so there is nothing to run by hand — \
focus a workspace, then yog {} {} <workspace> bz {args}",
crate::world::hatch::EXEC_SUBCMD,
crate::world::hatch::WS_FLAG,
);
};
format!(
"yog {} {} {} bz {args}",
crate::world::hatch::EXEC_SUBCMD,
crate::world::hatch::WS_FLAG,
ws.display(),
)
}
pub fn start(
bz: &Cli,
provider: &str,
state_root: &Path,
ts: &str,
workspace: Option<&Path>,
flow: Flow,
) -> std::io::Result<LoginRun> {
let args = login_args(provider, flow);
let mut argv = vec![bz.binary().display().to_string()];
argv.extend(args.iter().map(|s| (*s).to_string()));
match bz.run(&args) {
Ok(stream) => Ok(LoginRun {
streamed: Streamed::new(stream),
view: LoginView::default(),
argv,
by_hand: by_hand(workspace, provider, flow),
state_root: state_root.to_path_buf(),
ts: ts.to_owned(),
}),
Err(spawn) => {
let entry = OpEntry::synthetic_failure(
ts.to_owned(),
argv,
String::new(),
spawn.to_string(),
Origin::World,
bz.client(),
);
opslog::append(state_root, &entry)?;
Err(std::io::Error::other(spawn))
}
}
}
impl LoginRun {
pub fn view(&self) -> LoginView {
self.view.clone()
}
pub fn poll(&mut self) -> bool {
if self.view.outcome.is_some() {
return false;
}
match self.streamed.poll() {
StreamedPoll::Lines(lines) => {
self.view.lines.extend(lines);
true
}
StreamedPoll::Pending => true,
StreamedPoll::Done(outcome) => {
self.finalize(outcome);
false
}
}
}
fn finalize(&mut self, outcome: StreamedOutcome) {
self.view.lines.extend(outcome.lines);
self.view.outcome = Some(outcome.exit);
if outcome.exit != 0 {
self.view.fallback = Some(self.by_hand.clone());
}
let entry = OpEntry {
ts: self.ts.clone(),
argv: self.argv.clone(),
cwd: String::new(),
exit: outcome.exit,
stdout: String::new(),
stderr: stderr_text(&self.view.lines),
origin: Origin::World,
client: crate::registry::Client::default(),
};
let _ = opslog::append(&self.state_root, &entry);
}
#[cfg(test)]
pub(crate) fn from_streamed(streamed: Streamed, argv: Vec<String>, state_root: &Path) -> Self {
Self {
streamed,
view: LoginView::default(),
by_hand: by_hand(Some(Path::new("/ws")), "openai", Flow::Browser),
argv,
state_root: state_root.to_path_buf(),
ts: "TS".to_owned(),
}
}
}