ali-tool 0.1.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.
- ali_tool/__init__.py +3 -0
- ali_tool/cli.py +219 -0
- ali_tool/core/__init__.py +13 -0
- ali_tool/core/logging.py +130 -0
- ali_tool/core/plugin.py +101 -0
- ali_tool/core/registry.py +62 -0
- ali_tool/core/resolver.py +347 -0
- ali_tool/core/router.py +286 -0
- ali_tool/plugins/broot/ali.hjson +36 -0
- ali_tool/plugins/broot/plugin.yaml +61 -0
- ali_tool/plugins/llms.txt +156 -0
- ali_tool/plugins/micro/plugin.yaml +50 -0
- ali_tool/plugins/tmux/plugin.yaml +156 -0
- ali_tool/plugins/tmux/scripts/distribute.py +295 -0
- ali_tool/plugins/tmux/tmux.conf.ali +14 -0
- ali_tool/scripts.py +90 -0
- ali_tool-0.1.0.dist-info/METADATA +144 -0
- ali_tool-0.1.0.dist-info/RECORD +20 -0
- ali_tool-0.1.0.dist-info/WHEEL +4 -0
- ali_tool-0.1.0.dist-info/entry_points.txt +3 -0
ali_tool/__init__.py
ADDED
ali_tool/cli.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ALI CLI - Main entry point."""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import os
|
|
6
|
+
import argparse
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .core import ServiceRegistry, Router
|
|
10
|
+
from .core.logging import ALILogger
|
|
11
|
+
from .scripts import execute_script
|
|
12
|
+
|
|
13
|
+
EXIT_SUCCESS = 0
|
|
14
|
+
EXIT_ERROR = 1
|
|
15
|
+
EXIT_COMMAND_NOT_FOUND = 127
|
|
16
|
+
|
|
17
|
+
ALI_VERSION = "2.0.0"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_plugins_dir() -> Path:
|
|
21
|
+
"""Find plugins directory in package or user config."""
|
|
22
|
+
package_dir = Path(__file__).parent / "plugins"
|
|
23
|
+
if package_dir.exists():
|
|
24
|
+
return package_dir
|
|
25
|
+
|
|
26
|
+
user_plugins = Path.home() / ".config" / "ali" / "plugins"
|
|
27
|
+
if user_plugins.exists():
|
|
28
|
+
return user_plugins
|
|
29
|
+
|
|
30
|
+
return package_dir
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main():
|
|
34
|
+
"""Main CLI entry point."""
|
|
35
|
+
if "--plugin-script" in sys.argv:
|
|
36
|
+
exit_code = execute_script(sys.argv)
|
|
37
|
+
sys.exit(exit_code)
|
|
38
|
+
|
|
39
|
+
parser = argparse.ArgumentParser(
|
|
40
|
+
description="ALI - Action Language Interpreter",
|
|
41
|
+
epilog="Examples:\n"
|
|
42
|
+
" ali GO .2 # Go to pane 2\n"
|
|
43
|
+
" ali SPLIT pop # Open popup shell\n"
|
|
44
|
+
" ali EDIT file.txt pop # Edit in popup\n"
|
|
45
|
+
" ali WIDTH 012 # Distribute panes evenly\n"
|
|
46
|
+
" ali ECHO ed? # Edit and pipe output\n",
|
|
47
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--version",
|
|
52
|
+
action="version",
|
|
53
|
+
version=f"ALI {ALI_VERSION}",
|
|
54
|
+
help="Show version and exit",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--list-verbs",
|
|
59
|
+
action="store_true",
|
|
60
|
+
help="List all available verbs",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--list-services",
|
|
65
|
+
action="store_true",
|
|
66
|
+
help="List all available services",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--list-grammar",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="List grammar definitions for all plugins",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--verbose",
|
|
77
|
+
"-v",
|
|
78
|
+
action="store_true",
|
|
79
|
+
help="Enable verbose logging to stderr",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--quiet",
|
|
84
|
+
"-q",
|
|
85
|
+
action="store_true",
|
|
86
|
+
help="Disable all logging",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--plugins-dir",
|
|
91
|
+
type=Path,
|
|
92
|
+
help="Directory containing plugin definitions",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--init",
|
|
97
|
+
metavar="PLUGIN",
|
|
98
|
+
help="Initialize plugin integration (e.g., --init tmux)",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"command",
|
|
103
|
+
nargs="*",
|
|
104
|
+
help="ALI command to execute",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
args = parser.parse_args()
|
|
108
|
+
|
|
109
|
+
log_level = os.environ.get("ALI_LOG_LEVEL", "")
|
|
110
|
+
if args.quiet:
|
|
111
|
+
logger = None
|
|
112
|
+
elif args.verbose or log_level.upper() in ["DEBUG", "VERBOSE"]:
|
|
113
|
+
logger = ALILogger(verbose=True)
|
|
114
|
+
else:
|
|
115
|
+
logger = ALILogger(verbose=False)
|
|
116
|
+
|
|
117
|
+
registry = ServiceRegistry(logger=logger)
|
|
118
|
+
plugins_dir = args.plugins_dir or find_plugins_dir()
|
|
119
|
+
|
|
120
|
+
if not plugins_dir.exists():
|
|
121
|
+
print(f"Error: Plugins directory not found: {plugins_dir}", file=sys.stderr)
|
|
122
|
+
sys.exit(EXIT_ERROR)
|
|
123
|
+
|
|
124
|
+
registry.load_plugins(plugins_dir)
|
|
125
|
+
|
|
126
|
+
if logger:
|
|
127
|
+
logger.registry = registry
|
|
128
|
+
|
|
129
|
+
if args.init:
|
|
130
|
+
found = False
|
|
131
|
+
for plugin in registry.plugins:
|
|
132
|
+
if plugin.name == args.init:
|
|
133
|
+
found = True
|
|
134
|
+
exit_code = plugin.init()
|
|
135
|
+
sys.exit(exit_code if exit_code == 0 else EXIT_ERROR)
|
|
136
|
+
if not found:
|
|
137
|
+
print(f"Error: Plugin '{args.init}' not found", file=sys.stderr)
|
|
138
|
+
print(
|
|
139
|
+
f"Available plugins: {', '.join(p.name for p in registry.plugins)}",
|
|
140
|
+
file=sys.stderr,
|
|
141
|
+
)
|
|
142
|
+
sys.exit(EXIT_ERROR)
|
|
143
|
+
|
|
144
|
+
if args.list_services:
|
|
145
|
+
print("Available services:", file=sys.stderr)
|
|
146
|
+
for service, providers in sorted(registry.providers.items()):
|
|
147
|
+
provider_names = [p.name for p in providers]
|
|
148
|
+
print(
|
|
149
|
+
f" {service:20} provided by: {', '.join(provider_names)}",
|
|
150
|
+
file=sys.stderr,
|
|
151
|
+
)
|
|
152
|
+
sys.exit(EXIT_SUCCESS)
|
|
153
|
+
|
|
154
|
+
if args.list_grammar:
|
|
155
|
+
print("Grammar definitions by plugin:", file=sys.stderr)
|
|
156
|
+
for plugin in registry.plugins:
|
|
157
|
+
if plugin.grammar:
|
|
158
|
+
print(f"\n{plugin.name}:", file=sys.stderr)
|
|
159
|
+
for field, grammar in plugin.grammar.items():
|
|
160
|
+
if "pattern" in grammar:
|
|
161
|
+
pattern = grammar["pattern"]
|
|
162
|
+
if len(pattern) > 40:
|
|
163
|
+
pattern = pattern[:40] + "..."
|
|
164
|
+
print(f" {field:15} pattern: {pattern}", file=sys.stderr)
|
|
165
|
+
elif "values" in grammar:
|
|
166
|
+
values_str = ", ".join(grammar["values"][:3])
|
|
167
|
+
if len(grammar["values"]) > 3:
|
|
168
|
+
values_str += "..."
|
|
169
|
+
print(f" {field:15} values: [{values_str}]", file=sys.stderr)
|
|
170
|
+
elif "type" in grammar:
|
|
171
|
+
print(f" {field:15} type: {grammar['type']}", file=sys.stderr)
|
|
172
|
+
sys.exit(EXIT_SUCCESS)
|
|
173
|
+
|
|
174
|
+
if args.list_verbs:
|
|
175
|
+
if registry.verb_index:
|
|
176
|
+
print("Available verbs:", file=sys.stderr)
|
|
177
|
+
for verb, plugin_list in sorted(registry.verb_index.items()):
|
|
178
|
+
if plugin_list:
|
|
179
|
+
plugin_names = ", ".join(p.name for p in plugin_list)
|
|
180
|
+
print(f" {verb:12} ({plugin_names})", file=sys.stderr)
|
|
181
|
+
else:
|
|
182
|
+
print("No verbs available (no plugins loaded)", file=sys.stderr)
|
|
183
|
+
sys.exit(EXIT_SUCCESS)
|
|
184
|
+
|
|
185
|
+
if not args.command:
|
|
186
|
+
parser.print_help()
|
|
187
|
+
sys.exit(EXIT_ERROR)
|
|
188
|
+
|
|
189
|
+
command_str = " ".join(args.command)
|
|
190
|
+
|
|
191
|
+
router = Router(registry)
|
|
192
|
+
result = router.execute(command_str)
|
|
193
|
+
|
|
194
|
+
if logger:
|
|
195
|
+
tokens = router.last_tokens if hasattr(router, "last_tokens") else None
|
|
196
|
+
state = router.last_state if hasattr(router, "last_state") else None
|
|
197
|
+
success = not (result.startswith("Error:") or result.startswith("Unknown"))
|
|
198
|
+
logger.log_command(
|
|
199
|
+
command_str,
|
|
200
|
+
tokens=tokens,
|
|
201
|
+
state=state,
|
|
202
|
+
result=result if success else None,
|
|
203
|
+
error=result if not success else None,
|
|
204
|
+
success=success,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if result.startswith("Error:") or result.startswith("Unknown"):
|
|
208
|
+
print(result, file=sys.stderr)
|
|
209
|
+
exit_code = (
|
|
210
|
+
EXIT_COMMAND_NOT_FOUND if result.startswith("Unknown") else EXIT_ERROR
|
|
211
|
+
)
|
|
212
|
+
sys.exit(exit_code)
|
|
213
|
+
|
|
214
|
+
print(result)
|
|
215
|
+
sys.exit(EXIT_SUCCESS)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
main()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""ALI Core - Service-oriented plugin architecture."""
|
|
2
|
+
|
|
3
|
+
from .plugin import Plugin
|
|
4
|
+
from .registry import ServiceRegistry
|
|
5
|
+
from .resolver import resolve_command
|
|
6
|
+
from .router import Router
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Plugin",
|
|
10
|
+
"ServiceRegistry",
|
|
11
|
+
"resolve_command",
|
|
12
|
+
"Router",
|
|
13
|
+
]
|
ali_tool/core/logging.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Centralized logging for ALI - plugin-driven environment capture."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
import os
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ALILogger:
|
|
12
|
+
"""Structured logging for pattern analysis."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, verbose: bool = False, registry=None):
|
|
15
|
+
self.session_id = self._generate_session_id()
|
|
16
|
+
self.log_dir = Path.home() / ".local/share/ali/logs"
|
|
17
|
+
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
self.registry = registry
|
|
19
|
+
|
|
20
|
+
self._setup_python_logging(verbose)
|
|
21
|
+
|
|
22
|
+
def _generate_session_id(self) -> str:
|
|
23
|
+
"""Generate unique session ID with timestamp and UUID."""
|
|
24
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
25
|
+
unique = str(uuid.uuid4())[:8]
|
|
26
|
+
return f"{timestamp}_{unique}"
|
|
27
|
+
|
|
28
|
+
def _setup_python_logging(self, verbose: bool):
|
|
29
|
+
"""Configure Python logging handlers and formatters."""
|
|
30
|
+
self.logger = logging.getLogger("ali")
|
|
31
|
+
self.logger.setLevel(logging.DEBUG)
|
|
32
|
+
|
|
33
|
+
file_handler = logging.FileHandler(
|
|
34
|
+
self.log_dir / f"debug_{self.session_id}.log"
|
|
35
|
+
)
|
|
36
|
+
file_handler.setLevel(logging.DEBUG)
|
|
37
|
+
file_formatter = logging.Formatter(
|
|
38
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
39
|
+
)
|
|
40
|
+
file_handler.setFormatter(file_formatter)
|
|
41
|
+
self.logger.addHandler(file_handler)
|
|
42
|
+
|
|
43
|
+
if verbose:
|
|
44
|
+
import sys
|
|
45
|
+
|
|
46
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
47
|
+
console_handler.setLevel(logging.DEBUG)
|
|
48
|
+
console_formatter = logging.Formatter("%(levelname)s: %(message)s")
|
|
49
|
+
console_handler.setFormatter(console_formatter)
|
|
50
|
+
self.logger.addHandler(console_handler)
|
|
51
|
+
|
|
52
|
+
def _collect_plugin_env(self) -> dict:
|
|
53
|
+
"""Collect environment variables defined by plugins."""
|
|
54
|
+
env = {}
|
|
55
|
+
|
|
56
|
+
if not self.registry:
|
|
57
|
+
return env
|
|
58
|
+
|
|
59
|
+
for plugin in self.registry.plugins:
|
|
60
|
+
metadata = plugin.config.get("metadata", {})
|
|
61
|
+
env_config = metadata.get("environment", {})
|
|
62
|
+
|
|
63
|
+
for var_list in ["requires", "optional", "captures"]:
|
|
64
|
+
for var in env_config.get(var_list, []):
|
|
65
|
+
if var in os.environ:
|
|
66
|
+
env[var] = os.environ[var]
|
|
67
|
+
|
|
68
|
+
return env
|
|
69
|
+
|
|
70
|
+
def log_command(
|
|
71
|
+
self,
|
|
72
|
+
command_str: str,
|
|
73
|
+
tokens: list | None = None,
|
|
74
|
+
state: dict | None = None,
|
|
75
|
+
result: str | None = None,
|
|
76
|
+
error: str | None = None,
|
|
77
|
+
success: bool = True,
|
|
78
|
+
):
|
|
79
|
+
"""Log command execution with context and results.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
command_str: Raw command string
|
|
83
|
+
tokens: Parsed command tokens
|
|
84
|
+
state: Command state after parsing
|
|
85
|
+
result: Command result if successful
|
|
86
|
+
error: Error message if failed
|
|
87
|
+
success: Whether command executed successfully
|
|
88
|
+
"""
|
|
89
|
+
event = {
|
|
90
|
+
"timestamp": datetime.now().isoformat(),
|
|
91
|
+
"session_id": self.session_id,
|
|
92
|
+
"command_raw": command_str,
|
|
93
|
+
"tokens": tokens,
|
|
94
|
+
"state": state,
|
|
95
|
+
"result": result,
|
|
96
|
+
"error": error,
|
|
97
|
+
"success": success,
|
|
98
|
+
"env": self._collect_plugin_env(),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
with open(self.log_dir / "commands.jsonl", "a") as f:
|
|
102
|
+
f.write(json.dumps(event) + "\n")
|
|
103
|
+
|
|
104
|
+
if success:
|
|
105
|
+
self.logger.debug(f"Command: {command_str} -> {result}")
|
|
106
|
+
else:
|
|
107
|
+
self.logger.error(f"Command failed: {command_str} -> {error}")
|
|
108
|
+
|
|
109
|
+
def log_plugin_load(
|
|
110
|
+
self, plugin_name: str, success: bool, error: str | None = None
|
|
111
|
+
):
|
|
112
|
+
"""Log plugin loading success or failure.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
plugin_name: Name of the plugin
|
|
116
|
+
success: Whether plugin loaded successfully
|
|
117
|
+
error: Error message if loading failed
|
|
118
|
+
"""
|
|
119
|
+
if success:
|
|
120
|
+
self.logger.debug(f"Loaded plugin: {plugin_name}")
|
|
121
|
+
else:
|
|
122
|
+
self.logger.warning(f"Failed to load plugin {plugin_name}: {error}")
|
|
123
|
+
|
|
124
|
+
def debug(self, message: str):
|
|
125
|
+
"""Log debug message."""
|
|
126
|
+
self.logger.debug(message)
|
|
127
|
+
|
|
128
|
+
def error(self, message: str):
|
|
129
|
+
"""Log error message."""
|
|
130
|
+
self.logger.error(message)
|
ali_tool/core/plugin.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Plugin - Loads and holds plugin YAML configuration."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import yaml
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Plugin:
|
|
11
|
+
"""A plugin loaded from YAML. Just data, no logic."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, yaml_path: Path):
|
|
14
|
+
self.path = yaml_path
|
|
15
|
+
self.name = yaml_path.parent.name
|
|
16
|
+
|
|
17
|
+
with open(yaml_path) as f:
|
|
18
|
+
self.config = yaml.safe_load(f)
|
|
19
|
+
|
|
20
|
+
self.version = self.config.get("version", "1.0")
|
|
21
|
+
self.description = self.config.get("description", "")
|
|
22
|
+
self.provides = self.config.get("provides", {})
|
|
23
|
+
self.requires = self.config.get("requires", [])
|
|
24
|
+
self.grammar = self.config.get("grammar", {})
|
|
25
|
+
|
|
26
|
+
vocab = self.config.get("vocabulary", {})
|
|
27
|
+
self.verbs = set(vocab.get("verbs", []))
|
|
28
|
+
|
|
29
|
+
self.expectations = self.config.get("expectations", {})
|
|
30
|
+
self.inference = self.config.get("inference", [])
|
|
31
|
+
self.commands = self.config.get("commands", [])
|
|
32
|
+
self.selectors = self.config.get("selectors", {})
|
|
33
|
+
self.context = self.config.get("context", {})
|
|
34
|
+
self.integration = self.config.get("integration", {})
|
|
35
|
+
|
|
36
|
+
def is_active(self) -> bool:
|
|
37
|
+
"""Check if plugin should be active based on context requirements."""
|
|
38
|
+
if not self.context:
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
if "requires_env" in self.context:
|
|
42
|
+
import os
|
|
43
|
+
|
|
44
|
+
required = self.context["requires_env"]
|
|
45
|
+
if isinstance(required, str):
|
|
46
|
+
required = [required]
|
|
47
|
+
for env_var in required:
|
|
48
|
+
if env_var not in os.environ:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
def init(self) -> int:
|
|
54
|
+
"""Initialize plugin integration files and show instructions.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Exit code: 0 for success, 1 for error
|
|
58
|
+
"""
|
|
59
|
+
if not self.integration:
|
|
60
|
+
print(
|
|
61
|
+
f"Plugin '{self.name}' has no integration configuration",
|
|
62
|
+
file=sys.stderr,
|
|
63
|
+
)
|
|
64
|
+
return 1
|
|
65
|
+
|
|
66
|
+
print(f"Initializing {self.name} integration...", file=sys.stderr)
|
|
67
|
+
|
|
68
|
+
for file_info in self.integration.get("files", []):
|
|
69
|
+
source = self.path.parent / file_info["source"]
|
|
70
|
+
target = Path(file_info["target"]).expanduser()
|
|
71
|
+
|
|
72
|
+
if not source.exists():
|
|
73
|
+
print(f"✗ Source file not found: {source}", file=sys.stderr)
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
try:
|
|
78
|
+
shutil.copy(source, target)
|
|
79
|
+
print(f"✓ Copied {file_info['source']} to {target}", file=sys.stderr)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
print(f"✗ Failed to copy {file_info['source']}: {e}", file=sys.stderr)
|
|
82
|
+
|
|
83
|
+
check_cmd = self.integration.get("check_command")
|
|
84
|
+
if check_cmd:
|
|
85
|
+
result = subprocess.run(check_cmd, shell=True, capture_output=True)
|
|
86
|
+
if result.returncode == 0:
|
|
87
|
+
print("✓ Already integrated", file=sys.stderr)
|
|
88
|
+
else:
|
|
89
|
+
print("✗ Not yet integrated", file=sys.stderr)
|
|
90
|
+
print("\nTo complete setup:", file=sys.stderr)
|
|
91
|
+
print(self.integration.get("instructions", ""), file=sys.stderr)
|
|
92
|
+
|
|
93
|
+
usage = self.integration.get("usage")
|
|
94
|
+
if usage:
|
|
95
|
+
print(f"\nUsage: {usage}", file=sys.stderr)
|
|
96
|
+
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
def __repr__(self):
|
|
100
|
+
"""Return string representation of plugin."""
|
|
101
|
+
return f"Plugin({self.name} v{self.version})"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Service Registry - Tracks plugin services and dependencies."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .plugin import Plugin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ServiceRegistry:
|
|
9
|
+
"""Tracks what services plugins provide and require."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, logger=None):
|
|
12
|
+
self.plugins: List[Plugin] = []
|
|
13
|
+
self.providers: Dict[str, List[Plugin]] = {}
|
|
14
|
+
self.verb_index: Dict[str, List[Plugin]] = {}
|
|
15
|
+
self.logger = logger
|
|
16
|
+
|
|
17
|
+
def load_plugins(self, plugin_dir: Path) -> None:
|
|
18
|
+
"""Load all plugins from directory and build indices."""
|
|
19
|
+
self.plugins = []
|
|
20
|
+
self.providers = {}
|
|
21
|
+
self.verb_index = {}
|
|
22
|
+
|
|
23
|
+
for yaml_path in plugin_dir.glob("*/plugin.yaml"):
|
|
24
|
+
try:
|
|
25
|
+
plugin = Plugin(yaml_path)
|
|
26
|
+
|
|
27
|
+
if plugin.is_active():
|
|
28
|
+
self.register(plugin)
|
|
29
|
+
else:
|
|
30
|
+
if self.logger:
|
|
31
|
+
self.logger.debug(f"Skipped (inactive): {plugin.name}")
|
|
32
|
+
|
|
33
|
+
except Exception as e:
|
|
34
|
+
if self.logger:
|
|
35
|
+
self.logger.log_plugin_load(yaml_path.parent.name, False, str(e))
|
|
36
|
+
|
|
37
|
+
def register(self, plugin: Plugin) -> None:
|
|
38
|
+
"""Register plugin and build service and verb indices."""
|
|
39
|
+
self.plugins.append(plugin)
|
|
40
|
+
|
|
41
|
+
if isinstance(plugin.provides, dict):
|
|
42
|
+
for service_name in plugin.provides:
|
|
43
|
+
if service_name not in self.providers:
|
|
44
|
+
self.providers[service_name] = []
|
|
45
|
+
self.providers[service_name].append(plugin)
|
|
46
|
+
elif isinstance(plugin.provides, list):
|
|
47
|
+
for service_name in plugin.provides:
|
|
48
|
+
if service_name not in self.providers:
|
|
49
|
+
self.providers[service_name] = []
|
|
50
|
+
self.providers[service_name].append(plugin)
|
|
51
|
+
|
|
52
|
+
for verb in plugin.verbs:
|
|
53
|
+
if verb not in self.verb_index:
|
|
54
|
+
self.verb_index[verb] = []
|
|
55
|
+
self.verb_index[verb].append(plugin)
|
|
56
|
+
|
|
57
|
+
if self.logger:
|
|
58
|
+
self.logger.log_plugin_load(plugin.name, True)
|
|
59
|
+
|
|
60
|
+
def get_plugins_for_verb(self, verb: str) -> List[Plugin]:
|
|
61
|
+
"""Get all plugins that handle the given verb."""
|
|
62
|
+
return self.verb_index.get(verb.upper(), [])
|