bidi2pdf 0.1.15 → 0.1.16

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +20 -0
  3. data/CHANGELOG.md +22 -2
  4. data/README.md +196 -11
  5. data/docker/Dockerfile +4 -0
  6. data/docker/Dockerfile.slim +5 -1
  7. data/lib/bidi2pdf/cli/json_output.rb +30 -0
  8. data/lib/bidi2pdf/cli.rb +559 -17
  9. data/lib/bidi2pdf/diagnose.rb +119 -0
  10. data/lib/bidi2pdf/error_codes.rb +59 -0
  11. data/lib/bidi2pdf/exit_codes.rb +44 -0
  12. data/lib/bidi2pdf/launcher.rb +23 -0
  13. data/lib/bidi2pdf/manifest.rb +65 -0
  14. data/lib/bidi2pdf/notifications/json_subscriber.rb +78 -0
  15. data/lib/bidi2pdf/pdf_inspection.rb +62 -0
  16. data/lib/bidi2pdf/recipe/loader.rb +44 -0
  17. data/lib/bidi2pdf/recipe/runner.rb +229 -0
  18. data/lib/bidi2pdf/recipe/schema_shape.rb +228 -0
  19. data/lib/bidi2pdf/recipe/validator.rb +138 -0
  20. data/lib/bidi2pdf/recipe.rb +83 -0
  21. data/lib/bidi2pdf/result.rb +67 -0
  22. data/lib/bidi2pdf/result_collector.rb +150 -0
  23. data/lib/bidi2pdf/schema.rb +382 -0
  24. data/lib/bidi2pdf/session_runner.rb +42 -0
  25. data/lib/bidi2pdf/version.rb +1 -1
  26. data/lib/bidi2pdf.rb +90 -8
  27. data/sig/bidi2pdf/cli/json_output.rbs +19 -0
  28. data/sig/bidi2pdf/cli.rbs +112 -2
  29. data/sig/bidi2pdf/diagnose.rbs +30 -0
  30. data/sig/bidi2pdf/error_codes.rbs +21 -0
  31. data/sig/bidi2pdf/exit_codes.rbs +21 -0
  32. data/sig/bidi2pdf/launcher.rbs +9 -0
  33. data/sig/bidi2pdf/manifest.rbs +38 -0
  34. data/sig/bidi2pdf/notifications/json_subscriber.rbs +45 -0
  35. data/sig/bidi2pdf/pdf_inspection.rbs +33 -0
  36. data/sig/bidi2pdf/recipe/loader.rbs +20 -0
  37. data/sig/bidi2pdf/recipe/runner.rbs +92 -0
  38. data/sig/bidi2pdf/recipe/schema_shape.rbs +85 -0
  39. data/sig/bidi2pdf/recipe/validator.rbs +54 -0
  40. data/sig/bidi2pdf/recipe.rbs +72 -0
  41. data/sig/bidi2pdf/result.rbs +71 -0
  42. data/sig/bidi2pdf/result_collector.rbs +106 -0
  43. data/sig/bidi2pdf/schema.rbs +45 -0
  44. data/sig/bidi2pdf/session_runner.rbs +17 -0
  45. data/sig/bidi2pdf/version.rbs +1 -1
  46. data/sig/bidi2pdf.rbs +66 -0
  47. metadata +31 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36afd0d1f8c64ff6d72a8051785d3f65a7efcb336c07e1a6756b772ff4f17414
4
- data.tar.gz: 6ecddfbacf09de3757b65009e1c41ca3cb90b8667d2fe80972200159d9f0c2b3
3
+ metadata.gz: 3533d126bc13406c70b6931112c07952aac0c19b718f3100258fb2182da8fcea
4
+ data.tar.gz: a761495802d5507fba3469405a571555547ed58891b216575f947e2b6a0c63be
5
5
  SHA512:
