8000 feat(minimal): add bulk session destruction · gominimal/minimal@ae0bfa8 · GitHub
[go: up one dir, main page]

Skip to content

Commit ae0bfa8

Browse files
author
Jake King
committed
feat(minimal): add bulk session destruction
1 parent e296663 commit ae0bfa8

2 files changed

Lines changed: 136 additions & 11 deletions

File tree

‎crates/minimal/src/lib.rs‎

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,14 @@ pub struct LsArgs {
343343
#[derive(Debug, Args)]
344344
pub struct DestroyArgs {
345345
/// Session identifier (UUID or session name)
346-
pub session: String,
346+
#[arg(required_unless_present = "all", conflicts_with = "all")]
347+
pub session: Option<String>,
348+
/// Destroy all sessions
349+
#[arg(long)]
350+
pub all: bool,
351+
/// Skip confirmation when destroying all sessions
352+
#[arg(long, short, requires = "all")]
353+
pub force: bool,
347354
}
348355

349356
#[derive(Debug, Args)]
@@ -601,6 +608,19 @@ fn confirm(question: &str) -> Result<bool, anyhow::Error> {
601608
|| trimmed.eq_ignore_ascii_case("yes"))
602609
}
603610

611+
/// Prompt for confirmation, defaulting to no.
612+
fn confirm_default_no(question: &str) -> Result<bool, anyhow::Error> {
613+
eprint!("{question} [y/N] ");
614+
std::io::stderr().flush().ok();
615+
616+
let mut input = String::new();
617+
std::io::stdin()
618+
.read_line(&mut input)
619+
.context("reading stdin")?;
620+
let trimmed = input.trim();
621+
Ok(trimmed.eq_ignore_ascii_case("y") || trimmed.eq_ignore_ascii_case("yes"))
622+
}
623+
604624
/// List sessions via the `ListSessions` RPC.
605625
pub async fn cmd_ls(global: &GlobalArgs, args: LsArgs) -> Result<(), anyhow::Error> {
606626
ensure_daemon(global)?;
@@ -1267,20 +1287,80 @@ pub async fn cmd_destroy(global: &GlobalArgs, args: DestroyArgs) -> Result<(), a
12671287

12681288
let mut client = connect_daemon(global).await?;
12691289

1290+
if args.all {
1291+
return destroy_all_sessions(&mut client, args.force).await;
1292+
}
1293+
1294+
let session = args
1295+
.session
1296+
.as_deref()
1297+
.context("a session or --all is required")?;
1298+
let record = resolve_session(&mut client, session).await?;
1299+
1300+
destroy_session(&mut client, record.id, record.name.as_deref()).await
1301+
}
1302+
1303+
async fn destroy_all_sessions(
1304+
client: &mut client::Client,
1305+
force: bool,
1306+
) -> Result<(), anyhow::Error> {
1307+
use minimald_rpc::ListSessions;
1308+
1309+
let sessions = client
1310+
.oneshot_rpc::<ListSessions>(())
1311+
.await
1312+
.context("ListSessions RPC failed")?
1313+
.sessions;
1314+
1315+
if sessions.is_empty() {
1316+
println!("No active sessions.");
1317+
return Ok(());
1318+
}
1319+
1320+
if !force {
1321+
if !std::io::stdin().is_terminal() {
1322+
bail!("refusing to destroy all sessions without confirmation; pass --force")
1323+
}
1324+
if !confirm_default_no(&format!("Destroy all {} sessions?", sessions.len()))? {
1325+
println!("Aborted.");
1326+
return Ok(());
1327+
}
1328+
}
1329+
1330+
let session_count = sessions.len();
1331+
let mut failures = 0;
1332+
for session in sessions {
1333+
if let Err(error) = destroy_session(client, session.id, session.name.as_deref()).await {
1334+
failures += 1;
1335+
eprintln!(
1336+
"Failed to destroy session {} ({}): {error:#}",
1337+
session.id,
1338+
session.name.as_deref().unwrap_or("-")
1339+
);
1340+
}
1341+
}
1342+
1343+
if failures > 0 {
1344+
bail!("failed to destroy {failures} of {session_count} sessions")
1345+
}
1346+
1347+
Ok(())
1348+
}
1349+
1350+
async fn destroy_session(
1351+
client: &mut client::Client,
1352+
id: sessions::SessionId,
1353+
name: Option<&str>,
1354+
) -> Result<(), anyhow::Error> {
12701355
use minimald_rpc::{DestroySession, DestroySessionRequest};
1271-
let record = resolve_session(&mut client, &args.session).await?;
12721356

12731357
let resp = client
1274-
.oneshot_rpc::<DestroySession>(DestroySessionRequest { id: record.id })
1358+
.oneshot_rpc::<DestroySession>(DestroySessionRequest { id })
12751359
.await
12761360
.context("DestroySession RPC failed")?;
12771361

12781362
if resp.ok().is_some() {
1279-
println!(
1280-
"Destroyed session {} ({})",
1281-
record.id,
1282-
record.name.as_deref().unwrap_or("-")
1283-
);
1363+
println!("Destroyed session {} ({})", id, name.unwrap_or("-"));
12841364
} else {
12851365
bail!("DestroySession returned an error from the daemon");
12861366
}

‎crates/minimal/tests/cli.rs‎

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,9 @@ async fn destroy_removes_session() {
263263
cmd_destroy(
264264
&args,
265265
DestroyArgs {
266-
session: session_id.to_string(),
266+
session: Some(session_id.to_string()),
267+
all: false,
268+
force: false,
267269
},
268270
)
269271
.await
@@ -284,7 +286,9 @@ async fn destroy_by_name() {
284286
cmd_destroy(
285287
&args,
286288
DestroyArgs {
287-
session: "by-name".to_string(),
289+
session: Some("by-name".to_string()),
290+
all: false,
291+
force: false,
288292
},
289293
)
290294
.await
@@ -297,13 +301,54 @@ async fn destroy_unknown_session_fails() {
297301
let result = cmd_destroy(
298302
&args,
299303
DestroyArgs {
300-
session: "nonexistent".to_string(),
304+
session: Some("nonexistent".to_string()),
305+
all: false,
306+
force: false,
301307
},
302308
)
303309
.await;
304310
assert!(result.is_err());
305311
}
306312

313+
#[tokio::test]
314+
async fn destroy_all_removes_every_session() {
315+
let (daemon, args) = setup().await;
316+
let _ = create_session(&daemon, "first").await;
317+
let _ = create_session(&daemon, "second").await;
318+
319+
cmd_destroy(
320+
&args,
321+
DestroyArgs {
322+
session: None,
323+
all: true,
324+
force: true,
325+
},
326+
)
327+
.await
328+
.unwrap();
329+
330+
let mut client = daemon.server.connect().await;
331+
use minimald_rpc::ListSessions;
332+
let resp = client.call::<ListSessions>(&()).await;
333+
assert!(resp.sessions.is_empty());
334+
}
335+
336+
#[tokio::test]
337+
async fn destroy_all_succeeds_when_there_are_no_sessions() {
338+
let (_daemon, args) = setup().await;
339+
340+
cmd_destroy(
341+
&args,
342+
DestroyArgs {
343+
session: None,
344+
all: true,
345+
force: true,
346+
},
347+
)
348+
.await
349+
.unwrap();
350+
}
351+
307352
// --- stop ---
308353

309354
#[tokio::test]

0 commit comments

Comments
 (0)
0