Skip to content

feat(cli): add fern docs link check command for live site link validation#15818

Merged
dsinghvi merged 45 commits into
mainfrom
devin/1778435733-docs-check-links
May 14, 2026
Merged

feat(cli): add fern docs link check command for live site link validation#15818
dsinghvi merged 45 commits into
mainfrom
devin/1778435733-docs-check-links

Conversation

@dsinghvi
Copy link
Copy Markdown
Member

@dsinghvi dsinghvi commented May 10, 2026

Description

Adds fern docs check --links (v2) / fern docs link check (v1) command that validates all links on a live Fern docs site via the dashboard's link-checker SSE endpoint.

Changes Made

  • Link check command with real-time SSE streaming, progress bars, and categorized output (internal/external/blocked)
  • Source file resolution: When sourcePageIds are available from the dashboard, resolves broken links to local file:line:column locations
  • Multiple output formats: Human-readable (default), JSON, and CSV
  • Runtime dashboard URL override: getDashboardBaseUrl() getter (deferred evaluation) ensures .env file overrides are respected after dotenv loading
  • Shared constant: getDashboardBaseUrl exported from @fern-api/login, imported by both v1 and v2 CLI
  • Updated README.md generator (if applicable) — N/A

Key Fixes from Code Review

  • SSE robustness: WHATWG-compliant data: prefix handling (with/without space), decoder flush on EOF, null/type validation after JSON.parse
  • Type safety: Runtime validation on error response body (no blind as casts), type-safe toBrokenLink helper, validated complete event array elements, narrowed type guard return types
  • Output correctness: RFC 4180 CSV compliance (no comment lines, runStatus column), interrupted status in JSON/CSV, process.stderr.write bypass for text output
  • Error handling: progress.finish() before error messages in catch, EPIPE-safe writeAndDrain, empty catch blocks documented
  • Path handling: Segment-boundary check in stripBasePath, no double toRelativePath processing
  • Logging: warn level for stream-interrupted messages, case-insensitive scheme regex
  • UX parity: v1 CLI now mirrors v2 status message formatting (Icons + chalk styling)

Testing

  • E2E tested against smoke-test-dev.docs.dev.buildwithfern.com
  • Manual testing completed
  • CI passing (42/42 on all pushes)

Link to Devin session: https://app.devin.ai/sessions/66d97c97cdbc4f6a9d8723b9d9641aac
Requested by: @Ryan-Amirthan

@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@devin-ai-integration devin-ai-integration Bot changed the title feat(cli): add fern docs check --links command for live site link validation feat(cli): add fern docs link check command for live site link validation May 10, 2026
Comment on lines +67 to +73
if (result.brokenLinks.length > 0) {
throw new CliError({ code: CliError.Code.ValidationError });
}

