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.
- shellmate/__init__.py +0 -0
- shellmate/adapters/__init__.py +67 -0
- shellmate/adapters/bash_adapter.sh +7 -0
- shellmate/adapters/ble_adapter.sh +350 -0
- shellmate/adapters/powershell_adapter.ps1 +417 -0
- shellmate/adapters/powershell_predictor.ps1 +97 -0
- shellmate/adapters/zsh_adapter.zsh +8 -0
- shellmate/agents/__init__.py +23 -0
- shellmate/agents/base.py +248 -0
- shellmate/agents/chain_patterns.yaml +98 -0
- shellmate/agents/command_chain.py +322 -0
- shellmate/agents/context_gatherer.py +175 -0
- shellmate/agents/environment.py +363 -0
- shellmate/agents/librarian.py +502 -0
- shellmate/agents/query.py +415 -0
- shellmate/agents/repomix_backend.py +171 -0
- shellmate/agents/watcher.py +266 -0
- shellmate/cache/__init__.py +0 -0
- shellmate/cache/semantic_cache.py +41 -0
- shellmate/cli/__init__.py +0 -0
- shellmate/cli/commands/__init__.py +27 -0
- shellmate/cli/commands/analyze.py +5 -0
- shellmate/cli/commands/config.py +319 -0
- shellmate/cli/commands/configure.py +118 -0
- shellmate/cli/commands/disable.py +11 -0
- shellmate/cli/commands/enable.py +49 -0
- shellmate/cli/commands/init.py +412 -0
- shellmate/cli/commands/install.py +242 -0
- shellmate/cli/commands/migrate.py +147 -0
- shellmate/cli/commands/status.py +31 -0
- shellmate/cli/commands/uninstall.py +78 -0
- shellmate/cli/main.py +114 -0
- shellmate/cli/parser.py +184 -0
- shellmate/cli/version.py +1 -0
- shellmate/completion/__init__.py +4 -0
- shellmate/completion/bash_adapter.py +145 -0
- shellmate/config/__init__.py +33 -0
- shellmate/config/credentials.py +120 -0
- shellmate/config/defaults.py +88 -0
- shellmate/config/manager.py +94 -0
- shellmate/config/paths.py +77 -0
- shellmate/config/secrets.py +66 -0
- shellmate/daemon/__init__.py +0 -0
- shellmate/daemon/lifecycle.py +308 -0
- shellmate/daemon/observability.py +82 -0
- shellmate/daemon/pipeline.py +119 -0
- shellmate/daemon/server.py +605 -0
- shellmate/daemon/session.py +170 -0
- shellmate/daemon/startup_ui.py +43 -0
- shellmate/environment/__init__.py +0 -0
- shellmate/environment/detector.py +130 -0
- shellmate/ipc/__init__.py +0 -0
- shellmate/ipc/client.py +210 -0
- shellmate/ipc/ps_client.py +190 -0
- shellmate/orchestration/__init__.py +3 -0
- shellmate/orchestration/analysis_graph.py +1274 -0
- shellmate/orchestration/state.py +35 -0
- shellmate/parser/__init__.py +0 -0
- shellmate/parser/command_parser.py +111 -0
- shellmate/plugins/__init__.py +0 -0
- shellmate/plugins/base.py +80 -0
- shellmate/plugins/loader.py +41 -0
- shellmate/providers/__init__.py +0 -0
- shellmate/providers/base.py +97 -0
- shellmate/providers/groq_command_guide.py +108 -0
- shellmate/providers/groq_provider.py +127 -0
- shellmate/providers/groq_supervisor.py +78 -0
- shellmate/providers/openrouter_provider.py +149 -0
- shellmate/providers/router.py +272 -0
- shellmate/providers/validate_keys.py +98 -0
- shellmate/providers/xai_provider.py +142 -0
- shellmate/rules/__init__.py +0 -0
- shellmate/rules/catastrophic.yaml +96 -0
- shellmate/rules/catastrophic_matcher.py +167 -0
- shellmate/rules/definitions/__init__.py +0 -0
- shellmate/rules/definitions/file_deletion.yaml +54 -0
- shellmate/rules/definitions/filesystem.yaml +43 -0
- shellmate/rules/definitions/network_execution.yaml +46 -0
- shellmate/rules/definitions/permissions.yaml +54 -0
- shellmate/rules/definitions/process_control.yaml +37 -0
- shellmate/rules/definitions/system_destruction.yaml +67 -0
- shellmate/rules/engine.py +200 -0
- shellmate/sanitizer/__init__.py +0 -0
- shellmate/sanitizer/typo_detector.py +114 -0
- shellmate/ui/__init__.py +10 -0
- shellmate/ui/analysis.py +164 -0
- shellmate/ui/annotations.py +232 -0
- shellmate/ui/banner.py +49 -0
- shellmate/ui/progress.py +126 -0
- shellmate/ui/runtime/__init__.py +15 -0
- shellmate/ui/runtime/console_factory.py +65 -0
- shellmate/ui/runtime/live_scene.py +116 -0
- shellmate/ui/runtime/motion_policy.py +69 -0
- shellmate/ui/runtime/tokens.py +47 -0
- shellmate/ui/scenes/__init__.py +24 -0
- shellmate/ui/scenes/enable.py +316 -0
- shellmate/ui/scenes/help.py +53 -0
- shellmate/ui/scenes/stages.py +128 -0
- shellmate/ui/scenes/status.py +181 -0
- shellmate/ui/status.py +3 -0
- shellmate_cli-3.0.0.dist-info/METADATA +352 -0
- shellmate_cli-3.0.0.dist-info/RECORD +106 -0
- shellmate_cli-3.0.0.dist-info/WHEEL +5 -0
- shellmate_cli-3.0.0.dist-info/entry_points.txt +3 -0
- shellmate_cli-3.0.0.dist-info/licenses/LICENSE +21 -0
- 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()
|