Skip to main content

lean_ctx/tools/
ctx_package.rs

1//! `ctx_package` business logic (#293): save/resume portable context packages.
2
3use std::path::Path;
4
5use crate::core::context_package;
6use crate::core::session::SessionState;
7
8pub fn handle(
9    project_root: &str,
10    session: Option<&SessionState>,
11    action: &str,
12    path: Option<&str>,
13    agent_id: Option<&str>,
14    description: Option<&str>,
15) -> String {
16    match action.trim() {
17        "save" => handle_save(project_root, session, path, agent_id, description),
18        "resume" => handle_resume(project_root, session, path),
19        "list" => handle_list(project_root),
20        "info" => handle_info(path),
21        other => format!(
22            "ERR: unknown package action '{other}'. Use: save | resume <path> | list | info <path>"
23        ),
24    }
25}
26
27fn handle_save(
28    project_root: &str,
29    session: Option<&SessionState>,
30    path: Option<&str>,
31    agent_id: Option<&str>,
32    description: Option<&str>,
33) -> String {
34    let Some(session) = session else {
35        return "ERR: no active session to save".to_string();
36    };
37    let output_path = path.map(Path::new);
38    match context_package::save_package(session, project_root, agent_id, description, output_path) {
39        Ok(p) => format!("package saved: {}", p.display()),
40        Err(e) => format!("ERR: {e}"),
41    }
42}
43
44fn handle_resume(project_root: &str, session: Option<&SessionState>, path: Option<&str>) -> String {
45    let Some(path_str) = path else {
46        return "ERR: resume requires a path to the .ctx.json package".to_string();
47    };
48    let pkg_path = Path::new(path_str);
49    if !pkg_path.exists() {
50        // Try the default packages directory.
51        let hash = crate::core::project_hash::hash_project_root(project_root);
52        let alt = crate::core::data_dir::lean_ctx_data_dir()
53            .unwrap_or_else(|_| std::path::PathBuf::from(".lean-ctx"))
54            .join("packages")
55            .join(hash)
56            .join(path_str);
57        if !alt.exists() {
58            return format!("ERR: package not found: {path_str}");
59        }
60        return do_resume(session, &alt);
61    }
62    do_resume(session, pkg_path)
63}
64
65fn do_resume(session: Option<&SessionState>, path: &Path) -> String {
66    let mut target = match session {
67        Some(base) => base.clone(),
68        None => SessionState::new(),
69    };
70    match context_package::resume_package(&mut target, path) {
71        Ok(report) => report.format(),
72        Err(e) => format!("ERR: {e}"),
73    }
74}
75
76fn handle_list(project_root: &str) -> String {
77    let hash = crate::core::project_hash::hash_project_root(project_root);
78    let dir = crate::core::data_dir::lean_ctx_data_dir()
79        .unwrap_or_else(|_| std::path::PathBuf::from(".lean-ctx"))
80        .join("packages")
81        .join(hash);
82    if !dir.exists() {
83        return "No saved packages yet.".to_string();
84    }
85    let mut entries: Vec<String> = Vec::new();
86    if let Ok(rd) = std::fs::read_dir(&dir) {
87        for entry in rd.flatten() {
88            let p = entry.path();
89            if p.extension().and_then(|e| e.to_str()) == Some("json") {
90                if let Ok(json) = std::fs::read_to_string(&p) {
91                    if let Ok(pkg) = serde_json::from_str::<context_package::ContextPackage>(&json)
92                    {
93                        entries.push(format!(
94                            "  {} — {}",
95                            p.file_name().unwrap_or_default().to_string_lossy(),
96                            pkg.summary_line()
97                        ));
98                    }
99                }
100            }
101        }
102    }
103    if entries.is_empty() {
104        return "No saved packages yet.".to_string();
105    }
106    entries.sort();
107    format!("packages ({}):\n{}", entries.len(), entries.join("\n"))
108}
109
110fn handle_info(path: Option<&str>) -> String {
111    let Some(path_str) = path else {
112        return "ERR: info requires a path".to_string();
113    };
114    let p = Path::new(path_str);
115    if !p.exists() {
116        return format!("ERR: not found: {path_str}");
117    }
118    match std::fs::read_to_string(p) {
119        Ok(json) => match serde_json::from_str::<context_package::ContextPackage>(&json) {
120            Ok(pkg) => format!(
121                "format_version: {}\ncreated: {}\nproject: {}\n{}",
122                pkg.format_version,
123                pkg.created_at.format("%Y-%m-%d %H:%M"),
124                pkg.project_root,
125                pkg.summary_line()
126            ),
127            Err(e) => format!("ERR: parse: {e}"),
128        },
129        Err(e) => format!("ERR: read: {e}"),
130    }
131}