use aacode_rs::config::AgentConfig;
use aacode_rs::runtime::AgentRuntime;
use aacode_rs::stream::StdoutSink;
use std::io::Write;
use std::sync::atomic::AtomicBool;
fn load_dotenv(path: &std::path::Path) {
if !path.exists() {
return;
}
let content = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(_) => return,
};
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if let Some(eq) = trimmed.find('=') {
let key = trimmed[..eq].trim();
let val = trimmed[eq + 1..].trim();
if !key.is_empty() {
if std::env::var(key).is_err() {
std::env::set_var(key, val);
}
}
}
}
}
#[cfg(all(feature = "pty", unix))]
fn run_terminal(args: &[String]) {
use std::io::{Read, Write};
use std::sync::{Arc, Mutex};
use std::time::Duration;
let shell = args.first().map(String::as_str).unwrap_or("/bin/sh");
let (cols, rows) = term_size();
let mgr = Arc::new(Mutex::new(aacode_rs::terminal::TerminalManager::new(2)));
let id = match mgr
.lock()
.unwrap()
.start(shell, std::path::Path::new("."), &[], cols, rows)
{
Ok(id) => id,
Err(e) => {
eprintln!("terminal: {e}");
return;
}
};
eprintln!("aacode terminal — {shell} ({cols}x{rows}). Type `exit` to leave.");
let _raw = RawMode::enable();
let writer = {
let mgr = mgr.clone();
let id = id.clone();
std::thread::spawn(move || {
let mut stdin = std::io::stdin();
let mut buf = [0u8; 1024];
loop {
match stdin.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if mgr.lock().unwrap().write(&id, &buf[..n]).is_err() {
break;
}
}
Err(_) => break,
}
}
})
};
let mut cursor = 0u64;
loop {
let read = mgr.lock().unwrap().read(&id, cursor, 8192);
match read {
Ok(r) => {
if !r.data.is_empty() {
let mut out = std::io::stdout();
let _ = out.write_all(&r.data);
let _ = out.flush();
cursor = r.cursor;
}
if r.eof {
break;
}
}
Err(_) => break,
}
std::thread::sleep(Duration::from_millis(15));
}
let _ = mgr.lock().unwrap().close(&id);
let _ = writer;
}
#[cfg(all(feature = "pty", unix))]
struct RawMode {
orig: libc::termios,
}
#[cfg(all(feature = "pty", unix))]
impl RawMode {
fn enable() -> Option<Self> {
unsafe {
let mut t: libc::termios = std::mem::zeroed();
if libc::tcgetattr(0, &mut t) != 0 {
return None;
}
let orig = t;
libc::cfmakeraw(&mut t);
if libc::tcsetattr(0, libc::TCSANOW, &t) != 0 {
return None;
}
Some(RawMode { orig })
}
}
}
#[cfg(all(feature = "pty", unix))]
impl Drop for RawMode {
fn drop(&mut self) {
unsafe {
libc::tcsetattr(0, libc::TCSANOW, &self.orig);
}
}
}
#[cfg(all(feature = "pty", unix))]
fn term_size() -> (u16, u16) {
unsafe {
let mut ws: libc::winsize = std::mem::zeroed();
if libc::ioctl(0, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 && ws.ws_row > 0 {
(ws.ws_col, ws.ws_row)
} else {
(80, 24)
}
}
}
fn main() {
let tokio_rt = tokio::runtime::Runtime::new().expect("tokio runtime");
let _args: Vec<String> = std::env::args().collect();
let args: Vec<String> = std::env::args().collect();
if args.get(1).map(String::as_str) == Some("terminal") {
#[cfg(all(feature = "pty", unix))]
{
run_terminal(&args[2..]);
return;
}
#[cfg(not(all(feature = "pty", unix)))]
{
eprintln!("terminal requires building aacode-rs with --features pty on Unix");
return;
}
}
let mut project = ".".to_string();
let mut session_id: Option<String> = None;
let mut plan_first = false;
let mut interactive = false;
let mut task_parts: Vec<String> = Vec::new();
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"-p" | "--project" => {
i += 1;
if i < args.len() {
project = args[i].clone();
}
}
"--session" => {
i += 1;
if i < args.len() {
session_id = Some(args[i].clone());
}
}
"--plan-first" => plan_first = true,
"--interactive" | "-i" => interactive = true,
"-h" | "--help" => {
print_help();
return;
}
other => task_parts.push(other.to_string()),
}
i += 1;
}
load_dotenv(&std::env::current_dir().unwrap_or_default().join(".env"));
load_dotenv(&std::path::PathBuf::from(&project).join(".env"));
let mut config = AgentConfig::default();
config.apply_env();
config.plan_first = plan_first;
let errs = config.validate();
if !errs.is_empty() {
eprintln!("Configuration error:");
for e in errs {
eprintln!(" - {e}");
}
eprintln!("\nSet LLM_API_KEY (and optionally LLM_API_URL / LLM_MODEL_NAME / LLM_GATEWAY).");
std::process::exit(1);
}
let project_path = std::path::PathBuf::from(&project);
let agent_rt = match AgentRuntime::init(config, project_path) {
Ok(r) => r,
Err(e) => {
eprintln!("Failed to initialize: {e}");
std::process::exit(1);
}
};
let sink = StdoutSink::new(true); let cancel = AtomicBool::new(false);
if interactive {
run_interactive(&tokio_rt, &agent_rt, &sink, &cancel);
return;
}
let task = task_parts.join(" ");
if task.trim().is_empty() {
run_interactive(&tokio_rt, &agent_rt, &sink, &cancel);
return;
}
match tokio_rt.block_on(agent_rt.run_task(&task, session_id.as_deref(), &sink, &cancel)) {
Ok(res) => {
println!(
"\n[status: {:?}, iterations: {}]",
res.status, res.iterations
);
}
Err(e) => {
eprintln!("Task failed: {e}");
std::process::exit(1);
}
}
}
fn run_interactive(
tokio_rt: &tokio::runtime::Runtime,
agent_rt: &AgentRuntime,
sink: &StdoutSink,
cancel: &AtomicBool,
) {
println!("aacode-rs interactive mode. Type a task, or 'exit' to quit.");
loop {
print!("\n> ");
let _ = std::io::stdout().flush();
let mut line = String::new();
if std::io::stdin().read_line(&mut line).is_err() {
break;
}
let task = line.trim();
if task.is_empty() {
continue;
}
if matches!(task, "exit" | "quit" | "q") {
println!("bye");
break;
}
match tokio_rt.block_on(agent_rt.run_task(task, None, sink, cancel)) {
Ok(res) => println!(
"\n[status: {:?}, iterations: {}]",
res.status, res.iterations
),
Err(e) => eprintln!("error: {e}"),
}
}
}
fn print_help() {
println!(
"aacode-rs — Rust coding agent\n\n\
USAGE:\n \
aacode [-p <project>] [--session <id>] [--plan-first] \"task\"\n \
aacode [-p <project>] --interactive\n\n\
OPTIONS:\n \
-p, --project <dir> Project sandbox directory (default: .)\n \
--session <id> Continue an existing session\n \
--plan-first Plan before executing\n \
-i, --interactive Interactive session mode\n \
-h, --help Show this help\n\n\
ENV:\n \
LLM_API_KEY, LLM_API_URL, LLM_MODEL_NAME, LLM_GATEWAY, SEARCHXNG_URL"
);
}