shellmate-cli 3.0.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.
Files changed (106) hide show
  1. shellmate/__init__.py +0 -0
  2. shellmate/adapters/__init__.py +67 -0
  3. shellmate/adapters/bash_adapter.sh +7 -0
  4. shellmate/adapters/ble_adapter.sh +350 -0
  5. shellmate/adapters/powershell_adapter.ps1 +417 -0
  6. shellmate/adapters/powershell_predictor.ps1 +97 -0
  7. shellmate/adapters/zsh_adapter.zsh +8 -0
  8. shellmate/agents/__init__.py +23 -0
  9. shellmate/agents/base.py +248 -0
  10. shellmate/agents/chain_patterns.yaml +98 -0
  11. shellmate/agents/command_chain.py +322 -0
  12. shellmate/agents/context_gatherer.py +175 -0
  13. shellmate/agents/environment.py +363 -0
  14. shellmate/agents/librarian.py +502 -0
  15. shellmate/agents/query.py +415 -0
  16. shellmate/agents/repomix_backend.py +171 -0
  17. shellmate/agents/watcher.py +266 -0
  18. shellmate/cache/__init__.py +0 -0
  19. shellmate/cache/semantic_cache.py +41 -0
  20. shellmate/cli/__init__.py +0 -0
  21. shellmate/cli/commands/__init__.py +27 -0
  22. shellmate/cli/commands/analyze.py +5 -0
  23. shellmate/cli/commands/config.py +319 -0
  24. shellmate/cli/commands/configure.py +118 -0
  25. shellmate/cli/commands/disable.py +11 -0
  26. shellmate/cli/commands/enable.py +49 -0
  27. shellmate/cli/commands/init.py +412 -0
  28. shellmate/cli/commands/install.py +242 -0
  29. shellmate/cli/commands/migrate.py +147 -0
  30. shellmate/cli/commands/status.py +31 -0
  31. shellmate/cli/commands/uninstall.py +78 -0
  32. shellmate/cli/main.py +114 -0
  33. shellmate/cli/parser.py +184 -0
  34. shellmate/cli/version.py +1 -0
  35. shellmate/completion/__init__.py +4 -0
  36. shellmate/completion/bash_adapter.py +145 -0
  37. shellmate/config/__init__.py +33 -0
  38. shellmate/config/credentials.py +120 -0
  39. shellmate/config/defaults.py +88 -0
  40. shellmate/config/manager.py +94 -0
  41. shellmate/config/paths.py +77 -0
  42. shellmate/config/secrets.py +66 -0
  43. shellmate/daemon/__init__.py +0 -0
  44. shellmate/daemon/lifecycle.py +308 -0
  45. shellmate/daemon/observability.py +82 -0
  46. shellmate/daemon/pipeline.py +119 -0
  47. shellmate/daemon/server.py +605 -0
  48. shellmate/daemon/session.py +170 -0
  49. shellmate/daemon/startup_ui.py +43 -0
  50. shellmate/environment/__init__.py +0 -0
  51. shellmate/environment/detector.py +130 -0
  52. shellmate/ipc/__init__.py +0 -0
  53. shellmate/ipc/client.py +210 -0
  54. shellmate/ipc/ps_client.py +190 -0
  55. shellmate/orchestration/__init__.py +3 -0
  56. shellmate/orchestration/analysis_graph.py +1274 -0
  57. shellmate/orchestration/state.py +35 -0
  58. shellmate/parser/__init__.py +0 -0
  59. shellmate/parser/command_parser.py +111 -0
  60. shellmate/plugins/__init__.py +0 -0
  61. shellmate/plugins/base.py +80 -0
  62. shellmate/plugins/loader.py +41 -0
  63. shellmate/providers/__init__.py +0 -0
  64. shellmate/providers/base.py +97 -0
  65. shellmate/providers/groq_command_guide.py +108 -0
  66. shellmate/providers/groq_provider.py +127 -0
  67. shellmate/providers/groq_supervisor.py +78 -0
  68. shellmate/providers/openrouter_provider.py +149 -0
  69. shellmate/providers/router.py +272 -0
  70. shellmate/providers/validate_keys.py +98 -0
  71. shellmate/providers/xai_provider.py +142 -0
  72. shellmate/rules/__init__.py +0 -0
  73. shellmate/rules/catastrophic.yaml +96 -0
  74. shellmate/rules/catastrophic_matcher.py +167 -0
  75. shellmate/rules/definitions/__init__.py +0 -0
  76. shellmate/rules/definitions/file_deletion.yaml +54 -0
  77. shellmate/rules/definitions/filesystem.yaml +43 -0
  78. shellmate/rules/definitions/network_execution.yaml +46 -0
  79. shellmate/rules/definitions/permissions.yaml +54 -0
  80. shellmate/rules/definitions/process_control.yaml +37 -0
  81. shellmate/rules/definitions/system_destruction.yaml +67 -0
  82. shellmate/rules/engine.py +200 -0
  83. shellmate/sanitizer/__init__.py +0 -0
  84. shellmate/sanitizer/typo_detector.py +114 -0
  85. shellmate/ui/__init__.py +10 -0
  86. shellmate/ui/analysis.py +164 -0
  87. shellmate/ui/annotations.py +232 -0
  88. shellmate/ui/banner.py +49 -0
  89. shellmate/ui/progress.py +126 -0
  90. shellmate/ui/runtime/__init__.py +15 -0
  91. shellmate/ui/runtime/console_factory.py +65 -0
  92. shellmate/ui/runtime/live_scene.py +116 -0
  93. shellmate/ui/runtime/motion_policy.py +69 -0
  94. shellmate/ui/runtime/tokens.py +47 -0
  95. shellmate/ui/scenes/__init__.py +24 -0
  96. shellmate/ui/scenes/enable.py +316 -0
  97. shellmate/ui/scenes/help.py +53 -0
  98. shellmate/ui/scenes/stages.py +128 -0
  99. shellmate/ui/scenes/status.py +181 -0
  100. shellmate/ui/status.py +3 -0
  101. shellmate_cli-3.0.0.dist-info/METADATA +352 -0
  102. shellmate_cli-3.0.0.dist-info/RECORD +106 -0
  103. shellmate_cli-3.0.0.dist-info/WHEEL +5 -0
  104. shellmate_cli-3.0.0.dist-info/entry_points.txt +3 -0
  105. shellmate_cli-3.0.0.dist-info/licenses/LICENSE +21 -0
  106. shellmate_cli-3.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,242 @@
