cdpilot 0.4.1 → 0.4.2

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.
package/bin/cdpilot.js CHANGED
@@ -329,6 +329,10 @@ function showHelp() {
329
329
  captcha-check Detect CAPTCHA on active page (JSON output)
330
330
  captcha-wait [s] Pause until user solves CAPTCHA (default 300s)
331
331
 
332
+ RELIABILITY
333
+ browser [name] Show or set preferred browser (chrome|brave|chromium|edge|vivaldi|auto)
334
+ health JSON status: alive, port, tabs, browser, today's crashes
335
+
332
336
  PROJECTS
333
337
  projects List all project browser instances
334
338
  project-stop <id> Stop a specific project's browser
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdpilot",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Zero-dependency browser automation from your terminal. One command, full control.",
5
5
  "bin": {
6
6
  "cdpilot": "./bin/cdpilot.js",
package/src/cdpilot.py CHANGED
@@ -14,7 +14,7 @@ Environment:
14
14
  CDPILOT_PROFILE Isolated browser profile directory
15
15
  """
16
16
 
17
- __version__ = "0.4.1"
17
+ __version__ = "0.4.2"
18
18
 
19
19
  import asyncio
20
20
  import json
@@ -1227,44 +1227,110 @@ async def navigate_collect(ws_url, url, network=False, console=False, glow=True)
1227
1227
 
1228
1228
  # ─── Helper Functions ───
1229
1229
 
1230
- def _find_browser():
1231
- """İşletim sistemine göre tarayıcı ikili dosyasını otomatik olarak bulur."""
1232
- for b in ["brave-browser", "google-chrome", "chromium-browser", "chromium"]:
1233
- found = shutil.which(b)
1234
- if found:
1235
- return found
1236
-
1237
- system = platform.system()
1238
- if system == "Darwin":
1239
- paths = [
1240
- "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
1241
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
1242
- "/Applications/Chromium.app/Contents/MacOS/Chromium",
1243
- ]
1244
- elif system == "Linux":
1245
- paths = [
1246
- "/usr/bin/brave-browser",
1247
- "/usr/bin/google-chrome",
1248
- "/usr/bin/chromium-browser",
1249
- "/usr/bin/chromium",
1250
- "/snap/bin/chromium",
1251
- ]
1252
- elif system == "Windows":
1253
- paths = [
1254
- os.path.expandvars(r"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe"),
1255
- os.path.expandvars(r"C:\Program Files\Google\Chrome\Application\chrome.exe"),
1256
- os.path.expandvars(r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"),
1257
- os.path.expandvars(r"C:\Program Files\Chromium\Application\chromium.exe"),
1258
- ]
1259
- else:
1230
+ # ─── Browser preference & detection ───────────────────────────────────────────
1231
+ # Map of canonical browser names to platform-specific binary candidates.
1232
+ # Order matters within each list: first existing path wins.
1233
+ BROWSER_BINARIES = {
1234
+ 'brave': {'Darwin': ["/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"],
1235
+ 'Linux': ["/usr/bin/brave-browser", "/usr/bin/brave"],
1236
+ 'Windows': [os.path.expandvars(r"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe")]},
1237
+ 'chrome': {'Darwin': ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"],
1238
+ 'Linux': ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable"],
1239
+ 'Windows': [os.path.expandvars(r"C:\Program Files\Google\Chrome\Application\chrome.exe"),
1240
+ os.path.expandvars(r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe")]},
1241
+ 'vivaldi': {'Darwin': ["/Applications/Vivaldi.app/Contents/MacOS/Vivaldi"],
1242
+ 'Linux': ["/usr/bin/vivaldi", "/usr/bin/vivaldi-stable"],
1243
+ 'Windows': [os.path.expandvars(r"C:\Program Files\Vivaldi\Application\vivaldi.exe")]},
1244
+ 'edge': {'Darwin': ["/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"],
1245
+ 'Linux': ["/usr/bin/microsoft-edge"],
1246
+ 'Windows': [os.path.expandvars(r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe")]},
1247
+ 'chromium': {'Darwin': ["/Applications/Chromium.app/Contents/MacOS/Chromium"],
1248
+ 'Linux': ["/usr/bin/chromium", "/usr/bin/chromium-browser", "/snap/bin/chromium"],
1249
+ 'Windows': [os.path.expandvars(r"C:\Program Files\Chromium\Application\chromium.exe")]},
1250
+ }
1251
+
1252
+ BROWSER_CONFIG_FILE = os.path.join(CDPILOT_HOME, 'browser.json')
1253
+
1254
+
1255
+ def _macos_major():
1256
+ """Return macOS major version (e.g. 26) or None on other platforms."""
1257
+ if platform.system() != 'Darwin':
1258
+ return None
1259
+ try:
1260
+ return int(platform.mac_ver()[0].split('.')[0])
1261
+ except (ValueError, IndexError):
1260
1262
  return None
1261
1263
 
1262
- for path in paths:
1264
+
1265
+ def _resolve_browser_name(name):
1266
+ """Resolve a browser name to its binary path. Returns None if not installed."""
1267
+ name = (name or '').lower().strip()
1268
+ if name not in BROWSER_BINARIES:
1269
+ return None
1270
+ candidates = BROWSER_BINARIES[name].get(platform.system(), [])
1271
+ for path in candidates:
1263
1272
  if os.path.exists(path):
1264
1273
  return path
1265
1274
  return None
1266
1275
 
1267
1276
 
1277
+ def get_browser_preference():
1278
+ """Read user's persisted browser preference. Returns name or 'auto'."""
1279
+ if os.path.exists(BROWSER_CONFIG_FILE):
1280
+ try:
1281
+ with open(BROWSER_CONFIG_FILE) as f:
1282
+ return (json.load(f).get('browser') or 'auto').lower()
1283
+ except (OSError, ValueError):
1284
+ pass
1285
+ return 'auto'
1286
+
1287
+
1288
+ def _auto_browser_priority():
1289
+ """Default detection order. macOS 26 (Tahoe) demotes Brave because the
1290
+ current Brave 1.89 build crashes deterministically at ~7min uptime
1291
+ (SIGTRAP in ThreadPoolForegroundWorker, observed across 9+ dumps).
1292
+ Vivaldi takes priority because it's Chromium-based AND honors
1293
+ --load-extension (Chrome silently drops unpacked extensions in 147+).
1294
+ """
1295
+ if _macos_major() and _macos_major() >= 26:
1296
+ return ['vivaldi', 'brave', 'chrome', 'chromium', 'edge']
1297
+ return ['brave', 'chrome', 'vivaldi', 'chromium', 'edge']
1298
+
1299
+
1300
+ def _find_browser():
1301
+ """Locate the browser binary. Priority:
1302
+ 1. CHROME_BIN env var (full path, backward-compatible)
1303
+ 2. ~/.cdpilot/browser.json preference (set via `cdpilot browser <name>`)
1304
+ 3. Auto-detection per _auto_browser_priority()
1305
+ """
1306
+ # 1) PATH lookup for common command names (Linux/Brew installs)
1307
+ for b in ["brave-browser", "vivaldi", "google-chrome", "chromium-browser", "chromium"]:
1308
+ found = shutil.which(b)
1309
+ if found:
1310
+ # Only use PATH match as fallback — preference still wins below.
1311
+ path_match = found
1312
+ break
1313
+ else:
1314
+ path_match = None
1315
+
1316
+ # 2) User preference from config (overrides auto)
1317
+ pref = get_browser_preference()
1318
+ if pref and pref != 'auto':
1319
+ resolved = _resolve_browser_name(pref)
1320
+ if resolved:
1321
+ return resolved
1322
+ # Configured browser not installed — fall through to auto with a warning.
1323
+ sys.stderr.write(f"⚠️ Configured browser '{pref}' not found, falling back to auto-detect.\n")
1324
+
1325
+ # 3) Auto-detection in priority order
1326
+ for name in _auto_browser_priority():
1327
+ resolved = _resolve_browser_name(name)
1328
+ if resolved:
1329
+ return resolved
1330
+
1331
+ return path_match # Last resort
1332
+
1333
+
1268
1334
  def _is_port_in_use(port):
1269
1335
  """Check if a port is actively in use."""
1270
1336
  return not _is_port_free(port)
@@ -1512,10 +1578,28 @@ def cmd_launch():
1512
1578
  # 2) `--disable-features=...` added — catches Brave-specific background
1513
1579
  # tasks that individual `--disable-brave-*` flags don't fully kill
1514
1580
  # (Rewards ad scanner, Sync heartbeat, News fetcher).
1581
+ # Per-browser profile isolation. Brave-written profiles include keys that
1582
+ # confuse Vivaldi/Chrome (search_engines.json missing, prefs schema diffs),
1583
+ # so each browser gets its own subdir. Brave keeps the original path for
1584
+ # backward compatibility — existing users don't lose state on upgrade.
1585
+ browser_basename = os.path.basename(CHROME_BIN).lower()
1586
+ if 'brave' in browser_basename:
1587
+ profile_dir = PROFILE_DIR
1588
+ elif 'vivaldi' in browser_basename:
1589
+ profile_dir = PROFILE_DIR + '-vivaldi'
1590
+ elif 'edge' in browser_basename or 'msedge' in browser_basename:
1591
+ profile_dir = PROFILE_DIR + '-edge'
1592
+ elif 'chromium' in browser_basename:
1593
+ profile_dir = PROFILE_DIR + '-chromium'
1594
+ else:
1595
+ # Default: chrome and unknown chromium-based browsers
1596
+ profile_dir = PROFILE_DIR + '-chrome'
1597
+ os.makedirs(profile_dir, exist_ok=True)
1598
+
1515
1599
  chrome_args = [
1516
1600
  CHROME_BIN,
1517
1601
  f'--remote-debugging-port={CDP_PORT}',
1518
- f'--user-data-dir={PROFILE_DIR}',
1602
+ f'--user-data-dir={profile_dir}',
1519
1603
  '--remote-allow-origins=*',
1520
1604
  '--disable-fre', '--no-default-browser-check', '--no-first-run',
1521
1605
  # ─── Brave-specific features (harmless on other Chromium builds) ───
@@ -2627,6 +2711,53 @@ def cmd_headless(state=None):
2627
2711
  print('Restart browser (stop → launch).')
2628
2712
 
2629
2713
 
2714
+ def cmd_browser(name=None):
2715
+ """Show or set the preferred browser (chrome|brave|chromium|edge|vivaldi|auto).
2716
+
2717
+ The choice is persisted in ~/.cdpilot/browser.json and applies to all
2718
+ projects. `auto` (default) picks per-platform priority — on macOS 26
2719
+ Vivaldi is preferred over Brave due to a known Brave 1.89/macOS 26
2720
+ crash. `CHROME_BIN` env var still overrides if set.
2721
+
2722
+ Usage:
2723
+ cdpilot browser # show current + which is detected
2724
+ cdpilot browser vivaldi # force Vivaldi
2725
+ cdpilot browser auto # restore platform default
2726
+ """
2727
+ if name is None or name.lower() == 'status':
2728
+ pref = get_browser_preference()
2729
+ resolved = _resolve_browser_name(pref) if pref != 'auto' else None
2730
+ auto_resolved = _find_browser()
2731
+ installed = [n for n in BROWSER_BINARIES if _resolve_browser_name(n)]
2732
+ print(f"Preference: {pref}")
2733
+ if pref != 'auto':
2734
+ print(f" resolved: {resolved or '(not found — falling back to auto)'}")
2735
+ print(f"Auto-pick: {auto_resolved or '(none)'}")
2736
+ print(f"Installed: {', '.join(installed) or '(none detected)'}")
2737
+ if _macos_major() and _macos_major() >= 26:
2738
+ print(f"Note: macOS {_macos_major()} demotes Brave in auto-pick (Brave 1.89 crashes ~7min).")
2739
+ return
2740
+
2741
+ name = name.lower().strip()
2742
+ if name not in BROWSER_BINARIES and name != 'auto':
2743
+ valid = ', '.join(['auto'] + list(BROWSER_BINARIES.keys()))
2744
+ print(f"Invalid browser: {name}. Valid: {valid}", file=sys.stderr)
2745
+ sys.exit(1)
2746
+
2747
+ if name != 'auto' and not _resolve_browser_name(name):
2748
+ print(f"⚠️ Browser '{name}' not installed on this system.", file=sys.stderr)
2749
+ candidates = BROWSER_BINARIES[name].get(platform.system(), [])
2750
+ if candidates:
2751
+ print(f" Looked at: {candidates[0]}", file=sys.stderr)
2752
+ sys.exit(1)
2753
+
2754
+ os.makedirs(os.path.dirname(BROWSER_CONFIG_FILE), exist_ok=True)
2755
+ with open(BROWSER_CONFIG_FILE, 'w') as f:
2756
+ json.dump({'browser': name}, f)
2757
+ print(f"Browser preference: {name}")
2758
+ print('Restart the browser (`cdpilot stop` then any command) for the change to take effect.')
2759
+
2760
+
2630
2761
  def cmd_health():
2631
2762
  """Print JSON health summary of the cdpilot browser session.
2632
2763
 
@@ -4842,6 +4973,7 @@ if __name__ == "__main__":
4842
4973
  'proxy': lambda: cmd_proxy(args[0] if args else None),
4843
4974
  'headless': lambda: cmd_headless(args[0] if args else None),
4844
4975
  'stealth': lambda: cmd_stealth(args[0] if args else None),
4976
+ 'browser': lambda: cmd_browser(args[0] if args else None),
4845
4977
  'health': cmd_health,
4846
4978
  'session': cmd_session,
4847
4979
  'sessions': cmd_sessions,