6
- metadata.gz: '0490c1e9e84467c4437eaf65128cf9adddac4509a66e7e41a791f10cb5fee6ee86d5fb821177af022a04a41441c6c57a37e8015e6a288c5e6838ecbc966bb628'
7
- data.tar.gz: 935185356734aded9f38453cb2e6340501d1f99c482ffc17d62201867dd95224fe7018dfc079cfbdeed72ea44c63233fb5769987ddaa339ed7425ee6b0e281f5
6
+ metadata.gz: cb29c3bf90a6a6dbd6fcf488980c51b9f666206ea18748d0c670e6fc2a64c453ca0f20043dce113094e74eb34d4b45f6be8b7d7a433522a59ac6ebab2d6ba8bb
7
+ data.tar.gz: bb9c384545c42d45630576b523c3c82025b1df2a2fc8975b8959276566173ad684b9423e7f30fc46b7834c61328ef68f9ddb1ee73d83b08e0ec8ea10b93f117f
data/.rubocop.yml CHANGED
@@ -1,6 +1,17 @@
1
+ # Keep RuboCop's own default excludes (vendor/**, tmp/**, node_modules/**, .git/**) when adding to
2
+ # AllCops/Exclude below - without this, a project-level Exclude replaces them and vendor/bundle gets
3
+ # linted.
4
+ inherit_mode:
5
+ merge:
6
+ - Exclude
7
+
1
8
  AllCops:
2
9
  NewCops: enable
3
10
  TargetRubyVersion: 3.4
11
+ Exclude:
12
+ # Local Claude devbox (gitignored). Its Dockerfile.ruby ends in ".ruby", which RuboCop takes for
13
+ # a Ruby source file and then fails to parse.
14
+ - 'docker-claude/**/*'
4
15
 
5
16
  Style/StringLiterals:
6
17
  EnforcedStyle: double_quotes
@@ -79,6 +90,15 @@ RSpec/DescribeClass:
79
90
  Exclude:
80
91
  - 'spec/acceptance/**/*_spec.rb'
81
92
 
93
+ # Matches bidi2pdf-rails' own spec/rails_helper.rb: scenario/when_ are custom
94
+ # alias_example_group_to groups there (and here, see spec/spec_helper.rb), not the single example
95
+ # Capybara's own `scenario` conventionally means - this cop doesn't know that and miscounts every
96
+ # then_/and_ nested underneath as belonging to one example.
97
+ RSpec/MultipleExpectations:
98
+ Enabled: true
99
+ Exclude:
100
+ - 'spec/acceptance/**/*_spec.rb'
101
+
82
102
  plugins:
83
103
  - rubocop-rake
84
104
  - rubocop-rspec
data/CHANGELOG.md CHANGED
@@ -8,10 +8,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
- [unreleased]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.15..HEAD
11
+ [unreleased]: https://github.com/dieter-medium/bidi2pdf/compare/v0.1.16..HEAD
12
12
 
13
13
  <!-- generated by git-cliff end -->
14
14
 
15
+ ## [0.1.16] - 2026-09-22
16
+
17
+ ### 🐛 Fixed
18
+ - Give NavigationDNSError its own initializer
19
+ - Enforce the complete recipe shape in --validate, not just semantic checks
20
+ - Close schema/validator mismatches in recipe schema
21
+ - Document and address blank PDFs from Chrome's network checks
22
+ - Add browser tab cookie integration specs
23
+
24
+ ### 🔄 Changed
25
+ - Merge pull request #134 from dieter-medium/feat/llm-friendly-cli
26
+ - Merge pull request #133 from dieter-medium/feat/llm-friendly-cli
27
+ - Merge pull request #131 from dieter-medium/docs/local-network-access
28
+ - Merge pull request #130 from dieter-medium/fix/host-only-cookie-domain
29
+ - Merge pull request #126 from dieter-medium/chore-fix-release-creds-check
30
+
31
+ ### 🚀 Added
32
+ - Agent-friendly CLI - structured JSON, diagnose, and recipes
33
+
15
34
  ## [0.1.15] - 2026-09-20
16
35
 
17
36
  ### 🎨 Refactored
@@ -453,7 +472,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
453
472
 
