Skip to main content

angreal/
lib.rs

1//!  Angreal - project templating and task management
2//!
3//!  A package for templating code based projects and providing methods
4//! for the creation and management of common operational tasks associated with the
5//! project.
6//!
7
8#[macro_use]
9extern crate version;
10#[macro_use]
11pub mod macros;
12
13pub mod builder;
14pub mod completion;
15pub mod error_formatter;
16pub mod git;
17pub mod init;
18pub mod integrations;
19pub mod logger;
20pub mod mcp;
21pub mod py_logger;
22pub mod python_bindings;
23pub mod task;
24pub mod utils;
25pub mod validation;
26
27use builder::{build_app, command_tree, tree_output};
28use error_formatter::PythonErrorFormatter;
29use integrations::uv::{UvIntegration, UvVirtualEnv};
30use task::ANGREAL_TASKS;
31
32use pyo3::types::{IntoPyDict, PyDict};
33use std::ops::Not;
34use std::path::{Path, PathBuf};
35use std::vec::Vec;
36
37use std::process::exit;
38
39use pyo3::{prelude::*, wrap_pymodule, IntoPyObjectExt};
40use std::collections::HashMap;
41use std::fs;
42
43use log::{debug, error, warn};
44
45use crate::integrations::git::Git;
46use crate::task::{generate_command_path_key, generate_path_key_from_parts};
47
48#[pyclass]
49struct PyGit {
50    inner: Git,
51}
52
53#[pymethods]
54impl PyGit {
55    #[new]
56    #[pyo3(signature = (working_dir=None))]
57    fn new(working_dir: Option<&str>) -> PyResult<Self> {
58        let git = Git::new(working_dir.map(Path::new))
59            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
60        Ok(Self { inner: git })
61    }
62
63    fn execute(&self, subcommand: &str, args: Vec<String>) -> PyResult<(i32, String, String)> {
64        let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
65        let output = self
66            .inner
67            .execute(subcommand, &arg_refs)
68            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
69        Ok((output.exit_code, output.stderr, output.stdout))
70    }
71
72    fn init(&self, bare: Option<bool>) -> PyResult<()> {
73        self.inner
74            .init(bare.unwrap_or(false))
75            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
76    }
77
78    fn add(&self, paths: Vec<String>) -> PyResult<()> {
79        let path_refs: Vec<&str> = paths.iter().map(|s| s.as_str()).collect();
80        self.inner
81            .add(&path_refs)
82            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
83    }
84
85    fn commit(&self, message: &str, all: Option<bool>) -> PyResult<()> {
86        self.inner
87            .commit(message, all.unwrap_or(false))
88            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
89    }
90
91    fn push(&self, remote: Option<&str>, branch: Option<&str>) -> PyResult<()> {
92        self.inner
93            .push(remote, branch)
94            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
95    }
96
97    fn pull(&self, remote: Option<&str>, branch: Option<&str>) -> PyResult<()> {
98        self.inner
99            .pull(remote, branch)
100            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
101    }
102
103    fn status(&self, short: Option<bool>) -> PyResult<String> {
104        self.inner
105            .status(short.unwrap_or(false))
106            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
107    }
108
109    fn branch(&self, name: Option<&str>, delete: Option<bool>) -> PyResult<String> {
110        self.inner
111            .branch(name, delete.unwrap_or(false))
112            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
113    }
114
115    fn checkout(&self, branch: &str, create: Option<bool>) -> PyResult<()> {
116        self.inner
117            .checkout(branch, create.unwrap_or(false))
118            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
119    }
120
121    fn tag(&self, name: &str, message: Option<&str>) -> PyResult<()> {
122        self.inner
123            .tag(name, message)
124            .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
125    }
126
127    fn __call__(
128        &self,
129        command: &str,
130        args: Vec<String>,
131        kwargs: Option<&Bound<'_, pyo3::types::PyDict>>,
132    ) -> PyResult<(i32, String, String)> {
133        let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
134        let output = if let Some(dict) = kwargs {
135            // Convert PyDict to HashMap<String, String> then to HashMap<&str, &str>
136            let mut options_owned = HashMap::new();
137            for (key, value) in dict.iter() {
138                let key_str = key.extract::<String>()?;
139                let value_str = if value.is_truthy()? {
140                    "".to_string() // For boolean flags like --bare
141                } else {
142                    value.extract::<String>()?
143                };
144                options_owned.insert(key_str, value_str);
145            }
146            let options: HashMap<&str, &str> = options_owned
147                .iter()
148                .map(|(k, v)| (k.as_str(), v.as_str()))
149                .collect();
150            self.inner.execute_with_options(command, options, &arg_refs)
151        } else {
152            self.inner.execute(command, &arg_refs)
153        }
154        .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
155
156        Ok((output.exit_code, output.stderr, output.stdout))
157    }
158}
159
160#[pyfunction]
161#[pyo3(signature = (remote, destination=None))]
162fn git_clone(remote: &str, destination: Option<&str>) -> PyResult<String> {
163    let dest = Git::clone(remote, destination.map(Path::new))
164        .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
165    Ok(dest.display().to_string())
166}
167
168#[pyfunction]
169fn ensure_uv_installed() -> PyResult<()> {
170    UvIntegration::ensure_installed()
171        .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
172}
173
174#[pyfunction]
175fn uv_version() -> PyResult<String> {
176    UvIntegration::version()
177        .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
178}
179
180#[pyfunction]
181fn create_virtualenv(path: &str, python_version: Option<&str>) -> PyResult<()> {
182    UvVirtualEnv::create(Path::new(path), python_version)
183        .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
184    Ok(())
185}
186
187#[pyfunction]
188fn install_packages(venv_path: &str, packages: Vec<String>) -> PyResult<()> {
189    let venv = UvVirtualEnv {
190        path: PathBuf::from(venv_path),
191    };
192    venv.install_packages(&packages)
193        .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
194}
195
196#[pyfunction]
197fn install_requirements(venv_path: &str, requirements_file: &str) -> PyResult<()> {
198    let venv = UvVirtualEnv {
199        path: PathBuf::from(venv_path),
200    };
201    venv.install_requirements(Path::new(requirements_file))
202        .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
203}
204
205#[pyfunction]
206fn discover_pythons() -> PyResult<Vec<(String, String)>> {
207    UvVirtualEnv::discover_pythons()
208        .map(|pythons| {
209            pythons
210                .into_iter()
211                .map(|(version, path)| (version, path.display().to_string()))
212                .collect()
213        })
214        .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
215}
216
217#[pyfunction]
218fn install_python(version: &str) -> PyResult<String> {
219    UvVirtualEnv::install_python(version)
220        .map(|path| path.display().to_string())
221        .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
222}
223
224#[pyfunction]
225fn get_venv_activation_info(venv_path: &str) -> PyResult<integrations::uv::ActivationInfo> {
226    let venv = UvVirtualEnv {
227        path: PathBuf::from(venv_path),
228    };
229    venv.get_activation_info()
230        .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
231}
232
233#[pyfunction]
234fn register_entrypoint(name: &str) -> PyResult<()> {
235    use home::home_dir;
236    use serde_json;
237
238    // Get home directory, with fallback to environment variables for testing
239    let home = if let Some(home_from_env) = std::env::var_os("HOME") {
240        PathBuf::from(home_from_env)
241    } else if let Some(userprofile) = std::env::var_os("USERPROFILE") {
242        PathBuf::from(userprofile)
243    } else {
244        home_dir().ok_or_else(|| {
245            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("Cannot find home directory")
246        })?
247    };
248
249    // Create directories
250    let local_bin = home.join(".local").join("bin");
251    fs::create_dir_all(&local_bin).map_err(|e| {
252        PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
253            "Failed to create bin directory: {}",
254            e
255        ))
256    })?;
257
258    let data_dir = home.join(".angrealrc");
259    fs::create_dir_all(&data_dir).map_err(|e| {
260        PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
261            "Failed to create data directory: {}",
262            e
263        ))
264    })?;
265
266    // Determine script path based on platform
267    #[cfg(unix)]
268    let script_path = local_bin.join(name);
269    #[cfg(windows)]
270    let script_path = local_bin.join(format!("{}.bat", name));
271
272    // Check for conflicts
273    if script_path.exists() {
274        return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
275            "Command '{}' already exists at {}",
276            name,
277            script_path.display()
278        )));
279    }
280
281    // Create wrapper script
282    #[cfg(unix)]
283    {
284        let script_content = format!(
285            "#!/usr/bin/env python\n# ANGREAL_ALIAS: {}\n# Auto-generated by angreal.register_entrypoint\nimport sys\ntry:\n    import angreal\n    angreal.main()\nexcept ImportError:\n    print(f\"Error: angreal not installed. Remove alias: rm {}\", file=sys.stderr)\n    sys.exit(1)\n",
286            name,
287            script_path.display()
288        );
289
290        fs::write(&script_path, script_content).map_err(|e| {
291            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
292                "Failed to write script: {}",
293                e
294            ))
295        })?;
296
297        // Make executable
298        use std::os::unix::fs::PermissionsExt;
299        let mut perms = fs::metadata(&script_path)
300            .map_err(|e| {
301                PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
302                    "Failed to get permissions: {}",
303                    e
304                ))
305            })?
306            .permissions();
307        perms.set_mode(0o755);
308        fs::set_permissions(&script_path, perms).map_err(|e| {
309            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
310                "Failed to set permissions: {}",
311                e
312            ))
313        })?;
314    }
315
316    #[cfg(windows)]
317    {
318        let script_content = format!(
319            "@echo off\nREM ANGREAL_ALIAS: {}\nREM Auto-generated by angreal.register_entrypoint\npython -m angreal %*\n",
320            name
321        );
322        fs::write(&script_path, script_content).map_err(|e| {
323            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
324                "Failed to write script: {}",
325                e
326            ))
327        })?;
328    }
329
330    // Update registry
331    let registry_path = home.join(".angrealrc").join("aliases.json");
332    let mut aliases: Vec<String> = if registry_path.exists() {
333        let content = fs::read_to_string(&registry_path).map_err(|e| {
334            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
335                "Failed to read registry: {}",
336                e
337            ))
338        })?;
339        serde_json::from_str(&content).unwrap_or_else(|_| Vec::new())
340    } else {
341        Vec::new()
342    };
343
344    if !aliases.contains(&name.to_string()) {
345        aliases.push(name.to_string());
346        let json = serde_json::to_string_pretty(&aliases).map_err(|e| {
347            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
348                "Failed to serialize registry: {}",
349                e
350            ))
351        })?;
352        fs::write(&registry_path, json).map_err(|e| {
353            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
354                "Failed to write registry: {}",
355                e
356            ))
357        })?;
358    }
359
360    println!("✅ Registered '{}' as angreal alias", name);
361    println!("Make sure ~/.local/bin is in your PATH");
362    Ok(())
363}
364
365#[pyfunction]
366fn list_entrypoints() -> PyResult<Vec<String>> {
367    use home::home_dir;
368
369    // Get home directory, with fallback to environment variables for testing
370    let home = if let Some(home_from_env) = std::env::var_os("HOME") {
371        PathBuf::from(home_from_env)
372    } else if let Some(userprofile) = std::env::var_os("USERPROFILE") {
373        PathBuf::from(userprofile)
374    } else {
375        home_dir().ok_or_else(|| {
376            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("Cannot find home directory")
377        })?
378    };
379
380    let registry_path = home.join(".angrealrc").join("aliases.json");
381
382    if !registry_path.exists() {
383        return Ok(Vec::new());
384    }
385
386    let content = fs::read_to_string(&registry_path).map_err(|e| {
387        PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to read registry: {}", e))
388    })?;
389
390    let aliases: Vec<String> = serde_json::from_str(&content).unwrap_or_else(|_| Vec::new());
391    Ok(aliases)
392}
393
394#[pyfunction]
395fn unregister_entrypoint(name: &str) -> PyResult<()> {
396    use home::home_dir;
397
398    // Get home directory, with fallback to environment variables for testing
399    let home = if let Some(home_from_env) = std::env::var_os("HOME") {
400        PathBuf::from(home_from_env)
401    } else if let Some(userprofile) = std::env::var_os("USERPROFILE") {
402        PathBuf::from(userprofile)
403    } else {
404        home_dir().ok_or_else(|| {
405            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>("Cannot find home directory")
406        })?
407    };
408
409    // Remove script
410    let local_bin = home.join(".local").join("bin");
411    #[cfg(unix)]
412    let script_path = local_bin.join(name);
413    #[cfg(windows)]
414    let script_path = local_bin.join(format!("{}.bat", name));
415
416    if script_path.exists() {
417        fs::remove_file(&script_path).map_err(|e| {
418            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
419                "Failed to remove script: {}",
420                e
421            ))
422        })?;
423    }
424
425    // Update registry
426    let registry_path = home.join(".angrealrc").join("aliases.json");
427
428    if registry_path.exists() {
429        let content = fs::read_to_string(&registry_path).map_err(|e| {
430            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
431                "Failed to read registry: {}",
432                e
433            ))
434        })?;
435
436        let mut aliases: Vec<String> =
437            serde_json::from_str(&content).unwrap_or_else(|_| Vec::new());
438        aliases.retain(|alias| alias != name);
439
440        let json = serde_json::to_string_pretty(&aliases).map_err(|e| {
441            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
442                "Failed to serialize registry: {}",
443                e
444            ))
445        })?;
446        fs::write(&registry_path, json).map_err(|e| {
447            PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
448                "Failed to write registry: {}",
449                e
450            ))
451        })?;
452    }
453
454    println!("✅ Unregistered '{}' alias", name);
455    Ok(())
456}
457
458#[pyfunction]
459fn cleanup_entrypoints() -> PyResult<()> {
460    let aliases = list_entrypoints()?;
461
462    for alias in aliases {
463        if let Err(e) = unregister_entrypoint(&alias) {
464            eprintln!("Warning: Failed to unregister '{}': {}", alias, e);
465        }
466    }
467
468    println!("✅ Cleaned up all angreal aliases");
469    Ok(())
470}
471
472/// The main function is just an entry point to be called from the core angreal library.
473#[pyfunction]
474fn main() -> PyResult<()> {
475    let handle = logger::init_logger();
476    if std::env::var("ANGREAL_DEBUG").unwrap_or_default() == "true" {
477        logger::update_verbosity(&handle, 2);
478        warn!("Angreal application starting with debug level logging from environment");
479    }
480    debug!("Angreal application starting...");
481
482    // because we execute this from python main, we remove the first elements that
483    // IIRC its python and angreal
484    let mut argvs: Vec<String> = std::env::args().collect();
485    argvs = argvs.split_off(2);
486
487    // Auto-install shell completion on first run (before other operations)
488    if let Err(e) = completion::auto_install_completion() {
489        warn!("Failed to auto-install shell completion: {}", e);
490    }
491
492    debug!("Checking if binary is up to date...");
493    match utils::check_up_to_date() {
494        Ok(()) => (),
495        Err(e) => warn!(
496            "An error occurred while checking if our binary is up to date. {}",
497            e
498        ),
499    };
500
501    // Load any angreal task assets that are available to us
502    let angreal_project_result = utils::is_angreal_project();
503    let in_angreal_project = angreal_project_result.is_ok();
504
505    if in_angreal_project {
506        debug!("Angreal project detected, loading found tasks.");
507        let angreal_path = angreal_project_result.expect("Expected angreal project path");
508        // get a list of files
509        let angreal_tasks_to_load = utils::get_task_files(angreal_path);
510
511        // Explicitly capture error with exit
512        let _angreal_tasks_to_load = match angreal_tasks_to_load {
513            Ok(tasks) => tasks,
514            Err(_) => {
515                error!("Exiting due to unrecoverable error.");
516                exit(1);
517            }
518        };
519
520        // load the files , IF a file has command or task decorators - they'll register themselves now
521        for task in _angreal_tasks_to_load.iter() {
522            if let Err(e) = utils::load_python(task.clone()) {
523                error!("Failed to load Python task: {}", e);
524            }
525        }
526    }
527
528    let app = build_app(in_angreal_project);
529    let mut app_copy = app.clone();
530    let sub_command = app.get_matches_from(&argvs);
531
532    // Get our asked for verbosity and set the logger up. TODO: find a way to initialize earlier and reset after.
533    let verbosity = sub_command.get_count("verbose");
534
535    // If the user hasn't set the ANGREAL_DEBUG environment variable, set the verbosity from CLI settings
536    if std::env::var("ANGREAL_DEBUG").is_err() {
537        logger::update_verbosity(&handle, verbosity);
538        debug!("Log verbosity set to level: {}", verbosity);
539    }
540
541    match sub_command.subcommand() {
542        Some(("init", _sub_matches)) => init::init(
543            _sub_matches.value_of("template").unwrap(),
544            _sub_matches.is_present("force"),
545            _sub_matches.is_present("defaults").not(),
546            if _sub_matches.is_present("values_file") {
547                Some(_sub_matches.value_of("values_file").unwrap())
548            } else {
549                None
550            },
551        ),
552        Some(("_complete", _sub_matches)) => {
553            // Hidden command for shell completion
554            let args: Vec<String> = _sub_matches
555                .values_of("args")
556                .unwrap_or_default()
557                .map(|s| s.to_string())
558                .collect();
559
560            match completion::generate_completions(&args) {
561                Ok(completions) => {
562                    for completion in completions {
563                        println!("{}", completion);
564                    }
565                }
566                Err(e) => {
567                    debug!("Completion generation failed: {}", e);
568                }
569            }
570            return Ok(());
571        }
572        Some(("_completion", _sub_matches)) => {
573            // Hidden command for completion script generation
574            let shell = _sub_matches.value_of("shell").unwrap_or("bash");
575            match shell {
576                "bash" => println!("{}", completion::bash::generate_completion_script()),
577                "zsh" => println!("{}", completion::zsh::generate_completion_script()),
578                _ => {
579                    error!("Unsupported shell for completion: {}", shell);
580                    exit(1);
581                }
582            }
583            return Ok(());
584        }
585        Some(("alias", sub_matches)) => {
586            // Handle alias subcommands
587            match sub_matches.subcommand() {
588                Some(("create", create_matches)) => {
589                    let name = create_matches.value_of("name").unwrap();
590                    Python::attach(|_py| {
591                        if let Err(e) = register_entrypoint(name) {
592                            error!("Failed to create alias: {}", e);
593                            exit(1);
594                        }
595                    });
596                }
597                Some(("remove", remove_matches)) => {
598                    let name = remove_matches.value_of("name").unwrap();
599                    Python::attach(|_py| {
600                        if let Err(e) = unregister_entrypoint(name) {
601                            error!("Failed to remove alias: {}", e);
602                            exit(1);
603                        }
604                    });
605                }
606                Some(("list", _)) => {
607                    Python::attach(|_py| match list_entrypoints() {
608                        Ok(aliases) => {
609                            if aliases.is_empty() {
610                                println!("No aliases registered.");
611                            } else {
612                                println!("Registered aliases:");
613                                for alias in aliases {
614                                    println!("  {}", alias);
615                                }
616                            }
617                        }
618                        Err(e) => {
619                            error!("Failed to list aliases: {}", e);
620                            exit(1);
621                        }
622                    });
623                }
624                _ => {
625                    error!("Invalid alias subcommand. Use 'create', 'remove', or 'list'.");
626                    exit(1);
627                }
628            }
629            return Ok(());
630        }
631        Some(("completion", sub_matches)) => {
632            // Handle completion management subcommands
633            match sub_matches.subcommand() {
634                Some(("install", install_matches)) => {
635                    let shell = install_matches.value_of("shell");
636                    match crate::completion::force_install_completion(shell) {
637                        Ok(()) => {}
638                        Err(e) => {
639                            error!("Failed to install completion: {}", e);
640                            exit(1);
641                        }
642                    }
643                }
644                Some(("uninstall", uninstall_matches)) => {
645                    let shell = uninstall_matches.value_of("shell");
646                    match crate::completion::uninstall_completion(shell) {
647                        Ok(()) => {}
648                        Err(e) => {
649                            error!("Failed to uninstall completion: {}", e);
650                            exit(1);
651                        }
652                    }
653                }
654                Some(("status", _)) => match crate::completion::show_completion_status() {
655                    Ok(()) => {}
656                    Err(e) => {
657                        error!("Failed to show completion status: {}", e);
658                        exit(1);
659                    }
660                },
661                _ => {
662                    error!(
663                        "Invalid completion subcommand. Use 'install', 'uninstall', or 'status'."
664                    );
665                    exit(1);
666                }
667            }
668            return Ok(());
669        }
670        Some(("tree", sub_matches)) => {
671            if !in_angreal_project {
672                error!("This doesn't appear to be an angreal project.");
673                exit(1);
674            }
675
676            // Build command tree from registry
677            let mut root = command_tree::CommandNode::new_group("angreal".to_string(), None);
678            for (_, cmd) in ANGREAL_TASKS.lock().unwrap().iter() {
679                root.add_command(cmd.clone());
680            }
681
682            let long = sub_matches.get_flag("long");
683            tree_output::print_tree(&root, long);
684
685            return Ok(());
686        }
687        Some(("mcp", _)) => {
688            if !in_angreal_project {
689                error!("This doesn't appear to be an angreal project.");
690                exit(1);
691            }
692
693            mcp::serve();
694            return Ok(());
695        }
696        Some((task, sub_m)) => {
697            if !in_angreal_project {
698                error!("This doesn't appear to be an angreal project.");
699                exit(1)
700            }
701
702            let mut command_groups: Vec<String> = Vec::new();
703            command_groups.push(task.to_string());
704
705            // iterate matches to get our final command and get our final arg matches
706            // object for applying down stream
707            let mut next = sub_m.subcommand();
708            let mut arg_matches = sub_m.clone();
709            while next.is_some() {
710                let cmd = next.unwrap();
711                command_groups.push(cmd.0.to_string());
712                next = cmd.1.subcommand();
713                arg_matches = cmd.1.clone();
714            }
715
716            let task = command_groups.pop().unwrap();
717
718            // Generate the logical path key for command lookup
719            let command_path = generate_path_key_from_parts(&command_groups, &task);
720            let tasks_registry = ANGREAL_TASKS.lock().unwrap();
721
722            debug!("Looking up command with path: {}", command_path);
723            // Find the command by its logical path (registry keys include a
724            // unique suffix to prevent collisions during decoration).
725            let (registry_key, command) = match tasks_registry
726                .iter()
727                .find(|(_, cmd)| generate_command_path_key(cmd) == command_path)
728            {
729                None => {
730                    error!("Command '{}' not found.", task);
731                    app_copy.print_help().unwrap_or(());
732                    exit(1)
733                }
734                Some((key, found_command)) => (key.clone(), found_command),
735            };
736
737            debug!(
738                "Executing command: {} (registry key: {})",
739                task, registry_key
740            );
741
742            let args = builder::select_args(&registry_key);
743            Python::attach(|py| {
744                debug!("Starting Python execution for command: {}", task);
745                let mut kwargs: Vec<(&str, Py<PyAny>)> = Vec::new();
746
747                for arg in args.into_iter() {
748                    let n = Box::leak(Box::new(arg.name));
749                    // unable to find the value of the passed arg with sub_m when its been wrapped
750                    // in a command group
751
752                    if arg.is_flag.unwrap() {
753                        let v = arg_matches.get_flag(&n.clone());
754                        kwargs.push((
755                            n.as_str(),
756                            v.into_bound_py_any(py)
757                                .expect("Failed to convert to Python object")
758                                .unbind(),
759                        ));
760                    } else {
761                        let v = arg_matches.value_of(n.clone());
762                        match v {
763                            None => {
764                                // We need to handle "boolean flags" that are present w/o a value
765                                // should probably test that the name is a "boolean type also"
766                                kwargs.push((
767                                    n.as_str(),
768                                    v.into_bound_py_any(py)
769                                        .expect("Failed to convert to Python object")
770                                        .unbind(),
771                                ));
772                            }
773                            Some(v) => match arg.python_type.unwrap().as_str() {
774                                "str" => kwargs.push((
775                                    n.as_str(),
776                                    v.into_bound_py_any(py)
777                                        .expect("Failed to convert to Python object")
778                                        .unbind(),
779                                )),
780                                "int" => kwargs.push((
781                                    n.as_str(),
782                                    v.parse::<i32>()
783                                        .unwrap()
784                                        .into_bound_py_any(py)
785                                        .expect("Failed to convert to Python object")
786                                        .unbind(),
787                                )),
788                                "float" => kwargs.push((
789                                    n.as_str(),
790                                    v.parse::<f32>()
791                                        .unwrap()
792                                        .into_bound_py_any(py)
793                                        .expect("Failed to convert to Python object")
794                                        .unbind(),
795                                )),
796                                _ => kwargs.push((
797                                    n.as_str(),
798                                    v.into_bound_py_any(py)
799                                        .expect("Failed to convert to Python object")
800                                        .unbind(),
801                                )),
802                            },
803                        }
804                    }
805                }
806
807                let kwargs_dict = match kwargs.into_py_dict(py) {
808                    Ok(dict) => dict,
809                    Err(err) => {
810                        error!("Failed to convert kwargs to dict");
811                        let formatter = PythonErrorFormatter::new(err);
812                        println!("{}", formatter);
813                        exit(1);
814                    }
815                };
816                let r_value = command.func.call(py, (), Some(&kwargs_dict));
817
818                match r_value {
819                    Ok(r_value) => {
820                        // Check bool before int — in Python, bool is a subtype of int
821                        // (True == 1, False == 0), so extract::<i32> would match bools
822                        if let Ok(val) = r_value.extract::<bool>(py) {
823                            if !val {
824                                exit(1);
825                            }
826                        } else if let Ok(code) = r_value.extract::<i32>(py) {
827                            if code != 0 {
828                                exit(code);
829                            }
830                        }
831                        // None, True, or other → success
832                        debug!("Successfully executed Python command: {}", task);
833                    }
834                    Err(err) => {
835                        // SystemExit → exit with the original code
836                        let is_sys_exit = err
837                            .value(py)
838                            .get_type()
839                            .name()
840                            .map(|n| n == "SystemExit")
841                            .unwrap_or(false);
842                        if is_sys_exit {
843                            let code = err
844                                .value(py)
845                                .getattr("code")
846                                .and_then(|c| c.extract::<i32>())
847                                .unwrap_or(1);
848                            exit(code);
849                        }
850                        error!("Failed to execute Python command: {}", task);
851                        let formatter = PythonErrorFormatter::new(err);
852                        println!("{}", formatter);
853                        exit(56);
854                    }
855                }
856            });
857        }
858        _ => {
859            println!("process for current context")
860        }
861    }
862
863    debug!("Angreal application completed successfully.");
864    Ok(())
865}
866
867/// Initialize Python bindings and load angreal tasks for external tools
868/// This function should be called by any external tool that needs to discover angreal commands
869pub fn initialize_python_tasks() -> Result<(), Box<dyn std::error::Error>> {
870    use pyo3::types::PyDict;
871
872    debug!("Initializing Python bindings for angreal tasks");
873
874    // First, ensure the angreal module is registered in Python
875    Python::attach(|py| -> PyResult<()> {
876        // Get sys.modules
877        let sys = PyModule::import(py, "sys")?;
878        let modules_attr = sys.getattr("modules")?;
879        let modules = modules_attr.cast::<PyDict>()?;
880
881        // Check if angreal module is already available
882        if !modules.contains("angreal")? {
883            debug!("Registering angreal module in Python sys.modules");
884
885            // Create the angreal module manually
886            let angreal_module = PyModule::new(py, "angreal")?;
887
888            // Register the module components (from the pymodule function)
889            angreal_module.add("__version__", env!("CARGO_PKG_VERSION"))?;
890
891            // Register logger
892            py_logger::register();
893
894            // Register core components
895            task::register(py, &angreal_module)?;
896            utils::register(py, &angreal_module)?;
897            python_bindings::decorators::register_decorators(py, &angreal_module)?;
898
899            // Register integrations submodule (from the full pymodule function)
900            angreal_module
901                .add_wrapped(wrap_pymodule!(python_bindings::integrations::integrations))?;
902
903            // Set up sys.modules entries for integrations (matching the pymodule function)
904            modules.set_item(
905                "angreal.integrations",
906                angreal_module.getattr("integrations")?,
907            )?;
908            modules.set_item(
909                "angreal.integrations.docker",
910                angreal_module
911                    .getattr("integrations")?
912                    .getattr("docker_integration")?,
913            )?;
914            modules.set_item(
915                "angreal.integrations.git",
916                angreal_module
917                    .getattr("integrations")?
918                    .getattr("git_integration")?,
919            )?;
920            modules.set_item(
921                "angreal.integrations.venv",
922                angreal_module.getattr("integrations")?.getattr("venv")?,
923            )?;
924            modules.set_item(
925                "angreal.integrations.flox",
926                angreal_module.getattr("integrations")?.getattr("flox")?,
927            )?;
928
929            // Register the main module in sys.modules
930            modules.set_item("angreal", angreal_module)?;
931
932            debug!("Successfully registered angreal module in Python");
933        } else {
934            debug!("Angreal module already available in sys.modules");
935        }
936
937        Ok(())
938    })?;
939
940    // Check if we're in an angreal project
941    let angreal_path =
942        utils::is_angreal_project().map_err(|e| format!("Not in angreal project: {}", e))?;
943
944    debug!("Found angreal project at: {}", angreal_path.display());
945
946    // Get task files
947    let task_files = utils::get_task_files(angreal_path)
948        .map_err(|e| format!("Failed to get task files: {}", e))?;
949
950    debug!("Found {} task files to load", task_files.len());
951
952    // Load each Python task file to populate ANGREAL_TASKS registry
953    for task_file in task_files.iter() {
954        debug!("Loading Python task file: {}", task_file.display());
955
956        match utils::load_python(task_file.clone()) {
957            Ok(_) => debug!("Successfully loaded task file: {}", task_file.display()),
958            Err(e) => {
959                warn!("Failed to load task file {}: {}", task_file.display(), e);
960                // Continue loading other files even if one fails
961            }
962        }
963    }
964
965    let task_count = ANGREAL_TASKS.lock().unwrap().len();
966    debug!("Successfully initialized {} angreal tasks", task_count);
967
968    Ok(())
969}
970
971#[pymodule]
972fn angreal(m: &Bound<'_, PyModule>) -> PyResult<()> {
973    m.add("__version__", env!("CARGO_PKG_VERSION"))?;
974
975    py_logger::register();
976    m.add_function(wrap_pyfunction!(main, m)?)?;
977    task::register(m.py(), m)?;
978    utils::register(m.py(), m)?;
979
980    // Register decorators from our new python_bindings module
981    python_bindings::decorators::register_decorators(m.py(), m)?;
982
983    // UV integration functions
984    m.add_function(wrap_pyfunction!(ensure_uv_installed, m)?)?;
985    m.add_function(wrap_pyfunction!(uv_version, m)?)?;
986    m.add_function(wrap_pyfunction!(create_virtualenv, m)?)?;
987    m.add_function(wrap_pyfunction!(install_packages, m)?)?;
988    m.add_function(wrap_pyfunction!(install_requirements, m)?)?;
989    m.add_function(wrap_pyfunction!(discover_pythons, m)?)?;
990    m.add_function(wrap_pyfunction!(install_python, m)?)?;
991    m.add_function(wrap_pyfunction!(get_venv_activation_info, m)?)?;
992    m.add_class::<integrations::uv::ActivationInfo>()?;
993
994    // Entrypoint registration functions
995    m.add_function(wrap_pyfunction!(register_entrypoint, m)?)?;
996    m.add_function(wrap_pyfunction!(list_entrypoints, m)?)?;
997    m.add_function(wrap_pyfunction!(unregister_entrypoint, m)?)?;
998    m.add_function(wrap_pyfunction!(cleanup_entrypoints, m)?)?;
999
1000    let integrations_module = PyModule::new(m.py(), "integrations")?;
1001    python_bindings::integrations::integrations(m.py(), &integrations_module)?;
1002    m.add_submodule(&integrations_module)?;
1003
1004    let sys = PyModule::import(m.py(), "sys")?;
1005    let modules_attr = sys.getattr("modules")?;
1006    let sys_modules = modules_attr.cast::<PyDict>()?;
1007    sys_modules.set_item("angreal.integrations", m.getattr("integrations")?)?;
1008    sys_modules.set_item(
1009        "angreal.integrations.docker",
1010        m.getattr("integrations")?.getattr("docker_integration")?,
1011    )?;
1012
1013    sys_modules.set_item(
1014        "angreal.integrations.docker.image",
1015        m.getattr("integrations")?
1016            .getattr("docker_integration")?
1017            .getattr("image")?,
1018    )?;
1019    sys_modules.set_item(
1020        "angreal.integrations.docker.container",
1021        m.getattr("integrations")?
1022            .getattr("docker_integration")?
1023            .getattr("container")?,
1024    )?;
1025    sys_modules.set_item(
1026        "angreal.integrations.docker.network",
1027        m.getattr("integrations")?
1028            .getattr("docker_integration")?
1029            .getattr("network")?,
1030    )?;
1031    sys_modules.set_item(
1032        "angreal.integrations.docker.volume",
1033        m.getattr("integrations")?
1034            .getattr("docker_integration")?
1035            .getattr("volume")?,
1036    )?;
1037
1038    sys_modules.set_item(
1039        "angreal.integrations.git",
1040        m.getattr("integrations")?.getattr("git_integration")?,
1041    )?;
1042
1043    sys_modules.set_item(
1044        "angreal.integrations.venv",
1045        m.getattr("integrations")?.getattr("venv")?,
1046    )?;
1047
1048    sys_modules.set_item(
1049        "angreal.integrations.flox",
1050        m.getattr("integrations")?.getattr("flox")?,
1051    )?;
1052
1053    Ok(())
1054}
1055
1056#[pymodule]
1057fn _integrations(m: &Bound<'_, PyModule>) -> PyResult<()> {
1058    let docker_module = pyo3::wrap_pymodule!(docker)(m.py());
1059    m.add_submodule(docker_module.bind(m.py()))?;
1060    let git = pyo3::wrap_pymodule!(git_module)(m.py());
1061    m.add_submodule(git.bind(m.py()))?;
1062    Ok(())
1063}
1064
1065#[pymodule]
1066fn docker(m: &Bound<'_, PyModule>) -> PyResult<()> {
1067    m.add_class::<docker_pyo3::Pyo3Docker>()?;
1068
1069    let image_module = PyModule::new(m.py(), "image")?;
1070    docker_pyo3::image::image(m.py(), &image_module)?;
1071    m.add_submodule(&image_module)?;
1072
1073    let container_module = PyModule::new(m.py(), "container")?;
1074    docker_pyo3::container::container(m.py(), &container_module)?;
1075    m.add_submodule(&container_module)?;
1076
1077    let network_module = PyModule::new(m.py(), "network")?;
1078    docker_pyo3::network::network(m.py(), &network_module)?;
1079    m.add_submodule(&network_module)?;
1080
1081    let volume_module = PyModule::new(m.py(), "volume")?;
1082    docker_pyo3::volume::volume(m.py(), &volume_module)?;
1083    m.add_submodule(&volume_module)?;
1084    Ok(())
1085}
1086
1087#[pymodule]
1088fn git_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
1089    m.add_class::<PyGit>()?;
1090    m.add_function(wrap_pyfunction!(git_clone, m)?)?;
1091    Ok(())
1092}