@adgine/geo-cli 1.2.8

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 (115) hide show
  1. package/README.md +93 -0
  2. package/bin/adgine-geo.js +224 -0
  3. package/package.json +38 -0
  4. package/skills/.env.example +11 -0
  5. package/skills/README.md +247 -0
  6. package/skills/SKILL.md +117 -0
  7. package/skills/VERSION +1 -0
  8. package/skills/adgine-geo-aiagent/SKILL.md +200 -0
  9. package/skills/adgine-geo-aiagent/scripts/_client.py +341 -0
  10. package/skills/adgine-geo-aiagent/scripts/bot_traffic.py +278 -0
  11. package/skills/adgine-geo-aiagent/scripts/human_traffic.py +357 -0
  12. package/skills/adgine-geo-aiagent/scripts/page_analytics.py +317 -0
  13. package/skills/adgine-geo-aiagent/scripts/page_detail.py +273 -0
  14. package/skills/adgine-geo-analytics/API.md +89 -0
  15. package/skills/adgine-geo-analytics/SKILL.md +191 -0
  16. package/skills/adgine-geo-analytics/scripts/_client.py +341 -0
  17. package/skills/adgine-geo-analytics/scripts/get_overview.py +129 -0
  18. package/skills/adgine-geo-billing/SKILL.md +118 -0
  19. package/skills/adgine-geo-billing/scripts/_client.py +341 -0
  20. package/skills/adgine-geo-billing/scripts/get_credits.py +51 -0
  21. package/skills/adgine-geo-billing/scripts/get_credits_pricing.py +52 -0
  22. package/skills/adgine-geo-billing/scripts/get_subscription.py +48 -0
  23. package/skills/adgine-geo-billing/scripts/list_plans.py +66 -0
  24. package/skills/adgine-geo-brand/SKILL.md +193 -0
  25. package/skills/adgine-geo-brand/WORKFLOW.md +66 -0
  26. package/skills/adgine-geo-brand/scripts/_client.py +341 -0
  27. package/skills/adgine-geo-brand/scripts/generate_brand.py +95 -0
  28. package/skills/adgine-geo-brand/scripts/get_brand.py +67 -0
  29. package/skills/adgine-geo-brand/scripts/list_jobs.py +153 -0
  30. package/skills/adgine-geo-brand/scripts/update_brand.py +61 -0
  31. package/skills/adgine-geo-citation/SKILL.md +220 -0
  32. package/skills/adgine-geo-citation/scripts/_client.py +341 -0
  33. package/skills/adgine-geo-citation/scripts/create_tests.py +62 -0
  34. package/skills/adgine-geo-citation/scripts/get_aggregate.py +108 -0
  35. package/skills/adgine-geo-citation/scripts/get_results.py +179 -0
  36. package/skills/adgine-geo-content/SKILL.md +224 -0
  37. package/skills/adgine-geo-content/WORKFLOW.md +100 -0
  38. package/skills/adgine-geo-content/scripts/_client.py +341 -0
  39. package/skills/adgine-geo-content/scripts/generate_article.py +108 -0
  40. package/skills/adgine-geo-content/scripts/generate_outline.py +113 -0
  41. package/skills/adgine-geo-content/scripts/generate_titles.py +51 -0
  42. package/skills/adgine-geo-content/scripts/list_content.py +65 -0
  43. package/skills/adgine-geo-content/scripts/manage_content.py +90 -0
  44. package/skills/adgine-geo-content/scripts/manage_jobs.py +184 -0
  45. package/skills/adgine-geo-dashboard/SKILL.md +181 -0
  46. package/skills/adgine-geo-dashboard/scripts/_client.py +341 -0
  47. package/skills/adgine-geo-dashboard/scripts/check_integrations.py +139 -0
  48. package/skills/adgine-geo-dashboard/scripts/get_overview.py +155 -0
  49. package/skills/adgine-geo-dashboard/scripts/get_visibility_overview.py +93 -0
  50. package/skills/adgine-geo-docs/SKILL.md +60 -0
  51. package/skills/adgine-geo-domains/SKILL.md +223 -0
  52. package/skills/adgine-geo-domains/scripts/_client.py +341 -0
  53. package/skills/adgine-geo-domains/scripts/get_domain.py +75 -0
  54. package/skills/adgine-geo-domains/scripts/list_domains.py +66 -0
  55. package/skills/adgine-geo-domains/scripts/search_domains.py +85 -0
  56. package/skills/adgine-geo-integrations/SKILL.md +188 -0
  57. package/skills/adgine-geo-integrations/scripts/_client.py +341 -0
  58. package/skills/adgine-geo-integrations/scripts/cloudflare_connect.py +155 -0
  59. package/skills/adgine-geo-integrations/scripts/cloudflare_worker.py +212 -0
  60. package/skills/adgine-geo-integrations/scripts/ga4_data.py +192 -0
  61. package/skills/adgine-geo-integrations/scripts/ga4_setup.py +120 -0
  62. package/skills/adgine-geo-opportunities/SKILL.md +162 -0
  63. package/skills/adgine-geo-opportunities/scripts/_client.py +341 -0
  64. package/skills/adgine-geo-opportunities/scripts/get_opportunity.py +90 -0
  65. package/skills/adgine-geo-opportunities/scripts/list_opportunities.py +92 -0
  66. package/skills/adgine-geo-performance/SKILL.md +177 -0
  67. package/skills/adgine-geo-performance/scripts/_client.py +341 -0
  68. package/skills/adgine-geo-performance/scripts/analyze_page.py +100 -0
  69. package/skills/adgine-geo-projects/SKILL.md +279 -0
  70. package/skills/adgine-geo-projects/scripts/_client.py +341 -0
  71. package/skills/adgine-geo-projects/scripts/check_auth.py +45 -0
  72. package/skills/adgine-geo-projects/scripts/list_projects.py +47 -0
  73. package/skills/adgine-geo-projects/scripts/manage_competitors.py +122 -0
  74. package/skills/adgine-geo-projects/scripts/manage_project.py +88 -0
  75. package/skills/adgine-geo-saas/SKILL.md +144 -0
  76. package/skills/adgine-geo-saas/scripts/_client.py +341 -0
  77. package/skills/adgine-geo-saas/scripts/check_domain.py +43 -0
  78. package/skills/adgine-geo-saas/scripts/create_website.py +56 -0
  79. package/skills/adgine-geo-saas/scripts/get_task.py +94 -0
  80. package/skills/adgine-geo-site-audit/README.md +281 -0
  81. package/skills/adgine-geo-site-audit/SKILL.md +449 -0
  82. package/skills/adgine-geo-site-audit/requirements.txt +5 -0
  83. package/skills/adgine-geo-site-audit/scripts/_client.py +341 -0
  84. package/skills/adgine-geo-site-audit/scripts/_geo_notice.py +55 -0
  85. package/skills/adgine-geo-site-audit/scripts/export_skill_package.py +197 -0
  86. package/skills/adgine-geo-site-audit/scripts/geo_collect.py +2443 -0
  87. package/skills/adgine-geo-site-audit/scripts/geo_score.py +490 -0
  88. package/skills/adgine-geo-site-audit/scripts/geo_timing.py +399 -0
  89. package/skills/adgine-geo-site-audit/scripts/geo_visibility.py +718 -0
  90. package/skills/adgine-geo-site-audit/scripts/render_report_pdf.py +1502 -0
  91. package/skills/adgine-geo-topics/SKILL.md +232 -0
  92. package/skills/adgine-geo-topics/scripts/_client.py +341 -0
  93. package/skills/adgine-geo-topics/scripts/generate_prompts.py +140 -0
  94. package/skills/adgine-geo-topics/scripts/manage_prompts.py +153 -0
  95. package/skills/adgine-geo-topics/scripts/manage_topics.py +145 -0
  96. package/skills/adgine-geo-visibility/SKILL.md +168 -0
  97. package/skills/adgine-geo-visibility/scripts/_client.py +341 -0
  98. package/skills/adgine-geo-visibility/scripts/get_execution.py +151 -0
  99. package/skills/adgine-geo-visibility/scripts/get_matrix.py +111 -0
  100. package/skills/adgine-geo-visibility/scripts/get_prompt_metrics.py +104 -0
  101. package/skills/adgine-geo-visibility/scripts/get_topic_metrics.py +201 -0
  102. package/skills/adgine-geo-visibility/scripts/get_visibility.py +166 -0
  103. package/skills/adgine-geo-wordpress/SKILL.md +151 -0
  104. package/skills/adgine-geo-wordpress/scripts/_client.py +341 -0
  105. package/skills/adgine-geo-wordpress/scripts/list_publishable.py +62 -0
  106. package/skills/adgine-geo-wordpress/scripts/manage_credentials.py +139 -0
  107. package/skills/adgine-geo-wordpress/scripts/manage_publishes.py +108 -0
  108. package/skills/adgine-geo-wordpress/scripts/publish.py +79 -0
  109. package/skills/scripts/check_version.py +290 -0
  110. package/skills/setup.py +248 -0
  111. package/src/config.js +126 -0
  112. package/src/doctor.js +137 -0
  113. package/src/install.js +90 -0
  114. package/src/manifest.js +99 -0
  115. package/src/runner.js +159 -0
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env python3
2
+ """Summarize GEO audit timings without slowing down the normal workflow.
3
+
4
+ Normal audits should use the `artifacts` command once at the end. It reads the
5
+ timings already written by the collector/scorer/visibility/PDF scripts and does
6
+ not add start/stop marker overhead to the workflow.
7
+
8
+ The legacy `start`/`mark` ledger commands are kept for deep debugging only.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import time
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+ from typing import Any, Optional
18
+
19
+
20
+ def _now() -> tuple[float, str]:
21
+ timestamp = time.time()
22
+ iso = datetime.fromtimestamp(timestamp, timezone.utc).isoformat()
23
+ return timestamp, iso
24
+
25
+
26
+ def _read_json(path: Path) -> dict[str, Any]:
27
+ return json.loads(path.read_text(encoding="utf-8"))
28
+
29
+
30
+ def _write_json(path: Path, data: dict[str, Any]) -> None:
31
+ path.parent.mkdir(parents=True, exist_ok=True)
32
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
33
+
34
+
35
+ def create_ledger(path: Path, *, label: str = "", metadata: Optional[dict[str, Any]] = None) -> dict[str, Any]:
36
+ timestamp, iso = _now()
37
+ ledger = {
38
+ "label": label,
39
+ "metadata": metadata or {},
40
+ "started_at": iso,
41
+ "events": [
42
+ {"name": "workflow_start", "timestamp": timestamp, "iso": iso, "metadata": {}}
43
+ ],
44
+ "attachments": {},
45
+ }
46
+ _write_json(path, ledger)
47
+ return ledger
48
+
49
+
50
+ def mark_event(path: Path, name: str, *, metadata: Optional[dict[str, Any]] = None) -> dict[str, Any]:
51
+ ledger = _read_json(path)
52
+ timestamp, iso = _now()
53
+ ledger.setdefault("events", []).append({
54
+ "name": name,
55
+ "timestamp": timestamp,
56
+ "iso": iso,
57
+ "metadata": metadata or {},
58
+ })
59
+ _write_json(path, ledger)
60
+ return ledger
61
+
62
+
63
+ def _extract_timings(payload: dict[str, Any]) -> dict[str, Any]:
64
+ meta = payload.get("meta")
65
+ if isinstance(meta, dict) and isinstance(meta.get("timings"), dict):
66
+ return {
67
+ "source": "meta.timings",
68
+ "timings": meta["timings"],
69
+ "total_seconds": meta.get("report_generation_elapsed_seconds") or meta["timings"].get("total_collection_seconds"),
70
+ }
71
+ if isinstance(payload.get("timings"), dict):
72
+ return {
73
+ "source": "timings",
74
+ "timings": payload["timings"],
75
+ "total_seconds": _first_total(payload["timings"]),
76
+ }
77
+ return {"source": "", "timings": {}, "total_seconds": None}
78
+
79
+
80
+ def _first_total(timings: dict[str, Any]) -> Optional[float]:
81
+ for key in (
82
+ "total_collection_seconds",
83
+ "total_score_module_seconds",
84
+ "total_visibility_prepare_seconds",
85
+ "total_visibility_score_seconds",
86
+ "total_pdf_module_seconds",
87
+ ):
88
+ if key in timings:
89
+ try:
90
+ return float(timings[key])
91
+ except (TypeError, ValueError):
92
+ return None
93
+ return None
94
+
95
+
96
+ def attach_json(path: Path, name: str, json_path: Path) -> dict[str, Any]:
97
+ ledger = _read_json(path)
98
+ payload = _read_json(json_path)
99
+ attachment = {
100
+ "path": str(json_path.expanduser().absolute()),
101
+ }
102
+ attachment.update(_extract_timings(payload))
103
+ ledger.setdefault("attachments", {})[name] = attachment
104
+ _write_json(path, ledger)
105
+ return ledger
106
+
107
+
108
+ def artifact_attachment(name: str, json_path: Optional[Path]) -> Optional[dict[str, Any]]:
109
+ if not json_path:
110
+ return None
111
+ path = json_path.expanduser().absolute()
112
+ if not path.exists():
113
+ return {
114
+ "path": str(path),
115
+ "missing": True,
116
+ "source": "",
117
+ "timings": {},
118
+ "total_seconds": None,
119
+ }
120
+ payload = _read_json(path)
121
+ attachment = {
122
+ "path": str(path),
123
+ "missing": False,
124
+ "modified_at": datetime.fromtimestamp(path.stat().st_mtime, timezone.utc).isoformat(),
125
+ }
126
+ attachment.update(_extract_timings(payload))
127
+ return attachment
128
+
129
+
130
+ def summarize_artifacts(
131
+ *,
132
+ label: str = "",
133
+ collect_json: Optional[Path] = None,
134
+ score_json: Optional[Path] = None,
135
+ visibility_plan_json: Optional[Path] = None,
136
+ visibility_results_json: Optional[Path] = None,
137
+ pdf_timing_json: Optional[Path] = None,
138
+ ui_elapsed_seconds: Optional[float] = None,
139
+ ) -> dict[str, Any]:
140
+ attachments: dict[str, Any] = {}
141
+ for name, path in (
142
+ ("collect", collect_json),
143
+ ("score", score_json),
144
+ ("visibility_prepare", visibility_plan_json),
145
+ ("visibility_score", visibility_results_json),
146
+ ("pdf", pdf_timing_json),
147
+ ):
148
+ attachment = artifact_attachment(name, path)
149
+ if attachment:
150
+ attachments[name] = attachment
151
+
152
+ known_script_seconds = round(
153
+ sum(
154
+ float(item["total_seconds"])
155
+ for item in attachments.values()
156
+ if isinstance(item.get("total_seconds"), (int, float))
157
+ ),
158
+ 3,
159
+ )
160
+ untracked_agent_overhead_seconds = None
161
+ if ui_elapsed_seconds is not None:
162
+ untracked_agent_overhead_seconds = round(float(ui_elapsed_seconds) - known_script_seconds, 3)
163
+
164
+ artifact_timeline = sorted(
165
+ [
166
+ {
167
+ "name": name,
168
+ "modified_at": attachment["modified_at"],
169
+ "path": attachment["path"],
170
+ }
171
+ for name, attachment in attachments.items()
172
+ if attachment.get("modified_at")
173
+ ],
174
+ key=lambda item: item["modified_at"],
175
+ )
176
+
177
+ return {
178
+ "label": label,
179
+ "measurement_mode": "artifact_summary",
180
+ "note": (
181
+ "Low-overhead timing summary. It reads existing artifact timings once at the end. "
182
+ "It does not measure full UI/agent elapsed time unless ui_elapsed_seconds is provided."
183
+ ),
184
+ "total_workflow_seconds": ui_elapsed_seconds,
185
+ "known_script_seconds": known_script_seconds,
186
+ "untracked_agent_overhead_seconds": untracked_agent_overhead_seconds,
187
+ "artifact_timeline": artifact_timeline,
188
+ "attachments": attachments,
189
+ }
190
+
191
+
192
+ def summarize_ledger(ledger: dict[str, Any]) -> dict[str, Any]:
193
+ events = ledger.get("events", [])
194
+ spans: list[dict[str, Any]] = []
195
+ for previous, current in zip(events, events[1:]):
196
+ elapsed = float(current["timestamp"]) - float(previous["timestamp"])
197
+ spans.append({
198
+ "from": previous["name"],
199
+ "to": current["name"],
200
+ "seconds": round(elapsed, 3),
201
+ })
202
+
203
+ named_starts: dict[str, dict[str, Any]] = {}
204
+ named_spans: list[dict[str, Any]] = []
205
+ for event in events:
206
+ name = str(event.get("name", ""))
207
+ if name.endswith("_start"):
208
+ named_starts[name[:-6]] = event
209
+ elif name.endswith("_end"):
210
+ key = name[:-4]
211
+ start = named_starts.get(key)
212
+ if start:
213
+ named_spans.append({
214
+ "name": key,
215
+ "seconds": round(float(event["timestamp"]) - float(start["timestamp"]), 3),
216
+ "start": start.get("iso", ""),
217
+ "end": event.get("iso", ""),
218
+ })
219
+
220
+ total_seconds = None
221
+ if events:
222
+ total_seconds = round(float(events[-1]["timestamp"]) - float(events[0]["timestamp"]), 3)
223
+
224
+ attachments = ledger.get("attachments", {})
225
+ known_script_seconds = round(
226
+ sum(
227
+ float(item["total_seconds"])
228
+ for item in attachments.values()
229
+ if isinstance(item.get("total_seconds"), (int, float))
230
+ ),
231
+ 3,
232
+ )
233
+
234
+ return {
235
+ "label": ledger.get("label", ""),
236
+ "metadata": ledger.get("metadata", {}),
237
+ "started_at": ledger.get("started_at", ""),
238
+ "total_workflow_seconds": total_seconds,
239
+ "known_script_seconds": known_script_seconds,
240
+ "named_spans": named_spans,
241
+ "event_spans": spans,
242
+ "attachments": attachments,
243
+ }
244
+
245
+
246
+ def format_markdown_summary(summary: dict[str, Any]) -> str:
247
+ if summary.get("measurement_mode") == "artifact_summary":
248
+ lines = [
249
+ f"# GEO Timing Debug: {summary.get('label') or 'workflow'}",
250
+ "",
251
+ summary.get("note", ""),
252
+ "",
253
+ f"**Workflow total**: {summary.get('total_workflow_seconds') if summary.get('total_workflow_seconds') is not None else 'not measured'}",
254
+ f"**Known script-internal seconds**: {summary.get('known_script_seconds', '')}s",
255
+ ]
256
+ if summary.get("untracked_agent_overhead_seconds") is not None:
257
+ lines.append(f"**Untracked agent/UI overhead**: {summary['untracked_agent_overhead_seconds']}s")
258
+
259
+ timeline = summary.get("artifact_timeline", [])
260
+ if timeline:
261
+ lines.extend(["", "## Artifact Timeline", "", "| Artifact | Modified At | Path |", "|---|---|---|"])
262
+ for item in timeline:
263
+ lines.append(f"| {item['name']} | {item['modified_at']} | `{item['path']}` |")
264
+
265
+ attachments = summary.get("attachments", {})
266
+ if attachments:
267
+ lines.extend([
268
+ "",
269
+ "## Script-Internal Timings",
270
+ "",
271
+ "These values come from each script's own timing block. They are not end-to-end phase durations.",
272
+ "",
273
+ "| Attachment | Source | Internal Total | Path |",
274
+ "|---|---|---:|---|",
275
+ ])
276
+ for name, attachment in attachments.items():
277
+ total = attachment.get("total_seconds")
278
+ total_text = f"{float(total):.3f}s" if isinstance(total, (int, float)) else ""
279
+ lines.append(
280
+ f"| {name} | {attachment.get('source', '')} | {total_text} | `{attachment.get('path', '')}` |"
281
+ )
282
+ return "\n".join(lines) + "\n"
283
+
284
+ lines = [
285
+ f"# GEO Timing Debug: {summary.get('label') or 'workflow'}",
286
+ "",
287
+ f"**Started at**: {summary.get('started_at', '')}",
288
+ f"**Workflow total**: {summary.get('total_workflow_seconds')}s",
289
+ f"**Known script seconds**: {summary.get('known_script_seconds', '')}s",
290
+ "",
291
+ "## Named Spans",
292
+ "",
293
+ "| Step | Seconds |",
294
+ "|---|---:|",
295
+ ]
296
+ for span in summary.get("named_spans", []):
297
+ lines.append(f"| {span['name']} | {span['seconds']:.3f}s |")
298
+
299
+ attachments = summary.get("attachments", {})
300
+ if attachments:
301
+ lines.extend(["", "## Attached Script Timings", "", "| Attachment | Source | Total | Path |", "|---|---|---:|---|"])
302
+ for name, attachment in attachments.items():
303
+ total = attachment.get("total_seconds")
304
+ total_text = f"{float(total):.3f}s" if isinstance(total, (int, float)) else ""
305
+ lines.append(
306
+ f"| {name} | {attachment.get('source', '')} | {total_text} | `{attachment.get('path', '')}` |"
307
+ )
308
+ return "\n".join(lines) + "\n"
309
+
310
+
311
+ def main(argv: Optional[list[str]] = None) -> int:
312
+ parser = argparse.ArgumentParser(description="Track and summarize GEO audit workflow timings.")
313
+ subparsers = parser.add_subparsers(dest="command", required=True)
314
+
315
+ start = subparsers.add_parser("start", help="Create a new timing ledger")
316
+ start.add_argument("ledger_json")
317
+ start.add_argument("--label", default="")
318
+ start.add_argument("--metadata-json", default="{}")
319
+
320
+ mark = subparsers.add_parser("mark", help="Append a timing event")
321
+ mark.add_argument("ledger_json")
322
+ mark.add_argument("name")
323
+ mark.add_argument("--metadata-json", default="{}")
324
+
325
+ attach = subparsers.add_parser("attach", help="Attach a JSON artifact and extract its timings")
326
+ attach.add_argument("ledger_json")
327
+ attach.add_argument("name")
328
+ attach.add_argument("json_path")
329
+
330
+ summary = subparsers.add_parser("summary", help="Summarize a timing ledger")
331
+ summary.add_argument("ledger_json")
332
+ summary.add_argument("--output", "-o")
333
+ summary.add_argument("--report")
334
+
335
+ artifacts = subparsers.add_parser("artifacts", help="Build a low-overhead timing summary from existing artifact JSON files")
336
+ artifacts.add_argument("--label", default="")
337
+ artifacts.add_argument("--collect-json")
338
+ artifacts.add_argument("--score-json")
339
+ artifacts.add_argument("--visibility-plan-json")
340
+ artifacts.add_argument("--visibility-results-json")
341
+ artifacts.add_argument("--pdf-timing-json")
342
+ artifacts.add_argument("--ui-elapsed-seconds", type=float, help="Optional UI-reported elapsed seconds, if available")
343
+ artifacts.add_argument("--output", "-o")
344
+ artifacts.add_argument("--report")
345
+
346
+ args = parser.parse_args(argv)
347
+
348
+ if args.command == "start":
349
+ metadata = json.loads(args.metadata_json)
350
+ create_ledger(Path(args.ledger_json), label=args.label, metadata=metadata)
351
+ return 0
352
+
353
+ if args.command == "mark":
354
+ metadata = json.loads(args.metadata_json)
355
+ mark_event(Path(args.ledger_json), args.name, metadata=metadata)
356
+ return 0
357
+
358
+ if args.command == "attach":
359
+ attach_json(Path(args.ledger_json), args.name, Path(args.json_path))
360
+ return 0
361
+
362
+ if args.command == "summary":
363
+ result = summarize_ledger(_read_json(Path(args.ledger_json)))
364
+ if args.output:
365
+ _write_json(Path(args.output), result)
366
+ else:
367
+ print(json.dumps(result, ensure_ascii=False, indent=2))
368
+ if args.report:
369
+ Path(args.report).expanduser().absolute().write_text(format_markdown_summary(result), encoding="utf-8")
370
+ return 0
371
+
372
+ if args.command == "artifacts":
373
+ result = summarize_artifacts(
374
+ label=args.label,
375
+ collect_json=Path(args.collect_json) if args.collect_json else None,
376
+ score_json=Path(args.score_json) if args.score_json else None,
377
+ visibility_plan_json=Path(args.visibility_plan_json) if args.visibility_plan_json else None,
378
+ visibility_results_json=Path(args.visibility_results_json) if args.visibility_results_json else None,
379
+ pdf_timing_json=Path(args.pdf_timing_json) if args.pdf_timing_json else None,
380
+ ui_elapsed_seconds=args.ui_elapsed_seconds,
381
+ )
382
+ if args.output:
383
+ _write_json(Path(args.output), result)
384
+ else:
385
+ print(json.dumps(result, ensure_ascii=False, indent=2))
386
+ if args.report:
387
+ Path(args.report).expanduser().absolute().write_text(format_markdown_summary(result), encoding="utf-8")
388
+ return 0
389
+
390
+ return 1
391
+
392
+
393
+ if __name__ == "__main__":
394
+ try:
395
+ import _geo_notice
396
+ _geo_notice.emit()
397
+ except Exception:
398
+ pass
399
+ raise SystemExit(main())