Skip to main content

lean_ctx/cli/
plugin_cmd.rs

1use crate::core::plugins::{
2    executor::HookPoint,
3    registry::{default_plugin_dir, PluginRegistry},
4    PluginManager,
5};
6
7pub fn cmd_plugin(args: &[String]) {
8    let action = args.first().map_or("help", String::as_str);
9
10    match action {
11        "list" | "ls" => cmd_list(),
12        "enable" => {
13            let Some(name) = args.get(1).map(String::as_str).filter(|s| !s.is_empty()) else {
14                eprintln!("Usage: lean-ctx plugin enable <name>");
15                std::process::exit(1);
16            };
17            cmd_enable(name);
18        }
19        "disable" => {
20            let Some(name) = args.get(1).map(String::as_str).filter(|s| !s.is_empty()) else {
21                eprintln!("Usage: lean-ctx plugin disable <name>");
22                std::process::exit(1);
23            };
24            cmd_disable(name);
25        }
26        "info" => {
27            let Some(name) = args.get(1).map(String::as_str).filter(|s| !s.is_empty()) else {
28                eprintln!("Usage: lean-ctx plugin info <name>");
29                std::process::exit(1);
30            };
31            cmd_info(name);
32        }
33        "init" => {
34            let Some(name) = args.get(1).map(String::as_str).filter(|s| !s.is_empty()) else {
35                eprintln!("Usage: lean-ctx plugin init <name>");
36                std::process::exit(1);
37            };
38            cmd_init(name);
39        }
40        "hooks" => cmd_hooks(),
41        "help" | "--help" | "-h" => print_help(),
42        _ => {
43            eprintln!("Unknown plugin action: {action}");
44            print_help();
45            std::process::exit(1);
46        }
47    }
48}
49
50fn cmd_list() {
51    let mut registry = PluginRegistry::from_default_dir();
52    let errors = registry.discover();
53    for err in &errors {
54        eprintln!("Warning: {}: {}", err.path.display(), err.error);
55    }
56
57    let plugins = registry.list();
58    if plugins.is_empty() {
59        println!("No plugins installed.");
60        println!("\nPlugin directory: {}", default_plugin_dir().display());
61        println!("Use 'lean-ctx plugin init <name>' to create a plugin template.");
62        return;
63    }
64
65    println!("Installed plugins:\n");
66    for plugin in &plugins {
67        let status = if plugin.enabled { "✓" } else { "✗" };
68        let hooks_count = plugin.manifest.hooks.len();
69        println!(
70            "  [{status}] {} v{} ({hooks_count} hook{})",
71            plugin.manifest.plugin.name,
72            plugin.manifest.plugin.version,
73            if hooks_count == 1 { "" } else { "s" }
74        );
75        if !plugin.manifest.plugin.description.is_empty() {
76            println!("      {}", plugin.manifest.plugin.description);
77        }
78    }
79    println!("\nPlugin directory: {}", default_plugin_dir().display());
80}
81
82fn cmd_enable(name: &str) {
83    PluginManager::init();
84    match PluginManager::with_registry_mut(|reg| reg.enable(name)) {
85        Some(Ok(())) => println!("Enabled plugin: {name}"),
86        Some(Err(e)) => {
87            eprintln!("Error: {e}");
88            std::process::exit(1);
89        }
90        None => {
91            eprintln!("Error: plugin registry not initialized");
92            std::process::exit(1);
93        }
94    }
95}
96
97fn cmd_disable(name: &str) {
98    PluginManager::init();
99    match PluginManager::with_registry_mut(|reg| reg.disable(name)) {
100        Some(Ok(())) => println!("Disabled plugin: {name}"),
101        Some(Err(e)) => {
102            eprintln!("Error: {e}");
103            std::process::exit(1);
104        }
105        None => {
106            eprintln!("Error: plugin registry not initialized");
107            std::process::exit(1);
108        }
109    }
110}
111
112fn cmd_info(name: &str) {
113    let mut registry = PluginRegistry::from_default_dir();
114    registry.discover();
115
116    if let Some(plugin) = registry.get(name) {
117        println!("Plugin: {}", plugin.manifest.plugin.name);
118        println!("Version: {}", plugin.manifest.plugin.version);
119        if !plugin.manifest.plugin.description.is_empty() {
120            println!("Description: {}", plugin.manifest.plugin.description);
121        }
122        if !plugin.manifest.plugin.author.is_empty() {
123            println!("Author: {}", plugin.manifest.plugin.author);
124        }
125        println!("Enabled: {}", plugin.enabled);
126        println!("Path: {}", plugin.path.display());
127        if !plugin.manifest.hooks.is_empty() {
128            println!("\nHooks:");
129            for (hook_name, entry) in &plugin.manifest.hooks {
130                println!(
131                    "  {hook_name}: {} (timeout: {}ms)",
132                    entry.command, entry.timeout_ms
133                );
134            }
135        }
136    } else {
137        eprintln!("Plugin not found: {name}");
138        std::process::exit(1);
139    }
140}
141
142fn cmd_init(name: &str) {
143    let dir = default_plugin_dir();
144    match crate::core::plugins::init_plugin_template(name, &dir) {
145        Ok(()) => {
146            println!("Created plugin template: {}", dir.join(name).display());
147            println!("\nNext steps:");
148            println!("  1. Edit {}/plugin.toml", dir.join(name).display());
149            println!("  2. Implement your plugin binary");
150            println!("  3. Run 'lean-ctx plugin list' to verify");
151        }
152        Err(e) => {
153            eprintln!("Error creating plugin template: {e}");
154            std::process::exit(1);
155        }
156    }
157}
158
159fn cmd_hooks() {
160    println!("Available hook points:\n");
161    for name in HookPoint::all_hook_names() {
162        let desc = match *name {
163            "on_session_start" => "Called when a new lean-ctx session begins",
164            "on_session_end" => "Called when a session ends",
165            "pre_read" => "Called before a file is read (receives {path} in stdin)",
166            "post_compress" => {
167                "Called after compression (receives {path, original_tokens, compressed_tokens})"
168            }
169            "on_knowledge_update" => "Called when knowledge is updated (receives {fact_id})",
170            _ => "",
171        };
172        println!("  {name}");
173        println!("    {desc}\n");
174    }
175}
176
177fn print_help() {
178    eprintln!(
179        "lean-ctx plugin — Plugin management\n\
180         \n\
181         USAGE:\n    \
182             lean-ctx plugin <action> [args]\n\
183         \n\
184         ACTIONS:\n    \
185             list              List installed plugins\n    \
186             enable <name>     Enable a plugin\n    \
187             disable <name>    Disable a plugin\n    \
188             info <name>       Show plugin details\n    \
189             init <name>       Create a plugin template\n    \
190             hooks             List available hook points\n    \
191             help              Show this help"
192    );
193}