fn prompt_yes_no(question: &str) -> bool {
if std::env::var("ROBOTICUS_YES")
.ok()
.as_deref()
.map(|v| v == "1")
.unwrap_or(false)
{
return true;
}
print!("{question} [y/N] ");
let _ = io::stdout().flush();
let mut input = String::new();
if io::stdin().read_line(&mut input).is_err() {
return false;
}
matches!(input.trim().to_ascii_lowercase().as_str(), "y" | "yes")
}
fn has_toml_section(raw: &str, section: &str) -> bool {
raw.lines().any(|line| line.trim() == section)
}
fn migrate_removed_legacy_config_if_needed(
config_path: &Path,
repair: bool,
) -> Result<Option<roboticus_core::config::ConfigMigrationReport>, Box<dyn std::error::Error>> {
if !repair {
return Ok(None);
}
roboticus_core::config_utils::migrate_removed_legacy_config_file(config_path)
}
fn prompt_line(prompt: &str) -> String {
print!("{prompt}");
let _ = io::stdout().flush();
let mut input = String::new();
if io::stdin().read_line(&mut input).is_err() {
return String::new();
}
input.trim().to_string()
}
fn path_contains_dir_in(dir: &Path, path_var: &std::ffi::OsStr) -> bool {
std::env::split_paths(path_var).any(|p| {
#[cfg(windows)]
{
p.to_string_lossy().to_ascii_lowercase() == dir.to_string_lossy().to_ascii_lowercase()
}
#[cfg(not(windows))]
{
p == dir
}
})
}
fn path_contains_dir(dir: &Path) -> bool {
let Some(path_var) = std::env::var_os("PATH") else {
return false;
};
path_contains_dir_in(dir, &path_var)
}
fn go_bin_candidates_with(gopath: Option<&str>) -> Vec<PathBuf> {
let mut out = Vec::new();
if let Some(gopath) = gopath {
out.push(PathBuf::from(gopath).join("bin"));
}
#[cfg(windows)]
if let Ok(profile) = std::env::var("USERPROFILE") {
out.push(PathBuf::from(profile).join("go").join("bin"));
}
#[cfg(not(windows))]
if let Ok(home) = std::env::var("HOME") {
out.push(PathBuf::from(home).join("go").join("bin"));
}
out
}
fn go_bin_candidates() -> Vec<PathBuf> {
go_bin_candidates_with(std::env::var("GOPATH").ok().as_deref())
}
fn find_gosh_in_go_bins_with(gopath: Option<&str>) -> Option<PathBuf> {
#[cfg(windows)]
let gosh_name = "gosh.exe";
#[cfg(not(windows))]
let gosh_name = "gosh";
go_bin_candidates_with(gopath)
.into_iter()
.map(|d| d.join(gosh_name))
.find(|p| p.is_file())
}
fn find_gosh_in_go_bins() -> Option<PathBuf> {
find_gosh_in_go_bins_with(std::env::var("GOPATH").ok().as_deref())
}
fn recent_log_snapshot(log_dir: &Path, max_bytes: usize) -> Option<String> {
let entries = std::fs::read_dir(log_dir).ok()?;
let mut candidates: Vec<(std::time::SystemTime, PathBuf)> = entries
.filter_map(|entry| entry.ok())
.filter_map(|entry| {
let path = entry.path();
let name = path.file_name()?.to_str()?;
if !(name.starts_with("roboticus.log") || name == "roboticus.stderr.log") {
return None;
}
let modified = entry.metadata().ok()?.modified().ok()?;
Some((modified, path))
})
.collect();
candidates.sort_by_key(|(modified, _)| *modified);
let (_, newest) = candidates.into_iter().last()?;
let data = std::fs::read(newest).ok()?;
let start = data.len().saturating_sub(max_bytes);
Some(String::from_utf8_lossy(&data[start..]).to_string())
}
fn count_occurrences(haystack: &str, needle: &str) -> usize {
haystack.match_indices(needle).count()
}
const INTERNALIZED_SKILLS: &[&str] = &[
"update-and-rollback",
"workflow-design",
"skill-creation",
"session-operator",
"claims-auditor",
"efficacy-assessment",
"fast-cache",
"model-routing-tuner",
];
const DEPRECATED_GENERIC_SKILLS: &[&str] =
&["hello", "explain", "plan", "summarize", "review", "search"];