history : historique structure par session + am history (issue #7) - migration one-shot de history.txt, filtres kind/cwd/search/failed/session, enregistrement REPL avec duree et exit

This commit is contained in:
2026-08-16 22:34:48 -04:00
parent 9f4a13eae2
commit c9e2543f50
8 changed files with 576 additions and 5 deletions
+96
View File
@@ -0,0 +1,96 @@
//! history: search and reuse the structured command history.
use super::*;
use crate::history::{self, HistoryEntry};
use crate::output::print_json;
/// Filter history entries; returns them most recent first.
pub fn filter(
app: &App,
kind: Option<&str>,
cwd: Option<&str>,
search: Option<&str>,
failed: bool,
session: Option<&str>,
limit: usize,
) -> Vec<(String, HistoryEntry)> {
let _ = session; // session filtering happens by reading one file directly
let mut out: Vec<(String, HistoryEntry)> = Vec::new();
let dir = history::dir(app);
for f in history::files(&dir).iter().rev() {
let sid = history::session_of_file(f).unwrap_or_default();
if let Some(s) = session {
if sid != s {
continue;
}
}
let Ok(text) = std::fs::read_to_string(f) else {
continue;
};
let mut entries: Vec<HistoryEntry> = text
.lines()
.filter(|l| !l.trim().is_empty())
.filter_map(|l| serde_json::from_str::<HistoryEntry>(l).ok())
.collect();
entries.reverse();
for e in entries {
if kind.map_or(true, |k| e.kind == k)
&& cwd.map_or(true, |c| e.cwd.as_deref() == Some(c))
&& search.map_or(true, |s| e.cmd.to_lowercase().contains(&s.to_lowercase()))
&& (!failed || e.exit_code.is_some())
{
out.push((sid.clone(), e));
if limit > 0 && out.len() >= limit {
return out;
}
}
}
}
out
}
pub fn run(
app: &App,
kind: Option<&str>,
cwd: Option<&str>,
search: Option<&str>,
failed: bool,
session: Option<&str>,
limit: usize,
) -> Result<i32> {
history::migrate(app)?;
let rows = filter(app, kind, cwd, search, failed, session, limit);
if app.json() {
let payload: Vec<serde_json::Value> = rows
.iter()
.enumerate()
.map(|(i, (sid, e))| {
serde_json::json!({
"n": rows.len() - i,
"ts": e.ts,
"cmd": e.cmd,
"kind": e.kind,
"cwd": e.cwd,
"exit_code": e.exit_code,
"duration_ms": e.duration_ms,
"session": sid,
})
})
.collect();
print_json(&payload);
return Ok(0);
}
for (i, (_sid, e)) in rows.iter().enumerate() {
let n = rows.len() - i;
let ts = e.ts.get(0..16).unwrap_or(&e.ts);
let flag = match e.exit_code {
Some(_) => "✗".to_string(),
None => " ".to_string(),
};
println!("{n:>4} {flag} {ts} {}", e.cmd);
}
if rows.is_empty() {
app.log.info("no matching history — run 'am' and type some commands");
}
Ok(0)
}
+17
View File
@@ -5,6 +5,7 @@ pub mod config_cmd;
pub mod doctor_cmd;
pub mod export_import;
pub mod help_cmd;
pub mod history_cmd;
pub mod info_cmd;
pub mod install_cmd;
pub mod list_cmd;
@@ -78,6 +79,22 @@ pub fn execute_command(app: &App, cmd: &Command) -> Result<i32> {
Command::Uninstall { agent, purge } => uninstall_cmd::run(app, agent, *purge),
Command::Update { agent, all } => update_cmd::run(app, agent.as_deref(), *all),
Command::Search { keyword, category } => search_cmd::run(app, keyword, category.as_deref()),
Command::History {
kind,
cwd,
search,
failed,
session,
limit,
} => history_cmd::run(
app,
kind.as_deref(),
cwd.as_deref(),
search.as_deref(),
*failed,
session.as_deref(),
*limit,
),
Command::Info { agent } => info_cmd::run(app, agent),
Command::Help { command, find } => help_cmd::run(app, command.as_deref(), find.as_deref()),
Command::Version => version_cmd::run(app),