ezshell 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ezshell-0.2.0.dist-info/METADATA +174 -0
- ezshell-0.2.0.dist-info/RECORD +37 -0
- ezshell-0.2.0.dist-info/WHEEL +5 -0
- ezshell-0.2.0.dist-info/entry_points.txt +3 -0
- ezshell-0.2.0.dist-info/licenses/LICENSE +21 -0
- ezshell-0.2.0.dist-info/top_level.txt +1 -0
- src/__init__.py +3 -0
- src/app.py +286 -0
- src/core/__init__.py +0 -0
- src/core/assistant.py +525 -0
- src/core/capture.py +213 -0
- src/core/chat_bridge.py +107 -0
- src/core/config.py +238 -0
- src/core/context.py +229 -0
- src/core/groq_client.py +376 -0
- src/core/llm_client.py +111 -0
- src/core/prompts.py +262 -0
- src/core/provider.py +233 -0
- src/core/proxy_client.py +166 -0
- src/core/pty_shell.py +273 -0
- src/core/redact.py +95 -0
- src/core/risk.py +455 -0
- src/core/safety.py +268 -0
- src/core/sanitize.py +39 -0
- src/core/schema.py +65 -0
- src/core/shell_hook.py +388 -0
- src/core/suggest.py +94 -0
- src/core/updater.py +428 -0
- src/i18n.py +422 -0
- src/main.py +346 -0
- src/ui/chat_tui.py +634 -0
- src/ui/gui_window.py +1018 -0
- src/ui/inline.py +330 -0
- src/ui/logo.py +194 -0
- src/ui/panel.py +163 -0
- src/ui/prompt.py +184 -0
- src/ui/render.py +269 -0
src/ui/prompt.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from prompt_toolkit import PromptSession
|
|
7
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
8
|
+
from prompt_toolkit.completion import (
|
|
9
|
+
Completer,
|
|
10
|
+
Completion,
|
|
11
|
+
PathCompleter,
|
|
12
|
+
WordCompleter,
|
|
13
|
+
)
|
|
14
|
+
from prompt_toolkit.formatted_text import HTML
|
|
15
|
+
from prompt_toolkit.history import FileHistory
|
|
16
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
17
|
+
from prompt_toolkit.styles import Style
|
|
18
|
+
|
|
19
|
+
from src.core.context import SystemContext
|
|
20
|
+
|
|
21
|
+
COMMON_COMMANDS = [
|
|
22
|
+
"git",
|
|
23
|
+
"docker",
|
|
24
|
+
"docker-compose",
|
|
25
|
+
"kubectl",
|
|
26
|
+
"terraform",
|
|
27
|
+
"python",
|
|
28
|
+
"python3",
|
|
29
|
+
"pip",
|
|
30
|
+
"pytest",
|
|
31
|
+
"npm",
|
|
32
|
+
"yarn",
|
|
33
|
+
"pnpm",
|
|
34
|
+
"cargo",
|
|
35
|
+
"rustc",
|
|
36
|
+
"go",
|
|
37
|
+
"curl",
|
|
38
|
+
"wget",
|
|
39
|
+
"ssh",
|
|
40
|
+
"scp",
|
|
41
|
+
"systemctl",
|
|
42
|
+
"journalctl",
|
|
43
|
+
"make",
|
|
44
|
+
"grep",
|
|
45
|
+
"find",
|
|
46
|
+
"cat",
|
|
47
|
+
"ls",
|
|
48
|
+
"cd",
|
|
49
|
+
"pwd",
|
|
50
|
+
"export",
|
|
51
|
+
"env",
|
|
52
|
+
"chmod",
|
|
53
|
+
"chown",
|
|
54
|
+
"tar",
|
|
55
|
+
"zip",
|
|
56
|
+
"unzip",
|
|
57
|
+
"df",
|
|
58
|
+
"du",
|
|
59
|
+
"top",
|
|
60
|
+
"htop",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
EZSHELL_BUILTINS = {
|
|
64
|
+
"!ask": "Ask AI a natural language terminal question",
|
|
65
|
+
"!explain": "Explain the last command output or error",
|
|
66
|
+
"!fix": "Re-execute the last suggested fix",
|
|
67
|
+
"!config": "View current ezShell configuration",
|
|
68
|
+
"!help": "Show ezShell documentation and built-in commands",
|
|
69
|
+
"exit": "Exit ezShell",
|
|
70
|
+
"quit": "Exit ezShell",
|
|
71
|
+
"clear": "Clear terminal screen",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class EzShellCompleter(Completer):
|
|
76
|
+
"""Hybrid completer combining ezShell builtins, common commands, and filesystem paths."""
|
|
77
|
+
|
|
78
|
+
def __init__(self):
|
|
79
|
+
self.cmd_completer = WordCompleter(COMMON_COMMANDS, ignore_case=True)
|
|
80
|
+
self.path_completer = PathCompleter(expanduser=True)
|
|
81
|
+
|
|
82
|
+
def get_completions(self, document, complete_event):
|
|
83
|
+
text_before_cursor = document.text_before_cursor
|
|
84
|
+
|
|
85
|
+
# If user is typing an ezShell meta-command (!...)
|
|
86
|
+
if text_before_cursor.startswith("!"):
|
|
87
|
+
word = document.get_word_before_cursor()
|
|
88
|
+
for cmd, desc in EZSHELL_BUILTINS.items():
|
|
89
|
+
if cmd.startswith("!") and cmd.startswith(word):
|
|
90
|
+
yield Completion(
|
|
91
|
+
cmd,
|
|
92
|
+
start_position=-len(word),
|
|
93
|
+
display=cmd,
|
|
94
|
+
display_meta=desc,
|
|
95
|
+
)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
# Offer command completion if at the beginning of the line
|
|
99
|
+
tokens = text_before_cursor.split()
|
|
100
|
+
if len(tokens) <= 1 and not text_before_cursor.endswith(" "):
|
|
101
|
+
yield from self.cmd_completer.get_completions(document, complete_event)
|
|
102
|
+
|
|
103
|
+
# Always offer path completion
|
|
104
|
+
yield from self.path_completer.get_completions(document, complete_event)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_prompt_text(context: SystemContext, last_exit_code: int = 0) -> HTML:
|
|
108
|
+
"""Builds a high-contrast, vivid 2-line prompt with bright neon accents."""
|
|
109
|
+
user = context.current_user
|
|
110
|
+
|
|
111
|
+
# Compact home directory representation
|
|
112
|
+
cwd = context.current_directory
|
|
113
|
+
home = str(Path.home())
|
|
114
|
+
if cwd.startswith(home):
|
|
115
|
+
display_cwd = "~" + cwd[len(home):]
|
|
116
|
+
else:
|
|
117
|
+
display_cwd = cwd
|
|
118
|
+
|
|
119
|
+
# Truncate overly long paths
|
|
120
|
+
parts = display_cwd.split("/")
|
|
121
|
+
if len(parts) > 4:
|
|
122
|
+
display_cwd = f".../{parts[-2]}/{parts[-1]}"
|
|
123
|
+
|
|
124
|
+
# Git branch segment in vivid neon green
|
|
125
|
+
git_segment = ""
|
|
126
|
+
if context.git_branch:
|
|
127
|
+
dirty_indicator = '<style fg="#FFB800"><b>*</b></style>' if context.git_is_dirty else ""
|
|
128
|
+
git_segment = (
|
|
129
|
+
f' <style fg="#FFFFFF">on</style> '
|
|
130
|
+
f'<style fg="#00FF88"><b>⎇ {context.git_branch}</b></style>{dirty_indicator}'
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Exit code badge (vivid neon red on failure)
|
|
134
|
+
exit_badge = ""
|
|
135
|
+
if last_exit_code != 0:
|
|
136
|
+
exit_badge = f' <style fg="#FF2A6D"><b>[✘ {last_exit_code}]</b></style>'
|
|
137
|
+
|
|
138
|
+
arrow_color = "#00FF88" if last_exit_code == 0 else "#FF2A6D"
|
|
139
|
+
|
|
140
|
+
return HTML(
|
|
141
|
+
f'<style fg="#00E5FF"><b>╭─</b></style> '
|
|
142
|
+
f'<style fg="#C084FC"><b>{user}</b></style> '
|
|
143
|
+
f'<style fg="#FFFFFF">in</style> '
|
|
144
|
+
f'<style fg="#FACC15"><b>{display_cwd}</b></style>'
|
|
145
|
+
f'{git_segment}'
|
|
146
|
+
f'{exit_badge}\n'
|
|
147
|
+
f'<style fg="#00E5FF"><b>╰─</b></style><style fg="{arrow_color}"><b>❯</b></style> '
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def create_prompt_session(history_file: Optional[Path] = None) -> PromptSession:
|
|
152
|
+
"""Creates a configured prompt_toolkit PromptSession with high-contrast vibrant styles."""
|
|
153
|
+
hist_path = history_file or (Path.home() / ".ezshell_history")
|
|
154
|
+
hist_path.parent.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
|
|
156
|
+
history = FileHistory(str(hist_path))
|
|
157
|
+
completer = EzShellCompleter()
|
|
158
|
+
|
|
159
|
+
style = Style.from_dict(
|
|
160
|
+
{
|
|
161
|
+
# Autocompletion popup styling with vibrant accents
|
|
162
|
+
"completion-menu": "bg:#0F172A #F8FAFC",
|
|
163
|
+
"completion-menu.completion": "bg:#0F172A #F8FAFC",
|
|
164
|
+
"completion-menu.completion.current": "bg:#00E5FF #0F172A bold",
|
|
165
|
+
"completion-menu.meta": "bg:#1E293B #94A3B8 italic",
|
|
166
|
+
# Fish-style auto-suggestions
|
|
167
|
+
"auto-suggestion": "#64748B italic",
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
kb = KeyBindings()
|
|
172
|
+
|
|
173
|
+
@kb.add("c-space")
|
|
174
|
+
def _insert_ask(event):
|
|
175
|
+
"""Ctrl+Space inserts '!ask ' at prompt."""
|
|
176
|
+
event.current_buffer.insert_text("!ask ")
|
|
177
|
+
|
|
178
|
+
return PromptSession(
|
|
179
|
+
history=history,
|
|
180
|
+
completer=completer,
|
|
181
|
+
auto_suggest=AutoSuggestFromHistory(),
|
|
182
|
+
style=style,
|
|
183
|
+
key_bindings=kb,
|
|
184
|
+
)
|
src/ui/render.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from rich.box import ROUNDED
|
|
7
|
+
from rich.console import Console, Group
|
|
8
|
+
from rich.markdown import Markdown
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.rule import Rule
|
|
11
|
+
from rich.syntax import Syntax
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
from rich.theme import Theme
|
|
15
|
+
|
|
16
|
+
from src import __version__
|
|
17
|
+
from src.core.assistant import AskResult, DiagnosticResult
|
|
18
|
+
from src.core.safety import RiskLevel, SafetyReport
|
|
19
|
+
|
|
20
|
+
# Vivid neon palette — high-contrast, no muted tones
|
|
21
|
+
custom_theme = Theme(
|
|
22
|
+
{
|
|
23
|
+
"info": "#00E5FF bold", # electric cyan
|
|
24
|
+
"warning": "#FFB800 bold", # vivid amber
|
|
25
|
+
"error": "#FF2A6D bold", # neon red/pink
|
|
26
|
+
"success": "#00FF88 bold", # neon green
|
|
27
|
+
"command": "#FACC15 bold", # neon yellow
|
|
28
|
+
"muted": "#94A3B8", # cool grey (readable, not muddy)
|
|
29
|
+
"accent": "#C084FC bold", # vivid purple
|
|
30
|
+
"highlight": "#00E5FF bold", # electric cyan
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
console = Console(theme=custom_theme, file=sys.stdout)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def render_banner(model_name: str, shell_name: str) -> None:
|
|
38
|
+
"""Renders a sleek, modern ezShell header banner."""
|
|
39
|
+
console.print()
|
|
40
|
+
|
|
41
|
+
title_text = Text()
|
|
42
|
+
title_text.append("ezShell ", style="bold #00E5FF")
|
|
43
|
+
title_text.append(f"v{__version__}", style="#94A3B8")
|
|
44
|
+
title_text.append(" • ", style="#475569")
|
|
45
|
+
title_text.append("Model: ", style="#94A3B8")
|
|
46
|
+
title_text.append(f"{model_name}", style="bold #C084FC")
|
|
47
|
+
title_text.append(" • ", style="#475569")
|
|
48
|
+
title_text.append("Shell: ", style="#94A3B8")
|
|
49
|
+
title_text.append(f"{shell_name}", style="#00FF88")
|
|
50
|
+
|
|
51
|
+
subtitle_text = Text()
|
|
52
|
+
subtitle_text.append("Type ", style="#94A3B8")
|
|
53
|
+
subtitle_text.append("!ask <query>", style="bold #00E5FF")
|
|
54
|
+
subtitle_text.append(" for instant AI help • ", style="#94A3B8")
|
|
55
|
+
subtitle_text.append("!help", style="bold #C084FC")
|
|
56
|
+
subtitle_text.append(" for commands • ", style="#94A3B8")
|
|
57
|
+
subtitle_text.append("exit", style="#FF2A6D")
|
|
58
|
+
subtitle_text.append(" to quit", style="#94A3B8")
|
|
59
|
+
|
|
60
|
+
content = Table.grid(padding=(0, 0))
|
|
61
|
+
content.add_row(title_text)
|
|
62
|
+
content.add_row(subtitle_text)
|
|
63
|
+
|
|
64
|
+
banner_panel = Panel(
|
|
65
|
+
content,
|
|
66
|
+
box=ROUNDED,
|
|
67
|
+
border_style="#00E5FF",
|
|
68
|
+
padding=(1, 2),
|
|
69
|
+
)
|
|
70
|
+
console.print(banner_panel)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def render_api_key_missing_card() -> None:
|
|
74
|
+
"""Renders an attractive notice card when GROQ_API_KEY is not configured."""
|
|
75
|
+
text = Text()
|
|
76
|
+
text.append("Groq AI Engine Inactive\n", style="bold #FFB800")
|
|
77
|
+
text.append("ezShell will run commands normally, but automated error diagnosis is off.\n\n", style="#FFFFFF")
|
|
78
|
+
text.append("To activate AI assistance:\n", style="#94A3B8")
|
|
79
|
+
text.append(" 1. Add to .env.local: ", style="#94A3B8")
|
|
80
|
+
text.append("GROQ_API_KEY=gsk_...\n", style="bold #00E5FF")
|
|
81
|
+
text.append(" 2. Or configure via CLI: ", style="#94A3B8")
|
|
82
|
+
text.append("ezshell --set-key <key>", style="bold #C084FC")
|
|
83
|
+
|
|
84
|
+
panel = Panel(
|
|
85
|
+
text,
|
|
86
|
+
box=ROUNDED,
|
|
87
|
+
border_style="#FFB800",
|
|
88
|
+
padding=(0, 2),
|
|
89
|
+
)
|
|
90
|
+
console.print(panel)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def render_error_card(result: DiagnosticResult) -> None:
|
|
94
|
+
"""Renders the AI diagnostic card matching the official ezShell reference design."""
|
|
95
|
+
console.print()
|
|
96
|
+
|
|
97
|
+
elements = []
|
|
98
|
+
|
|
99
|
+
# 1. User Query row: > explain this error
|
|
100
|
+
query_text = Text()
|
|
101
|
+
query_text.append("> ", style="bold #00E5FF")
|
|
102
|
+
query_text.append(f"explain this error: {result.command}", style="#F1F5F9")
|
|
103
|
+
elements.append(query_text)
|
|
104
|
+
elements.append(Text(""))
|
|
105
|
+
|
|
106
|
+
# 2. Section: CAUSE
|
|
107
|
+
cause_lbl = Text("CAUSE\n", style="bold #64748B")
|
|
108
|
+
cause_body = result.summary or result.explanation or "Unknown root cause."
|
|
109
|
+
elements.append(cause_lbl)
|
|
110
|
+
elements.append(Text(f"{cause_body}\n", style="#CAD5E2"))
|
|
111
|
+
|
|
112
|
+
# 3. Section: FIX
|
|
113
|
+
if result.suggested_fix:
|
|
114
|
+
fix_lbl = Text("FIX", style="bold #64748B")
|
|
115
|
+
elements.append(fix_lbl)
|
|
116
|
+
|
|
117
|
+
cmd_text = Text()
|
|
118
|
+
cmd_text.append("$ ", style="#64748B")
|
|
119
|
+
cmd_text.append(result.suggested_fix.strip(), style="bold #00E5FF")
|
|
120
|
+
|
|
121
|
+
fix_panel = Panel(
|
|
122
|
+
cmd_text,
|
|
123
|
+
box=ROUNDED,
|
|
124
|
+
border_style="#182739",
|
|
125
|
+
style="on #070D16",
|
|
126
|
+
padding=(0, 1),
|
|
127
|
+
)
|
|
128
|
+
elements.append(fix_panel)
|
|
129
|
+
|
|
130
|
+
# 4. Section: RISK
|
|
131
|
+
report = result.safety_report
|
|
132
|
+
if report:
|
|
133
|
+
if report.risk_level == RiskLevel.DANGEROUS:
|
|
134
|
+
dot_color = "#FF2A6D"
|
|
135
|
+
risk_label = "HIGH RISK"
|
|
136
|
+
elif report.risk_level == RiskLevel.WARNING:
|
|
137
|
+
dot_color = "#FFB800"
|
|
138
|
+
risk_label = "MEDIUM RISK"
|
|
139
|
+
else:
|
|
140
|
+
dot_color = "#00FF88"
|
|
141
|
+
risk_label = "LOW RISK"
|
|
142
|
+
reason = report.reason or "Only changes your local environment."
|
|
143
|
+
else:
|
|
144
|
+
dot_color = "#00FF88"
|
|
145
|
+
risk_label = "LOW RISK"
|
|
146
|
+
reason = "Only changes your local environment."
|
|
147
|
+
|
|
148
|
+
risk_text = Text()
|
|
149
|
+
risk_text.append("● ", style=f"bold {dot_color}")
|
|
150
|
+
risk_text.append(f"{risk_label} ", style=f"bold {dot_color}")
|
|
151
|
+
risk_text.append(f"{reason}\n", style="#7B8C9F")
|
|
152
|
+
elements.append(risk_text)
|
|
153
|
+
|
|
154
|
+
# 5. Divider & Footer
|
|
155
|
+
elements.append(Rule(style="#152232"))
|
|
156
|
+
elements.append(Text("ASK ANOTHER QUESTION\n", style="bold #64748B"))
|
|
157
|
+
|
|
158
|
+
footer = Text()
|
|
159
|
+
footer.append("[Enter run] ", style="bold #FFFFFF on #0E1724")
|
|
160
|
+
footer.append("[c copy] ", style="bold #FFFFFF on #0E1724")
|
|
161
|
+
footer.append("[e edit] ", style="bold #FFFFFF on #0E1724")
|
|
162
|
+
footer.append("[q close]", style="bold #FFFFFF on #0E1724")
|
|
163
|
+
elements.append(footer)
|
|
164
|
+
|
|
165
|
+
outer_panel = Panel(
|
|
166
|
+
Group(*elements),
|
|
167
|
+
title="[bold #00E5FF]>_[/bold #00E5FF] [bold #FFFFFF]ezShell[/bold #FFFFFF]",
|
|
168
|
+
subtitle="[dim #64748B][Esc] to close[/dim #64748B]",
|
|
169
|
+
border_style="#00E5FF",
|
|
170
|
+
box=ROUNDED,
|
|
171
|
+
padding=(1, 2),
|
|
172
|
+
)
|
|
173
|
+
console.print(outer_panel)
|
|
174
|
+
console.print()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def render_ask_card(result: AskResult) -> None:
|
|
178
|
+
"""Renders the response card for an '!ask' query."""
|
|
179
|
+
console.print()
|
|
180
|
+
|
|
181
|
+
elements = []
|
|
182
|
+
|
|
183
|
+
if result.explanation:
|
|
184
|
+
elements.append(Markdown(result.explanation))
|
|
185
|
+
|
|
186
|
+
if result.suggested_fix:
|
|
187
|
+
fix_title = Text()
|
|
188
|
+
fix_title.append("\n› Recommended Command", style="bold #00E5FF")
|
|
189
|
+
if result.fix_description:
|
|
190
|
+
fix_title.append(f" ({result.fix_description})", style="italic #94A3B8")
|
|
191
|
+
elements.append(fix_title)
|
|
192
|
+
|
|
193
|
+
syntax = Syntax(
|
|
194
|
+
result.suggested_fix.strip(),
|
|
195
|
+
lexer="bash",
|
|
196
|
+
theme="monokai",
|
|
197
|
+
line_numbers=False,
|
|
198
|
+
padding=(1, 2),
|
|
199
|
+
background_color="#0F172A",
|
|
200
|
+
)
|
|
201
|
+
elements.append(syntax)
|
|
202
|
+
|
|
203
|
+
if result.safety_report and result.safety_report.risk_level != RiskLevel.SAFE:
|
|
204
|
+
level_style = "bold #FF2A6D" if result.safety_report.risk_level == RiskLevel.DANGEROUS else "bold #FFB800"
|
|
205
|
+
elements.append(
|
|
206
|
+
f"\n[{level_style}]SAFETY WARNING:[/{level_style}] {result.safety_report.reason}"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
panel = Panel(
|
|
210
|
+
Group(*elements),
|
|
211
|
+
title=f"[bold #00E5FF] ? {result.question} [/bold #00E5FF]",
|
|
212
|
+
border_style="#00E5FF",
|
|
213
|
+
box=ROUNDED,
|
|
214
|
+
padding=(1, 2),
|
|
215
|
+
)
|
|
216
|
+
console.print(panel)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def render_info(message: str) -> None:
|
|
220
|
+
"""Renders a styled info message."""
|
|
221
|
+
console.print(f"[#00E5FF]i[/] [#FFFFFF]{message}[/]")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def render_success(message: str) -> None:
|
|
225
|
+
"""Renders a styled success message."""
|
|
226
|
+
console.print(f"[#00FF88]✔[/] [bold #FFFFFF]{message}[/]")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def render_warning(message: str) -> None:
|
|
230
|
+
"""Renders a styled warning message."""
|
|
231
|
+
console.print(f"[#FFB800]![/] [bold #FFB800]{message}[/]")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def render_error(message: str) -> None:
|
|
235
|
+
"""Renders a styled error message."""
|
|
236
|
+
console.print(f"[#FF2A6D]✖[/] [bold #FF2A6D]{message}[/]")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def prompt_fix_action(
|
|
240
|
+
fix_command: str,
|
|
241
|
+
safety_report: Optional[SafetyReport] = None,
|
|
242
|
+
) -> str:
|
|
243
|
+
"""Prompts the user to decide whether to run the suggested fix command.
|
|
244
|
+
Returns: 'run', 'edit', or 'skip'
|
|
245
|
+
"""
|
|
246
|
+
if safety_report and safety_report.risk_level == RiskLevel.DANGEROUS:
|
|
247
|
+
console.print(
|
|
248
|
+
f"\n[bold #FF2A6D][!] DANGEROUS COMMAND DETECTED:[/] [bold #FFFFFF]{safety_report.reason}[/]"
|
|
249
|
+
)
|
|
250
|
+
try:
|
|
251
|
+
confirmation = console.input(
|
|
252
|
+
"Type [bold #FF2A6D]'CONFIRM'[/] to execute this command or press Enter to skip: "
|
|
253
|
+
).strip()
|
|
254
|
+
return "run" if confirmation == "CONFIRM" else "skip"
|
|
255
|
+
except (KeyboardInterrupt, EOFError):
|
|
256
|
+
return "skip"
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
choice = console.input(
|
|
260
|
+
"\n[bold #00FF88]› Execute fix?[/] [[bold #00E5FF]y[/]es / [bold #94A3B8]N[/]o / [bold #C084FC]e[/]dit]: "
|
|
261
|
+
).strip().lower()
|
|
262
|
+
|
|
263
|
+
if choice in ("y", "yes"):
|
|
264
|
+
return "run"
|
|
265
|
+
elif choice in ("e", "edit"):
|
|
266
|
+
return "edit"
|
|
267
|
+
return "skip"
|
|
268
|
+
except (KeyboardInterrupt, EOFError):
|
|
269
|
+
return "skip"
|