feat: improve output of exec subcommand (#719)
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
mod cli;
|
||||
mod event_processor;
|
||||
|
||||
use std::io::IsTerminal;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use cli::Cli;
|
||||
@@ -8,76 +11,59 @@ use codex_core::config::ConfigOverrides;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::Event;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::FileChange;
|
||||
use codex_core::protocol::InputItem;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::util::is_inside_git_repo;
|
||||
use event_processor::EventProcessor;
|
||||
use owo_colors::OwoColorize;
|
||||
use owo_colors::Style;
|
||||
use tracing::debug;
|
||||
use tracing::error;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
/// Returns `true` if a recognised API key is present in the environment.
|
||||
///
|
||||
/// At present we only support `OPENAI_API_KEY`, mirroring the behaviour of the
|
||||
/// Node-based `codex-cli`. Additional providers can be added here when the
|
||||
/// Rust implementation gains first-class support for them.
|
||||
fn has_api_key() -> bool {
|
||||
std::env::var("OPENAI_API_KEY")
|
||||
.map(|s| !s.trim().is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
||||
// TODO(mbolin): Take a more thoughtful approach to logging.
|
||||
let default_level = "error";
|
||||
let allow_ansi = true;
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env()
|
||||
.or_else(|_| EnvFilter::try_new(default_level))
|
||||
.unwrap(),
|
||||
)
|
||||
.with_ansi(allow_ansi)
|
||||
.with_writer(std::io::stderr)
|
||||
.try_init();
|
||||
|
||||
let Cli {
|
||||
images,
|
||||
model,
|
||||
sandbox_policy,
|
||||
skip_git_repo_check,
|
||||
disable_response_storage,
|
||||
color,
|
||||
prompt,
|
||||
..
|
||||
} = cli;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// API key handling
|
||||
// ---------------------------------------------------------------------
|
||||
let (stdout_with_ansi, stderr_with_ansi) = match color {
|
||||
cli::Color::Always => (true, true),
|
||||
cli::Color::Never => (false, false),
|
||||
cli::Color::Auto => (
|
||||
std::io::stdout().is_terminal(),
|
||||
std::io::stderr().is_terminal(),
|
||||
),
|
||||
};
|
||||
|
||||
if !has_api_key() {
|
||||
eprintln!(
|
||||
"\n{msg}\n\nSet the environment variable {var} and re-run this command.\nYou can create a key here: {url}\n",
|
||||
msg = "Missing OpenAI API key.".red(),
|
||||
var = "OPENAI_API_KEY".bold(),
|
||||
url = "https://platform.openai.com/account/api-keys".bold().underline(),
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
assert_api_key(stderr_with_ansi);
|
||||
|
||||
if !skip_git_repo_check && !is_inside_git_repo() {
|
||||
eprintln!("Not inside a Git repo and --skip-git-repo-check was not specified.");
|
||||
std::process::exit(1);
|
||||
} else if images.is_empty() && prompt.is_none() {
|
||||
eprintln!("No images or prompt specified.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// TODO(mbolin): Take a more thoughtful approach to logging.
|
||||
let default_level = "error";
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env()
|
||||
.or_else(|_| EnvFilter::try_new(default_level))
|
||||
.unwrap(),
|
||||
)
|
||||
.with_ansi(stderr_with_ansi)
|
||||
.with_writer(std::io::stderr)
|
||||
.try_init();
|
||||
|
||||
// Load configuration and determine approval policy
|
||||
let overrides = ConfigOverrides {
|
||||
model: model.clone(),
|
||||
model,
|
||||
// This CLI is intended to be headless and has no affordances for asking
|
||||
// the user for approval.
|
||||
approval_policy: Some(AskForApproval::Never),
|
||||
@@ -115,7 +101,6 @@ pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
||||
res = codex.next_event() => match res {
|
||||
Ok(event) => {
|
||||
debug!("Received event: {event:?}");
|
||||
process_event(&event);
|
||||
if let Err(e) = tx.send(event) {
|
||||
error!("Error sending event: {e:?}");
|
||||
break;
|
||||
@@ -131,8 +116,8 @@ pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
||||
});
|
||||
}
|
||||
|
||||
// Send images first, if any.
|
||||
if !images.is_empty() {
|
||||
// Send images first.
|
||||
let items: Vec<InputItem> = images
|
||||
.into_iter()
|
||||
.map(|path| InputItem::LocalImage { path })
|
||||
@@ -146,101 +131,56 @@ pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(prompt) = prompt {
|
||||
// Send the prompt.
|
||||
let items: Vec<InputItem> = vec![InputItem::Text { text: prompt }];
|
||||
let initial_prompt_task_id = codex.submit(Op::UserInput { items }).await?;
|
||||
info!("Sent prompt with event ID: {initial_prompt_task_id}");
|
||||
while let Some(event) = rx.recv().await {
|
||||
if event.id == initial_prompt_task_id && matches!(event.msg, EventMsg::TaskComplete) {
|
||||
break;
|
||||
}
|
||||
// Send the prompt.
|
||||
let items: Vec<InputItem> = vec![InputItem::Text { text: prompt }];
|
||||
let initial_prompt_task_id = codex.submit(Op::UserInput { items }).await?;
|
||||
info!("Sent prompt with event ID: {initial_prompt_task_id}");
|
||||
|
||||
// Run the loop until the task is complete.
|
||||
let mut event_processor = EventProcessor::create_with_ansi(stdout_with_ansi);
|
||||
while let Some(event) = rx.recv().await {
|
||||
let last_event =
|
||||
event.id == initial_prompt_task_id && matches!(event.msg, EventMsg::TaskComplete);
|
||||
event_processor.process_event(event);
|
||||
if last_event {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_event(event: &Event) {
|
||||
let Event { id, msg } = event;
|
||||
match msg {
|
||||
EventMsg::Error { message } => {
|
||||
println!("Error: {message}");
|
||||
}
|
||||
EventMsg::BackgroundEvent { .. } => {
|
||||
// Ignore these for now.
|
||||
}
|
||||
EventMsg::TaskStarted => {
|
||||
println!("Task started: {id}");
|
||||
}
|
||||
EventMsg::TaskComplete => {
|
||||
println!("Task complete: {id}");
|
||||
}
|
||||
EventMsg::AgentMessage { message } => {
|
||||
println!("Agent message: {message}");
|
||||
}
|
||||
EventMsg::ExecCommandBegin {
|
||||
call_id,
|
||||
command,
|
||||
cwd,
|
||||
} => {
|
||||
println!("exec('{call_id}'): {:?} in {cwd}", command);
|
||||
}
|
||||
EventMsg::ExecCommandEnd {
|
||||
call_id,
|
||||
stdout,
|
||||
stderr,
|
||||
exit_code,
|
||||
} => {
|
||||
let output = if *exit_code == 0 { stdout } else { stderr };
|
||||
let truncated_output = output.lines().take(5).collect::<Vec<_>>().join("\n");
|
||||
println!("exec('{call_id}') exited {exit_code}:\n{truncated_output}");
|
||||
}
|
||||
EventMsg::PatchApplyBegin {
|
||||
call_id,
|
||||
auto_approved,
|
||||
changes,
|
||||
} => {
|
||||
let changes = changes
|
||||
.iter()
|
||||
.map(|(path, change)| {
|
||||
format!("{} {}", format_file_change(change), path.to_string_lossy())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
println!("apply_patch('{call_id}') auto_approved={auto_approved}:\n{changes}");
|
||||
}
|
||||
EventMsg::PatchApplyEnd {
|
||||
call_id,
|
||||
stdout,
|
||||
stderr,
|
||||
success,
|
||||
} => {
|
||||
let (exit_code, output) = if *success { (0, stdout) } else { (1, stderr) };
|
||||
let truncated_output = output.lines().take(5).collect::<Vec<_>>().join("\n");
|
||||
println!("apply_patch('{call_id}') exited {exit_code}:\n{truncated_output}");
|
||||
}
|
||||
EventMsg::ExecApprovalRequest { .. } => {
|
||||
// Should we exit?
|
||||
}
|
||||
EventMsg::ApplyPatchApprovalRequest { .. } => {
|
||||
// Should we exit?
|
||||
}
|
||||
_ => {
|
||||
// Ignore event.
|
||||
}
|
||||
/// If a valid API key is not present in the environment, print an error to
|
||||
/// stderr and exits with 1; otherwise, does nothing.
|
||||
fn assert_api_key(stderr_with_ansi: bool) {
|
||||
if !has_api_key() {
|
||||
let (msg_style, var_style, url_style) = if stderr_with_ansi {
|
||||
(
|
||||
Style::new().red(),
|
||||
Style::new().bold(),
|
||||
Style::new().bold().underline(),
|
||||
)
|
||||
} else {
|
||||
(Style::new(), Style::new(), Style::new())
|
||||
};
|
||||
|
||||
eprintln!(
|
||||
"\n{msg}\n\nSet the environment variable {var} and re-run this command.\nYou can create a key here: {url}\n",
|
||||
msg = "Missing OpenAI API key.".style(msg_style),
|
||||
var = "OPENAI_API_KEY".style(var_style),
|
||||
url = "https://platform.openai.com/account/api-keys".style(url_style),
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn format_file_change(change: &FileChange) -> &'static str {
|
||||
match change {
|
||||
FileChange::Add { .. } => "A",
|
||||
FileChange::Delete => "D",
|
||||
FileChange::Update {
|
||||
move_path: Some(_), ..
|
||||
} => "R",
|
||||
FileChange::Update {
|
||||
move_path: None, ..
|
||||
} => "M",
|
||||
}
|
||||
/// Returns `true` if a recognized API key is present in the environment.
|
||||
///
|
||||
/// At present we only support `OPENAI_API_KEY`, mirroring the behavior of the
|
||||
/// Node-based `codex-cli`. Additional providers can be added here when the
|
||||
/// Rust implementation gains first-class support for them.
|
||||
fn has_api_key() -> bool {
|
||||
std::env::var("OPENAI_API_KEY")
|
||||
.map(|s| !s.trim().is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user