browser-control-and-automation-cli 0.1.1.dev3__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.
Files changed (104) hide show
  1. browser_cli/__init__.py +7 -0
  2. browser_cli/actions/__init__.py +5 -0
  3. browser_cli/actions/cli_specs.py +868 -0
  4. browser_cli/agent_scope/__init__.py +14 -0
  5. browser_cli/automation/__init__.py +11 -0
  6. browser_cli/automation/api/__init__.py +5 -0
  7. browser_cli/automation/api/server.py +321 -0
  8. browser_cli/automation/hooks.py +42 -0
  9. browser_cli/automation/loader.py +134 -0
  10. browser_cli/automation/models.py +127 -0
  11. browser_cli/automation/persistence/__init__.py +5 -0
  12. browser_cli/automation/persistence/store.py +682 -0
  13. browser_cli/automation/projections.py +279 -0
  14. browser_cli/automation/publisher.py +153 -0
  15. browser_cli/automation/scheduler/__init__.py +5 -0
  16. browser_cli/automation/scheduler/schedule.py +113 -0
  17. browser_cli/automation/service/__init__.py +8 -0
  18. browser_cli/automation/service/__main__.py +64 -0
  19. browser_cli/automation/service/client.py +232 -0
  20. browser_cli/automation/service/runtime.py +388 -0
  21. browser_cli/automation/toml.py +35 -0
  22. browser_cli/automation/web/__init__.py +5 -0
  23. browser_cli/automation/web/assets.py +377 -0
  24. browser_cli/browser/__init__.py +1 -0
  25. browser_cli/browser/models.py +44 -0
  26. browser_cli/browser/network_capture.py +392 -0
  27. browser_cli/browser/service.py +1519 -0
  28. browser_cli/browser/session.py +158 -0
  29. browser_cli/browser/snapshot.py +35 -0
  30. browser_cli/browser/stealth.py +450 -0
  31. browser_cli/cli/__init__.py +1 -0
  32. browser_cli/cli/error_hints.py +26 -0
  33. browser_cli/cli/main.py +298 -0
  34. browser_cli/commands/__init__.py +1 -0
  35. browser_cli/commands/action.py +25 -0
  36. browser_cli/commands/automation.py +304 -0
  37. browser_cli/commands/doctor.py +336 -0
  38. browser_cli/commands/install_skills.py +138 -0
  39. browser_cli/commands/paths.py +31 -0
  40. browser_cli/commands/read.py +33 -0
  41. browser_cli/commands/reload.py +51 -0
  42. browser_cli/commands/status.py +444 -0
  43. browser_cli/commands/task.py +71 -0
  44. browser_cli/constants.py +97 -0
  45. browser_cli/daemon/__init__.py +1 -0
  46. browser_cli/daemon/__main__.py +34 -0
  47. browser_cli/daemon/app.py +941 -0
  48. browser_cli/daemon/browser_service.py +998 -0
  49. browser_cli/daemon/client.py +280 -0
  50. browser_cli/daemon/models.py +63 -0
  51. browser_cli/daemon/runtime_presentation.py +167 -0
  52. browser_cli/daemon/server.py +103 -0
  53. browser_cli/daemon/state.py +15 -0
  54. browser_cli/daemon/transport.py +77 -0
  55. browser_cli/drivers/__init__.py +6 -0
  56. browser_cli/drivers/_extension/__init__.py +1 -0
  57. browser_cli/drivers/_extension/artifact_actions.py +154 -0
  58. browser_cli/drivers/_extension/dialog_actions.py +35 -0
  59. browser_cli/drivers/_extension/input_actions.py +63 -0
  60. browser_cli/drivers/_extension/locator_actions.py +100 -0
  61. browser_cli/drivers/_extension/observe_actions.py +285 -0
  62. browser_cli/drivers/_extension/page_actions.py +210 -0
  63. browser_cli/drivers/_extension/state_actions.py +134 -0
  64. browser_cli/drivers/base.py +391 -0
  65. browser_cli/drivers/extension_driver.py +33 -0
  66. browser_cli/drivers/models.py +21 -0
  67. browser_cli/drivers/playwright_driver.py +499 -0
  68. browser_cli/error_codes.py +27 -0
  69. browser_cli/errors.py +146 -0
  70. browser_cli/exit_codes.py +9 -0
  71. browser_cli/extension/__init__.py +32 -0
  72. browser_cli/extension/protocol.py +222 -0
  73. browser_cli/extension/session.py +393 -0
  74. browser_cli/network.py +147 -0
  75. browser_cli/outputs/__init__.py +1 -0
  76. browser_cli/outputs/json.py +10 -0
  77. browser_cli/outputs/render.py +7 -0
  78. browser_cli/packaged_skills/__init__.py +1 -0
  79. browser_cli/packaged_skills/browser-cli-converge/SKILL.md +62 -0
  80. browser_cli/packaged_skills/browser-cli-delivery/SKILL.md +61 -0
  81. browser_cli/packaged_skills/browser-cli-explore/SKILL.md +77 -0
  82. browser_cli/profiles/__init__.py +1 -0
  83. browser_cli/profiles/discovery.py +185 -0
  84. browser_cli/refs/__init__.py +17 -0
  85. browser_cli/refs/generator.py +386 -0
  86. browser_cli/refs/models.py +127 -0
  87. browser_cli/refs/registry.py +51 -0
  88. browser_cli/refs/resolver.py +214 -0
  89. browser_cli/tabs/__init__.py +5 -0
  90. browser_cli/tabs/registry.py +283 -0
  91. browser_cli/task_runtime/__init__.py +31 -0
  92. browser_cli/task_runtime/client.py +107 -0
  93. browser_cli/task_runtime/entrypoint.py +110 -0
  94. browser_cli/task_runtime/errors.py +17 -0
  95. browser_cli/task_runtime/flow.py +171 -0
  96. browser_cli/task_runtime/models.py +97 -0
  97. browser_cli/task_runtime/read.py +71 -0
  98. browser_cli/task_runtime/templates.py +58 -0
  99. browser_control_and_automation_cli-0.1.1.dev3.dist-info/METADATA +340 -0
  100. browser_control_and_automation_cli-0.1.1.dev3.dist-info/RECORD +104 -0
  101. browser_control_and_automation_cli-0.1.1.dev3.dist-info/WHEEL +5 -0
  102. browser_control_and_automation_cli-0.1.1.dev3.dist-info/entry_points.txt +2 -0
  103. browser_control_and_automation_cli-0.1.1.dev3.dist-info/licenses/LICENSE +21 -0
  104. browser_control_and_automation_cli-0.1.1.dev3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,336 @@
