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