use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::Args as ClapArgs;
use scrybe_application::{PageRequest, SessionRef, MAX_PAGE_LIMIT};
use crate::runtime::application;
#[derive(ClapArgs, Debug)]
pub struct Args {
pub id_or_folder: String,
#[arg(long)]
pub root: Option<PathBuf>,
#[arg(long, default_value_t = false)]
pub no_transcript: bool,
}
#[allow(clippy::unused_async)]
pub async fn run(args: Args) -> Result<()> {
let app = application(args.root.as_deref())?;
let repository = app.sessions();
let id = SessionRef::parse(&args.id_or_folder)
.map_err(scrybe_application::ApplicationError::from)
.with_context(|| format!("resolving session {}", args.id_or_folder))?;
let detail = repository
.get_session(&id)
.with_context(|| format!("resolving session {}", args.id_or_folder))?;
let folder = repository.root().resolve(&detail.id);
if !args.no_transcript && detail.artifacts.transcript {
println!(
"=== transcript ({}): ===",
folder.join("transcript.md").display()
);
let mut cursor = 0;
loop {
let page = repository
.read_transcript_page(&detail.id, PageRequest::new(cursor, MAX_PAGE_LIMIT))?;
for line in &page.lines {
println!("{line}");
}
match page.next {
Some(next) => cursor = next.line,
None => break,
}
}
}
let notes = repository.read_notes(&detail.id)?;
if let Some(markdown) = notes.markdown {
println!("\n=== notes ({}): ===", folder.join("notes.md").display());
print!("{markdown}");
if !markdown.ends_with('\n') {
println!();
}
} else {
println!(
"\nscrybe show: notes.md missing in {}; run `scrybe doctor` to recover",
folder.display()
);
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn write_session(dir: &std::path::Path, folder: &str) -> PathBuf {
let folder_path = dir.join(folder);
std::fs::create_dir(&folder_path).unwrap();
std::fs::write(
folder_path.join("transcript.md"),
"# title\n\n**Me** [00:00:00]: hello\n",
)
.unwrap();
std::fs::write(
folder_path.join("notes.md"),
"## TL;DR\n- a meeting happened\n",
)
.unwrap();
folder_path
}
#[tokio::test]
async fn test_run_prints_transcript_and_notes_for_existing_session() {
let dir = tempfile::tempdir().unwrap();
write_session(dir.path(), "2026-04-29-1430-acme-01HXYZ");
run(Args {
id_or_folder: "2026-04-29-1430-acme-01HXYZ".into(),
root: Some(dir.path().to_path_buf()),
no_transcript: false,
})
.await
.unwrap();
}
#[tokio::test]
async fn test_run_skips_transcript_when_no_transcript_flag_set() {
let dir = tempfile::tempdir().unwrap();
write_session(dir.path(), "2026-04-29-1430-acme-01HXYZ");
run(Args {
id_or_folder: "2026-04-29-1430-acme-01HXYZ".into(),
root: Some(dir.path().to_path_buf()),
no_transcript: true,
})
.await
.unwrap();
}
#[tokio::test]
async fn test_run_emits_recovery_hint_when_notes_md_missing() {
let dir = tempfile::tempdir().unwrap();
let folder = dir.path().join("2026-04-29-1430-acme-01HXYZ");
std::fs::create_dir(&folder).unwrap();
std::fs::write(folder.join("transcript.md"), "# title\n").unwrap();
run(Args {
id_or_folder: "2026-04-29-1430-acme-01HXYZ".into(),
root: Some(dir.path().to_path_buf()),
no_transcript: false,
})
.await
.unwrap();
}
#[tokio::test]
async fn test_run_resolves_a_session_by_unambiguous_ulid_fragment() {
let dir = tempfile::tempdir().unwrap();
write_session(dir.path(), "2026-04-29-1430-acme-01HXYZ");
run(Args {
id_or_folder: "01HXYZ".into(),
root: Some(dir.path().to_path_buf()),
no_transcript: false,
})
.await
.unwrap();
}
#[tokio::test]
async fn test_run_refuses_an_absolute_path_instead_of_reading_outside_the_root() {
let dir = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
write_session(outside.path(), "2026-04-29-1430-secret-01HXYZ");
let error = run(Args {
id_or_folder: outside
.path()
.join("2026-04-29-1430-secret-01HXYZ")
.display()
.to_string(),
root: Some(dir.path().to_path_buf()),
no_transcript: false,
})
.await
.unwrap_err();
assert!(error.to_string().contains("resolving session"));
}
}