webtap-tool 0.1.5__py3-none-any.whl → 0.2.1__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,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/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
 
64
71
  @app.command(
65
72
  display="markdown",
66
- typer={"name": "setup-desktop", "help": "Install desktop entry to use Chrome wrapper from GUI"},
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 desktop entry to ~/.local/share/applications/google-chrome.desktop.
77
+ """Install Chrome Debug GUI launcher (separate from system Chrome).
71
78
 
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.
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 desktop entry (default: False)
86
+ force: Overwrite existing launcher (default: False)
77
87
 
78
88
  Returns:
79
89
  Markdown-formatted result with success/error messages
@@ -140,22 +150,100 @@ def _format_setup_result(result: dict, component: str) -> dict:
140
150
  {
141
151
  "type": "list",
142
152
  "items": [
143
- "Run `google-chrome-stable` to start Chrome with debugging",
153
+ "Run `chrome-debug` to start Chrome with debugging",
144
154
  "Or use `run-chrome` command for direct launch",
145
155
  ],
146
156
  }
147
157
  )
148
158
  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
- )
159
+ # Platform-specific instructions are already in the service's details
160
+ pass
161
+
162
+ return {"elements": elements}
163
+
164
+
165
+ @app.command(
166
+ display="markdown",
167
+ typer={"name": "setup-cleanup", "help": "Clean up old WebTap installations"},
168
+ fastmcp={"enabled": False},
169
+ )
170
+ def setup_cleanup(state, dry_run: bool = True) -> dict:
171
+ """Clean up old WebTap installations from previous versions.
172
+
173
+ Checks for and removes:
174
+ - Old extension location (~/.config/webtap/extension/)
175
+ - Old desktop entries created by webtap
176
+ - Unmounted bindfs directories
177
+
178
+ Args:
179
+ dry_run: Only show what would be cleaned (default: True)
180
+
181
+ Returns:
182
+ Markdown report of cleanup actions
183
+ """
184
+ service = SetupService()
185
+ result = service.cleanup_old_installations(dry_run=dry_run)
186
+
187
+ elements = []
188
+
189
+ # Header
190
+ elements.append({"type": "heading", "level": 2, "content": "WebTap Cleanup Report"})
191
+
192
+ # Old installations found
193
+ if result.get("old_extension"):
194
+ elements.append({"type": "heading", "level": 3, "content": "Old Extension Location"})
195
+ elements.append({"type": "text", "content": f"Found: `{result['old_extension']['path']}`"})
196
+ elements.append({"type": "text", "content": f"Size: {result['old_extension']['size']}"})
197
+ if not dry_run and result["old_extension"].get("removed"):
198
+ elements.append({"type": "alert", "message": "✓ Removed old extension", "level": "success"})
199
+ elif dry_run:
200
+ elements.append({"type": "alert", "message": "Would remove (dry-run mode)", "level": "info"})
201
+
202
+ # Old Chrome wrapper
203
+ if result.get("old_wrapper"):
204
+ elements.append({"type": "heading", "level": 3, "content": "Old Chrome Wrapper"})
205
+ elements.append({"type": "text", "content": f"Found: `{result['old_wrapper']['path']}`"})
206
+ if not dry_run and result["old_wrapper"].get("removed"):
207
+ elements.append({"type": "alert", "message": "✓ Removed old wrapper", "level": "success"})
208
+ elif dry_run:
209
+ elements.append({"type": "alert", "message": "Would remove (dry-run mode)", "level": "info"})
210
+
211
+ # Old desktop entry
212
+ if result.get("old_desktop"):
213
+ elements.append({"type": "heading", "level": 3, "content": "Old Desktop Entry"})
214
+ elements.append({"type": "text", "content": f"Found: `{result['old_desktop']['path']}`"})
215
+ if not dry_run and result["old_desktop"].get("removed"):
216
+ elements.append({"type": "alert", "message": "✓ Removed old desktop entry", "level": "success"})
217
+ elif dry_run:
218
+ elements.append({"type": "alert", "message": "Would remove (dry-run mode)", "level": "info"})
219
+
220
+ # Check for bindfs mounts
221
+ if result.get("bindfs_mount"):
222
+ elements.append({"type": "heading", "level": 3, "content": "Bindfs Mount Detected"})
223
+ elements.append({"type": "text", "content": f"Mount: `{result['bindfs_mount']}`"})
224
+ elements.append(
225
+ {"type": "alert", "message": "To unmount: fusermount -u " + result["bindfs_mount"], "level": "warning"}
226
+ )
227
+
228
+ # Summary
229
+ elements.append({"type": "heading", "level": 3, "content": "Summary"})
230
+ if dry_run:
231
+ elements.append({"type": "text", "content": "**Dry-run mode** - no changes made"})
232
+ elements.append({"type": "text", "content": "To perform cleanup: `setup-cleanup --no-dry-run`"})
233
+ else:
234
+ elements.append({"type": "alert", "message": "Cleanup completed", "level": "success"})
235
+
236
+ # Next steps
237
+ elements.append({"type": "heading", "level": 3, "content": "Next Steps"})
238
+ elements.append(
239
+ {
240
+ "type": "list",
241
+ "items": [
242
+ "Run `setup-extension` to install extension in new location",
243
+ "Run `setup-chrome --bindfs` for bindfs mode or `setup-chrome` for standard mode",
244
+ "Run `setup-desktop` to create Chrome Debug launcher",
245
+ ],
246
+ }
247
+ )
160
248
 
161
249
  return {"elements": elements}
@@ -1,4 +1,4 @@
1
- """Setup service for installing WebTap components.
1
+ """Setup service for installing WebTap components (cross-platform).
2
2
 
3
3
  PUBLIC API:
4
4
  - SetupService: Main service class for all setup operations
@@ -6,18 +6,30 @@ PUBLIC API:
6
6
 
7
7
  from typing import Dict, Any
8
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
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
13
14
 
14
15
 
15
16
  class SetupService:
16
- """Service for installing WebTap components.
17
+ """Orchestrator service for installing WebTap components.
17
18
 
18
- Delegates to specialized modules for each component type.
19
+ Delegates to specialized service classes for each component type.
19
20
  """
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
+
21
33
  def install_filters(self, force: bool = False) -> Dict[str, Any]:
22
34
  """Install filter configuration.
23
35
 
@@ -27,7 +39,7 @@ class SetupService:
27
39
  Returns:
28
40
  Dict with success, message, path, details
29
41
  """
30
- return install_filters(force=force)
42
+ return self.filters_service.install_filters(force=force)
31
43
 
32
44
  def install_extension(self, force: bool = False) -> Dict[str, Any]:
33
45
  """Install Chrome extension files.
@@ -38,21 +50,25 @@ class SetupService:
38
50
  Returns:
39
51
  Dict with success, message, path, details
40
52
  """
41
- return install_extension(force=force)
53
+ return self.extension_service.install_extension(force=force)
42
54
 
43
- def install_chrome_wrapper(self, force: bool = False) -> Dict[str, Any]:
55
+ def install_chrome_wrapper(self, force: bool = False, bindfs: bool = False) -> Dict[str, Any]:
44
56
  """Install Chrome wrapper script.
45
57
 
46
58
  Args:
47
59
  force: Overwrite existing script
60
+ bindfs: Use bindfs to mount real Chrome profile (Linux only)
48
61
 
49
62
  Returns:
50
63
  Dict with success, message, path, details
51
64
  """
52
- return install_chrome_wrapper(force=force)
65
+ return self.chrome_service.install_wrapper(force=force, bindfs=bindfs)
53
66
 
54
67
  def install_desktop_entry(self, force: bool = False) -> Dict[str, Any]:
55
- """Install desktop entry for GUI integration.
68
+ """Install desktop entry or app bundle for GUI integration.
69
+
70
+ On Linux: Creates .desktop file
71
+ On macOS: Creates .app bundle
56
72
 
57
73
  Args:
58
74
  force: Overwrite existing entry
@@ -60,7 +76,103 @@ class SetupService:
60
76
  Returns:
61
77
  Dict with success, message, path, details
62
78
  """
63
- return install_desktop_entry(force=force)
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
64
176
 
65
177
 
66
178
  __all__ = ["SetupService"]
@@ -1,41 +1,37 @@
1
- """Chrome wrapper setup functionality for WebTap."""
1
+ """Chrome setup service for installing wrapper scripts (cross-platform)."""
2
2
 
3
3
  import os
4
4
  import logging
5
- from pathlib import Path
6
5
  from typing import Dict, Any
7
6
 
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
7
+ from .platform import get_platform_info, ensure_directories
16
8
 
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
- }
9
+ logger = logging.getLogger(__name__)
29
10
 
30
- wrapper_script = """#!/bin/bash
31
- # Chrome wrapper using bindfs for perfect state sync with debug port
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
32
16
 
33
17
  DEBUG_DIR="$HOME/.config/google-chrome-debug"
34
18
  REAL_DIR="$HOME/.config/google-chrome"
19
+ PORT=${{WEBTAP_PORT:-9222}}
20
+ CHROME_BIN="{chrome_path}"
35
21
 
36
22
  # Check if bindfs is installed
37
23
  if ! command -v bindfs &>/dev/null; then
38
- echo "Error: bindfs not installed. Install with: yay -S bindfs" >&2
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
39
35
  exit 1
40
36
  fi
41
37
 
@@ -50,28 +46,184 @@ if ! mountpoint -q "$DEBUG_DIR" 2>/dev/null; then
50
46
  fi
51
47
 
52
48
  # Launch Chrome with debugging on bindfs mount
53
- exec /usr/bin/google-chrome-stable \\
54
- --remote-debugging-port=9222 \\
49
+ exec "$CHROME_BIN" \\
50
+ --remote-debugging-port="$PORT" \\
55
51
  --remote-allow-origins='*' \\
56
52
  --user-data-dir="$DEBUG_DIR" \\
57
53
  "$@"
58
54
  """
59
55
 
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
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
+ --remote-allow-origins='*' \\
79
+ --user-data-dir="$PROFILE_DIR" \\
80
+ --no-first-run \\
81
+ --no-default-browser-check \\
82
+ "$@"
83
+ """
84
+
85
+ MACOS_WRAPPER = """#!/bin/bash
86
+ # Chrome Debug Launcher for macOS
87
+ # Generated by webtap setup-chrome
88
+
89
+ # Configuration
90
+ PORT=${{WEBTAP_PORT:-9222}}
91
+ PROFILE_BASE="{profile_dir}"
92
+ CHROME_APP="{chrome_path}"
93
+
94
+ # Profile handling
95
+ if [ "$1" = "--temp" ]; then
96
+ PROFILE_DIR="$(mktemp -d /tmp/webtap-chrome-XXXXXX)"
97
+ shift
98
+ echo "Using temporary profile: $PROFILE_DIR" >&2
99
+ else
100
+ PROFILE_DIR="$PROFILE_BASE/default"
101
+ mkdir -p "$PROFILE_DIR"
102
+ fi
103
+
104
+ # Launch Chrome with debugging
105
+ exec "$CHROME_APP" \\
106
+ --remote-debugging-port="$PORT" \\
107
+ --remote-allow-origins='*' \\
108
+ --user-data-dir="$PROFILE_DIR" \\
109
+ --no-first-run \\
110
+ --no-default-browser-check \\
111
+ "$@"
112
+ """
113
+
114
+
115
+ class ChromeSetupService:
116
+ """Chrome wrapper installation service (cross-platform)."""
117
+
118
+ def __init__(self):
119
+ self.info = get_platform_info()
120
+ self.paths = self.info["paths"]
121
+ self.chrome = self.info["chrome"]
122
+
123
+ # Unified wrapper location for both platforms
124
+ self.wrapper_dir = self.paths["bin_dir"] # ~/.local/bin
125
+ self.wrapper_name = self.chrome["wrapper_name"] # chrome-debug
126
+ self.wrapper_path = self.wrapper_dir / self.wrapper_name
127
+
128
+ # Profile locations
129
+ self.profile_dir = self.paths["data_dir"] / "profiles"
130
+ self.temp_profile_dir = self.paths["runtime_dir"] / "profiles"
131
+
132
+ def install_wrapper(self, force: bool = False, bindfs: bool = False) -> Dict[str, Any]:
133
+ """Install Chrome wrapper script appropriate for platform.
134
+
135
+ Args:
136
+ force: Overwrite existing wrapper
137
+ bindfs: Use bindfs to mount real Chrome profile (Linux only)
138
+
139
+ Returns:
140
+ Installation result
141
+ """
142
+ if not self.chrome["found"]:
143
+ return {
144
+ "success": False,
145
+ "message": "Chrome not found on system",
146
+ "details": "Please install Google Chrome first",
147
+ }
148
+
149
+ if self.wrapper_path.exists() and not force:
150
+ return {
151
+ "success": False,
152
+ "message": f"Wrapper already exists at {self.wrapper_path}",
153
+ "details": "Use --force to overwrite",
154
+ "path": str(self.wrapper_path),
155
+ }
156
+
157
+ # Ensure directories exist
158
+ ensure_directories()
159
+ self.wrapper_dir.mkdir(parents=True, exist_ok=True)
160
+
161
+ # Generate platform-specific wrapper
162
+ if self.info["is_macos"]:
163
+ wrapper_content = self._generate_macos_wrapper()
164
+ else:
165
+ wrapper_content = self._generate_linux_wrapper(bindfs=bindfs)
166
+
167
+ self.wrapper_path.write_text(wrapper_content)
168
+ self.wrapper_path.chmod(0o755)
169
+
170
+ # Check if wrapper dir is in PATH
171
+ path_dirs = os.environ.get("PATH", "").split(os.pathsep)
172
+ in_path = str(self.wrapper_dir) in path_dirs
173
+
174
+ logger.info(f"Installed Chrome wrapper to {self.wrapper_path}")
175
+
176
+ # Build detailed message with PATH setup instructions
177
+ if in_path:
178
+ details = "✅ Run 'chrome-debug' to launch Chrome with debugging"
179
+ else:
180
+ shell = os.environ.get("SHELL", "/bin/bash")
181
+ if "zsh" in shell:
182
+ rc_file = "~/.zshrc"
183
+ elif "fish" in shell:
184
+ rc_file = "~/.config/fish/config.fish"
185
+ else:
186
+ rc_file = "~/.bashrc"
187
+
188
+ details = (
189
+ f"⚠️ Add to PATH to use from terminal:\n"
190
+ f"echo 'export PATH=\"$HOME/.local/bin:$PATH\"' >> {rc_file}\n"
191
+ f"source {rc_file}\n\n"
192
+ f"Or run directly: ~/.local/bin/chrome-debug\n"
193
+ f"✅ GUI launcher will work regardless (uses full path)"
194
+ )
195
+
196
+ return {
197
+ "success": True,
198
+ "message": "Chrome wrapper 'chrome-debug' installed successfully",
199
+ "path": str(self.wrapper_path),
200
+ "details": details,
201
+ }
202
+
203
+ def _generate_linux_wrapper(self, bindfs: bool = False) -> str:
204
+ """Generate Linux wrapper script with optional bindfs support.
205
+
206
+ Args:
207
+ bindfs: If True, use bindfs to mount real Chrome profile
208
+
209
+ Returns:
210
+ Wrapper script content
211
+ """
212
+ chrome_path = self.chrome["path"]
213
+ profile_dir = self.profile_dir
214
+
215
+ if bindfs:
216
+ return LINUX_BINDFS_WRAPPER.format(chrome_path=chrome_path)
217
+ else:
218
+ return LINUX_STANDARD_WRAPPER.format(chrome_path=chrome_path, profile_dir=profile_dir)
64
219
 
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
220
+ def _generate_macos_wrapper(self) -> str:
221
+ """Generate macOS wrapper script.
69
222
 
70
- logger.info(f"Installed Chrome wrapper to {target_path}")
223
+ Returns:
224
+ Wrapper script content
225
+ """
226
+ chrome_path = self.chrome["path"]
227
+ profile_dir = self.profile_dir
71
228
 
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
- }
229
+ return MACOS_WRAPPER.format(chrome_path=chrome_path, profile_dir=profile_dir)