webtap-tool 0.1.4__tar.gz → 0.1.5__tar.gz

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.

Files changed (55) hide show
  1. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/CHANGELOG.md +17 -0
  2. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/PKG-INFO +1 -1
  3. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/pyproject.toml +1 -1
  4. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/setup.py +34 -0
  5. webtap_tool-0.1.5/src/webtap/services/setup/__init__.py +66 -0
  6. webtap_tool-0.1.5/src/webtap/services/setup/chrome.py +77 -0
  7. webtap_tool-0.1.5/src/webtap/services/setup/desktop.py +151 -0
  8. webtap_tool-0.1.5/src/webtap/services/setup/extension.py +88 -0
  9. webtap_tool-0.1.5/src/webtap/services/setup/filters.py +79 -0
  10. webtap_tool-0.1.4/src/webtap/services/setup.py +0 -229
  11. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/.gitignore +0 -0
  12. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/ARCHITECTURE.md +0 -0
  13. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/README.md +0 -0
  14. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/data/filters.json +0 -0
  15. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/extension/manifest.json +0 -0
  16. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/extension/popup.html +0 -0
  17. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/extension/popup.js +0 -0
  18. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/llms.txt +0 -0
  19. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/VISION.md +0 -0
  20. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/__init__.py +0 -0
  21. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/api.py +0 -0
  22. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/app.py +0 -0
  23. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/cdp/README.md +0 -0
  24. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/cdp/__init__.py +0 -0
  25. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/cdp/query.py +0 -0
  26. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/cdp/schema/README.md +0 -0
  27. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/cdp/schema/cdp_protocol.json +0 -0
  28. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/cdp/schema/cdp_version.json +0 -0
  29. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/cdp/session.py +0 -0
  30. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/DEVELOPER_GUIDE.md +0 -0
  31. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/TIPS.md +0 -0
  32. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/__init__.py +0 -0
  33. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/_builders.py +0 -0
  34. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/_errors.py +0 -0
  35. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/_tips.py +0 -0
  36. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/_utils.py +0 -0
  37. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/body.py +0 -0
  38. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/connection.py +0 -0
  39. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/console.py +0 -0
  40. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/events.py +0 -0
  41. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/fetch.py +0 -0
  42. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/filters.py +0 -0
  43. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/inspect.py +0 -0
  44. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/javascript.py +0 -0
  45. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/launch.py +0 -0
  46. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/navigation.py +0 -0
  47. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/commands/network.py +0 -0
  48. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/filters.py +0 -0
  49. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/services/README.md +0 -0
  50. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/services/__init__.py +0 -0
  51. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/services/body.py +0 -0
  52. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/services/console.py +0 -0
  53. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/services/fetch.py +0 -0
  54. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/services/main.py +0 -0
  55. {webtap_tool-0.1.4 → webtap_tool-0.1.5}/src/webtap/services/network.py +0 -0
@@ -15,6 +15,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
15
 
16
16
  ### Removed
17
17
 
18
+ ## [0.1.5] - 2025-09-10
19
+
20
+ ### Added
21
+ - New `setup-desktop` command to install desktop entry for GUI Chrome integration
22
+ - Desktop entry installation that overrides system Chrome launcher to use debug wrapper
23
+ - Automatic detection and modification of system Chrome desktop files
24
+
25
+ ### Changed
26
+ - Refactored setup service from monolithic file into modular components
27
+ - Split `services/setup.py` into separate modules: `filters.py`, `extension.py`, `chrome.py`, `desktop.py`
28
+ - Updated type hints to use modern Python 3.9+ style (`dict` instead of `Dict`)
29
+
30
+ ### Fixed
31
+ - Code style issues across setup modules (imports, formatting, type hints)
32
+
33
+ ### Removed
34
+
18
35
  ## [0.1.4] - 2025-09-08
19
36
 
