outrig_cli/cli/
discard.rs1use std::future::Future;
8use std::path::{Path, PathBuf};
9
10use clap::{ArgGroup, Parser};
11use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncWrite, AsyncWriteExt};
12
13use crate::error::{OutrigError, Result};
14use crate::session::{self, Session, SessionStore};
15use outrig::container::Container;
16
17#[derive(Debug, Parser)]
18#[command(group(
19 ArgGroup::new("discard_target")
20 .args(["session", "session_dir"])
21 .multiple(false)
22 .required(false)
23))]
24pub struct DiscardArgs {
25 pub session: Option<String>,
27 #[arg(short = 'y', long = "yes")]
29 pub yes: bool,
30 #[arg(long = "session-dir", value_name = "PATH")]
32 pub session_dir: Option<PathBuf>,
33}
34
35pub async fn execute(
36 args: &DiscardArgs,
37 session_root_flag: Option<&Path>,
38 repo_cfg_override: Option<&Path>,
39 global_cfg_path: &Path,
40 cwd: &Path,
41) -> Result<i32> {
42 let root = if args.session_dir.is_some() {
46 PathBuf::new()
47 } else {
48 session::resolve_session_root_for_cli(
49 session_root_flag,
50 repo_cfg_override,
51 global_cfg_path,
52 cwd,
53 )?
54 };
55 let store = SessionStore::new(root);
56 let stdin = tokio::io::BufReader::new(tokio::io::stdin());
57 let mut stderr = tokio::io::stderr();
58 execute_with(&mut stderr, stdin, &store, args, podman_is_running).await
59}
60
61pub async fn execute_with<E, R, F, Fut>(
66 stderr: &mut E,
67 stdin: R,
68 store: &SessionStore,
69 args: &DiscardArgs,
70 is_running: F,
71) -> Result<i32>
72where
73 E: AsyncWrite + Unpin,
74 R: AsyncBufRead + Unpin,
75 F: FnOnce(String) -> Fut,
76 Fut: Future<Output = Result<bool>>,
77{
78 let target = resolve_target(args, store)?;
79
80 if is_running(target.session.container_name.clone()).await? {
81 return Err(OutrigError::Configuration(format!(
82 "session {} is still running (container {}); stop it before discarding",
83 target.session.id, target.session.container_name
84 ))
85 .into());
86 }
87
88 if !args.yes && !confirm(stderr, stdin, &target.dir).await? {
89 stderr.write_all(b"[outrig] aborted\n").await?;
90 return Ok(0);
91 }
92
93 if args.session_dir.is_some() {
94 store.remove_by_path(&target.dir)?;
95 } else {
96 store.remove_by_id(&target.session.id)?;
97 }
98
99 let dir_msg = format!("[outrig] removed {}\n", target.dir.display());
100 stderr.write_all(dir_msg.as_bytes()).await?;
101 if args.session_dir.is_none() && target.session.link_target.is_some() {
102 let link_path = store.symlink_path(&target.session.id);
103 let link_msg = format!("[outrig] removed {} (symlink)\n", link_path.display());
104 stderr.write_all(link_msg.as_bytes()).await?;
105 }
106 Ok(0)
107}
108
109async fn confirm<E, R>(stderr: &mut E, mut stdin: R, dir: &Path) -> Result<bool>
110where
111 E: AsyncWrite + Unpin,
112 R: AsyncBufRead + Unpin,
113{
114 let prompt = format!("Discard {}? [y/N]: ", dir.display());
115 stderr.write_all(prompt.as_bytes()).await?;
116 stderr.flush().await?;
117 let mut line = String::new();
118 stdin.read_line(&mut line).await?;
119 let answer = line.trim().to_ascii_lowercase();
120 Ok(answer == "y" || answer == "yes")
121}
122
123struct Target {
124 dir: PathBuf,
125 session: Session,
126}
127
128fn resolve_target(args: &DiscardArgs, store: &SessionStore) -> Result<Target> {
129 if let Some(dir) = args.session_dir.as_deref() {
130 let session = store.get_by_path(dir)?;
131 return Ok(Target {
132 dir: dir.to_path_buf(),
133 session,
134 });
135 }
136 let Some(query) = args.session.as_deref() else {
137 return Err(OutrigError::Configuration(
138 "outrig discard requires either a session id or --session-dir".to_string(),
139 )
140 .into());
141 };
142 let (dir, session) = super::resolve_session_arg(store, query)?;
143 Ok(Target { dir, session })
144}
145
146async fn podman_is_running(name: String) -> Result<bool> {
147 Ok(Container::is_running(&name).await?)
148}