max-cli 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
max_cli/main.py ADDED
@@ -0,0 +1,91 @@
1
+ import typer
2
+ import sys
3
+ from rich.console import Console
4
+
5
+ # Import interfaces
6
+ from max_cli.interface import (
7
+ cli_images,
8
+ cli_files,
9
+ cli_pdf,
10
+ cli_ai,
11
+ cli_media,
12
+ cli_network,
13
+ cli_tools,
14
+ cli_config,
15
+ ) # <--- Import this
16
+ from max_cli.common.exceptions import MaxError
17
+
18
+ # Initialize Console directly here to ensure it's available for the crash handler
19
+ console = Console()
20
+
21
+ app = typer.Typer(
22
+ name="max",
23
+ help="MAX: The High-Performance CLI Utility.",
24
+ add_completion=True,
25
+ no_args_is_help=True,
26
+ )
27
+
28
+ # --- 1. Register Commands with Aliases ---
29
+ # We register 'images' AND 'img' so users can type less.
30
+ app.add_typer(
31
+ cli_images.app, name="images", help="Compress, resize, and convert images."
32
+ )
33
+ app.add_typer(cli_images.app, name="img", hidden=True) # Hidden alias
34
+
35
+ app.add_typer(cli_files.app, name="files", help="Organize and bulk-rename files.")
36
+ app.add_typer(cli_files.app, name="file", hidden=True) # Hidden alias
37
+
38
+ app.add_typer(cli_pdf.app, name="pdf", help="Merge, split, and compress PDFs.")
39
+
40
+ # Register Video/Audio
41
+ app.add_typer(
42
+ cli_media.app, name="video", help="Compress, convert, and process video/audio."
43
+ )
44
+ app.add_typer(cli_media.app, name="v", hidden=True) # Short alias
45
+
46
+ app.add_typer(cli_network.app, name="net", help="Network tools (Download, Speedtest).")
47
+
48
+ app.add_typer(cli_ai.app, name="ai", help="Ask AI to run commands.")
49
+
50
+ # Create a top-level shortcut because 'max grab' is very common
51
+ app.command("grab")(cli_network.download_media)
52
+
53
+ # Register the group (optional, if you want 'max tools share')
54
+ app.add_typer(cli_tools.app, name="tools", help="System utilities (Clipboard, QR).")
55
+
56
+ # Register config commands
57
+ app.add_typer(cli_config.app, name="config", help="Manage API keys and settings.")
58
+
59
+ # Register top-level Shortcuts (The "Lazy" Way)
60
+ # This allows 'max share' instead of 'max tools share'
61
+ app.command("share")(cli_tools.share_qr)
62
+ app.command("paste")(cli_tools.paste_image)
63
+ app.command("copy")(cli_tools.copy_file)
64
+
65
+ # --- CRITICAL LINKING STEP ---
66
+ # Give the AI module access to this app instance so it can read the docs
67
+ cli_ai.MAIN_APP_REF = app
68
+
69
+
70
+ def main():
71
+ """
72
+ Main entry point with Global Error Handling.
73
+ """
74
+ try:
75
+ app()
76
+ except MaxError as e:
77
+ # Expected errors (User mistake, missing file)
78
+ console.print(f"[bold red]✖ Error:[/bold red] {e}")
79
+ sys.exit(1)
80
+ except Exception as e:
81
+ # Unexpected crashes (Bugs)
82
+ console.print("[bold red]💥 Critical Error (Unexpected)[/bold red]")
83
+ console.print(f"An error occurred: {e}")
84
+ console.print("[dim]If this persists, please report it to the developer.[/dim]")
85
+ # Uncomment the next line during development to see the full stack trace:
86
+ # raise e
87
+ sys.exit(1)
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
@@ -0,0 +1,4 @@
1
+ from max_cli.plugins.base import CLIPlugin, EnginePlugin, Plugin
2
+ from max_cli.plugins.manager import PluginManager
3
+
4
+ __all__ = ["CLIPlugin", "EnginePlugin", "Plugin", "PluginManager"]
@@ -0,0 +1,39 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+
5
+ class Plugin(ABC):
6
+ @property
7
+ @abstractmethod
8
+ def name(self) -> str:
9
+ pass
10
+
11
+ @property
12
+ @abstractmethod
13
+ def version(self) -> str:
14
+ pass
15
+
16
+ @property
17
+ def description(self) -> str:
18
+ return ""
19
+
20
+ @abstractmethod
21
+ def register(self, app: Any) -> None:
22
+ pass
23
+
24
+ def unregister(self, app: Any) -> None:
25
+ pass
26
+
27
+
28
+ class CLIPlugin(Plugin):
29
+ @property
30
+ def command_name(self) -> str:
31
+ return self.name.replace("-", "_").replace(" ", "_").lower()
32
+
33
+ @property
34
+ def help_text(self) -> str:
35
+ return self.description
36
+
37
+
38
+ class EnginePlugin(Plugin):
39
+ pass
@@ -0,0 +1,81 @@
1
+ import importlib
2
+ import importlib.util
3
+ import logging
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional, Type
7
+
8
+ from max_cli.plugins.base import Plugin
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class PluginManager:
14
+ def __init__(self, plugin_dirs: Optional[List[Path]] = None):
15
+ self._plugins: Dict[str, Plugin] = {}
16
+ self._plugin_dirs = plugin_dirs or self._get_default_plugin_dirs()
17
+
18
+ def _get_default_plugin_dirs(self) -> List[Path]:
19
+ dirs = [
20
+ Path.home() / ".max_cli" / "plugins",
21
+ Path.cwd() / "plugins",
22
+ ]
23
+ return [d for d in dirs if d.exists()]
24
+
25
+ def discover_plugins(self) -> List[Type[Plugin]]:
26
+ plugins: List[Type[Plugin]] = []
27
+ for plugin_dir in self._plugin_dirs:
28
+ if not plugin_dir.exists():
29
+ continue
30
+ for plugin_file in plugin_dir.glob("*.py"):
31
+ if plugin_file.name.startswith("_"):
32
+ continue
33
+ try:
34
+ module_name = f"max_cli_plugins.{plugin_file.stem}"
35
+ spec = importlib.util.spec_from_file_location(
36
+ module_name, plugin_file
37
+ )
38
+ if spec and spec.loader:
39
+ module = importlib.util.module_from_spec(spec)
40
+ sys.modules[module_name] = module
41
+ spec.loader.exec_module(module)
42
+ for attr_name in dir(module):
43
+ attr = getattr(module, attr_name)
44
+ if (
45
+ isinstance(attr, type)
46
+ and issubclass(attr, Plugin)
47
+ and attr is not Plugin
48
+ ):
49
+ plugins.append(attr)
50
+ except Exception as e:
51
+ logger.warning(f"Failed to load plugin from {plugin_file}: {e}")
52
+ return plugins
53
+
54
+ def load_plugin(self, plugin_class: Type[Plugin], **kwargs: Any) -> Plugin:
55
+ plugin = plugin_class(**kwargs)
56
+ self._plugins[plugin.name] = plugin
57
+ return plugin
58
+
59
+ def register_plugin(self, plugin: Plugin) -> None:
60
+ self._plugins[plugin.name] = plugin
61
+
62
+ def unregister_plugin(self, name: str) -> None:
63
+ if name in self._plugins:
64
+ del self._plugins[name]
65
+
66
+ def get_plugin(self, name: str) -> Optional[Plugin]:
67
+ return self._plugins.get(name)
68
+
69
+ def list_plugins(self) -> List[str]:
70
+ return list(self._plugins.keys())
71
+
72
+ def get_all_plugins(self) -> Dict[str, Plugin]:
73
+ return self._plugins.copy()
74
+
75
+ def load_all(self) -> None:
76
+ plugin_classes = self.discover_plugins()
77
+ for plugin_class in plugin_classes:
78
+ try:
79
+ self.load_plugin(plugin_class)
80
+ except Exception as e:
81
+ logger.warning(f"Failed to load plugin {plugin_class}: {e}")