use super::goal::mint_name_of;
use super::{StartInputs, identity_preamble};
use crate::actions::verbs::{self, Outcome, log_step_failure};
use crate::binding::work_worktree_path;
use crate::cli_outbound::Cli;
use crate::names::{MintError, Rng};
use crate::opslog::{self, OpEntry};
use crate::world::seed;
use crate::world::toolgate::{Refusal, ToolchainState};
use std::io;
use std::path::{Path, PathBuf};
const CREATE: &str = "create";
const CLAIM: &str = "claim";
const AS: &str = "--as";
const BODY: &str = "--body";
const NEW: &str = "new";
const PROMPT: &str = "prompt";
const YOG_NAME: &str = "YOG_NAME";
const REPO_MARK: &str = "repo.git";
const MINT: &str = "mint";
const MKDIR: &str = "mkdir";
const DRIFT: &str = "cross-check";
pub use crate::opslog::DETACHED_EXIT;
pub struct Deps {
pub bl: Cli,
pub lernie: Cli,
pub state_root: PathBuf,
pub gate: ToolchainState,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClaimResolved {
pub worktree: PathBuf,
pub suffixed: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Prepared {
pub name: String,
pub workspace: PathBuf,
pub cwd: PathBuf,
pub goal: String,
}
#[derive(Debug, thiserror::Error)]
pub enum StartError {
#[error("`{verb}` failed (exit {}): {}", .outcome.exit, .outcome.stderr)]
VerbFailed {
verb: &'static str,
outcome: Outcome,
},
#[error("claim worktree drift: bl printed {stdout:?}, expected {canonical} or {suffixed}")]
Drift {
stdout: String,
canonical: String,
suffixed: String,
},
#[error(transparent)]
Mint(#[from] MintError),
#[error(transparent)]
Seed(#[from] seed::SeedError),
#[error(transparent)]
GateRefused(#[from] Refusal),
#[error(transparent)]
Io(#[from] io::Error),
}
pub fn on_mint(
result: Result<String, MintError>,
state_root: &Path,
ts: &str,
cwd: &Path,
) -> Result<String, StartError> {
match result {
Ok(name) => Ok(name),
Err(e) => {
log_step_failure(state_root, ts, cwd, MINT, &e.to_string())?;
Err(StartError::Mint(e))
}
}
}
pub fn resolve_name(
inputs: &StartInputs,
rng: &mut dyn Rng,
state_root: &Path,
ts: &str,
) -> Result<String, StartError> {
let minted = mint_name_of(
&inputs.target,
&inputs.yog_data_root,
&inputs.claimants,
rng,
);
on_mint(minted, state_root, ts, &inputs.home)
}
pub fn execute_create(
bl: &Cli,
state_root: &Path,
ts: &str,
project: &Path,
title: &str,
body: &str,
name: &str,
) -> Result<String, StartError> {
let mut args = vec![CREATE, title, AS, name];
if !body.is_empty() {
args.extend([BODY, body]);
}
let out = verbs::run_logged(bl, state_root, ts, project, &args)?;
verb_ok(out, CREATE).map(|o| o.stdout.trim().to_owned())
}
pub fn execute_claim(
bl: &Cli,
state_root: &Path,
ts: &str,
project: &Path,
id: &str,
name: &str,
balls_state_root: &Path,
) -> Result<ClaimResolved, StartError> {
let out = verb_ok(
verbs::run_logged(bl, state_root, ts, project, &[CLAIM, id, AS, name])?,
CLAIM,
)?;
cross_check_claim(
&out.stdout,
balls_state_root,
project,
id,
name,
state_root,
ts,
)
}
pub fn cross_check_claim(
stdout: &str,
balls_state_root: &Path,
project: &Path,
id: &str,
name: &str,
state_root: &Path,
ts: &str,
) -> Result<ClaimResolved, StartError> {
let got = PathBuf::from(stdout.trim());
let canonical = work_worktree_path(balls_state_root, project, id, None);
let suffixed = work_worktree_path(balls_state_root, project, id, Some(name));
if got == canonical {
return Ok(ClaimResolved {
worktree: canonical,
suffixed: false,
});
}
if got == suffixed {
return Ok(ClaimResolved {
worktree: suffixed,
suffixed: true,
});
}
let err = StartError::Drift {
stdout: got.display().to_string(),
canonical: canonical.display().to_string(),
suffixed: suffixed.display().to_string(),
};
log_step_failure(state_root, ts, project, DRIFT, &err.to_string())?;
Err(err)
}
pub fn execute_ensure_workspace(
lernie: &Cli,
state_root: &Path,
ts: &str,
workspace: &Path,
) -> Result<bool, StartError> {
if workspace.join(REPO_MARK).exists() {
return Ok(false);
}
let parent = workspace.parent().unwrap_or(workspace);
if let Err(e) = std::fs::create_dir_all(parent) {
log_step_failure(state_root, ts, parent, MKDIR, &e.to_string())?;
return Err(StartError::Io(e));
}
let ws_s = workspace.to_string_lossy();
verb_ok(
verbs::run_logged(lernie, state_root, ts, parent, &[NEW, &ws_s])?,
NEW,
)?;
Ok(true)
}
pub fn execute_prompt(
lernie: &Cli,
state_root: &Path,
ts: &str,
name: &str,
cwd: &Path,
workspace: &Path,
goal: &str,
) -> io::Result<()> {
let composed = format!("{}\n\n{goal}", identity_preamble(name));
let ws_s = workspace.to_string_lossy();
let named = lernie.and_env(vec![(YOG_NAME.to_owned(), name.to_owned())]);
let sink = opslog::detached::sink(state_root, ts, workspace);
let spawn = named.spawn_detached(Some(cwd), &sink, &[PROMPT, ws_s.as_ref(), &composed]);
let entry = OpEntry {
ts: ts.to_owned(),
argv: vec![
lernie.binary().display().to_string(),
PROMPT.to_owned(),
ws_s.into_owned(),
composed,
],
cwd: cwd.display().to_string(),
exit: DETACHED_EXIT,
stdout: String::new(),
stderr: spawn
.as_ref()
.err()
.map(ToString::to_string)
.unwrap_or_default(),
};
opslog::append(state_root, &opslog::clip_goal(&entry))?;
spawn.map(|_pid| ()).map_err(io::Error::other)
}
fn verb_ok(out: Outcome, verb: &'static str) -> Result<Outcome, StartError> {
if out.ok() {
Ok(out)
} else {
Err(StartError::VerbFailed { verb, outcome: out })
}
}