Skip to content

Commit 026b97c

Browse files
authored
Merge pull request #91 from 0xMassi/fix/urls-file-single-row
fix(cli): --urls-file with a single row now works (#86)
2 parents 63056a8 + 36cde1d commit 026b97c

1 file changed

Lines changed: 58 additions & 17 deletions

File tree

crates/webclaw-cli/src/main.rs

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,18 @@ fn collect_urls(cli: &Cli) -> Result<Vec<(String, Option<String>)>, String> {
864864
Ok(entries)
865865
}
866866

867+
/// When `--urls-file` supplies exactly one URL and no positional URL was given,
868+
/// treat it like a single positional URL so a one-line file behaves identically
869+
/// to `webclaw <url>` (stdout, `--output-dir`, cloud fallback, `--raw-html`,
870+
/// `--diff`, LLM paths). Without this the lone file URL gives `entries.len() == 1`,
871+
/// which the batch gates skip, and the single path then finds an empty `cli.urls`
872+
/// and errors "no input provided". See issue #86.
873+
fn backfill_single_file_url(urls: &mut Vec<String>, entries: &[(String, Option<String>)]) {
874+
if urls.is_empty() && entries.len() == 1 {
875+
urls.push(entries[0].0.clone());
876+
}
877+
}
878+
867879
/// Result that can be either a local extraction or a cloud API JSON response.
868880
enum FetchOutput {
869881
Local(Box<ExtractionResult>),
@@ -2633,7 +2645,7 @@ async fn run_research(cli: &Cli, query: &str) -> Result<(), String> {
26332645
async fn main() {
26342646
dotenvy::dotenv().ok();
26352647

2636-
let cli = Cli::parse();
2648+
let mut cli = Cli::parse();
26372649
init_logging(cli.verbose);
26382650

26392651
// Subcommand path. Handled before the flag dispatch so a subcommand
@@ -2776,6 +2788,20 @@ async fn main() {
27762788
return;
27772789
}
27782790

2791+
// Collect URLs from args + --urls-file up front, and backfill a lone
2792+
// --urls-file URL into cli.urls so a one-line file behaves like a positional
2793+
// URL across ALL modes below (crawl / watch / diff / brand / batch / single).
2794+
// See issue #86 — the mode gates and single path read cli.urls, which is
2795+
// empty when the URL came only from --urls-file.
2796+
let entries = match collect_urls(&cli) {
2797+
Ok(u) => u,
2798+
Err(e) => {
2799+
eprintln!("error: {e}");
2800+
process::exit(1);
2801+
}
2802+
};
2803+
backfill_single_file_url(&mut cli.urls, &entries);
2804+
27792805
// --crawl: recursive crawl mode
27802806
if cli.crawl {
27812807
if let Err(e) = run_crawl(&cli).await {
@@ -2787,13 +2813,7 @@ async fn main() {
27872813

27882814
// --watch: poll URL(s) for changes
27892815
if cli.watch {
2790-
let watch_urls: Vec<String> = match collect_urls(&cli) {
2791-
Ok(entries) => entries.into_iter().map(|(url, _)| url).collect(),
2792-
Err(e) => {
2793-
eprintln!("error: {e}");
2794-
process::exit(1);
2795-
}
2796-
};
2816+
let watch_urls: Vec<String> = entries.iter().map(|(url, _)| url.clone()).collect();
27972817
if let Err(e) = run_watch(&cli, &watch_urls).await {
27982818
eprintln!("error: {e}");
27992819
process::exit(1);
@@ -2828,15 +2848,6 @@ async fn main() {
28282848
return;
28292849
}
28302850

2831-
// Collect all URLs from args + --urls-file
2832-
let entries = match collect_urls(&cli) {
2833-
Ok(u) => u,
2834-
Err(e) => {
2835-
eprintln!("error: {e}");
2836-
process::exit(1);
2837-
}
2838-
};
2839-
28402851
// LLM modes: --extract-json, --extract-prompt, --summarize
28412852
// When multiple URLs are provided, run batch LLM extraction over all of them.
28422853
if has_llm_flags(&cli) {
@@ -2908,6 +2919,36 @@ mod tests {
29082919
use super::*;
29092920
use webclaw_core::Content;
29102921

2922+
// issue #86: a single URL sourced only from --urls-file must be promoted to
2923+
// a positional URL so it takes the single-scrape path (batch gates need >1).
2924+
#[test]
2925+
fn single_file_url_backfilled_into_positional() {
2926+
let mut urls: Vec<String> = Vec::new();
2927+
let entries = vec![("https://example.com".to_string(), None)];
2928+
backfill_single_file_url(&mut urls, &entries);
2929+
assert_eq!(urls, vec!["https://example.com".to_string()]);
2930+
}
2931+
2932+
#[test]
2933+
fn positional_urls_left_untouched() {
2934+
let mut urls = vec!["https://a.com".to_string()];
2935+
let entries = vec![("https://a.com".to_string(), None)];
2936+
backfill_single_file_url(&mut urls, &entries);
2937+
assert_eq!(urls, vec!["https://a.com".to_string()]);
2938+
}
2939+
2940+
#[test]
2941+
fn multiple_file_urls_not_backfilled() {
2942+
// 2+ URLs already route to run_batch, so cli.urls stays empty here.
2943+
let mut urls: Vec<String> = Vec::new();
2944+
let entries = vec![
2945+
("https://a.com".to_string(), None),
2946+
("https://b.com".to_string(), None),
2947+
];
2948+
backfill_single_file_url(&mut urls, &entries);
2949+
assert!(urls.is_empty());
2950+
}
2951+
29112952
fn empty_result(title: Option<&str>, url: Option<&str>, markdown: &str) -> ExtractionResult {
29122953
ExtractionResult {
29132954
metadata: Metadata {

0 commit comments

Comments
 (0)