#!/usr/bin/env bun import { runConvertCommand, type ConvertCommandOptions, type OutputFormat, type WaitMode, } from "./commands/convert"; export const HELP_TEXT = ` baoyu-fetch - Read a URL into Markdown or JSON with Chrome CDP Usage: baoyu-fetch [options] Options: --output Save output to file --format Output format: markdown | json --json Alias for --format json --adapter Force an adapter (e.g. x, generic) --download-media Download adapter-reported media into ./imgs and ./videos, then rewrite markdown links --media-dir Base directory for downloaded media. Defaults to the output directory --debug-dir Write debug artifacts --cdp-url Reuse an existing Chrome DevTools endpoint --browser-path Explicit Chrome binary path --chrome-profile-dir Chrome user data dir. Defaults to BAOYU_CHROME_PROFILE_DIR or baoyu-skills/chrome-profile. --headless Launch a temporary headless Chrome if needed --wait-for Wait mode: interaction | force interaction: start visible Chrome and auto-wait only when login or verification is required force: start visible Chrome, then auto-continue after it detects login/challenge progress or continue immediately when you press Enter --wait-for-interaction Alias for --wait-for interaction --wait-for-login Alias for --wait-for interaction --interaction-timeout How long to wait for manual interaction before failing (default: 600000) --interaction-poll-interval How often to poll interaction state while waiting (default: 1500) --login-timeout Alias for --interaction-timeout --login-poll-interval Alias for --interaction-poll-interval --timeout Page timeout in milliseconds (default: 30000) --help Show help Examples: baoyu-fetch https://example.com baoyu-fetch https://example.com --format markdown --output article.md --download-media baoyu-fetch https://example.com --format json --output article.json baoyu-fetch https://x.com/lennysan/status/2036483059407810640 --wait-for interaction baoyu-fetch https://x.com/lennysan/status/2036483059407810640 --wait-for force `.trim(); interface CliOptions extends ConvertCommandOptions { url?: string; help: boolean; } function normalizeWaitMode(raw: string): WaitMode { const value = raw.toLowerCase(); if (value === "interaction" || value === "auto") { return "interaction"; } if (value === "force" || value === "manual" || value === "always") { return "force"; } throw new Error(`Invalid wait mode: ${raw}. Expected interaction or force.`); } function normalizeOutputFormat(raw: string): OutputFormat { const value = raw.toLowerCase(); if (value === "markdown" || value === "json") { return value; } throw new Error(`Invalid output format: ${raw}. Expected markdown or json.`); } export function parseArgs(argv: string[]): CliOptions { const options: CliOptions = { format: "markdown", headless: false, downloadMedia: false, waitMode: "none", interactionTimeoutMs: 600_000, interactionPollIntervalMs: 1_500, timeoutMs: 30_000, help: false, }; const args = argv.slice(2); for (let index = 0; index < args.length; index += 1) { const value = args[index]; if (value === "--help" || value === "-h") { options.help = true; continue; } if (value === "--format") { const format = args[index + 1]; if (!format) { throw new Error("--format requires a value"); } options.format = normalizeOutputFormat(format); index += 1; continue; } if (value === "--json") { options.format = "json"; continue; } if (value === "--download-media") { options.downloadMedia = true; continue; } if (value === "--headless") { options.headless = true; continue; } if (value === "--wait-for") { const mode = args[index + 1]; if (!mode) { throw new Error("--wait-for requires a mode"); } options.waitMode = normalizeWaitMode(mode); index += 1; continue; } if (value === "--wait-for-interaction" || value === "--wait-for-login") { options.waitMode = "interaction"; continue; } if (value === "--output") { options.output = args[index + 1]; index += 1; continue; } if (value === "--adapter") { options.adapter = args[index + 1]; index += 1; continue; } if (value === "--debug-dir") { options.debugDir = args[index + 1]; index += 1; continue; } if (value === "--media-dir") { options.mediaDir = args[index + 1]; index += 1; continue; } if (value === "--cdp-url") { options.cdpUrl = args[index + 1]; index += 1; continue; } if (value === "--browser-path") { options.browserPath = args[index + 1]; index += 1; continue; } if (value === "--chrome-profile-dir") { options.chromeProfileDir = args[index + 1]; index += 1; continue; } if (value === "--timeout") { const parsed = Number(args[index + 1]); if (!Number.isFinite(parsed) || parsed <= 0) { throw new Error(`Invalid timeout: ${args[index + 1]}`); } options.timeoutMs = parsed; index += 1; continue; } if (value === "--interaction-timeout" || value === "--login-timeout") { const parsed = Number(args[index + 1]); if (!Number.isFinite(parsed) || parsed <= 0) { throw new Error(`Invalid interaction timeout: ${args[index + 1]}`); } options.interactionTimeoutMs = parsed; index += 1; continue; } if (value === "--interaction-poll-interval" || value === "--login-poll-interval") { const parsed = Number(args[index + 1]); if (!Number.isFinite(parsed) || parsed <= 0) { throw new Error(`Invalid interaction poll interval: ${args[index + 1]}`); } options.interactionPollIntervalMs = parsed; index += 1; continue; } if (value.startsWith("-")) { throw new Error(`Unknown option: ${value}`); } if (!options.url) { options.url = value; continue; } throw new Error(`Unexpected argument: ${value}`); } return options; } async function main(): Promise { try { const options = parseArgs(process.argv); if (options.help || !options.url) { console.log(HELP_TEXT); return; } await runConvertCommand(options); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(message); process.exitCode = 1; } } if (import.meta.main) { void main(); }