1
+ import os
2
+ import shutil
3
+ from importlib.resources import as_file, files as _pkg_files
4
+ from pathlib import Path
5
+
6
+ import click
7
+ from shellmate.environment.detector import detect_environment
8
+
9
+
10
+ def _get_adapter_src(filename: str) -> Path:
11
+ """Return path to a bundled adapter file inside the installed package."""
12
+ ref = _pkg_files("shellmate").joinpath(f"adapters/{filename}")
13
+ # importlib.resources may return a non-Path on older Python — make it concrete
14
+ with as_file(ref) as p:
15
+ return Path(p)
16
+
17
+
18
+ def _get_shellmate_dir() -> Path:
19
+ """~/.shellmate on Unix, %USERPROFILE%\\.shellmate on Windows."""
20
+ return Path.home() / ".shellmate"
21
+
22
+
23
+ def _install_adapter(filename: str) -> Path:
24
+ """Copy adapter from package to ~/.shellmate/adapters/ and return dest path."""
25
+ dest_dir = _get_shellmate_dir() / "adapters"
26
+ dest_dir.mkdir(parents=True, exist_ok=True)
27
+ src = _get_adapter_src(filename)
28
+ dest = dest_dir / filename
29
+ shutil.copy2(src, dest)
30
+ dest.chmod(0o755)
31
+ return dest
32
+
33
+
34
+ def _adapter_filename_for_shell(shell: str):
35
+ """Unix shells use ble_adapter.sh; Windows uses powershell_adapter.ps1."""
36
+ if shell == "powershell":
37
+ return "powershell_adapter.ps1"
38
+ if shell in ("bash", "zsh"):
39
+ return "ble_adapter.sh"
40
+ return None
41
+
42
+
43
+ def _get_rc_file(shell):
44
+ home = os.path.expanduser("~")
45
+ rc_map = {
46
+ "bash": [os.path.join(home, ".bashrc"), os.path.join(home, ".bash_profile")],
47
+ "zsh": [os.path.join(home, ".zshrc")],
48
+ "powershell": [
49
+ os.path.join(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1"),
50
+ os.path.join(home, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1"),
51
+ ],
52
+ }
53
+ return rc_map.get(shell, [])
54
+
55
+
56
+ def _source_line(shell: str, filename: str) -> str:
57
+ if shell == "powershell":
58
+ return '. "$env:USERPROFILE\\.shellmate\\adapters\\powershell_adapter.ps1"\n'
59
+ return f'source "$HOME/.shellmate/adapters/{filename}"\n'
60
+
61
+
62
+ def _install_hook(shell):
63
+ adapter_name = _adapter_filename_for_shell(shell)
64
+ if not adapter_name:
65
+ return False, f"No adapter available for {shell}"
66
+
67
+ try:
68
+ dest_path = _install_adapter(adapter_name)
69
+ except (FileNotFoundError, OSError) as e:
70
+ return False, f"Adapter file not found: {e}"
71
+
72
+ rc_files = _get_rc_file(shell)
73
+ if not rc_files:
74
+ return False, f"No RC file known for {shell}"
75
+
76
+ source_line = _source_line(shell, adapter_name)
77
+
78
+ installed = False
79
+ for rc_file in rc_files:
80
+ if os.path.exists(rc_file):
81
+ with open(rc_file, "r") as f:
82
+ content = f.read()
83
+ if source_line.strip() in content:
84
+ installed = True # already present — idempotent success
85
+ continue
86
+ with open(rc_file, "a") as f:
87
+ f.write(f"\n# ShellMate hook\n{source_line}")
88
+ installed = True
89
+ else:
90
+ rc_dir = os.path.dirname(rc_file)
91
+ if rc_dir and not os.path.exists(rc_dir):
92
+ os.makedirs(rc_dir, exist_ok=True)
93
+ try:
94
+ with open(rc_file, "w") as f:
95
+ f.write(f"# ShellMate hook\n{source_line}")
96
+ installed = True
97
+ except OSError:
98
+ pass
99
+
100
+ if installed:
101
+ return True, f"Hook installed for {shell} → {dest_path}"
102
+ return False, f"Could not install hook for {shell}"
103
+
104
+
105
+ def _dry_run_adapter_plan(shell):
106
+ """Describe the copy that install would perform (for --dry-run)."""
107
+ adapter_name = _adapter_filename_for_shell(shell)
108
+ if not adapter_name:
109
+ return False, f"No adapter available for {shell}"
110
+ try:
111
+ src = _get_adapter_src(adapter_name)
112
+ except (FileNotFoundError, OSError, ModuleNotFoundError) as e:
113
+ return False, f"Adapter file not found: {e}"
114
+ dest = _get_shellmate_dir() / "adapters" / adapter_name
115
+ return True, f"Would copy {src} → {dest}"
116
+
117
+
118
+ def _prompt_credential_setup():
119
+ click.echo()
120
+ click.echo(click.style(" -------------------------------------------------", fg="cyan"))
121
+ click.echo(click.style(" ShellMate Credential Setup", bold=True, fg="cyan"))
122
+ click.echo(click.style(" -------------------------------------------------", fg="cyan"))
123
+ click.echo()
124
+ click.echo(" ShellMate needs API keys for AI-powered analysis and remediation.")
125
+ click.echo(" You can set them up now or skip and configure later with:")
126
+ click.echo(" " + click.style("shellmate configure", bold=True))
127
+ click.echo()
128
+
129
+ if not click.confirm(click.style(" Would you like to configure API keys now?", bold=True), default=True):
130
+ click.echo()
131
+ click.echo(" You can always run " + click.style("shellmate configure", bold=True) + " later.")
132
+ click.echo(" Until then, ShellMate will use deterministic rule-based analysis.")
133
+ return
134
+
135
+ from shellmate.cli.commands.configure import print_header, print_status, prompt_for_key
136
+ from shellmate.config.credentials import CREDENTIAL_KEYS, load_env, save_env
137
+
138
+ env = load_env()
139
+ print_header()
140
+ print_status()
141
+
142
+ click.echo()
143
+ for key_name in CREDENTIAL_KEYS:
144
+ current = env.get(key_name, os.environ.get(key_name, ""))
145
+ new_value = prompt_for_key(key_name, current)
146
+ env[key_name] = new_value
147
+
148
+ save_env(env)
149
+
150
+ click.echo()
151
+ click.echo(click.style(" [OK] Credentials saved to ~/.shellmate/.env", fg="green"))
152
+ click.echo()
153
+ click.echo(" " + click.style("Next step:", bold=True))
154
+ click.echo(" Run " + click.style("shellmate enable", bold=True) + " to start the daemon.")
155
+ click.echo()
156
+
157
+
158
+ @click.command()
159
+ @click.option("--dry-run", is_flag=True, help="Show what would be installed without making changes")
160
+ def install(dry_run):
161
+ """Install ShellMate shell hooks"""
162
+ from rich.console import Console
163
+ from rich.progress import Progress, SpinnerColumn, TextColumn
164
+ from rich.panel import Panel
165
+ import time
166
+ from shellmate.ui.runtime import tokens
167
+
168
+ console = Console()
169
+ env = detect_environment()
170
+ shell = env["shell"]
171
+ os_name = env["os"]
172
+ # Windows consoles often lack UTF-8; use ASCII marks (same pattern as daemon/server)
173
+ icon = tokens.icons(ascii_safe=(os.name == "nt" or console.no_color))
174
+ done = icon["done"]
175
+ fail = icon["error"]
176
+
177
+ console.print()
178
+ console.print(Panel(
179
+ f"[bold]ShellMate Install[/bold]\nSetting up hooks for [cyan]{os_name}[/cyan] / [cyan]{shell}[/cyan]",
180
+ border_style="cyan",
181
+ padding=(0, 2),
182
+ ))
183
+ console.print()
184
+
185
+ steps = [
186
+ ("Detecting shell environment", lambda: (True, f"Detected {shell} on {os_name}")),
187
+ ("Locating adapter file", lambda: (
188
+ (
189
+ True,
190
+ f"Adapter: {_adapter_filename_for_shell(shell)}"
191
+ ) if _adapter_filename_for_shell(shell) else (False, "not found")
192
+ )),
193
+ ("Installing hook", lambda: (
194
+ _dry_run_adapter_plan(shell) if dry_run else _install_hook(shell)
195
+ )),
196
+ ("Verifying shell config", lambda: (
197
+ True,
198
+ (
199
+ f'[DRY RUN] Would write: {_source_line(shell, _adapter_filename_for_shell(shell) or "").strip()}'
200
+ if dry_run
201
+ else "Source line written"
202
+ )
203
+ )),
204
+ ]
205
+
206
+ all_ok = True
207
+ with Progress(
208
+ SpinnerColumn(),
209
+ TextColumn("[progress.description]{task.description}"),
210
+ console=console,
211
+ transient=False,
212
+ ) as progress:
213
+ for label, action in steps:
214
+ task = progress.add_task(f"[cyan]{label}...[/cyan]", total=None)
215
+ time.sleep(0.05) # let spinner render at least one frame
216
+ try:
217
+ result = action()
218
+ ok = result[0] if isinstance(result, tuple) else bool(result)
219
+ detail = result[1] if isinstance(result, tuple) and len(result) > 1 else ""
220
+ if ok:
221
+ progress.update(task, description=f"[green]{done} {label}[/green] [dim]{detail}[/dim]", completed=1, total=1)
222
+ else:
223
+ progress.update(task, description=f"[red]{fail} {label}[/red] [dim]{detail}[/dim]", completed=1, total=1)
224
+ all_ok = False
225
+ except Exception as e:
226
+ progress.update(task, description=f"[red]{fail} {label}: {e}[/red]", completed=1, total=1)
227
+ all_ok = False
228
+
229
+ console.print()
230
+ if all_ok and not dry_run:
231
+ console.print("[green]Shell hooks installed.[/green] Restart your shell or run:")
232
+ if shell == "powershell":
233
+ console.print(" [bold]. $PROFILE[/bold]")
234
+ else:
235
+ console.print(f" [bold]source ~/{'.bashrc' if shell == 'bash' else '.zshrc'}[/bold]")
236
+ console.print()
237
+ console.print("[dim]Next step:[/dim] run [bold cyan]sm config[/bold cyan] to set up API keys")
238
+ _prompt_credential_setup()
239
+ elif dry_run:
240
+ console.print("[dim]Dry run complete -- no changes made.[/dim]")
241
+ else:
242
+ console.print("[red]Install incomplete -- check output above.[/red]")
@@ -0,0 +1,147 @@
1
+ """Migrate from deprecated interception hooks to autocomplete mode."""
2
+
3
+ import os
4
+
5
+ import click
6
+
7
+ from shellmate.cli.commands.init import (
8
+ _check_blesh_installed,
9
+ _confirm,
10
+ _echo,
11
+ _install_autocomplete_hook,
12
+ _install_blesh,
13
+ _install_powershell_hook,
14
+ _is_windows_target,
15
+ _powershell_profile_paths,
16
+ _profile_integration_ok,
17
+ )
18
+
19
+
20
+ @click.command(name="migrate-to-autocomplete")
21
+ def migrate():
22
+ """Migrate from deprecated interception mode to autocomplete mode"""
23
+
24
+ _echo("\nShellMate Migration Tool\n", bold=True, fg="cyan")
25
+ _echo("Interception mode (preexec hooks) has been removed.")
26
+ _echo("This migration sets up autocomplete mode for your shell.\n")
27
+
28
+ windows = _is_windows_target()
29
+ home = os.path.expanduser("~")
30
+ bashrc = os.path.join(home, ".bashrc")
31
+
32
+ has_old_bash = _has_legacy_bash_hooks(bashrc)
33
+ has_old_ps = _has_legacy_powershell_hooks()
34
+
35
+ if not has_old_bash and not has_old_ps:
36
+ _echo("[OK] No interception mode hooks found.")
37
+ if _profile_integration_ok():
38
+ _echo("[OK] Autocomplete mode already configured!")
39
+ return
40
+ _echo("[!] Autocomplete mode not configured. Run: sm init")
41
+ return
42
+
43
+ if has_old_bash:
44
+ _echo("Found old interception mode hooks in .bashrc")
45
+ if has_old_ps:
46
+ _echo("Found old ShellMate hooks in a PowerShell profile")
47
+
48
+ if not _confirm("Continue with migration?", default=True):
49
+ return
50
+
51
+ if windows:
52
+ if has_old_ps:
53
+ _echo("\nRemoving old PowerShell hooks...")
54
+ for path in _powershell_profile_paths():
55
+ if os.path.exists(path):
56
+ _remove_legacy_marker_sections(path, markers=("# ShellMate hook",))
57
+ _echo("Installing PowerShell autocomplete hooks...")
58
+ _install_powershell_hook()
59
+ else:
60
+ if not _check_blesh_installed():
61
+ _echo("\nble.sh is required for autocomplete mode.")
62
+ if _confirm("Install ble.sh now?", default=True):
63
+ if not _install_blesh():
64
+ _echo("Migration aborted.")
65
+ return
66
+ else:
67
+ _echo("Migration aborted.")
68
+ return
69
+
70
+ if has_old_bash:
71
+ _echo("\nRemoving old interception mode hooks from .bashrc...")
72
+ _remove_old_hooks(bashrc)
73
+
74
+ _echo("Installing autocomplete hooks...")
75
+ _install_autocomplete_hook()
76
+
77
+ _echo("\n[OK] Migration complete!", bold=True, fg="green")
78
+ _echo("\nWhat changed:")
79
+ _echo(" - Removed preexec-based interception hooks (if present)")
80
+ if windows:
81
+ _echo(" - Installed PowerShell adapter for real-time annotations")
82
+ else:
83
+ _echo(" - Installed ble.sh autocomplete integration")
84
+ _echo(" - Preserved FAISS semantic cache for fast lookups")
85
+ _echo("\nNext steps:")
86
+ if windows:
87
+ _echo(" 1. Restart PowerShell (or reload your profile)")
88
+ else:
89
+ _echo(" 1. Restart your shell (or run: source ~/.bashrc)")
90
+ _echo(" 2. Run: sm enable")
91
+ _echo(" 3. Start typing commands to see autocomplete!\n")
92
+
93
+
94
+ def _has_legacy_bash_hooks(bashrc_path: str) -> bool:
95
+ if not os.path.exists(bashrc_path):
96
+ return False
97
+ with open(bashrc_path, "r", encoding="utf-8", errors="replace") as f:
98
+ content = f.read()
99
+ return "bash_adapter.sh" in content and (
100
+ "preexec" in content or "# ShellMate hook" in content
101
+ )
102
+
103
+
104
+ def _has_legacy_powershell_hooks() -> bool:
105
+ for path in _powershell_profile_paths():
106
+ if not os.path.exists(path):
107
+ continue
108
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
109
+ content = f.read()
110
+ # Legacy install.py wrote "# ShellMate hook" without autocomplete marker
111
+ if "# ShellMate hook" in content and "ShellMate Autocomplete" not in content:
112
+ return True
113
+ if "powershell_adapter.ps1" in content and "ShellMate Autocomplete" not in content:
114
+ return True
115
+ return False
116
+
117
+
118
+ def _remove_legacy_marker_sections(path: str, markers=("# ShellMate hook",)):
119
+ """Remove simple one/two-line hook blocks starting with markers."""
120
+ with open(path, "r", encoding="utf-8", errors="replace") as f:
121
+ lines = f.readlines()
122
+
123
+ new_lines = []
124
+ skip_section = False
125
+ for line in lines:
126
+ if any(marker in line for marker in markers):
127
+ skip_section = True
128
+ continue
129
+ if skip_section:
130
+ stripped = line.strip()
131
+ if not stripped:
132
+ skip_section = False
133
+ continue
134
+ if stripped.startswith("source") or stripped.startswith("."):
135
+ skip_section = False
136
+ continue
137
+ skip_section = False
138
+ new_lines.append(line)
139
+
140
+ with open(path, "w", encoding="utf-8") as f:
141
+ f.writelines(new_lines)
142
+ _echo(f"[OK] Cleaned hooks in {path}")
143
+
144
+
145
+ def _remove_old_hooks(bashrc_path):
146
+ """Remove old ShellMate preexec hooks from .bashrc"""
147
+ _remove_legacy_marker_sections(bashrc_path, markers=("# ShellMate hook",))
@@ -0,0 +1,31 @@
1
+ from shellmate.ui.scenes.status import render_status
2
+
3
+ from shellmate.daemon.lifecycle import check_daemon_status
4
+ from shellmate.providers.router import get_provider_status
5
+ from shellmate.config.credentials import get_credential_status
6
+ from shellmate.config.defaults import get_default_config
7
+ from shellmate.ui.runtime.console_factory import ConsoleFactory
8
+
9
+
10
+ def status():
11
+ daemon_status = check_daemon_status()
12
+ provider_status = get_provider_status()
13
+ credential_status = get_credential_status()
14
+ config = get_default_config()
15
+
16
+ agent_status = None
17
+ if daemon_status.get("status") == "running":
18
+ # Optional: agents may be absent from lifecycle status
19
+ agent_status = daemon_status.get("agents")
20
+
21
+ factory = ConsoleFactory()
22
+ console = factory.create()
23
+ render_status(
24
+ daemon_status,
25
+ provider_status,
26
+ credential_status,
27
+ agent_status=agent_status,
28
+ config=config,
29
+ console=console,
30
+ plain=factory.is_plain(),
31
+ )
@@ -0,0 +1,78 @@
1
+ import os
2
+ import click
3
+ from shellmate.environment.detector import detect_environment
4
+
5
+ SHELLMATE_DIR = os.path.expanduser("~/.shellmate")
6
+ HOOKS_DIR = os.path.join(SHELLMATE_DIR, "hooks")
7
+
8
+
9
+ def _get_rc_file(shell):
10
+ home = os.path.expanduser("~")
11
+ rc_map = {
12
+ "bash": [os.path.join(home, ".bashrc"), os.path.join(home, ".bash_profile")],
13
+ "zsh": [os.path.join(home, ".zshrc")],
14
+ "powershell": [
15
+ os.path.join(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1"),
16
+ os.path.join(home, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1"),
17
+ ],
18
+ }
19
+ return rc_map.get(shell, [])
20
+
21
+
22
+ def _remove_hook(shell):
23
+ rc_files = _get_rc_file(shell)
24
+ removed = False
25
+
26
+ for rc_file in rc_files:
27
+ if not os.path.exists(rc_file):
28
+ continue
29
+ try:
30
+ with open(rc_file, "r") as f:
31
+ lines = f.readlines()
32
+
33
+ new_lines = []
34
+ skip_next = False
35
+ for line in lines:
36
+ if "# ShellMate hook" in line:
37
+ skip_next = True
38
+ removed = True
39
+ continue
40
+ if skip_next:
41
+ skip_next = False
42
+ removed = True
43
+ continue
44
+ if "shellmate" in line.lower() and ("source" in line or ". " in line):
45
+ if HOOKS_DIR in line:
46
+ removed = True
47
+ continue
48
+ new_lines.append(line)
49
+
50
+ with open(rc_file, "w") as f:
51
+ f.writelines(new_lines)
52
+ except OSError:
53
+ pass
54
+
55
+ return removed
56
+
57
+
58
+ @click.command()
59
+ @click.option("--dry-run", is_flag=True, help="Show what would be removed without making changes")
60
+ def uninstall(dry_run):
61
+ """Remove ShellMate and all hooks"""
62
+ env = detect_environment()
63
+ shell = env["shell"]
64
+
65
+ if dry_run:
66
+ rc_files = _get_rc_file(shell)
67
+ click.echo(f"[DRY RUN] Would remove ShellMate hooks for {shell}")
68
+ for rc in rc_files:
69
+ click.echo(f"[DRY RUN] Would remove hooks from: {rc}")
70
+ return
71
+
72
+ removed = _remove_hook(shell)
73
+ if removed:
74
+ click.echo("Shell hooks removed from RC files.")
75
+ else:
76
+ click.echo("No ShellMate hooks found in RC files.")
77
+
78
+ click.echo("ShellMate uninstalled.")
shellmate/cli/main.py ADDED
@@ -0,0 +1,114 @@
1
+ import sys
2
+ import click
3
+ from rich.console import Console
4
+ from rich.panel import Panel
5
+ from rich.text import Text
6
+
7
+ from shellmate.cli.parser import parse_argv, ParsedCommand, Action, ExitCode
8
+ from shellmate.cli.commands.enable import enable
9
+ from shellmate.cli.commands.disable import disable
10
+ from shellmate.cli.commands.status import status
11
+ from shellmate.cli.commands.config import config
12
+ from shellmate.cli.version import VERSION
13
+
14
+ console = Console()
15
+
16
+ _plain_console = Console(no_color=True)
17
+
18
+
19
+ def _get_console(parsed=None):
20
+ if parsed and getattr(parsed, 'no_color', False):
21
+ return _plain_console
22
+ return console
23
+
24
+
25
+ def print_error_panel(error: str, suggestion: str = None, next_steps: list = None, _console=None):
26
+ c = _console or console
27
+ text = Text()
28
+ text.append("Error: ", style="bold red")
29
+ text.append(error, style="red")
30
+ if suggestion:
31
+ text.append("\n\n")
32
+ text.append("Suggestion: ", style="bold yellow")
33
+ text.append(suggestion, style="yellow")
34
+ if next_steps:
35
+ text.append("\n\n")
36
+ text.append("Next steps:", style="bold cyan")
37
+ for i, step in enumerate(next_steps, 1):
38
+ text.append(f"\n {i}. {step}", style="cyan")
39
+ c.print(Panel(text, border_style="red", title="[bold red]ShellMate Error[/bold red]"))
40
+
41
+
42
+ def print_usage(c: Console = None):
43
+ from shellmate.ui.scenes.help import print_help
44
+ print_help(c or console)
45
+
46
+
47
+ def _safe_echo(message: str):
48
+ """click.echo that survives Windows consoles rejecting emoji/codepoints."""
49
+ try:
50
+ click.echo(message)
51
+ except UnicodeEncodeError:
52
+ click.echo(message.encode("ascii", errors="replace").decode("ascii"))
53
+
54
+
55
+ def main():
56
+ parsed = parse_argv(sys.argv[1:])
57
+ c = _get_console(parsed)
58
+
59
+ if parsed.exit_code != ExitCode.SUCCESS:
60
+ print_error_panel(parsed.error, parsed.suggestion, _console=c)
61
+ sys.exit(parsed.exit_code.value)
62
+
63
+ try:
64
+ if parsed.action == Action.ENABLE:
65
+ enable()
66
+ sys.exit(ExitCode.SUCCESS.value)
67
+
68
+ elif parsed.action == Action.DISABLE:
69
+ disable()
70
+ sys.exit(ExitCode.SUCCESS.value)
71
+
72
+ elif parsed.action == Action.STATUS:
73
+ status()
74
+ sys.exit(ExitCode.SUCCESS.value)
75
+
76
+ elif parsed.action == Action.CONFIG:
77
+ config()
78
+ sys.exit(ExitCode.SUCCESS.value)
79
+
80
+ elif parsed.action == Action.INSTALL:
81
+ from shellmate.cli.commands.install import install
82
+ # Click Command: invoke underlying callback, not Command.__call__
83
+ # Accept --dry-run before or after the subcommand
84
+ dry_run = parsed.dry_run or ("--dry-run" in (getattr(parsed, "tokens", None) or []))
85
+ install.callback(dry_run=dry_run)
86
+ sys.exit(ExitCode.SUCCESS.value)
87
+
88
+ elif parsed.action == Action.UNINSTALL:
89
+ from shellmate.cli.commands.uninstall import uninstall
90
+ dry_run = parsed.dry_run or ("--dry-run" in (getattr(parsed, "tokens", None) or []))
91
+ uninstall.callback(dry_run=dry_run)
92
+ sys.exit(ExitCode.SUCCESS.value)
93
+
94
+ elif parsed.action == Action.HELP:
95
+ print_usage(c)
96
+ sys.exit(ExitCode.SUCCESS.value)
97
+
98
+ elif parsed.action == Action.VERSION:
99
+ c.print(f"ShellMate v{VERSION}")
100
+ sys.exit(ExitCode.SUCCESS.value)
101
+
102
+ except KeyboardInterrupt:
103
+ c.print("\n[yellow]Interrupted[/]")
104
+ sys.exit(ExitCode.TEMP_ERROR.value)
105
+ except click.Abort:
106
+ c.print("\n[yellow]Aborted[/]")
107
+ sys.exit(ExitCode.TEMP_ERROR.value)
108
+ except Exception as e:
109
+ print_error_panel(f"Internal error: {e}", "Run 'sm --help' or check logs", _console=c)
110
+ sys.exit(ExitCode.INTERNAL_ERROR.value)
111
+
112
+
113
+ if __name__ == "__main__":
114
+ main()