1
+ """Install and runtime diagnostics for Browser CLI environments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import os
7
+ import shutil
8
+ import sys
9
+ from argparse import Namespace
10
+ from dataclasses import asdict, dataclass
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from browser_cli.automation.service.client import (
15
+ _probe_automation_service,
16
+ read_automation_service_run_info,
17
+ )
18
+ from browser_cli.constants import AppPaths, get_app_paths
19
+ from browser_cli.daemon.client import run_info_is_compatible, send_command
20
+ from browser_cli.daemon.transport import probe_socket, read_run_info
21
+ from browser_cli.outputs.json import render_json_payload
22
+
23
+ LOCK_FILES = ("SingletonLock", "SingletonCookie", "SingletonSocket")
24
+
25
+
26
+ @dataclass(slots=True, frozen=True)
27
+ class DoctorCheck:
28
+ id: str
29
+ status: str
30
+ summary: str
31
+ details: str | None = None
32
+ next: str | None = None
33
+
34
+
35
+ def run_doctor_command(args: Namespace) -> str:
36
+ report = collect_doctor_report()
37
+ if getattr(args, "json", False):
38
+ return render_json_payload({"ok": True, "data": report, "meta": {"action": "doctor"}})
39
+ return render_doctor_report(report)
40
+
41
+
42
+ def collect_doctor_report() -> dict[str, Any]:
43
+ app_paths = get_app_paths()
44
+ daemon_payload = _daemon_runtime_payload()
45
+ checks = [
46
+ _package_check(),
47
+ _chrome_check(),
48
+ _playwright_check(),
49
+ _home_check(app_paths),
50
+ _managed_profile_check(),
51
+ _daemon_check(app_paths),
52
+ _automation_service_check(app_paths),
53
+ _extension_check(daemon_payload),
54
+ ]
55
+ overall_status = "pass"
56
+ if any(item.status == "fail" for item in checks):
57
+ overall_status = "fail"
58
+ elif any(item.status == "warn" for item in checks):
59
+ overall_status = "warn"
60
+ return {
61
+ "overall_status": overall_status,
62
+ "checks": [asdict(item) for item in checks],
63
+ }
64
+
65
+
66
+ def render_doctor_report(report: dict[str, Any]) -> str:
67
+ lines = [f"Doctor: {report['overall_status']}", ""]
68
+ for check in report["checks"]:
69
+ details = f" ({check['details']})" if check.get("details") else ""
70
+ lines.append(f"{check['id']}: {check['status']} - {check['summary']}{details}")
71
+ if check.get("next"):
72
+ lines.append(f"Next: {check['next']}")
73
+ lines.append("")
74
+ return "\n".join(lines).rstrip() + "\n"
75
+
76
+
77
+ def _package_check() -> DoctorCheck:
78
+ entrypoint = importlib.util.find_spec("browser_cli")
79
+ if entrypoint is None:
80
+ return DoctorCheck(
81
+ id="package",
82
+ status="fail",
83
+ summary="Python package import failed.",
84
+ next=(
85
+ "reinstall browser-control-and-automation-cli in this Python environment "
86
+ "and re-run browser-cli doctor"
87
+ ),
88
+ )
89
+ return DoctorCheck(
90
+ id="package",
91
+ status="pass",
92
+ summary="Python package import works.",
93
+ )
94
+
95
+
96
+ def _chrome_check() -> DoctorCheck:
97
+ try:
98
+ executable = _discover_chrome_executable()
99
+ except Exception as exc:
100
+ return DoctorCheck(
101
+ id="chrome",
102
+ status="fail",
103
+ summary="Stable Google Chrome was not found.",
104
+ details=str(exc),
105
+ next="install stable Google Chrome and re-run browser-cli doctor",
106
+ )
107
+ return DoctorCheck(
108
+ id="chrome",
109
+ status="pass",
110
+ summary="Stable Google Chrome was found.",
111
+ details=str(executable),
112
+ )
113
+
114
+
115
+ def _playwright_check() -> DoctorCheck:
116
+ if importlib.util.find_spec("playwright") is None:
117
+ return DoctorCheck(
118
+ id="playwright",
119
+ status="fail",
120
+ summary="Playwright Python package is not installed.",
121
+ next=(
122
+ "reinstall browser-control-and-automation-cli with uv tool install "
123
+ "--reinstall browser-control-and-automation-cli, or run uv sync --dev "
124
+ "in a repository checkout, then re-run browser-cli doctor"
125
+ ),
126
+ )
127
+ return DoctorCheck(
128
+ id="playwright",
129
+ status="pass",
130
+ summary="Playwright Python package import works.",
131
+ )
132
+
133
+
134
+ def _home_check(app_paths: AppPaths) -> DoctorCheck:
135
+ if not app_paths.home.exists():
136
+ return DoctorCheck(
137
+ id="home",
138
+ status="fail",
139
+ summary="Browser CLI home does not exist yet.",
140
+ details=str(app_paths.home),
141
+ next="create the Browser CLI home directory or run a Browser CLI command that initializes it, then re-run browser-cli doctor",
142
+ )
143
+ if not _is_writable_directory(app_paths.home):
144
+ return DoctorCheck(
145
+ id="home",
146
+ status="fail",
147
+ summary="Browser CLI home is not writable.",
148
+ details=str(app_paths.home),
149
+ next="set BROWSER_CLI_HOME to a writable directory and re-run browser-cli doctor",
150
+ )
151
+ return DoctorCheck(
152
+ id="home",
153
+ status="pass",
154
+ summary="Browser CLI home is ready.",
155
+ details=str(app_paths.home),
156
+ )
157
+
158
+
159
+ def _managed_profile_check() -> DoctorCheck:
160
+ profile_root = get_app_paths().home / "default-profile"
161
+ if not profile_root.exists():
162
+ return DoctorCheck(
163
+ id="managed_profile",
164
+ status="fail",
165
+ summary="Managed profile directory does not exist yet.",
166
+ details=str(profile_root),
167
+ next="run browser-cli read https://example.com once to initialize the managed profile, then re-run browser-cli doctor",
168
+ )
169
+ if not _is_writable_directory(profile_root):
170
+ return DoctorCheck(
171
+ id="managed_profile",
172
+ status="fail",
173
+ summary="Managed profile directory is not writable.",
174
+ details=str(profile_root),
175
+ next="fix permissions for the Browser CLI home directory and re-run browser-cli doctor",
176
+ )
177
+
178
+ lock_paths = [
179
+ profile_root / lock_name for lock_name in LOCK_FILES if (profile_root / lock_name).exists()
180
+ ]
181
+ if lock_paths:
182
+ return DoctorCheck(
183
+ id="managed_profile",
184
+ status="warn",
185
+ summary="Managed profile directory exists but appears to be in use.",
186
+ details=", ".join(str(path) for path in lock_paths),
187
+ next="close Browser CLI-owned Chrome windows or inspect browser-cli status",
188
+ )
189
+ return DoctorCheck(
190
+ id="managed_profile",
191
+ status="pass",
192
+ summary="Managed profile directory is writable.",
193
+ details=str(profile_root),
194
+ )
195
+
196
+
197
+ def _daemon_check(app_paths: AppPaths) -> DoctorCheck:
198
+ run_info = read_run_info()
199
+ return _runtime_reachability_check(
200
+ id="daemon",
201
+ metadata_exists=run_info is not None,
202
+ reachable=probe_socket(),
203
+ metadata_path=app_paths.run_info_path,
204
+ reachable_summary="Browser daemon is reachable.",
205
+ unreachable_summary="Daemon runtime metadata exists but the daemon is not reachable.",
206
+ missing_summary="Daemon is not running yet.",
207
+ reachable_details=app_paths.socket_path,
208
+ unreachable_next="run browser-cli reload",
209
+ missing_next="run browser-cli read https://example.com or browser-cli status when you are ready",
210
+ )
211
+
212
+
213
+ def _automation_service_check(app_paths: AppPaths) -> DoctorCheck:
214
+ run_info = read_automation_service_run_info()
215
+ return _runtime_reachability_check(
216
+ id="automation_service",
217
+ metadata_exists=run_info is not None,
218
+ reachable=_probe_automation_service(run_info),
219
+ metadata_path=app_paths.automation_service_run_info_path,
220
+ reachable_summary="Automation service is reachable.",
221
+ unreachable_summary="Automation service metadata exists but service is not reachable.",
222
+ missing_summary="Automation service is not running yet.",
223
+ reachable_details=app_paths.automation_service_run_info_path,
224
+ unreachable_next="run browser-cli automation status or browser-cli automation ui after restarting the service",
225
+ missing_next="run browser-cli automation status or browser-cli automation ui when you need published automation management",
226
+ )
227
+
228
+
229
+ def _extension_check(daemon_payload: dict[str, Any] | None) -> DoctorCheck:
230
+ if daemon_payload is None:
231
+ return DoctorCheck(
232
+ id="extension",
233
+ status="warn",
234
+ summary="Extension reachability is unavailable until the daemon is running.",
235
+ next="run browser-cli status after the daemon starts if you need extension mode",
236
+ )
237
+ extension = dict(daemon_payload.get("extension") or {})
238
+ if bool(extension.get("connected")) and bool(extension.get("capability_complete")):
239
+ return DoctorCheck(
240
+ id="extension",
241
+ status="pass",
242
+ summary="Browser CLI extension is connected and capability-complete.",
243
+ )
244
+ if bool(extension.get("connected")):
245
+ missing = ", ".join(str(item) for item in (extension.get("missing_capabilities") or []))
246
+ details = missing or "required capability set is incomplete"
247
+ return DoctorCheck(
248
+ id="extension",
249
+ status="warn",
250
+ summary="Browser CLI extension is connected but incomplete.",
251
+ details=details,
252
+ next="reload the extension or run browser-cli reload after reconnecting it",
253
+ )
254
+ return DoctorCheck(
255
+ id="extension",
256
+ status="warn",
257
+ summary="Browser CLI extension is not connected.",
258
+ next="start with managed profile mode, or connect the extension if you need real Chrome behavior",
259
+ )
260
+
261
+
262
+ def _discover_chrome_executable() -> Path:
263
+ if sys.platform == "darwin":
264
+ candidates = [Path("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")]
265
+ elif sys.platform.startswith("linux"):
266
+ candidates = []
267
+ for binary_name in ("google-chrome", "google-chrome-stable"):
268
+ binary_path = shutil.which(binary_name)
269
+ if binary_path:
270
+ candidates.append(Path(binary_path))
271
+ candidates.extend(
272
+ [
273
+ Path("/opt/google/chrome/chrome"),
274
+ Path("/usr/bin/google-chrome"),
275
+ Path("/usr/bin/google-chrome-stable"),
276
+ ]
277
+ )
278
+ else:
279
+ raise RuntimeError(f"Unsupported platform for Chrome discovery: {sys.platform}")
280
+
281
+ for candidate in candidates:
282
+ if candidate.exists():
283
+ return candidate
284
+ raise RuntimeError("Stable Google Chrome was not found on this machine.")
285
+
286
+
287
+ def _daemon_runtime_payload() -> dict[str, Any] | None:
288
+ run_info = read_run_info()
289
+ if not probe_socket() or not run_info_is_compatible(run_info):
290
+ return None
291
+ try:
292
+ response = send_command("runtime-status", {"warmup": False}, start_if_needed=False)
293
+ except Exception:
294
+ return None
295
+ data = response.get("data")
296
+ return dict(data) if isinstance(data, dict) else None
297
+
298
+
299
+ def _is_writable_directory(path: Path) -> bool:
300
+ return path.is_dir() and os.access(path, os.W_OK)
301
+
302
+
303
+ def _runtime_reachability_check(
304
+ *,
305
+ id: str,
306
+ metadata_exists: bool,
307
+ reachable: bool,
308
+ metadata_path: Path,
309
+ reachable_summary: str,
310
+ unreachable_summary: str,
311
+ missing_summary: str,
312
+ reachable_details: Path,
313
+ unreachable_next: str,
314
+ missing_next: str,
315
+ ) -> DoctorCheck:
316
+ if reachable:
317
+ return DoctorCheck(
318
+ id=id,
319
+ status="pass",
320
+ summary=reachable_summary,
321
+ details=str(reachable_details),
322
+ )
323
+ if metadata_exists:
324
+ return DoctorCheck(
325
+ id=id,
326
+ status="warn",
327
+ summary=unreachable_summary,
328
+ details=str(metadata_path),
329
+ next=unreachable_next,
330
+ )
331
+ return DoctorCheck(
332
+ id=id,
333
+ status="warn",
334
+ summary=missing_summary,
335
+ next=missing_next,
336
+ )
@@ -0,0 +1,138 @@
1
+ """Install packaged Browser CLI skills into a target skills directory."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import shutil
8
+ import uuid
9
+ from dataclasses import dataclass
10
+ from importlib import resources
11
+ from importlib.abc import Traversable
12
+ from pathlib import Path
13
+
14
+ from browser_cli.errors import InvalidInputError, OperationFailedError
15
+
16
+ PUBLIC_SKILL_NAMES = (
17
+ "browser-cli-converge",
18
+ "browser-cli-delivery",
19
+ "browser-cli-explore",
20
+ )
21
+
22
+
23
+ @dataclass(frozen=True, slots=True)
24
+ class PackagedSkill:
25
+ name: str
26
+ source: Traversable | Path
27
+
28
+
29
+ def _packaged_skills_root() -> Traversable:
30
+ return resources.files("browser_cli.packaged_skills")
31
+
32
+
33
+ def discover_packaged_skills() -> list[PackagedSkill]:
34
+ root = _packaged_skills_root()
35
+ discovered: list[PackagedSkill] = []
36
+ for name in PUBLIC_SKILL_NAMES:
37
+ skill_root = root.joinpath(name)
38
+ if not skill_root.is_dir():
39
+ raise InvalidInputError(f"Packaged skill is missing from this build: {name}")
40
+ skill_doc = skill_root.joinpath("SKILL.md")
41
+ if not skill_doc.is_file():
42
+ raise InvalidInputError(
43
+ f"Packaged skill is incomplete in this build: {name} is missing SKILL.md"
44
+ )
45
+ discovered.append(PackagedSkill(name=name, source=skill_root))
46
+ return discovered
47
+
48
+
49
+ def get_skills_target_path(target: str | None) -> Path:
50
+ if target:
51
+ return Path(target).expanduser().resolve()
52
+ return Path.home() / ".agents" / "skills"
53
+
54
+
55
+ def install_skills_from_paths(
56
+ skills: list[PackagedSkill],
57
+ target: Path,
58
+ *,
59
+ dry_run: bool = False,
60
+ ) -> list[tuple[str, str]]:
61
+ results: list[tuple[str, str]] = []
62
+ if not dry_run:
63
+ try:
64
+ target.mkdir(parents=True, exist_ok=True)
65
+ except OSError as exc:
66
+ raise OperationFailedError(
67
+ f"Could not create skills target directory {target}: {exc}"
68
+ ) from exc
69
+ for skill in skills:
70
+ destination = target / skill.name
71
+ exists = destination.exists()
72
+ status = (
73
+ "would update"
74
+ if dry_run and exists
75
+ else "would install"
76
+ if dry_run
77
+ else "updated"
78
+ if exists
79
+ else "installed"
80
+ )
81
+ if not dry_run:
82
+ _install_one_skill(skill, destination)
83
+ results.append((skill.name, status))
84
+ return results
85
+
86
+
87
+ def _install_one_skill(skill: PackagedSkill, destination: Path) -> None:
88
+ source = skill.source if isinstance(skill.source, Traversable) else Path(skill.source)
89
+ staged = destination.with_name(f"{destination.name}.tmp-{uuid.uuid4().hex}")
90
+ try:
91
+ _copy_skill_tree(source, staged)
92
+ backup = _swap_staged_directory(staged, destination)
93
+ if backup is not None:
94
+ shutil.rmtree(backup, ignore_errors=True)
95
+ except OSError as exc:
96
+ if staged.exists():
97
+ shutil.rmtree(staged, ignore_errors=True)
98
+ raise OperationFailedError(
99
+ f"Could not install skill {skill.name} to {destination}: {exc}"
100
+ ) from exc
101
+
102
+
103
+ def _copy_skill_tree(source: Traversable | Path, destination: Path) -> None:
104
+ destination.mkdir(parents=True, exist_ok=False)
105
+ for child in source.iterdir():
106
+ child_destination = destination / child.name
107
+ if child.is_dir():
108
+ _copy_skill_tree(child, child_destination)
109
+ continue
110
+ with child.open("rb") as handle, child_destination.open("wb") as output:
111
+ shutil.copyfileobj(handle, output)
112
+
113
+
114
+ def _swap_staged_directory(staged: Path, destination: Path) -> Path | None:
115
+ backup: Path | None = None
116
+ if destination.exists():
117
+ backup = destination.with_name(f"{destination.name}.bak-{uuid.uuid4().hex}")
118
+ os.replace(destination, backup)
119
+ try:
120
+ os.replace(staged, destination)
121
+ except OSError:
122
+ if backup is not None and backup.exists() and not destination.exists():
123
+ os.replace(backup, destination)
124
+ raise
125
+ return backup
126
+
127
+
128
+ def run_install_skills_command(args: argparse.Namespace) -> str:
129
+ skills = discover_packaged_skills()
130
+ target = get_skills_target_path(getattr(args, "target", None))
131
+ results = install_skills_from_paths(skills, target, dry_run=bool(args.dry_run))
132
+ mode = "(dry-run) " if args.dry_run else ""
133
+ lines = [f"{mode}Installing skills to {target}:", ""]
134
+ for skill_name, status in results:
135
+ lines.append(f" {skill_name}: {status}")
136
+ lines.append("")
137
+ lines.append(f"Total: {len(results)} skill(s)")
138
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,31 @@
1
+ """Filesystem path discovery command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from argparse import Namespace
6
+
7
+ from browser_cli.constants import get_app_paths
8
+ from browser_cli.outputs.json import render_json_payload
9
+
10
+
11
+ def _paths_payload() -> dict[str, str]:
12
+ app_paths = get_app_paths()
13
+ return {
14
+ "home": str(app_paths.home),
15
+ "tasks_dir": str(app_paths.tasks_dir),
16
+ "automations_dir": str(app_paths.automations_dir),
17
+ "artifacts_dir": str(app_paths.artifacts_dir),
18
+ "daemon_log_path": str(app_paths.daemon_log_path),
19
+ "automation_db_path": str(app_paths.automation_db_path),
20
+ "automation_service_run_info_path": str(app_paths.automation_service_run_info_path),
21
+ "automation_service_log_path": str(app_paths.automation_service_log_path),
22
+ }
23
+
24
+
25
+ def run_paths_command(args: Namespace) -> str:
26
+ payload = _paths_payload()
27
+ if getattr(args, "json", False):
28
+ return render_json_payload({"ok": True, "data": payload, "meta": {"action": "paths"}})
29
+ lines = ["Paths", ""]
30
+ lines.extend(f"{key}: {value}" for key, value in payload.items())
31
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,33 @@
1
+ """Read command handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from argparse import Namespace
7
+
8
+ from browser_cli.outputs.render import render_output
9
+ from browser_cli.task_runtime import BrowserCliTaskClient
10
+
11
+
12
+ def normalize_url(url: str) -> str:
13
+ if "://" in url:
14
+ return url
15
+ return f"https://{url}"
16
+
17
+
18
+ def run_read_command(args: Namespace) -> str:
19
+ client = BrowserCliTaskClient()
20
+ result = client.read(
21
+ normalize_url(args.url),
22
+ output_mode="snapshot" if args.snapshot else "html",
23
+ scroll_bottom=bool(args.scroll_bottom),
24
+ )
25
+ if result.used_fallback_profile and result.fallback_profile_dir:
26
+ message = (
27
+ "Info: primary Chrome profile unavailable; using fallback profile at "
28
+ f"{result.fallback_profile_dir}"
29
+ )
30
+ if result.fallback_reason:
31
+ message += f". Reason: {result.fallback_reason}"
32
+ sys.stderr.write(message + "\n")
33
+ return render_output(result.body)
@@ -0,0 +1,51 @@
1
+ """Lifecycle reload command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from argparse import Namespace
6
+
7
+ from browser_cli.daemon.client import (
8
+ cleanup_runtime,
9
+ ensure_daemon_running,
10
+ send_command,
11
+ wait_for_daemon_stop,
12
+ )
13
+ from browser_cli.errors import BrowserCliError, OperationFailedError
14
+
15
+ from .status import collect_status_report, render_status_report
16
+
17
+
18
+ def run_reload_command(_args: Namespace) -> str:
19
+ graceful_stop = False
20
+ forced_cleanup = False
21
+
22
+ try:
23
+ stop_response = send_command("stop", start_if_needed=False)
24
+ graceful_stop = not bool((stop_response.get("data") or {}).get("already_stopped"))
25
+ except BrowserCliError:
26
+ graceful_stop = False
27
+
28
+ if not wait_for_daemon_stop():
29
+ forced_cleanup = True
30
+
31
+ if cleanup_runtime(fast_kill=True):
32
+ forced_cleanup = True
33
+
34
+ try:
35
+ ensure_daemon_running()
36
+ after = collect_status_report(warmup=False)
37
+ except BrowserCliError as exc:
38
+ raise OperationFailedError(
39
+ "Reload cleared Browser CLI runtime state, but restart failed. "
40
+ f"Reason: {exc}. Run `browser-cli status` and inspect the daemon log for details."
41
+ ) from exc
42
+
43
+ lines = [
44
+ "Reload: complete",
45
+ f"- graceful stop: {'yes' if graceful_stop else 'no'}",
46
+ f"- forced cleanup: {'yes' if forced_cleanup else 'no'}",
47
+ f"- result: {after.overall_status}",
48
+ "",
49
+ render_status_report(after).rstrip(),
50
+ ]
51
+ return "\n".join(lines) + "\n"