20
37
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webtap-tool
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Terminal-based web page inspector for AI debugging sessions
5
5
  Author-email: Fredrik Angelsen <fredrikangelsen@gmail.com>
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "webtap-tool"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "Terminal-based web page inspector for AI debugging sessions"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -61,6 +61,28 @@ def setup_chrome(state, force: bool = False) -> dict:
61
61
  return _format_setup_result(result, "chrome")
62
62
 
63
63
 
64
+ @app.command(
65
+ display="markdown",
66
+ typer={"name": "setup-desktop", "help": "Install desktop entry to use Chrome wrapper from GUI"},
67
+ fastmcp={"enabled": False},
68
+ )
69
+ def setup_desktop(state, force: bool = False) -> dict:
70
+ """Install desktop entry to ~/.local/share/applications/google-chrome.desktop.
71
+
72
+ This makes the Chrome wrapper work when launching from GUI (app launcher, dock, etc).
73
+ The desktop entry overrides the system Chrome launcher to use our debug-enabled wrapper.
74
+
75
+ Args:
76
+ force: Overwrite existing desktop entry (default: False)
77
+
78
+ Returns:
79
+ Markdown-formatted result with success/error messages
80
+ """
81
+ service = SetupService()
82
+ result = service.install_desktop_entry(force=force)
83
+ return _format_setup_result(result, "desktop")
84
+
85
+
64
86
  def _format_setup_result(result: dict, component: str) -> dict:
65
87
  """Format setup result as markdown."""
66
88
  elements = []
@@ -123,5 +145,17 @@ def _format_setup_result(result: dict, component: str) -> dict:
123
145
  ],
124
146
  }
125
147
  )
148
+ elif component == "desktop":
149
+ elements.append({"type": "text", "content": "\n**Next steps:**"})
150
+ elements.append(
151
+ {
152
+ "type": "list",
153
+ "items": [
154
+ "Log out and log back in (or run `update-desktop-database ~/.local/share/applications`)",
155
+ "Chrome will now launch with debugging enabled from GUI",
156
+ "The wrapper at ~/.local/bin/wrappers/google-chrome-stable will be used",
157
+ ],
158
+ }
159
+ )
126
160
 
127
161
  return {"elements": elements}
