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 ADDED
@@ -0,0 +1,2 @@
1
+ __version__ = "0.1.0"
2
+ __app_name__ = "sharkit"
sharkit/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from sharkit.app import main
4
+
5
+ sys.exit(main())
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
+ ]
@@ -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