Skip to content

Commit 72aa6ac

Browse files
committed
feat: add revert_command parameter to terminal_run_async
- Optional parameter to specify how to undo a command's effects - Empty string = no state change (e.g., echo, cat) - null/omit = cannot be reverted (e.g., rm without backup) - String value = command to run to revert changes CLI: ash terminal start 'mkdir foo' --revert 'rmdir foo' MCP: terminal_run_async with revert_command field
1 parent 439253c commit 72aa6ac

File tree

2 files changed

+31
-10
lines changed

2 files changed

+31
-10
lines changed

src/bin/ash.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,9 @@ enum TerminalOp {
346346
workdir: Option<String>,
347347
#[arg(short, long)]
348348
env: Vec<String>,
349+
/// Command to revert changes (empty string = no state change, omit = cannot revert)
350+
#[arg(long)]
351+
revert: Option<String>,
349352
},
350353
/// Get output from handle
351354
Output {
@@ -740,10 +743,11 @@ async fn main() -> anyhow::Result<()> {
740743

741744
Commands::Terminal { op } => {
742745
let (tool_name, args): (&str, Value) = match op {
743-
TerminalOp::Start { command, workdir, env } => {
746+
TerminalOp::Start { command, workdir, env, revert } => {
744747
let env_map = parse_key_value(&env);
745748
("terminal_run_async", serde_json::json!({
746-
"command": command, "working_dir": workdir, "env": env_map, "session_id": session_id
749+
"command": command, "working_dir": workdir, "env": env_map,
750+
"revert_command": revert, "session_id": session_id
747751
}))
748752
}
749753
TerminalOp::Output { handle, tail } => {

src/tools/terminal.rs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ struct AsyncProcess {
9090
stderr_lines: Arc<Mutex<Vec<String>>>,
9191
exit_code: Arc<Mutex<Option<i32>>>,
9292
command: String,
93+
revert_command: Option<String>, // None = cannot revert, Some("") = no state change
9394
started_at: chrono::DateTime<chrono::Utc>,
9495
// Keep child for kill
9596
child: Arc<Mutex<Option<Child>>>,
@@ -105,7 +106,7 @@ impl ProcessRegistry {
105106
Self { processes: HashMap::new() }
106107
}
107108

108-
async fn start(&mut self, command: &str, working_dir: Option<&str>, env: Option<HashMap<String, String>>) -> anyhow::Result<String> {
109+
async fn start(&mut self, command: &str, working_dir: Option<&str>, env: Option<HashMap<String, String>>, revert_command: Option<String>) -> anyhow::Result<String> {
109110
let id = Uuid::new_v4().to_string();
110111

111112
let mut cmd = Command::new("sh");
@@ -186,6 +187,7 @@ impl ProcessRegistry {
186187
stderr_lines,
187188
exit_code,
188189
command: command.to_string(),
190+
revert_command,
189191
started_at: chrono::Utc::now(),
190192
child: child_arc,
191193
};
@@ -237,16 +239,20 @@ impl ProcessRegistry {
237239
false
238240
}
239241

240-
fn list(&self) -> Vec<(String, String, String, bool)> {
242+
fn list(&self) -> Vec<(String, String, String, bool, Option<String>)> {
241243
self.processes.iter().map(|(id, p)| {
242244
let running = p.exit_code.try_lock().map(|e| e.is_none()).unwrap_or(true);
243-
(id.clone(), p.command.clone(), p.started_at.to_rfc3339(), running)
245+
(id.clone(), p.command.clone(), p.started_at.to_rfc3339(), running, p.revert_command.clone())
244246
}).collect()
245247
}
246248

247249
fn remove(&mut self, id: &str) -> bool {
248250
self.processes.remove(id).is_some()
249251
}
252+
253+
fn get_revert_command(&self, id: &str) -> Option<Option<String>> {
254+
self.processes.get(id).map(|p| p.revert_command.clone())
255+
}
250256
}
251257

252258
lazy_static! {
@@ -257,7 +263,7 @@ lazy_static! {
257263
pub async fn local_process_counts() -> (usize, usize) {
258264
let registry = REGISTRY.lock().await;
259265
let list = registry.list();
260-
let running = list.iter().filter(|(_, _, _, r)| *r).count();
266+
let running = list.iter().filter(|(_, _, _, r, _)| *r).count();
261267
(running, list.len() - running)
262268
}
263269

@@ -275,7 +281,11 @@ impl Tool for TerminalRunAsyncTool {
275281
"properties": {
276282
"command": {"type": "string", "description": "Shell command to run"},
277283
"working_dir": {"type": "string", "description": "Working directory"},
278-
"env": {"type": "object", "description": "Environment variables"}
284+
"env": {"type": "object", "description": "Environment variables"},
285+
"revert_command": {
286+
"type": ["string", "null"],
287+
"description": "Command to revert this command's changes. Empty string = no state change, null = cannot revert"
288+
}
279289
},
280290
"required": ["command"]
281291
})
@@ -288,6 +298,7 @@ impl Tool for TerminalRunAsyncTool {
288298
command: String,
289299
working_dir: Option<String>,
290300
env: Option<HashMap<String, String>>,
301+
revert_command: Option<String>,
291302
session_id: Option<String>,
292303
}
293304

@@ -300,6 +311,7 @@ impl Tool for TerminalRunAsyncTool {
300311
let mut remote_args = serde_json::json!({"command": args.command});
301312
if let Some(ref dir) = args.working_dir { remote_args["working_dir"] = serde_json::json!(dir); }
302313
if let Some(ref env) = args.env { remote_args["env"] = serde_json::json!(env); }
314+
if let Some(ref revert) = args.revert_command { remote_args["revert_command"] = serde_json::json!(revert); }
303315
return match session::call_tool_in_session(sid, "terminal_run_async", remote_args).await {
304316
Ok(result) => {
305317
// Persist handle → session_id so later commands auto-resolve
@@ -321,7 +333,7 @@ impl Tool for TerminalRunAsyncTool {
321333
}
322334

323335
let mut registry = REGISTRY.lock().await;
324-
match registry.start(&args.command, args.working_dir.as_deref(), args.env).await {
336+
match registry.start(&args.command, args.working_dir.as_deref(), args.env, args.revert_command).await {
325337
Ok(id) => ToolResult::ok(serde_json::json!({"handle": id}).to_string()),
326338
Err(e) => ToolResult::err(e.to_string()),
327339
}
@@ -466,9 +478,14 @@ impl Tool for TerminalListTool {
466478
let list = registry.list();
467479
if !list.is_empty() {
468480
out.push_str("Local:\n");
469-
for (id, cmd, started, running) in list {
481+
for (id, cmd, started, running, revert) in list {
470482
let status = if running { "running" } else { "complete" };
471-
out.push_str(&format!(" {} [{}] {} ({})\n", id, status, cmd, started));
483+
let revert_info = match &revert {
484+
Some(s) if s.is_empty() => " [no state change]",
485+
Some(_) => " [revertible]",
486+
None => " [non-revertible]",
487+
};
488+
out.push_str(&format!(" {} [{}]{} {} ({})\n", id, status, revert_info, cmd, started));
472489
}
473490
}
474491
}

0 commit comments

Comments
 (0)