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,118 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import click
|
|
3
|
+
from shellmate.config.credentials import CREDENTIAL_KEYS, load_env, save_env, get_credential_status
|
|
4
|
+
from shellmate.config.paths import CREDENTIALS_FILE
|
|
5
|
+
from shellmate.environment.detector import detect_environment
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def print_header():
|
|
9
|
+
click.echo()
|
|
10
|
+
click.echo(click.style(" +================================================+", fg="cyan"))
|
|
11
|
+
click.echo(click.style(" | ShellMate Credential Setup |", fg="cyan"))
|
|
12
|
+
click.echo(click.style(" +================================================+", fg="cyan"))
|
|
13
|
+
click.echo()
|
|
14
|
+
click.echo(" Configure your API keys to enable AI-powered features.")
|
|
15
|
+
click.echo(" Keys are stored in: " + click.style(str(CREDENTIALS_FILE), bold=True))
|
|
16
|
+
click.echo()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def print_status():
|
|
20
|
+
status = get_credential_status()
|
|
21
|
+
click.echo(" Current status:")
|
|
22
|
+
for key, info in status.items():
|
|
23
|
+
if info["configured"]:
|
|
24
|
+
icon = click.style("[OK]", fg="green")
|
|
25
|
+
value = click.style(info["masked"], fg="green")
|
|
26
|
+
else:
|
|
27
|
+
icon = click.style("[--]", fg="red")
|
|
28
|
+
value = click.style("not set", fg="red", dim=True)
|
|
29
|
+
click.echo(f" {icon} {info['label']}: {value}")
|
|
30
|
+
click.echo()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def prompt_for_key(key_name, current_value):
|
|
34
|
+
info = CREDENTIAL_KEYS[key_name]
|
|
35
|
+
label = info["label"]
|
|
36
|
+
description = info["description"]
|
|
37
|
+
doc_url = info.get("doc_url", "")
|
|
38
|
+
|
|
39
|
+
click.echo(click.style(f" -- {label} --", bold=True))
|
|
40
|
+
click.echo(f" {description}")
|
|
41
|
+
if doc_url:
|
|
42
|
+
click.echo(f" Get it at: {click.style(doc_url, fg='cyan')}")
|
|
43
|
+
click.echo()
|
|
44
|
+
|
|
45
|
+
if current_value:
|
|
46
|
+
masked = current_value[:4] + "****" + current_value[-4:]
|
|
47
|
+
prompt_text = f" Enter key (leave empty to keep [{masked}]): "
|
|
48
|
+
else:
|
|
49
|
+
prompt_text = f" Enter key (leave empty to skip): "
|
|
50
|
+
|
|
51
|
+
new_value = click.prompt(prompt_text, default="", show_default=False)
|
|
52
|
+
new_value = new_value.strip()
|
|
53
|
+
|
|
54
|
+
if not new_value and current_value:
|
|
55
|
+
return current_value
|
|
56
|
+
if not new_value:
|
|
57
|
+
return ""
|
|
58
|
+
|
|
59
|
+
return new_value
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@click.command()
|
|
63
|
+
@click.option("--show", is_flag=True, help="Show current credential status without editing")
|
|
64
|
+
def configure(show):
|
|
65
|
+
"""Configure API credentials for AI providers"""
|
|
66
|
+
shell = detect_environment()["shell"]
|
|
67
|
+
|
|
68
|
+
if show:
|
|
69
|
+
print_header()
|
|
70
|
+
print_status()
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
env = load_env()
|
|
74
|
+
|
|
75
|
+
print_header()
|
|
76
|
+
print_status()
|
|
77
|
+
|
|
78
|
+
click.echo(" Enter your API keys below. You can skip any key by leaving it empty.")
|
|
79
|
+
click.echo()
|
|
80
|
+
|
|
81
|
+
for key_name in CREDENTIAL_KEYS:
|
|
82
|
+
current = env.get(key_name, os.environ.get(key_name, ""))
|
|
83
|
+
new_value = prompt_for_key(key_name, current)
|
|
84
|
+
env[key_name] = new_value
|
|
85
|
+
|
|
86
|
+
save_env(env)
|
|
87
|
+
|
|
88
|
+
click.echo()
|
|
89
|
+
click.echo(click.style(" ✓ Credentials saved!", fg="green"))
|
|
90
|
+
|
|
91
|
+
print_status()
|
|
92
|
+
|
|
93
|
+
has_keys = any(env.values())
|
|
94
|
+
if has_keys:
|
|
95
|
+
click.echo()
|
|
96
|
+
click.echo(" " + click.style("Next steps:", bold=True))
|
|
97
|
+
click.echo(" 1. Start the daemon: " + click.style("shellmate enable", bold=True))
|
|
98
|
+
click.echo(" 2. Use Tab completion for consequence preview")
|
|
99
|
+
|
|
100
|
+
missing = [k for k in CREDENTIAL_KEYS if not env.get(k)]
|
|
101
|
+
if missing:
|
|
102
|
+
click.echo()
|
|
103
|
+
click.echo(" " + click.style("Note:", bold=True) + " Some keys are still missing. AI analysis will use fallback mode.")
|
|
104
|
+
|
|
105
|
+
shell_source = {
|
|
106
|
+
"bash": "source ~/.shellmate/.env",
|
|
107
|
+
"zsh": "source ~/.shellmate/.env",
|
|
108
|
+
"powershell": ". ~\\.shellmate\\.env",
|
|
109
|
+
}
|
|
110
|
+
src_cmd = shell_source.get(shell, "source ~/.shellmate/.env")
|
|
111
|
+
if shell == "powershell":
|
|
112
|
+
src_cmd = ". $HOME\\.shellmate\\.env"
|
|
113
|
+
click.echo(f" 3. Load credentials: {click.style(src_cmd, bold=True)}")
|
|
114
|
+
click.echo(f" 4. Or restart your shell to pick up the .env file")
|
|
115
|
+
else:
|
|
116
|
+
click.echo()
|
|
117
|
+
click.echo(" " + click.style("No keys configured.", bold=True) + " AI features will use fallback mode.")
|
|
118
|
+
click.echo(" Run " + click.style("shellmate configure", bold=True) + " anytime to set them up.")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
|
|
3
|
+
from shellmate.daemon.lifecycle import stop_daemon
|
|
4
|
+
|
|
5
|
+
console = Console()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def disable():
|
|
9
|
+
console.print("[bold]Stopping ShellMate daemon...[/bold]")
|
|
10
|
+
stop_daemon()
|
|
11
|
+
console.print("[green]ShellMate has been disabled.[/green]")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from shellmate.daemon.lifecycle import start_daemon, print_provider_warnings
|
|
5
|
+
from shellmate.daemon.startup_ui import run_startup_ui
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def enable():
|
|
9
|
+
"""Start ShellMate daemon (autocomplete mode only)"""
|
|
10
|
+
|
|
11
|
+
click.echo("Starting daemon in autocomplete mode")
|
|
12
|
+
|
|
13
|
+
# Unix autocomplete surface needs ble.sh; Windows uses PowerShell adapter.
|
|
14
|
+
if os.name != "nt" and not _check_blesh_installed():
|
|
15
|
+
click.echo("[!] ble.sh not found. Autocomplete mode requires ble.sh.")
|
|
16
|
+
click.echo(" Run: sm init")
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
if os.name == "nt" and not _powershell_adapter_present():
|
|
20
|
+
click.echo("[!] PowerShell adapter not found.")
|
|
21
|
+
click.echo(" Run: sm init --skip-keys --install-hooks")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
# Spawn without waiting — Live scene polls stages as the daemon boots.
|
|
25
|
+
if start_daemon(wait_ready=False):
|
|
26
|
+
ok = run_startup_ui(timeout=30)
|
|
27
|
+
if ok:
|
|
28
|
+
# wait_ready=False skips start_daemon's own warning print; relay it here.
|
|
29
|
+
print_provider_warnings()
|
|
30
|
+
else:
|
|
31
|
+
click.echo("Daemon may still be starting. Run 'sm status' to check.")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _check_blesh_installed():
|
|
35
|
+
"""Check if ble.sh is already installed"""
|
|
36
|
+
blesh_paths = [
|
|
37
|
+
os.path.expanduser("~/.local/share/blesh/ble.sh"),
|
|
38
|
+
os.path.expanduser("~/ble.sh/out/ble.sh"),
|
|
39
|
+
]
|
|
40
|
+
return any(os.path.exists(p) for p in blesh_paths)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _powershell_adapter_present():
|
|
44
|
+
try:
|
|
45
|
+
from shellmate.cli.commands.init import _adapter_exists
|
|
46
|
+
|
|
47
|
+
return _adapter_exists()
|
|
48
|
+
except Exception:
|
|
49
|
+
return False
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import click
|
|
4
|
+
import subprocess
|
|
5
|
+
from shellmate.config.credentials import load_env, save_env, CREDENTIAL_KEYS
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _echo(message: str, **style_kwargs):
|
|
9
|
+
"""Echo text safely on Windows consoles that reject emoji codepoints."""
|
|
10
|
+
text = message if not style_kwargs else click.style(message, **style_kwargs)
|
|
11
|
+
try:
|
|
12
|
+
click.echo(text)
|
|
13
|
+
except UnicodeEncodeError:
|
|
14
|
+
click.echo(text.encode("ascii", errors="replace").decode("ascii"))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _confirm(text: str, default: bool = True) -> bool:
|
|
18
|
+
"""click.confirm that returns default when stdin is non-interactive."""
|
|
19
|
+
try:
|
|
20
|
+
return click.confirm(text, default=default)
|
|
21
|
+
except click.Abort:
|
|
22
|
+
return default
|
|
23
|
+
except EOFError:
|
|
24
|
+
return default
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_sm_path() -> str:
|
|
28
|
+
"""Resolve ShellMate package/repo root for adapter paths."""
|
|
29
|
+
try:
|
|
30
|
+
import shellmate
|
|
31
|
+
|
|
32
|
+
path = os.path.dirname(shellmate.__file__)
|
|
33
|
+
if os.path.isdir(os.path.join(path, "adapters")):
|
|
34
|
+
return path
|
|
35
|
+
except ImportError:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
# cli/commands/init.py -> commands -> cli -> repo root
|
|
39
|
+
here = os.path.abspath(__file__)
|
|
40
|
+
repo_root = os.path.dirname(os.path.dirname(os.path.dirname(here)))
|
|
41
|
+
if os.path.isdir(os.path.join(repo_root, "adapters")):
|
|
42
|
+
return repo_root
|
|
43
|
+
|
|
44
|
+
# Last resort: walk parents looking for adapters/
|
|
45
|
+
cur = os.path.dirname(here)
|
|
46
|
+
for _ in range(5):
|
|
47
|
+
if os.path.isdir(os.path.join(cur, "adapters")):
|
|
48
|
+
return cur
|
|
49
|
+
parent = os.path.dirname(cur)
|
|
50
|
+
if parent == cur:
|
|
51
|
+
break
|
|
52
|
+
cur = parent
|
|
53
|
+
return repo_root
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _powershell_profile_paths():
|
|
57
|
+
home = os.path.expanduser("~")
|
|
58
|
+
return [
|
|
59
|
+
os.path.join(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1"),
|
|
60
|
+
os.path.join(
|
|
61
|
+
home, "Documents", "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1"
|
|
62
|
+
),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_windows_target() -> bool:
|
|
67
|
+
if sys.platform == "win32":
|
|
68
|
+
return True
|
|
69
|
+
try:
|
|
70
|
+
from shellmate.environment.detector import detect_os, detect_shell
|
|
71
|
+
|
|
72
|
+
return detect_os() == "windows" or detect_shell() == "powershell"
|
|
73
|
+
except Exception:
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@click.command()
|
|
78
|
+
@click.option("--skip-keys", is_flag=True, help="Skip API key setup")
|
|
79
|
+
@click.option("--skip-blesh", is_flag=True, help="Skip ble.sh installation")
|
|
80
|
+
@click.option(
|
|
81
|
+
"--install-hooks",
|
|
82
|
+
is_flag=True,
|
|
83
|
+
help="Install shell hooks even with --skip-keys/--skip-blesh",
|
|
84
|
+
)
|
|
85
|
+
@click.option(
|
|
86
|
+
"--validate",
|
|
87
|
+
"validate_only",
|
|
88
|
+
is_flag=True,
|
|
89
|
+
help="Only run setup validation checks",
|
|
90
|
+
)
|
|
91
|
+
def init(skip_keys, skip_blesh, install_hooks, validate_only):
|
|
92
|
+
"""First-run setup wizard for ShellMate autocomplete"""
|
|
93
|
+
|
|
94
|
+
if validate_only:
|
|
95
|
+
_validate_setup()
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
_echo("\nShellMate Autocomplete Setup\n", bold=True, fg="cyan")
|
|
99
|
+
|
|
100
|
+
windows = _is_windows_target()
|
|
101
|
+
|
|
102
|
+
# Step 1: Check ble.sh (Unix/WSL autocomplete surface)
|
|
103
|
+
if windows:
|
|
104
|
+
if not skip_blesh:
|
|
105
|
+
_echo("Windows detected: PowerShell adapter will be used (ble.sh skipped).")
|
|
106
|
+
elif not skip_blesh:
|
|
107
|
+
if not _check_blesh_installed():
|
|
108
|
+
_echo("ble.sh not found. Installing...")
|
|
109
|
+
if _confirm("Install ble.sh now?", default=True):
|
|
110
|
+
_install_blesh()
|
|
111
|
+
else:
|
|
112
|
+
_echo("[!] ble.sh required for autocomplete mode.")
|
|
113
|
+
_echo("Install manually: https://github.com/akinomyoga/ble.sh")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# Step 2: API keys
|
|
117
|
+
if not skip_keys:
|
|
118
|
+
_setup_api_keys()
|
|
119
|
+
|
|
120
|
+
# Step 3: Install platform hook
|
|
121
|
+
# Smoke path (--skip-keys --skip-blesh) avoids mutating shell RC files
|
|
122
|
+
# unless --install-hooks is set (so --validate can pass after a dry init).
|
|
123
|
+
smoke_skip_hooks = skip_keys and skip_blesh and not install_hooks
|
|
124
|
+
if smoke_skip_hooks:
|
|
125
|
+
_echo("Skipping shell hook install (--skip-keys --skip-blesh).")
|
|
126
|
+
_echo(" Tip: re-run with --install-hooks to wire your shell profile.")
|
|
127
|
+
elif windows:
|
|
128
|
+
_install_powershell_hook()
|
|
129
|
+
elif _check_blesh_installed():
|
|
130
|
+
_install_autocomplete_hook()
|
|
131
|
+
else:
|
|
132
|
+
_echo("[!] Skipping autocomplete hook (ble.sh not installed).")
|
|
133
|
+
|
|
134
|
+
# Step 4: Validate setup
|
|
135
|
+
if smoke_skip_hooks:
|
|
136
|
+
_echo("Skipping interactive validation (--skip-keys --skip-blesh).")
|
|
137
|
+
elif install_hooks or skip_keys:
|
|
138
|
+
# Non-interactive polish path: always validate after hook install
|
|
139
|
+
_validate_setup()
|
|
140
|
+
elif _confirm("\nRun validation check?", default=True):
|
|
141
|
+
_validate_setup()
|
|
142
|
+
|
|
143
|
+
_echo("\n[OK] Setup complete!", bold=True, fg="green")
|
|
144
|
+
_echo("\nNext steps:")
|
|
145
|
+
if windows:
|
|
146
|
+
_echo(" 1. Restart PowerShell (or reload your profile)")
|
|
147
|
+
_echo(" 2. Run: shellmate enable")
|
|
148
|
+
_echo(" 3. Start typing commands to see annotations!\n")
|
|
149
|
+
else:
|
|
150
|
+
_echo(" 1. Restart your shell (or run: source ~/.bashrc)")
|
|
151
|
+
_echo(" 2. Run: shellmate enable")
|
|
152
|
+
_echo(" 3. Start typing commands to see autocomplete!\n")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _check_blesh_installed():
|
|
156
|
+
"""Check if ble.sh is already installed"""
|
|
157
|
+
blesh_paths = [
|
|
158
|
+
os.path.expanduser("~/.local/share/blesh/ble.sh"),
|
|
159
|
+
os.path.expanduser("~/ble.sh/out/ble.sh"),
|
|
160
|
+
]
|
|
161
|
+
return any(os.path.exists(p) for p in blesh_paths)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _install_blesh():
|
|
165
|
+
"""Auto-install ble.sh to ~/.local (requires bash + make; typically Linux/WSL)."""
|
|
166
|
+
import shutil
|
|
167
|
+
|
|
168
|
+
home = os.path.expanduser("~")
|
|
169
|
+
clone_dir = os.path.join(home, ".shellmate", "ble.sh")
|
|
170
|
+
make_bin = shutil.which("make")
|
|
171
|
+
|
|
172
|
+
# Windows/MINGW often has no make on PATH — fail early with guidance
|
|
173
|
+
if not make_bin and sys.platform == "win32":
|
|
174
|
+
_echo("[X] Cannot build ble.sh: 'make' was not found on PATH.", fg="red")
|
|
175
|
+
_echo("")
|
|
176
|
+
_echo("ble.sh autocomplete needs a Unix build toolchain.")
|
|
177
|
+
_echo("On this Windows machine you can:")
|
|
178
|
+
_echo(" 1. Use ShellMate PowerShell adapter (recommended here):")
|
|
179
|
+
_echo(" sm init --skip-blesh")
|
|
180
|
+
_echo(" sm enable")
|
|
181
|
+
_echo(" 2. Install make (MSYS2 / Git for Windows SDK) and re-run: sm init")
|
|
182
|
+
_echo(" 3. Use WSL/Linux bash for full ble.sh autocomplete:")
|
|
183
|
+
_echo(" wsl")
|
|
184
|
+
_echo(" cd /mnt/d/projects/shellmate && sm init && sm enable")
|
|
185
|
+
_echo("")
|
|
186
|
+
if os.path.isdir(clone_dir):
|
|
187
|
+
_echo(f"(ble.sh sources already cloned at {clone_dir})")
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
if not os.path.exists(clone_dir):
|
|
192
|
+
_echo("Cloning ble.sh...")
|
|
193
|
+
subprocess.run(
|
|
194
|
+
[
|
|
195
|
+
"git",
|
|
196
|
+
"clone",
|
|
197
|
+
"--recursive",
|
|
198
|
+
"https://github.com/akinomyoga/ble.sh.git",
|
|
199
|
+
clone_dir,
|
|
200
|
+
],
|
|
201
|
+
check=True,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
_echo("Building ble.sh...")
|
|
205
|
+
subprocess.run(
|
|
206
|
+
[make_bin or "make", "install", f"PREFIX={home}/.local"],
|
|
207
|
+
cwd=clone_dir,
|
|
208
|
+
check=True,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
_echo("[OK] ble.sh installed", fg="green")
|
|
212
|
+
return True
|
|
213
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
214
|
+
_echo(f"[X] Installation failed: {e}", fg="red")
|
|
215
|
+
_echo("\nManual installation (Linux/WSL/Git Bash with make):")
|
|
216
|
+
_echo(" git clone --recursive https://github.com/akinomyoga/ble.sh.git")
|
|
217
|
+
_echo(" cd ble.sh && make install PREFIX=~/.local")
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _setup_api_keys():
|
|
222
|
+
"""Interactive API key setup"""
|
|
223
|
+
_echo("\nAPI Key Setup")
|
|
224
|
+
_echo(
|
|
225
|
+
"ShellMate uses AI for risk analysis. Configure keys or skip for rule-based mode.\n"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if not _confirm("Configure API keys now?", default=True):
|
|
229
|
+
_echo("Skipped. Run 'shellmate configure' later.")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
env = load_env()
|
|
233
|
+
for key_name in CREDENTIAL_KEYS:
|
|
234
|
+
current = env.get(key_name, "")
|
|
235
|
+
if current:
|
|
236
|
+
_echo(f"{key_name}: [already set]")
|
|
237
|
+
if not _confirm("Update?", default=False):
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
value = click.prompt(f"Enter {key_name}", default="", show_default=False)
|
|
242
|
+
except (click.Abort, EOFError):
|
|
243
|
+
value = ""
|
|
244
|
+
if value:
|
|
245
|
+
env[key_name] = value
|
|
246
|
+
|
|
247
|
+
save_env(env)
|
|
248
|
+
_echo("[OK] Keys saved to ~/.shellmate/.env", fg="green")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _install_autocomplete_hook():
|
|
252
|
+
"""Add ble.sh + ShellMate to .bashrc (Unix autocomplete surface)."""
|
|
253
|
+
home = os.path.expanduser("~")
|
|
254
|
+
bashrc = os.path.join(home, ".bashrc")
|
|
255
|
+
|
|
256
|
+
# Detect ble.sh location
|
|
257
|
+
blesh_path = None
|
|
258
|
+
for path in [
|
|
259
|
+
os.path.join(home, ".local/share/blesh/ble.sh"),
|
|
260
|
+
os.path.join(home, "ble.sh/out/ble.sh"),
|
|
261
|
+
]:
|
|
262
|
+
if os.path.exists(path):
|
|
263
|
+
blesh_path = path
|
|
264
|
+
break
|
|
265
|
+
|
|
266
|
+
if not blesh_path:
|
|
267
|
+
_echo("[X] ble.sh not found after installation")
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
sm_path = _get_sm_path()
|
|
271
|
+
adapter_path = os.path.join(sm_path, "adapters", "ble_adapter.sh")
|
|
272
|
+
|
|
273
|
+
if not os.path.exists(adapter_path):
|
|
274
|
+
_echo(f"[X] Adapter not found at {adapter_path}")
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
snippet = f'''
|
|
278
|
+
# ShellMate Autocomplete Integration
|
|
279
|
+
if [[ $- == *i* ]]; then
|
|
280
|
+
# Ensure ShellMate is in PYTHONPATH
|
|
281
|
+
export PYTHONPATH="{sm_path}:$PYTHONPATH"
|
|
282
|
+
|
|
283
|
+
source {blesh_path}
|
|
284
|
+
[[ -f {adapter_path} ]] && source {adapter_path}
|
|
285
|
+
fi
|
|
286
|
+
'''
|
|
287
|
+
|
|
288
|
+
if os.path.exists(bashrc):
|
|
289
|
+
with open(bashrc, "r") as f:
|
|
290
|
+
content = f.read()
|
|
291
|
+
if "ShellMate Autocomplete" in content:
|
|
292
|
+
_echo("[OK] Already integrated in .bashrc")
|
|
293
|
+
return True
|
|
294
|
+
|
|
295
|
+
with open(bashrc, "a") as f:
|
|
296
|
+
f.write(snippet)
|
|
297
|
+
|
|
298
|
+
_echo(f"[OK] Added to {bashrc}", fg="green")
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _install_powershell_hook():
|
|
303
|
+
"""Add ShellMate PowerShell adapter to the user profile."""
|
|
304
|
+
sm_path = _get_sm_path()
|
|
305
|
+
adapter_path = os.path.join(sm_path, "adapters", "powershell_adapter.ps1")
|
|
306
|
+
|
|
307
|
+
if not os.path.exists(adapter_path):
|
|
308
|
+
_echo(f"[X] Adapter not found at {adapter_path}")
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
# Prefer forward-slash style that PowerShell accepts; keep absolute path.
|
|
312
|
+
adapter_ps = adapter_path.replace("'", "''")
|
|
313
|
+
snippet = (
|
|
314
|
+
"\n# ShellMate Autocomplete Integration\n"
|
|
315
|
+
f"if (Test-Path '{adapter_ps}') {{ . '{adapter_ps}' }}\n"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
installed = False
|
|
319
|
+
for profile_path in _powershell_profile_paths():
|
|
320
|
+
profile_dir = os.path.dirname(profile_path)
|
|
321
|
+
if profile_dir and not os.path.exists(profile_dir):
|
|
322
|
+
try:
|
|
323
|
+
os.makedirs(profile_dir, exist_ok=True)
|
|
324
|
+
except OSError as e:
|
|
325
|
+
_echo(f"[!] Could not create {profile_dir}: {e}")
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
if os.path.exists(profile_path):
|
|
329
|
+
with open(profile_path, "r", encoding="utf-8", errors="replace") as f:
|
|
330
|
+
content = f.read()
|
|
331
|
+
if "ShellMate Autocomplete" in content:
|
|
332
|
+
_echo(f"[OK] Already integrated in {profile_path}")
|
|
333
|
+
installed = True
|
|
334
|
+
continue
|
|
335
|
+
with open(profile_path, "a", encoding="utf-8") as f:
|
|
336
|
+
f.write(snippet)
|
|
337
|
+
else:
|
|
338
|
+
try:
|
|
339
|
+
with open(profile_path, "w", encoding="utf-8") as f:
|
|
340
|
+
f.write(snippet.lstrip("\n"))
|
|
341
|
+
except OSError as e:
|
|
342
|
+
_echo(f"[!] Could not write {profile_path}: {e}")
|
|
343
|
+
continue
|
|
344
|
+
|
|
345
|
+
_echo(f"[OK] Added to {profile_path}", fg="green")
|
|
346
|
+
installed = True
|
|
347
|
+
|
|
348
|
+
if not installed:
|
|
349
|
+
_echo("[X] Could not install PowerShell profile hook")
|
|
350
|
+
return False
|
|
351
|
+
return True
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _profile_integration_ok() -> bool:
|
|
355
|
+
"""True if bashrc or a PowerShell profile already has ShellMate Autocomplete."""
|
|
356
|
+
bashrc = os.path.expanduser("~/.bashrc")
|
|
357
|
+
if os.path.exists(bashrc):
|
|
358
|
+
with open(bashrc, "r", encoding="utf-8", errors="replace") as f:
|
|
359
|
+
if "ShellMate Autocomplete" in f.read():
|
|
360
|
+
return True
|
|
361
|
+
|
|
362
|
+
for profile_path in _powershell_profile_paths():
|
|
363
|
+
if os.path.exists(profile_path):
|
|
364
|
+
with open(profile_path, "r", encoding="utf-8", errors="replace") as f:
|
|
365
|
+
if "ShellMate Autocomplete" in f.read():
|
|
366
|
+
return True
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _adapter_exists() -> bool:
|
|
371
|
+
sm_path = _get_sm_path()
|
|
372
|
+
ble = os.path.join(sm_path, "adapters", "ble_adapter.sh")
|
|
373
|
+
ps = os.path.join(sm_path, "adapters", "powershell_adapter.ps1")
|
|
374
|
+
if _is_windows_target():
|
|
375
|
+
return os.path.exists(ps)
|
|
376
|
+
return os.path.exists(ble) or os.path.exists(ps)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _validate_setup():
|
|
380
|
+
"""Validate ShellMate configuration. Returns True if all checks passed."""
|
|
381
|
+
_echo("\nValidating setup...")
|
|
382
|
+
|
|
383
|
+
checks = []
|
|
384
|
+
windows = _is_windows_target()
|
|
385
|
+
|
|
386
|
+
# Check 1: ble.sh (required on Unix autocomplete; informational on Windows)
|
|
387
|
+
if windows:
|
|
388
|
+
checks.append(("ble.sh installed (optional on Windows)", True))
|
|
389
|
+
else:
|
|
390
|
+
checks.append(("ble.sh installed", _check_blesh_installed()))
|
|
391
|
+
|
|
392
|
+
# Check 2: shell RC integration
|
|
393
|
+
checks.append(("shell profile integration", _profile_integration_ok()))
|
|
394
|
+
|
|
395
|
+
# Check 3: API keys configured
|
|
396
|
+
env = load_env()
|
|
397
|
+
has_keys = any(env.get(k) for k in CREDENTIAL_KEYS)
|
|
398
|
+
checks.append(("API keys configured", has_keys))
|
|
399
|
+
|
|
400
|
+
# Check 4: Adapter file exists
|
|
401
|
+
checks.append(("Adapter file exists", _adapter_exists()))
|
|
402
|
+
|
|
403
|
+
for check_name, passed in checks:
|
|
404
|
+
icon = "[OK]" if passed else "[X]"
|
|
405
|
+
_echo(f" {icon} {check_name}")
|
|
406
|
+
|
|
407
|
+
all_passed = all(c[1] for c in checks)
|
|
408
|
+
if all_passed:
|
|
409
|
+
_echo("\n[OK] All checks passed!", fg="green")
|
|
410
|
+
else:
|
|
411
|
+
_echo("\n[!] Some checks failed", fg="yellow")
|
|
412
|
+
return all_passed
|