* fix(cli): reject directory --composition and add --browser-timeout (#1199)
Two unrelated symptoms from issue #1199, fixed together:
1. `--composition .` (or any directory path) used to slip past the
existsSync check in render.ts and explode downstream as
`EISDIR: illegal operation on a directory, read` when the producer
readFileSync'd the entry. The CLI now treats `.` / `""` as "omit
the flag" (falls back to index.html) and rejects other directory
paths with an actionable error pointing at the .html shape.
2. The 60s Puppeteer page.goto timeout in frameCapture.ts was hard-
coded, so heavy compositions (many videos / fonts / asset requests)
could not complete `domcontentloaded` in time. Add a configurable
`pageNavigationTimeout` to EngineConfig (default 60_000, env
fallback PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS) and expose it as
`--browser-timeout <seconds>` on `hyperframes render`. The flag
threads through both renderLocal (via resolveConfig) and the
docker bridge (via buildDockerRunArgs).
Tests:
- render.test.ts: forwards/omits pageNavigationTimeout into resolveConfig
- dockerRunArgs.test.ts: forwards/omits --browser-timeout (seconds)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): address PR #1200 review — extract validators, tighten bounds
Addresses Vai's blockers and Miguel's nits on PR #1200:
- Vai blocker 1 (fallow CRAP) + blocker 3 (no argv tests):
Extract --browser-timeout and --composition validators into pure
helpers in utils/renderArgs.ts with a structured-result discriminant.
Drops ~45 lines of inline validation from run(), reducing its CRAP
score 1290→978 and cyclomatic 75→65. 19 new unit tests cover the
parse branches (sub-ms, overflow, NaN, Infinity, empty, negative,
".", "./", whitespace, directory, missing, ../escape, sibling-prefix).
- Vai blocker 2 (sub-ms → timeout:0 = "no timeout"): reject inputs
that round to <1 ms. Puppeteer treats page.goto({timeout:0}) as
wait-forever, so --browser-timeout 0.0004 silently flipped the
semantics. Now rejected with an explicit "rounds to 0 ms" error.
- Vai important 5 (1e10 accepted → setTimeout overflow): cap at
86_400s (24h). Above Node's TIMEOUT_MAX ≈ 2^31-1 ms setTimeout
fires immediately, the opposite of "long timeout."
- Vai important 4 (related timeouts unmentioned): CLI help and docs
now flag PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS and the 45s
playerReadyTimeout as the other knobs heavy compositions may need.
- Vai nit 7 (s/ms unit mismatch): help text and docs row both call
out the SECONDS-vs-MILLISECONDS difference between flag and env.
- Vai nit 8 / Miguel nit (composition flag discoverability): the
--composition description now says "Pass `.` (or omit the flag)
to render the project's index.html."
- Miguel nit (dead branch): the entryFile === "" unreachable branch
is gone. New helper uses `if (!trimmed || trimmed === ".")`.
Also adds a trailing-separator guard on the project-containment check
(sibling-prefix bypass: /proj-evil/x.html no longer slips past
startsWith('/proj')) — flagged by the code review.
The three remaining fallow complexity findings on render.ts (run,
renderDocker, trackRenderMetrics) are inherited from main; this PR
reduces run() but does not refactor it. Suppressed with
fallow-ignore-next-line markers and inline rationale.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): diverge --browser-timeout error messages per Vai nit 5
The `not-a-number` and `not-positive` branches in browserTimeoutErrorMessage
shared the generic "Must be a positive number of seconds" message even
though the discriminant carried distinct kinds. Diverge them so users see
the specific failure mode:
--browser-timeout abc → "Got \"abc\", which is not a number."
--browser-timeout -5 → "Got \"-5\" seconds, which is not positive."
The shared hint ("pass a positive number of seconds, e.g. 180") is
preserved on both branches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>