webtap-tool 0.1.3__py3-none-any.whl → 0.1.5__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/__init__.py +5 -1
- webtap/api.py +65 -58
- webtap/app.py +15 -0
- webtap/commands/setup.py +34 -0
- webtap/services/setup/__init__.py +66 -0
- webtap/services/setup/chrome.py +77 -0
- webtap/services/setup/desktop.py +151 -0
- webtap/services/setup/extension.py +88 -0
- webtap/services/setup/filters.py +79 -0
- {webtap_tool-0.1.3.dist-info → webtap_tool-0.1.5.dist-info}/METADATA +1 -1
- {webtap_tool-0.1.3.dist-info → webtap_tool-0.1.5.dist-info}/RECORD +13 -9
- webtap/services/setup.py +0 -229
- {webtap_tool-0.1.3.dist-info → webtap_tool-0.1.5.dist-info}/WHEEL +0 -0
- {webtap_tool-0.1.3.dist-info → webtap_tool-0.1.5.dist-info}/entry_points.txt +0 -0
webtap/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ PUBLIC API:
|
|
|
9
9
|
- main: Entry point function for CLI
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import atexit
|
|
12
13
|
import sys
|
|
13
14
|
import logging
|
|
14
15
|
|
|
@@ -45,12 +46,15 @@ def main():
|
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def _start_api_server_safely():
|
|
48
|
-
"""Start API server with error handling."""
|
|
49
|
+
"""Start API server with error handling and cleanup registration."""
|
|
49
50
|
try:
|
|
50
51
|
thread = start_api_server(app.state)
|
|
51
52
|
if thread and app.state:
|
|
52
53
|
app.state.api_thread = thread
|
|
53
54
|
logger.info("API server started on port 8765")
|
|
55
|
+
|
|
56
|
+
# Register cleanup to shut down API server on exit
|
|
57
|
+
atexit.register(lambda: app.state.cleanup() if app.state else None)
|
|
54
58
|
else:
|
|
55
59
|
logger.info("Port 8765 in use by another instance")
|
|
56
60
|
except Exception as e:
|
webtap/api.py
CHANGED
|
@@ -50,22 +50,54 @@ api.add_middleware(
|
|
|
50
50
|
app_state = None
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
@api.get("/
|
|
54
|
-
async def
|
|
55
|
-
"""
|
|
53
|
+
@api.get("/health")
|
|
54
|
+
async def health_check() -> Dict[str, Any]:
|
|
55
|
+
"""Quick health check endpoint for extension."""
|
|
56
|
+
return {"status": "ok", "pid": os.getpid()}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@api.get("/info")
|
|
60
|
+
async def get_info() -> Dict[str, Any]:
|
|
61
|
+
"""Combined endpoint for pages and instance info - reduces round trips."""
|
|
56
62
|
if not app_state:
|
|
57
|
-
return {"error": "WebTap not initialized", "pages": []}
|
|
63
|
+
return {"error": "WebTap not initialized", "pages": [], "pid": os.getpid()}
|
|
64
|
+
|
|
65
|
+
# Get pages
|
|
66
|
+
pages_data = app_state.service.list_pages()
|
|
67
|
+
|
|
68
|
+
# Get instance info
|
|
69
|
+
connected_to = None
|
|
70
|
+
if app_state.cdp.is_connected and app_state.cdp.page_info:
|
|
71
|
+
connected_to = app_state.cdp.page_info.get("title", "Untitled")
|
|
58
72
|
|
|
59
|
-
return
|
|
73
|
+
return {
|
|
74
|
+
"pid": os.getpid(),
|
|
75
|
+
"connected_to": connected_to,
|
|
76
|
+
"events": app_state.service.event_count,
|
|
77
|
+
"pages": pages_data.get("pages", []),
|
|
78
|
+
"error": pages_data.get("error"),
|
|
79
|
+
}
|
|
60
80
|
|
|
61
81
|
|
|
62
82
|
@api.get("/status")
|
|
63
83
|
async def get_status() -> Dict[str, Any]:
|
|
64
|
-
"""Get
|
|
84
|
+
"""Get comprehensive status including connection, events, and fetch details."""
|
|
65
85
|
if not app_state:
|
|
66
86
|
return {"connected": False, "error": "WebTap not initialized", "events": 0}
|
|
67
87
|
|
|
68
|
-
|
|
88
|
+
status = app_state.service.get_status()
|
|
89
|
+
|
|
90
|
+
# Add fetch details if fetch is enabled
|
|
91
|
+
if status.get("fetch_enabled"):
|
|
92
|
+
fetch_service = app_state.service.fetch
|
|
93
|
+
paused_list = fetch_service.get_paused_list()
|
|
94
|
+
status["fetch_details"] = {
|
|
95
|
+
"paused_requests": paused_list,
|
|
96
|
+
"paused_count": len(paused_list),
|
|
97
|
+
"response_stage": fetch_service.enable_response_stage,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return status
|
|
69
101
|
|
|
70
102
|
|
|
71
103
|
@api.post("/connect")
|
|
@@ -108,25 +140,6 @@ async def set_fetch_interception(request: FetchRequest) -> Dict[str, Any]:
|
|
|
108
140
|
return result
|
|
109
141
|
|
|
110
142
|
|
|
111
|
-
@api.get("/fetch/paused")
|
|
112
|
-
async def get_paused_requests() -> Dict[str, Any]:
|
|
113
|
-
"""Get list of currently paused fetch requests."""
|
|
114
|
-
if not app_state:
|
|
115
|
-
return {"error": "WebTap not initialized", "requests": []}
|
|
116
|
-
|
|
117
|
-
fetch_service = app_state.service.fetch
|
|
118
|
-
if not fetch_service.enabled:
|
|
119
|
-
return {"enabled": False, "requests": []}
|
|
120
|
-
|
|
121
|
-
paused_list = fetch_service.get_paused_list()
|
|
122
|
-
return {
|
|
123
|
-
"enabled": True,
|
|
124
|
-
"requests": paused_list,
|
|
125
|
-
"count": len(paused_list),
|
|
126
|
-
"response_stage": fetch_service.enable_response_stage,
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
143
|
@api.get("/filters/status")
|
|
131
144
|
async def get_filter_status() -> Dict[str, Any]:
|
|
132
145
|
"""Get current filter configuration and enabled categories."""
|
|
@@ -186,19 +199,6 @@ async def disable_all_filters() -> Dict[str, Any]:
|
|
|
186
199
|
return {"enabled": [], "total": 0}
|
|
187
200
|
|
|
188
201
|
|
|
189
|
-
@api.get("/instance")
|
|
190
|
-
async def get_instance_info() -> Dict[str, Any]:
|
|
191
|
-
"""Get info about this WebTap instance."""
|
|
192
|
-
if not app_state:
|
|
193
|
-
return {"error": "WebTap not initialized"}
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
"pid": os.getpid(),
|
|
197
|
-
"connected_to": app_state.cdp.current_page_title if app_state.cdp.is_connected else None,
|
|
198
|
-
"events": app_state.cdp.event_count,
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
202
|
@api.post("/release")
|
|
203
203
|
async def release_port() -> Dict[str, Any]:
|
|
204
204
|
"""Release API port for another WebTap instance."""
|
|
@@ -237,8 +237,9 @@ def start_api_server(state, host: str = "127.0.0.1", port: int = 8765) -> thread
|
|
|
237
237
|
logger.info(f"Port {port} already in use")
|
|
238
238
|
return None
|
|
239
239
|
|
|
240
|
-
global app_state
|
|
240
|
+
global app_state, _shutdown_requested
|
|
241
241
|
app_state = state
|
|
242
|
+
_shutdown_requested = False # Reset flag for new instance
|
|
242
243
|
|
|
243
244
|
thread = threading.Thread(target=run_server, args=(host, port), daemon=True)
|
|
244
245
|
thread.start()
|
|
@@ -249,7 +250,10 @@ def start_api_server(state, host: str = "127.0.0.1", port: int = 8765) -> thread
|
|
|
249
250
|
|
|
250
251
|
def run_server(host: str, port: int):
|
|
251
252
|
"""Run the FastAPI server in a thread."""
|
|
252
|
-
|
|
253
|
+
import asyncio
|
|
254
|
+
|
|
255
|
+
async def run():
|
|
256
|
+
"""Run server with proper shutdown handling."""
|
|
253
257
|
config = uvicorn.Config(
|
|
254
258
|
api,
|
|
255
259
|
host=host,
|
|
@@ -259,30 +263,33 @@ def run_server(host: str, port: int):
|
|
|
259
263
|
)
|
|
260
264
|
server = uvicorn.Server(config)
|
|
261
265
|
|
|
262
|
-
#
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
loop = asyncio.new_event_loop()
|
|
266
|
-
asyncio.set_event_loop(loop)
|
|
266
|
+
# Start server in background task
|
|
267
|
+
serve_task = asyncio.create_task(server.serve())
|
|
267
268
|
|
|
268
|
-
|
|
269
|
-
await server.serve()
|
|
270
|
-
|
|
271
|
-
# Start serving
|
|
272
|
-
task = loop.create_task(serve())
|
|
273
|
-
|
|
274
|
-
# Check for shutdown flag
|
|
269
|
+
# Wait for shutdown signal
|
|
275
270
|
while not _shutdown_requested:
|
|
276
|
-
|
|
277
|
-
if
|
|
271
|
+
await asyncio.sleep(0.1)
|
|
272
|
+
if serve_task.done():
|
|
278
273
|
break
|
|
279
274
|
|
|
280
|
-
#
|
|
281
|
-
if
|
|
275
|
+
# Trigger shutdown
|
|
276
|
+
if not serve_task.done():
|
|
282
277
|
logger.info("API server shutting down")
|
|
283
278
|
server.should_exit = True
|
|
284
|
-
|
|
279
|
+
# Wait a bit for graceful shutdown
|
|
280
|
+
try:
|
|
281
|
+
await asyncio.wait_for(serve_task, timeout=1.0)
|
|
282
|
+
except asyncio.TimeoutError:
|
|
283
|
+
logger.debug("Server task timeout - cancelling")
|
|
284
|
+
serve_task.cancel()
|
|
285
|
+
try:
|
|
286
|
+
await serve_task
|
|
287
|
+
except asyncio.CancelledError:
|
|
288
|
+
pass
|
|
285
289
|
|
|
290
|
+
try:
|
|
291
|
+
# Use asyncio.run() which properly cleans up
|
|
292
|
+
asyncio.run(run())
|
|
286
293
|
except Exception as e:
|
|
287
294
|
logger.error(f"API server failed: {e}")
|
|
288
295
|
|
webtap/app.py
CHANGED
|
@@ -36,6 +36,21 @@ class WebTapState:
|
|
|
36
36
|
"""Initialize service with self reference after dataclass init."""
|
|
37
37
|
self.service = WebTapService(self)
|
|
38
38
|
|
|
39
|
+
def cleanup(self):
|
|
40
|
+
"""Cleanup resources on exit."""
|
|
41
|
+
# Disconnect CDP if connected
|
|
42
|
+
if self.cdp.is_connected:
|
|
43
|
+
self.cdp.disconnect()
|
|
44
|
+
|
|
45
|
+
# Stop API server if we own it
|
|
46
|
+
if self.api_thread and self.api_thread.is_alive():
|
|
47
|
+
# Import here to avoid circular dependency
|
|
48
|
+
import webtap.api
|
|
49
|
+
|
|
50
|
+
webtap.api._shutdown_requested = True
|
|
51
|
+
# Wait up to 1 second for graceful shutdown
|
|
52
|
+
self.api_thread.join(timeout=1.0)
|
|
53
|
+
|
|
39
54
|
|
|
40
55
|
# Must be created before command imports for decorator registration
|
|
41
56
|
app = App(
|
webtap/commands/setup.py
CHANGED
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
webtap/VISION.md,sha256=kfoJfEPVc4chOrD9tNMDmYBY9rX9KB-286oZj70ALCE,7681
|
|
2
|
-
webtap/__init__.py,sha256=
|
|
3
|
-
webtap/api.py,sha256=
|
|
4
|
-
webtap/app.py,sha256=
|
|
2
|
+
webtap/__init__.py,sha256=6sTctXhLKByd8TnKJ99Y4o6xv7AbbPJ37S6YMNsQGBM,1970
|
|
3
|
+
webtap/api.py,sha256=mfAqxQfu2lF91jiI6kHQnpmwtwq0Hh9UtuV8ApENXe0,8504
|
|
4
|
+
webtap/app.py,sha256=M6-tCT6uvR5aMMYYUymYrXhq4PaUlHVYTv7MDN_svdQ,3246
|
|
5
5
|
webtap/filters.py,sha256=nphF2bFRbFtS2ue-CbV1uzKpuK3IYBbwjLeLhMDdLEk,11034
|
|
6
6
|
webtap/cdp/README.md,sha256=0TS0V_dRgRAzBqhddpXWD4S0YVi5wI4JgFJSll_KUBE,5660
|
|
7
7
|
webtap/cdp/__init__.py,sha256=c6NFG0XJnAa5GTe9MLr9mDZcLZqoTQN7A1cvvOfLcgY,453
|
|
@@ -28,7 +28,7 @@ webtap/commands/javascript.py,sha256=QpQdqqoQwwTyz1lpibZ92XKOL89scu_ndgSjkhaYuDk
|
|
|
28
28
|
webtap/commands/launch.py,sha256=iZDLundKlxKRLKf3Vz5at42-tp2f-Uj5wZf7fbhBfA0,2202
|
|
29
29
|
webtap/commands/navigation.py,sha256=Mapawp2AZTJQaws2uwlTgMUhqz7HlVTLxiZ06n_MQc0,6071
|
|
30
30
|
webtap/commands/network.py,sha256=hwZshGGdVsJ_9MFjOKJXT07I990JjZInw2LLnKXLQ5Y,2910
|
|
31
|
-
webtap/commands/setup.py,sha256=
|
|
31
|
+
webtap/commands/setup.py,sha256=NvCZF9FJuw6vaNQjeSi0UF2QORwIvGiQeMuX9cf44Mo,5830
|
|
32
32
|
webtap/services/README.md,sha256=rala_jtnNgSiQ1lFLM7x_UQ4SJZDceAm7dpkQMRTYaI,2346
|
|
33
33
|
webtap/services/__init__.py,sha256=IjFqu0Ak6D-r18aokcQMtenDV3fbelvfjTCejGv6CZ0,570
|
|
34
34
|
webtap/services/body.py,sha256=XQPa19y5eUc3XJ2TuwVK6kffO1VQoKqNs33MBBz7hzU,3913
|
|
@@ -36,8 +36,12 @@ webtap/services/console.py,sha256=XVfSKTvEHyyOdujsg85S3wtj1CdZhzKtWwlx25MvSv8,37
|
|
|
36
36
|
webtap/services/fetch.py,sha256=nl6bpU2Vnf40kau4-mqAnIkhC-7Lx2vbTJKUglz9KnE,13602
|
|
37
37
|
webtap/services/main.py,sha256=HcXdPuI7hzsxsNvfN0npGhj_M7HObc83Lr3fuy7BMeE,5673
|
|
38
38
|
webtap/services/network.py,sha256=0o_--F6YvmXqqFqrcjL1gc6Vr9V1Ytb_U7r_DSUWupA,3444
|
|
39
|
-
webtap/services/setup.py,sha256=
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
webtap/services/setup/__init__.py,sha256=HZ--2oYU724onxa3h4wHSy_CSanG3EXzpK39Ha4V-zY,1738
|
|
40
|
+
webtap/services/setup/chrome.py,sha256=-xX5YHMkhzSwxcn9wf_oZ9naKOhQdjHnAcRCiA7jn8E,2279
|
|
41
|
+
webtap/services/setup/desktop.py,sha256=MGaylnMCbiqIY5xCPSReTt_WAQOmAkuiKsermRPEJFQ,5392
|
|
42
|
+
webtap/services/setup/extension.py,sha256=sYeZBbNjhG_lkqlx6dTqiGjfVba3kyff4_Q6HvKYdao,2651
|
|
43
|
+
webtap/services/setup/filters.py,sha256=fe85CoH4H-06B1vwjdIIroICz5EfLp0HUYSdLZz8DuQ,2661
|
|
44
|
+
webtap_tool-0.1.5.dist-info/METADATA,sha256=ZGwJqOrlH7PrHyux90aoZWuNURxEgXqtBkqXFN3lSBo,17457
|
|
45
|
+
webtap_tool-0.1.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
46
|
+
webtap_tool-0.1.5.dist-info/entry_points.txt,sha256=iFe575I0CIb1MbfPt0oX2VYyY5gSU_dA551PKVR83TU,39
|
|
47
|
+
webtap_tool-0.1.5.dist-info/RECORD,,
|
webtap/services/setup.py
DELETED
|
@@ -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
|