sharkit 0.2.0.dev1__tar.gz → 0.2.1.dev1__tar.gz
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-0.2.0.dev1 → sharkit-0.2.1.dev1}/PKG-INFO +2 -2
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/README.md +1 -1
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/pyproject.toml +1 -1
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/app.py +2 -2
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/commands/builtins.py +40 -17
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/console/prompt.py +8 -3
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/console/repl.py +10 -3
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/output/renderer.py +23 -6
- {sharkit-0.2.0.dev1/src/sharkit/tools → sharkit-0.2.1.dev1/src/sharkit/tools/osint/recon/hybrid}/blackbird.py +16 -4
- sharkit-0.2.1.dev1/src/sharkit/tools/osint/recon/username/maigret.py +252 -0
- sharkit-0.2.1.dev1/src/sharkit/tools/osint/recon/username/sherlock.py +252 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/tools/registry.py +14 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/.gitignore +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/LICENSE +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/__main__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/commands/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/commands/base.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/commands/registry.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/config/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/config/manager.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/config/paths.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/console/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/console/completion.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/exceptions.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/history/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/history/manager.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/network/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/network/client.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/output/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/output/theme.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/tools/__init__.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/tools/base.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/tools/loader.py +0 -0
- {sharkit-0.2.0.dev1 → sharkit-0.2.1.dev1}/src/sharkit/tools/manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sharkit
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1.dev1
|
|
4
4
|
Summary: OSINT / HUMINT / GEOINT research framework
|
|
5
5
|
Project-URL: Homepage, https://github.com/sshsrv/sharkit
|
|
6
6
|
Project-URL: Repository, https://github.com/sshsrv/sharkit
|
|
@@ -27,7 +27,7 @@ Requires-Dist: prompt-toolkit<4.0.0,>=3.0.0
|
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
29
29
|
<div align="center"><img width="640" height="347" alt="banner" src="https://github.com/user-attachments/assets/c027fb38-fe0f-4baa-b35f-597f9f8cf39b" /></div>
|
|
30
|
-
|
|
30
|
+
<br>
|
|
31
31
|
<div align="center">
|
|
32
32
|
|
|
33
33
|

|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div align="center"><img width="640" height="347" alt="banner" src="https://github.com/user-attachments/assets/c027fb38-fe0f-4baa-b35f-597f9f8cf39b" /></div>
|
|
2
|
-
|
|
2
|
+
<br>
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
5
|