454
473
  ### 🔄 Released
455
474
 
456
- - [unreleased](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.15..HEAD)
475
+ - [unreleased](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.16..HEAD)
476
+ - [0.1.16](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.15..v0.1.16)
457
477
  - [0.1.15](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.14..v0.1.15)
458
478
  - [0.1.14](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.13..v0.1.14)
459
479
  - [0.1.13](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.12..v0.1.13)
data/README.md CHANGED
@@ -20,16 +20,17 @@ Bidi2pdf gives you **precision, flexibility, and full control**.
20
20
  3. [Why BiDi?](#why-bidi-instead-of-cdp)
21
21
  4. [Installation](#installation)
22
22
  5. [CLI Usage](#cli-usage)
23
- 6. [Library API](#library-api)
24
- 7. [Architecture](#architecture)
25
- 8. [Docker](#docker)
26
- 9. [Configuration Options](#configuration-options)
27
- 10. [Programmatic Configuration](#programmatic-configuration)
28
- 11. [Rails Integration](#rails-integration)
29
- 12. [Test Helpers](#test-helpers)
30
- 13. [Development](#development)
31
- 14. [Contributing](#contributing)
32
- 15. [License](#license)
23
+ 6. [Agent and Automation Usage](#agent-and-automation-usage)
24
+ 7. [Library API](#library-api)
25
+ 8. [Architecture](#architecture)
26
+ 9. [Docker](#docker)
27
+ 10. [Configuration Options](#configuration-options)
28
+ 11. [Programmatic Configuration](#programmatic-configuration)
29
+ 12. [Rails Integration](#rails-integration)
30
+ 13. [Test Helpers](#test-helpers)
31
+ 14. [Development](#development)
32
+ 15. [Contributing](#contributing)
33
+ 16. [License](#license)
33
34
 
34
35
  ## ✨ Key Features
35
36
 
@@ -40,7 +41,9 @@ Bidi2pdf gives you **precision, flexibility, and full control**.
40
41
  ✅ **Docker-ready** – Plug and play with containers
41
42
  ✅ **Modern architecture** – Built on Chrome's next-gen BiDi protocol
42
43
  ✅ **Network logging** – Know which requests fail during rendering
43
- ✅ **Console log capture** – See what goes wrong inside the browser
44
+ ✅ **Console log capture** – See what goes wrong inside the browser
45
+ ✅ **Agent-ready** – Structured JSON output, NDJSON progress streaming, a page diagnostic, and
46
+ declarative recipes, so LLM agents and CI can drive it without parsing logs
44
47
 
45
48
  ---
46
49
 
@@ -109,6 +112,152 @@ bidi2pdf render \
109
112
 
110
113
  ---
111
114
 
115
+ ## 🤖 Agent and Automation Usage
116
+
117
+ Every command below also works without `--json` (human-readable output on stdout); with it, stdout
118
+ carries exactly one JSON document and nothing else - safe to pipe into `jq` or parse directly.
119
+ Human-readable logs move to stderr for the duration of any `--json`/`--output -` call, never
120
+ mixing into stdout. `--json-stream` works independently of the two: it always writes progress
121
+ events to stderr, whether or not `--json` is also given - in human mode, that just means
122
+ human-readable output keeps going to stdout as usual, alongside the stream. Full JSON Schema for
123
+ every shape is built into the gem, so an agent can discover it without reading this file:
124
+
125
+ ```bash
126
+ # 1. Check compatibility first - no browser launched
127
+ bidi2pdf version --json
128
+
129
+ # 2. Discover the shape of each command's own result, a manifest, an NDJSON event, or a recipe file
130
+ bidi2pdf schema render
131
+ bidi2pdf schema diagnose
132
+ bidi2pdf schema run
133
+ bidi2pdf schema manifest
134
+ bidi2pdf schema event
135
+ bidi2pdf schema recipe
136
+
137
+ # 3. Validate a recipe - no browser launched, the cheapest way to iterate on one
138
+ bidi2pdf run recipe.yml --validate
139
+
140
+ # 4. Run it for real
141
+ bidi2pdf run recipe.yml --json
142
+ ```
143
+
144
+ ### Structured render output
145
+
146
+ ```bash
147
+ bidi2pdf render --url https://example.com/invoice/14432423 --output example.pdf --json
148
+ ```
149
+
150
+ ```json
151
+ {
152
+ "schema_version": 1,
153
+ "ok": true,
154
+ "command": "render",
155
+ "output": "example.pdf",
156
+ "bytes": 182734,
157
+ "sha256": "abcd...",
158
+ "pages": 2,
159
+ "duration_ms": 842,
160
+ "navigation": { "requested_url": "https://example.com/invoice/14432423", "final_url": "https://example.com/invoice/14432423", "status": 200 },
161
+ "console": [],
162
+ "network_failures": [],
163
+ "warnings": [],
164
+ "error": null
165
+ }
166
+ ```
167
+
168
+ `pages` is `null`, with a warning explaining why, when the optional `pdf-reader` gem isn't
169
+ installed - see [Docker](#docker) for why the published images always have it. A failed render
170
+ still emits exactly this shape, with `ok: false` and a structured `error` (`code`, `message`,
171
+ `retryable`, `hint`, `details`) instead of a stack trace:
172
+
173
+ | Exit code | Meaning |
174
+ |---|---|
175
+ | `0` | success |
176
+ | `2` | CLI, configuration, or recipe-validation error |
177
+ | `3` | browser or navigation error |
178
+ | `4` | page not as expected (a recipe action/assertion, or a diagnose selector, failed) |
179
+ | `6` | output or PDF generation failure |
180
+ | `70` | unexpected internal error |
181
+
182
+ ### stdin/stdout, manifests, and progress streaming
183
+
184
+ ```bash
185
+ # Render HTML piped on stdin, PDF bytes piped out on stdout - no files touched
186
+ cat page.html | bidi2pdf render --stdin --output - > page.pdf
187
+
188
+ # A render manifest: enough to reproduce and diagnose the render later
189
+ bidi2pdf render --url https://example.com --output example.pdf --manifest render.json
190
+
191
+ # One JSON progress event per stderr line as the render happens - works with human-readable
192
+ # output too, not just --json
193
+ bidi2pdf render --url https://example.com --output example.pdf --json-stream
194
+ ```
195
+
196
+ ### Diagnosing a page before trusting its PDF
197
+
198
+ `bidi2pdf diagnose` loads a page like `render` does but produces no PDF - it answers *why does the
199
+ PDF not look like the page* (console errors, failed requests, font-loading status, `@media
200
+ print`/`@page` rules, fixed/sticky elements, Paged.js detection), not what the page's content is:
201
+
202
+ ```bash
203
+ bidi2pdf diagnose --url https://example.com/invoice/14432423 --json
204
+ ```
205
+
206
+ ### Declarative recipes
207
+
208
+ A recipe is a rendering contract - the waits a page needs before it's printed, and the properties
209
+ the resulting PDF must have - not a general browser-automation script:
210
+
211
+ ```yaml
212
+ # invoice.yml
213
+ version: 1
214
+
215
+ source:
216
+ url: https://example.com/invoice/123
217
+
218
+ actions:
219
+ - wait_for:
220
+ selector: "#invoice"
221
+ timeout: 10
222
+ - click:
223
+ selector: "#show-details"
224
+ - wait_network_idle:
225
+ timeout: 10
226
+
227
+ assert:
228
+ - selector_exists:
229
+ selector: "#total"
230
+ - no_console_errors: true
231
+ - page_count: 2
232
+ - pdf_text_present:
233
+ text: "Invoice #123"
234
+
235
+ output:
236
+ pdf: invoice.pdf
237
+ manifest: invoice.json
238
+ ```
239
+
240
+ ```bash
241
+ bidi2pdf run invoice.yml --validate # schema + known actions/assertions, no browser
242
+ bidi2pdf run invoice.yml --json # actions, then the PDF, then assertions against it
243
+ ```
244
+
245
+ Actions (`wait_for`, `click`, `evaluate`, `inject_script`, `inject_style`, `set_viewport`,
246
+ `wait_network_idle`) and page assertions (`selector_exists`, `text_present`, `no_console_errors`,
247
+ `no_network_failures`, `fonts_loaded`) are a thin layer over the same `BrowserTab` methods the
248
+ [Programmatic API](#-programmatic-api) below uses directly. PDF assertions (`page_count`,
249
+ `pdf_text_present`, `pdf_not_blank`) need the `pdf-reader` gem - a recipe using one fails
250
+ `--validate` immediately, before any browser launches, when it isn't installed.
251
+
252
+ `no_console_errors`, `no_network_failures`, `fonts_loaded`, and `pdf_not_blank` are presence-only
253
+ assertions - the step is either there or it isn't, nothing reads the value beside it - so they must
254
+ be written as `true` exactly, e.g. `- no_console_errors: true`; `false` (or any other value) is
255
+ rejected by both `bidi2pdf schema recipe` and `--validate` rather than being silently ignored.
256
+ `wait_for` needs exactly one of `selector`, `paged_js`, `script` - zero or more than one is
257
+ rejected the same way, before any browser launches.
258
+
259
+ ---
260
+
112
261
  ## 🧠 Programmatic API
113
262
 
114
263
  ### Classic Approach
@@ -286,6 +435,11 @@ docker run -it --rm \
286
435
 
287
436
  ✅ Tip: Mount your local directory (e.g. ./output) to /reports in the container to easily access the generated PDFs.
288
437
 
438
+ ✅ Both published images also install [`pdf-reader`](https://github.com/yob/pdf-reader) - not a
439
+ runtime dependency of the gem itself (see [Agent and Automation Usage](#agent-and-automation-usage)) -
440
+ so `pages` and the `page_count`/`pdf_text_present`/`pdf_not_blank` recipe assertions work out of
441
+ the box in either image, no extra install step needed.
442
+
289
443
  ### Docker Compose
290
444
 
291
445
  ```bash
@@ -332,6 +486,15 @@ docker compose -f docker/docker-compose.yml down
332
486
  | `--log_level` | Log level: debug, info, warn, error, fatal |
333
487
  | `--remote_browser_url` | Connect to remote Chrome session |
334
488
  | `--default_timeout` | Operation timeout (default: 60s) |
489
+ | `--json` | Emit a single structured JSON result document (see `bidi2pdf schema render`) |
490
+ | `--json_stream` | Emit one JSON progress event per line to stderr (see `bidi2pdf schema event`) - works with or without `--json` |
491
+ | `--stdin` | Read the HTML document from stdin, instead of `--url`/`--html-file` |
492
+ | `--output -` | Write raw PDF bytes to stdout instead of a file |
493
+ | `--manifest FILE` | Write a render manifest (see `bidi2pdf schema manifest`) to `FILE` |
494
+
495
+ See [Agent and Automation Usage](#agent-and-automation-usage) above for `bidi2pdf diagnose`,
496
+ `bidi2pdf run recipe.yml`, `bidi2pdf schema <kind>`, and `bidi2pdf version --json` - a separate
497
+ set of commands with their own options, not additional flags on `render`.
335
498
 
336
499
  ---
337
500
 
@@ -395,6 +558,28 @@ Bidi2pdf::SessionWarmer.shutdown
395
558
  > nothing else can reach those ports. With `remote_browser_url`, who can reach that endpoint on the
396
559
  > network is what matters, exactly as it does without the warmer.
397
560
 
561
+ ### Customizing Chrome arguments (and blank PDFs from inline HTML)
562
+
563
+ `Bidi2pdf::Bidi::Session::DEFAULT_CHROME_ARGS` already contains one `--disable-features=...` entry,
564
+ and Chrome only honors the **last** occurrence of that switch. To turn another feature off, extend
565
+ that entry instead of appending a second switch, which would silently drop the defaults:
566
+
567
+ ```ruby
568
+ chrome_args = Bidi2pdf::Bidi::Session::DEFAULT_CHROME_ARGS.map do |arg|
569
+ arg.start_with?("--disable-features=") ? "#{arg},LocalNetworkAccessChecks" : arg
570
+ end
571
+ ```
572
+
573
+ `LocalNetworkAccessChecks` is the one you are most likely to need. HTML rendered through
574
+ `BrowserTab#render_html_content` is loaded as a `data:` URL, which Chrome treats as a public origin;
575
+ if that HTML references assets on a private address (`localhost`, a Docker hostname, ...), recent
576
+ Chrome versions (confirmed with Chrome 153) block those requests before they are sent. Nothing
577
+ raises - the assets show up as failed network events, the server never sees a request, and the PDF
578
+ comes out unstyled or blank. Disable the check only where the asset host really is private, and keep
579
+ it on when rendering pages you do not control.
580
+ The [bidi2pdf-rails README](https://github.com/dieter-medium/bidi2pdf-rails#readme) has the full
581
+ walkthrough under "Blank PDFs: Chrome's Local Network Access Check".
582
+
398
583
  ---
399
584
 
400
585
  ## 🚂 Rails Integration
data/docker/Dockerfile CHANGED
@@ -34,7 +34,11 @@ WORKDIR /app
34
34
  # Copy your gem into container
35
35
  COPY ./pkg/bidi2pdf-*.gem ./
36
36
 
37
+ # pdf-reader is deliberately not a bidi2pdf runtime dependency - it is installed here, as its own
38
+ # step, so the published image always has `pages`/PDF assertions available, the environment an
39
+ # agent or CI job actually renders in.
37
40
  RUN gem install ./bidi2pdf-*.gem && \
41
+ gem install pdf-reader -v "~> 2.14" && \
38
42
  chown -R appuser:appuser /app
39
43
 
40
44
  # Switch to non-root user
@@ -26,7 +26,11 @@ WORKDIR /app
26
26
  # Copy your gem into container
27
27
  COPY ./pkg/bidi2pdf-*.gem ./
28
28
 
29
- RUN gem install ./bidi2pdf-*.gem
29
+ # pdf-reader is deliberately not a bidi2pdf runtime dependency - it is installed here, as its own
30
+ # step, so the published image always has `pages`/PDF assertions available, the environment an
31
+ # agent or CI job actually renders in.
32
+ RUN gem install ./bidi2pdf-*.gem && \
33
+ gem install pdf-reader -v "~> 2.14"
30
34
 
31
35
 
32
36
  # Stage 2
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ class CLI < Thor
5
+ # Shared machinery for --json/--json-stream/--output - across render/diagnose/run: reserving
6
+ # stdout for exactly one machine-readable payload, and mapping a Result to a process exit
7
+ # status.
8
+ module JsonOutput
9
+ private
10
+
11
+ # Bidi2pdf.logger (and friends) default to $stdout (see lib/bidi2pdf.rb), so a
12
+ # --json/--output - render has to redirect them for its duration or every log line would
13
+ # land in the same stream as the JSON document / PDF bytes it is trying to keep pure.
14
+ # Logger#reopen swaps the destination in place, so nothing else holding a reference to
15
+ # these loggers needs to know.
16
+ def reserve_stdout_for_machine_output
17
+ loggers = [Bidi2pdf.logger, Bidi2pdf.network_events_logger, Bidi2pdf.browser_console_logger].compact
18
+ loggers.each { |logger| logger.logger.reopen($stderr) }
19
+
20
+ yield
21
+ ensure
22
+ loggers.each { |logger| logger.logger.reopen($stdout) }
23
+ end
24
+
25
+ def exit_for_result(result)
26
+ exit(result.ok? ? Bidi2pdf::ExitCodes::SUCCESS : Bidi2pdf::ExitCodes.for(result.error[:code]))
27
+ end
28
+ end
29
+ end
30
+ end