sessions : registre des sessions + commande am sessions (issue #5) - lifecycle agent/repl, reconciliation des sessions interrompues, compteur sessions_count, filtres agent/projet/statut, show avec extrait de log
This commit is contained in:
@@ -11,6 +11,7 @@ pub mod list_cmd;
|
||||
pub mod run_cmd;
|
||||
pub mod search_cmd;
|
||||
pub mod self_uninstall;
|
||||
pub mod sessions_cmd;
|
||||
pub mod self_update;
|
||||
pub mod status_cmd;
|
||||
pub mod uninstall_cmd;
|
||||
@@ -48,6 +49,18 @@ pub fn execute_command(app: &App, cmd: &Command) -> Result<i32> {
|
||||
timeout,
|
||||
} => run_cmd::restart(app, start, *force, *timeout),
|
||||
Command::Status { agent } => status_cmd::run(app, agent.as_deref()),
|
||||
Command::Sessions {
|
||||
agent,
|
||||
project,
|
||||
status,
|
||||
show,
|
||||
} => sessions_cmd::run(
|
||||
app,
|
||||
agent.as_deref(),
|
||||
project.as_deref(),
|
||||
status.as_deref(),
|
||||
show.as_deref(),
|
||||
),
|
||||
Command::Install {
|
||||
agent,
|
||||
method,
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::*;
|
||||
use crate::cli::StartArgs;
|
||||
use crate::events::{self, Event, EventKind};
|
||||
use crate::process;
|
||||
use crate::sessions;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::OsString;
|
||||
@@ -65,6 +66,15 @@ fn start_one(
|
||||
let _ = app.state.touch(&agent.name);
|
||||
}
|
||||
let log = process::agent_log_path(app, &agent.name);
|
||||
let _ = sessions::start_agent(
|
||||
app,
|
||||
&agent.name,
|
||||
pid,
|
||||
cwd_string(),
|
||||
exec.args.clone(),
|
||||
events::env_keys(&exec.env),
|
||||
Some(log.display().to_string()),
|
||||
);
|
||||
app.log.success(&format!(
|
||||
"{} started (pid {pid}); log: {}",
|
||||
agent.title(),
|
||||
@@ -154,6 +164,7 @@ fn stop_one(app: &App, agent: &AgentDef, force: bool, timeout: u64) -> Result<i3
|
||||
.with_pid(pid)
|
||||
.with_reason("stale"),
|
||||
);
|
||||
let _ = sessions::finish_agent(app, &agent.name, Some(pid), 0, true);
|
||||
app.state.update_pid(&agent.name, None, None)?;
|
||||
return Ok(0);
|
||||
}
|
||||
@@ -169,6 +180,7 @@ fn stop_one(app: &App, agent: &AgentDef, force: bool, timeout: u64) -> Result<i3
|
||||
ev = ev.with_duration(d);
|
||||
}
|
||||
app.emit(&ev);
|
||||
let _ = sessions::finish_agent(app, &agent.name, Some(pid), if stopped { 0 } else { 1 }, stopped);
|
||||
app.state.update_pid(&agent.name, None, None)?;
|
||||
if stopped {
|
||||
app.log.success(&format!("{} stopped", agent.title()));
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
//! sessions: the registry of every agent and REPL session.
|
||||
|
||||
use super::*;
|
||||
use crate::output::{print_json, status_style, Table};
|
||||
use crate::sessions::{self, SessionRecord};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SessionRow {
|
||||
id: String,
|
||||
kind: String,
|
||||
agent: Option<String>,
|
||||
status: String,
|
||||
pid: Option<u32>,
|
||||
cwd: Option<String>,
|
||||
project: Option<String>,
|
||||
started_at: String,
|
||||
ended_at: Option<String>,
|
||||
duration_s: Option<u64>,
|
||||
exit_code: Option<i32>,
|
||||
log: Option<String>,
|
||||
}
|
||||
|
||||
fn row_of(r: &SessionRecord) -> SessionRow {
|
||||
SessionRow {
|
||||
id: r.id.clone(),
|
||||
kind: r.kind.clone(),
|
||||
agent: r.agent.clone(),
|
||||
status: r.status.clone(),
|
||||
pid: r.pid,
|
||||
cwd: r.cwd.clone(),
|
||||
project: r.project.clone(),
|
||||
started_at: r.started_at.clone(),
|
||||
ended_at: r.ended_at.clone(),
|
||||
duration_s: sessions::duration_s(r),
|
||||
exit_code: r.exit_code,
|
||||
log: r.log.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
app: &App,
|
||||
agent: Option<&str>,
|
||||
project: Option<&str>,
|
||||
status: Option<&str>,
|
||||
show: Option<&str>,
|
||||
) -> Result<i32> {
|
||||
if let Some(id) = show {
|
||||
return show_one(app, id);
|
||||
}
|
||||
sessions::reconcile(app)?;
|
||||
let sf = sessions::load(app)?;
|
||||
let mut rows: Vec<SessionRow> = sf
|
||||
.sessions
|
||||
.values()
|
||||
.filter(|r| agent.map_or(true, |a| r.agent.as_deref() == Some(a)))
|
||||
.filter(|r| project.map_or(true, |p| r.project.as_deref() == Some(p)))
|
||||
.filter(|r| status.map_or(true, |s| r.status == s))
|
||||
.map(row_of)
|
||||
.collect();
|
||||
rows.sort_by(|a, b| b.started_at.cmp(&a.started_at));
|
||||
if app.json() {
|
||||
print_json(&rows);
|
||||
return Ok(0);
|
||||
}
|
||||
let color = app.color();
|
||||
let theme = app.theme();
|
||||
let mut table = Table::new(vec!["SESSION", "AGENT", "STATUS", "DURATION", "EXIT"]);
|
||||
for r in &rows {
|
||||
table.row(vec![
|
||||
short_id(&r.id),
|
||||
r.agent
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("({})", r.kind)),
|
||||
status_style(&r.status, theme, color),
|
||||
r.duration_s
|
||||
.map(format_duration)
|
||||
.unwrap_or_else(|| "—".to_string()),
|
||||
r.exit_code
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_else(|| "—".to_string()),
|
||||
]);
|
||||
}
|
||||
print!("{}", table.render());
|
||||
if rows.is_empty() {
|
||||
app.log
|
||||
.info("no sessions recorded yet — 'am start <agent> --background' or run 'am'");
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
fn show_one(app: &App, id: &str) -> Result<i32> {
|
||||
sessions::reconcile(app)?;
|
||||
let sf = sessions::load(app)?;
|
||||
let Some(r) = sf.sessions.get(id).cloned() else {
|
||||
bail!("unknown session '{id}' — 'am sessions' lists every session");
|
||||
};
|
||||
if app.json() {
|
||||
print_json(&row_of(&r));
|
||||
return Ok(0);
|
||||
}
|
||||
println!("session {}", r.id);
|
||||
println!(" kind: {}", r.kind);
|
||||
println!(" agent: {}", r.agent.as_deref().unwrap_or("—"));
|
||||
println!(" status: {}", r.status);
|
||||
println!(" pid: {}", r.pid.map(|p| p.to_string()).unwrap_or_else(|| "—".to_string()));
|
||||
println!(" cwd: {}", r.cwd.as_deref().unwrap_or("—"));
|
||||
println!(" project: {}", r.project.as_deref().unwrap_or("—"));
|
||||
println!(" started: {}", r.started_at);
|
||||
println!(" ended: {}", r.ended_at.as_deref().unwrap_or("—"));
|
||||
if let Some(d) = sessions::duration_s(&r) {
|
||||
println!(" duration: {}", format_duration(d));
|
||||
}
|
||||
if let Some(c) = r.exit_code {
|
||||
println!(" exit code: {c}");
|
||||
}
|
||||
if let Some(l) = &r.log {
|
||||
let p = std::path::PathBuf::from(l);
|
||||
if p.exists() {
|
||||
println!(" log: {l}");
|
||||
let tail = crate::process::last_lines(&p, 8);
|
||||
if !tail.is_empty() {
|
||||
println!(" --- log excerpt ---");
|
||||
println!("{tail}");
|
||||
}
|
||||
} else {
|
||||
println!(" log: {l} (missing)");
|
||||
}
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// "20260815_143926_a1b2c3" -> "20260815_143926"
|
||||
fn short_id(id: &str) -> String {
|
||||
id.chars().take(15).collect()
|
||||
}
|
||||
|
||||
fn format_duration(secs: u64) -> String {
|
||||
if secs < 60 {
|
||||
format!("{secs}s")
|
||||
} else if secs < 3600 {
|
||||
format!("{}m{:02}s", secs / 60, secs % 60)
|
||||
} else {
|
||||
format!("{}h{:02}m", secs / 3600, (secs % 3600) / 60)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user