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/__init__.py +0 -0
- max_cli/common/cache.py +145 -0
- max_cli/common/concurrent.py +83 -0
- max_cli/common/exceptions.py +40 -0
- max_cli/common/logger.py +22 -0
- max_cli/common/logging.py +24 -0
- max_cli/common/retry.py +51 -0
- max_cli/common/utils.py +40 -0
- max_cli/config.py +43 -0
- max_cli/core/ai_engine.py +541 -0
- max_cli/core/file_organizer.py +254 -0
- max_cli/core/image_processor.py +139 -0
- max_cli/core/media_engine.py +681 -0
- max_cli/core/network_engine.py +103 -0
- max_cli/core/pdf_engine.py +520 -0
- max_cli/core/system_engine.py +57 -0
- max_cli/interface/cli_ai.py +376 -0
- max_cli/interface/cli_config.py +363 -0
- max_cli/interface/cli_files.py +388 -0
- max_cli/interface/cli_images.py +176 -0
- max_cli/interface/cli_media.py +558 -0
- max_cli/interface/cli_network.py +174 -0
- max_cli/interface/cli_pdf.py +651 -0
- max_cli/interface/cli_tools.py +60 -0
- max_cli/main.py +91 -0
- max_cli/plugins/__init__.py +4 -0
- max_cli/plugins/base.py +39 -0
- max_cli/plugins/manager.py +81 -0
- max_cli-0.2.0.dist-info/METADATA +632 -0
- max_cli-0.2.0.dist-info/RECORD +34 -0
- max_cli-0.2.0.dist-info/WHEEL +5 -0
- max_cli-0.2.0.dist-info/entry_points.txt +2 -0
- max_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- max_cli-0.2.0.dist-info/top_level.txt +1 -0
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()
|
max_cli/plugins/base.py
ADDED
|
@@ -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}")
|