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.
- browser_cli/__init__.py +7 -0
- browser_cli/actions/__init__.py +5 -0
- browser_cli/actions/cli_specs.py +868 -0
- browser_cli/agent_scope/__init__.py +14 -0
- browser_cli/automation/__init__.py +11 -0
- browser_cli/automation/api/__init__.py +5 -0
- browser_cli/automation/api/server.py +321 -0
- browser_cli/automation/hooks.py +42 -0
- browser_cli/automation/loader.py +134 -0
- browser_cli/automation/models.py +127 -0
- browser_cli/automation/persistence/__init__.py +5 -0
- browser_cli/automation/persistence/store.py +682 -0
- browser_cli/automation/projections.py +279 -0
- browser_cli/automation/publisher.py +153 -0
- browser_cli/automation/scheduler/__init__.py +5 -0
- browser_cli/automation/scheduler/schedule.py +113 -0
- browser_cli/automation/service/__init__.py +8 -0
- browser_cli/automation/service/__main__.py +64 -0
- browser_cli/automation/service/client.py +232 -0
- browser_cli/automation/service/runtime.py +388 -0
- browser_cli/automation/toml.py +35 -0
- browser_cli/automation/web/__init__.py +5 -0
- browser_cli/automation/web/assets.py +377 -0
- browser_cli/browser/__init__.py +1 -0
- browser_cli/browser/models.py +44 -0
- browser_cli/browser/network_capture.py +392 -0
- browser_cli/browser/service.py +1519 -0
- browser_cli/browser/session.py +158 -0
- browser_cli/browser/snapshot.py +35 -0
- browser_cli/browser/stealth.py +450 -0
- browser_cli/cli/__init__.py +1 -0
- browser_cli/cli/error_hints.py +26 -0
- browser_cli/cli/main.py +298 -0
- browser_cli/commands/__init__.py +1 -0
- browser_cli/commands/action.py +25 -0
- browser_cli/commands/automation.py +304 -0
- browser_cli/commands/doctor.py +336 -0
- browser_cli/commands/install_skills.py +138 -0
- browser_cli/commands/paths.py +31 -0
- browser_cli/commands/read.py +33 -0
- browser_cli/commands/reload.py +51 -0
- browser_cli/commands/status.py +444 -0
- browser_cli/commands/task.py +71 -0
- browser_cli/constants.py +97 -0
- browser_cli/daemon/__init__.py +1 -0
- browser_cli/daemon/__main__.py +34 -0
- browser_cli/daemon/app.py +941 -0
- browser_cli/daemon/browser_service.py +998 -0
- browser_cli/daemon/client.py +280 -0
- browser_cli/daemon/models.py +63 -0
- browser_cli/daemon/runtime_presentation.py +167 -0
- browser_cli/daemon/server.py +103 -0
- browser_cli/daemon/state.py +15 -0
- browser_cli/daemon/transport.py +77 -0
- browser_cli/drivers/__init__.py +6 -0
- browser_cli/drivers/_extension/__init__.py +1 -0
- browser_cli/drivers/_extension/artifact_actions.py +154 -0
- browser_cli/drivers/_extension/dialog_actions.py +35 -0
- browser_cli/drivers/_extension/input_actions.py +63 -0
- browser_cli/drivers/_extension/locator_actions.py +100 -0
- browser_cli/drivers/_extension/observe_actions.py +285 -0
- browser_cli/drivers/_extension/page_actions.py +210 -0
- browser_cli/drivers/_extension/state_actions.py +134 -0
- browser_cli/drivers/base.py +391 -0
- browser_cli/drivers/extension_driver.py +33 -0
- browser_cli/drivers/models.py +21 -0
- browser_cli/drivers/playwright_driver.py +499 -0
- browser_cli/error_codes.py +27 -0
- browser_cli/errors.py +146 -0
- browser_cli/exit_codes.py +9 -0
- browser_cli/extension/__init__.py +32 -0
- browser_cli/extension/protocol.py +222 -0
- browser_cli/extension/session.py +393 -0
- browser_cli/network.py +147 -0
- browser_cli/outputs/__init__.py +1 -0
- browser_cli/outputs/json.py +10 -0
- browser_cli/outputs/render.py +7 -0
- browser_cli/packaged_skills/__init__.py +1 -0
- browser_cli/packaged_skills/browser-cli-converge/SKILL.md +62 -0
- browser_cli/packaged_skills/browser-cli-delivery/SKILL.md +61 -0
- browser_cli/packaged_skills/browser-cli-explore/SKILL.md +77 -0
- browser_cli/profiles/__init__.py +1 -0
- browser_cli/profiles/discovery.py +185 -0
- browser_cli/refs/__init__.py +17 -0
- browser_cli/refs/generator.py +386 -0
- browser_cli/refs/models.py +127 -0
- browser_cli/refs/registry.py +51 -0
- browser_cli/refs/resolver.py +214 -0
- browser_cli/tabs/__init__.py +5 -0
- browser_cli/tabs/registry.py +283 -0
- browser_cli/task_runtime/__init__.py +31 -0
- browser_cli/task_runtime/client.py +107 -0
- browser_cli/task_runtime/entrypoint.py +110 -0
- browser_cli/task_runtime/errors.py +17 -0
- browser_cli/task_runtime/flow.py +171 -0
- browser_cli/task_runtime/models.py +97 -0
- browser_cli/task_runtime/read.py +71 -0
- browser_cli/task_runtime/templates.py +58 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/METADATA +340 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/RECORD +104 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/WHEEL +5 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/entry_points.txt +2 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/licenses/LICENSE +21 -0
- browser_control_and_automation_cli-0.1.1.dev3.dist-info/top_level.txt +1 -0
browser_cli/cli/main.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""CLI entrypoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
from browser_cli import __version__, exit_codes
|
|
10
|
+
from browser_cli.actions import get_action_specs
|
|
11
|
+
from browser_cli.cli.error_hints import next_hint_for_error
|
|
12
|
+
from browser_cli.commands.action import run_action_command
|
|
13
|
+
from browser_cli.commands.automation import run_automation_command
|
|
14
|
+
from browser_cli.commands.doctor import run_doctor_command
|
|
15
|
+
from browser_cli.commands.install_skills import run_install_skills_command
|
|
16
|
+
from browser_cli.commands.paths import run_paths_command
|
|
17
|
+
from browser_cli.commands.read import run_read_command
|
|
18
|
+
from browser_cli.commands.reload import run_reload_command
|
|
19
|
+
from browser_cli.commands.status import run_status_command
|
|
20
|
+
from browser_cli.commands.task import run_task_command
|
|
21
|
+
from browser_cli.errors import BrowserCliError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
25
|
+
parser = argparse.ArgumentParser(prog="browser-cli")
|
|
26
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
27
|
+
|
|
28
|
+
subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
|
|
29
|
+
|
|
30
|
+
read_parser = subparsers.add_parser(
|
|
31
|
+
"read",
|
|
32
|
+
help="Read a rendered page as HTML or snapshot text.",
|
|
33
|
+
description="Open a page, wait for render completion, and capture rendered output.",
|
|
34
|
+
)
|
|
35
|
+
read_parser.add_argument("url", help="Target page URL.")
|
|
36
|
+
read_parser.add_argument(
|
|
37
|
+
"--snapshot",
|
|
38
|
+
action="store_true",
|
|
39
|
+
help="Return snapshot tree output instead of rendered HTML.",
|
|
40
|
+
)
|
|
41
|
+
read_parser.add_argument(
|
|
42
|
+
"--scroll-bottom",
|
|
43
|
+
action="store_true",
|
|
44
|
+
help="Scroll to the bottom before capture to trigger lazy-loaded content.",
|
|
45
|
+
)
|
|
46
|
+
read_parser.set_defaults(handler=run_read_command)
|
|
47
|
+
|
|
48
|
+
doctor_parser = subparsers.add_parser(
|
|
49
|
+
"doctor",
|
|
50
|
+
help="Diagnose whether Browser CLI is ready on this machine.",
|
|
51
|
+
description="Diagnose install, browser, runtime, and service readiness with next-step guidance.",
|
|
52
|
+
)
|
|
53
|
+
doctor_parser.add_argument(
|
|
54
|
+
"--json",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help="Return machine-readable diagnostic results.",
|
|
57
|
+
)
|
|
58
|
+
doctor_parser.set_defaults(handler=run_doctor_command)
|
|
59
|
+
|
|
60
|
+
paths_parser = subparsers.add_parser(
|
|
61
|
+
"paths",
|
|
62
|
+
help="Show Browser CLI runtime paths.",
|
|
63
|
+
description="Show the canonical Browser CLI runtime paths for home, tasks, automations, logs, and artifacts.",
|
|
64
|
+
)
|
|
65
|
+
paths_parser.add_argument(
|
|
66
|
+
"--json",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Return machine-readable path data.",
|
|
69
|
+
)
|
|
70
|
+
paths_parser.set_defaults(handler=run_paths_command)
|
|
71
|
+
|
|
72
|
+
task_parser = subparsers.add_parser(
|
|
73
|
+
"task",
|
|
74
|
+
help="Run or validate a local task directory.",
|
|
75
|
+
description="`task` is local editable source. Run task.py + task.meta.json from a local task directory.",
|
|
76
|
+
)
|
|
77
|
+
task_subparsers = task_parser.add_subparsers(dest="task_subcommand", metavar="TASK_COMMAND")
|
|
78
|
+
|
|
79
|
+
task_run_parser = task_subparsers.add_parser(
|
|
80
|
+
"run",
|
|
81
|
+
help="Run a task directory.",
|
|
82
|
+
description="Load task.py from a local task directory and execute its run() entrypoint.",
|
|
83
|
+
)
|
|
84
|
+
task_run_parser.add_argument("path", help="Path to the task directory.")
|
|
85
|
+
task_run_parser.add_argument(
|
|
86
|
+
"--set",
|
|
87
|
+
dest="set_values",
|
|
88
|
+
action="append",
|
|
89
|
+
default=[],
|
|
90
|
+
help="Override one input as KEY=VALUE. Repeat as needed.",
|
|
91
|
+
)
|
|
92
|
+
task_run_parser.add_argument(
|
|
93
|
+
"--inputs-json",
|
|
94
|
+
help="JSON object with input overrides.",
|
|
95
|
+
)
|
|
96
|
+
task_run_parser.set_defaults(handler=run_task_command)
|
|
97
|
+
|
|
98
|
+
task_validate_parser = task_subparsers.add_parser(
|
|
99
|
+
"validate",
|
|
100
|
+
help="Validate a task directory.",
|
|
101
|
+
description="Validate task.py and task.meta.json in a local task directory.",
|
|
102
|
+
)
|
|
103
|
+
task_validate_parser.add_argument("path", help="Path to the task directory.")
|
|
104
|
+
task_validate_parser.set_defaults(handler=run_task_command)
|
|
105
|
+
|
|
106
|
+
task_examples_parser = task_subparsers.add_parser(
|
|
107
|
+
"examples",
|
|
108
|
+
help="List built-in task examples.",
|
|
109
|
+
description="Show canonical task examples available from the installed package.",
|
|
110
|
+
)
|
|
111
|
+
task_examples_parser.set_defaults(handler=run_task_command)
|
|
112
|
+
|
|
113
|
+
task_template_parser = task_subparsers.add_parser(
|
|
114
|
+
"template",
|
|
115
|
+
help="Print or write a minimal task template.",
|
|
116
|
+
description="Expose canonical task.py, task.meta.json, and automation.toml templates.",
|
|
117
|
+
)
|
|
118
|
+
task_template_parser.add_argument(
|
|
119
|
+
"--print",
|
|
120
|
+
dest="print_template",
|
|
121
|
+
action="store_true",
|
|
122
|
+
help="Print the template files to stdout.",
|
|
123
|
+
)
|
|
124
|
+
task_template_parser.add_argument(
|
|
125
|
+
"--output",
|
|
126
|
+
help="Write the template files into the given directory.",
|
|
127
|
+
)
|
|
128
|
+
task_template_parser.set_defaults(handler=run_task_command)
|
|
129
|
+
|
|
130
|
+
automation_parser = subparsers.add_parser(
|
|
131
|
+
"automation",
|
|
132
|
+
help="Publish and operate versioned automations.",
|
|
133
|
+
description="`automation` is a published immutable snapshot. Publish a task snapshot and manage automation service state.",
|
|
134
|
+
)
|
|
135
|
+
automation_subparsers = automation_parser.add_subparsers(
|
|
136
|
+
dest="automation_subcommand", metavar="AUTOMATION_COMMAND"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
automation_publish_parser = automation_subparsers.add_parser(
|
|
140
|
+
"publish",
|
|
141
|
+
help="Publish one task directory into the automation service.",
|
|
142
|
+
description="Create a published automation from a task directory.",
|
|
143
|
+
)
|
|
144
|
+
automation_publish_parser.add_argument("path", help="Path to the task directory.")
|
|
145
|
+
automation_publish_parser.set_defaults(handler=run_automation_command)
|
|
146
|
+
|
|
147
|
+
automation_list_parser = automation_subparsers.add_parser(
|
|
148
|
+
"list",
|
|
149
|
+
help="List published automations.",
|
|
150
|
+
description="Show persisted automation ids, versions, and latest run summaries.",
|
|
151
|
+
)
|
|
152
|
+
automation_list_parser.set_defaults(handler=run_automation_command)
|
|
153
|
+
|
|
154
|
+
automation_versions_parser = automation_subparsers.add_parser(
|
|
155
|
+
"versions",
|
|
156
|
+
help="List published snapshot versions for one automation.",
|
|
157
|
+
description="Inspect the local snapshot history for a published automation.",
|
|
158
|
+
)
|
|
159
|
+
automation_versions_parser.add_argument("automation_id", help="Persisted automation id.")
|
|
160
|
+
automation_versions_parser.set_defaults(handler=run_automation_command)
|
|
161
|
+
|
|
162
|
+
automation_inspect_parser = automation_subparsers.add_parser(
|
|
163
|
+
"inspect",
|
|
164
|
+
help="Inspect one published automation.",
|
|
165
|
+
description="Combine service metadata, latest run status, and local snapshot version details.",
|
|
166
|
+
)
|
|
167
|
+
automation_inspect_parser.add_argument("automation_id", help="Persisted automation id.")
|
|
168
|
+
automation_inspect_parser.add_argument("--version", type=int, help="Specific snapshot version.")
|
|
169
|
+
automation_inspect_parser.set_defaults(handler=run_automation_command)
|
|
170
|
+
|
|
171
|
+
automation_ui_parser = automation_subparsers.add_parser(
|
|
172
|
+
"ui",
|
|
173
|
+
help="Start the local automation service if needed and print the UI URL.",
|
|
174
|
+
description="Ensure the automation service is running and print the local Web UI URL.",
|
|
175
|
+
)
|
|
176
|
+
automation_ui_parser.set_defaults(handler=run_automation_command)
|
|
177
|
+
|
|
178
|
+
automation_status_parser = automation_subparsers.add_parser(
|
|
179
|
+
"status",
|
|
180
|
+
help="Show local automation-service status.",
|
|
181
|
+
description="Inspect automation-service health without forcing a browser action.",
|
|
182
|
+
)
|
|
183
|
+
automation_status_parser.set_defaults(handler=run_automation_command)
|
|
184
|
+
|
|
185
|
+
automation_stop_parser = automation_subparsers.add_parser(
|
|
186
|
+
"stop",
|
|
187
|
+
help="Stop the local automation service.",
|
|
188
|
+
description="Request automation-service shutdown and clean stale runtime metadata.",
|
|
189
|
+
)
|
|
190
|
+
automation_stop_parser.set_defaults(handler=run_automation_command)
|
|
191
|
+
|
|
192
|
+
automation_import_parser = automation_subparsers.add_parser(
|
|
193
|
+
"import",
|
|
194
|
+
help="Import an automation manifest into the persistent automation service.",
|
|
195
|
+
description="Load automation.toml and publish it into the local automation-service database.",
|
|
196
|
+
)
|
|
197
|
+
automation_import_parser.add_argument("path", help="Path to automation.toml.")
|
|
198
|
+
automation_import_parser.add_argument(
|
|
199
|
+
"--enable",
|
|
200
|
+
action="store_true",
|
|
201
|
+
help="Enable the imported automation immediately.",
|
|
202
|
+
)
|
|
203
|
+
automation_import_parser.set_defaults(handler=run_automation_command)
|
|
204
|
+
|
|
205
|
+
automation_export_parser = automation_subparsers.add_parser(
|
|
206
|
+
"export",
|
|
207
|
+
help="Export one persisted automation to automation.toml.",
|
|
208
|
+
description="Write a persisted automation definition back out as automation.toml.",
|
|
209
|
+
)
|
|
210
|
+
automation_export_parser.add_argument("automation_id", help="Persisted automation id.")
|
|
211
|
+
automation_export_parser.add_argument(
|
|
212
|
+
"--output",
|
|
213
|
+
required=True,
|
|
214
|
+
help="Path to write automation.toml.",
|
|
215
|
+
)
|
|
216
|
+
automation_export_parser.set_defaults(handler=run_automation_command)
|
|
217
|
+
|
|
218
|
+
status_parser = subparsers.add_parser(
|
|
219
|
+
"status",
|
|
220
|
+
help="Show daemon, backend, and workspace runtime status.",
|
|
221
|
+
description="Inspect Browser CLI runtime state and print operational guidance.",
|
|
222
|
+
)
|
|
223
|
+
status_parser.set_defaults(handler=run_status_command)
|
|
224
|
+
|
|
225
|
+
reload_parser = subparsers.add_parser(
|
|
226
|
+
"reload",
|
|
227
|
+
help="Reset Browser CLI runtime and restart the daemon/browser backend.",
|
|
228
|
+
description="Clear Browser CLI runtime state, restart the daemon, and print the refreshed status.",
|
|
229
|
+
)
|
|
230
|
+
reload_parser.set_defaults(handler=run_reload_command)
|
|
231
|
+
|
|
232
|
+
skills_parser = subparsers.add_parser(
|
|
233
|
+
"install-skills",
|
|
234
|
+
help="Install packaged skills for Browser CLI to a skills directory.",
|
|
235
|
+
description="Copy packaged skills for Browser CLI from the installed package to the target skills directory.",
|
|
236
|
+
)
|
|
237
|
+
skills_parser.add_argument(
|
|
238
|
+
"--dry-run",
|
|
239
|
+
action="store_true",
|
|
240
|
+
help="Show what would be installed without making changes.",
|
|
241
|
+
)
|
|
242
|
+
skills_parser.add_argument(
|
|
243
|
+
"--target",
|
|
244
|
+
help="Optional target directory. Defaults to ~/.agents/skills.",
|
|
245
|
+
)
|
|
246
|
+
skills_parser.set_defaults(handler=run_install_skills_command)
|
|
247
|
+
|
|
248
|
+
for spec in get_action_specs():
|
|
249
|
+
parser_name = spec.cli_name or spec.name
|
|
250
|
+
action_parser = subparsers.add_parser(
|
|
251
|
+
parser_name,
|
|
252
|
+
help=spec.help,
|
|
253
|
+
description=spec.description,
|
|
254
|
+
)
|
|
255
|
+
spec.add_arguments(action_parser)
|
|
256
|
+
action_parser.set_defaults(
|
|
257
|
+
handler=run_action_command,
|
|
258
|
+
action_name=spec.name,
|
|
259
|
+
action_request_builder=spec.build_request,
|
|
260
|
+
action_start_if_needed=spec.start_if_needed,
|
|
261
|
+
)
|
|
262
|
+
return parser
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
266
|
+
parser = build_parser()
|
|
267
|
+
try:
|
|
268
|
+
args = parser.parse_args(list(argv) if argv is not None else None)
|
|
269
|
+
except SystemExit as exc:
|
|
270
|
+
code = exc.code if isinstance(exc.code, int) else exit_codes.USAGE_ERROR
|
|
271
|
+
return code
|
|
272
|
+
|
|
273
|
+
if not hasattr(args, "handler"):
|
|
274
|
+
parser.print_help()
|
|
275
|
+
return exit_codes.USAGE_ERROR
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
result = args.handler(args)
|
|
279
|
+
except BrowserCliError as exc:
|
|
280
|
+
sys.stderr.write(f"Error: {exc}\n")
|
|
281
|
+
hint = next_hint_for_error(exc)
|
|
282
|
+
if hint:
|
|
283
|
+
sys.stderr.write(f"Next: {hint}\n")
|
|
284
|
+
return exc.exit_code
|
|
285
|
+
except KeyboardInterrupt:
|
|
286
|
+
sys.stderr.write("Error: interrupted.\n")
|
|
287
|
+
return exit_codes.TEMPORARY_FAILURE
|
|
288
|
+
except Exception as exc: # pragma: no cover - last-resort guard
|
|
289
|
+
sys.stderr.write(f"Error: unexpected failure: {exc}\n")
|
|
290
|
+
return exit_codes.INTERNAL_ERROR
|
|
291
|
+
|
|
292
|
+
if result:
|
|
293
|
+
sys.stdout.write(result)
|
|
294
|
+
return exit_codes.SUCCESS
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == "__main__": # pragma: no cover
|
|
298
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command handlers."""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Generic daemon-backed action command runner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from argparse import Namespace
|
|
6
|
+
|
|
7
|
+
from browser_cli.daemon.client import send_command
|
|
8
|
+
from browser_cli.errors import InvalidInputError
|
|
9
|
+
from browser_cli.outputs.json import render_json_payload
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_action_command(args: Namespace) -> str:
|
|
13
|
+
builder = getattr(args, "action_request_builder", None)
|
|
14
|
+
if builder is None:
|
|
15
|
+
raise InvalidInputError("Internal CLI configuration error: missing action request builder.")
|
|
16
|
+
try:
|
|
17
|
+
request_payload = builder(args)
|
|
18
|
+
except ValueError as exc:
|
|
19
|
+
raise InvalidInputError(str(exc)) from exc
|
|
20
|
+
response = send_command(
|
|
21
|
+
args.action_name,
|
|
22
|
+
request_payload,
|
|
23
|
+
start_if_needed=bool(getattr(args, "action_start_if_needed", True)),
|
|
24
|
+
)
|
|
25
|
+
return render_json_payload(response)
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Automation CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from argparse import Namespace
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from browser_cli.automation import load_automation_manifest, publish_task_dir
|
|
10
|
+
from browser_cli.automation.models import AutomationManifest
|
|
11
|
+
from browser_cli.automation.projections import (
|
|
12
|
+
manifest_to_config_payload,
|
|
13
|
+
manifest_to_persisted_definition,
|
|
14
|
+
payload_to_persisted_definition,
|
|
15
|
+
persisted_definition_to_config_payload,
|
|
16
|
+
)
|
|
17
|
+
from browser_cli.automation.service.client import (
|
|
18
|
+
automation_service_ui_url,
|
|
19
|
+
ensure_automation_service_running,
|
|
20
|
+
read_automation_service_run_info,
|
|
21
|
+
request_automation_service,
|
|
22
|
+
stop_automation_service,
|
|
23
|
+
)
|
|
24
|
+
from browser_cli.constants import get_app_paths
|
|
25
|
+
from browser_cli.errors import InvalidInputError
|
|
26
|
+
from browser_cli.outputs.json import render_json_payload
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run_automation_command(args: Namespace) -> str:
|
|
30
|
+
subcommand = args.automation_subcommand
|
|
31
|
+
if subcommand == "publish":
|
|
32
|
+
source_task_dir = Path(args.path).expanduser().resolve()
|
|
33
|
+
published = publish_task_dir(source_task_dir, app_paths=get_app_paths())
|
|
34
|
+
ensure_automation_service_running()
|
|
35
|
+
payload = request_automation_service(
|
|
36
|
+
"POST",
|
|
37
|
+
"/api/automations/import",
|
|
38
|
+
body={
|
|
39
|
+
"path": str(published.manifest_path),
|
|
40
|
+
"enabled": True,
|
|
41
|
+
},
|
|
42
|
+
start_if_needed=False,
|
|
43
|
+
)
|
|
44
|
+
return render_json_payload(
|
|
45
|
+
{
|
|
46
|
+
"ok": True,
|
|
47
|
+
"data": {
|
|
48
|
+
"published": {
|
|
49
|
+
"automation_id": published.automation_id,
|
|
50
|
+
"automation_name": published.automation_name,
|
|
51
|
+
"version": published.version,
|
|
52
|
+
"manifest_source": published.manifest_source,
|
|
53
|
+
"source_task_dir": str(source_task_dir),
|
|
54
|
+
"snapshot_dir": str(published.snapshot_dir),
|
|
55
|
+
"manifest_path": str(published.manifest_path),
|
|
56
|
+
},
|
|
57
|
+
"service": payload.get("data") or {},
|
|
58
|
+
"next_commands": {
|
|
59
|
+
"inspect": f"browser-cli automation inspect {published.automation_id}",
|
|
60
|
+
"status": "browser-cli automation status",
|
|
61
|
+
"ui": "browser-cli automation ui",
|
|
62
|
+
},
|
|
63
|
+
"model": {
|
|
64
|
+
"task": "local editable source",
|
|
65
|
+
"automation": "published immutable snapshot",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
"meta": {"action": "automation-publish"},
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
if subcommand == "list":
|
|
72
|
+
payload = request_automation_service("GET", "/api/automations", start_if_needed=True)
|
|
73
|
+
return render_json_payload(
|
|
74
|
+
{
|
|
75
|
+
"ok": True,
|
|
76
|
+
"data": {"automations": payload.get("data") or []},
|
|
77
|
+
"meta": {"action": "automation-list"},
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
if subcommand == "versions":
|
|
81
|
+
versions = _load_snapshot_versions(args.automation_id)
|
|
82
|
+
return render_json_payload(
|
|
83
|
+
{
|
|
84
|
+
"ok": True,
|
|
85
|
+
"data": {"automation_id": args.automation_id, "versions": versions},
|
|
86
|
+
"meta": {"action": "automation-versions"},
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
if subcommand == "inspect":
|
|
90
|
+
payload = request_automation_service(
|
|
91
|
+
"GET", f"/api/automations/{args.automation_id}", start_if_needed=True
|
|
92
|
+
)
|
|
93
|
+
versions = _load_snapshot_versions(args.automation_id)
|
|
94
|
+
live_automation_data = dict(payload.get("data") or {})
|
|
95
|
+
live_config = (
|
|
96
|
+
persisted_definition_to_config_payload(
|
|
97
|
+
payload_to_persisted_definition(live_automation_data)
|
|
98
|
+
)
|
|
99
|
+
if live_automation_data
|
|
100
|
+
else None
|
|
101
|
+
)
|
|
102
|
+
selected = (
|
|
103
|
+
_select_snapshot_version(
|
|
104
|
+
args.automation_id,
|
|
105
|
+
versions,
|
|
106
|
+
version=getattr(args, "version", None),
|
|
107
|
+
)
|
|
108
|
+
if getattr(args, "version", None) is not None
|
|
109
|
+
else None
|
|
110
|
+
)
|
|
111
|
+
snapshot_config = (
|
|
112
|
+
selected.get("snapshot_automation")
|
|
113
|
+
if isinstance(selected, dict) and isinstance(selected.get("snapshot_automation"), dict)
|
|
114
|
+
else None
|
|
115
|
+
)
|
|
116
|
+
snapshot_config_error = (
|
|
117
|
+
str(selected.get("snapshot_config_error") or "")
|
|
118
|
+
if isinstance(selected, dict) and selected.get("snapshot_config_error")
|
|
119
|
+
else None
|
|
120
|
+
)
|
|
121
|
+
latest_run = live_automation_data.get("latest_run")
|
|
122
|
+
latest_run_payload = latest_run if isinstance(latest_run, dict) else None
|
|
123
|
+
return render_json_payload(
|
|
124
|
+
{
|
|
125
|
+
"ok": True,
|
|
126
|
+
"data": {
|
|
127
|
+
"snapshot_config": snapshot_config,
|
|
128
|
+
"snapshot_config_error": snapshot_config_error,
|
|
129
|
+
"live_config": live_config,
|
|
130
|
+
"versions": versions,
|
|
131
|
+
"selected_version": selected,
|
|
132
|
+
"latest_run": latest_run_payload,
|
|
133
|
+
"summary": _build_inspect_summary(
|
|
134
|
+
args.automation_id,
|
|
135
|
+
live_automation_data,
|
|
136
|
+
versions,
|
|
137
|
+
selected,
|
|
138
|
+
latest_run_payload,
|
|
139
|
+
),
|
|
140
|
+
},
|
|
141
|
+
"meta": {"action": "automation-inspect"},
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
if subcommand == "import":
|
|
145
|
+
ensure_automation_service_running()
|
|
146
|
+
manifest = load_automation_manifest(args.path)
|
|
147
|
+
automation = manifest_to_persisted_definition(
|
|
148
|
+
manifest, enabled=bool(getattr(args, "enable", False))
|
|
149
|
+
)
|
|
150
|
+
body = persisted_definition_to_config_payload(automation)
|
|
151
|
+
body["enabled"] = automation.enabled
|
|
152
|
+
payload = request_automation_service(
|
|
153
|
+
"POST",
|
|
154
|
+
"/api/automations",
|
|
155
|
+
body=body,
|
|
156
|
+
start_if_needed=False,
|
|
157
|
+
)
|
|
158
|
+
payload["meta"] = {"action": "automation-import"}
|
|
159
|
+
return render_json_payload(payload)
|
|
160
|
+
if subcommand == "export":
|
|
161
|
+
payload = request_automation_service(
|
|
162
|
+
"GET",
|
|
163
|
+
f"/api/automations/{args.automation_id}/export",
|
|
164
|
+
start_if_needed=True,
|
|
165
|
+
)
|
|
166
|
+
output_path = Path(args.output).expanduser().resolve()
|
|
167
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
output_path.write_text(str((payload.get("data") or {}).get("toml") or ""), encoding="utf-8")
|
|
169
|
+
return render_json_payload(
|
|
170
|
+
{
|
|
171
|
+
"ok": True,
|
|
172
|
+
"data": {"automation_id": args.automation_id, "output": str(output_path)},
|
|
173
|
+
"meta": {"action": "automation-export"},
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
if subcommand == "ui":
|
|
177
|
+
return render_json_payload(
|
|
178
|
+
{
|
|
179
|
+
"ok": True,
|
|
180
|
+
"data": {
|
|
181
|
+
"url": automation_service_ui_url(),
|
|
182
|
+
"run_info": read_automation_service_run_info(),
|
|
183
|
+
},
|
|
184
|
+
"meta": {"action": "automation-ui"},
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
if subcommand == "status":
|
|
188
|
+
run_info = read_automation_service_run_info()
|
|
189
|
+
if not run_info:
|
|
190
|
+
return render_json_payload(
|
|
191
|
+
{
|
|
192
|
+
"ok": True,
|
|
193
|
+
"data": {"running": False},
|
|
194
|
+
"meta": {"action": "automation-status"},
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
payload = request_automation_service("GET", "/api/service/status", start_if_needed=False)
|
|
198
|
+
payload["meta"] = {"action": "automation-status"}
|
|
199
|
+
return render_json_payload(payload)
|
|
200
|
+
if subcommand == "stop":
|
|
201
|
+
return render_json_payload(
|
|
202
|
+
{
|
|
203
|
+
"ok": True,
|
|
204
|
+
"data": stop_automation_service(),
|
|
205
|
+
"meta": {"action": "automation-stop"},
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
raise ValueError(f"Unsupported automation subcommand: {subcommand}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _load_snapshot_versions(automation_id: str) -> list[dict[str, object]]:
|
|
212
|
+
versions_dir = get_app_paths().automations_dir / automation_id / "versions"
|
|
213
|
+
if not versions_dir.exists():
|
|
214
|
+
return []
|
|
215
|
+
versions: list[dict[str, object]] = []
|
|
216
|
+
for entry in sorted(versions_dir.iterdir(), key=_version_sort_key, reverse=True):
|
|
217
|
+
if not entry.is_dir() or not entry.name.isdigit():
|
|
218
|
+
continue
|
|
219
|
+
publish_path = entry / "publish.json"
|
|
220
|
+
publish_data = _read_json_file(publish_path)
|
|
221
|
+
snapshot_manifest, snapshot_config_error = _load_snapshot_manifest(
|
|
222
|
+
entry / "automation.toml"
|
|
223
|
+
)
|
|
224
|
+
versions.append(
|
|
225
|
+
{
|
|
226
|
+
"version": int(entry.name),
|
|
227
|
+
"snapshot_dir": str(entry),
|
|
228
|
+
"publish": publish_data,
|
|
229
|
+
"snapshot_automation": (
|
|
230
|
+
_snapshot_manifest_to_automation_payload(snapshot_manifest)
|
|
231
|
+
if snapshot_manifest is not None
|
|
232
|
+
else None
|
|
233
|
+
),
|
|
234
|
+
"snapshot_config_error": snapshot_config_error,
|
|
235
|
+
"snapshot_latest_run": publish_data.get("latest_run")
|
|
236
|
+
if isinstance(publish_data.get("latest_run"), dict)
|
|
237
|
+
else None,
|
|
238
|
+
"task_path": str(entry / "task.py"),
|
|
239
|
+
"task_meta_path": str(entry / "task.meta.json"),
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
return versions
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _select_snapshot_version(
|
|
246
|
+
automation_id: str,
|
|
247
|
+
versions: list[dict[str, object]],
|
|
248
|
+
*,
|
|
249
|
+
version: int | None,
|
|
250
|
+
) -> dict[str, object] | None:
|
|
251
|
+
if version is None:
|
|
252
|
+
return versions[0] if versions else None
|
|
253
|
+
for item in versions:
|
|
254
|
+
if item["version"] == version:
|
|
255
|
+
return item
|
|
256
|
+
raise InvalidInputError(f"Automation version not found for {automation_id}: {version}")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _version_sort_key(path: Path) -> int:
|
|
260
|
+
try:
|
|
261
|
+
return int(path.name)
|
|
262
|
+
except ValueError:
|
|
263
|
+
return -1
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _read_json_file(path: Path) -> dict[str, object]:
|
|
267
|
+
if not path.exists():
|
|
268
|
+
return {}
|
|
269
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _build_inspect_summary(
|
|
273
|
+
automation_id: str,
|
|
274
|
+
automation_data: dict[str, object],
|
|
275
|
+
versions: list[dict[str, object]],
|
|
276
|
+
selected: dict[str, object] | None,
|
|
277
|
+
latest_run: dict[str, object] | None,
|
|
278
|
+
) -> dict[str, object]:
|
|
279
|
+
latest_run_status = None
|
|
280
|
+
if isinstance(latest_run, dict):
|
|
281
|
+
latest_run_status = latest_run.get("status")
|
|
282
|
+
return {
|
|
283
|
+
"automation_id": automation_id,
|
|
284
|
+
"persisted_version": automation_data.get("version"),
|
|
285
|
+
"available_versions": [item["version"] for item in versions],
|
|
286
|
+
"selected_version": selected.get("version") if selected else None,
|
|
287
|
+
"selected_snapshot_dir": selected.get("snapshot_dir") if selected else None,
|
|
288
|
+
"selected_task_path": selected.get("task_path") if selected else None,
|
|
289
|
+
"schedule_mode": automation_data.get("schedule_kind"),
|
|
290
|
+
"latest_run_status": latest_run_status,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _load_snapshot_manifest(path: Path) -> tuple[AutomationManifest | None, str | None]:
|
|
295
|
+
if not path.exists():
|
|
296
|
+
return None, None
|
|
297
|
+
try:
|
|
298
|
+
return load_automation_manifest(path), None
|
|
299
|
+
except InvalidInputError as exc:
|
|
300
|
+
return None, str(exc)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _snapshot_manifest_to_automation_payload(manifest: AutomationManifest) -> dict[str, object]:
|
|
304
|
+
return manifest_to_config_payload(manifest)
|