sharkit 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.
- sharkit/__init__.py +2 -0
- sharkit/__main__.py +5 -0
- sharkit/app.py +80 -0
- sharkit/commands/__init__.py +10 -0
- sharkit/commands/base.py +43 -0
- sharkit/commands/builtins.py +493 -0
- sharkit/commands/registry.py +32 -0
- sharkit/config/__init__.py +0 -0
- sharkit/config/manager.py +77 -0
- sharkit/config/paths.py +34 -0
- sharkit/console/__init__.py +3 -0
- sharkit/console/completion.py +41 -0
- sharkit/console/prompt.py +25 -0
- sharkit/console/repl.py +181 -0
- sharkit/exceptions.py +54 -0
- sharkit/history/__init__.py +0 -0
- sharkit/history/manager.py +83 -0
- sharkit/modules/__init__.py +13 -0
- sharkit/modules/base.py +55 -0
- sharkit/modules/loader.py +58 -0
- sharkit/modules/registry.py +61 -0
- sharkit/modules/testing/__init__.py +0 -0
- sharkit/modules/testing/demo/__init__.py +0 -0
- sharkit/modules/testing/demo/echo.py +92 -0
- sharkit/modules/testing/http/__init__.py +0 -0
- sharkit/modules/testing/http/metadata.py +83 -0
- sharkit/network/__init__.py +3 -0
- sharkit/network/client.py +81 -0
- sharkit/output/__init__.py +3 -0
- sharkit/output/renderer.py +249 -0
- sharkit/output/theme.py +9 -0
- sharkit-0.1.0.dist-info/METADATA +68 -0
- sharkit-0.1.0.dist-info/RECORD +36 -0
- sharkit-0.1.0.dist-info/WHEEL +4 -0
- sharkit-0.1.0.dist-info/entry_points.txt +2 -0
- sharkit-0.1.0.dist-info/licenses/LICENSE +21 -0
sharkit/__init__.py
ADDED
sharkit/__main__.py
ADDED
sharkit/app.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from sharkit.commands.builtins import BUILTIN_COMMANDS
|
|
7
|
+
from sharkit.commands.registry import CommandRegistry
|
|
8
|
+
from sharkit.config.manager import ConfigManager
|
|
9
|
+
from sharkit.console.repl import Console
|
|
10
|
+
from sharkit.exceptions import SharkitError
|
|
11
|
+
from sharkit.history.manager import HistoryManager
|
|
12
|
+
from sharkit.modules.loader import discover_modules
|
|
13
|
+
from sharkit.modules.registry import ModuleRegistry
|
|
14
|
+
from sharkit.network.client import HttpClient
|
|
15
|
+
from sharkit.output.renderer import Renderer
|
|
16
|
+
|
|
17
|
+
NON_LINUX_MESSAGE = (
|
|
18
|
+
"sharkit is a shark.\n"
|
|
19
|
+
"it was built for open water,\n"
|
|
20
|
+
"where it can actually swim.\n"
|
|
21
|
+
"\n"
|
|
22
|
+
"windows/mac is more of a glass aquarium.\n"
|
|
23
|
+
"tidy, bright, and a little too enclosed.\n"
|
|
24
|
+
"\n"
|
|
25
|
+
"the shark doesn't dislike the aquarium.\n"
|
|
26
|
+
"it just wasn't shaped for it,\n"
|
|
27
|
+
"and it shows.\n"
|
|
28
|
+
"\n"
|
|
29
|
+
"find us in linux.\n"
|
|
30
|
+
"mrrp :3"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> int:
|
|
35
|
+
sys.stdout.write("\033[2J\033[3J\033[H")
|
|
36
|
+
sys.stdout.flush()
|
|
37
|
+
renderer = Renderer()
|
|
38
|
+
|
|
39
|
+
if sys.platform != "linux":
|
|
40
|
+
renderer.banner(module_count=0, message=NON_LINUX_MESSAGE)
|
|
41
|
+
return 1
|
|
42
|
+
|
|
43
|
+
config_manager = ConfigManager()
|
|
44
|
+
|
|
45
|
+
history_manager = HistoryManager()
|
|
46
|
+
|
|
47
|
+
module_registry = ModuleRegistry()
|
|
48
|
+
modules_dir = Path(__file__).parent / "modules"
|
|
49
|
+
discovered = discover_modules(modules_dir)
|
|
50
|
+
for module_class, module_path in discovered:
|
|
51
|
+
module_registry.register_module(module_class, module_path)
|
|
52
|
+
|
|
53
|
+
command_registry = CommandRegistry()
|
|
54
|
+
for command_class in BUILTIN_COMMANDS:
|
|
55
|
+
command_registry.register_command(command_class)
|
|
56
|
+
|
|
57
|
+
http_client = HttpClient()
|
|
58
|
+
|
|
59
|
+
renderer.banner(module_count=len(discovered))
|
|
60
|
+
print()
|
|
61
|
+
|
|
62
|
+
console = Console(
|
|
63
|
+
command_registry=command_registry,
|
|
64
|
+
module_registry=module_registry,
|
|
65
|
+
config_manager=config_manager,
|
|
66
|
+
history_manager=history_manager,
|
|
67
|
+
renderer=renderer,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
console.run()
|
|
72
|
+
except KeyboardInterrupt:
|
|
73
|
+
print()
|
|
74
|
+
except SharkitError as exc:
|
|
75
|
+
renderer.error(str(exc))
|
|
76
|
+
return 1
|
|
77
|
+
finally:
|
|
78
|
+
http_client.close()
|
|
79
|
+
|
|
80
|
+
return 0
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from sharkit.commands.base import Command, CommandContext
|
|
2
|
+
from sharkit.commands.builtins import BUILTIN_COMMANDS
|
|
3
|
+
from sharkit.commands.registry import CommandRegistry
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"Command",
|
|
7
|
+
"CommandContext",
|
|
8
|
+
"CommandRegistry",
|
|
9
|
+
"BUILTIN_COMMANDS",
|
|
10
|
+
]
|
sharkit/commands/base.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class CommandContext:
|
|
10
|
+
session: dict[str, Any] = field(default_factory=dict)
|
|
11
|
+
modules: list[Any] = field(default_factory=list)
|
|
12
|
+
current_module: str | None = None
|
|
13
|
+
output: Any = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Command(ABC):
|
|
17
|
+
name: str
|
|
18
|
+
aliases: list[str] = []
|
|
19
|
+
description: str = ""
|
|
20
|
+
usage: str = ""
|
|
21
|
+
options: list[tuple[str, str]] = []
|
|
22
|
+
notes: str = ""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None: ...
|
|
26
|
+
|
|
27
|
+
def validate_args(self, args: list[str]) -> tuple[bool, str | None]:
|
|
28
|
+
return True, None
|
|
29
|
+
|
|
30
|
+
def format_help(self) -> str:
|
|
31
|
+
usage = self.usage or self.name
|
|
32
|
+
lines = [f"Usage: {usage}", "", self.description]
|
|
33
|
+
if self.notes:
|
|
34
|
+
lines.append("")
|
|
35
|
+
lines.append(self.notes)
|
|
36
|
+
if self.options:
|
|
37
|
+
lines.append("")
|
|
38
|
+
lines.append("OPTIONS:")
|
|
39
|
+
lines.append("")
|
|
40
|
+
max_flag = max((len(f) for f, _ in self.options), default=0)
|
|
41
|
+
for flag, desc in self.options:
|
|
42
|
+
lines.append(f" {flag:<{max_flag}} {desc}")
|
|
43
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from sharkit import __version__
|
|
6
|
+
from sharkit.commands.base import Command, CommandContext
|
|
7
|
+
from sharkit.modules.base import Module
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_module_instance(context: CommandContext) -> Module | None:
|
|
11
|
+
module_name = context.current_module
|
|
12
|
+
if module_name is None:
|
|
13
|
+
return None
|
|
14
|
+
module_registry = context.session.get("module_registry")
|
|
15
|
+
if module_registry is None:
|
|
16
|
+
return None
|
|
17
|
+
module_cls = module_registry.get_module(module_name)
|
|
18
|
+
if module_cls is None:
|
|
19
|
+
return None
|
|
20
|
+
instances: dict[str, Module] = context.session.setdefault("module_instances", {})
|
|
21
|
+
if module_name not in instances:
|
|
22
|
+
instances[module_name] = module_cls()
|
|
23
|
+
return instances[module_name]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HelpCommand(Command):
|
|
27
|
+
name = "help"
|
|
28
|
+
aliases = ["h", "?"]
|
|
29
|
+
description = "Show available commands or help for a specific command"
|
|
30
|
+
usage = "help [command]"
|
|
31
|
+
|
|
32
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
33
|
+
if args:
|
|
34
|
+
return self._show_command_help(context, args[0])
|
|
35
|
+
return self._show_all_commands(context)
|
|
36
|
+
|
|
37
|
+
def _show_all_commands(self, context: CommandContext) -> str | None:
|
|
38
|
+
registry = context.session.get("command_registry")
|
|
39
|
+
if registry is None:
|
|
40
|
+
return "No command registry available."
|
|
41
|
+
renderer = context.session.get("renderer")
|
|
42
|
+
if renderer is None:
|
|
43
|
+
return "No command registry available."
|
|
44
|
+
rows = []
|
|
45
|
+
for cmd_class in registry.get_all_commands():
|
|
46
|
+
aliases = ", ".join(cmd_class.aliases)
|
|
47
|
+
rows.append([cmd_class.name, cmd_class.description, aliases])
|
|
48
|
+
renderer.table("help", ["Command", "Description", "Aliases"], rows)
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def _show_command_help(self, context: CommandContext, name: str) -> str | None:
|
|
52
|
+
registry = context.session.get("command_registry")
|
|
53
|
+
renderer = context.session.get("renderer")
|
|
54
|
+
if registry is None or renderer is None:
|
|
55
|
+
return "No command registry available."
|
|
56
|
+
try:
|
|
57
|
+
cmd_class = registry.get_command(name)
|
|
58
|
+
except Exception:
|
|
59
|
+
return f'Command "{name}" not found.'
|
|
60
|
+
if cmd_class is None:
|
|
61
|
+
return f'Command "{name}" not found.'
|
|
62
|
+
renderer.panel(cmd_class.name, cmd_class().format_help())
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class BannerCommand(Command):
|
|
67
|
+
name = "banner"
|
|
68
|
+
aliases = ["logo"]
|
|
69
|
+
description = "Show sharkit banner"
|
|
70
|
+
usage = "banner"
|
|
71
|
+
|
|
72
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
73
|
+
renderer = context.session.get("renderer")
|
|
74
|
+
if renderer is None:
|
|
75
|
+
return f"sharkit v{__version__}"
|
|
76
|
+
module_registry = context.session.get("module_registry")
|
|
77
|
+
module_count = len(module_registry.get_all_modules()) if module_registry else 0
|
|
78
|
+
renderer.banner(module_count=module_count)
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class VersionCommand(Command):
|
|
83
|
+
name = "version"
|
|
84
|
+
aliases = ["ver"]
|
|
85
|
+
description = "Show sharkit version"
|
|
86
|
+
usage = "version"
|
|
87
|
+
|
|
88
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
89
|
+
return f"sharkit v{__version__}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class StatusCommand(Command):
|
|
93
|
+
name = "status"
|
|
94
|
+
aliases = []
|
|
95
|
+
description = "Show framework status"
|
|
96
|
+
usage = "status"
|
|
97
|
+
|
|
98
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
99
|
+
module_registry = context.session.get("module_registry")
|
|
100
|
+
module_count = len(module_registry.get_all_modules()) if module_registry else 0
|
|
101
|
+
current = context.current_module or "none"
|
|
102
|
+
lines = [
|
|
103
|
+
f"modules {module_count}",
|
|
104
|
+
f"current {current}",
|
|
105
|
+
"status ready",
|
|
106
|
+
]
|
|
107
|
+
return "\n".join(lines)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class UseCommand(Command):
|
|
111
|
+
name = "use"
|
|
112
|
+
aliases = []
|
|
113
|
+
description = "Select a module"
|
|
114
|
+
usage = "use <module_path>"
|
|
115
|
+
|
|
116
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
117
|
+
valid, error = self.validate_args(args)
|
|
118
|
+
if not valid:
|
|
119
|
+
return error
|
|
120
|
+
module_path = args[0]
|
|
121
|
+
module_registry = context.session.get("module_registry")
|
|
122
|
+
if module_registry is not None and module_registry.get_module(module_path) is None:
|
|
123
|
+
return f'Module "{module_path}" not found.\nTry: search {module_path}'
|
|
124
|
+
context.current_module = module_path
|
|
125
|
+
return f'Module "{module_path}" selected.'
|
|
126
|
+
|
|
127
|
+
def validate_args(self, args: list[str]) -> tuple[bool, str | None]:
|
|
128
|
+
if not args:
|
|
129
|
+
return False, f"Usage: {self.usage}"
|
|
130
|
+
return True, None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class BackCommand(Command):
|
|
134
|
+
name = "back"
|
|
135
|
+
aliases = []
|
|
136
|
+
description = "Deselect current module"
|
|
137
|
+
usage = "back"
|
|
138
|
+
|
|
139
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
140
|
+
if context.current_module is None:
|
|
141
|
+
return "No module is currently selected."
|
|
142
|
+
previous = context.current_module
|
|
143
|
+
context.current_module = None
|
|
144
|
+
return f'Deselected module "{previous}".'
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class InfoCommand(Command):
|
|
148
|
+
name = "info"
|
|
149
|
+
aliases = []
|
|
150
|
+
description = "Show info about current module"
|
|
151
|
+
usage = "info"
|
|
152
|
+
|
|
153
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
154
|
+
if context.current_module is None:
|
|
155
|
+
return "No module selected. Use 'use <module>' first."
|
|
156
|
+
module_registry = context.session.get("module_registry")
|
|
157
|
+
if module_registry is None:
|
|
158
|
+
return f"Module: {context.current_module}"
|
|
159
|
+
try:
|
|
160
|
+
module_cls = module_registry.get_module(context.current_module)
|
|
161
|
+
metadata = module_cls.metadata
|
|
162
|
+
lines = [
|
|
163
|
+
f"description {metadata.description}",
|
|
164
|
+
f"category {metadata.category}",
|
|
165
|
+
f"author {metadata.author}",
|
|
166
|
+
f"version {metadata.version}",
|
|
167
|
+
f"safety {metadata.safety}",
|
|
168
|
+
]
|
|
169
|
+
return "\n".join(lines)
|
|
170
|
+
except Exception:
|
|
171
|
+
return f"Module: {context.current_module}"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class ShowCommand(Command):
|
|
175
|
+
name = "show"
|
|
176
|
+
aliases = []
|
|
177
|
+
description = "Show tools or module options"
|
|
178
|
+
usage = "show <tools|options> [module]"
|
|
179
|
+
|
|
180
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
181
|
+
if not args:
|
|
182
|
+
if context.current_module is not None:
|
|
183
|
+
return self._show_options(context, None)
|
|
184
|
+
return self._show_tools(context)
|
|
185
|
+
if args[0] == "options":
|
|
186
|
+
return self._show_options(context, args[1] if len(args) > 1 else None)
|
|
187
|
+
if args[0] == "tools":
|
|
188
|
+
return self._show_tools(context)
|
|
189
|
+
return f"Unknown show subcommand: {args[0]}\nUsage: {self.usage}"
|
|
190
|
+
|
|
191
|
+
def _show_tools(self, context: CommandContext) -> str | None:
|
|
192
|
+
module_registry = context.session.get("module_registry")
|
|
193
|
+
renderer = context.session.get("renderer")
|
|
194
|
+
if module_registry is None or renderer is None:
|
|
195
|
+
return "Module registry not available."
|
|
196
|
+
all_modules = module_registry.get_all_modules()
|
|
197
|
+
if not all_modules:
|
|
198
|
+
return "No modules available."
|
|
199
|
+
rows = [
|
|
200
|
+
[module_path, module_cls.metadata.description]
|
|
201
|
+
for module_path, module_cls in all_modules.items()
|
|
202
|
+
]
|
|
203
|
+
renderer.table("show > tools", ["Module", "Description"], rows)
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
def _show_options(self, context: CommandContext, module_arg: str | None) -> str | None:
|
|
207
|
+
module_registry = context.session.get("module_registry")
|
|
208
|
+
renderer = context.session.get("renderer")
|
|
209
|
+
if module_registry is None or renderer is None:
|
|
210
|
+
return "Module registry not available."
|
|
211
|
+
if module_arg is not None:
|
|
212
|
+
module_cls = module_registry.get_module(module_arg)
|
|
213
|
+
if module_cls is None:
|
|
214
|
+
return f'Module "{module_arg}" not found.\nTry: show tools'
|
|
215
|
+
instance = module_cls()
|
|
216
|
+
title_module = module_cls.metadata.name
|
|
217
|
+
else:
|
|
218
|
+
if context.current_module is None:
|
|
219
|
+
return "No module selected. Use 'show tools' or 'use <module>' first."
|
|
220
|
+
module_cls = module_registry.get_module(context.current_module)
|
|
221
|
+
if module_cls is None:
|
|
222
|
+
return "Current module not found."
|
|
223
|
+
instance = _get_module_instance(context)
|
|
224
|
+
if instance is None:
|
|
225
|
+
return "Module registry not available."
|
|
226
|
+
title_module = module_cls.metadata.name
|
|
227
|
+
try:
|
|
228
|
+
options = instance.get_options()
|
|
229
|
+
if not options:
|
|
230
|
+
return "This module has no options."
|
|
231
|
+
rows = []
|
|
232
|
+
for opt in options.values():
|
|
233
|
+
value = opt.value or opt.default or "not set"
|
|
234
|
+
required = " (required)" if opt.required else ""
|
|
235
|
+
rows.append([opt.name, value, f"{opt.description}{required}"])
|
|
236
|
+
renderer.table(
|
|
237
|
+
f"show > options > {title_module}",
|
|
238
|
+
["Name", "Value", "Description"],
|
|
239
|
+
rows,
|
|
240
|
+
)
|
|
241
|
+
return None
|
|
242
|
+
except Exception:
|
|
243
|
+
return "Could not retrieve options for module."
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class SetCommand(Command):
|
|
247
|
+
name = "set"
|
|
248
|
+
aliases = []
|
|
249
|
+
description = "Set option value for current module"
|
|
250
|
+
usage = "set <option> <value>"
|
|
251
|
+
|
|
252
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
253
|
+
valid, error = self.validate_args(args)
|
|
254
|
+
if not valid:
|
|
255
|
+
return error
|
|
256
|
+
if context.current_module is None:
|
|
257
|
+
return "No module selected. Use 'use <module>' first."
|
|
258
|
+
module_instance = _get_module_instance(context)
|
|
259
|
+
if module_instance is None:
|
|
260
|
+
return "Module registry not available."
|
|
261
|
+
try:
|
|
262
|
+
options = module_instance.get_options()
|
|
263
|
+
key = args[0]
|
|
264
|
+
if key not in options:
|
|
265
|
+
available = ", ".join(options.keys())
|
|
266
|
+
return f'Option "{key}" not found.\nAvailable options: {available}'
|
|
267
|
+
module_instance.set_option(key, " ".join(args[1:]))
|
|
268
|
+
return f"Set {key} = {' '.join(args[1:])}"
|
|
269
|
+
except Exception as e:
|
|
270
|
+
return f"Failed to set option: {e}"
|
|
271
|
+
|
|
272
|
+
def validate_args(self, args: list[str]) -> tuple[bool, str | None]:
|
|
273
|
+
if len(args) < 2:
|
|
274
|
+
return False, f"Usage: {self.usage}"
|
|
275
|
+
return True, None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class UnsetCommand(Command):
|
|
279
|
+
name = "unset"
|
|
280
|
+
aliases = []
|
|
281
|
+
description = "Unset option value"
|
|
282
|
+
usage = "unset <option>"
|
|
283
|
+
|
|
284
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
285
|
+
valid, error = self.validate_args(args)
|
|
286
|
+
if not valid:
|
|
287
|
+
return error
|
|
288
|
+
if context.current_module is None:
|
|
289
|
+
return "No module selected. Use 'use <module>' first."
|
|
290
|
+
module_instance = _get_module_instance(context)
|
|
291
|
+
if module_instance is None:
|
|
292
|
+
return "Module registry not available."
|
|
293
|
+
try:
|
|
294
|
+
options = module_instance.get_options()
|
|
295
|
+
key = args[0]
|
|
296
|
+
if key not in options:
|
|
297
|
+
available = ", ".join(options.keys())
|
|
298
|
+
return f'Option "{key}" not found.\nAvailable options: {available}'
|
|
299
|
+
module_instance.set_option(key, options[key].default or "")
|
|
300
|
+
return f"Unset {key}."
|
|
301
|
+
except Exception as e:
|
|
302
|
+
return f"Failed to unset option: {e}"
|
|
303
|
+
|
|
304
|
+
def validate_args(self, args: list[str]) -> tuple[bool, str | None]:
|
|
305
|
+
if not args:
|
|
306
|
+
return False, f"Usage: {self.usage}"
|
|
307
|
+
return True, None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class RunCommand(Command):
|
|
311
|
+
name = "run"
|
|
312
|
+
aliases = ["sharkit"]
|
|
313
|
+
description = "Execute current module"
|
|
314
|
+
usage = "run"
|
|
315
|
+
|
|
316
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
317
|
+
if context.current_module is None:
|
|
318
|
+
return "No module selected. Use 'use <module>' first."
|
|
319
|
+
module_instance = _get_module_instance(context)
|
|
320
|
+
if module_instance is None:
|
|
321
|
+
return "Module registry not available."
|
|
322
|
+
try:
|
|
323
|
+
options = module_instance.get_options()
|
|
324
|
+
option_values = {k: v.value or v.default or "" for k, v in options.items()}
|
|
325
|
+
config_dir = context.session.get("config_dir") or Path("/tmp")
|
|
326
|
+
from sharkit.modules.base import ExecutionContext
|
|
327
|
+
|
|
328
|
+
exec_context = ExecutionContext(
|
|
329
|
+
module_name=context.current_module,
|
|
330
|
+
options=option_values,
|
|
331
|
+
config_dir=config_dir,
|
|
332
|
+
)
|
|
333
|
+
result = module_instance.execute(exec_context)
|
|
334
|
+
renderer = context.session.get("renderer")
|
|
335
|
+
if result.success:
|
|
336
|
+
if renderer is not None:
|
|
337
|
+
renderer.result(result.data)
|
|
338
|
+
return None
|
|
339
|
+
lines = [f"{key}: {value}" for key, value in result.data.items()]
|
|
340
|
+
return "\n".join(lines) if lines else "Module executed successfully."
|
|
341
|
+
if renderer is not None:
|
|
342
|
+
renderer.error(f"Module execution failed: {result.error}")
|
|
343
|
+
return None
|
|
344
|
+
return f"Module execution failed: {result.error}"
|
|
345
|
+
except Exception as e:
|
|
346
|
+
return f"Module execution error: {e}"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class SearchCommand(Command):
|
|
350
|
+
name = "search"
|
|
351
|
+
aliases = ["find"]
|
|
352
|
+
description = "Search modules by query"
|
|
353
|
+
usage = "search <query>"
|
|
354
|
+
|
|
355
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
356
|
+
valid, error = self.validate_args(args)
|
|
357
|
+
if not valid:
|
|
358
|
+
return error
|
|
359
|
+
query = args[0].lower()
|
|
360
|
+
module_registry = context.session.get("module_registry")
|
|
361
|
+
if module_registry is None:
|
|
362
|
+
return "Module registry not available."
|
|
363
|
+
try:
|
|
364
|
+
all_modules = module_registry.get_all_modules()
|
|
365
|
+
matches = []
|
|
366
|
+
for module_path, module_cls in all_modules.items():
|
|
367
|
+
metadata = module_cls.metadata
|
|
368
|
+
searchable = f"{metadata.name} {metadata.description} {metadata.category}".lower()
|
|
369
|
+
if query in searchable or query in module_path.lower():
|
|
370
|
+
matches.append(f" {module_path:<30}{metadata.description}")
|
|
371
|
+
if not matches:
|
|
372
|
+
return f'No modules found matching "{query}".'
|
|
373
|
+
lines = [f"Modules matching \"{query}\":"] + matches
|
|
374
|
+
return "\n".join(lines)
|
|
375
|
+
except Exception as e:
|
|
376
|
+
return f"Search failed: {e}"
|
|
377
|
+
|
|
378
|
+
def validate_args(self, args: list[str]) -> tuple[bool, str | None]:
|
|
379
|
+
if not args:
|
|
380
|
+
return False, f"Usage: {self.usage}"
|
|
381
|
+
return True, None
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class HistoryCommand(Command):
|
|
385
|
+
name = "history"
|
|
386
|
+
aliases = []
|
|
387
|
+
description = "Show command history"
|
|
388
|
+
usage = "history [options]"
|
|
389
|
+
options = [
|
|
390
|
+
("-a, --all-commands", "Show all commands in history."),
|
|
391
|
+
("-c, --clear", "Clear command history and history file."),
|
|
392
|
+
("-h, --help", "Help banner."),
|
|
393
|
+
("-n <num>", "Show the last n commands."),
|
|
394
|
+
]
|
|
395
|
+
notes = (
|
|
396
|
+
"By default only the last 100 entries are shown. Use -a to dump them all.\n"
|
|
397
|
+
"-c wipes both the live history and the history file on disk. mrrp :3\n"
|
|
398
|
+
"Lead a command with a space and it stays out of the history."
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
402
|
+
clear = False
|
|
403
|
+
all_commands = False
|
|
404
|
+
limit = 100
|
|
405
|
+
i = 0
|
|
406
|
+
while i < len(args):
|
|
407
|
+
arg = args[i]
|
|
408
|
+
if arg in ("-c", "--clear"):
|
|
409
|
+
clear = True
|
|
410
|
+
elif arg in ("-a", "--all-commands"):
|
|
411
|
+
all_commands = True
|
|
412
|
+
elif arg in ("-n", "--last"):
|
|
413
|
+
i += 1
|
|
414
|
+
if i >= len(args):
|
|
415
|
+
return f"Error: {arg} requires a number."
|
|
416
|
+
try:
|
|
417
|
+
limit = int(args[i])
|
|
418
|
+
except ValueError:
|
|
419
|
+
return f"Error: invalid number for {arg}: {args[i]}"
|
|
420
|
+
elif arg in ("-h", "--help"):
|
|
421
|
+
return self.format_help()
|
|
422
|
+
else:
|
|
423
|
+
return f"Unknown option: {arg}\nUsage: {self.usage}"
|
|
424
|
+
i += 1
|
|
425
|
+
|
|
426
|
+
manager = context.session.get("history_manager")
|
|
427
|
+
if manager is None:
|
|
428
|
+
return "History manager not available."
|
|
429
|
+
|
|
430
|
+
if clear:
|
|
431
|
+
manager.clear()
|
|
432
|
+
context.session["history"] = []
|
|
433
|
+
return "[+] Command history and history file cleared"
|
|
434
|
+
|
|
435
|
+
all_entries = manager.get_history(10_000_000)
|
|
436
|
+
if all_commands:
|
|
437
|
+
limit = len(all_entries)
|
|
438
|
+
if limit < 0:
|
|
439
|
+
limit = 0
|
|
440
|
+
shown = all_entries[-limit:] if limit else all_entries
|
|
441
|
+
if not shown:
|
|
442
|
+
return "No command history."
|
|
443
|
+
start = len(all_entries) - len(shown) + 1
|
|
444
|
+
rows = [[str(start + idx), cmd] for idx, cmd in enumerate(shown)]
|
|
445
|
+
renderer = context.session.get("renderer")
|
|
446
|
+
if renderer is None:
|
|
447
|
+
return "Renderer not available."
|
|
448
|
+
renderer.table("history", ["#", "Command"], rows)
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
class ClearCommand(Command):
|
|
453
|
+
name = "clear"
|
|
454
|
+
aliases = ["cls"]
|
|
455
|
+
description = "Clear terminal"
|
|
456
|
+
usage = "clear"
|
|
457
|
+
|
|
458
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
459
|
+
renderer = context.session.get("renderer")
|
|
460
|
+
if renderer is not None:
|
|
461
|
+
renderer.raw("\033[2J\033[3J\033[H")
|
|
462
|
+
return None
|
|
463
|
+
return "\033[2J\033[3J\033[H"
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class ExitCommand(Command):
|
|
467
|
+
name = "exit"
|
|
468
|
+
aliases = ["quit"]
|
|
469
|
+
description = "Exit application"
|
|
470
|
+
usage = "exit"
|
|
471
|
+
|
|
472
|
+
def execute(self, context: CommandContext, args: list[str]) -> str | None:
|
|
473
|
+
context.session["exit_requested"] = True
|
|
474
|
+
return "Bye! :3"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
BUILTIN_COMMANDS: list[type[Command]] = [
|
|
478
|
+
HelpCommand,
|
|
479
|
+
BannerCommand,
|
|
480
|
+
VersionCommand,
|
|
481
|
+
StatusCommand,
|
|
482
|
+
UseCommand,
|
|
483
|
+
BackCommand,
|
|
484
|
+
InfoCommand,
|
|
485
|
+
ShowCommand,
|
|
486
|
+
SetCommand,
|
|
487
|
+
UnsetCommand,
|
|
488
|
+
RunCommand,
|
|
489
|
+
SearchCommand,
|
|
490
|
+
HistoryCommand,
|
|
491
|
+
ClearCommand,
|
|
492
|
+
ExitCommand,
|
|
493
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sharkit.commands.base import Command
|
|
4
|
+
from sharkit.exceptions import CommandNotFoundError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CommandRegistry:
|
|
8
|
+
def __init__(self) -> None:
|
|
9
|
+
self._commands: dict[str, type[Command]] = {}
|
|
10
|
+
self._aliases: dict[str, str] = {}
|
|
11
|
+
|
|
12
|
+
def register_command(self, command_class: type[Command]) -> None:
|
|
13
|
+
self._commands[command_class.name] = command_class
|
|
14
|
+
for alias in command_class.aliases:
|
|
15
|
+
self._aliases[alias] = command_class.name
|
|
16
|
+
|
|
17
|
+
def get_command(self, name: str) -> type[Command]:
|
|
18
|
+
canonical = self._aliases.get(name, name)
|
|
19
|
+
command = self._commands.get(canonical)
|
|
20
|
+
if command is None:
|
|
21
|
+
raise CommandNotFoundError(name)
|
|
22
|
+
return command
|
|
23
|
+
|
|
24
|
+
def get_all_commands(self) -> list[type[Command]]:
|
|
25
|
+
return list(self._commands.values())
|
|
26
|
+
|
|
27
|
+
def get_command_names(self) -> list[str]:
|
|
28
|
+
return sorted(self._commands.keys())
|
|
29
|
+
|
|
30
|
+
def get_completions(self, prefix: str) -> list[str]:
|
|
31
|
+
names = set(self._commands.keys()) | set(self._aliases.keys())
|
|
32
|
+
return sorted(name for name in names if name.startswith(prefix))
|
|
File without changes
|