quash-mcp 0.2.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.
Potentially problematic release.
This version of quash-mcp might be problematic. Click here for more details.
- quash_mcp/__init__.py +1 -0
- quash_mcp/__main__.py +10 -0
- quash_mcp/backend_client.py +203 -0
- quash_mcp/server.py +399 -0
- quash_mcp/state.py +137 -0
- quash_mcp/tools/__init__.py +9 -0
- quash_mcp/tools/build.py +739 -0
- quash_mcp/tools/build_old.py +185 -0
- quash_mcp/tools/configure.py +140 -0
- quash_mcp/tools/connect.py +153 -0
- quash_mcp/tools/execute.py +177 -0
- quash_mcp/tools/runsuite.py +209 -0
- quash_mcp/tools/usage.py +31 -0
- quash_mcp-0.2.0.dist-info/METADATA +271 -0
- quash_mcp-0.2.0.dist-info/RECORD +17 -0
- quash_mcp-0.2.0.dist-info/WHEEL +4 -0
- quash_mcp-0.2.0.dist-info/entry_points.txt +2 -0
quash_mcp/tools/build.py
ADDED
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build tool V2 - Comprehensive dependency checker and installer.
|
|
3
|
+
Checks, installs, validates versions, and fixes PATH issues.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import subprocess
|
|
8
|
+
import shutil
|
|
9
|
+
import platform
|
|
10
|
+
import os
|
|
11
|
+
from typing import Dict, Any, Tuple, List
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DependencyChecker:
|
|
16
|
+
"""Comprehensive dependency checker and installer"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.os_type = platform.system() # Darwin, Linux, Windows
|
|
20
|
+
self.issues = []
|
|
21
|
+
self.successes = []
|
|
22
|
+
|
|
23
|
+
# ==================== PYTHON VERSION ====================
|
|
24
|
+
|
|
25
|
+
def check_python_version(self) -> Tuple[bool, str, str]:
|
|
26
|
+
"""Check if Python version is >= 3.11 and show path"""
|
|
27
|
+
version = sys.version_info
|
|
28
|
+
current = f"{version.major}.{version.minor}.{version.micro}"
|
|
29
|
+
python_path = sys.executable
|
|
30
|
+
|
|
31
|
+
if version.major >= 3 and version.minor >= 11:
|
|
32
|
+
return True, f"✓ Python {current} at {python_path}", None
|
|
33
|
+
else:
|
|
34
|
+
return False, f"✗ Python {current} at {python_path} (requires >= 3.11)", \
|
|
35
|
+
f"Please upgrade Python to 3.11 or higher. Current: {current}"
|
|
36
|
+
|
|
37
|
+
def install_python(self) -> Tuple[bool, str]:
|
|
38
|
+
"""Attempt to install Python 3.13 based on OS"""
|
|
39
|
+
|
|
40
|
+
if self.os_type == "Darwin": # macOS
|
|
41
|
+
return self._install_python_macos()
|
|
42
|
+
elif self.os_type == "Linux":
|
|
43
|
+
return self._install_python_linux()
|
|
44
|
+
elif self.os_type == "Windows":
|
|
45
|
+
return self._install_python_windows()
|
|
46
|
+
else:
|
|
47
|
+
return False, f"✗ Unsupported OS: {self.os_type}"
|
|
48
|
+
|
|
49
|
+
def _install_python_macos(self) -> Tuple[bool, str]:
|
|
50
|
+
"""Install Python 3.13 on macOS via Homebrew"""
|
|
51
|
+
if not shutil.which("brew"):
|
|
52
|
+
return False, "✗ Homebrew not found. Install from: https://brew.sh/"
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
print(" Installing Python 3.13 via Homebrew...")
|
|
56
|
+
subprocess.run(
|
|
57
|
+
["brew", "install", "python@3.13"],
|
|
58
|
+
check=True,
|
|
59
|
+
capture_output=True,
|
|
60
|
+
timeout=600,
|
|
61
|
+
text=True
|
|
62
|
+
)
|
|
63
|
+
return True, "✓ Python 3.13 installed via Homebrew. Please restart your terminal and run build again."
|
|
64
|
+
except subprocess.TimeoutExpired:
|
|
65
|
+
return False, "✗ Installation timed out"
|
|
66
|
+
except subprocess.CalledProcessError as e:
|
|
67
|
+
error_msg = e.stderr if e.stderr else str(e)
|
|
68
|
+
return False, f"✗ Homebrew install failed: {error_msg}"
|
|
69
|
+
except Exception as e:
|
|
70
|
+
return False, f"✗ Installation error: {str(e)}"
|
|
71
|
+
|
|
72
|
+
def _install_python_linux(self) -> Tuple[bool, str]:
|
|
73
|
+
"""Install Python 3.13 on Linux"""
|
|
74
|
+
# Try deadsnakes PPA for Ubuntu/Debian
|
|
75
|
+
if shutil.which("apt-get"):
|
|
76
|
+
try:
|
|
77
|
+
print(" Adding deadsnakes PPA and installing Python 3.13...")
|
|
78
|
+
subprocess.run(
|
|
79
|
+
["sudo", "apt-get", "install", "-y", "software-properties-common"],
|
|
80
|
+
check=True,
|
|
81
|
+
capture_output=True,
|
|
82
|
+
timeout=120
|
|
83
|
+
)
|
|
84
|
+
subprocess.run(
|
|
85
|
+
["sudo", "add-apt-repository", "-y", "ppa:deadsnakes/ppa"],
|
|
86
|
+
check=True,
|
|
87
|
+
capture_output=True,
|
|
88
|
+
timeout=120
|
|
89
|
+
)
|
|
90
|
+
subprocess.run(
|
|
91
|
+
["sudo", "apt-get", "update"],
|
|
92
|
+
check=True,
|
|
93
|
+
capture_output=True,
|
|
94
|
+
timeout=120
|
|
95
|
+
)
|
|
96
|
+
subprocess.run(
|
|
97
|
+
["sudo", "apt-get", "install", "-y", "python3.13", "python3.13-venv"],
|
|
98
|
+
check=True,
|
|
99
|
+
capture_output=True,
|
|
100
|
+
timeout=300
|
|
101
|
+
)
|
|
102
|
+
return True, "✓ Python 3.13 installed. Use 'python3.13' to run."
|
|
103
|
+
except Exception as e:
|
|
104
|
+
return False, f"✗ apt-get install failed: {str(e)}"
|
|
105
|
+
|
|
106
|
+
# Try dnf for Fedora/RHEL
|
|
107
|
+
elif shutil.which("dnf"):
|
|
108
|
+
try:
|
|
109
|
+
print(" Installing Python 3.13 via dnf...")
|
|
110
|
+
subprocess.run(
|
|
111
|
+
["sudo", "dnf", "install", "-y", "python3.13"],
|
|
112
|
+
check=True,
|
|
113
|
+
capture_output=True,
|
|
114
|
+
timeout=300
|
|
115
|
+
)
|
|
116
|
+
return True, "✓ Python 3.13 installed via dnf"
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return False, f"✗ dnf install failed: {str(e)}"
|
|
119
|
+
else:
|
|
120
|
+
return False, "✗ No supported package manager found"
|
|
121
|
+
|
|
122
|
+
def _install_python_windows(self) -> Tuple[bool, str]:
|
|
123
|
+
"""Install Python on Windows"""
|
|
124
|
+
return False, (
|
|
125
|
+
"✗ Please install Python manually:\n"
|
|
126
|
+
" 1. Download: https://www.python.org/downloads/\n"
|
|
127
|
+
" 2. Run installer and check 'Add to PATH'\n"
|
|
128
|
+
" 3. Install Python 3.11 or higher"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# ==================== ADB ====================
|
|
132
|
+
|
|
133
|
+
def check_adb(self) -> Tuple[bool, str, str]:
|
|
134
|
+
"""Check if ADB is installed and get version"""
|
|
135
|
+
adb_path = shutil.which("adb")
|
|
136
|
+
|
|
137
|
+
if adb_path:
|
|
138
|
+
try:
|
|
139
|
+
result = subprocess.run(
|
|
140
|
+
["adb", "version"],
|
|
141
|
+
capture_output=True,
|
|
142
|
+
text=True,
|
|
143
|
+
timeout=5
|
|
144
|
+
)
|
|
145
|
+
version_line = result.stdout.split('\n')[0]
|
|
146
|
+
return True, f"✓ ADB installed at {adb_path} ({version_line})", None
|
|
147
|
+
except Exception as e:
|
|
148
|
+
return True, f"✓ ADB found at {adb_path} (version check failed)", None
|
|
149
|
+
|
|
150
|
+
return False, "✗ ADB not found in PATH", None
|
|
151
|
+
|
|
152
|
+
def install_adb(self) -> Tuple[bool, str]:
|
|
153
|
+
"""Attempt to install ADB based on OS"""
|
|
154
|
+
|
|
155
|
+
if self.os_type == "Darwin": # macOS
|
|
156
|
+
return self._install_adb_macos()
|
|
157
|
+
elif self.os_type == "Linux":
|
|
158
|
+
return self._install_adb_linux()
|
|
159
|
+
elif self.os_type == "Windows":
|
|
160
|
+
return self._install_adb_windows()
|
|
161
|
+
else:
|
|
162
|
+
return False, f"✗ Unsupported OS: {self.os_type}"
|
|
163
|
+
|
|
164
|
+
def _install_adb_macos(self) -> Tuple[bool, str]:
|
|
165
|
+
"""Install ADB on macOS"""
|
|
166
|
+
# Check if Homebrew is installed
|
|
167
|
+
if not shutil.which("brew"):
|
|
168
|
+
return False, "✗ Homebrew not found. Install from: https://brew.sh/"
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
print(" Installing ADB via Homebrew...")
|
|
172
|
+
result = subprocess.run(
|
|
173
|
+
["brew", "install", "android-platform-tools"],
|
|
174
|
+
capture_output=True,
|
|
175
|
+
timeout=300,
|
|
176
|
+
text=True
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Check if already installed (cask returns different messages)
|
|
180
|
+
combined_output = (result.stdout + result.stderr).lower()
|
|
181
|
+
already_installed = (
|
|
182
|
+
"android-platform-tools" in combined_output and
|
|
183
|
+
("already installed" in combined_output or
|
|
184
|
+
"not upgrading" in combined_output or
|
|
185
|
+
"reinstalling" in combined_output)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Verify ADB is in PATH after installation
|
|
189
|
+
if not shutil.which("adb"):
|
|
190
|
+
if already_installed:
|
|
191
|
+
print(" ADB package already installed but not in PATH, reinstalling...")
|
|
192
|
+
# For casks, we need to reinstall to recreate symlinks
|
|
193
|
+
try:
|
|
194
|
+
subprocess.run(
|
|
195
|
+
["brew", "reinstall", "android-platform-tools"],
|
|
196
|
+
check=True,
|
|
197
|
+
capture_output=True,
|
|
198
|
+
timeout=120
|
|
199
|
+
)
|
|
200
|
+
except Exception as reinstall_error:
|
|
201
|
+
pass # Continue to manual symlink creation
|
|
202
|
+
|
|
203
|
+
# Always check and create symlink manually after reinstall
|
|
204
|
+
# (casks don't automatically create symlinks in /opt/homebrew/bin)
|
|
205
|
+
if not shutil.which("adb"):
|
|
206
|
+
print(" Reinstall completed, creating symlink manually...")
|
|
207
|
+
cask_path = Path("/opt/homebrew/Caskroom/android-platform-tools")
|
|
208
|
+
if cask_path.exists():
|
|
209
|
+
# Find the version directory (sorted to get latest)
|
|
210
|
+
versions = sorted(cask_path.glob("*"))
|
|
211
|
+
if versions:
|
|
212
|
+
adb_src = versions[-1] / "platform-tools" / "adb"
|
|
213
|
+
if adb_src.exists():
|
|
214
|
+
adb_link = Path("/opt/homebrew/bin/adb")
|
|
215
|
+
try:
|
|
216
|
+
if adb_link.exists() or adb_link.is_symlink():
|
|
217
|
+
adb_link.unlink()
|
|
218
|
+
adb_link.symlink_to(adb_src)
|
|
219
|
+
print(f" Created symlink: {adb_link} -> {adb_src}")
|
|
220
|
+
except Exception as symlink_error:
|
|
221
|
+
return False, f"✗ Failed to create symlink: {str(symlink_error)}"
|
|
222
|
+
else:
|
|
223
|
+
return False, f"✗ ADB binary not found at {adb_src}"
|
|
224
|
+
else:
|
|
225
|
+
return False, f"✗ No version directories found in {cask_path}"
|
|
226
|
+
else:
|
|
227
|
+
return False, f"✗ Caskroom path not found: {cask_path}"
|
|
228
|
+
else:
|
|
229
|
+
print(" Installation completed but ADB not found in PATH")
|
|
230
|
+
|
|
231
|
+
# Final verification
|
|
232
|
+
if shutil.which("adb"):
|
|
233
|
+
return True, "✓ ADB installed successfully via Homebrew"
|
|
234
|
+
else:
|
|
235
|
+
return False, "✗ ADB installation completed but not found in PATH. Try restarting terminal."
|
|
236
|
+
|
|
237
|
+
except subprocess.TimeoutExpired:
|
|
238
|
+
return False, "✗ Installation timed out"
|
|
239
|
+
except subprocess.CalledProcessError as e:
|
|
240
|
+
error_msg = e.stderr.decode() if e.stderr else str(e)
|
|
241
|
+
# Even if brew install fails due to already installed, continue to verification
|
|
242
|
+
if "already installed" in error_msg.lower():
|
|
243
|
+
if shutil.which("adb"):
|
|
244
|
+
return True, "✓ ADB already installed via Homebrew"
|
|
245
|
+
return False, f"✗ Homebrew install failed: {error_msg}"
|
|
246
|
+
except Exception as e:
|
|
247
|
+
return False, f"✗ Installation error: {str(e)}"
|
|
248
|
+
|
|
249
|
+
def _install_adb_linux(self) -> Tuple[bool, str]:
|
|
250
|
+
"""Install ADB on Linux"""
|
|
251
|
+
# Try apt-get first (Debian/Ubuntu)
|
|
252
|
+
if shutil.which("apt-get"):
|
|
253
|
+
try:
|
|
254
|
+
print(" Installing ADB via apt-get (may require sudo)...")
|
|
255
|
+
subprocess.run(
|
|
256
|
+
["sudo", "apt-get", "update"],
|
|
257
|
+
check=True,
|
|
258
|
+
capture_output=True,
|
|
259
|
+
timeout=120
|
|
260
|
+
)
|
|
261
|
+
subprocess.run(
|
|
262
|
+
["sudo", "apt-get", "install", "-y", "adb"],
|
|
263
|
+
check=True,
|
|
264
|
+
capture_output=True,
|
|
265
|
+
timeout=300
|
|
266
|
+
)
|
|
267
|
+
return True, "✓ ADB installed successfully via apt-get"
|
|
268
|
+
except Exception as e:
|
|
269
|
+
return False, f"✗ apt-get install failed: {str(e)}"
|
|
270
|
+
|
|
271
|
+
# Try dnf (Fedora/RHEL)
|
|
272
|
+
elif shutil.which("dnf"):
|
|
273
|
+
try:
|
|
274
|
+
print(" Installing ADB via dnf (may require sudo)...")
|
|
275
|
+
subprocess.run(
|
|
276
|
+
["sudo", "dnf", "install", "-y", "android-tools"],
|
|
277
|
+
check=True,
|
|
278
|
+
capture_output=True,
|
|
279
|
+
timeout=300
|
|
280
|
+
)
|
|
281
|
+
return True, "✓ ADB installed successfully via dnf"
|
|
282
|
+
except Exception as e:
|
|
283
|
+
return False, f"✗ dnf install failed: {str(e)}"
|
|
284
|
+
|
|
285
|
+
else:
|
|
286
|
+
return False, "✗ No supported package manager found (apt-get/dnf)"
|
|
287
|
+
|
|
288
|
+
def _install_adb_windows(self) -> Tuple[bool, str]:
|
|
289
|
+
"""Install ADB on Windows"""
|
|
290
|
+
# Windows doesn't have good package manager by default
|
|
291
|
+
# Provide download link instead
|
|
292
|
+
return False, (
|
|
293
|
+
"✗ Please install ADB manually:\n"
|
|
294
|
+
" 1. Download: https://developer.android.com/tools/releases/platform-tools\n"
|
|
295
|
+
" 2. Extract to C:\\platform-tools\n"
|
|
296
|
+
" 3. Add to PATH: C:\\platform-tools"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def check_adb_in_path(self) -> Tuple[bool, str, str]:
|
|
300
|
+
"""Check if ADB is in PATH and suggest fix if not"""
|
|
301
|
+
adb_path = shutil.which("adb")
|
|
302
|
+
|
|
303
|
+
if adb_path:
|
|
304
|
+
return True, f"✓ ADB in PATH: {adb_path}", None
|
|
305
|
+
|
|
306
|
+
# Try to find ADB in common locations
|
|
307
|
+
common_paths = []
|
|
308
|
+
|
|
309
|
+
if self.os_type == "Darwin":
|
|
310
|
+
common_paths = [
|
|
311
|
+
"/opt/homebrew/bin/adb",
|
|
312
|
+
"/usr/local/bin/adb",
|
|
313
|
+
Path.home() / "Library/Android/sdk/platform-tools/adb",
|
|
314
|
+
]
|
|
315
|
+
elif self.os_type == "Linux":
|
|
316
|
+
common_paths = [
|
|
317
|
+
"/usr/bin/adb",
|
|
318
|
+
"/usr/local/bin/adb",
|
|
319
|
+
Path.home() / "Android/Sdk/platform-tools/adb",
|
|
320
|
+
]
|
|
321
|
+
elif self.os_type == "Windows":
|
|
322
|
+
common_paths = [
|
|
323
|
+
"C:\\platform-tools\\adb.exe",
|
|
324
|
+
"C:\\Android\\sdk\\platform-tools\\adb.exe",
|
|
325
|
+
Path.home() / "AppData/Local/Android/Sdk/platform-tools/adb.exe",
|
|
326
|
+
]
|
|
327
|
+
|
|
328
|
+
for path in common_paths:
|
|
329
|
+
if Path(path).exists():
|
|
330
|
+
return False, f"⚠️ ADB found at {path} but not in PATH", str(path)
|
|
331
|
+
|
|
332
|
+
return False, "✗ ADB not found anywhere", None
|
|
333
|
+
|
|
334
|
+
def add_to_path(self, adb_path: str) -> Tuple[bool, str]:
|
|
335
|
+
"""Add ADB directory to PATH"""
|
|
336
|
+
adb_dir = str(Path(adb_path).parent)
|
|
337
|
+
|
|
338
|
+
shell_rc = None
|
|
339
|
+
if self.os_type == "Darwin":
|
|
340
|
+
# Check which shell
|
|
341
|
+
shell = os.environ.get("SHELL", "")
|
|
342
|
+
if "zsh" in shell:
|
|
343
|
+
shell_rc = Path.home() / ".zshrc"
|
|
344
|
+
else:
|
|
345
|
+
shell_rc = Path.home() / ".bash_profile"
|
|
346
|
+
elif self.os_type == "Linux":
|
|
347
|
+
shell_rc = Path.home() / ".bashrc"
|
|
348
|
+
|
|
349
|
+
if shell_rc:
|
|
350
|
+
try:
|
|
351
|
+
with open(shell_rc, "a") as f:
|
|
352
|
+
f.write(f'\n# Added by Quash MCP\nexport PATH="$PATH:{adb_dir}"\n')
|
|
353
|
+
|
|
354
|
+
return True, (
|
|
355
|
+
f"✓ Added to PATH in {shell_rc}\n"
|
|
356
|
+
f" Please run: source {shell_rc}\n"
|
|
357
|
+
f" Or restart your terminal"
|
|
358
|
+
)
|
|
359
|
+
except Exception as e:
|
|
360
|
+
return False, f"✗ Failed to update {shell_rc}: {str(e)}"
|
|
361
|
+
|
|
362
|
+
return False, "✗ Could not determine shell config file"
|
|
363
|
+
|
|
364
|
+
# ==================== MAHORAGA PACKAGE ====================
|
|
365
|
+
|
|
366
|
+
def check_mahoraga(self) -> Tuple[bool, str, str]:
|
|
367
|
+
"""Check if Quash package is installed"""
|
|
368
|
+
try:
|
|
369
|
+
import mahoraga
|
|
370
|
+
version = getattr(mahoraga, "__version__", "unknown")
|
|
371
|
+
location = Path(mahoraga.__file__).parent
|
|
372
|
+
return True, f"✓ Quash v{version} installed at {location}", None
|
|
373
|
+
except ImportError:
|
|
374
|
+
return False, "✗ Quash package not installed", None
|
|
375
|
+
|
|
376
|
+
def install_mahoraga(self) -> Tuple[bool, str]:
|
|
377
|
+
"""Install Quash package"""
|
|
378
|
+
try:
|
|
379
|
+
# Check if we're in development mode (source available)
|
|
380
|
+
project_root = Path(__file__).parent.parent.parent.parent
|
|
381
|
+
mahoraga_src = project_root / "mahoraga"
|
|
382
|
+
|
|
383
|
+
if mahoraga_src.exists() and (mahoraga_src / "pyproject.toml").exists():
|
|
384
|
+
# Install from local source
|
|
385
|
+
print(f" Installing Quash from {mahoraga_src}...")
|
|
386
|
+
subprocess.run(
|
|
387
|
+
[sys.executable, "-m", "pip", "install", "-e", str(mahoraga_src)],
|
|
388
|
+
check=True,
|
|
389
|
+
capture_output=True
|
|
390
|
+
)
|
|
391
|
+
return True, f"✓ Quash installed from local source: {mahoraga_src}"
|
|
392
|
+
else:
|
|
393
|
+
# Try to install from PyPI (if published)
|
|
394
|
+
print(" Installing Quash from PyPI...")
|
|
395
|
+
subprocess.run(
|
|
396
|
+
[sys.executable, "-m", "pip", "install", "mahoraga"],
|
|
397
|
+
check=True,
|
|
398
|
+
capture_output=True
|
|
399
|
+
)
|
|
400
|
+
return True, "✓ Quash installed from PyPI"
|
|
401
|
+
|
|
402
|
+
except subprocess.CalledProcessError as e:
|
|
403
|
+
error_msg = e.stderr.decode() if e.stderr else str(e)
|
|
404
|
+
return False, f"✗ Installation failed: {error_msg}"
|
|
405
|
+
except Exception as e:
|
|
406
|
+
return False, f"✗ Error: {str(e)}"
|
|
407
|
+
|
|
408
|
+
def check_mahoraga_version(self) -> Tuple[bool, str, str]:
|
|
409
|
+
"""Check if Quash version is compatible"""
|
|
410
|
+
try:
|
|
411
|
+
import mahoraga
|
|
412
|
+
version = getattr(mahoraga, "__version__", "0.0.0")
|
|
413
|
+
|
|
414
|
+
# Parse version
|
|
415
|
+
major, minor, patch = version.split(".")[:3]
|
|
416
|
+
|
|
417
|
+
# We need at least 0.3.0
|
|
418
|
+
if int(major) == 0 and int(minor) >= 3:
|
|
419
|
+
return True, f"✓ Quash v{version} (compatible)", None
|
|
420
|
+
elif int(major) > 0:
|
|
421
|
+
return True, f"✓ Quash v{version} (compatible)", None
|
|
422
|
+
else:
|
|
423
|
+
return False, f"✗ Quash v{version} (requires >= 0.3.0)", version
|
|
424
|
+
except Exception as e:
|
|
425
|
+
return False, f"✗ Version check failed: {str(e)}", None
|
|
426
|
+
|
|
427
|
+
# ==================== PYTHON DEPENDENCIES ====================
|
|
428
|
+
|
|
429
|
+
def check_python_dependencies(self) -> Tuple[bool, str, List[str]]:
|
|
430
|
+
"""Check if all required Python packages are installed with correct versions"""
|
|
431
|
+
required_packages = {
|
|
432
|
+
"click": "8.1.0",
|
|
433
|
+
"rich": "13.0.0",
|
|
434
|
+
"pydantic": "2.0.0",
|
|
435
|
+
"aiofiles": "23.0.0",
|
|
436
|
+
"openai": "1.0.0",
|
|
437
|
+
"pillow": "10.0.0",
|
|
438
|
+
"python-dotenv": "1.0.0",
|
|
439
|
+
"requests": "2.31.0",
|
|
440
|
+
"llama-index": None, # No specific version
|
|
441
|
+
"llama-index-llms-openai-like": None,
|
|
442
|
+
"adbutils": "2.10.0", # Exact version
|
|
443
|
+
"apkutils": "2.0.0", # Exact version
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
missing = []
|
|
447
|
+
incompatible = []
|
|
448
|
+
|
|
449
|
+
for package, min_version in required_packages.items():
|
|
450
|
+
try:
|
|
451
|
+
# Import the package to check if it's installed
|
|
452
|
+
if package == "python-dotenv":
|
|
453
|
+
import dotenv
|
|
454
|
+
pkg = dotenv
|
|
455
|
+
pkg_name = "dotenv"
|
|
456
|
+
elif package == "pillow":
|
|
457
|
+
from PIL import Image
|
|
458
|
+
pkg = Image
|
|
459
|
+
pkg_name = "PIL"
|
|
460
|
+
elif package == "llama-index-llms-openai-like":
|
|
461
|
+
from llama_index.llms import openai_like
|
|
462
|
+
pkg = openai_like
|
|
463
|
+
pkg_name = "llama_index.llms.openai_like"
|
|
464
|
+
else:
|
|
465
|
+
pkg_name = package.replace("-", "_")
|
|
466
|
+
pkg = __import__(pkg_name)
|
|
467
|
+
|
|
468
|
+
# Check version if required
|
|
469
|
+
if min_version:
|
|
470
|
+
version = getattr(pkg, "__version__", None)
|
|
471
|
+
if version:
|
|
472
|
+
# Parse version numbers
|
|
473
|
+
current_parts = version.split(".")[:3]
|
|
474
|
+
required_parts = min_version.split(".")[:3]
|
|
475
|
+
|
|
476
|
+
# For exact version requirements (adbutils, apkutils)
|
|
477
|
+
if package in ["adbutils", "apkutils"]:
|
|
478
|
+
if version != min_version:
|
|
479
|
+
incompatible.append(f"{package}=={min_version} (current: {version})")
|
|
480
|
+
else:
|
|
481
|
+
# For minimum version requirements
|
|
482
|
+
try:
|
|
483
|
+
current_ver = tuple(int(x) for x in current_parts)
|
|
484
|
+
required_ver = tuple(int(x) for x in required_parts)
|
|
485
|
+
if current_ver < required_ver:
|
|
486
|
+
incompatible.append(f"{package}>={min_version} (current: {version})")
|
|
487
|
+
except ValueError:
|
|
488
|
+
# If version parsing fails, skip version check
|
|
489
|
+
pass
|
|
490
|
+
except ImportError:
|
|
491
|
+
missing.append(package)
|
|
492
|
+
|
|
493
|
+
if missing or incompatible:
|
|
494
|
+
msg_parts = []
|
|
495
|
+
if missing:
|
|
496
|
+
msg_parts.append(f"Missing: {', '.join(missing)}")
|
|
497
|
+
if incompatible:
|
|
498
|
+
msg_parts.append(f"Incompatible: {', '.join(incompatible)}")
|
|
499
|
+
return False, f"✗ Python dependencies: {'; '.join(msg_parts)}", missing + incompatible
|
|
500
|
+
else:
|
|
501
|
+
return True, f"✓ All Python dependencies installed", None
|
|
502
|
+
|
|
503
|
+
def install_python_dependencies(self) -> Tuple[bool, str]:
|
|
504
|
+
"""Install all required Python dependencies"""
|
|
505
|
+
try:
|
|
506
|
+
print(" Installing Python dependencies...")
|
|
507
|
+
|
|
508
|
+
# Install from Quash's pyproject.toml if available
|
|
509
|
+
project_root = Path(__file__).parent.parent.parent.parent
|
|
510
|
+
mahoraga_src = project_root / "mahoraga"
|
|
511
|
+
|
|
512
|
+
if mahoraga_src.exists() and (mahoraga_src / "pyproject.toml").exists():
|
|
513
|
+
print(f" Installing from {mahoraga_src}...")
|
|
514
|
+
subprocess.run(
|
|
515
|
+
[sys.executable, "-m", "pip", "install", "-e", str(mahoraga_src)],
|
|
516
|
+
check=True,
|
|
517
|
+
capture_output=True,
|
|
518
|
+
timeout=300
|
|
519
|
+
)
|
|
520
|
+
return True, "✓ Python dependencies installed from local source"
|
|
521
|
+
else:
|
|
522
|
+
# Install individual packages with version constraints
|
|
523
|
+
packages = [
|
|
524
|
+
"click>=8.1.0",
|
|
525
|
+
"rich>=13.0.0",
|
|
526
|
+
"pydantic>=2.0.0",
|
|
527
|
+
"aiofiles>=23.0.0",
|
|
528
|
+
"openai>=1.0.0",
|
|
529
|
+
"pillow>=10.0.0",
|
|
530
|
+
"python-dotenv>=1.0.0",
|
|
531
|
+
"requests>=2.31.0",
|
|
532
|
+
"llama-index",
|
|
533
|
+
"llama-index-llms-openai-like",
|
|
534
|
+
"adbutils==2.10.0",
|
|
535
|
+
"apkutils==2.0.0",
|
|
536
|
+
]
|
|
537
|
+
|
|
538
|
+
print(f" Installing {len(packages)} packages...")
|
|
539
|
+
subprocess.run(
|
|
540
|
+
[sys.executable, "-m", "pip", "install"] + packages,
|
|
541
|
+
check=True,
|
|
542
|
+
capture_output=True,
|
|
543
|
+
timeout=600
|
|
544
|
+
)
|
|
545
|
+
return True, "✓ Python dependencies installed"
|
|
546
|
+
|
|
547
|
+
except subprocess.TimeoutExpired:
|
|
548
|
+
return False, "✗ Dependency installation timed out"
|
|
549
|
+
except subprocess.CalledProcessError as e:
|
|
550
|
+
error_msg = e.stderr.decode() if e.stderr else str(e)
|
|
551
|
+
return False, f"✗ Dependency installation failed: {error_msg}"
|
|
552
|
+
except Exception as e:
|
|
553
|
+
return False, f"✗ Installation error: {str(e)}"
|
|
554
|
+
|
|
555
|
+
# ==================== PORTAL APK ====================
|
|
556
|
+
|
|
557
|
+
def check_portal(self) -> Tuple[bool, str, str]:
|
|
558
|
+
"""Check if Portal APK download works"""
|
|
559
|
+
try:
|
|
560
|
+
from mahoraga.portal import download_portal_apk
|
|
561
|
+
return True, "✓ Portal APK download available", None
|
|
562
|
+
except ImportError as e:
|
|
563
|
+
return False, f"✗ Portal module not found: {str(e)}", None
|
|
564
|
+
except Exception as e:
|
|
565
|
+
return False, f"✗ Portal check failed: {str(e)}", None
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
async def build() -> Dict[str, Any]:
|
|
569
|
+
"""
|
|
570
|
+
Comprehensive build check and setup.
|
|
571
|
+
|
|
572
|
+
1. Checks all dependencies
|
|
573
|
+
2. Validates versions
|
|
574
|
+
3. Auto-installs where possible
|
|
575
|
+
4. Fixes PATH issues
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
Dict with detailed status of all checks and fixes
|
|
579
|
+
"""
|
|
580
|
+
checker = DependencyChecker()
|
|
581
|
+
details = {}
|
|
582
|
+
fixes_applied = []
|
|
583
|
+
all_ok = True
|
|
584
|
+
|
|
585
|
+
print("=" * 70)
|
|
586
|
+
print("MAHORAGA MCP - DEPENDENCY CHECK & SETUP")
|
|
587
|
+
print("=" * 70)
|
|
588
|
+
print()
|
|
589
|
+
|
|
590
|
+
# 1. Check Python Version
|
|
591
|
+
print("1️⃣ Checking Python version...")
|
|
592
|
+
py_ok, py_msg, py_fix = checker.check_python_version()
|
|
593
|
+
details["python"] = py_msg
|
|
594
|
+
print(f" {py_msg}")
|
|
595
|
+
|
|
596
|
+
if not py_ok:
|
|
597
|
+
# Try to install Python 3.13
|
|
598
|
+
print(" Attempting to install Python 3.13...")
|
|
599
|
+
install_ok, install_msg = checker.install_python()
|
|
600
|
+
details["python_install"] = install_msg
|
|
601
|
+
print(f" {install_msg}")
|
|
602
|
+
|
|
603
|
+
if not install_ok:
|
|
604
|
+
all_ok = False
|
|
605
|
+
else:
|
|
606
|
+
fixes_applied.append("Installed Python 3.13")
|
|
607
|
+
print(f" ⚠️ Please restart your terminal and run build again with python3.13")
|
|
608
|
+
print()
|
|
609
|
+
|
|
610
|
+
# 2. Check ADB
|
|
611
|
+
print("2️⃣ Checking ADB...")
|
|
612
|
+
adb_ok, adb_msg, _ = checker.check_adb()
|
|
613
|
+
details["adb"] = adb_msg
|
|
614
|
+
print(f" {adb_msg}")
|
|
615
|
+
|
|
616
|
+
if not adb_ok:
|
|
617
|
+
# Try to install
|
|
618
|
+
print(" Attempting to install ADB...")
|
|
619
|
+
install_ok, install_msg = checker.install_adb()
|
|
620
|
+
details["adb_install"] = install_msg
|
|
621
|
+
print(f" {install_msg}")
|
|
622
|
+
|
|
623
|
+
if not install_ok:
|
|
624
|
+
all_ok = False
|
|
625
|
+
else:
|
|
626
|
+
fixes_applied.append("Installed ADB")
|
|
627
|
+
|
|
628
|
+
# 3. Check ADB in PATH
|
|
629
|
+
print("3️⃣ Checking ADB PATH...")
|
|
630
|
+
path_ok, path_msg, found_path = checker.check_adb_in_path()
|
|
631
|
+
details["adb_path"] = path_msg
|
|
632
|
+
print(f" {path_msg}")
|
|
633
|
+
|
|
634
|
+
if not path_ok and found_path:
|
|
635
|
+
# Try to add to PATH
|
|
636
|
+
print(" Attempting to add ADB to PATH...")
|
|
637
|
+
path_fix_ok, path_fix_msg = checker.add_to_path(found_path)
|
|
638
|
+
details["adb_path_fix"] = path_fix_msg
|
|
639
|
+
print(f" {path_fix_msg}")
|
|
640
|
+
|
|
641
|
+
if path_fix_ok:
|
|
642
|
+
fixes_applied.append("Added ADB to PATH")
|
|
643
|
+
elif not path_ok:
|
|
644
|
+
all_ok = False
|
|
645
|
+
print()
|
|
646
|
+
|
|
647
|
+
# 4. Check Quash Package
|
|
648
|
+
print("4️⃣ Checking Quash package...")
|
|
649
|
+
mhg_ok, mhg_msg, _ = checker.check_mahoraga()
|
|
650
|
+
details["mahoraga"] = mhg_msg
|
|
651
|
+
print(f" {mhg_msg}")
|
|
652
|
+
|
|
653
|
+
mahoraga_just_installed = False
|
|
654
|
+
if not mhg_ok:
|
|
655
|
+
# Try to install
|
|
656
|
+
print(" Attempting to install Quash...")
|
|
657
|
+
install_ok, install_msg = checker.install_mahoraga()
|
|
658
|
+
details["mahoraga_install"] = install_msg
|
|
659
|
+
print(f" {install_msg}")
|
|
660
|
+
|
|
661
|
+
if not install_ok:
|
|
662
|
+
all_ok = False
|
|
663
|
+
else:
|
|
664
|
+
fixes_applied.append("Installed Quash")
|
|
665
|
+
mahoraga_just_installed = True # Track that we just installed Quash
|
|
666
|
+
print()
|
|
667
|
+
|
|
668
|
+
# 5. Check Quash Version
|
|
669
|
+
print("5️⃣ Checking Quash version...")
|
|
670
|
+
ver_ok, ver_msg, ver_fix = checker.check_mahoraga_version()
|
|
671
|
+
details["mahoraga_version"] = ver_msg
|
|
672
|
+
print(f" {ver_msg}")
|
|
673
|
+
|
|
674
|
+
if not ver_ok:
|
|
675
|
+
all_ok = False
|
|
676
|
+
if ver_fix:
|
|
677
|
+
fix_msg = f"Please upgrade: pip install --upgrade mahoraga"
|
|
678
|
+
details["mahoraga_version_fix"] = fix_msg
|
|
679
|
+
print(f" 💡 {fix_msg}")
|
|
680
|
+
print()
|
|
681
|
+
|
|
682
|
+
# 6. Check Python Dependencies
|
|
683
|
+
# Note: Only check if Quash wasn't just installed, because 'pip install -e'
|
|
684
|
+
# automatically installs all dependencies from pyproject.toml
|
|
685
|
+
if mahoraga_just_installed:
|
|
686
|
+
print("6️⃣ Checking Python dependencies...")
|
|
687
|
+
print(f" ⏭️ Skipped (dependencies installed with Quash in step 4)")
|
|
688
|
+
details["python_dependencies"] = "⏭️ Skipped (installed with Quash)"
|
|
689
|
+
else:
|
|
690
|
+
print("6️⃣ Checking Python dependencies...")
|
|
691
|
+
deps_ok, deps_msg, missing_deps = checker.check_python_dependencies()
|
|
692
|
+
details["python_dependencies"] = deps_msg
|
|
693
|
+
print(f" {deps_msg}")
|
|
694
|
+
|
|
695
|
+
if not deps_ok:
|
|
696
|
+
# Try to install missing/incompatible dependencies
|
|
697
|
+
print(" Attempting to install Python dependencies...")
|
|
698
|
+
install_ok, install_msg = checker.install_python_dependencies()
|
|
699
|
+
details["python_dependencies_install"] = install_msg
|
|
700
|
+
print(f" {install_msg}")
|
|
701
|
+
|
|
702
|
+
if not install_ok:
|
|
703
|
+
all_ok = False
|
|
704
|
+
else:
|
|
705
|
+
fixes_applied.append("Installed Python dependencies")
|
|
706
|
+
print()
|
|
707
|
+
|
|
708
|
+
# 7. Check Portal
|
|
709
|
+
print("7️⃣ Checking Portal APK...")
|
|
710
|
+
portal_ok, portal_msg, _ = checker.check_portal()
|
|
711
|
+
details["portal"] = portal_msg
|
|
712
|
+
print(f" {portal_msg}")
|
|
713
|
+
|
|
714
|
+
if not portal_ok:
|
|
715
|
+
all_ok = False
|
|
716
|
+
print()
|
|
717
|
+
|
|
718
|
+
# Final Summary
|
|
719
|
+
print("=" * 70)
|
|
720
|
+
if all_ok:
|
|
721
|
+
status = "success"
|
|
722
|
+
message = "✅ All dependencies ready! You can now use Quash."
|
|
723
|
+
if fixes_applied:
|
|
724
|
+
message += f"\n Fixes applied: {', '.join(fixes_applied)}"
|
|
725
|
+
else:
|
|
726
|
+
failed = [k for k, v in details.items() if v.startswith("✗")]
|
|
727
|
+
status = "failed"
|
|
728
|
+
message = f"❌ Setup incomplete. Issues with: {', '.join(failed)}"
|
|
729
|
+
message += "\n Please review the details above and follow the suggestions."
|
|
730
|
+
|
|
731
|
+
print(message)
|
|
732
|
+
print("=" * 70)
|
|
733
|
+
|
|
734
|
+
return {
|
|
735
|
+
"status": status,
|
|
736
|
+
"details": details,
|
|
737
|
+
"fixes_applied": fixes_applied,
|
|
738
|
+
"message": message
|
|
739
|
+
}
|