webtap-tool 0.1.5__py3-none-any.whl → 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 webtap-tool might be problematic. Click here for more details.
- webtap/commands/setup.py +106 -9
- webtap/services/setup/__init__.py +125 -13
- webtap/services/setup/chrome.py +192 -42
- webtap/services/setup/desktop.py +203 -126
- webtap/services/setup/extension.py +84 -71
- webtap/services/setup/filters.py +66 -56
- webtap/services/setup/platform.py +126 -0
- {webtap_tool-0.1.5.dist-info → webtap_tool-0.2.0.dist-info}/METADATA +4 -1
- {webtap_tool-0.1.5.dist-info → webtap_tool-0.2.0.dist-info}/RECORD +11 -10
- {webtap_tool-0.1.5.dist-info → webtap_tool-0.2.0.dist-info}/WHEEL +0 -0
- {webtap_tool-0.1.5.dist-info → webtap_tool-0.2.0.dist-info}/entry_points.txt +0 -0
webtap/services/setup/desktop.py
CHANGED
|
@@ -1,143 +1,27 @@
|
|
|
1
|
-
"""Desktop
|
|
1
|
+
"""Desktop/Application launcher setup (cross-platform)."""
|
|
2
2
|
|
|
3
|
-
import re
|
|
4
3
|
import logging
|
|
5
|
-
from pathlib import Path
|
|
6
4
|
from typing import Dict, Any
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def install_desktop_entry(force: bool = False) -> Dict[str, Any]:
|
|
12
|
-
"""Install desktop entry to override system Chrome launcher.
|
|
13
|
-
|
|
14
|
-
Creates a .desktop file that makes Chrome launch with our debug wrapper
|
|
15
|
-
when started from GUI (app launcher, dock, etc).
|
|
16
|
-
|
|
17
|
-
Args:
|
|
18
|
-
force: Overwrite existing desktop entry
|
|
19
|
-
|
|
20
|
-
Returns:
|
|
21
|
-
Dict with success, message, path, details
|
|
22
|
-
"""
|
|
23
|
-
# Check if wrapper exists first
|
|
24
|
-
wrapper_path = Path.home() / ".local" / "bin" / "wrappers" / "google-chrome-stable"
|
|
25
|
-
if not wrapper_path.exists():
|
|
26
|
-
return {
|
|
27
|
-
"success": False,
|
|
28
|
-
"message": "Chrome wrapper not found. Run 'setup-chrome' first",
|
|
29
|
-
"path": None,
|
|
30
|
-
"details": f"Expected wrapper at {wrapper_path}",
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
# Desktop entry location (user override of system entry)
|
|
34
|
-
desktop_path = Path.home() / ".local" / "share" / "applications" / "google-chrome.desktop"
|
|
6
|
+
from .platform import get_platform_info
|
|
35
7
|
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
"success": False,
|
|
39
|
-
"message": "Desktop entry already exists",
|
|
40
|
-
"path": str(desktop_path),
|
|
41
|
-
"details": "Use --force to overwrite",
|
|
42
|
-
}
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
43
9
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if system_desktop:
|
|
48
|
-
desktop_content = _create_from_system_desktop(system_desktop, wrapper_path)
|
|
49
|
-
source_info = system_desktop.name
|
|
50
|
-
else:
|
|
51
|
-
desktop_content = _create_minimal_desktop(wrapper_path)
|
|
52
|
-
source_info = "minimal template"
|
|
53
|
-
|
|
54
|
-
# Create directory and save
|
|
55
|
-
desktop_path.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
-
desktop_path.write_text(desktop_content)
|
|
57
|
-
desktop_path.chmod(0o644) # Standard permissions for desktop files
|
|
58
|
-
|
|
59
|
-
logger.info(f"Installed desktop entry to {desktop_path}")
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
"success": True,
|
|
63
|
-
"message": "Installed Chrome desktop entry with debug wrapper",
|
|
64
|
-
"path": str(desktop_path),
|
|
65
|
-
"details": f"Based on {source_info}",
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _find_system_desktop_file() -> Path | None:
|
|
70
|
-
"""Find the system Chrome desktop file."""
|
|
71
|
-
for system_path in [
|
|
72
|
-
Path("/usr/share/applications/google-chrome.desktop"),
|
|
73
|
-
Path("/usr/share/applications/google-chrome-stable.desktop"),
|
|
74
|
-
Path("/usr/local/share/applications/google-chrome.desktop"),
|
|
75
|
-
]:
|
|
76
|
-
if system_path.exists():
|
|
77
|
-
return system_path
|
|
78
|
-
return None
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _create_from_system_desktop(system_desktop: Path, wrapper_path: Path) -> str:
|
|
82
|
-
"""Create desktop content by modifying the system desktop file."""
|
|
83
|
-
try:
|
|
84
|
-
system_content = system_desktop.read_text()
|
|
85
|
-
|
|
86
|
-
# Replace all Exec= lines to use our wrapper
|
|
87
|
-
# Match Exec= lines that point to chrome executables
|
|
88
|
-
exec_pattern = re.compile(
|
|
89
|
-
r"^Exec=.*?(?:google-chrome-stable|google-chrome|chromium|chrome)(?:\s|$)", re.MULTILINE
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
# Replace with our wrapper, preserving arguments
|
|
93
|
-
def replace_exec(match):
|
|
94
|
-
line = match.group(0)
|
|
95
|
-
# Find where the executable ends and arguments begin
|
|
96
|
-
# Look for common chrome executables
|
|
97
|
-
for exe in ["google-chrome-stable", "google-chrome", "chromium-browser", "chromium", "chrome"]:
|
|
98
|
-
if exe in line:
|
|
99
|
-
# Split at the executable name
|
|
100
|
-
parts = line.split(exe, 1)
|
|
101
|
-
if len(parts) > 1:
|
|
102
|
-
# Has arguments after executable
|
|
103
|
-
return f"Exec={wrapper_path}{parts[1]}"
|
|
104
|
-
else:
|
|
105
|
-
# No arguments
|
|
106
|
-
return f"Exec={wrapper_path}"
|
|
107
|
-
# Fallback - shouldn't happen
|
|
108
|
-
return f"Exec={wrapper_path}"
|
|
109
|
-
|
|
110
|
-
desktop_content = exec_pattern.sub(replace_exec, system_content)
|
|
111
|
-
|
|
112
|
-
# Also handle TryExec if present (used to check if program exists)
|
|
113
|
-
tryexec_pattern = re.compile(
|
|
114
|
-
r"^TryExec=.*?(?:google-chrome-stable|google-chrome|chromium|chrome)", re.MULTILINE
|
|
115
|
-
)
|
|
116
|
-
desktop_content = tryexec_pattern.sub(f"TryExec={wrapper_path}", desktop_content)
|
|
117
|
-
|
|
118
|
-
return desktop_content
|
|
119
|
-
|
|
120
|
-
except Exception as e:
|
|
121
|
-
logger.warning(f"Failed to parse system desktop file, using minimal entry: {e}")
|
|
122
|
-
return _create_minimal_desktop(wrapper_path)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _create_minimal_desktop(wrapper_path: Path) -> str:
|
|
126
|
-
"""Create a minimal desktop entry from scratch."""
|
|
127
|
-
return f"""[Desktop Entry]
|
|
10
|
+
# Desktop entry template for Linux
|
|
11
|
+
LINUX_DESKTOP_ENTRY = """[Desktop Entry]
|
|
128
12
|
Version=1.0
|
|
129
13
|
Type=Application
|
|
130
|
-
Name=
|
|
131
|
-
GenericName=Web Browser
|
|
132
|
-
Comment=
|
|
14
|
+
Name=Chrome Debug
|
|
15
|
+
GenericName=Web Browser (Debug Mode)
|
|
16
|
+
Comment=Chrome with remote debugging enabled
|
|
133
17
|
Icon=google-chrome
|
|
134
|
-
Categories=
|
|
18
|
+
Categories=Development;WebBrowser;
|
|
135
19
|
MimeType=application/pdf;application/rdf+xml;application/rss+xml;application/xhtml+xml;application/xhtml_xml;application/xml;image/gif;image/jpeg;image/png;image/webp;text/html;text/xml;x-scheme-handler/ftp;x-scheme-handler/http;x-scheme-handler/https;
|
|
136
20
|
StartupWMClass=Google-chrome
|
|
137
21
|
StartupNotify=true
|
|
138
22
|
Terminal=false
|
|
139
23
|
Exec={wrapper_path} %U
|
|
140
|
-
Actions=new-window;new-private-window;
|
|
24
|
+
Actions=new-window;new-private-window;temp-profile;
|
|
141
25
|
|
|
142
26
|
[Desktop Action new-window]
|
|
143
27
|
Name=New Window
|
|
@@ -148,4 +32,197 @@ Exec={wrapper_path}
|
|
|
148
32
|
Name=New Incognito Window
|
|
149
33
|
StartupWMClass=Google-chrome
|
|
150
34
|
Exec={wrapper_path} --incognito
|
|
35
|
+
|
|
36
|
+
[Desktop Action temp-profile]
|
|
37
|
+
Name=New Window (Temp Profile)
|
|
38
|
+
StartupWMClass=Google-chrome
|
|
39
|
+
Exec={wrapper_path} --temp
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Info.plist template for macOS app bundle
|
|
43
|
+
MACOS_INFO_PLIST = """<?xml version="1.0" encoding="UTF-8"?>
|
|
44
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
45
|
+
<plist version="1.0">
|
|
46
|
+
<dict>
|
|
47
|
+
<key>CFBundleExecutable</key>
|
|
48
|
+
<string>Chrome Debug</string>
|
|
49
|
+
<key>CFBundleIdentifier</key>
|
|
50
|
+
<string>com.webtap.chrome-debug</string>
|
|
51
|
+
<key>CFBundleName</key>
|
|
52
|
+
<string>Chrome Debug</string>
|
|
53
|
+
<key>CFBundleDisplayName</key>
|
|
54
|
+
<string>Chrome Debug</string>
|
|
55
|
+
<key>CFBundleVersion</key>
|
|
56
|
+
<string>1.0</string>
|
|
57
|
+
<key>CFBundleShortVersionString</key>
|
|
58
|
+
<string>1.0</string>
|
|
59
|
+
<key>CFBundlePackageType</key>
|
|
60
|
+
<string>APPL</string>
|
|
61
|
+
<key>CFBundleSignature</key>
|
|
62
|
+
<string>????</string>
|
|
63
|
+
<key>LSMinimumSystemVersion</key>
|
|
64
|
+
<string>10.12</string>
|
|
65
|
+
<key>CFBundleDocumentTypes</key>
|
|
66
|
+
<array>
|
|
67
|
+
<dict>
|
|
68
|
+
<key>CFBundleTypeName</key>
|
|
69
|
+
<string>HTML Document</string>
|
|
70
|
+
<key>CFBundleTypeRole</key>
|
|
71
|
+
<string>Viewer</string>
|
|
72
|
+
<key>LSItemContentTypes</key>
|
|
73
|
+
<array>
|
|
74
|
+
<string>public.html</string>
|
|
75
|
+
</array>
|
|
76
|
+
</dict>
|
|
77
|
+
<dict>
|
|
78
|
+
<key>CFBundleTypeName</key>
|
|
79
|
+
<string>Web Location</string>
|
|
80
|
+
<key>CFBundleTypeRole</key>
|
|
81
|
+
<string>Viewer</string>
|
|
82
|
+
<key>LSItemContentTypes</key>
|
|
83
|
+
<array>
|
|
84
|
+
<string>public.url</string>
|
|
85
|
+
</array>
|
|
86
|
+
</dict>
|
|
87
|
+
</array>
|
|
88
|
+
<key>CFBundleURLTypes</key>
|
|
89
|
+
<array>
|
|
90
|
+
<dict>
|
|
91
|
+
<key>CFBundleURLName</key>
|
|
92
|
+
<string>Web site URL</string>
|
|
93
|
+
<key>CFBundleURLSchemes</key>
|
|
94
|
+
<array>
|
|
95
|
+
<string>http</string>
|
|
96
|
+
<string>https</string>
|
|
97
|
+
</array>
|
|
98
|
+
</dict>
|
|
99
|
+
</array>
|
|
100
|
+
</dict>
|
|
101
|
+
</plist>"""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class DesktopSetupService:
|
|
105
|
+
"""Platform-appropriate GUI launcher setup."""
|
|
106
|
+
|
|
107
|
+
def __init__(self):
|
|
108
|
+
self.info = get_platform_info()
|
|
109
|
+
self.paths = self.info["paths"]
|
|
110
|
+
self.chrome = self.info["chrome"]
|
|
111
|
+
|
|
112
|
+
# Unified wrapper path: ~/.local/bin/chrome-debug
|
|
113
|
+
self.wrapper_name = self.chrome["wrapper_name"] # chrome-debug
|
|
114
|
+
self.wrapper_path = self.paths["bin_dir"] / self.wrapper_name
|
|
115
|
+
|
|
116
|
+
def install_launcher(self, force: bool = False) -> Dict[str, Any]:
|
|
117
|
+
"""Install platform-appropriate launcher.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
force: Overwrite existing launcher
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Installation result
|
|
124
|
+
"""
|
|
125
|
+
# Check if wrapper exists first
|
|
126
|
+
if not self.wrapper_path.exists():
|
|
127
|
+
return {
|
|
128
|
+
"success": False,
|
|
129
|
+
"message": "Chrome wrapper 'chrome-debug' not found. Run 'setup-chrome' first",
|
|
130
|
+
"path": None,
|
|
131
|
+
"details": f"Expected wrapper at {self.wrapper_path}",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if self.info["is_macos"]:
|
|
135
|
+
return self._install_macos_app(force)
|
|
136
|
+
else:
|
|
137
|
+
return self._install_linux_desktop(force)
|
|
138
|
+
|
|
139
|
+
def _install_macos_app(self, force: bool) -> Dict[str, Any]:
|
|
140
|
+
"""Create .app bundle for macOS.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
force: Overwrite existing app
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Installation result
|
|
147
|
+
"""
|
|
148
|
+
app_path = self.paths["applications_dir"] / "Chrome Debug.app"
|
|
149
|
+
|
|
150
|
+
if app_path.exists() and not force:
|
|
151
|
+
return {
|
|
152
|
+
"success": False,
|
|
153
|
+
"message": "Chrome Debug app already exists",
|
|
154
|
+
"path": str(app_path),
|
|
155
|
+
"details": "Use --force to overwrite",
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Create app structure
|
|
159
|
+
contents_dir = app_path / "Contents"
|
|
160
|
+
macos_dir = contents_dir / "MacOS"
|
|
161
|
+
macos_dir.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
|
|
163
|
+
# Create launcher script
|
|
164
|
+
launcher_path = macos_dir / "Chrome Debug"
|
|
165
|
+
launcher_content = f"""#!/bin/bash
|
|
166
|
+
# Chrome Debug app launcher
|
|
167
|
+
# Uses absolute path to wrapper
|
|
168
|
+
exec "{self.wrapper_path.expanduser()}" "$@"
|
|
151
169
|
"""
|
|
170
|
+
launcher_path.write_text(launcher_content)
|
|
171
|
+
launcher_path.chmod(0o755)
|
|
172
|
+
|
|
173
|
+
# Create Info.plist
|
|
174
|
+
plist_path = contents_dir / "Info.plist"
|
|
175
|
+
plist_path.write_text(MACOS_INFO_PLIST)
|
|
176
|
+
|
|
177
|
+
logger.info(f"Created Chrome Debug app at {app_path}")
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
"success": True,
|
|
181
|
+
"message": "Chrome Debug app created successfully",
|
|
182
|
+
"path": str(app_path),
|
|
183
|
+
"details": "Available in Launchpad and Spotlight search",
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
def _install_linux_desktop(self, force: bool) -> Dict[str, Any]:
|
|
187
|
+
"""Install .desktop file for Linux.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
force: Overwrite existing desktop entry
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Installation result
|
|
194
|
+
"""
|
|
195
|
+
# Create separate Chrome Debug desktop entry (doesn't override system Chrome)
|
|
196
|
+
desktop_path = self.paths["applications_dir"] / "chrome-debug.desktop"
|
|
197
|
+
|
|
198
|
+
if desktop_path.exists() and not force:
|
|
199
|
+
return {
|
|
200
|
+
"success": False,
|
|
201
|
+
"message": "Desktop entry already exists",
|
|
202
|
+
"path": str(desktop_path),
|
|
203
|
+
"details": "Use --force to overwrite",
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Create Chrome Debug desktop entry
|
|
207
|
+
desktop_content = self._create_chrome_debug_desktop()
|
|
208
|
+
|
|
209
|
+
# Create directory and save
|
|
210
|
+
desktop_path.parent.mkdir(parents=True, exist_ok=True)
|
|
211
|
+
desktop_path.write_text(desktop_content)
|
|
212
|
+
desktop_path.chmod(0o644) # Standard permissions for desktop files
|
|
213
|
+
|
|
214
|
+
logger.info(f"Installed desktop entry to {desktop_path}")
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
"success": True,
|
|
218
|
+
"message": "Installed Chrome Debug desktop entry",
|
|
219
|
+
"path": str(desktop_path),
|
|
220
|
+
"details": "Available in application menu as 'Chrome Debug'",
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
def _create_chrome_debug_desktop(self) -> str:
|
|
224
|
+
"""Create Chrome Debug desktop entry with absolute paths."""
|
|
225
|
+
# Use absolute expanded path for Exec lines
|
|
226
|
+
wrapper_abs_path = self.wrapper_path.expanduser()
|
|
227
|
+
|
|
228
|
+
return LINUX_DESKTOP_ENTRY.format(wrapper_path=wrapper_abs_path)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
"""Chrome extension setup
|
|
1
|
+
"""Chrome extension setup service (cross-platform)."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
-
from pathlib import Path
|
|
6
5
|
from typing import Dict, Any
|
|
7
6
|
|
|
8
7
|
import requests
|
|
9
8
|
|
|
9
|
+
from .platform import get_platform_info, ensure_directories
|
|
10
|
+
|
|
10
11
|
logger = logging.getLogger(__name__)
|
|
11
12
|
|
|
12
13
|
# GitHub URLs for extension files
|
|
@@ -14,75 +15,87 @@ EXTENSION_BASE_URL = "https://raw.githubusercontent.com/angelsen/tap-tools/main/
|
|
|
14
15
|
EXTENSION_FILES = ["manifest.json", "popup.html", "popup.js"]
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
class ExtensionSetupService:
|
|
19
|
+
"""Chrome extension installation service."""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.info = get_platform_info()
|
|
23
|
+
self.paths = self.info["paths"]
|
|
24
|
+
|
|
25
|
+
# Extension goes in data directory (persistent app data)
|
|
26
|
+
self.extension_dir = self.paths["data_dir"] / "extension"
|
|
27
|
+
|
|
28
|
+
def install_extension(self, force: bool = False) -> Dict[str, Any]:
|
|
29
|
+
"""Install Chrome extension to platform-appropriate location.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
force: Overwrite existing files
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Installation result
|
|
36
|
+
"""
|
|
37
|
+
# Check if exists (manifest.json is required file)
|
|
38
|
+
if (self.extension_dir / "manifest.json").exists() and not force:
|
|
39
|
+
return {
|
|
40
|
+
"success": False,
|
|
41
|
+
"message": f"Extension already exists at {self.extension_dir}",
|
|
42
|
+
"path": str(self.extension_dir),
|
|
43
|
+
"details": "Use --force to overwrite",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Ensure base directories exist
|
|
47
|
+
ensure_directories()
|
|
48
|
+
|
|
49
|
+
# Create extension directory
|
|
50
|
+
self.extension_dir.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
# Download each file
|
|
53
|
+
downloaded = []
|
|
54
|
+
failed = []
|
|
55
|
+
|
|
56
|
+
for filename in EXTENSION_FILES:
|
|
57
|
+
url = f"{EXTENSION_BASE_URL}/{filename}"
|
|
58
|
+
target_file = self.extension_dir / filename
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
logger.info(f"Downloading {filename}")
|
|
62
|
+
response = requests.get(url, timeout=10)
|
|
63
|
+
response.raise_for_status()
|
|
64
|
+
|
|
65
|
+
# For manifest.json, validate it's proper JSON
|
|
66
|
+
if filename == "manifest.json":
|
|
67
|
+
json.loads(response.text)
|
|
68
|
+
|
|
69
|
+
target_file.write_text(response.text)
|
|
70
|
+
downloaded.append(filename)
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"Failed to download {filename}: {e}")
|
|
74
|
+
failed.append(filename)
|
|
75
|
+
|
|
76
|
+
# Determine success level
|
|
77
|
+
if not downloaded:
|
|
78
|
+
return {
|
|
79
|
+
"success": False,
|
|
80
|
+
"message": "Failed to download any extension files",
|
|
81
|
+
"path": None,
|
|
82
|
+
"details": "Check network connection and try again",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if failed:
|
|
86
|
+
# Partial success - some files downloaded
|
|
87
|
+
return {
|
|
88
|
+
"success": True, # Partial is still success
|
|
89
|
+
"message": f"Downloaded {len(downloaded)}/{len(EXTENSION_FILES)} files",
|
|
90
|
+
"path": str(self.extension_dir),
|
|
91
|
+
"details": f"Failed: {', '.join(failed)}",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
logger.info(f"Extension installed to {self.extension_dir}")
|
|
28
95
|
|
|
29
|
-
# Check if exists (manifest.json is required file)
|
|
30
|
-
if (target_dir / "manifest.json").exists() and not force:
|
|
31
96
|
return {
|
|
32
|
-
"success":
|
|
33
|
-
"message":
|
|
34
|
-
"path": str(
|
|
35
|
-
"details": "
|
|
97
|
+
"success": True,
|
|
98
|
+
"message": "Downloaded Chrome extension",
|
|
99
|
+
"path": str(self.extension_dir),
|
|
100
|
+
"details": f"Files: {', '.join(downloaded)}",
|
|
36
101
|
}
|
|
37
|
-
|
|
38
|
-
# Create directory
|
|
39
|
-
target_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
-
|
|
41
|
-
# Download each file
|
|
42
|
-
downloaded = []
|
|
43
|
-
failed = []
|
|
44
|
-
|
|
45
|
-
for filename in EXTENSION_FILES:
|
|
46
|
-
url = f"{EXTENSION_BASE_URL}/{filename}"
|
|
47
|
-
target_file = target_dir / filename
|
|
48
|
-
|
|
49
|
-
try:
|
|
50
|
-
logger.info(f"Downloading {filename}")
|
|
51
|
-
response = requests.get(url, timeout=10)
|
|
52
|
-
response.raise_for_status()
|
|
53
|
-
|
|
54
|
-
# For manifest.json, validate it's proper JSON
|
|
55
|
-
if filename == "manifest.json":
|
|
56
|
-
json.loads(response.text)
|
|
57
|
-
|
|
58
|
-
target_file.write_text(response.text)
|
|
59
|
-
downloaded.append(filename)
|
|
60
|
-
|
|
61
|
-
except Exception as e:
|
|
62
|
-
logger.error(f"Failed to download {filename}: {e}")
|
|
63
|
-
failed.append(filename)
|
|
64
|
-
|
|
65
|
-
# Determine success level
|
|
66
|
-
if not downloaded:
|
|
67
|
-
return {
|
|
68
|
-
"success": False,
|
|
69
|
-
"message": "Failed to download any extension files",
|
|
70
|
-
"path": None,
|
|
71
|
-
"details": "Check network connection and try again",
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if failed:
|
|
75
|
-
# Partial success - some files downloaded
|
|
76
|
-
return {
|
|
77
|
-
"success": True, # Partial is still success
|
|
78
|
-
"message": f"Downloaded {len(downloaded)}/{len(EXTENSION_FILES)} files",
|
|
79
|
-
"path": str(target_dir),
|
|
80
|
-
"details": f"Failed: {', '.join(failed)}",
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
"success": True,
|
|
85
|
-
"message": "Downloaded Chrome extension",
|
|
86
|
-
"path": str(target_dir),
|
|
87
|
-
"details": f"Files: {', '.join(downloaded)}",
|
|
88
|
-
}
|
webtap/services/setup/filters.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Filter setup
|
|
1
|
+
"""Filter setup service (cross-platform)."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
@@ -7,73 +7,83 @@ from typing import Dict, Any
|
|
|
7
7
|
|
|
8
8
|
import requests
|
|
9
9
|
|
|
10
|
+
from .platform import get_platform_info
|
|
11
|
+
|
|
10
12
|
logger = logging.getLogger(__name__)
|
|
11
13
|
|
|
12
14
|
# GitHub URL for filters
|
|
13
15
|
FILTERS_URL = "https://raw.githubusercontent.com/angelsen/tap-tools/main/packages/webtap/data/filters.json"
|
|
14
16
|
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
Args:
|
|
20
|
-
force: Overwrite existing file
|
|
18
|
+
class FilterSetupService:
|
|
19
|
+
"""Filter configuration installation service."""
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# Same path that FilterManager uses
|
|
26
|
-
target_path = Path.cwd() / ".webtap" / "filters.json"
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.info = get_platform_info()
|
|
23
|
+
self.paths = self.info["paths"]
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"success": False,
|
|
32
|
-
"message": f"Filters already exist at {target_path}",
|
|
33
|
-
"path": str(target_path),
|
|
34
|
-
"details": "Use --force to overwrite",
|
|
35
|
-
}
|
|
25
|
+
# Filters go in the current working directory's .webtap folder
|
|
26
|
+
# (project-specific, not global)
|
|
27
|
+
self.filters_path = Path.cwd() / ".webtap" / "filters.json"
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
logger.info(f"Downloading filters from {FILTERS_URL}")
|
|
40
|
-
response = requests.get(FILTERS_URL, timeout=10)
|
|
41
|
-
response.raise_for_status()
|
|
29
|
+
def install_filters(self, force: bool = False) -> Dict[str, Any]:
|
|
30
|
+
"""Install filters to .webtap/filters.json in current directory.
|
|
42
31
|
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
Args:
|
|
33
|
+
force: Overwrite existing file
|
|
45
34
|
|
|
46
|
-
|
|
47
|
-
|
|
35
|
+
Returns:
|
|
36
|
+
Installation result
|
|
37
|
+
"""
|
|
38
|
+
# Check if exists
|
|
39
|
+
if self.filters_path.exists() and not force:
|
|
48
40
|
return {
|
|
49
41
|
"success": False,
|
|
50
|
-
"message": "
|
|
51
|
-
"path":
|
|
52
|
-
"details":
|
|
42
|
+
"message": f"Filters already exist at {self.filters_path}",
|
|
43
|
+
"path": str(self.filters_path),
|
|
44
|
+
"details": "Use --force to overwrite",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Download from GitHub
|
|
48
|
+
try:
|
|
49
|
+
logger.info(f"Downloading filters from {FILTERS_URL}")
|
|
50
|
+
response = requests.get(FILTERS_URL, timeout=10)
|
|
51
|
+
response.raise_for_status()
|
|
52
|
+
|
|
53
|
+
# Validate it's proper JSON
|
|
54
|
+
filters_data = json.loads(response.text)
|
|
55
|
+
|
|
56
|
+
# Quick validation - should have dict structure
|
|
57
|
+
if not isinstance(filters_data, dict):
|
|
58
|
+
return {
|
|
59
|
+
"success": False,
|
|
60
|
+
"message": "Invalid filter format - expected JSON object",
|
|
61
|
+
"path": None,
|
|
62
|
+
"details": None,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Count categories for user feedback
|
|
66
|
+
category_count = len(filters_data)
|
|
67
|
+
|
|
68
|
+
# Create directory and save
|
|
69
|
+
self.filters_path.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
self.filters_path.write_text(response.text)
|
|
71
|
+
|
|
72
|
+
logger.info(f"Saved {category_count} filter categories to {self.filters_path}")
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
"success": True,
|
|
76
|
+
"message": f"Downloaded {category_count} filter categories",
|
|
77
|
+
"path": str(self.filters_path),
|
|
78
|
+
"details": f"Categories: {', '.join(filters_data.keys())}",
|
|
53
79
|
}
|
|
54
80
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
"success": True,
|
|
66
|
-
"message": f"Downloaded {category_count} filter categories",
|
|
67
|
-
"path": str(target_path),
|
|
68
|
-
"details": f"Categories: {', '.join(filters_data.keys())}",
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
except requests.RequestException as e:
|
|
72
|
-
logger.error(f"Network error downloading filters: {e}")
|
|
73
|
-
return {"success": False, "message": f"Network error: {e}", "path": None, "details": None}
|
|
74
|
-
except json.JSONDecodeError as e:
|
|
75
|
-
logger.error(f"Invalid JSON in filters: {e}")
|
|
76
|
-
return {"success": False, "message": f"Invalid JSON format: {e}", "path": None, "details": None}
|
|
77
|
-
except Exception as e:
|
|
78
|
-
logger.error(f"Unexpected error: {e}")
|
|
79
|
-
return {"success": False, "message": f"Failed to download filters: {e}", "path": None, "details": None}
|
|
81
|
+
except requests.RequestException as e:
|
|
82
|
+
logger.error(f"Network error downloading filters: {e}")
|
|
83
|
+
return {"success": False, "message": f"Network error: {e}", "path": None, "details": None}
|
|
84
|
+
except json.JSONDecodeError as e:
|
|
85
|
+
logger.error(f"Invalid JSON in filters: {e}")
|
|
86
|
+
return {"success": False, "message": f"Invalid JSON format: {e}", "path": None, "details": None}
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.error(f"Unexpected error: {e}")
|
|
89
|
+
return {"success": False, "message": f"Failed to download filters: {e}", "path": None, "details": None}
|