webtap-tool 0.11.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 (64) hide show
  1. webtap/VISION.md +246 -0
  2. webtap/__init__.py +84 -0
  3. webtap/__main__.py +6 -0
  4. webtap/api/__init__.py +9 -0
  5. webtap/api/app.py +26 -0
  6. webtap/api/models.py +69 -0
  7. webtap/api/server.py +111 -0
  8. webtap/api/sse.py +182 -0
  9. webtap/api/state.py +89 -0
  10. webtap/app.py +79 -0
  11. webtap/cdp/README.md +275 -0
  12. webtap/cdp/__init__.py +12 -0
  13. webtap/cdp/har.py +302 -0
  14. webtap/cdp/schema/README.md +41 -0
  15. webtap/cdp/schema/cdp_protocol.json +32785 -0
  16. webtap/cdp/schema/cdp_version.json +8 -0
  17. webtap/cdp/session.py +667 -0
  18. webtap/client.py +81 -0
  19. webtap/commands/DEVELOPER_GUIDE.md +401 -0
  20. webtap/commands/TIPS.md +269 -0
  21. webtap/commands/__init__.py +29 -0
  22. webtap/commands/_builders.py +331 -0
  23. webtap/commands/_code_generation.py +110 -0
  24. webtap/commands/_tips.py +147 -0
  25. webtap/commands/_utils.py +273 -0
  26. webtap/commands/connection.py +220 -0
  27. webtap/commands/console.py +87 -0
  28. webtap/commands/fetch.py +310 -0
  29. webtap/commands/filters.py +116 -0
  30. webtap/commands/javascript.py +73 -0
  31. webtap/commands/js_export.py +73 -0
  32. webtap/commands/launch.py +72 -0
  33. webtap/commands/navigation.py +197 -0
  34. webtap/commands/network.py +136 -0
  35. webtap/commands/quicktype.py +306 -0
  36. webtap/commands/request.py +93 -0
  37. webtap/commands/selections.py +138 -0
  38. webtap/commands/setup.py +219 -0
  39. webtap/commands/to_model.py +163 -0
  40. webtap/daemon.py +185 -0
  41. webtap/daemon_state.py +53 -0
  42. webtap/filters.py +219 -0
  43. webtap/rpc/__init__.py +14 -0
  44. webtap/rpc/errors.py +49 -0
  45. webtap/rpc/framework.py +223 -0
  46. webtap/rpc/handlers.py +625 -0
  47. webtap/rpc/machine.py +84 -0
  48. webtap/services/README.md +83 -0
  49. webtap/services/__init__.py +15 -0
  50. webtap/services/console.py +124 -0
  51. webtap/services/dom.py +547 -0
  52. webtap/services/fetch.py +415 -0
  53. webtap/services/main.py +392 -0
  54. webtap/services/network.py +401 -0
  55. webtap/services/setup/__init__.py +185 -0
  56. webtap/services/setup/chrome.py +233 -0
  57. webtap/services/setup/desktop.py +255 -0
  58. webtap/services/setup/extension.py +147 -0
  59. webtap/services/setup/platform.py +162 -0
  60. webtap/services/state_snapshot.py +86 -0
  61. webtap_tool-0.11.0.dist-info/METADATA +535 -0
  62. webtap_tool-0.11.0.dist-info/RECORD +64 -0
  63. webtap_tool-0.11.0.dist-info/WHEEL +4 -0
  64. webtap_tool-0.11.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,233 @@
