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
shellmate/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
ADAPTERS_DIR = Path(__file__).parent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_adapter_path(shell: str) -> Optional[Path]:
|
|
11
|
+
mapping = {
|
|
12
|
+
"bash": ADAPTERS_DIR / "bash_adapter.sh",
|
|
13
|
+
"zsh": ADAPTERS_DIR / "zsh_adapter.zsh",
|
|
14
|
+
"powershell": ADAPTERS_DIR / "powershell_adapter.ps1",
|
|
15
|
+
}
|
|
16
|
+
path = mapping.get(shell)
|
|
17
|
+
return path if path and path.exists() else None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_supported_shells() -> list[str]:
|
|
21
|
+
return ["bash", "zsh", "powershell"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def check_shell_available(shell: str) -> bool:
|
|
25
|
+
if shell == "powershell":
|
|
26
|
+
return sys.platform == "win32"
|
|
27
|
+
return shutil.which(shell) is not None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_adapter_syntax(shell: str) -> tuple[bool, str]:
|
|
31
|
+
path = get_adapter_path(shell)
|
|
32
|
+
if not path:
|
|
33
|
+
return False, f"Adapter not found for {shell}"
|
|
34
|
+
|
|
35
|
+
content = path.read_text(encoding="utf-8")
|
|
36
|
+
|
|
37
|
+
if shell == "powershell":
|
|
38
|
+
try:
|
|
39
|
+
result = subprocess.run(
|
|
40
|
+
["powershell", "-NoProfile", "-Command",
|
|
41
|
+
f"$errors = $null; [System.Management.Automation.PSParser]::Tokenize((Get-Content -Raw '{path}'), [ref]$null, [ref]$errors); if ($errors) {{ $errors | ForEach-Object {{ $_.Message }} }} else {{ 'OK' }}"],
|
|
42
|
+
capture_output=True, text=True, timeout=15
|
|
43
|
+
)
|
|
44
|
+
output = result.stdout.strip()
|
|
45
|
+
if output == "OK":
|
|
46
|
+
return True, "Syntax OK"
|
|
47
|
+
return False, f"Syntax errors: {output}"
|
|
48
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
|
49
|
+
return False, f"Cannot validate: {e}"
|
|
50
|
+
|
|
51
|
+
if shell in ("bash", "zsh"):
|
|
52
|
+
interpreter = shutil.which(shell)
|
|
53
|
+
if not interpreter:
|
|
54
|
+
return False, f"{shell} not available on this system"
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
result = subprocess.run(
|
|
58
|
+
[interpreter, "-n", str(path)],
|
|
59
|
+
capture_output=True, text=True, timeout=15
|
|
60
|
+
)
|
|
61
|
+
if result.returncode == 0:
|
|
62
|
+
return True, "Syntax OK"
|
|
63
|
+
return False, f"Syntax errors: {result.stderr.strip()}"
|
|
64
|
+
except (subprocess.TimeoutExpired, OSError) as e:
|
|
65
|
+
return False, f"Cannot validate: {e}"
|
|
66
|
+
|
|
67
|
+
return False, f"Unsupported shell: {shell}"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ShellMate Bash Adapter - Autocomplete Mode Only
|
|
3
|
+
# This file is deprecated - use ble_adapter.sh for autocomplete mode
|
|
4
|
+
# Kept for backward compatibility only
|
|
5
|
+
|
|
6
|
+
echo "⚠️ ShellMate: bash_adapter.sh is deprecated. Use ble_adapter.sh for autocomplete mode."
|
|
7
|
+
echo " Run: sm migrate-to-autocomplete"
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ShellMate ble.sh adapter — two-phase autocomplete annotations
|
|
3
|
+
#
|
|
4
|
+
# Phase 1 (instant): catastrophic matcher → placeholder annotation
|
|
5
|
+
# Phase 2 (debounced 300ms): /analyze-partial → replace annotation in-place
|
|
6
|
+
# Tab×2: accept completion → consequence preview below prompt
|
|
7
|
+
|
|
8
|
+
# Global state
|
|
9
|
+
_SHELLMATE_NEEDS_CONFIRM=""
|
|
10
|
+
_SHELLMATE_LAST_DANGEROUS=""
|
|
11
|
+
_SHELLMATE_LAST_ANNOTATION=""
|
|
12
|
+
_SHELLMATE_DEBOUNCE_PID=""
|
|
13
|
+
_SHELLMATE_ROOT=""
|
|
14
|
+
_SHELLMATE_TAB_STATE="" # "" | "suggested" | "consequence_shown"
|
|
15
|
+
_SHELLMATE_TAB_CMD="" # full command string after completion accepted
|
|
16
|
+
_SHELLMATE_CONSEQUENCE_REQUIRE_CONFIRM="" # cmd waiting for second Enter
|
|
17
|
+
_SHELLMATE_CONSEQUENCE_CONFIRM_ARMED="" # set after first Enter blocked
|
|
18
|
+
|
|
19
|
+
_shellmate_resolve_root() {
|
|
20
|
+
if [[ -n "$_SHELLMATE_ROOT" ]]; then
|
|
21
|
+
echo "$_SHELLMATE_ROOT"
|
|
22
|
+
return
|
|
23
|
+
fi
|
|
24
|
+
local root
|
|
25
|
+
root=$(python3 -c "import shellmate, os; print(os.path.dirname(shellmate.__file__))" 2>/dev/null)
|
|
26
|
+
if [[ -z "$root" ]]; then
|
|
27
|
+
# Fallback: adapter lives in <repo>/adapters/
|
|
28
|
+
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
29
|
+
fi
|
|
30
|
+
_SHELLMATE_ROOT="$root"
|
|
31
|
+
echo "$root"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_shellmate_render_annotation() {
|
|
35
|
+
# Render annotation below command line without moving cursor permanently.
|
|
36
|
+
# Uses ble.sh canvas when available; falls back to ANSI save/restore.
|
|
37
|
+
local text="$1"
|
|
38
|
+
_SHELLMATE_LAST_ANNOTATION="$text"
|
|
39
|
+
if type ble/canvas/put.draw &>/dev/null; then
|
|
40
|
+
ble/canvas/put.draw "\e[91m${text}\e[m"
|
|
41
|
+
else
|
|
42
|
+
# ANSI: save cursor, move down, clear line, write, restore
|
|
43
|
+
printf '\e[s\e[1B\e[2K%s\e[u' "$text" >/dev/tty 2>/dev/null || true
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_shellmate_clear_annotation() {
|
|
48
|
+
_SHELLMATE_LAST_ANNOTATION=""
|
|
49
|
+
if type ble/canvas/put.draw &>/dev/null; then
|
|
50
|
+
ble/canvas/put.draw ""
|
|
51
|
+
else
|
|
52
|
+
printf '\e[s\e[1B\e[2K\e[u' >/dev/tty 2>/dev/null || true
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_shellmate_render_consequence() {
|
|
57
|
+
# Render up to 3 consequence lines below the prompt using ANSI save/restore.
|
|
58
|
+
local line1="$1" line2="$2" line3="${3:-}"
|
|
59
|
+
if type ble/canvas/put.draw &>/dev/null; then
|
|
60
|
+
ble/canvas/put.draw "${line1}"
|
|
61
|
+
else
|
|
62
|
+
printf '\e[s\e[1B\e[2K%b' "${line1}" >/dev/tty 2>/dev/null || true
|
|
63
|
+
printf '\e[2B\e[2K%b' "${line2}" >/dev/tty 2>/dev/null || true
|
|
64
|
+
if [[ -n "$line3" ]]; then
|
|
65
|
+
printf '\e[3B\e[2K%b' "${line3}" >/dev/tty 2>/dev/null || true
|
|
66
|
+
fi
|
|
67
|
+
printf '\e[u' >/dev/tty 2>/dev/null || true
|
|
68
|
+
fi
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_shellmate_show_consequence_async() {
|
|
72
|
+
local cmd="$1"
|
|
73
|
+
# Show placeholder immediately
|
|
74
|
+
_shellmate_render_consequence "\e[90m ⚙ Analyzing…\e[m" "" ""
|
|
75
|
+
_shellmate_cancel_debounce
|
|
76
|
+
|
|
77
|
+
(
|
|
78
|
+
local sm_path
|
|
79
|
+
sm_path=$(_shellmate_resolve_root)
|
|
80
|
+
local raw
|
|
81
|
+
raw=$(timeout 1.5 python3 -c "
|
|
82
|
+
import sys, json, threading
|
|
83
|
+
sys.path.insert(0, r'''$sm_path''')
|
|
84
|
+
from ipc.client import analyze_partial_debounced
|
|
85
|
+
holder = {}
|
|
86
|
+
done = threading.Event()
|
|
87
|
+
def _cb(r): holder['r'] = r; done.set()
|
|
88
|
+
analyze_partial_debounced(sys.argv[1], _cb)
|
|
89
|
+
done.wait(timeout=1.2)
|
|
90
|
+
result = holder.get('r') or {}
|
|
91
|
+
cp = result.get('consequence_preview') or {}
|
|
92
|
+
print(json.dumps({
|
|
93
|
+
'what_happens': cp.get('what_happens', ''),
|
|
94
|
+
'scope': cp.get('scope', ''),
|
|
95
|
+
'reversible': cp.get('reversible', True),
|
|
96
|
+
'suggested_fix': cp.get('suggested_fix', ''),
|
|
97
|
+
'severity': result.get('severity', 'MEDIUM'),
|
|
98
|
+
}))
|
|
99
|
+
" "$cmd" 2>/dev/null)
|
|
100
|
+
|
|
101
|
+
if [[ -z "$raw" ]]; then return; fi
|
|
102
|
+
|
|
103
|
+
local what_happens scope reversible suggested_fix severity
|
|
104
|
+
what_happens=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print(d.get('what_happens',''))" "$raw" 2>/dev/null)
|
|
105
|
+
scope=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print(d.get('scope',''))" "$raw" 2>/dev/null)
|
|
106
|
+
reversible=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print('No' if not d.get('reversible',True) else 'Yes')" "$raw" 2>/dev/null)
|
|
107
|
+
suggested_fix=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print(d.get('suggested_fix',''))" "$raw" 2>/dev/null)
|
|
108
|
+
severity=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print(d.get('severity','MEDIUM'))" "$raw" 2>/dev/null)
|
|
109
|
+
|
|
110
|
+
local color="\e[93m"
|
|
111
|
+
[[ "$severity" == "HIGH" || "$severity" == "CRITICAL" || "$severity" == "blocking" ]] && color="\e[91m"
|
|
112
|
+
[[ "$severity" == "SAFE" || "$severity" == "LOW" ]] && color="\e[92m"
|
|
113
|
+
|
|
114
|
+
local line1 line2 line3=""
|
|
115
|
+
line1="${color} → ${what_happens}\e[m"
|
|
116
|
+
line2="${color} → Reversible: ${reversible} | ${scope}\e[m"
|
|
117
|
+
[[ -n "$suggested_fix" ]] && line3="\e[90m → Safe alt: ${suggested_fix}\e[m"
|
|
118
|
+
|
|
119
|
+
# Gate HIGH/CRITICAL on second Enter
|
|
120
|
+
if [[ "$severity" == "HIGH" || "$severity" == "CRITICAL" || "$severity" == "blocking" ]]; then
|
|
121
|
+
_SHELLMATE_CONSEQUENCE_REQUIRE_CONFIRM="$cmd"
|
|
122
|
+
line2="${line2} \e[91m[Press Enter again to execute]\e[m"
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
_shellmate_render_consequence "$line1" "$line2" "$line3"
|
|
126
|
+
) &
|
|
127
|
+
_SHELLMATE_DEBOUNCE_PID=$!
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
sm_instant_check() {
|
|
131
|
+
# Returns 0 if catastrophic match, 1 otherwise. Prints JSON to stdout.
|
|
132
|
+
local cmd="$1"
|
|
133
|
+
local sm_path
|
|
134
|
+
sm_path=$(_shellmate_resolve_root)
|
|
135
|
+
[[ -z "$sm_path" ]] && return 1
|
|
136
|
+
|
|
137
|
+
local result
|
|
138
|
+
result=$(python3 -c "
|
|
139
|
+
import sys, json
|
|
140
|
+
sys.path.insert(0, r'''$sm_path''')
|
|
141
|
+
from rules.catastrophic_matcher import match_catastrophic
|
|
142
|
+
cmd = sys.argv[1]
|
|
143
|
+
print(json.dumps(match_catastrophic(cmd)))
|
|
144
|
+
" "$cmd" 2>/dev/null)
|
|
145
|
+
|
|
146
|
+
if [[ "$result" == *'"matched": true'* ]] || [[ "$result" == *'"matched":true'* ]]; then
|
|
147
|
+
echo "$result"
|
|
148
|
+
return 0
|
|
149
|
+
fi
|
|
150
|
+
echo "$result"
|
|
151
|
+
return 1
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_shellmate_cancel_debounce() {
|
|
155
|
+
if [[ -n "$_SHELLMATE_DEBOUNCE_PID" ]]; then
|
|
156
|
+
kill "$_SHELLMATE_DEBOUNCE_PID" 2>/dev/null || true
|
|
157
|
+
_SHELLMATE_DEBOUNCE_PID=""
|
|
158
|
+
fi
|
|
159
|
+
python3 -c "
|
|
160
|
+
import sys
|
|
161
|
+
try:
|
|
162
|
+
sys.path.insert(0, r'''$(_shellmate_resolve_root)''')
|
|
163
|
+
from ipc.client import _debounced_client
|
|
164
|
+
_debounced_client.cancel()
|
|
165
|
+
except Exception:
|
|
166
|
+
pass
|
|
167
|
+
" 2>/dev/null || true
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_shellmate_start_debounce() {
|
|
171
|
+
# Pivot Phase 3 Task 3.3: use ipc.client.analyze_partial_debounced (300ms Timer)
|
|
172
|
+
local cmd="$1"
|
|
173
|
+
_shellmate_cancel_debounce
|
|
174
|
+
|
|
175
|
+
(
|
|
176
|
+
local sm_path
|
|
177
|
+
sm_path=$(_shellmate_resolve_root)
|
|
178
|
+
local annotation
|
|
179
|
+
annotation=$(timeout 1.2 python3 -c "
|
|
180
|
+
import sys
|
|
181
|
+
import threading
|
|
182
|
+
sys.path.insert(0, r'''$sm_path''')
|
|
183
|
+
from ipc.client import analyze_partial_debounced
|
|
184
|
+
|
|
185
|
+
holder = {}
|
|
186
|
+
done = threading.Event()
|
|
187
|
+
|
|
188
|
+
def _cb(result):
|
|
189
|
+
holder['r'] = result
|
|
190
|
+
done.set()
|
|
191
|
+
|
|
192
|
+
analyze_partial_debounced(sys.argv[1], _cb)
|
|
193
|
+
if not done.wait(timeout=1.0):
|
|
194
|
+
print('')
|
|
195
|
+
raise SystemExit(0)
|
|
196
|
+
|
|
197
|
+
result = holder.get('r') or {}
|
|
198
|
+
ann = result.get('annotation') or result.get('consequence_preview') or ''
|
|
199
|
+
if isinstance(ann, dict):
|
|
200
|
+
ann = ann.get('what_happens') or ann.get('summary') or ''
|
|
201
|
+
sev = str(result.get('severity', 'MEDIUM'))
|
|
202
|
+
if not ann:
|
|
203
|
+
print('')
|
|
204
|
+
else:
|
|
205
|
+
if sev in ('blocking', 'CRITICAL', 'HIGH'):
|
|
206
|
+
icon = '⚠️'
|
|
207
|
+
elif sev in ('SAFE', 'info', 'LOW'):
|
|
208
|
+
icon = 'ℹ️'
|
|
209
|
+
else:
|
|
210
|
+
icon = 'ℹ️'
|
|
211
|
+
print(f'{icon} {str(ann)[:80]}')
|
|
212
|
+
" "$cmd" 2>/dev/null)
|
|
213
|
+
if [[ -n "$annotation" ]]; then
|
|
214
|
+
# Replace placeholder in-place (do not append)
|
|
215
|
+
_shellmate_render_annotation "$annotation"
|
|
216
|
+
fi
|
|
217
|
+
) &
|
|
218
|
+
_SHELLMATE_DEBOUNCE_PID=$!
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# --- ble.sh widget: keystroke / pre-exec path ---
|
|
222
|
+
|
|
223
|
+
function ble/widget/shellmate-on-keystroke {
|
|
224
|
+
local cmd="${READLINE_LINE}"
|
|
225
|
+
|
|
226
|
+
# Skip empty / very short input
|
|
227
|
+
if [[ ${#cmd} -lt 2 ]]; then
|
|
228
|
+
_shellmate_clear_annotation
|
|
229
|
+
return 0
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
# Phase 1: Instant catastrophic check (target <5ms with cached rules)
|
|
233
|
+
local instant_json
|
|
234
|
+
if instant_json=$(sm_instant_check "$cmd"); then
|
|
235
|
+
_SHELLMATE_NEEDS_CONFIRM="$cmd"
|
|
236
|
+
_shellmate_cancel_debounce
|
|
237
|
+
_shellmate_render_annotation "⚡ BLOCKING — Analyzing..."
|
|
238
|
+
# Kick pipeline anyway so placeholder is replaced with full consequence
|
|
239
|
+
_shellmate_start_debounce "$cmd"
|
|
240
|
+
else
|
|
241
|
+
_SHELLMATE_NEEDS_CONFIRM=""
|
|
242
|
+
# Phase 2: debounce full pipeline for non-catastrophic
|
|
243
|
+
_shellmate_start_debounce "$cmd"
|
|
244
|
+
fi
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function ble/widget/shellmate-check-catastrophic {
|
|
248
|
+
# PRECMD_EXEC / PREEXEC path — keep confirm double-tap semantics
|
|
249
|
+
local line="${READLINE_LINE}"
|
|
250
|
+
local instant_json
|
|
251
|
+
if instant_json=$(sm_instant_check "$line"); then
|
|
252
|
+
_SHELLMATE_NEEDS_CONFIRM="$line"
|
|
253
|
+
_shellmate_render_annotation "⚡ BLOCKING — Type again to confirm"
|
|
254
|
+
else
|
|
255
|
+
_SHELLMATE_NEEDS_CONFIRM=""
|
|
256
|
+
fi
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function ble/widget/shellmate-before-exec {
|
|
260
|
+
# Gate 1: catastrophic double-tap
|
|
261
|
+
if [[ -n "$_SHELLMATE_NEEDS_CONFIRM" ]]; then
|
|
262
|
+
if [[ "$_SHELLMATE_LAST_DANGEROUS" != "$_SHELLMATE_NEEDS_CONFIRM" ]]; then
|
|
263
|
+
_SHELLMATE_LAST_DANGEROUS="$_SHELLMATE_NEEDS_CONFIRM"
|
|
264
|
+
echo -e "\e[91m⛔ Confirmation required. Type command again.\e[m"
|
|
265
|
+
return 1
|
|
266
|
+
fi
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
# Gate 2: consequence preview requires second Enter for HIGH/CRITICAL
|
|
270
|
+
local current_cmd="${READLINE_LINE}"
|
|
271
|
+
if [[ -n "$_SHELLMATE_CONSEQUENCE_REQUIRE_CONFIRM" && \
|
|
272
|
+
"$_SHELLMATE_CONSEQUENCE_REQUIRE_CONFIRM" == "$current_cmd" ]]; then
|
|
273
|
+
if [[ "$_SHELLMATE_CONSEQUENCE_CONFIRM_ARMED" != "$current_cmd" ]]; then
|
|
274
|
+
_SHELLMATE_CONSEQUENCE_CONFIRM_ARMED="$current_cmd"
|
|
275
|
+
echo -e "\e[91m[Press Enter again to execute]\e[m"
|
|
276
|
+
return 1
|
|
277
|
+
fi
|
|
278
|
+
_SHELLMATE_CONSEQUENCE_REQUIRE_CONFIRM=""
|
|
279
|
+
_SHELLMATE_CONSEQUENCE_CONFIRM_ARMED=""
|
|
280
|
+
else
|
|
281
|
+
_SHELLMATE_CONSEQUENCE_CONFIRM_ARMED=""
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
_SHELLMATE_LAST_DANGEROUS=""
|
|
285
|
+
_SHELLMATE_TAB_STATE=""
|
|
286
|
+
_SHELLMATE_TAB_CMD=""
|
|
287
|
+
return 0
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function ble/widget/shellmate-cancel-debounce {
|
|
291
|
+
_shellmate_cancel_debounce
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function ble/widget/shellmate-complete {
|
|
295
|
+
local line="${READLINE_LINE}"
|
|
296
|
+
|
|
297
|
+
# Second Tab: suggestion already shown → accept it and trigger consequence
|
|
298
|
+
if [[ "$_SHELLMATE_TAB_STATE" == "suggested" && -n "$_SHELLMATE_TAB_CMD" ]]; then
|
|
299
|
+
READLINE_LINE="$_SHELLMATE_TAB_CMD"
|
|
300
|
+
READLINE_POINT=${#READLINE_LINE}
|
|
301
|
+
_SHELLMATE_TAB_STATE="consequence_shown"
|
|
302
|
+
_shellmate_clear_annotation
|
|
303
|
+
_shellmate_show_consequence_async "$_SHELLMATE_TAB_CMD"
|
|
304
|
+
return
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
# Reset state on fresh Tab
|
|
308
|
+
_SHELLMATE_TAB_STATE=""
|
|
309
|
+
_SHELLMATE_TAB_CMD=""
|
|
310
|
+
[[ ${#line} -lt 2 ]] && return
|
|
311
|
+
|
|
312
|
+
local sm_path
|
|
313
|
+
sm_path=$(_shellmate_resolve_root)
|
|
314
|
+
[[ -z "$sm_path" ]] && return
|
|
315
|
+
|
|
316
|
+
local completions
|
|
317
|
+
completions=$(python3 -c "
|
|
318
|
+
import sys
|
|
319
|
+
sys.path.insert(0, r'''$sm_path''')
|
|
320
|
+
from completion.bash_adapter import get_completions
|
|
321
|
+
print('\n'.join(get_completions(sys.argv[1])))
|
|
322
|
+
" "$line" 2>/dev/null)
|
|
323
|
+
|
|
324
|
+
if [[ -n "$completions" ]]; then
|
|
325
|
+
local first
|
|
326
|
+
first=$(echo "$completions" | head -n1)
|
|
327
|
+
if [[ "$first" != "$line" ]]; then
|
|
328
|
+
_SHELLMATE_TAB_CMD="$first"
|
|
329
|
+
_SHELLMATE_TAB_STATE="suggested"
|
|
330
|
+
local suffix="${first#$line}"
|
|
331
|
+
if [[ -n "$suffix" ]] && type ble/canvas/put.draw &>/dev/null; then
|
|
332
|
+
ble/canvas/put.draw "\e[90m${suffix}\e[m"
|
|
333
|
+
elif [[ -n "$suffix" ]]; then
|
|
334
|
+
printf '\e[90m%s\e[m' "$suffix" >/dev/tty
|
|
335
|
+
printf '\e[%dD' "${#suffix}" >/dev/tty
|
|
336
|
+
fi
|
|
337
|
+
fi
|
|
338
|
+
fi
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# Bind hooks only when ble.sh is present
|
|
342
|
+
if type blehook &>/dev/null; then
|
|
343
|
+
ble-bind -f __default__ 'ble/widget/shellmate-on-keystroke' 2>/dev/null || true
|
|
344
|
+
ble-bind -f C-i 'ble/widget/shellmate-complete' 2>/dev/null || true # Tab = C-i
|
|
345
|
+
blehook PRECMD+=ble/widget/shellmate-check-catastrophic
|
|
346
|
+
blehook PREEXEC+=ble/widget/shellmate-before-exec
|
|
347
|
+
blehook PREEXEC+=ble/widget/shellmate-cancel-debounce
|
|
348
|
+
fi
|
|
349
|
+
|
|
350
|
+
|