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.
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())