|
|
@@ -20,7 +20,7 @@ NON_LINUX_MESSAGE = (
|
|
|
20
20
|
"it was built for open water,\n"
|
|
21
21
|
"where it can actually swim.\n"
|
|
22
22
|
"\n"
|
|
23
|
-
"windows
|
|
23
|
+
"windows is more of a glass aquarium.\n"
|
|
24
24
|
"tidy, bright, and a little too enclosed.\n"
|
|
25
25
|
"\n"
|
|
26
26
|
"the shark doesn't dislike the aquarium.\n"
|
|
@@ -37,7 +37,7 @@ def main() -> int:
|
|
|
37
37
|
sys.stdout.flush()
|
|
38
38
|
renderer = Renderer()
|
|
39
39
|
|
|
40
|
-
if sys.platform
|
|
40
|
+
if sys.platform == "win32":
|
|
41
41
|
renderer.banner(tool_count=0, message=NON_LINUX_MESSAGE)
|
|
42
42
|
return 1
|
|
43
43
|
|
|
@@ -187,6 +187,7 @@ class UseCommand(Command):
|
|
|
187
187
|
if tool_cls is not None and tool_cls.metadata.install is not None:
|
|
188
188
|
spec = tool_cls.metadata.install
|
|
189
189
|
name = tool_cls.metadata.name
|
|
190
|
+
display = tool_registry.format_display(path) if tool_registry else name
|
|
190
191
|
if tool_manager is not None and not tool_manager.is_installed(name):
|
|
191
192
|
if not self._prompt_install(name):
|
|
192
193
|
self._log(context, f'Tool "{name}" is not installed. Run: install {name}')
|
|
@@ -195,7 +196,7 @@ class UseCommand(Command):
|
|
|
195
196
|
|
|
196
197
|
def on_progress(msg: str) -> None:
|
|
197
198
|
if renderer is not None:
|
|
198
|
-
renderer.log_line(f"install
|
|
199
|
+
renderer.log_line(f"install {display}", msg, color=GREEN)
|
|
199
200
|
|
|
200
201
|
ok, message = tool_manager.install(name, spec, on_progress=on_progress)
|
|
201
202
|
if not ok:
|
|
@@ -203,6 +204,15 @@ class UseCommand(Command):
|
|
|
203
204
|
return None
|
|
204
205
|
context.current_tool = path
|
|
205
206
|
self._log(context, f'Tool "{path}" selected.')
|
|
207
|
+
if tool_cls is not None and tool_cls.metadata.install is not None:
|
|
208
|
+
renderer = context.session.get("renderer")
|
|
209
|
+
if renderer is not None:
|
|
210
|
+
install_spec = tool_cls.metadata.install
|
|
211
|
+
renderer.support_creator(
|
|
212
|
+
tool_cls.metadata.author,
|
|
213
|
+
tool_cls.metadata.name,
|
|
214
|
+
install_spec.git_url,
|
|
215
|
+
)
|
|
206
216
|
return None
|
|
207
217
|
|
|
208
218
|
def _prompt_install(self, name: str) -> bool:
|
|
@@ -372,7 +382,13 @@ class SetCommand(Command):
|
|
|
372
382
|
tool_instance.set_option(key, value)
|
|
373
383
|
renderer = context.session.get("renderer")
|
|
374
384
|
if renderer is not None:
|
|
375
|
-
|
|
385
|
+
tool_registry = context.session.get("tool_registry")
|
|
386
|
+
display = context.current_tool
|
|
387
|
+
if tool_registry is not None and context.current_tool is not None:
|
|
388
|
+
display = tool_registry.format_display(
|
|
389
|
+
tool_registry.get_tool_path(context.current_tool),
|
|
390
|
+
)
|
|
391
|
+
renderer.log_line(f"set {display}", f"{key} = {value}")
|
|
376
392
|
return None
|
|
377
393
|
return f"Set {key} = {value}"
|
|
378
394
|
except Exception as e:
|
|
@@ -552,6 +568,9 @@ class HistoryCommand(Command):
|
|
|
552
568
|
if clear:
|
|
553
569
|
manager.clear()
|
|
554
570
|
context.session["history"] = []
|
|
571
|
+
sharkit_history = context.session.get("sharkit_history")
|
|
572
|
+
if sharkit_history is not None:
|
|
573
|
+
sharkit_history.clear()
|
|
555
574
|
return "[+] Command history and history file cleared"
|
|
556
575
|
|
|
557
576
|
all_entries = manager.get_history(10_000_000)
|
|
@@ -614,23 +633,24 @@ class InstallCommand(Command):
|
|
|
614
633
|
tool_cls = tool_registry.get_tool(name)
|
|
615
634
|
if tool_cls is None or tool_cls.metadata.install is None:
|
|
616
635
|
return f'Tool "{name}" is not an external tool or was not found.'
|
|
636
|
+
display = tool_registry.format_display(tool_registry.get_tool_path(name))
|
|
617
637
|
if tool_manager.is_installed(name):
|
|
618
638
|
renderer = context.session.get("renderer")
|
|
619
639
|
if renderer is not None:
|
|
620
|
-
renderer.log_line(f"install
|
|
640
|
+
renderer.log_line(f"install {display}", "already installed", color=GREEN)
|
|
621
641
|
return None
|
|
622
642
|
renderer = context.session.get("renderer")
|
|
623
643
|
|
|
624
644
|
def on_progress(msg: str) -> None:
|
|
625
645
|
if renderer is not None:
|
|
626
|
-
renderer.log_line(f"install
|
|
646
|
+
renderer.log_line(f"install {display}", msg, color=GREEN)
|
|
627
647
|
|
|
628
648
|
ok, message = tool_manager.install(name, tool_cls.metadata.install, on_progress=on_progress)
|
|
629
649
|
if renderer is not None:
|
|
630
650
|
if ok:
|
|
631
|
-
renderer.log_line(f"install
|
|
651
|
+
renderer.log_line(f"install {display}", "installed", color=GREEN)
|
|
632
652
|
else:
|
|
633
|
-
renderer.log_line(f"install
|
|
653
|
+
renderer.log_line(f"install {display}", f"failed: {message}", color=GREEN)
|
|
634
654
|
return None
|
|
635
655
|
return str(message)
|
|
636
656
|
|
|
@@ -657,23 +677,24 @@ class UpgradeCommand(Command):
|
|
|
657
677
|
tool_cls = tool_registry.get_tool(name)
|
|
658
678
|
if tool_cls is None or tool_cls.metadata.install is None:
|
|
659
679
|
return f'Tool "{name}" is not an external tool or was not found.'
|
|
680
|
+
display = tool_registry.format_display(tool_registry.get_tool_path(name))
|
|
660
681
|
if not tool_manager.is_installed(name):
|
|
661
682
|
renderer = context.session.get("renderer")
|
|
662
683
|
if renderer is not None:
|
|
663
|
-
renderer.log_line(f"upgrade
|
|
684
|
+
renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
|
|
664
685
|
return None
|
|
665
686
|
renderer = context.session.get("renderer")
|
|
666
687
|
|
|
667
688
|
def on_progress(msg: str) -> None:
|
|
668
689
|
if renderer is not None:
|
|
669
|
-
renderer.log_line(f"upgrade
|
|
690
|
+
renderer.log_line(f"upgrade {display}", msg, color=BLUE)
|
|
670
691
|
|
|
671
692
|
ok, message = tool_manager.update(name, tool_cls.metadata.install, on_progress=on_progress)
|
|
672
693
|
if renderer is not None:
|
|
673
694
|
if ok:
|
|
674
|
-
renderer.log_line(f"upgrade
|
|
695
|
+
renderer.log_line(f"upgrade {display}", "upgraded", color=BLUE)
|
|
675
696
|
else:
|
|
676
|
-
renderer.log_line(f"upgrade
|
|
697
|
+
renderer.log_line(f"upgrade {display}", f"failed: {message}", color=BLUE)
|
|
677
698
|
return None
|
|
678
699
|
return str(message)
|
|
679
700
|
|
|
@@ -699,23 +720,24 @@ class UpgradeCommand(Command):
|
|
|
699
720
|
if spec is None:
|
|
700
721
|
continue
|
|
701
722
|
name = meta.name
|
|
723
|
+
display = tool_registry.format_display(tool_registry.get_tool_path(name))
|
|
702
724
|
if not tool_manager.is_installed(name):
|
|
703
725
|
renderer = context.session.get("renderer")
|
|
704
726
|
if renderer is not None:
|
|
705
|
-
renderer.log_line(f"upgrade
|
|
727
|
+
renderer.log_line(f"upgrade {display}", "not installed (skipped)", color=BLUE)
|
|
706
728
|
continue
|
|
707
729
|
renderer = context.session.get("renderer")
|
|
708
730
|
|
|
709
|
-
def on_progress(msg: str,
|
|
731
|
+
def on_progress(msg: str, display: str = display, renderer: Any = renderer) -> None:
|
|
710
732
|
if renderer is not None:
|
|
711
|
-
renderer.log_line(f"upgrade
|
|
733
|
+
renderer.log_line(f"upgrade {display}", msg, color=BLUE)
|
|
712
734
|
|
|
713
735
|
ok, message = tool_manager.update(name, spec, on_progress=on_progress)
|
|
714
736
|
if renderer is not None:
|
|
715
737
|
if ok:
|
|
716
|
-
renderer.log_line(f"upgrade
|
|
738
|
+
renderer.log_line(f"upgrade {display}", "upgraded", color=BLUE)
|
|
717
739
|
else:
|
|
718
|
-
renderer.log_line(f"upgrade
|
|
740
|
+
renderer.log_line(f"upgrade {display}", f"failed: {message}", color=BLUE)
|
|
719
741
|
return None
|
|
720
742
|
|
|
721
743
|
|
|
@@ -737,13 +759,14 @@ class RemoveCommand(Command):
|
|
|
737
759
|
tool_cls = tool_registry.get_tool(name)
|
|
738
760
|
if tool_cls is None or tool_cls.metadata.install is None:
|
|
739
761
|
return f'Tool "{name}" is not an external tool or was not found.'
|
|
762
|
+
display = tool_registry.format_display(tool_registry.get_tool_path(name))
|
|
740
763
|
renderer = context.session.get("renderer")
|
|
741
764
|
ok, message = tool_manager.uninstall(name)
|
|
742
765
|
if renderer is not None:
|
|
743
766
|
if ok:
|
|
744
|
-
renderer.log_line(f"remove
|
|
767
|
+
renderer.log_line(f"remove {display}", "removed", color=RED)
|
|
745
768
|
else:
|
|
746
|
-
renderer.log_line(f"remove
|
|
769
|
+
renderer.log_line(f"remove {display}", f"failed: {message}", color=RED)
|
|
747
770
|
return None
|
|
748
771
|
return str(message)
|
|
749
772
|
|
|
@@ -9,16 +9,21 @@ class PromptRenderer:
|
|
|
9
9
|
return "shark > "
|
|
10
10
|
|
|
11
11
|
def tool_prompt(self, tool_name: str) -> str:
|
|
12
|
-
|
|
12
|
+
if "/" in tool_name:
|
|
13
|
+
parts = tool_name.split("/", 1)
|
|
14
|
+
formatted = f"{parts[0]}({parts[1]})"
|
|
15
|
+
else:
|
|
16
|
+
formatted = tool_name
|
|
17
|
+
return f"shark {formatted} > "
|
|
13
18
|
|
|
14
19
|
def render(self, prompt: str) -> list[tuple[str, str]]:
|
|
15
20
|
if "(" in prompt and ")" in prompt:
|
|
16
21
|
before, rest = prompt.split("(", 1)
|
|
17
|
-
|
|
22
|
+
tool_path, after = rest.split(")", 1)
|
|
18
23
|
return [
|
|
19
24
|
(PROMPT_STYLE, before),
|
|
20
25
|
("", "("),
|
|
21
|
-
(MODULE_STYLE,
|
|
26
|
+
(MODULE_STYLE, tool_path),
|
|
22
27
|
("", ")"),
|
|
23
28
|
("", after),
|
|
24
29
|
]
|
|
@@ -36,6 +36,11 @@ class _SharkitHistory(History):
|
|
|
36
36
|
def store_string(self, string: str) -> None:
|
|
37
37
|
self._manager.add_command(string)
|
|
38
38
|
|
|
39
|
+
def clear(self) -> None:
|
|
40
|
+
"""Reset the in-memory buffer so next load() re-reads from file."""
|
|
41
|
+
self._loaded = False
|
|
42
|
+
self._loaded_strings = []
|
|
43
|
+
|
|
39
44
|
|
|
40
45
|
class _ConsoleCompleter(Completer):
|
|
41
46
|
def __init__(
|
|
@@ -100,9 +105,9 @@ class Console:
|
|
|
100
105
|
|
|
101
106
|
def _get_prompt(self) -> Any:
|
|
102
107
|
if self._current_tool is not None:
|
|
103
|
-
prompt =
|
|
108
|
+
prompt = self._prompt_renderer.tool_prompt(self._current_tool)
|
|
104
109
|
else:
|
|
105
|
-
prompt =
|
|
110
|
+
prompt = self._prompt_renderer.default_prompt()
|
|
106
111
|
return self._prompt_renderer.render(prompt)
|
|
107
112
|
|
|
108
113
|
def _init_session(self) -> None:
|
|
@@ -113,12 +118,14 @@ class Console:
|
|
|
113
118
|
tool_registry=self._tool_registry,
|
|
114
119
|
get_current_tool=lambda: self._current_tool,
|
|
115
120
|
)
|
|
121
|
+
self._sharkit_history = _SharkitHistory(self._history_manager)
|
|
116
122
|
self._prompt_session = PromptSession[str](
|
|
117
|
-
history=
|
|
123
|
+
history=self._sharkit_history,
|
|
118
124
|
completer=self._completer,
|
|
119
125
|
style=_PROMPT_STYLE,
|
|
120
126
|
complete_while_typing=True,
|
|
121
127
|
)
|
|
128
|
+
self._session_data["sharkit_history"] = self._sharkit_history
|
|
122
129
|
|
|
123
130
|
def run(self) -> None:
|
|
124
131
|
self._init_session()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import shutil
|
|
3
3
|
import textwrap
|
|
4
|
+
import unicodedata
|
|
4
5
|
|
|
5
6
|
from sharkit import __version__
|
|
6
7
|
from sharkit.output.theme import (
|
|
@@ -32,7 +33,8 @@ x88: `)8b. 888E~?888L .@88 "8888" 4888>'88" 888E`"88*" ''888E` 8888
|
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
def _visible_len(text: str) -> int:
|
|
35
|
-
|
|
36
|
+
clean = ANSI_RE.sub("", text)
|
|
37
|
+
return sum(2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1 for ch in clean)
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
def _pad(text: str, width: int) -> str:
|
|
@@ -55,7 +57,7 @@ def _term_width() -> int:
|
|
|
55
57
|
class Renderer:
|
|
56
58
|
def panel(
|
|
57
59
|
self,
|
|
58
|
-
title: str,
|
|
60
|
+
title: str | None,
|
|
59
61
|
content: str,
|
|
60
62
|
min_width: int = 0,
|
|
61
63
|
centered: bool = False,
|
|
@@ -73,11 +75,12 @@ class Renderer:
|
|
|
73
75
|
else:
|
|
74
76
|
lines = content.splitlines()
|
|
75
77
|
content_width = max((_visible_len(line) for line in lines), default=0)
|
|
76
|
-
title_width = _visible_len(title)
|
|
78
|
+
title_width = _visible_len(title) if title else 0
|
|
77
79
|
inner = max(content_width, title_width + 3, min_width)
|
|
78
|
-
box_width = inner + 4
|
|
79
80
|
|
|
80
|
-
if
|
|
81
|
+
if title is None:
|
|
82
|
+
top = f"{PINK}╭{'─' * (inner + 2)}╮{RESET}"
|
|
83
|
+
elif centered:
|
|
81
84
|
gap = inner - title_width
|
|
82
85
|
left = gap // 2
|
|
83
86
|
right = gap - left
|
|
@@ -88,6 +91,7 @@ class Renderer:
|
|
|
88
91
|
else:
|
|
89
92
|
dashes = max(0, inner - title_width - 1)
|
|
90
93
|
top = f"{PINK}╭─ {BOLD}{WHITE}{title}{RESET}{PINK} {'─' * dashes}╮{RESET}"
|
|
94
|
+
|
|
91
95
|
if centered:
|
|
92
96
|
body = [f"{PINK}│{RESET} {_center(line, inner)} {PINK}│{RESET}" for line in lines]
|
|
93
97
|
else:
|
|
@@ -96,7 +100,7 @@ class Renderer:
|
|
|
96
100
|
|
|
97
101
|
all_lines = [top, *body, bottom]
|
|
98
102
|
if centered:
|
|
99
|
-
pad = max(0, (_term_width() -
|
|
103
|
+
pad = max(0, (_term_width() - (inner + 4)) // 2)
|
|
100
104
|
all_lines = [" " * pad + ln for ln in all_lines]
|
|
101
105
|
for ln in all_lines:
|
|
102
106
|
print(ln)
|
|
@@ -257,3 +261,16 @@ class Renderer:
|
|
|
257
261
|
else:
|
|
258
262
|
prefix = f"{RESET}{' ' * (len(name) + 1)}{color}{BOLD}│{RESET} "
|
|
259
263
|
print(prefix + line)
|
|
264
|
+
|
|
265
|
+
def support_creator(self, author: str, tool_name: str, git_url: str) -> None:
|
|
266
|
+
lines = [
|
|
267
|
+
f"Thanks to {author} for building {tool_name}!",
|
|
268
|
+
"If you find this tool useful, please consider:",
|
|
269
|
+
"",
|
|
270
|
+
f"⭐ Star the repo: {git_url}",
|
|
271
|
+
f"🐛 Report issues: {git_url}/issues",
|
|
272
|
+
f"💰 Sponsor the author: {git_url}/sponsors",
|
|
273
|
+
"",
|
|
274
|
+
f"{DIM}Without creators like them, sharkit wouldn't exist! :3{RESET}",
|
|
275
|
+
]
|
|
276
|
+
self.panel(None, "\n".join(lines))
|
|
@@ -144,6 +144,7 @@ class BlackbirdTool(Tool):
|
|
|
144
144
|
first = True
|
|
145
145
|
current = ""
|
|
146
146
|
started = False
|
|
147
|
+
pending_blank = False
|
|
147
148
|
try:
|
|
148
149
|
while True:
|
|
149
150
|
try:
|
|
@@ -165,11 +166,20 @@ class BlackbirdTool(Tool):
|
|
|
165
166
|
started = True
|
|
166
167
|
for ch in chunk.decode("utf-8", errors="replace"):
|
|
167
168
|
if ch == "\n":
|
|
168
|
-
if
|
|
169
|
-
|
|
169
|
+
if current:
|
|
170
|
+
if pending_blank:
|
|
171
|
+
if renderer is not None:
|
|
172
|
+
renderer.gutter(name, "", tool_color, False)
|
|
173
|
+
else:
|
|
174
|
+
print()
|
|
175
|
+
pending_blank = False
|
|
176
|
+
if renderer is not None:
|
|
177
|
+
renderer.gutter(name, _clean_ansi(current), tool_color, first)
|
|
178
|
+
else:
|
|
179
|
+
print(_clean_ansi(current))
|
|
180
|
+
first = False
|
|
170
181
|
else:
|
|
171
|
-
|
|
172
|
-
first = False
|
|
182
|
+
pending_blank = True
|
|
173
183
|
current = ""
|
|
174
184
|
elif ch == "\r":
|
|
175
185
|
current = ""
|
|
@@ -181,6 +191,8 @@ class BlackbirdTool(Tool):
|
|
|
181
191
|
proc.kill()
|
|
182
192
|
current = ""
|
|
183
193
|
if renderer is not None:
|
|
194
|
+
# \r\033[K clears the ^C echo from the terminal before printing.
|
|
195
|
+
print("\r\033[K", end="")
|
|
184
196
|
renderer.gutter(name, "Run aborted (Ctrl+C).", tool_color, first)
|
|
185
197
|
else:
|
|
186
198
|
print("Run aborted (Ctrl+C).")
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import fcntl
|
|
5
|
+
import os
|
|
6
|
+
import pty
|
|
7
|
+
import re
|
|
8
|
+
import select
|
|
9
|
+
import struct
|
|
10
|
+
import subprocess
|
|
11
|
+
import termios
|
|
12
|
+
from dataclasses import replace
|
|
13
|
+
|
|
14
|
+
from sharkit.output.theme import PINK, hex_to_ansi
|
|
15
|
+
from sharkit.tools.base import (
|
|
16
|
+
ExecutionContext,
|
|
17
|
+
OptionDefinition,
|
|
18
|
+
Result,
|
|
19
|
+
Tool,
|
|
20
|
+
ToolInstallSpec,
|
|
21
|
+
ToolMetadata,
|
|
22
|
+
parse_bool,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_CSI_RE = re.compile(r"\x1b\[[0-9;?]*[a-zA-Z]")
|
|
26
|
+
_OSC_RE = re.compile(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)", re.DOTALL)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _clean_ansi(text: str) -> str:
|
|
30
|
+
text = _OSC_RE.sub("", text)
|
|
31
|
+
return _CSI_RE.sub(lambda m: m.group(0) if m.group(0).endswith("m") else "", text)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MaigretTool(Tool):
|
|
35
|
+
metadata = ToolMetadata(
|
|
36
|
+
name="maigret",
|
|
37
|
+
description="Maigret — username OSINT across 3000+ sites with recursive checks",
|
|
38
|
+
category="recon",
|
|
39
|
+
author="soxoj",
|
|
40
|
+
version="0.1.0",
|
|
41
|
+
safety="safe",
|
|
42
|
+
color="#FF6B35",
|
|
43
|
+
install=ToolInstallSpec(
|
|
44
|
+
git_url="https://github.com/soxoj/maigret.git",
|
|
45
|
+
pip_args=["maigret"],
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self._options = {
|
|
51
|
+
"username": OptionDefinition(
|
|
52
|
+
name="username",
|
|
53
|
+
description="Target username to hunt",
|
|
54
|
+
required=True,
|
|
55
|
+
),
|
|
56
|
+
"timeout": OptionDefinition(
|
|
57
|
+
name="timeout",
|
|
58
|
+
description="Timeout per site in seconds",
|
|
59
|
+
required=False,
|
|
60
|
+
default=None,
|
|
61
|
+
),
|
|
62
|
+
"output": OptionDefinition(
|
|
63
|
+
name="output",
|
|
64
|
+
description="Write results to file",
|
|
65
|
+
required=False,
|
|
66
|
+
default=None,
|
|
67
|
+
),
|
|
68
|
+
"json": OptionDefinition(
|
|
69
|
+
name="json",
|
|
70
|
+
description="Export results as JSON",
|
|
71
|
+
required=False,
|
|
72
|
+
default=None,
|
|
73
|
+
type="bool",
|
|
74
|
+
),
|
|
75
|
+
"csv": OptionDefinition(
|
|
76
|
+
name="csv",
|
|
77
|
+
description="Export results as CSV",
|
|
78
|
+
required=False,
|
|
79
|
+
default=None,
|
|
80
|
+
type="bool",
|
|
81
|
+
),
|
|
82
|
+
"site": OptionDefinition(
|
|
83
|
+
name="site",
|
|
84
|
+
description="Limit to specific site(s) (comma-separated)",
|
|
85
|
+
required=False,
|
|
86
|
+
default=None,
|
|
87
|
+
),
|
|
88
|
+
"top_sites": OptionDefinition(
|
|
89
|
+
name="top_sites",
|
|
90
|
+
description="Only check top N most popular sites",
|
|
91
|
+
required=False,
|
|
92
|
+
default=None,
|
|
93
|
+
),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
def get_metadata(self) -> ToolMetadata:
|
|
97
|
+
return self.metadata
|
|
98
|
+
|
|
99
|
+
def get_options(self) -> dict[str, OptionDefinition]:
|
|
100
|
+
return self._options
|
|
101
|
+
|
|
102
|
+
def set_option(self, key: str, value: str) -> None:
|
|
103
|
+
if key not in self._options:
|
|
104
|
+
raise ValueError(f'Option "{key}" not found.')
|
|
105
|
+
self._options[key] = replace(self._options[key], value=value)
|
|
106
|
+
|
|
107
|
+
def execute(self, context: ExecutionContext) -> Result:
|
|
108
|
+
from sharkit.tools.manager import ToolManager
|
|
109
|
+
|
|
110
|
+
name = self.metadata.name
|
|
111
|
+
tool_color = hex_to_ansi(self.metadata.color) if self.metadata.color else PINK
|
|
112
|
+
manager = ToolManager()
|
|
113
|
+
if not manager.is_installed(name):
|
|
114
|
+
return Result(
|
|
115
|
+
success=False,
|
|
116
|
+
error=f'Tool "{name}" is not installed. Run: install {name}',
|
|
117
|
+
)
|
|
118
|
+
spec = self.metadata.install
|
|
119
|
+
if spec is None:
|
|
120
|
+
return Result(success=False, error="Tool has no install spec.")
|
|
121
|
+
|
|
122
|
+
install_dir = manager.install_path(name)
|
|
123
|
+
maigret_bin = install_dir / "venv" / "bin" / "maigret"
|
|
124
|
+
if not maigret_bin.exists():
|
|
125
|
+
return Result(
|
|
126
|
+
success=False,
|
|
127
|
+
error=f"Maigret binary not found: {maigret_bin}",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
username = context.options.get("username") or ""
|
|
131
|
+
if not username:
|
|
132
|
+
return Result(success=False, error="Option 'username' is required.")
|
|
133
|
+
|
|
134
|
+
args = [username, "--no-progressbar"]
|
|
135
|
+
timeout = context.options.get("timeout")
|
|
136
|
+
if timeout:
|
|
137
|
+
args.extend(["--timeout", str(timeout)])
|
|
138
|
+
output = context.options.get("output")
|
|
139
|
+
if output:
|
|
140
|
+
args.extend(["--output", str(output)])
|
|
141
|
+
if parse_bool(context.options.get("json")):
|
|
142
|
+
args.append("--json")
|
|
143
|
+
if parse_bool(context.options.get("csv")):
|
|
144
|
+
args.append("--csv")
|
|
145
|
+
site = context.options.get("site")
|
|
146
|
+
if site:
|
|
147
|
+
for s in str(site).split(","):
|
|
148
|
+
args.extend(["--site", s.strip()])
|
|
149
|
+
top_sites = context.options.get("top_sites")
|
|
150
|
+
if top_sites:
|
|
151
|
+
args.extend(["--top-sites", str(top_sites)])
|
|
152
|
+
|
|
153
|
+
renderer = context.renderer
|
|
154
|
+
master, slave = pty.openpty()
|
|
155
|
+
try:
|
|
156
|
+
winsize = struct.pack("HHHH", 1000, 10000, 0, 0)
|
|
157
|
+
fcntl.ioctl(master, termios.TIOCSWINSZ, winsize)
|
|
158
|
+
attrs = termios.tcgetattr(master)
|
|
159
|
+
attrs[1] &= ~termios.OPOST
|
|
160
|
+
termios.tcsetattr(master, termios.TCSANOW, attrs)
|
|
161
|
+
except OSError:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
env = os.environ.copy()
|
|
165
|
+
env["TERM"] = "xterm-256color"
|
|
166
|
+
env["FORCE_COLOR"] = "1"
|
|
167
|
+
env["CLICOLOR"] = "1"
|
|
168
|
+
env["CLICOLOR_FORCE"] = "1"
|
|
169
|
+
env["COLUMNS"] = "10000"
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
proc = subprocess.Popen(
|
|
173
|
+
[str(maigret_bin), *args],
|
|
174
|
+
stdout=slave,
|
|
175
|
+
stderr=slave,
|
|
176
|
+
stdin=subprocess.DEVNULL,
|
|
177
|
+
cwd=str(install_dir),
|
|
178
|
+
env=env,
|
|
179
|
+
start_new_session=True,
|
|
180
|
+
)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
return Result(success=False, error=f"Failed to run {name}: {e}")
|
|
183
|
+
finally:
|
|
184
|
+
with contextlib.suppress(OSError):
|
|
185
|
+
os.close(slave)
|
|
186
|
+
|
|
187
|
+
first = True
|
|
188
|
+
current = ""
|
|
189
|
+
started = False
|
|
190
|
+
pending_blank = False
|
|
191
|
+
try:
|
|
192
|
+
while True:
|
|
193
|
+
try:
|
|
194
|
+
r, _, _ = select.select([master], [], [], 0.5)
|
|
195
|
+
except (OSError, ValueError):
|
|
196
|
+
break
|
|
197
|
+
if not r:
|
|
198
|
+
if proc.poll() is not None:
|
|
199
|
+
break
|
|
200
|
+
continue
|
|
201
|
+
try:
|
|
202
|
+
chunk = os.read(master, 65536)
|
|
203
|
+
except OSError:
|
|
204
|
+
break
|
|
205
|
+
if not chunk:
|
|
206
|
+
break
|
|
207
|
+
if not started:
|
|
208
|
+
chunk = chunk.lstrip(b"\n")
|
|
209
|
+
started = True
|
|
210
|
+
for ch in chunk.decode("utf-8", errors="replace"):
|
|
211
|
+
if ch == "\n":
|
|
212
|
+
if current:
|
|
213
|
+
if pending_blank:
|
|
214
|
+
if renderer is not None:
|
|
215
|
+
renderer.gutter(name, "", tool_color, False)
|
|
216
|
+
else:
|
|
217
|
+
print()
|
|
218
|
+
pending_blank = False
|
|
219
|
+
if renderer is not None:
|
|
220
|
+
renderer.gutter(name, _clean_ansi(current), tool_color, first)
|
|
221
|
+
else:
|
|
222
|
+
print(_clean_ansi(current))
|
|
223
|
+
first = False
|
|
224
|
+
else:
|
|
225
|
+
pending_blank = True
|
|
226
|
+
current = ""
|
|
227
|
+
elif ch == "\r":
|
|
228
|
+
current = ""
|
|
229
|
+
else:
|
|
230
|
+
current += ch
|
|
231
|
+
except KeyboardInterrupt:
|
|
232
|
+
with contextlib.suppress(OSError):
|
|
233
|
+
proc.kill()
|
|
234
|
+
current = ""
|
|
235
|
+
if renderer is not None:
|
|
236
|
+
print("\r\033[K", end="")
|
|
237
|
+
renderer.gutter(name, "Run aborted (Ctrl+C).", tool_color, first)
|
|
238
|
+
else:
|
|
239
|
+
print("Run aborted (Ctrl+C).")
|
|
240
|
+
return Result(success=True, data={})
|
|
241
|
+
finally:
|
|
242
|
+
if current:
|
|
243
|
+
if renderer is not None:
|
|
244
|
+
renderer.gutter(name, _clean_ansi(current), tool_color, first)
|
|
245
|
+
else:
|
|
246
|
+
print(_clean_ansi(current))
|
|
247
|
+
with contextlib.suppress(OSError):
|
|
248
|
+
os.close(master)
|
|
249
|
+
with contextlib.suppress(OSError):
|
|
250
|
+
proc.wait()
|
|
251
|
+
|
|
252
|
+
return Result(success=True, data={})
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import fcntl
|
|
5
|
+
import os
|
|
6
|
+
import pty
|
|
7
|
+
import re
|
|
8
|
+
import select
|
|
9
|
+
import struct
|
|
10
|
+
import subprocess
|
|
11
|
+
import termios
|
|
12
|
+
from dataclasses import replace
|
|
13
|
+
|
|
14
|
+
from sharkit.output.theme import PINK, hex_to_ansi
|
|
15
|
+
from sharkit.tools.base import (
|
|
16
|
+
ExecutionContext,
|
|
17
|
+
OptionDefinition,
|
|
18
|
+
Result,
|
|
19
|
+
Tool,
|
|
20
|
+
ToolInstallSpec,
|
|
21
|
+
ToolMetadata,
|
|
22
|
+
parse_bool,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_CSI_RE = re.compile(r"\x1b\[[0-9;?]*[a-zA-Z]")
|
|
26
|
+
_OSC_RE = re.compile(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)", re.DOTALL)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _clean_ansi(text: str) -> str:
|
|
30
|
+
text = _OSC_RE.sub("", text)
|
|
31
|
+
return _CSI_RE.sub(lambda m: m.group(0) if m.group(0).endswith("m") else "", text)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SherlockTool(Tool):
|
|
35
|
+
metadata = ToolMetadata(
|
|
36
|
+
name="sherlock",
|
|
37
|
+
description="Sherlock — find usernames across 400+ social networks",
|
|
38
|
+
category="recon",
|
|
39
|
+
author="sherlock-project",
|
|
40
|
+
version="0.1.0",
|
|
41
|
+
safety="safe",
|
|
42
|
+
color="#4A7AFF",
|
|
43
|
+
install=ToolInstallSpec(
|
|
44
|
+
git_url="https://github.com/sherlock-project/sherlock.git",
|
|
45
|
+
pip_args=["sherlock-project"],
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self._options = {
|
|
51
|
+
"username": OptionDefinition(
|
|
52
|
+
name="username",
|
|
53
|
+
description="Target username to hunt",
|
|
54
|
+
required=True,
|
|
55
|
+
),
|
|
56
|
+
"timeout": OptionDefinition(
|
|
57
|
+
name="timeout",
|
|
58
|
+
description="Timeout per site in seconds",
|
|
59
|
+
required=False,
|
|
60
|
+
default=None,
|
|
61
|
+
),
|
|
62
|
+
"output": OptionDefinition(
|
|
63
|
+
name="output",
|
|
64
|
+
description="Write results to file",
|
|
65
|
+
required=False,
|
|
66
|
+
default=None,
|
|
67
|
+
),
|
|
68
|
+
"json": OptionDefinition(
|
|
69
|
+
name="json",
|
|
70
|
+
description="Export results as JSON",
|
|
71
|
+
required=False,
|
|
72
|
+
default=None,
|
|
73
|
+
type="bool",
|
|
74
|
+
),
|
|
75
|
+
"csv": OptionDefinition(
|
|
76
|
+
name="csv",
|
|
77
|
+
description="Export results as CSV",
|
|
78
|
+
required=False,
|
|
79
|
+
default=None,
|
|
80
|
+
type="bool",
|
|
81
|
+
),
|
|
82
|
+
"site": OptionDefinition(
|
|
83
|
+
name="site",
|
|
84
|
+
description="Limit to specific site(s) (comma-separated)",
|
|
85
|
+
required=False,
|
|
86
|
+
default=None,
|
|
87
|
+
),
|
|
88
|
+
"print_found": OptionDefinition(
|
|
89
|
+
name="print_found",
|
|
90
|
+
description="Only print sites where the username was found",
|
|
91
|
+
required=False,
|
|
92
|
+
default=None,
|
|
93
|
+
type="bool",
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
def get_metadata(self) -> ToolMetadata:
|
|
98
|
+
return self.metadata
|
|
99
|
+
|
|
100
|
+
def get_options(self) -> dict[str, OptionDefinition]:
|
|
101
|
+
return self._options
|
|
102
|
+
|
|
103
|
+
def set_option(self, key: str, value: str) -> None:
|
|
104
|
+
if key not in self._options:
|
|
105
|
+
raise ValueError(f'Option "{key}" not found.')
|
|
106
|
+
self._options[key] = replace(self._options[key], value=value)
|
|
107
|
+
|
|
108
|
+
def execute(self, context: ExecutionContext) -> Result:
|
|
109
|
+
from sharkit.tools.manager import ToolManager
|
|
110
|
+
|
|
111
|
+
name = self.metadata.name
|
|
112
|
+
tool_color = hex_to_ansi(self.metadata.color) if self.metadata.color else PINK
|
|
113
|
+
manager = ToolManager()
|
|
114
|
+
if not manager.is_installed(name):
|
|
115
|
+
return Result(
|
|
116
|
+
success=False,
|
|
117
|
+
error=f'Tool "{name}" is not installed. Run: install {name}',
|
|
118
|
+
)
|
|
119
|
+
spec = self.metadata.install
|
|
120
|
+
if spec is None:
|
|
121
|
+
return Result(success=False, error="Tool has no install spec.")
|
|
122
|
+
|
|
123
|
+
install_dir = manager.install_path(name)
|
|
124
|
+
sherlock_bin = install_dir / "venv" / "bin" / "sherlock"
|
|
125
|
+
if not sherlock_bin.exists():
|
|
126
|
+
return Result(
|
|
127
|
+
success=False,
|
|
128
|
+
error=f"Sherlock binary not found: {sherlock_bin}",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
username = context.options.get("username") or ""
|
|
132
|
+
if not username:
|
|
133
|
+
return Result(success=False, error="Option 'username' is required.")
|
|
134
|
+
|
|
135
|
+
args = [username]
|
|
136
|
+
timeout = context.options.get("timeout")
|
|
137
|
+
if timeout:
|
|
138
|
+
args.extend(["--timeout", str(timeout)])
|
|
139
|
+
output = context.options.get("output")
|
|
140
|
+
if output:
|
|
141
|
+
args.extend(["--output", str(output)])
|
|
142
|
+
if parse_bool(context.options.get("json")):
|
|
143
|
+
args.append("--json")
|
|
144
|
+
if parse_bool(context.options.get("csv")):
|
|
145
|
+
args.append("--csv")
|
|
146
|
+
site = context.options.get("site")
|
|
147
|
+
if site:
|
|
148
|
+
for s in str(site).split(","):
|
|
149
|
+
args.extend(["--site", s.strip()])
|
|
150
|
+
if parse_bool(context.options.get("print_found")):
|
|
151
|
+
args.append("--print-found")
|
|
152
|
+
|
|
153
|
+
renderer = context.renderer
|
|
154
|
+
master, slave = pty.openpty()
|
|
155
|
+
try:
|
|
156
|
+
winsize = struct.pack("HHHH", 1000, 10000, 0, 0)
|
|
157
|
+
fcntl.ioctl(master, termios.TIOCSWINSZ, winsize)
|
|
158
|
+
attrs = termios.tcgetattr(master)
|
|
159
|
+
attrs[1] &= ~termios.OPOST
|
|
160
|
+
termios.tcsetattr(master, termios.TCSANOW, attrs)
|
|
161
|
+
except OSError:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
env = os.environ.copy()
|
|
165
|
+
env["TERM"] = "xterm-256color"
|
|
166
|
+
env["FORCE_COLOR"] = "1"
|
|
167
|
+
env["CLICOLOR"] = "1"
|
|
168
|
+
env["CLICOLOR_FORCE"] = "1"
|
|
169
|
+
env["COLUMNS"] = "10000"
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
proc = subprocess.Popen(
|
|
173
|
+
[str(sherlock_bin), *args],
|
|
174
|
+
stdout=slave,
|
|
175
|
+
stderr=slave,
|
|
176
|
+
stdin=subprocess.DEVNULL,
|
|
177
|
+
cwd=str(install_dir),
|
|
178
|
+
env=env,
|
|
179
|
+
start_new_session=True,
|
|
180
|
+
)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
return Result(success=False, error=f"Failed to run {name}: {e}")
|
|
183
|
+
finally:
|
|
184
|
+
with contextlib.suppress(OSError):
|
|
185
|
+
os.close(slave)
|
|
186
|
+
|
|
187
|
+
first = True
|
|
188
|
+
current = ""
|
|
189
|
+
started = False
|
|
190
|
+
pending_blank = False
|
|
191
|
+
try:
|
|
192
|
+
while True:
|
|
193
|
+
try:
|
|
194
|
+
r, _, _ = select.select([master], [], [], 0.5)
|
|
195
|
+
except (OSError, ValueError):
|
|
196
|
+
break
|
|
197
|
+
if not r:
|
|
198
|
+
if proc.poll() is not None:
|
|
199
|
+
break
|
|
200
|
+
continue
|
|
201
|
+
try:
|
|
202
|
+
chunk = os.read(master, 65536)
|
|
203
|
+
except OSError:
|
|
204
|
+
break
|
|
205
|
+
if not chunk:
|
|
206
|
+
break
|
|
207
|
+
if not started:
|
|
208
|
+
chunk = chunk.lstrip(b"\n")
|
|
209
|
+
started = True
|
|
210
|
+
for ch in chunk.decode("utf-8", errors="replace"):
|
|
211
|
+
if ch == "\n":
|
|
212
|
+
if current:
|
|
213
|
+
if pending_blank:
|
|
214
|
+
if renderer is not None:
|
|
215
|
+
renderer.gutter(name, "", tool_color, False)
|
|
216
|
+
else:
|
|
217
|
+
print()
|
|
218
|
+
pending_blank = False
|
|
219
|
+
if renderer is not None:
|
|
220
|
+
renderer.gutter(name, _clean_ansi(current), tool_color, first)
|
|
221
|
+
else:
|
|
222
|
+
print(_clean_ansi(current))
|
|
223
|
+
first = False
|
|
224
|
+
else:
|
|
225
|
+
pending_blank = True
|
|
226
|
+
current = ""
|
|
227
|
+
elif ch == "\r":
|
|
228
|
+
current = ""
|
|
229
|
+
else:
|
|
230
|
+
current += ch
|
|
231
|
+
except KeyboardInterrupt:
|
|
232
|
+
with contextlib.suppress(OSError):
|
|
233
|
+
proc.kill()
|
|
234
|
+
current = ""
|
|
235
|
+
if renderer is not None:
|
|
236
|
+
print("\r\033[K", end="")
|
|
237
|
+
renderer.gutter(name, "Run aborted (Ctrl+C).", tool_color, first)
|
|
238
|
+
else:
|
|
239
|
+
print("Run aborted (Ctrl+C).")
|
|
240
|
+
return Result(success=True, data={})
|
|
241
|
+
finally:
|
|
242
|
+
if current:
|
|
243
|
+
if renderer is not None:
|
|
244
|
+
renderer.gutter(name, _clean_ansi(current), tool_color, first)
|
|
245
|
+
else:
|
|
246
|
+
print(_clean_ansi(current))
|
|
247
|
+
with contextlib.suppress(OSError):
|
|
248
|
+
os.close(master)
|
|
249
|
+
with contextlib.suppress(OSError):
|
|
250
|
+
proc.wait()
|
|
251
|
+
|
|
252
|
+
return Result(success=True, data={})
|
|
@@ -71,3 +71,17 @@ class ToolRegistry:
|
|
|
71
71
|
for path, name in self._paths.items():
|
|
72
72
|
result[path] = self._tools[name]
|
|
73
73
|
return result
|
|
74
|
+
|
|
75
|
+
def get_tool_path(self, name: str) -> str:
|
|
76
|
+
"""Return the full path for a tool name, or just the name if no path."""
|
|
77
|
+
for path, tool_name in self._paths.items():
|
|
78
|
+
if tool_name == name:
|
|
79
|
+
return path
|
|
80
|
+
return name
|
|
81
|
+
|
|
82
|
+
def format_display(self, path: str) -> str:
|
|
83
|
+
"""Format tool path as 'category(subpath)' for display."""
|
|
84
|
+
if "/" in path:
|
|
85
|
+
parts = path.split("/", 1)
|
|
86
|
+
return f"{parts[0]}({parts[1]})"
|
|
87
|
+
return path
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|