@@ -0,0 +1,66 @@
1
+ """Setup service for installing WebTap components.
2
+
3
+ PUBLIC API:
4
+ - SetupService: Main service class for all setup operations
5
+ """
6
+
7
+ from typing import Dict, Any
8
+
9
+ from .filters import install_filters
10
+ from .extension import install_extension
11
+ from .chrome import install_chrome_wrapper
12
+ from .desktop import install_desktop_entry
13
+
14
+
15
+ class SetupService:
16
+ """Service for installing WebTap components.
17
+
18
+ Delegates to specialized modules for each component type.
19
+ """
20
+
21
+ def install_filters(self, force: bool = False) -> Dict[str, Any]:
22
+ """Install filter configuration.
23
+
24
+ Args:
25
+ force: Overwrite existing file
26
+
27
+ Returns:
28
+ Dict with success, message, path, details
29
+ """
30
+ return install_filters(force=force)
31
+
32
+ def install_extension(self, force: bool = False) -> Dict[str, Any]:
33
+ """Install Chrome extension files.
34
+
35
+ Args:
36
+ force: Overwrite existing files
37
+
38
+ Returns:
39
+ Dict with success, message, path, details
40
+ """
41
+ return install_extension(force=force)
42
+
43
+ def install_chrome_wrapper(self, force: bool = False) -> Dict[str, Any]:
44
+ """Install Chrome wrapper script.
45
+
46
+ Args:
47
+ force: Overwrite existing script
48
+
49
+ Returns:
50
+ Dict with success, message, path, details
51
+ """
52
+ return install_chrome_wrapper(force=force)
53
+
54
+ def install_desktop_entry(self, force: bool = False) -> Dict[str, Any]:
55
+ """Install desktop entry for GUI integration.
56
+
57
+ Args:
58
+ force: Overwrite existing entry
59
+
60
+ Returns:
61
+ Dict with success, message, path, details
62
+ """
63
+ return install_desktop_entry(force=force)
64
+
65
+
66
+ __all__ = ["SetupService"]
@@ -0,0 +1,77 @@
1
+ """Chrome wrapper setup functionality for WebTap."""
2
+
3
+ import os
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Dict, Any
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def install_chrome_wrapper(force: bool = False) -> Dict[str, Any]:
12
+ """Install Chrome wrapper script for debugging.
13
+
14
+ Args:
15
+ force: Overwrite existing script
16
+
17
+ Returns:
18
+ Dict with success, message, path, details
19
+ """
20
+ target_path = Path.home() / ".local" / "bin" / "wrappers" / "google-chrome-stable"
21
+
22
+ if target_path.exists() and not force:
23
+ return {
24
+ "success": False,
25
+ "message": "Chrome wrapper already exists",
26
+ "path": str(target_path),
27
+ "details": "Use --force to overwrite",
28
+ }
29
+
30
+ wrapper_script = """#!/bin/bash
31
+ # Chrome wrapper using bindfs for perfect state sync with debug port
32
+
33
+ DEBUG_DIR="$HOME/.config/google-chrome-debug"
34
+ REAL_DIR="$HOME/.config/google-chrome"
35
+
36
+ # Check if bindfs is installed
37
+ if ! command -v bindfs &>/dev/null; then
38
+ echo "Error: bindfs not installed. Install with: yay -S bindfs" >&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 /usr/bin/google-chrome-stable \\
54
+ --remote-debugging-port=9222 \\
55
+ --remote-allow-origins='*' \\
56
+ --user-data-dir="$DEBUG_DIR" \\
57
+ "$@"
58
+ """
59
+
60
+ # Create directory and save
61
+ target_path.parent.mkdir(parents=True, exist_ok=True)
62
+ target_path.write_text(wrapper_script)
63
+ target_path.chmod(0o755) # Make executable
64
+
65
+ # Check PATH
66
+ path_dirs = os.environ.get("PATH", "").split(":")
67
+ wrapper_dir = str(target_path.parent)
68
+ in_path = wrapper_dir in path_dirs
69
+
70
+ logger.info(f"Installed Chrome wrapper to {target_path}")
71
+
72
+ return {
73
+ "success": True,
74
+ "message": "Installed Chrome wrapper script",
75
+ "path": str(target_path),
76
+ "details": "Already in PATH ✓" if in_path else "Add to PATH",
77
+ }
@@ -0,0 +1,151 @@
1
+ """Desktop entry setup functionality for WebTap."""
2
+
3
+ import re
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Dict, Any
7
+
8
+ logger = logging.getLogger(__name__)
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"
35
+
36
+ if desktop_path.exists() and not force:
37
+ return {
38
+ "success": False,
39
+ "message": "Desktop entry already exists",
40
+ "path": str(desktop_path),
41
+ "details": "Use --force to overwrite",
42
+ }
43
+
44
+ # Get system Chrome desktop file for reference
45
+ system_desktop = _find_system_desktop_file()
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]
128
+ Version=1.0
129
+ Type=Application
130
+ Name=Google Chrome
131
+ GenericName=Web Browser
132
+ Comment=Access the Internet
133
+ Icon=google-chrome
134
+ Categories=Network;WebBrowser;
135
+ 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
+ StartupWMClass=Google-chrome
137
+ StartupNotify=true
138
+ Terminal=false
139
+ Exec={wrapper_path} %U
140
+ Actions=new-window;new-private-window;
141
+
142
+ [Desktop Action new-window]
143
+ Name=New Window
144
+ StartupWMClass=Google-chrome
145
+ Exec={wrapper_path}
146
+
147
+ [Desktop Action new-private-window]
148
+ Name=New Incognito Window
149
+ StartupWMClass=Google-chrome
150
+ Exec={wrapper_path} --incognito
151
+ """
@@ -0,0 +1,88 @@
1
+ """Chrome extension setup functionality for WebTap."""
2
+
3
+ import json
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Dict, Any
7
+
8
+ import requests
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # GitHub URLs for extension files
13
+ EXTENSION_BASE_URL = "https://raw.githubusercontent.com/angelsen/tap-tools/main/packages/webtap/extension"
14
+ EXTENSION_FILES = ["manifest.json", "popup.html", "popup.js"]
15
+
16
+
17
+ def install_extension(force: bool = False) -> Dict[str, Any]:
18
+ """Install Chrome extension to ~/.config/webtap/extension/.
19
+
20
+ Args:
21
+ force: Overwrite existing files
22
+
23
+ Returns:
24
+ Dict with success, message, path, details
25
+ """
26
+ # XDG config directory for Linux
27
+ target_dir = Path.home() / ".config" / "webtap" / "extension"
28
+
29
+ # Check if exists (manifest.json is required file)
30
+ if (target_dir / "manifest.json").exists() and not force:
31
+ return {
32
+ "success": False,
33
+ "message": f"Extension already exists at {target_dir}",
34
+ "path": str(target_dir),
35
+ "details": "Use --force to overwrite",
36
+ }
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
+ }
@@ -0,0 +1,79 @@
1
+ """Filter setup functionality for WebTap."""
2
+
3
+ import json
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Dict, Any
7
+
8
+ import requests
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # GitHub URL for filters
13
+ FILTERS_URL = "https://raw.githubusercontent.com/angelsen/tap-tools/main/packages/webtap/data/filters.json"
14
+
15
+
16
+ def install_filters(force: bool = False) -> Dict[str, Any]:
17
+ """Install filters to .webtap/filters.json.
18
+
19
+ Args:
20
+ force: Overwrite existing file
21
+
22
+ Returns:
23
+ Dict with success, message, path, details
24
+ """
25
+ # Same path that FilterManager uses
26
+ target_path = Path.cwd() / ".webtap" / "filters.json"
27
+
28
+ # Check if exists
29
+ if target_path.exists() and not force:
30
+ return {
31
+ "success": False,
32
+ "message": f"Filters already exist at {target_path}",
33
+ "path": str(target_path),
34
+ "details": "Use --force to overwrite",
35
+ }
36
+
37
+ # Download from GitHub
38
+ try:
39
+ logger.info(f"Downloading filters from {FILTERS_URL}")
40
+ response = requests.get(FILTERS_URL, timeout=10)
41
+ response.raise_for_status()
42
+
43
+ # Validate it's proper JSON
44
+ filters_data = json.loads(response.text)
45
+
46
+ # Quick validation - should have dict structure
47
+ if not isinstance(filters_data, dict):
48
+ return {
49
+ "success": False,
50
+ "message": "Invalid filter format - expected JSON object",
51
+ "path": None,
52
+ "details": None,
53
+ }
54
+
55
+ # Count categories for user feedback
56
+ category_count = len(filters_data)
57
+
58
+ # Create directory and save
59
+ target_path.parent.mkdir(parents=True, exist_ok=True)
60
+ target_path.write_text(response.text)
61
+
62
+ logger.info(f"Saved {category_count} filter categories to {target_path}")
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}
@@ -1,229 +0,0 @@
1
- """Setup service for installing WebTap components."""
2
-
3
- import os
4
- import json
5
- import logging
6
- from pathlib import Path
7
- from typing import Dict, Any
8
-
9
- import requests
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
-
14
- class SetupService:
15
- """Service for installing WebTap components."""
16
-
17
- # GitHub URLs
18
- FILTERS_URL = "https://raw.githubusercontent.com/angelsen/tap-tools/main/packages/webtap/data/filters.json"
19
- EXTENSION_BASE_URL = "https://raw.githubusercontent.com/angelsen/tap-tools/main/packages/webtap/extension"
20
- EXTENSION_FILES = ["manifest.json", "popup.html", "popup.js"]
21
-
22
- def install_filters(self, force: bool = False) -> Dict[str, Any]:
23
- """Install filters to .webtap/filters.json.
24
-
25
- Args:
26
- force: Overwrite existing file
27
-
28
- Returns:
29
- Dict with success, message, path, details
30
- """
31
- # Same path that FilterManager uses
32
- target_path = Path.cwd() / ".webtap" / "filters.json"
33
-
34
- # Check if exists
35
- if target_path.exists() and not force:
36
- return {
37
- "success": False,
38
- "message": f"Filters already exist at {target_path}",
39
- "path": str(target_path),
40
- "details": "Use --force to overwrite",
41
- }
42
-
43
- # Download from GitHub
44
- try:
45
- logger.info(f"Downloading filters from {self.FILTERS_URL}")
46
- response = requests.get(self.FILTERS_URL, timeout=10)
47
- response.raise_for_status()
48
-
49
- # Validate it's proper JSON
50
- filters_data = json.loads(response.text)
51
-
52
- # Quick validation - should have dict structure
53
- if not isinstance(filters_data, dict):
54
- return {
55
- "success": False,
56
- "message": "Invalid filter format - expected JSON object",
57
- "path": None,
58
- "details": None,
59
- }
60
-
61
- # Count categories for user feedback
62
- category_count = len(filters_data)
63
-
64
- # Create directory and save
65
- target_path.parent.mkdir(parents=True, exist_ok=True)
66
- target_path.write_text(response.text)
67
-
68
- logger.info(f"Saved {category_count} filter categories to {target_path}")
69
-
70
- return {
71
- "success": True,
72
- "message": f"Downloaded {category_count} filter categories",
73
- "path": str(target_path),
74
- "details": f"Categories: {', '.join(filters_data.keys())}",
75
- }
76
-
77
- except requests.RequestException as e:
78
- logger.error(f"Network error downloading filters: {e}")
79
- return {"success": False, "message": f"Network error: {e}", "path": None, "details": None}
80
- except json.JSONDecodeError as e:
81
- logger.error(f"Invalid JSON in filters: {e}")
82
- return {"success": False, "message": f"Invalid JSON format: {e}", "path": None, "details": None}
83
- except Exception as e:
84
- logger.error(f"Unexpected error: {e}")
85
- return {"success": False, "message": f"Failed to download filters: {e}", "path": None, "details": None}
86
-
87
- def install_extension(self, force: bool = False) -> Dict[str, Any]:
88
- """Install Chrome extension to ~/.config/webtap/extension/.
89
-
90
- Args:
91
- force: Overwrite existing files
92
-
93
- Returns:
94
- Dict with success, message, path, details
95
- """
96
- # XDG config directory for Linux
97
- target_dir = Path.home() / ".config" / "webtap" / "extension"
98
-
99
- # Check if exists (manifest.json is required file)
100
- if (target_dir / "manifest.json").exists() and not force:
101
- return {
102
- "success": False,
103
- "message": f"Extension already exists at {target_dir}",
104
- "path": str(target_dir),
105
- "details": "Use --force to overwrite",
106
- }
107
-
108
- # Create directory
109
- target_dir.mkdir(parents=True, exist_ok=True)
110
-
111
- # Download each file
112
- downloaded = []
113
- failed = []
114
-
115
- for filename in self.EXTENSION_FILES:
116
- url = f"{self.EXTENSION_BASE_URL}/{filename}"
117
- target_file = target_dir / filename
118
-
119
- try:
120
- logger.info(f"Downloading {filename}")
121
- response = requests.get(url, timeout=10)
122
- response.raise_for_status()
123
-
124
- # For manifest.json, validate it's proper JSON
125
- if filename == "manifest.json":
126
- json.loads(response.text)
127
-
128
- target_file.write_text(response.text)
129
- downloaded.append(filename)
130
-
131
- except Exception as e:
132
- logger.error(f"Failed to download {filename}: {e}")
133
- failed.append(filename)
134
-
135
- # Determine success level
136
- if not downloaded:
137
- return {
138
- "success": False,
139
- "message": "Failed to download any extension files",
140
- "path": None,
141
- "details": "Check network connection and try again",
142
- }
143
-
144
- if failed:
145
- # Partial success - some files downloaded
146
- return {
147
- "success": True, # Partial is still success
148
- "message": f"Downloaded {len(downloaded)}/{len(self.EXTENSION_FILES)} files",
149
- "path": str(target_dir),
150
- "details": f"Failed: {', '.join(failed)}",
151
- }
152
-
153
- return {
154
- "success": True,
155
- "message": "Downloaded Chrome extension",
156
- "path": str(target_dir),
157
- "details": f"Files: {', '.join(downloaded)}",
158
- }
159
-
160
- def install_chrome_wrapper(self, force: bool = False) -> Dict[str, Any]:
161
- """Install Chrome wrapper script for debugging.
162
-
163
- Args:
164
- force: Overwrite existing script
165
-
166
- Returns:
167
- Dict with success, message, path, details
168
- """
169
- target_path = Path.home() / ".local" / "bin" / "wrappers" / "google-chrome-stable"
170
-
171
- if target_path.exists() and not force:
172
- return {
173
- "success": False,
174
- "message": "Chrome wrapper already exists",
175
- "path": str(target_path),
176
- "details": "Use --force to overwrite",
177
- }
178
-
179
- wrapper_script = """#!/bin/bash
180
- # Chrome wrapper using bindfs for perfect state sync with debug port
181
-
182
- DEBUG_DIR="$HOME/.config/google-chrome-debug"
183
- REAL_DIR="$HOME/.config/google-chrome"
184
-
185
- # Check if bindfs is installed
186
- if ! command -v bindfs &>/dev/null; then
187
- echo "Error: bindfs not installed. Install with: yay -S bindfs" >&2
188
- exit 1
189
- fi
190
-
191
- # Mount real profile via bindfs if not already mounted
192
- if ! mountpoint -q "$DEBUG_DIR" 2>/dev/null; then
193
- mkdir -p "$DEBUG_DIR"
194
- if ! bindfs --no-allow-other "$REAL_DIR" "$DEBUG_DIR"; then
195
- echo "Error: Failed to mount Chrome profile via bindfs" >&2
196
- exit 1
197
- fi
198
- echo "Chrome debug profile mounted. To unmount: fusermount -u $DEBUG_DIR" >&2
199
- fi
200
-
201
- # Launch Chrome with debugging on bindfs mount
202
- exec /usr/bin/google-chrome-stable \\
203
- --remote-debugging-port=9222 \\
204
- --remote-allow-origins='*' \\
205
- --user-data-dir="$DEBUG_DIR" \\
206
- "$@"
207
- """
208
-
209
- # Create directory and save
210
- target_path.parent.mkdir(parents=True, exist_ok=True)
211
- target_path.write_text(wrapper_script)
212
- target_path.chmod(0o755) # Make executable
213
-
214
- # Check PATH
215
- path_dirs = os.environ.get("PATH", "").split(":")
216
- wrapper_dir = str(target_path.parent)
217
- in_path = wrapper_dir in path_dirs
218
-
219
- logger.info(f"Installed Chrome wrapper to {target_path}")
220
-
221
- return {
222
- "success": True,
223
- "message": "Installed Chrome wrapper script",
224
- "path": str(target_path),
225
- "details": "Already in PATH ✓" if in_path else "Add to PATH",
226
- }
227
-
228
-
229
- __all__ = ["SetupService"]
File without changes
File without changes
File without changes
File without changes