use anyhow::Result;
use console::style;
use regex::Regex;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
pub fn render_pty_output_fn(output: &str, title: &str, command: Option<&str>) -> Result<()> {
println!("{}", style("=".repeat(80)).dim());
println!(
"{} {}",
style("==").blue().bold(),
style(title).blue().bold()
);
if let Some(cmd) = command {
println!("{}", style(format!("> {}", cmd)).dim());
}
println!("{}", style("-".repeat(80)).dim());
print!("{}", output);
std::io::stdout().flush()?;
println!("{}", style("-".repeat(80)).dim());
println!("{}", style("==").blue().bold());
println!("{}", style("=".repeat(80)).dim());
Ok(())
}
pub struct ProjectOverview {
pub name: Option<String>,
pub version: Option<String>,
pub description: Option<String>,
pub readme_excerpt: Option<String>,
pub root: PathBuf,
}
impl ProjectOverview {
pub fn short_for_display(&self) -> String {
let mut out = String::new();
if let Some(name) = &self.name {
out.push_str(&format!("Project: {}", name));
}
if let Some(ver) = &self.version {
if !out.is_empty() {
out.push(' ');
}
out.push_str(&format!("v{}", ver));
}
if !out.is_empty() {
out.push('\n');
}
if let Some(desc) = &self.description {
out.push_str(desc);
out.push('\n');
}
out.push_str(&format!("Root: {}", self.root.display()));
out
}
pub fn as_prompt_block(&self) -> String {
let mut s = String::new();
if let Some(name) = &self.name {
s.push_str(&format!("- Name: {}\n", name));
}
if let Some(ver) = &self.version {
s.push_str(&format!("- Version: {}\n", ver));
}
if let Some(desc) = &self.description {
s.push_str(&format!("- Description: {}\n", desc));
}
s.push_str(&format!("- Workspace Root: {}\n", self.root.display()));
if let Some(excerpt) = &self.readme_excerpt {
s.push_str("- README Excerpt: \n");
s.push_str(excerpt);
if !excerpt.ends_with('\n') {
s.push('\n');
}
}
s
}
}
pub fn build_project_overview(root: &Path) -> Option<ProjectOverview> {
let mut overview = ProjectOverview {
name: None,
version: None,
description: None,
readme_excerpt: None,
root: root.to_path_buf(),
};
let cargo_toml_path = root.join("Cargo.toml");
if let Ok(cargo_toml) = fs::read_to_string(&cargo_toml_path) {
overview.name = extract_toml_str(&cargo_toml, "name");
overview.version = extract_toml_str(&cargo_toml, "version");
overview.description = extract_toml_str(&cargo_toml, "description");
}
let readme_path = root.join("README.md");
if let Ok(readme) = fs::read_to_string(&readme_path) {
overview.readme_excerpt = Some(extract_readme_excerpt(&readme, 1200));
} else {
for alt in [
"QUICKSTART.md",
"user-context.md",
"docs/project/ROADMAP.md",
] {
let path = root.join(alt);
if let Ok(txt) = fs::read_to_string(&path) {
overview.readme_excerpt = Some(extract_readme_excerpt(&txt, 800));
break;
}
}
}
if overview.name.is_none() && overview.readme_excerpt.is_none() {
return None;
}
Some(overview)
}
pub fn extract_toml_str(content: &str, key: &str) -> Option<String> {
let pkg_section = if let Some(start) = content.find("[package]") {
let rest = &content[start + "[package]".len()..];
if let Some(_next) = rest.find('\n') {
&content[start..]
} else {
&content[start..]
}
} else {
content
};
let pattern = format!(r#"(?m)^\s*{}\s*=\s*"([^"]+)"\s*$"#, regex::escape(key));
let re = Regex::new(&pattern).ok()?;
re.captures(pkg_section)
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
}
pub fn extract_readme_excerpt(md: &str, max_len: usize) -> String {
let mut excerpt = String::new();
for line in md.lines() {
if excerpt.len() > max_len {
break;
}
excerpt.push_str(line);
excerpt.push('\n');
if line.trim().starts_with("## ") && excerpt.len() > (max_len / 2) {
break;
}
}
if excerpt.len() > max_len {
excerpt.truncate(max_len);
excerpt.push_str("...\n");
}
excerpt
}
pub fn summarize_workspace_languages(root: &std::path::Path) -> Option<String> {
use indexmap::IndexMap;
let analyzer = match crate::tools::tree_sitter::analyzer::TreeSitterAnalyzer::new() {
Ok(a) => a,
Err(_) => return None,
};
let mut counts: IndexMap<String, usize> = IndexMap::new();
let mut total = 0usize;
for entry in walkdir::WalkDir::new(root)
.max_depth(4)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if path.is_file()
&& let Ok(lang) = analyzer.detect_language_from_path(path)
{
*counts.entry(format!("{:?}", lang)).or_insert(0) += 1;
total += 1;
}
if total > 5000 {
break;
}
}
if counts.is_empty() {
None
} else {
let mut parts: Vec<String> = counts
.into_iter()
.map(|(k, v)| format!("{}:{}", k, v))
.collect();
parts.sort();
Some(parts.join(", "))
}
}
pub fn safe_replace_text(
content: &str,
old_str: &str,
new_str: &str,
) -> Result<String, anyhow::Error> {
if old_str.is_empty() {
return Err(anyhow::anyhow!("old_string cannot be empty"));
}
if !content.contains(old_str) {
return Err(anyhow::anyhow!("Text '{}' not found in file", old_str));
}
Ok(content.replace(old_str, new_str))
}