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,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