Skip to content

Commit 17d94cb

Browse files
Copilottknkaa
andcommitted
Add command execution to the coding-human CLI
Co-authored-by: tknkaa <145080781+tknkaa@users.noreply.github.com>
1 parent 051eecd commit 17d94cb

3 files changed

Lines changed: 97 additions & 12 deletions

File tree

cli/src/ask.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use reqwest::Client;
66
use std::io::{self, Write};
77
use tokio_tungstenite::{connect_async, tungstenite::Message};
88

9-
pub async fn run() -> Result<()> {
9+
pub async fn run(yes: bool) -> Result<()> {
1010
dotenvy::dotenv().ok();
1111
let server_url =
1212
std::env::var("SERVER_URL").unwrap_or_else(|_| "http://localhost:8787".to_string());
@@ -86,6 +86,54 @@ pub async fn run() -> Result<()> {
8686
println!();
8787
break;
8888
}
89+
// Check if this is a JSON command message
90+
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&msg) {
91+
if json["type"].as_str() == Some("cmd") {
92+
let command = json["command"].as_str().unwrap_or("").to_string();
93+
println!("\nRun: {}?", command);
94+
95+
let execute = if yes {
96+
true
97+
} else {
98+
print!("[y/n]: ");
99+
io::stdout().flush()?;
100+
let mut answer = String::new();
101+
io::stdin().read_line(&mut answer)?;
102+
answer.trim().eq_ignore_ascii_case("y")
103+
};
104+
105+
let output = if execute {
106+
let result = tokio::process::Command::new("sh")
107+
.arg("-c")
108+
.arg(&command)
109+
.output()
110+
.await?;
111+
let stdout = String::from_utf8_lossy(&result.stdout).to_string();
112+
let stderr = String::from_utf8_lossy(&result.stderr).to_string();
113+
match (stdout.is_empty(), stderr.is_empty()) {
114+
(false, false) => format!("{}\n[stderr]\n{}", stdout, stderr),
115+
(false, true) => stdout,
116+
(true, false) => stderr,
117+
(true, true) => String::new(),
118+
}
119+
} else {
120+
"(skipped)".to_string()
121+
};
122+
123+
let result_msg = serde_json::json!({
124+
"type": "cmd_result",
125+
"command": command,
126+
"output": output,
127+
})
128+
.to_string();
129+
write.send(Message::text(result_msg)).await?;
130+
// Reset spinner for next chunk
131+
first_chunk = true;
132+
spinner.reset();
133+
spinner.set_message("Waiting for answer");
134+
continue;
135+
}
136+
}
89137
print!("{}", msg);
90138
io::stdout().flush()?;
91139
}

cli/src/main.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ struct Cli {
1414
#[derive(Subcommand)]
1515
enum Commands {
1616
/// Ask a question to an available programmer
17-
Ask,
17+
Ask {
18+
/// Automatically execute commands sent by the programmer without prompting
19+
#[arg(long)]
20+
yes: bool,
21+
},
1822
/// Register as a programmer and wait for questions
1923
Serve {
2024
/// Your display name shown to clients
@@ -27,8 +31,8 @@ async fn main() {
2731
let cli = Cli::parse();
2832

2933
match cli.command {
30-
Commands::Ask => {
31-
if let Err(e) = ask::run().await {
34+
Commands::Ask { yes } => {
35+
if let Err(e) = ask::run(yes).await {
3236
eprintln!("Error: {}", e);
3337
std::process::exit(1);
3438
}

cli/src/serve.rs

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,51 @@ pub async fn run(label: String) -> Result<()> {
5555
};
5656

5757
println!("\nQuestion: {}\n", question);
58-
println!("Answer (Ctrl+D to finish):\n");
58+
println!("Answer (Ctrl+D to finish, prefix a line with $ to run a command on client):\n");
5959

6060
loop {
6161
let mut line = String::new();
62-
if async_stdin.read_line(&mut line).await? == 0 {
63-
write.send(Message::Text("[DONE]".to_string())).await?;
64-
println!();
65-
break;
62+
tokio::select! {
63+
n = async_stdin.read_line(&mut line) => {
64+
if n? == 0 {
65+
write.send(Message::Text("[DONE]".to_string())).await?;
66+
println!();
67+
break;
68+
}
69+
let trimmed = line.trim_end_matches('\n');
70+
if let Some(cmd) = trimmed.strip_prefix('$') {
71+
let command = cmd.trim().to_string();
72+
let msg = serde_json::json!({"type": "cmd", "command": command}).to_string();
73+
write.send(Message::text(msg)).await?;
74+
} else {
75+
write.send(Message::text(trimmed)).await?;
76+
}
77+
}
78+
ws_msg = read.next() => {
79+
match ws_msg {
80+
Some(Ok(Message::Text(msg))) => {
81+
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&msg) {
82+
if json["type"].as_str() == Some("cmd_result") {
83+
let command = json["command"].as_str().unwrap_or("");
84+
let output = json["output"].as_str().unwrap_or("");
85+
println!("\n[cmd result] $ {}\n{}", command, output);
86+
}
87+
}
88+
}
89+
Some(Ok(Message::Close(_))) => {
90+
println!("Client disconnected.");
91+
let _ = client
92+
.delete(format!("{}/queue/{}", server_url, room_id))
93+
.send()
94+
.await;
95+
return Ok(());
96+
}
97+
Some(Err(e)) => return Err(e.into()),
98+
None => return Err(anyhow::anyhow!("Connection lost")),
99+
_ => {}
100+
}
101+
}
66102
}
67-
write
68-
.send(Message::text(line.trim_end_matches('\n')))
69-
.await?;
70103
}
71104
}
72105
}

0 commit comments

Comments
 (0)