webtap-tool 0.1.4__py3-none-any.whl → 0.2.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.

Potentially problematic release.


This version of webtap-tool might be problematic. Click here for more details.

webtap/commands/setup.py CHANGED
@@ -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 ~/.config/webtap/extension/.
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,20 +50,49 @@ 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/wrappers/google-chrome-stable.
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
 
71
+ @app.command(
72
+ display="markdown",
73
+ typer={"name": "setup-desktop", "help": "Install Chrome Debug GUI launcher"},
74
+ fastmcp={"enabled": False},
75
+ )
76
+ def setup_desktop(state, force: bool = False) -> dict:
77
+ """Install Chrome Debug GUI launcher (separate from system Chrome).
78
+
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.
84
+
85
+ Args:
86
+ force: Overwrite existing launcher (default: False)
87
+
88
+ Returns:
89
+ Markdown-formatted result with success/error messages
90
+ """
91
+ service = SetupService()
92
+ result = service.install_desktop_entry(force=force)
93
+ return _format_setup_result(result, "desktop")
94
+
95
+
64
96
  def _format_setup_result(result: dict, component: str) -> dict:
65
97
  """Format setup result as markdown."""
66
98
  elements = []
@@ -123,5 +155,104 @@ def _format_setup_result(result: dict, component: str) -> dict:
123
155
  ],
124
156
  }
125
157
  )
158
+ elif component == "desktop":
159
+ elements.append({"type": "text", "content": "\n**Next steps:**"})
160
+ elements.append(
161
+ {
162
+ "type": "list",
163
+ "items": [
164
+ "Log out and log back in (or run `update-desktop-database ~/.local/share/applications`)",
165
+ "Chrome will now launch with debugging enabled from GUI",
166
+ "The wrapper at ~/.local/bin/wrappers/google-chrome-stable will be used",
167
+ ],
168
+ }
169
+ )
170
+
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
+ )
126
257
 
127
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)