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/main.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _version() -> str:
|
|
9
|
+
from src import __version__
|
|
10
|
+
|
|
11
|
+
return __version__
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_arg_parser() -> argparse.ArgumentParser:
|
|
15
|
+
from src.core.config import DEFAULT_MODEL
|
|
16
|
+
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
prog="ezshell",
|
|
19
|
+
description="ezShell: An intelligent terminal assistant with real-time Groq AI diagnostics and GUI window.",
|
|
20
|
+
)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--update",
|
|
23
|
+
action="store_true",
|
|
24
|
+
help="Show what's new and update ezShell after confirmation (add --yes to skip the prompt).",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--yes",
|
|
28
|
+
"-y",
|
|
29
|
+
action="store_true",
|
|
30
|
+
help="With --update: do not ask for confirmation.",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--check-key",
|
|
34
|
+
action="store_true",
|
|
35
|
+
help="Check that the ezShell service is reachable (or your Groq key with EZSH_DIRECT=1).",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--set-key",
|
|
39
|
+
type=str,
|
|
40
|
+
metavar="KEY",
|
|
41
|
+
help="Save a Groq API key to your personal ezShell configuration (~/.ezshell/config.json).",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--model",
|
|
45
|
+
type=str,
|
|
46
|
+
default=None,
|
|
47
|
+
help=f"Override LLM model (default: {DEFAULT_MODEL}).",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--explain",
|
|
51
|
+
type=str,
|
|
52
|
+
metavar="ERROR_TEXT",
|
|
53
|
+
help="Diagnose an error message directly without entering the interactive shell session.",
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"-c",
|
|
57
|
+
"--command",
|
|
58
|
+
type=str,
|
|
59
|
+
metavar="CMD",
|
|
60
|
+
help="Execute a single command through ezShell and exit.",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--gui",
|
|
64
|
+
action="store_true",
|
|
65
|
+
help="Force open the desktop graphical assistant window.",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--tui",
|
|
69
|
+
action="store_true",
|
|
70
|
+
help="Open in terminal TUI popup mode instead of the desktop GUI window.",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--wrapper",
|
|
74
|
+
action="store_true",
|
|
75
|
+
help="Run ezShell in classic shell wrapper REPL mode.",
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--logo",
|
|
79
|
+
action="store_true",
|
|
80
|
+
help="Display the official ezShell ASCII art logo and welcome overview.",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--install-hook",
|
|
84
|
+
action="store_true",
|
|
85
|
+
help="Automatically install ezShell hook into your active ~/.zshrc or ~/.bashrc.",
|
|
86
|
+
)
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"query",
|
|
89
|
+
nargs="?",
|
|
90
|
+
default=None,
|
|
91
|
+
help="Optional prompt or error to diagnose immediately upon opening.",
|
|
92
|
+
)
|
|
93
|
+
return parser
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _fast_dispatch(argv: list) -> "int | None":
|
|
97
|
+
"""Sous-commandes légères : aucun import lourd (objectif de démarrage < 200 ms)."""
|
|
98
|
+
if not argv:
|
|
99
|
+
return None
|
|
100
|
+
cmd = argv[0]
|
|
101
|
+
if cmd in ("--version", "-V"):
|
|
102
|
+
from src.core.updater import version_hint
|
|
103
|
+
|
|
104
|
+
print(f"ezshell {_version()}")
|
|
105
|
+
hint = version_hint() # lit le cache local : aucun appel réseau
|
|
106
|
+
if hint:
|
|
107
|
+
print(hint)
|
|
108
|
+
return 0
|
|
109
|
+
if cmd == "--update":
|
|
110
|
+
from src.core.updater import run_update
|
|
111
|
+
|
|
112
|
+
rest = set(argv[1:])
|
|
113
|
+
return run_update(yes=bool(rest & {"--yes", "-y"}))
|
|
114
|
+
if cmd == "run":
|
|
115
|
+
from src.core.capture import run_and_capture
|
|
116
|
+
|
|
117
|
+
rest = argv[1:]
|
|
118
|
+
if rest and rest[0] == "--":
|
|
119
|
+
rest = rest[1:]
|
|
120
|
+
return run_and_capture(rest)
|
|
121
|
+
if cmd == "init" and not ({"--install", "-i"} & set(argv[1:])):
|
|
122
|
+
# Exécuté à chaque ouverture de terminal : aucun import lourd.
|
|
123
|
+
from src.core.shell_hook import detect_current_shell, get_hook_script
|
|
124
|
+
|
|
125
|
+
shells = [a for a in argv[1:] if not a.startswith("-")]
|
|
126
|
+
sys.stdout.write(get_hook_script(shells[0] if shells else detect_current_shell()))
|
|
127
|
+
sys.stdout.flush()
|
|
128
|
+
return 0
|
|
129
|
+
if cmd == "suggest":
|
|
130
|
+
ask, free, it = None, [], iter(argv[1:])
|
|
131
|
+
for a in it:
|
|
132
|
+
if a.startswith("--ask="):
|
|
133
|
+
ask = a[len("--ask=") :]
|
|
134
|
+
elif a == "--ask":
|
|
135
|
+
ask = next(it, None)
|
|
136
|
+
else:
|
|
137
|
+
free.append(a)
|
|
138
|
+
if ask is None and free:
|
|
139
|
+
ask = " ".join(free)
|
|
140
|
+
if ask is None: # hook PowerShell : la ligne passe par l'environnement (pas de souci de quoting)
|
|
141
|
+
ask = os.environ.get("EZSH_ASK") or None
|
|
142
|
+
from src.ui.inline import run_inline
|
|
143
|
+
|
|
144
|
+
return run_inline(ask)
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _utf8_console() -> None:
|
|
149
|
+
"""Console Windows : UTF-8 pour les accents et les cadres (sinon cp1252 / cp850)."""
|
|
150
|
+
if os.name != "nt":
|
|
151
|
+
return
|
|
152
|
+
for stream in (sys.stdout, sys.stderr):
|
|
153
|
+
try:
|
|
154
|
+
stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined]
|
|
155
|
+
except (AttributeError, ValueError):
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main() -> int:
|
|
160
|
+
_utf8_console()
|
|
161
|
+
fast = _fast_dispatch(sys.argv[1:])
|
|
162
|
+
if fast is not None:
|
|
163
|
+
return fast
|
|
164
|
+
|
|
165
|
+
from src.app import EzShellApp
|
|
166
|
+
from src.core.config import EzShellConfig
|
|
167
|
+
from src.core.groq_client import GroqClient
|
|
168
|
+
from src.ui.chat_tui import EzShellChatApp
|
|
169
|
+
from src.ui.logo import show_welcome_banner
|
|
170
|
+
from src.ui.render import (
|
|
171
|
+
console,
|
|
172
|
+
render_error,
|
|
173
|
+
render_info,
|
|
174
|
+
render_success,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Subcommand: init (e.g. `eval "$(ezsh init zsh)"` or `ezsh init --install`)
|
|
178
|
+
if len(sys.argv) > 1 and sys.argv[1] == "init":
|
|
179
|
+
from src.core.shell_hook import (
|
|
180
|
+
detect_current_shell,
|
|
181
|
+
get_hook_script,
|
|
182
|
+
install_hook,
|
|
183
|
+
reload_hint,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
remaining = sys.argv[2:]
|
|
187
|
+
if "--install" in remaining or "-i" in remaining:
|
|
188
|
+
target_shells = [arg for arg in remaining if not arg.startswith("-")]
|
|
189
|
+
shell_name = target_shells[0] if target_shells else None
|
|
190
|
+
ok, msg, rc_path = install_hook(shell_name)
|
|
191
|
+
if ok:
|
|
192
|
+
render_success(msg)
|
|
193
|
+
shell = (shell_name or detect_current_shell()).lower()
|
|
194
|
+
render_info(f"Reload your shell with: {reload_hint(shell, rc_path)}")
|
|
195
|
+
return 0
|
|
196
|
+
else:
|
|
197
|
+
render_error(msg)
|
|
198
|
+
return 1
|
|
199
|
+
|
|
200
|
+
shell_name = remaining[0] if remaining else detect_current_shell()
|
|
201
|
+
sys.stdout.write(get_hook_script(shell_name))
|
|
202
|
+
sys.stdout.flush()
|
|
203
|
+
return 0
|
|
204
|
+
|
|
205
|
+
parser = build_arg_parser()
|
|
206
|
+
args = parser.parse_args()
|
|
207
|
+
|
|
208
|
+
# Flag: --install-hook
|
|
209
|
+
if args.install_hook:
|
|
210
|
+
from src.core.shell_hook import detect_current_shell, install_hook, reload_hint
|
|
211
|
+
|
|
212
|
+
ok, msg, rc_path = install_hook()
|
|
213
|
+
if ok:
|
|
214
|
+
render_success(msg)
|
|
215
|
+
render_info(f"Reload your shell with: {reload_hint(detect_current_shell(), rc_path)}")
|
|
216
|
+
return 0
|
|
217
|
+
else:
|
|
218
|
+
render_error(msg)
|
|
219
|
+
return 1
|
|
220
|
+
|
|
221
|
+
config = EzShellConfig.load()
|
|
222
|
+
if args.model:
|
|
223
|
+
config.model = args.model
|
|
224
|
+
|
|
225
|
+
# Flag: --logo
|
|
226
|
+
if args.logo:
|
|
227
|
+
show_welcome_banner(force=True)
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
# Flag: --set-key
|
|
231
|
+
if args.set_key:
|
|
232
|
+
config.groq_api_key = args.set_key.strip()
|
|
233
|
+
config.save()
|
|
234
|
+
render_success("Groq API key saved successfully to ~/.ezshell/config.json")
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
# Flag: --check-key
|
|
238
|
+
if args.check_key:
|
|
239
|
+
from src.core.provider import get_provider
|
|
240
|
+
from src.core.proxy_client import ProxyProvider
|
|
241
|
+
|
|
242
|
+
provider = get_provider(config)
|
|
243
|
+
if isinstance(provider, ProxyProvider):
|
|
244
|
+
with console.status("[cyan]Checking the ezShell service...[/cyan]", spinner="dots"):
|
|
245
|
+
ok = provider.health()
|
|
246
|
+
if ok:
|
|
247
|
+
render_success(
|
|
248
|
+
f"ezShell service reachable ({provider.base_url}): no local key needed."
|
|
249
|
+
)
|
|
250
|
+
return 0
|
|
251
|
+
render_error(f"ezShell service unreachable ({provider.base_url}).")
|
|
252
|
+
return 1
|
|
253
|
+
api_key = config.get_effective_api_key()
|
|
254
|
+
if not api_key:
|
|
255
|
+
render_error(
|
|
256
|
+
"No API key found. Set GROQ_API_KEY in .env.local or run 'ezshell --set-key <your-key>'."
|
|
257
|
+
)
|
|
258
|
+
return 1
|
|
259
|
+
with console.status("[cyan]Verifying Groq API credentials...[/cyan]", spinner="dots"):
|
|
260
|
+
client = GroqClient(api_key=api_key)
|
|
261
|
+
valid = client.validate_connection()
|
|
262
|
+
|
|
263
|
+
if valid:
|
|
264
|
+
render_success("Groq API key is valid and connected!")
|
|
265
|
+
return 0
|
|
266
|
+
else:
|
|
267
|
+
render_error("Groq API key validation failed. Please verify your credentials.")
|
|
268
|
+
return 1
|
|
269
|
+
|
|
270
|
+
# Flag: --explain (même chemin que Alt+E : proxy, validation, plancher de risque)
|
|
271
|
+
if args.explain:
|
|
272
|
+
from src.core.chat_bridge import describe_error
|
|
273
|
+
from src.core.provider import get_provider
|
|
274
|
+
from src.core.redact import redact_secrets
|
|
275
|
+
from src.core.suggest import build_request, get_suggestion
|
|
276
|
+
from src.ui.panel import result_panel
|
|
277
|
+
|
|
278
|
+
req = build_request("")
|
|
279
|
+
req.mode = "explain_error"
|
|
280
|
+
req.stderr_tail = redact_secrets(args.explain)[-4096:]
|
|
281
|
+
if req.exit_code is None:
|
|
282
|
+
req.exit_code = 1
|
|
283
|
+
with console.status("[cyan]Analyzing error message...[/cyan]", spinner="dots"):
|
|
284
|
+
try:
|
|
285
|
+
suggestion = get_suggestion(req, get_provider(config))
|
|
286
|
+
except Exception as e:
|
|
287
|
+
render_error(describe_error(e))
|
|
288
|
+
return 1
|
|
289
|
+
console.print(result_panel(suggestion, req.lang))
|
|
290
|
+
return 0
|
|
291
|
+
|
|
292
|
+
# Flag: -c / --command
|
|
293
|
+
if args.command:
|
|
294
|
+
app = EzShellApp(config=config)
|
|
295
|
+
return app.execute_single_command(args.command)
|
|
296
|
+
|
|
297
|
+
# Automatic first-time install greeting banner
|
|
298
|
+
show_welcome_banner(force=False)
|
|
299
|
+
|
|
300
|
+
# Mode: Wrapper REPL (shell PTY : Unix uniquement)
|
|
301
|
+
if args.wrapper:
|
|
302
|
+
if os.name == "nt":
|
|
303
|
+
render_error(
|
|
304
|
+
"--wrapper is not available on Windows. Use Alt+E in PowerShell "
|
|
305
|
+
"(ezsh init powershell), ezsh --tui or ezsh."
|
|
306
|
+
)
|
|
307
|
+
return 2
|
|
308
|
+
app = EzShellApp(config=config)
|
|
309
|
+
app.run()
|
|
310
|
+
return 0
|
|
311
|
+
|
|
312
|
+
# Mode: Terminal TUI
|
|
313
|
+
if args.tui:
|
|
314
|
+
chat_app = EzShellChatApp(config=config)
|
|
315
|
+
chat_app.run()
|
|
316
|
+
return 0
|
|
317
|
+
|
|
318
|
+
# Default Mode: Desktop Graphical Window (with fallback to TUI)
|
|
319
|
+
has_display = bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
|
|
320
|
+
if has_display or args.gui:
|
|
321
|
+
if "DISPLAY" not in os.environ and "WAYLAND_DISPLAY" in os.environ:
|
|
322
|
+
os.environ["DISPLAY"] = ":0"
|
|
323
|
+
try:
|
|
324
|
+
import warnings
|
|
325
|
+
|
|
326
|
+
warnings.filterwarnings("ignore", message=".*Given image is not CTkImage.*")
|
|
327
|
+
|
|
328
|
+
from src.ui.gui_window import EzShellDesktopApp
|
|
329
|
+
|
|
330
|
+
gui_app = EzShellDesktopApp(
|
|
331
|
+
config=config,
|
|
332
|
+
initial_query=args.query,
|
|
333
|
+
)
|
|
334
|
+
gui_app.mainloop()
|
|
335
|
+
return 0
|
|
336
|
+
except Exception:
|
|
337
|
+
# If GUI fails to open (e.g. headless or X connection refused), fallback to TUI
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
chat_app = EzShellChatApp(config=config)
|
|
341
|
+
chat_app.run()
|
|
342
|
+
return 0
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
if __name__ == "__main__":
|
|
346
|
+
sys.exit(main())
|