1
+ """Chrome setup service for installing wrapper scripts (cross-platform).
2
+
3
+ PUBLIC API:
4
+ - ChromeSetupService: Chrome wrapper script installation
5
+ """
6
+
7
+ import os
8
+ import logging
9
+ from typing import Dict, Any
10
+
11
+ from .platform import get_platform_info, ensure_directories
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Wrapper script templates
16
+ LINUX_BINDFS_WRAPPER = """#!/bin/bash
17
+ # Chrome Debug Launcher for Linux (bindfs mode)
18
+ # Generated by webtap setup-chrome --bindfs
19
+ # Mounts your real Chrome profile via bindfs for debugging with full session state
20
+
21
+ DEBUG_DIR="$HOME/.config/google-chrome-debug"
22
+ REAL_DIR="$HOME/.config/google-chrome"
23
+ PORT=${{WEBTAP_PORT:-9222}}
24
+ CHROME_BIN="{chrome_path}"
25
+
26
+ # Check if bindfs is installed
27
+ if ! command -v bindfs &>/dev/null; then
28
+ echo "Error: bindfs not installed. Install with:" >&2
29
+ echo " Ubuntu/Debian: sudo apt install bindfs" >&2
30
+ echo " Arch: yay -S bindfs" >&2
31
+ echo " Fedora: sudo dnf install bindfs" >&2
32
+ exit 1
33
+ fi
34
+
35
+ # Check if real Chrome profile exists
36
+ if [ ! -d "$REAL_DIR" ]; then
37
+ echo "Error: Chrome profile not found at $REAL_DIR" >&2
38
+ echo "Please run Chrome normally first to create a profile" >&2
39
+ exit 1
40
+ fi
41
+
42
+ # Mount real profile via bindfs if not already mounted
43
+ if ! mountpoint -q "$DEBUG_DIR" 2>/dev/null; then
44
+ mkdir -p "$DEBUG_DIR"
45
+ if ! bindfs --no-allow-other "$REAL_DIR" "$DEBUG_DIR"; then
46
+ echo "Error: Failed to mount Chrome profile via bindfs" >&2
47
+ exit 1
48
+ fi
49
+ echo "Chrome debug profile mounted. To unmount: fusermount -u $DEBUG_DIR" >&2
50
+ fi
51
+
52
+ # Launch Chrome with debugging on bindfs mount
53
+ exec "$CHROME_BIN" \\
54
+ --remote-debugging-port="$PORT" \\
55
+ --remote-allow-origins='*' \\
56
+ --user-data-dir="$DEBUG_DIR" \\
57
+ "$@"
58
+ """
59
+
60
+ LINUX_STANDARD_WRAPPER = """#!/bin/bash
61
+ # Chrome Debug Launcher for Linux
62
+ # Generated by webtap setup-chrome
63
+
64
+ # Configuration
65
+ PORT=${{WEBTAP_PORT:-9222}}
66
+ PROFILE_BASE="{profile_dir}"
67
+ CHROME_BIN="{chrome_path}"
68
+
69
+ # Profile handling
70
+ if [ "$1" = "--temp" ]; then
71
+ PROFILE_DIR="$(mktemp -d /tmp/webtap-chrome-XXXXXX)"
72
+ shift
73
+ echo "Using temporary profile: $PROFILE_DIR" >&2
74
+ else
75
+ PROFILE_DIR="$PROFILE_BASE/default"
76
+ mkdir -p "$PROFILE_DIR"
77
+ fi
78
+
79
+ # Launch Chrome with debugging
80
+ exec "$CHROME_BIN" \\
81
+ --remote-debugging-port="$PORT" \\
82
+ --remote-allow-origins='*' \\
83
+ --user-data-dir="$PROFILE_DIR" \\
84
+ --no-first-run \\
85
+ --no-default-browser-check \\
86
+ "$@"
87
+ """
88
+
89
+ MACOS_WRAPPER = """#!/bin/bash
90
+ # Chrome Debug Launcher for macOS
91
+ # Generated by webtap setup-chrome
92
+
93
+ # Configuration
94
+ PORT=${{WEBTAP_PORT:-9222}}
95
+ PROFILE_BASE="{profile_dir}"
96
+ CHROME_APP="{chrome_path}"
97
+
98
+ # Profile handling
99
+ if [ "$1" = "--temp" ]; then
100
+ PROFILE_DIR="$(mktemp -d /tmp/webtap-chrome-XXXXXX)"
101
+ shift
102
+ echo "Using temporary profile: $PROFILE_DIR" >&2
103
+ else
104
+ PROFILE_DIR="$PROFILE_BASE/default"
105
+ mkdir -p "$PROFILE_DIR"
106
+ fi
107
+
108
+ # Launch Chrome with debugging
109
+ exec "$CHROME_APP" \\
110
+ --remote-debugging-port="$PORT" \\
111
+ --remote-allow-origins='*' \\
112
+ --user-data-dir="$PROFILE_DIR" \\
113
+ --no-first-run \\
114
+ --no-default-browser-check \\
115
+ "$@"
116
+ """
117
+
118
+
119
+ class ChromeSetupService:
120
+ """Chrome wrapper installation service (cross-platform)."""
121
+
122
+ def __init__(self):
123
+ self.info = get_platform_info()
124
+ self.paths = self.info["paths"]
125
+ self.chrome = self.info["chrome"]
126
+
127
+ # Unified wrapper location for both platforms
128
+ self.wrapper_dir = self.paths["bin_dir"] # ~/.local/bin
129
+ self.wrapper_name = self.chrome["wrapper_name"] # chrome-debug
130
+ self.wrapper_path = self.wrapper_dir / self.wrapper_name
131
+
132
+ # Profile locations
133
+ self.profile_dir = self.paths["data_dir"] / "profiles"
134
+ self.temp_profile_dir = self.paths["runtime_dir"] / "profiles"
135
+
136
+ def install_wrapper(self, force: bool = False, bindfs: bool = False) -> Dict[str, Any]:
137
+ """Install Chrome wrapper script appropriate for platform.
138
+
139
+ Args:
140
+ force: Overwrite existing wrapper
141
+ bindfs: Use bindfs to mount real Chrome profile (Linux only)
142
+
143
+ Returns:
144
+ Installation result
145
+ """
146
+ if not self.chrome["found"]:
147
+ return {
148
+ "success": False,
149
+ "message": "Chrome not found on system",
150
+ "details": "Please install Google Chrome first",
151
+ }
152
+
153
+ if self.wrapper_path.exists() and not force:
154
+ return {
155
+ "success": False,
156
+ "message": f"Wrapper already exists at {self.wrapper_path}",
157
+ "details": "Use --force to overwrite",
158
+ "path": str(self.wrapper_path),
159
+ }
160
+
161
+ # Ensure directories exist
162
+ ensure_directories()
163
+ self.wrapper_dir.mkdir(parents=True, exist_ok=True)
164
+
165
+ # Generate platform-specific wrapper
166
+ if self.info["is_macos"]:
167
+ wrapper_content = self._generate_macos_wrapper()
168
+ else:
169
+ wrapper_content = self._generate_linux_wrapper(bindfs=bindfs)
170
+
171
+ self.wrapper_path.write_text(wrapper_content)
172
+ self.wrapper_path.chmod(0o755)
173
+
174
+ # Check if wrapper dir is in PATH
175
+ path_dirs = os.environ.get("PATH", "").split(os.pathsep)
176
+ in_path = str(self.wrapper_dir) in path_dirs
177
+
178
+ logger.info(f"Installed Chrome wrapper to {self.wrapper_path}")
179
+
180
+ # Build detailed message with PATH setup instructions
181
+ if in_path:
182
+ details = "✅ Run 'chrome-debug' to launch Chrome with debugging"
183
+ else:
184
+ shell = os.environ.get("SHELL", "/bin/bash")
185
+ if "zsh" in shell:
186
+ rc_file = "~/.zshrc"
187
+ elif "fish" in shell:
188
+ rc_file = "~/.config/fish/config.fish"
189
+ else:
190
+ rc_file = "~/.bashrc"
191
+
192
+ details = (
193
+ f"⚠️ Add to PATH to use from terminal:\n"
194
+ f"echo 'export PATH=\"$HOME/.local/bin:$PATH\"' >> {rc_file}\n"
195
+ f"source {rc_file}\n\n"
196
+ f"Or run directly: ~/.local/bin/chrome-debug\n"
197
+ f"✅ GUI launcher will work regardless (uses full path)"
198
+ )
199
+
200
+ return {
201
+ "success": True,
202
+ "message": "Chrome wrapper 'chrome-debug' installed successfully",
203
+ "path": str(self.wrapper_path),
204
+ "details": details,
205
+ }
206
+
207
+ def _generate_linux_wrapper(self, bindfs: bool = False) -> str:
208
+ """Generate Linux wrapper script with optional bindfs support.
209
+
210
+ Args:
211
+ bindfs: If True, use bindfs to mount real Chrome profile
212
+
213
+ Returns:
214
+ Wrapper script content
215
+ """
216
+ chrome_path = self.chrome["path"]
217
+ profile_dir = self.profile_dir
218
+
219
+ if bindfs:
220
+ return LINUX_BINDFS_WRAPPER.format(chrome_path=chrome_path)
221
+ else:
222
+ return LINUX_STANDARD_WRAPPER.format(chrome_path=chrome_path, profile_dir=profile_dir)
223
+
224
+ def _generate_macos_wrapper(self) -> str:
225
+ """Generate macOS wrapper script.
226
+
227
+ Returns:
228
+ Wrapper script content
229
+ """
230
+ chrome_path = self.chrome["path"]
231
+ profile_dir = self.profile_dir
232
+
233
+ return MACOS_WRAPPER.format(chrome_path=chrome_path, profile_dir=profile_dir)
@@ -0,0 +1,255 @@
1
+ """Desktop/Application launcher setup (cross-platform).
2
+
3
+ PUBLIC API:
4
+ - DesktopSetupService: Desktop entry/app bundle installation
5
+ """
6
+
7
+ import logging
8
+ from typing import Dict, Any
9
+
10
+ from .platform import get_platform_info
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Desktop entry template for Linux
15
+ LINUX_DESKTOP_ENTRY = """[Desktop Entry]
16
+ Version=1.0
17
+ Type=Application
18
+ Name=Chrome Debug
19
+ GenericName=Web Browser (Debug Mode)
20
+ Comment=Chrome with remote debugging enabled
21
+ Icon=google-chrome
22
+ Categories=Development;WebBrowser;
23
+ 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;
24
+ StartupWMClass=Google-chrome
25
+ StartupNotify=true
26
+ Terminal=false
27
+ Exec={wrapper_path} %U
28
+ Actions=new-window;new-private-window;temp-profile;
29
+
30
+ [Desktop Action new-window]
31
+ Name=New Window
32
+ StartupWMClass=Google-chrome
33
+ Exec={wrapper_path}
34
+
35
+ [Desktop Action new-private-window]
36
+ Name=New Incognito Window
37
+ StartupWMClass=Google-chrome
38
+ Exec={wrapper_path} --incognito
39
+
40
+ [Desktop Action temp-profile]
41
+ Name=New Window (Temp Profile)
42
+ StartupWMClass=Google-chrome
43
+ Exec={wrapper_path} --temp
44
+ """
45
+
46
+ # Info.plist template for macOS app bundle
47
+ MACOS_INFO_PLIST = """<?xml version="1.0" encoding="UTF-8"?>
48
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
49
+ <plist version="1.0">
50
+ <dict>
51
+ <key>CFBundleExecutable</key>
52
+ <string>Chrome Debug</string>
53
+ <key>CFBundleIdentifier</key>
54
+ <string>com.webtap.chrome-debug</string>
55
+ <key>CFBundleName</key>
56
+ <string>Chrome Debug</string>
57
+ <key>CFBundleDisplayName</key>
58
+ <string>Chrome Debug</string>
59
+ <key>CFBundleVersion</key>
60
+ <string>1.0</string>
61
+ <key>CFBundleShortVersionString</key>
62
+ <string>1.0</string>
63
+ <key>CFBundlePackageType</key>
64
+ <string>APPL</string>
65
+ <key>CFBundleSignature</key>
66
+ <string>????</string>
67
+ <key>LSMinimumSystemVersion</key>
68
+ <string>10.12</string>
69
+ <key>LSArchitecturePriority</key>
70
+ <array>
71
+ <string>arm64</string>
72
+ <string>x86_64</string>
73
+ </array>
74
+ <key>CFBundleDocumentTypes</key>
75
+ <array>
76
+ <dict>
77
+ <key>CFBundleTypeName</key>
78
+ <string>HTML Document</string>
79
+ <key>CFBundleTypeRole</key>
80
+ <string>Viewer</string>
81
+ <key>LSItemContentTypes</key>
82
+ <array>
83
+ <string>public.html</string>
84
+ </array>
85
+ </dict>
86
+ <dict>
87
+ <key>CFBundleTypeName</key>
88
+ <string>Web Location</string>
89
+ <key>CFBundleTypeRole</key>
90
+ <string>Viewer</string>
91
+ <key>LSItemContentTypes</key>
92
+ <array>
93
+ <string>public.url</string>
94
+ </array>
95
+ </dict>
96
+ </array>
97
+ <key>CFBundleURLTypes</key>
98
+ <array>
99
+ <dict>
100
+ <key>CFBundleURLName</key>
101
+ <string>Web site URL</string>
102
+ <key>CFBundleURLSchemes</key>
103
+ <array>
104
+ <string>http</string>
105
+ <string>https</string>
106
+ </array>
107
+ </dict>
108
+ </array>
109
+ </dict>
110
+ </plist>"""
111
+
112
+
113
+ class DesktopSetupService:
114
+ """Platform-appropriate GUI launcher setup."""
115
+
116
+ def __init__(self):
117
+ self.info = get_platform_info()
118
+ self.paths = self.info["paths"]
119
+ self.chrome = self.info["chrome"]
120
+
121
+ # Unified wrapper path: ~/.local/bin/chrome-debug
122
+ self.wrapper_name = self.chrome["wrapper_name"] # chrome-debug
123
+ self.wrapper_path = self.paths["bin_dir"] / self.wrapper_name
124
+
125
+ def install_launcher(self, force: bool = False) -> Dict[str, Any]:
126
+ """Install platform-appropriate launcher.
127
+
128
+ Args:
129
+ force: Overwrite existing launcher
130
+
131
+ Returns:
132
+ Installation result
133
+ """
134
+ # Check if wrapper exists first
135
+ if not self.wrapper_path.exists():
136
+ return {
137
+ "success": False,
138
+ "message": "Chrome wrapper 'chrome-debug' not found. Run 'setup-chrome' first",
139
+ "path": None,
140
+ "details": f"Expected wrapper at {self.wrapper_path}",
141
+ }
142
+
143
+ if self.info["is_macos"]:
144
+ return self._install_macos_app(force)
145
+ else:
146
+ return self._install_linux_desktop(force)
147
+
148
+ def _install_macos_app(self, force: bool) -> Dict[str, Any]:
149
+ """Create .app bundle for macOS.
150
+
151
+ Args:
152
+ force: Overwrite existing app
153
+
154
+ Returns:
155
+ Installation result
156
+ """
157
+ app_path = self.paths["applications_dir"] / "Chrome Debug.app"
158
+
159
+ if app_path.exists() and not force:
160
+ return {
161
+ "success": False,
162
+ "message": "Chrome Debug app already exists",
163
+ "path": str(app_path),
164
+ "details": "Use --force to overwrite",
165
+ }
166
+
167
+ # Create app structure
168
+ contents_dir = app_path / "Contents"
169
+ macos_dir = contents_dir / "MacOS"
170
+ macos_dir.mkdir(parents=True, exist_ok=True)
171
+
172
+ # Create launcher script that directly launches Chrome
173
+ # This avoids Rosetta warnings from nested bash scripts
174
+ launcher_path = macos_dir / "Chrome Debug"
175
+
176
+ # Get Chrome path from platform info
177
+ chrome_path = self.chrome["path"]
178
+ profile_dir = self.paths["data_dir"] / "profiles" / "default"
179
+
180
+ launcher_content = f"""#!/bin/bash
181
+ # Chrome Debug app launcher - direct Chrome execution
182
+ # Avoids Rosetta warnings by directly launching Chrome
183
+
184
+ PORT=${{WEBTAP_PORT:-9222}}
185
+ PROFILE_DIR="{profile_dir}"
186
+ mkdir -p "$PROFILE_DIR"
187
+
188
+ # Launch Chrome directly with debugging
189
+ exec "{chrome_path}" \\
190
+ --remote-debugging-port="$PORT" \\
191
+ --remote-allow-origins='*' \\
192
+ --user-data-dir="$PROFILE_DIR" \\
193
+ --no-first-run \\
194
+ --no-default-browser-check \\
195
+ "$@"
196
+ """
197
+ launcher_path.write_text(launcher_content)
198
+ launcher_path.chmod(0o755)
199
+
200
+ # Create Info.plist
201
+ plist_path = contents_dir / "Info.plist"
202
+ plist_path.write_text(MACOS_INFO_PLIST)
203
+
204
+ logger.info(f"Created Chrome Debug app at {app_path}")
205
+
206
+ return {
207
+ "success": True,
208
+ "message": "Chrome Debug app created successfully",
209
+ "path": str(app_path),
210
+ "details": "Available in Launchpad and Spotlight search",
211
+ }
212
+
213
+ def _install_linux_desktop(self, force: bool) -> Dict[str, Any]:
214
+ """Install .desktop file for Linux.
215
+
216
+ Args:
217
+ force: Overwrite existing desktop entry
218
+
219
+ Returns:
220
+ Installation result
221
+ """
222
+ # Create separate Chrome Debug desktop entry (doesn't override system Chrome)
223
+ desktop_path = self.paths["applications_dir"] / "chrome-debug.desktop"
224
+
225
+ if desktop_path.exists() and not force:
226
+ return {
227
+ "success": False,
228
+ "message": "Desktop entry already exists",
229
+ "path": str(desktop_path),
230
+ "details": "Use --force to overwrite",
231
+ }
232
+
233
+ # Create Chrome Debug desktop entry
234
+ desktop_content = self._create_chrome_debug_desktop()
235
+
236
+ # Create directory and save
237
+ desktop_path.parent.mkdir(parents=True, exist_ok=True)
238
+ desktop_path.write_text(desktop_content)
239
+ desktop_path.chmod(0o644) # Standard permissions for desktop files
240
+
241
+ logger.info(f"Installed desktop entry to {desktop_path}")
242
+
243
+ return {
244
+ "success": True,
245
+ "message": "Installed Chrome Debug desktop entry",
246
+ "path": str(desktop_path),
247
+ "details": "Available in application menu as 'Chrome Debug'",
248
+ }
249
+
250
+ def _create_chrome_debug_desktop(self) -> str:
251
+ """Create Chrome Debug desktop entry with absolute paths."""
252
+ # Use absolute expanded path for Exec lines
253
+ wrapper_abs_path = self.wrapper_path.expanduser()
254
+
255
+ return LINUX_DESKTOP_ENTRY.format(wrapper_path=wrapper_abs_path)
@@ -0,0 +1,147 @@
1
+ """Chrome extension setup service (cross-platform).
2
+
3
+ PUBLIC API:
4
+ - ExtensionSetupService: Chrome extension file installation
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ from typing import Dict, Any
10
+
11
+ import requests
12
+
13
+ from .platform import get_platform_info, ensure_directories
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # GitHub URLs for extension files
18
+ EXTENSION_BASE_URL = "https://raw.githubusercontent.com/angelsen/tap-tools/main/packages/webtap/extension"
19
+ EXTENSION_FILES = [
20
+ "manifest.json",
21
+ "background.js",
22
+ "sidepanel.html",
23
+ "sidepanel.js",
24
+ "sidepanel.css",
25
+ "bind.js",
26
+ "client.js",
27
+ ]
28
+ EXTENSION_ASSETS = [
29
+ "assets/icon-16.png",
30
+ "assets/icon-32.png",
31
+ "assets/icon-48.png",
32
+ "assets/icon-128.png",
33
+ ]
34
+
35
+
36
+ class ExtensionSetupService:
37
+ """Chrome extension installation service."""
38
+
39
+ def __init__(self):
40
+ self.info = get_platform_info()
41
+ self.paths = self.info["paths"]
42
+
43
+ # Extension goes in data directory (persistent app data)
44
+ self.extension_dir = self.paths["data_dir"] / "extension"
45
+
46
+ def install_extension(self, force: bool = False) -> Dict[str, Any]:
47
+ """Install Chrome extension to platform-appropriate location.
48
+
49
+ Args:
50
+ force: Overwrite existing files
51
+
52
+ Returns:
53
+ Installation result
54
+ """
55
+ # Check if exists (manifest.json is required file)
56
+ if (self.extension_dir / "manifest.json").exists() and not force:
57
+ return {
58
+ "success": False,
59
+ "message": f"Extension already exists at {self.extension_dir}",
60
+ "path": str(self.extension_dir),
61
+ "details": "Use --force to overwrite",
62
+ }
63
+
64
+ # Ensure base directories exist
65
+ ensure_directories()
66
+
67
+ # If force, clean out old extension files first
68
+ if force and self.extension_dir.exists():
69
+ import shutil
70
+
71
+ shutil.rmtree(self.extension_dir)
72
+ logger.info(f"Cleaned old extension directory: {self.extension_dir}")
73
+
74
+ # Create extension directory
75
+ self.extension_dir.mkdir(parents=True, exist_ok=True)
76
+
77
+ # Download text files
78
+ downloaded = []
79
+ failed = []
80
+
81
+ for filename in EXTENSION_FILES:
82
+ url = f"{EXTENSION_BASE_URL}/{filename}"
83
+ target_file = self.extension_dir / filename
84
+
85
+ try:
86
+ logger.info(f"Downloading {filename}")
87
+ response = requests.get(url, timeout=10)
88
+ response.raise_for_status()
89
+
90
+ # For manifest.json, validate it's proper JSON
91
+ if filename == "manifest.json":
92
+ json.loads(response.text)
93
+
94
+ target_file.write_text(response.text)
95
+ downloaded.append(filename)
96
+
97
+ except Exception as e:
98
+ logger.error(f"Failed to download {filename}: {e}")
99
+ failed.append(filename)
100
+
101
+ # Download binary assets (icons)
102
+ assets_dir = self.extension_dir / "assets"
103
+ assets_dir.mkdir(exist_ok=True)
104
+
105
+ for asset_path in EXTENSION_ASSETS:
106
+ url = f"{EXTENSION_BASE_URL}/{asset_path}"
107
+ target_file = self.extension_dir / asset_path
108
+
109
+ try:
110
+ logger.info(f"Downloading {asset_path}")
111
+ response = requests.get(url, timeout=10)
112
+ response.raise_for_status()
113
+
114
+ target_file.write_bytes(response.content)
115
+ downloaded.append(asset_path)
116
+
117
+ except Exception as e:
118
+ logger.error(f"Failed to download {asset_path}: {e}")
119
+ failed.append(asset_path)
120
+
121
+ # Determine success level
122
+ if not downloaded:
123
+ return {
124
+ "success": False,
125
+ "message": "Failed to download any extension files",
126
+ "path": None,
127
+ "details": "Check network connection and try again",
128
+ }
129
+
130
+ total_files = len(EXTENSION_FILES) + len(EXTENSION_ASSETS)
131
+ if failed:
132
+ # Partial success - some files downloaded
133
+ return {
134
+ "success": True, # Partial is still success
135
+ "message": f"Downloaded {len(downloaded)}/{total_files} files",
136
+ "path": str(self.extension_dir),
137
+ "details": f"Failed: {', '.join(failed)}",
138
+ }
139
+
140
+ logger.info(f"Extension installed to {self.extension_dir}")
141
+
142
+ return {
143
+ "success": True,
144
+ "message": "Downloaded Chrome extension",
145
+ "path": str(self.extension_dir),
146
+ "details": f"Files: {', '.join(downloaded)}",
147
+ }