use std::io::{BufRead, Write};
use std::path::{Path, PathBuf};
pub(crate) const TRUST_PROMPT: &str = "No workspace is bound: this folder is not inside a git \
worktree and no `--workspace` was given, so file tools and `run_program` are unavailable \
and the host lane cannot compose. Trust this folder as the session workspace? [t]rust once \
/ [w]orkspace <dir> instead / [c]ontinue unbound. (Startup only; nothing is remembered.)";
pub(crate) const BYPASS_UNBOUND_NO_LANE: &str = "bypass on with no workspace bound: \
run_command is unavailable — the folder prompt needs a terminal, so nothing bound; \
launch with `--workspace <dir>` or answer the trust prompt on a terminal.";
const MAX_TRUST_READS: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum TrustAnswer {
TrustCwd,
Workspace(PathBuf),
ContinueUnbound,
}
#[derive(Debug, Clone)]
pub(crate) struct TrustPromptContext {
pub(crate) is_terminal: bool,
pub(crate) fresh: bool,
pub(crate) has_explicit: bool,
pub(crate) has_pin: bool,
pub(crate) root_bound: bool,
pub(crate) turn_file: bool,
}
pub(crate) fn should_prompt(ctx: &TrustPromptContext) -> bool {
ctx.is_terminal
&& ctx.fresh
&& !ctx.turn_file
&& !ctx.root_bound
&& !ctx.has_pin
&& !ctx.has_explicit
}
pub(crate) fn parse_trust_answer(line: &str) -> Result<TrustAnswer, String> {
let trimmed = line.trim();
if trimmed.eq_ignore_ascii_case("t") || trimmed.eq_ignore_ascii_case("trust") {
return Ok(TrustAnswer::TrustCwd);
}
if trimmed.eq_ignore_ascii_case("c")
|| trimmed.eq_ignore_ascii_case("continue")
|| trimmed.eq_ignore_ascii_case("continue unbound")
{
return Ok(TrustAnswer::ContinueUnbound);
}
let Some(tail) = trimmed
.strip_prefix('w')
.or_else(|| trimmed.strip_prefix('W'))
else {
return Err(format!(
"unrecognized answer {trimmed:?}: answer `t` (trust once), `w <dir>` \
(a different directory), or `c` (continue unbound)"
));
};
let dir = tail.trim();
if dir.is_empty() {
return Err("`w` names a directory: `w <dir>` binds that directory instead".to_owned());
}
Ok(TrustAnswer::Workspace(PathBuf::from(dir)))
}
pub(crate) fn resolve_trusted_dir(dir: &Path) -> Result<PathBuf, String> {
let canonical = std::fs::canonicalize(dir).map_err(|error| {
format!(
"the trusted folder {} could not be resolved: {error}; name an existing directory",
dir.display()
)
})?;
if !canonical.is_dir() {
return Err(format!(
"the trusted folder {} is not a directory; name an existing directory",
dir.display()
));
}
Ok(canonical)
}
pub(crate) fn ask_trust(
input: &mut dyn BufRead,
output: &mut dyn Write,
) -> Result<TrustAnswer, String> {
for _ in 0..MAX_TRUST_READS {
writeln!(output, "{TRUST_PROMPT}")
.map_err(|error| format!("the trust prompt could not be written: {error}"))?;
output
.flush()
.map_err(|error| format!("the trust prompt could not be written: {error}"))?;
let mut line = String::new();
let read = input
.read_line(&mut line)
.map_err(|error| format!("the trust answer could not be read: {error}"))?;
if read == 0 {
return Ok(TrustAnswer::ContinueUnbound);
}
match parse_trust_answer(&line) {
Ok(TrustAnswer::Workspace(dir)) => {
let resolved = resolve_trusted_dir(&dir)?;
return Ok(TrustAnswer::Workspace(resolved));
}
Ok(answer) => return Ok(answer),
Err(_) => continue,
}
}
Ok(TrustAnswer::ContinueUnbound)
}
pub(crate) fn trusted_root_line(root: &Path) -> String {
format!(
"workspace trusted for this session only: {} (nothing is remembered)",
root.display()
)
}
pub(crate) fn bypass_no_lane_note(bypass: bool, host_composed: bool) -> Option<String> {
(bypass && !host_composed).then(|| BYPASS_UNBOUND_NO_LANE.to_owned())
}