if (result.brokenLinks.length === 0 && result.blockedLinks.length === 0) {
context.stderr.info(`${Icons.success} ${chalk.green("All links valid")}`);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The success message will never be displayed when there are broken links because the error is thrown first, making lines 71-73 unreachable. However, there's a more critical issue: when there are only blocked links (no broken links), the command exits successfully (exit code 0) without throwing an error, but also shows no success message. This creates inconsistent behavior.

if (result.brokenLinks.length > 0) {
    throw new CliError({ code: CliError.Code.ValidationError });
}

// This condition is unreachable when brokenLinks > 0
// But also won't show when blockedLinks > 0 and brokenLinks === 0
if (result.brokenLinks.length === 0 && result.blockedLinks.length === 0) {
    context.stderr.info(`${Icons.success} ${chalk.green("All links valid")}`);
}

Fix by restructuring:

if (result.brokenLinks.length > 0) {
    throw new CliError({ code: CliError.Code.ValidationError });
} else if (result.blockedLinks.length === 0) {
    context.stderr.info(`${Icons.success} ${chalk.green("All links valid")}`);
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@devin fix

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — restructured to if/else if so the success message shows correctly when there are no broken links and no blocked links. Pushed in commit 58467c2.

@dsinghvi
Copy link
Copy Markdown
Member Author

@claude review

Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckClient.ts Outdated
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/command.ts
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckClient.ts Outdated
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckClient.ts Outdated
Comment thread packages/cli/cli/src/cli.ts
Comment thread packages/cli/cli/src/cli.ts
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckClient.ts Outdated
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckClient.ts Outdated
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckFormatter.ts Outdated
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/SourceResolver.ts Outdated
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/SourceResolver.ts Outdated
Comment thread packages/cli/cli/src/cli.ts Outdated
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/SourceResolver.ts
Comment thread packages/cli/cli/src/cli.ts Outdated
Comment thread packages/cli/cli/src/cli.ts
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/command.ts Outdated
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckClient.ts
Comment thread packages/cli/cli/src/cli.ts Outdated
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckClient.ts Outdated
Comment thread packages/cli/cli/changes/unreleased/add-docs-check-links.yml
Comment on lines +216 to +220
function isLinkCheckedData(
data: Record<string, unknown>
): data is { url: string; statusCode: number | null; isInternal: boolean; sourcePages: string[]; error?: string } {
return typeof data.url === "string" && Array.isArray(data.sourcePages);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type guard isLinkCheckedData is incomplete and doesn't validate all required fields. It only checks url is a string and sourcePages is an array, but doesn't validate statusCode, isInternal, or that sourcePages contains strings. If the server sends invalid types (e.g., statusCode: "404" as a string instead of number), the type guard will pass but create a BrokenLink with the wrong types, violating the interface contract.

function isLinkCheckedData(
    data: Record<string, unknown>
): data is { url: string; statusCode: number | null; isInternal: boolean; sourcePages: string[]; error?: string } {
    return (
        typeof data.url === "string" &&
        (data.statusCode === null || data.statusCode === undefined || typeof data.statusCode === "number") &&
        (data.isInternal === undefined || typeof data.isInternal === "boolean") &&
        Array.isArray(data.sourcePages) &&
        data.sourcePages.every((page) => typeof page === "string") &&
        (data.error === undefined || typeof data.error === "string")
    );
}
Suggested change
function isLinkCheckedData(
data: Record<string, unknown>
): data is { url: string; statusCode: number | null; isInternal: boolean; sourcePages: string[]; error?: string } {
return typeof data.url === "string" && Array.isArray(data.sourcePages);
}
function isLinkCheckedData(
data: Record<string, unknown>
): data is { url: string; statusCode: number | null; isInternal: boolean; sourcePages: string[]; error?: string } {
return (
typeof data.url === "string" &&
(data.statusCode === null || data.statusCode === undefined || typeof data.statusCode === "number") &&
(data.isInternal === undefined || typeof data.isInternal === "boolean") &&
Array.isArray(data.sourcePages) &&
data.sourcePages.every((page) => typeof page === "string") &&
(data.error === undefined || typeof data.error === "string")
);
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 03102c9: isLinkCheckedData now validates all fields — statusCode (number|null|undefined), isInternal (boolean|undefined), sourcePages (string[] with element check), and error (string|undefined).

Comment thread packages/cli/cli/src/cli.ts
Comment thread packages/cli/cli/src/cli.ts
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckFormatter.ts Outdated
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckClient.ts Outdated
Comment thread packages/cli/cli/changes/unreleased/add-docs-check-links.yml
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/SourceResolver.ts Outdated
Comment thread packages/cli/cli/src/cli.ts
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/SourceResolver.ts Outdated
Comment thread packages/cli/cli/src/cli.ts
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/command.ts
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/command.ts
Comment thread packages/cli/cli/src/cli.ts Outdated
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/SourceResolver.ts Outdated
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/SourceResolver.ts
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckClient.ts Outdated
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckFormatter.ts Outdated
Comment thread packages/cli/cli/changes/unreleased/link-check-source-page-ids.yml
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/SourceResolver.ts
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/command.ts
Comment thread packages/cli/cli/src/cli.ts
- LinkCheckClient: flush SSE buffer/decoder after read loop exits
- LinkCheckClient: extract processLine helper to avoid duplication
- LinkCheckClient: move onStreamInterrupted inside partial-result branch
- LinkCheckClient: use actual counts in partial-result return (pagesScrapedSoFar, linksCheckedSoFar, computed workingLinks)
- LinkCheckClient: pass pageIndex+1 (1-based count) to onPageScraped callbacks
- ProgressRenderer: rename parameter to pagesScraped for clarity
- LinkCheckFormatter: add statusCode to blocked link text output
- LinkCheckFormatter: add link.error display in text output for broken and blocked
- LinkCheckFormatter: rename CSV header isInternal -> scope to match values
- LinkCheckFormatter: fix stripBasePath regression by using toRelativePath for URL slugs
- LinkCheckFormatter: thread interrupted flag for 'Interrupted after' vs 'Finished in'
- command.ts (v2): use process.stdout.write for JSON/CSV to bypass logger prefix
- cli.ts (v1): add onStreamInterrupted callback matching v2
- cli.ts (v1): add 'Link check failed:' prefix for InternalError
- cli.ts (v1): route error messages to stderr before failAndThrow in resolveDocsLinkCheckContext
- SourceResolver: gate sourcePageIds branch on docsConfigDir != null
- Changelog: fix inaccurate descriptions (removed [docs]: prefix claim, line:column claim)

Co-Authored-By: ryanstep <ryanstep@umich.edu>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

@claude review

Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckFormatter.ts Outdated
- ProgressRenderer: add non-TTY completion message for link checking phase
- LinkCheckFormatter: add 'status' field to JSON summary (complete/interrupted)
- LinkCheckFormatter: add '# status:' comment to CSV output header

Co-Authored-By: ryanstep <ryanstep@umich.edu>
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckClient.ts Outdated
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/command.ts
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckClient.ts
devin-ai-integration Bot and others added 2 commits May 14, 2026 01:28
…window

Co-Authored-By: ryanstep <ryanstep@umich.edu>
- command.ts: add explanatory comment to empty catch block
- command.ts: make normalizeDomain case-insensitive for scheme prefix
- cli.ts: make normalizeDomain case-insensitive for scheme prefix
- LinkCheckClient: normalize trailing slash on dashboardUrl
- LinkCheckFormatter: replace CSV comment line with RFC 4180 runStatus column
- cli.ts: add error handler to writeAndDrain to prevent hangs on EPIPE

Co-Authored-By: ryanstep <ryanstep@umich.edu>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

@claude review

Comment thread packages/cli/login/src/constants.ts Outdated
devin-ai-integration Bot and others added 2 commits May 14, 2026 02:11
- LinkCheckClient: add null/type check after JSON.parse, comment empty catch,
  remove unused SseEvent interface, use ParsedSseEvent with validation
- LinkCheckFormatter: use toRelativePath for internal blocked links,
  add segment boundary check to stripBasePath, fix escapeCsv for CR,
  stop double-calling toRelativePath in formatReference/formatJsonReference
- SourceResolver: add comment to empty catch, fix isUserAuthoredPage
  to recognize root .mdx/.md files

Co-Authored-By: ryanstep <ryanstep@umich.edu>
- LinkCheckClient: validate data field exists and is object in SSE parser,
  add toBrokenLink helper for type-safe conversion from validated data,
  validate complete event array elements with isLinkCheckedData,
  narrow isCompleteData return type to match actual validation (unknown[])
- LinkCheckFormatter: use ref.slug directly in CSV (no double processing)
- cli.ts: call progress.finish() before error message in catch block
  to prevent garbled TTY output from active progress bar

Co-Authored-By: ryanstep <ryanstep@umich.edu>
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/command.ts Outdated
…ext output

- Convert DASHBOARD_BASE_URL const to getDashboardBaseUrl() getter so
  .env file overrides are respected after dotenv loading
- Update all call sites (cli.ts, command.ts, login.ts, index.ts)
- Use process.stderr.write for text output in v2 command to bypass
  logger prefix corruption with --log-level debug

Co-Authored-By: ryanstep <ryanstep@umich.edu>
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckClient.ts
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckClient.ts Outdated
- Support both 'data: {...}' and 'data:{...}' SSE prefix forms per
  WHATWG SSE spec (space after colon is optional)
- Add null/object guard before casting complete event array elements
  to prevent TypeError on malformed payloads

Co-Authored-By: ryanstep <ryanstep@umich.edu>
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/SourceResolver.ts
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/command.ts Outdated
…nterruption

- Revert isUserAuthoredPage to only check for '/' (existsSync handles
  root-level files; .md/.mdx extension check misclassifies auto-generated
  pages like tag-plant.md)
- Use stderr.warn instead of stderr.info for onStreamInterrupted in both
  v1 and v2 so the data-fidelity warning is visible with --log-level warn

Co-Authored-By: ryanstep <ryanstep@umich.edu>
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckFormatter.ts Outdated
The \r\n check is unreachable since \r is already checked on the
preceding line. Any string with \r\n necessarily contains \r.

Co-Authored-By: ryanstep <ryanstep@umich.edu>
Comment thread packages/cli/cli-v2/src/commands/docs/link/check/LinkCheckClient.ts
devin-ai-integration Bot and others added 3 commits May 14, 2026 12:48
…tion

Validate response.json() shape with typeof checks before accessing
message/error fields, matching the type-guard pattern used for SSE
event data throughout the file.

Co-Authored-By: ryanstep <ryanstep@umich.edu>
Add Icons and chalk formatting to v1 'Checking links' and 'All links
valid' messages to match v2 styling (cyan domain, green checkmark).

Co-Authored-By: ryanstep <ryanstep@umich.edu>
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization's overage spend limit has been reached.

Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.

Once credits are available, reopen this pull request to trigger a review.

Co-Authored-By: ryanstep <ryanstep@umich.edu>
@dsinghvi dsinghvi merged commit a1d3ce4 into main May 14, 2026
71 checks passed
@dsinghvi dsinghvi deleted the devin/1778435733-docs-check-links branch May 14, 2026 17:43
fern-support pushed a commit that referenced this pull request May 14, 2026
…dation (#15818)

* feat(cli): add fern docs check --links command for live site link validation

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* refactor: restructure to fern docs link check --url

Move link checker from 'fern docs check --links --instance' to
'fern docs link check --url' for clearer UX. Removes fern-specific
jargon ('instance') in favor of user-friendly '--url' flag.

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: restructure broken/blocked link check logic per review

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* feat: wire up fern docs link check in main CLI entry point

The main CLI (packages/cli/cli) registers docs subcommands directly
in cli.ts, not through cli-v2. Added addDocsLinkCommand and
addDocsLinkCheckCommand to the main CLI, importing LinkCheckClient,
LinkCheckFormatter, and ProgressRenderer from @fern-api/cli-v2.

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: address Claude review — SSE error handling and progress cleanup

- SSE 'error' events now capture the message and throw LinkCheckError
  after the stream closes, preventing false 'All links valid' output
- Move progress.finish() to finally block so TTY progress line is
  always cleared, even on mid-stream errors
- Fix biome import ordering in cli-v2/src/index.ts and cli/src/cli.ts

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: resolve TypeScript errors in main CLI link check command

- Use token.value (FernToken has a .value property, not string)
- Access config.instances instead of nonexistent docsInstances
- Rename map parameter to avoid implicit any

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: align SSE client with actual server event schema

- sitemap_fetched: totalPages (not pageCount)
- page_scraped: pageUrl/pageIndex (not url/pagesScraped)
- link_checked: statusCode/isInternal (not status/type/classification)
- Add link_check_progress handler for progress bar during link checking
- Add links_check_started handler for total link count
- Use complete event's brokenLinks/blockedLinks arrays as source of truth
- Update formatter to use new BrokenLink field names

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* feat: improve link check output formatting with colored output, progress bars, and local file resolution

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: biome import ordering for SourceResolver

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: address all claude review comments on link checker

- Replace blind `as` type assertions with type guard functions for all SSE events
- Add `default: assertNever()` to output format switch statement
- Add explanatory comment to empty catch block for non-JSON SSE lines
- Remove double error printing (onError callback removed, error thrown after stream)
- Add `sawComplete` guard to throw if stream ends without completion event
- Fix SourceResolver: use path.dirname() since docsAbsolutePath is a file path
- Fix SourceResolver: boundary-aware link matching to prevent false positives
- Add `error` field to blocked links JSON output
- Fix output flushing: await drain before throwing CliError in v1 CLI
- Add pathname-without-slash search pattern for relative link matching

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: address remaining claude review comments

- Add prefix boundary check to findLinkMatch (prevents /api matching /foo/api)
- Remove overly permissive pathname.slice(1) pattern from getSearchPatterns
- Map 404 → ConfigError in v1 CLI to match v2 behavior
- Add comments to empty catch blocks in walkDir and getDocsConfigPath

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix(cli): align progress bar indent, fix output flushing race, deduplicate project load

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* refactor: replace [broken-link]/[blocked-link] with [docs]: prefix, remove footer and SourceResolver heuristic

- Change link prefixes from [broken-link]/[blocked-link] to [docs]: for consistency
- Remove 'Broken links found' redundant footer line
- Simplify SourceResolver to pass through source page URLs directly
  (file:line:column resolution will be handled server-side via pageId)
- Remove unused resolveDocsConfigPath function and docsAbsolutePath plumbing

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: prevent duplicate error message for unauthorized link check

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix formatting and personal taste for UX

* fix: address review comments — CSV escape, dead code, stderr routing, type guards, changelog

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* feat: consume pageId from SSE and resolve to local file:line:column

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: resolve docsConfigDir even when --url is provided for sourcePageId resolution

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: resolve docsConfigDir in v1 CLI when --url is provided

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: allow runtime override of dashboard URL via FERN_DASHBOARD_URL_OVERRIDE

process.env.FERN_DASHBOARD_URL is replaced at build time by tsup's env
option, making it impossible to override at runtime for testing preview
deployments. Add FERN_DASHBOARD_URL_OVERRIDE which uses bracket notation
to avoid compile-time replacement, enabling runtime override for both
v1 and v2 CLI paths.

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: format DASHBOARD_BASE_URL for biome line length

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* refactor: make FERN_DASHBOARD_URL a clean runtime override

Rename build-time env var to FERN_DASHBOARD_URL_DEFAULT so that
FERN_DASHBOARD_URL can be set at runtime to point at any arbitrary
dashboard URL (Vercel preview, localhost, etc.).

- All 4 build scripts now bake FERN_DASHBOARD_URL_DEFAULT
- Runtime resolution: FERN_DASHBOARD_URL > FERN_DASHBOARD_URL_DEFAULT > hardcoded fallback
- Single shared constant exported from @fern-api/login
- v1 and v2 CLI both import from @fern-api/login instead of duplicating

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: biome import ordering for DASHBOARD_BASE_URL

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: filter out auto-generated API reference pageIds from link check output

Only show source files that exist locally. Auto-generated API reference
pages (e.g. tag-plant.md) are omitted since they don't correspond to
user-authored files. Falls back to source page URLs when no local files
resolve.

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* refactor: remove line:column resolution from link checker

Simplify SourceResolver to only show file paths without line:column
numbers. Line:column support can be added back as a follow-up with
better URL matching (basepath stripping, relative paths, etc.).

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: improve error message when link check stream is interrupted

Instead of the unhelpful 'stream ended unexpectedly without a completion
event', the CLI now:
- Shows which phase was interrupted (connecting/scraping/checking)
- Includes progress stats (pages scraped, links checked)
- Displays partial results if any broken links were found
- Only throws an error if no results were collected at all

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: filter auto-generated API reference pages even without local workspace

PageIds without a directory separator (e.g. 'tag-plant.md') are
auto-generated API reference entries. These are now filtered out
regardless of whether a local workspace is available.

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: show URL slug for auto-generated API reference pages

Instead of hiding auto-generated API reference pages entirely, show
the URL slug (e.g. /platform/api-reference/api-reference/plant) as
the source reference. User-authored pages still show local file paths.

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* chore: update pnpm-lock.yaml after rebase

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: rename JSON fields (display→slug, localFile→filePath), fix partial-result counters and basepath stripping

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: format long ternary line for biome

Co-Authored-By: Deep Singhvi <deep@buildwithfern.com>

* fix: address all Claude Code review comments on link check command

- LinkCheckClient: flush SSE buffer/decoder after read loop exits
- LinkCheckClient: extract processLine helper to avoid duplication
- LinkCheckClient: move onStreamInterrupted inside partial-result branch
- LinkCheckClient: use actual counts in partial-result return (pagesScrapedSoFar, linksCheckedSoFar, computed workingLinks)
- LinkCheckClient: pass pageIndex+1 (1-based count) to onPageScraped callbacks
- ProgressRenderer: rename parameter to pagesScraped for clarity
- LinkCheckFormatter: add statusCode to blocked link text output
- LinkCheckFormatter: add link.error display in text output for broken and blocked
- LinkCheckFormatter: rename CSV header isInternal -> scope to match values
- LinkCheckFormatter: fix stripBasePath regression by using toRelativePath for URL slugs
- LinkCheckFormatter: thread interrupted flag for 'Interrupted after' vs 'Finished in'
- command.ts (v2): use process.stdout.write for JSON/CSV to bypass logger prefix
- cli.ts (v1): add onStreamInterrupted callback matching v2
- cli.ts (v1): add 'Link check failed:' prefix for InternalError
- cli.ts (v1): route error messages to stderr before failAndThrow in resolveDocsLinkCheckContext
- SourceResolver: gate sourcePageIds branch on docsConfigDir != null
- Changelog: fix inaccurate descriptions (removed [docs]: prefix claim, line:column claim)

Co-Authored-By: ryanstep <ryanstep@umich.edu>

* fix: address round-2 Claude Code comments

- ProgressRenderer: add non-TTY completion message for link checking phase
- LinkCheckFormatter: add 'status' field to JSON summary (complete/interrupted)
- LinkCheckFormatter: add '# status:' comment to CSV output header

Co-Authored-By: ryanstep <ryanstep@umich.edu>

* fix: use totalLinks for phase detection to cover links_check_started window

Co-Authored-By: ryanstep <ryanstep@umich.edu>

* fix: address round-3 Claude Code review comments

- command.ts: add explanatory comment to empty catch block
- command.ts: make normalizeDomain case-insensitive for scheme prefix
- cli.ts: make normalizeDomain case-insensitive for scheme prefix
- LinkCheckClient: normalize trailing slash on dashboardUrl
- LinkCheckFormatter: replace CSV comment line with RFC 4180 runStatus column
- cli.ts: add error handler to writeAndDrain to prevent hangs on EPIPE

Co-Authored-By: ryanstep <ryanstep@umich.edu>

* fix: address remaining Claude Code review comments (round 4)

- LinkCheckClient: add null/type check after JSON.parse, comment empty catch,
  remove unused SseEvent interface, use ParsedSseEvent with validation
- LinkCheckFormatter: use toRelativePath for internal blocked links,
  add segment boundary check to stripBasePath, fix escapeCsv for CR,
  stop double-calling toRelativePath in formatReference/formatJsonReference
- SourceResolver: add comment to empty catch, fix isUserAuthoredPage
  to recognize root .mdx/.md files

Co-Authored-By: ryanstep <ryanstep@umich.edu>

* fix: deeper SSE validation, type-safe complete data, progress ordering

- LinkCheckClient: validate data field exists and is object in SSE parser,
  add toBrokenLink helper for type-safe conversion from validated data,
  validate complete event array elements with isLinkCheckedData,
  narrow isCompleteData return type to match actual validation (unknown[])
- LinkCheckFormatter: use ref.slug directly in CSV (no double processing)
- cli.ts: call progress.finish() before error message in catch block
  to prevent garbled TTY output from active progress bar

Co-Authored-By: ryanstep <ryanstep@umich.edu>

* fix: defer DASHBOARD_BASE_URL to invocation time, bypass logger for text output

- Convert DASHBOARD_BASE_URL const to getDashboardBaseUrl() getter so
  .env file overrides are respected after dotenv loading
- Update all call sites (cli.ts, command.ts, login.ts, index.ts)
- Use process.stderr.write for text output in v2 command to bypass
  logger prefix corruption with --log-level debug

Co-Authored-By: ryanstep <ryanstep@umich.edu>

* fix: handle SSE data prefix without space, guard null array elements

- Support both 'data: {...}' and 'data:{...}' SSE prefix forms per
  WHATWG SSE spec (space after colon is optional)
- Add null/object guard before casting complete event array elements
  to prevent TypeError on malformed payloads

Co-Authored-By: ryanstep <ryanstep@umich.edu>

* fix: revert isUserAuthoredPage extension check, use warn for stream interruption

- Revert isUserAuthoredPage to only check for '/' (existsSync handles
  root-level files; .md/.mdx extension check misclassifies auto-generated
  pages like tag-plant.md)
- Use stderr.warn instead of stderr.info for onStreamInterrupted in both
  v1 and v2 so the data-fidelity warning is visible with --log-level warn

Co-Authored-By: ryanstep <ryanstep@umich.edu>

* fix: remove unreachable \r\n check in escapeCsv

The \r\n check is unreachable since \r is already checked on the
preceding line. Any string with \r\n necessarily contains \r.

Co-Authored-By: ryanstep <ryanstep@umich.edu>

* fix: replace blind as cast on error response body with runtime validation

Validate response.json() shape with typeof checks before accessing
message/error fields, matching the type-guard pattern used for SSE
event data throughout the file.

Co-Authored-By: ryanstep <ryanstep@umich.edu>

* quick fix for formatting

* fix: mirror v2 status message formatting in v1 CLI

Add Icons and chalk formatting to v1 'Checking links' and 'All links
valid' messages to match v2 styling (cyan domain, green checkmark).

Co-Authored-By: ryanstep <ryanstep@umich.edu>

* fix: sort imports in cli.ts

Co-Authored-By: ryanstep <ryanstep@umich.edu>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Ryan Amirthan Stephen <105958906+Ryan-Amirthan@users.noreply.github.com>
Co-authored-by: ryanstep <ryanstep@umich.edu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants