webtap-tool 0.1.5__tar.gz → 0.2.0__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.
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/CHANGELOG.md +21 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/PKG-INFO +4 -1
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/pyproject.toml +4 -1
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/setup.py +106 -9
- webtap_tool-0.2.0/src/webtap/services/setup/__init__.py +178 -0
- webtap_tool-0.2.0/src/webtap/services/setup/chrome.py +227 -0
- webtap_tool-0.2.0/src/webtap/services/setup/desktop.py +228 -0
- webtap_tool-0.2.0/src/webtap/services/setup/extension.py +101 -0
- webtap_tool-0.2.0/src/webtap/services/setup/filters.py +89 -0
- webtap_tool-0.2.0/src/webtap/services/setup/platform.py +126 -0
- webtap_tool-0.1.5/src/webtap/services/setup/__init__.py +0 -66
- webtap_tool-0.1.5/src/webtap/services/setup/chrome.py +0 -77
- webtap_tool-0.1.5/src/webtap/services/setup/desktop.py +0 -151
- webtap_tool-0.1.5/src/webtap/services/setup/extension.py +0 -88
- webtap_tool-0.1.5/src/webtap/services/setup/filters.py +0 -79
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/.gitignore +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/ARCHITECTURE.md +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/README.md +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/data/filters.json +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/extension/manifest.json +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/extension/popup.html +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/extension/popup.js +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/llms.txt +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/VISION.md +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/__init__.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/api.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/app.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/cdp/README.md +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/cdp/__init__.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/cdp/query.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/cdp/schema/README.md +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/cdp/schema/cdp_protocol.json +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/cdp/schema/cdp_version.json +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/cdp/session.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/DEVELOPER_GUIDE.md +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/TIPS.md +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/__init__.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/_builders.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/_errors.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/_tips.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/_utils.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/body.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/connection.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/console.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/events.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/fetch.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/filters.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/inspect.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/javascript.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/launch.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/navigation.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/commands/network.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/filters.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/services/README.md +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/services/__init__.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/services/body.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/services/console.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/services/fetch.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/services/main.py +0 -0
- {webtap_tool-0.1.5 → webtap_tool-0.2.0}/src/webtap/services/network.py +0 -0
|
@@ -15,6 +15,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
15
|
|
|
16
16
|
### Removed
|
|
17
17
|
|
|
18
|
+
## [0.2.0] - 2025-09-12
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Cross-platform support (Linux and macOS)
|
|
22
|
+
- `setup-cleanup` command to identify and remove old WebTap installations
|
|
23
|
+
- `--bindfs` flag for `setup-chrome` to mount real Chrome profile for debugging (Linux only)
|
|
24
|
+
- Unified `chrome-debug` wrapper at `~/.local/bin/chrome-debug`
|
|
25
|
+
- Separate Chrome Debug launcher (doesn't override system Chrome)
|
|
26
|
+
- Platform detection with XDG-compliant paths via platformdirs
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- Chrome wrapper location from `~/.local/bin/wrappers/google-chrome-stable` to `~/.local/bin/chrome-debug`
|
|
30
|
+
- Desktop entry from `google-chrome.desktop` override to separate `chrome-debug.desktop`
|
|
31
|
+
- Extension location to platform-appropriate data directories
|
|
32
|
+
- Setup service refactored into platform-aware modules
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- Cleanup command now detects all old installation locations
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
|
|
18
39
|
## [0.1.5] - 2025-09-10
|
|
19
40
|
|
|
20
41
|
### Added
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: webtap-tool
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
7
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
8
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
7
9
|
Classifier: Programming Language :: Python :: 3.12
|
|
8
10
|
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
9
11
|
Classifier: Topic :: Software Development :: Debuggers
|
|
@@ -15,6 +17,7 @@ Requires-Dist: fastapi>=0.116.1
|
|
|
15
17
|
Requires-Dist: httpx>=0.28.1
|
|
16
18
|
Requires-Dist: lxml>=6.0.1
|
|
17
19
|
Requires-Dist: msgpack-python>=0.5.6
|
|
20
|
+
Requires-Dist: platformdirs>=4.4.0
|
|
18
21
|
Requires-Dist: protobuf>=6.32.0
|
|
19
22
|
Requires-Dist: pyjwt>=2.10.1
|
|
20
23
|
Requires-Dist: pyyaml>=6.0.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "webtap-tool"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Terminal-based web page inspector for AI debugging sessions"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -10,6 +10,8 @@ requires-python = ">=3.12"
|
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Development Status :: 3 - Alpha",
|
|
12
12
|
"Programming Language :: Python :: 3.12",
|
|
13
|
+
"Operating System :: POSIX :: Linux",
|
|
14
|
+
"Operating System :: MacOS :: MacOS X",
|
|
13
15
|
"Topic :: Software Development :: Debuggers",
|
|
14
16
|
"Topic :: Internet :: WWW/HTTP :: Browsers",
|
|
15
17
|
]
|
|
@@ -21,6 +23,7 @@ dependencies = [
|
|
|
21
23
|
"httpx>=0.28.1",
|
|
22
24
|
"lxml>=6.0.1",
|
|
23
25
|
"msgpack-python>=0.5.6",
|
|
26
|
+
"platformdirs>=4.4.0",
|
|
24
27
|
"protobuf>=6.32.0",
|
|
25
28
|
"pyjwt>=2.10.1",
|
|
26
29
|
"pyyaml>=6.0.2",
|
|
@@ -29,7 +29,10 @@ def setup_filters(state, force: bool = False) -> dict:
|
|
|
29
29
|
fastmcp={"enabled": False},
|
|
30
30
|
)
|
|
31
31
|
def setup_extension(state, force: bool = False) -> dict:
|
|
32
|
-
"""Download Chrome extension to
|
|
32
|
+
"""Download Chrome extension to platform-appropriate location.
|
|
33
|
+
|
|
34
|
+
Linux: ~/.local/share/webtap/extension/
|
|
35
|
+
macOS: ~/Library/Application Support/webtap/extension/
|
|
33
36
|
|
|
34
37
|
Args:
|
|
35
38
|
force: Overwrite existing files (default: False)
|
|
@@ -47,33 +50,40 @@ def setup_extension(state, force: bool = False) -> dict:
|
|
|
47
50
|
typer={"name": "setup-chrome", "help": "Install Chrome wrapper script for debugging"},
|
|
48
51
|
fastmcp={"enabled": False},
|
|
49
52
|
)
|
|
50
|
-
def setup_chrome(state, force: bool = False) -> dict:
|
|
51
|
-
"""Install Chrome wrapper to ~/.local/bin
|
|
53
|
+
def setup_chrome(state, force: bool = False, bindfs: bool = False) -> dict:
|
|
54
|
+
"""Install Chrome wrapper script 'chrome-debug' to ~/.local/bin/.
|
|
55
|
+
|
|
56
|
+
The wrapper enables remote debugging on port 9222.
|
|
57
|
+
Same location on both Linux and macOS: ~/.local/bin/chrome-debug
|
|
52
58
|
|
|
53
59
|
Args:
|
|
54
60
|
force: Overwrite existing script (default: False)
|
|
61
|
+
bindfs: Use bindfs to mount real Chrome profile for debugging (Linux only, default: False)
|
|
55
62
|
|
|
56
63
|
Returns:
|
|
57
64
|
Markdown-formatted result with success/error messages
|
|
58
65
|
"""
|
|
59
66
|
service = SetupService()
|
|
60
|
-
result = service.install_chrome_wrapper(force=force)
|
|
67
|
+
result = service.install_chrome_wrapper(force=force, bindfs=bindfs)
|
|
61
68
|
return _format_setup_result(result, "chrome")
|
|
62
69
|
|
|
63
70
|
|
|
64
71
|
@app.command(
|
|
65
72
|
display="markdown",
|
|
66
|
-
typer={"name": "setup-desktop", "help": "Install
|
|
73
|
+
typer={"name": "setup-desktop", "help": "Install Chrome Debug GUI launcher"},
|
|
67
74
|
fastmcp={"enabled": False},
|
|
68
75
|
)
|
|
69
76
|
def setup_desktop(state, force: bool = False) -> dict:
|
|
70
|
-
"""Install
|
|
77
|
+
"""Install Chrome Debug GUI launcher (separate from system Chrome).
|
|
71
78
|
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
Linux: Creates desktop entry at ~/.local/share/applications/chrome-debug.desktop
|
|
80
|
+
Shows as "Chrome Debug" in application menu.
|
|
81
|
+
|
|
82
|
+
macOS: Creates app bundle at ~/Applications/Chrome Debug.app
|
|
83
|
+
Shows as "Chrome Debug" in Launchpad and Spotlight.
|
|
74
84
|
|
|
75
85
|
Args:
|
|
76
|
-
force: Overwrite existing
|
|
86
|
+
force: Overwrite existing launcher (default: False)
|
|
77
87
|
|
|
78
88
|
Returns:
|
|
79
89
|
Markdown-formatted result with success/error messages
|
|
@@ -159,3 +169,90 @@ def _format_setup_result(result: dict, component: str) -> dict:
|
|
|
159
169
|
)
|
|
160
170
|
|
|
161
171
|
return {"elements": elements}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@app.command(
|
|
175
|
+
display="markdown",
|
|
176
|
+
typer={"name": "setup-cleanup", "help": "Clean up old WebTap installations"},
|
|
177
|
+
fastmcp={"enabled": False},
|
|
178
|
+
)
|
|
179
|
+
def setup_cleanup(state, dry_run: bool = True) -> dict:
|
|
180
|
+
"""Clean up old WebTap installations from previous versions.
|
|
181
|
+
|
|
182
|
+
Checks for and removes:
|
|
183
|
+
- Old extension location (~/.config/webtap/extension/)
|
|
184
|
+
- Old desktop entries created by webtap
|
|
185
|
+
- Unmounted bindfs directories
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
dry_run: Only show what would be cleaned (default: True)
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Markdown report of cleanup actions
|
|
192
|
+
"""
|
|
193
|
+
service = SetupService()
|
|
194
|
+
result = service.cleanup_old_installations(dry_run=dry_run)
|
|
195
|
+
|
|
196
|
+
elements = []
|
|
197
|
+
|
|
198
|
+
# Header
|
|
199
|
+
elements.append({"type": "heading", "level": 2, "content": "WebTap Cleanup Report"})
|
|
200
|
+
|
|
201
|
+
# Old installations found
|
|
202
|
+
if result.get("old_extension"):
|
|
203
|
+
elements.append({"type": "heading", "level": 3, "content": "Old Extension Location"})
|
|
204
|
+
elements.append({"type": "text", "content": f"Found: `{result['old_extension']['path']}`"})
|
|
205
|
+
elements.append({"type": "text", "content": f"Size: {result['old_extension']['size']}"})
|
|
206
|
+
if not dry_run and result["old_extension"].get("removed"):
|
|
207
|
+
elements.append({"type": "alert", "message": "✓ Removed old extension", "level": "success"})
|
|
208
|
+
elif dry_run:
|
|
209
|
+
elements.append({"type": "alert", "message": "Would remove (dry-run mode)", "level": "info"})
|
|
210
|
+
|
|
211
|
+
# Old Chrome wrapper
|
|
212
|
+
if result.get("old_wrapper"):
|
|
213
|
+
elements.append({"type": "heading", "level": 3, "content": "Old Chrome Wrapper"})
|
|
214
|
+
elements.append({"type": "text", "content": f"Found: `{result['old_wrapper']['path']}`"})
|
|
215
|
+
if not dry_run and result["old_wrapper"].get("removed"):
|
|
216
|
+
elements.append({"type": "alert", "message": "✓ Removed old wrapper", "level": "success"})
|
|
217
|
+
elif dry_run:
|
|
218
|
+
elements.append({"type": "alert", "message": "Would remove (dry-run mode)", "level": "info"})
|
|
219
|
+
|
|
220
|
+
# Old desktop entry
|
|
221
|
+
if result.get("old_desktop"):
|
|
222
|
+
elements.append({"type": "heading", "level": 3, "content": "Old Desktop Entry"})
|
|
223
|
+
elements.append({"type": "text", "content": f"Found: `{result['old_desktop']['path']}`"})
|
|
224
|
+
if not dry_run and result["old_desktop"].get("removed"):
|
|
225
|
+
elements.append({"type": "alert", "message": "✓ Removed old desktop entry", "level": "success"})
|
|
226
|
+
elif dry_run:
|
|
227
|
+
elements.append({"type": "alert", "message": "Would remove (dry-run mode)", "level": "info"})
|
|
228
|
+
|
|
229
|
+
# Check for bindfs mounts
|
|
230
|
+
if result.get("bindfs_mount"):
|
|
231
|
+
elements.append({"type": "heading", "level": 3, "content": "Bindfs Mount Detected"})
|
|
232
|
+
elements.append({"type": "text", "content": f"Mount: `{result['bindfs_mount']}`"})
|
|
233
|
+
elements.append(
|
|
234
|
+
{"type": "alert", "message": "To unmount: fusermount -u " + result["bindfs_mount"], "level": "warning"}
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Summary
|
|
238
|
+
elements.append({"type": "heading", "level": 3, "content": "Summary"})
|
|
239
|
+
if dry_run:
|
|
240
|
+
elements.append({"type": "text", "content": "**Dry-run mode** - no changes made"})
|
|
241
|
+
elements.append({"type": "text", "content": "To perform cleanup: `setup-cleanup --no-dry-run`"})
|
|
242
|
+
else:
|
|
243
|
+
elements.append({"type": "alert", "message": "Cleanup completed", "level": "success"})
|
|
244
|
+
|
|
245
|
+
# Next steps
|
|
246
|
+
elements.append({"type": "heading", "level": 3, "content": "Next Steps"})
|
|
247
|
+
elements.append(
|
|
248
|
+
{
|
|
249
|
+
"type": "list",
|
|
250
|
+
"items": [
|
|
251
|
+
"Run `setup-extension` to install extension in new location",
|
|
252
|
+
"Run `setup-chrome --bindfs` for bindfs mode or `setup-chrome` for standard mode",
|
|
253
|
+
"Run `setup-desktop` to create Chrome Debug launcher",
|
|
254
|
+
],
|
|
255
|
+
}
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return {"elements": elements}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Setup service for installing WebTap components (cross-platform).
|
|
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 FilterSetupService
|
|
10
|
+
from .extension import ExtensionSetupService
|
|
11
|
+
from .chrome import ChromeSetupService
|
|
12
|
+
from .desktop import DesktopSetupService
|
|
13
|
+
from .platform import get_platform_info, ensure_directories
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SetupService:
|
|
17
|
+
"""Orchestrator service for installing WebTap components.
|
|
18
|
+
|
|
19
|
+
Delegates to specialized service classes for each component type.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
"""Initialize setup service with platform information."""
|
|
24
|
+
self.info = get_platform_info()
|
|
25
|
+
ensure_directories()
|
|
26
|
+
|
|
27
|
+
# Initialize component services
|
|
28
|
+
self.filters_service = FilterSetupService()
|
|
29
|
+
self.extension_service = ExtensionSetupService()
|
|
30
|
+
self.chrome_service = ChromeSetupService()
|
|
31
|
+
self.desktop_service = DesktopSetupService()
|
|
32
|
+
|
|
33
|
+
def install_filters(self, force: bool = False) -> Dict[str, Any]:
|
|
34
|
+
"""Install filter configuration.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
force: Overwrite existing file
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Dict with success, message, path, details
|
|
41
|
+
"""
|
|
42
|
+
return self.filters_service.install_filters(force=force)
|
|
43
|
+
|
|
44
|
+
def install_extension(self, force: bool = False) -> Dict[str, Any]:
|
|
45
|
+
"""Install Chrome extension files.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
force: Overwrite existing files
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dict with success, message, path, details
|
|
52
|
+
"""
|
|
53
|
+
return self.extension_service.install_extension(force=force)
|
|
54
|
+
|
|
55
|
+
def install_chrome_wrapper(self, force: bool = False, bindfs: bool = False) -> Dict[str, Any]:
|
|
56
|
+
"""Install Chrome wrapper script.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
force: Overwrite existing script
|
|
60
|
+
bindfs: Use bindfs to mount real Chrome profile (Linux only)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Dict with success, message, path, details
|
|
64
|
+
"""
|
|
65
|
+
return self.chrome_service.install_wrapper(force=force, bindfs=bindfs)
|
|
66
|
+
|
|
67
|
+
def install_desktop_entry(self, force: bool = False) -> Dict[str, Any]:
|
|
68
|
+
"""Install desktop entry or app bundle for GUI integration.
|
|
69
|
+
|
|
70
|
+
On Linux: Creates .desktop file
|
|
71
|
+
On macOS: Creates .app bundle
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
force: Overwrite existing entry
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Dict with success, message, path, details
|
|
78
|
+
"""
|
|
79
|
+
return self.desktop_service.install_launcher(force=force)
|
|
80
|
+
|
|
81
|
+
def get_platform_info(self) -> Dict[str, Any]:
|
|
82
|
+
"""Get platform information for debugging.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Platform information including paths and capabilities
|
|
86
|
+
"""
|
|
87
|
+
return self.info
|
|
88
|
+
|
|
89
|
+
def cleanup_old_installations(self, dry_run: bool = True) -> Dict[str, Any]:
|
|
90
|
+
"""Clean up old WebTap installations.
|
|
91
|
+
|
|
92
|
+
Checks locations that webtap previously wrote to:
|
|
93
|
+
- ~/.config/webtap/extension/ (old extension location)
|
|
94
|
+
- ~/.local/bin/wrappers/google-chrome-stable (old wrapper location)
|
|
95
|
+
- ~/.local/share/applications/google-chrome.desktop (old desktop entry)
|
|
96
|
+
- ~/.config/google-chrome-debug (bindfs mount)
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
dry_run: If True, only report what would be done
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dict with cleanup results
|
|
103
|
+
"""
|
|
104
|
+
import shutil
|
|
105
|
+
import subprocess
|
|
106
|
+
from pathlib import Path
|
|
107
|
+
|
|
108
|
+
result = {}
|
|
109
|
+
|
|
110
|
+
# Check old extension location
|
|
111
|
+
old_extension_path = Path.home() / ".config" / "webtap" / "extension"
|
|
112
|
+
if old_extension_path.exists():
|
|
113
|
+
# Calculate size
|
|
114
|
+
size = sum(f.stat().st_size for f in old_extension_path.rglob("*") if f.is_file())
|
|
115
|
+
size_str = f"{size / 1024:.1f} KB" if size > 0 else "empty"
|
|
116
|
+
|
|
117
|
+
result["old_extension"] = {"path": str(old_extension_path), "size": size_str, "removed": False}
|
|
118
|
+
|
|
119
|
+
if not dry_run:
|
|
120
|
+
try:
|
|
121
|
+
shutil.rmtree(old_extension_path)
|
|
122
|
+
result["old_extension"]["removed"] = True
|
|
123
|
+
# Also try to remove parent if empty
|
|
124
|
+
parent = old_extension_path.parent
|
|
125
|
+
if parent.exists() and not any(parent.iterdir()):
|
|
126
|
+
parent.rmdir()
|
|
127
|
+
except Exception as e:
|
|
128
|
+
result["old_extension"]["error"] = str(e)
|
|
129
|
+
|
|
130
|
+
# Check old Chrome wrapper location
|
|
131
|
+
old_wrapper_path = Path.home() / ".local" / "bin" / "wrappers" / "google-chrome-stable"
|
|
132
|
+
if old_wrapper_path.exists():
|
|
133
|
+
result["old_wrapper"] = {"path": str(old_wrapper_path), "removed": False}
|
|
134
|
+
|
|
135
|
+
if not dry_run:
|
|
136
|
+
try:
|
|
137
|
+
old_wrapper_path.unlink()
|
|
138
|
+
result["old_wrapper"]["removed"] = True
|
|
139
|
+
# Try to remove wrappers dir if empty (but keep it if other wrappers exist)
|
|
140
|
+
wrappers_dir = old_wrapper_path.parent
|
|
141
|
+
if wrappers_dir.exists() and not any(wrappers_dir.iterdir()):
|
|
142
|
+
wrappers_dir.rmdir()
|
|
143
|
+
except Exception as e:
|
|
144
|
+
result["old_wrapper"]["error"] = str(e)
|
|
145
|
+
|
|
146
|
+
# Check old desktop entry
|
|
147
|
+
old_desktop_path = Path.home() / ".local" / "share" / "applications" / "google-chrome.desktop"
|
|
148
|
+
if old_desktop_path.exists():
|
|
149
|
+
# Check if it's our override (contains reference to wrapper)
|
|
150
|
+
try:
|
|
151
|
+
content = old_desktop_path.read_text()
|
|
152
|
+
if "wrappers/google-chrome-stable" in content or "webtap" in content.lower():
|
|
153
|
+
result["old_desktop"] = {"path": str(old_desktop_path), "removed": False}
|
|
154
|
+
|
|
155
|
+
if not dry_run:
|
|
156
|
+
try:
|
|
157
|
+
old_desktop_path.unlink()
|
|
158
|
+
result["old_desktop"]["removed"] = True
|
|
159
|
+
except Exception as e:
|
|
160
|
+
result["old_desktop"]["error"] = str(e)
|
|
161
|
+
except Exception:
|
|
162
|
+
pass # If we can't read it, skip it
|
|
163
|
+
|
|
164
|
+
# Check for bindfs mount
|
|
165
|
+
debug_dir = Path.home() / ".config" / "google-chrome-debug"
|
|
166
|
+
if debug_dir.exists():
|
|
167
|
+
try:
|
|
168
|
+
# Check if it's a mount point
|
|
169
|
+
output = subprocess.run(["mountpoint", "-q", str(debug_dir)], capture_output=True)
|
|
170
|
+
if output.returncode == 0:
|
|
171
|
+
result["bindfs_mount"] = str(debug_dir)
|
|
172
|
+
except (FileNotFoundError, OSError):
|
|
173
|
+
pass # mountpoint command might not exist
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
__all__ = ["SetupService"]
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Chrome setup service for installing wrapper scripts (cross-platform)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Dict, Any
|
|
6
|
+
|
|
7
|
+
from .platform import get_platform_info, ensure_directories
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Wrapper script templates
|
|
12
|
+
LINUX_BINDFS_WRAPPER = """#!/bin/bash
|
|
13
|
+
# Chrome Debug Launcher for Linux (bindfs mode)
|
|
14
|
+
# Generated by webtap setup-chrome --bindfs
|
|
15
|
+
# Mounts your real Chrome profile via bindfs for debugging with full session state
|
|
16
|
+
|
|
17
|
+
DEBUG_DIR="$HOME/.config/google-chrome-debug"
|
|
18
|
+
REAL_DIR="$HOME/.config/google-chrome"
|
|
19
|
+
PORT=${{WEBTAP_PORT:-9222}}
|
|
20
|
+
CHROME_BIN="{chrome_path}"
|
|
21
|
+
|
|
22
|
+
# Check if bindfs is installed
|
|
23
|
+
if ! command -v bindfs &>/dev/null; then
|
|
24
|
+
echo "Error: bindfs not installed. Install with:" >&2
|
|
25
|
+
echo " Ubuntu/Debian: sudo apt install bindfs" >&2
|
|
26
|
+
echo " Arch: yay -S bindfs" >&2
|
|
27
|
+
echo " Fedora: sudo dnf install bindfs" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Check if real Chrome profile exists
|
|
32
|
+
if [ ! -d "$REAL_DIR" ]; then
|
|
33
|
+
echo "Error: Chrome profile not found at $REAL_DIR" >&2
|
|
34
|
+
echo "Please run Chrome normally first to create a profile" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Mount real profile via bindfs if not already mounted
|
|
39
|
+
if ! mountpoint -q "$DEBUG_DIR" 2>/dev/null; then
|
|
40
|
+
mkdir -p "$DEBUG_DIR"
|
|
41
|
+
if ! bindfs --no-allow-other "$REAL_DIR" "$DEBUG_DIR"; then
|
|
42
|
+
echo "Error: Failed to mount Chrome profile via bindfs" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
echo "Chrome debug profile mounted. To unmount: fusermount -u $DEBUG_DIR" >&2
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Launch Chrome with debugging on bindfs mount
|
|
49
|
+
exec "$CHROME_BIN" \\
|
|
50
|
+
--remote-debugging-port="$PORT" \\
|
|
51
|
+
--remote-allow-origins='*' \\
|
|
52
|
+
--user-data-dir="$DEBUG_DIR" \\
|
|
53
|
+
"$@"
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
LINUX_STANDARD_WRAPPER = """#!/bin/bash
|
|
57
|
+
# Chrome Debug Launcher for Linux
|
|
58
|
+
# Generated by webtap setup-chrome
|
|
59
|
+
|
|
60
|
+
# Configuration
|
|
61
|
+
PORT=${{WEBTAP_PORT:-9222}}
|
|
62
|
+
PROFILE_BASE="{profile_dir}"
|
|
63
|
+
CHROME_BIN="{chrome_path}"
|
|
64
|
+
|
|
65
|
+
# Profile handling
|
|
66
|
+
if [ "$1" = "--temp" ]; then
|
|
67
|
+
PROFILE_DIR="$(mktemp -d /tmp/webtap-chrome-XXXXXX)"
|
|
68
|
+
shift
|
|
69
|
+
echo "Using temporary profile: $PROFILE_DIR" >&2
|
|
70
|
+
else
|
|
71
|
+
PROFILE_DIR="$PROFILE_BASE/default"
|
|
72
|
+
mkdir -p "$PROFILE_DIR"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Launch Chrome with debugging
|
|
76
|
+
exec "$CHROME_BIN" \\
|
|
77
|
+
--remote-debugging-port="$PORT" \\
|
|
78
|
+
--user-data-dir="$PROFILE_DIR" \\
|
|
79
|
+
--no-first-run \\
|
|
80
|
+
--no-default-browser-check \\
|
|
81
|
+
"$@"
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
MACOS_WRAPPER = """#!/bin/bash
|
|
85
|
+
# Chrome Debug Launcher for macOS
|
|
86
|
+
# Generated by webtap setup-chrome
|
|
87
|
+
|
|
88
|
+
# Configuration
|
|
89
|
+
PORT=${{WEBTAP_PORT:-9222}}
|
|
90
|
+
PROFILE_BASE="{profile_dir}"
|
|
91
|
+
CHROME_APP="{chrome_path}"
|
|
92
|
+
|
|
93
|
+
# Profile handling
|
|
94
|
+
if [ "$1" = "--temp" ]; then
|
|
95
|
+
PROFILE_DIR="$(mktemp -d /tmp/webtap-chrome-XXXXXX)"
|
|
96
|
+
shift
|
|
97
|
+
echo "Using temporary profile: $PROFILE_DIR" >&2
|
|
98
|
+
else
|
|
99
|
+
PROFILE_DIR="$PROFILE_BASE/default"
|
|
100
|
+
mkdir -p "$PROFILE_DIR"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Launch Chrome with debugging
|
|
104
|
+
exec "$CHROME_APP" \\
|
|
105
|
+
--remote-debugging-port="$PORT" \\
|
|
106
|
+
--user-data-dir="$PROFILE_DIR" \\
|
|
107
|
+
--no-first-run \\
|
|
108
|
+
--no-default-browser-check \\
|
|
109
|
+
"$@"
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ChromeSetupService:
|
|
114
|
+
"""Chrome wrapper installation service (cross-platform)."""
|
|
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 location for both platforms
|
|
122
|
+
self.wrapper_dir = self.paths["bin_dir"] # ~/.local/bin
|
|
123
|
+
self.wrapper_name = self.chrome["wrapper_name"] # chrome-debug
|
|
124
|
+
self.wrapper_path = self.wrapper_dir / self.wrapper_name
|
|
125
|
+
|
|
126
|
+
# Profile locations
|
|
127
|
+
self.profile_dir = self.paths["data_dir"] / "profiles"
|
|
128
|
+
self.temp_profile_dir = self.paths["runtime_dir"] / "profiles"
|
|
129
|
+
|
|
130
|
+
def install_wrapper(self, force: bool = False, bindfs: bool = False) -> Dict[str, Any]:
|
|
131
|
+
"""Install Chrome wrapper script appropriate for platform.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
force: Overwrite existing wrapper
|
|
135
|
+
bindfs: Use bindfs to mount real Chrome profile (Linux only)
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Installation result
|
|
139
|
+
"""
|
|
140
|
+
if not self.chrome["found"]:
|
|
141
|
+
return {
|
|
142
|
+
"success": False,
|
|
143
|
+
"message": "Chrome not found on system",
|
|
144
|
+
"details": "Please install Google Chrome first",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if self.wrapper_path.exists() and not force:
|
|
148
|
+
return {
|
|
149
|
+
"success": False,
|
|
150
|
+
"message": f"Wrapper already exists at {self.wrapper_path}",
|
|
151
|
+
"details": "Use --force to overwrite",
|
|
152
|
+
"path": str(self.wrapper_path),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Ensure directories exist
|
|
156
|
+
ensure_directories()
|
|
157
|
+
self.wrapper_dir.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
|
|
159
|
+
# Generate platform-specific wrapper
|
|
160
|
+
if self.info["is_macos"]:
|
|
161
|
+
wrapper_content = self._generate_macos_wrapper()
|
|
162
|
+
else:
|
|
163
|
+
wrapper_content = self._generate_linux_wrapper(bindfs=bindfs)
|
|
164
|
+
|
|
165
|
+
self.wrapper_path.write_text(wrapper_content)
|
|
166
|
+
self.wrapper_path.chmod(0o755)
|
|
167
|
+
|
|
168
|
+
# Check if wrapper dir is in PATH
|
|
169
|
+
path_dirs = os.environ.get("PATH", "").split(os.pathsep)
|
|
170
|
+
in_path = str(self.wrapper_dir) in path_dirs
|
|
171
|
+
|
|
172
|
+
logger.info(f"Installed Chrome wrapper to {self.wrapper_path}")
|
|
173
|
+
|
|
174
|
+
# Build detailed message with PATH setup instructions
|
|
175
|
+
if in_path:
|
|
176
|
+
details = "✅ Run 'chrome-debug' to launch Chrome with debugging"
|
|
177
|
+
else:
|
|
178
|
+
shell = os.environ.get("SHELL", "/bin/bash")
|
|
179
|
+
if "zsh" in shell:
|
|
180
|
+
rc_file = "~/.zshrc"
|
|
181
|
+
elif "fish" in shell:
|
|
182
|
+
rc_file = "~/.config/fish/config.fish"
|
|
183
|
+
else:
|
|
184
|
+
rc_file = "~/.bashrc"
|
|
185
|
+
|
|
186
|
+
details = (
|
|
187
|
+
f"⚠️ Add to PATH to use from terminal:\n"
|
|
188
|
+
f"echo 'export PATH=\"$HOME/.local/bin:$PATH\"' >> {rc_file}\n"
|
|
189
|
+
f"source {rc_file}\n\n"
|
|
190
|
+
f"Or run directly: ~/.local/bin/chrome-debug\n"
|
|
191
|
+
f"✅ GUI launcher will work regardless (uses full path)"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
"success": True,
|
|
196
|
+
"message": "Chrome wrapper 'chrome-debug' installed successfully",
|
|
197
|
+
"path": str(self.wrapper_path),
|
|
198
|
+
"details": details,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
def _generate_linux_wrapper(self, bindfs: bool = False) -> str:
|
|
202
|
+
"""Generate Linux wrapper script with optional bindfs support.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
bindfs: If True, use bindfs to mount real Chrome profile
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Wrapper script content
|
|
209
|
+
"""
|
|
210
|
+
chrome_path = self.chrome["path"]
|
|
211
|
+
profile_dir = self.profile_dir
|
|
212
|
+
|
|
213
|
+
if bindfs:
|
|
214
|
+
return LINUX_BINDFS_WRAPPER.format(chrome_path=chrome_path)
|
|
215
|
+
else:
|
|
216
|
+
return LINUX_STANDARD_WRAPPER.format(chrome_path=chrome_path, profile_dir=profile_dir)
|
|
217
|
+
|
|
218
|
+
def _generate_macos_wrapper(self) -> str:
|
|
219
|
+
"""Generate macOS wrapper script.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Wrapper script content
|
|
223
|
+
"""
|
|
224
|
+
chrome_path = self.chrome["path"]
|
|
225
|
+
profile_dir = self.profile_dir
|
|
226
|
+
|
|
227
|
+
return MACOS_WRAPPER.format(chrome_path=chrome_path, profile_dir=profile_dir)
|