diff --git a/CHANGELOG.md b/CHANGELOG.md index ce74d36..f12d8e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - `replies` and `thread` now support pagination (`--all`, `--max-pages`, `--cursor`, `--delay`) (#35) — thanks @crcatala. - Long-form article tweets now render rich Draft.js content blocks/entities (#36) — thanks @crcatala. - `news`/`trending` command for Explore tabs with AI-curated headlines (#39) — thanks @aavetis. +- `search` now supports pagination (`--all`, `--max-pages`, `--cursor`) (#42) — thanks @pjtf93. ### Changed - Library typing: `SearchResult` is now a discriminated union (so `error` only exists when `success: false`). diff --git a/src/commands/search.ts b/src/commands/search.ts index 326bf80..0461b7f 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -14,58 +14,70 @@ export function registerSearchCommands(program: Command, ctx: CliContext): void .option('--cursor ', 'Resume pagination from a cursor') .option('--json', 'Output as JSON') .option('--json-full', 'Output as JSON with full raw API response in _raw field') - .action(async (query: string, cmdOpts: { count?: string; all?: boolean; maxPages?: string; cursor?: string; json?: boolean; jsonFull?: boolean }) => { - const opts = program.opts(); - const timeoutMs = ctx.resolveTimeoutFromOptions(opts); - const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); - const count = Number.parseInt(cmdOpts.count || '10', 10); - const maxPages = cmdOpts.maxPages ? Number.parseInt(cmdOpts.maxPages, 10) : undefined; + .action( + async ( + query: string, + cmdOpts: { + count?: string; + all?: boolean; + maxPages?: string; + cursor?: string; + json?: boolean; + jsonFull?: boolean; + }, + ) => { + const opts = program.opts(); + const timeoutMs = ctx.resolveTimeoutFromOptions(opts); + const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); + const count = Number.parseInt(cmdOpts.count || '10', 10); + const maxPages = cmdOpts.maxPages ? Number.parseInt(cmdOpts.maxPages, 10) : undefined; - const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); + const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts); - for (const warning of warnings) { - console.error(`${ctx.p('warn')}${warning}`); - } + for (const warning of warnings) { + console.error(`${ctx.p('warn')}${warning}`); + } - if (!cookies.authToken || !cookies.ct0) { - console.error(`${ctx.p('err')}Missing required credentials`); - process.exit(1); - } + if (!cookies.authToken || !cookies.ct0) { + console.error(`${ctx.p('err')}Missing required credentials`); + process.exit(1); + } - const usePagination = cmdOpts.all || cmdOpts.cursor; - if (maxPages !== undefined && !usePagination) { - console.error(`${ctx.p('err')}--max-pages requires --all or --cursor.`); - process.exit(1); - } - if (!usePagination && (!Number.isFinite(count) || count <= 0)) { - console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`); - process.exit(1); - } - if (maxPages !== undefined && (!Number.isFinite(maxPages) || maxPages <= 0)) { - console.error(`${ctx.p('err')}Invalid --max-pages. Expected a positive integer.`); - process.exit(1); - } + const usePagination = cmdOpts.all || cmdOpts.cursor; + if (maxPages !== undefined && !usePagination) { + console.error(`${ctx.p('err')}--max-pages requires --all or --cursor.`); + process.exit(1); + } + if (!usePagination && (!Number.isFinite(count) || count <= 0)) { + console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`); + process.exit(1); + } + if (maxPages !== undefined && (!Number.isFinite(maxPages) || maxPages <= 0)) { + console.error(`${ctx.p('err')}Invalid --max-pages. Expected a positive integer.`); + process.exit(1); + } - const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); - const includeRaw = cmdOpts.jsonFull ?? false; - const searchOptions = { includeRaw }; - const paginationOptions = { includeRaw, maxPages, cursor: cmdOpts.cursor }; - const result = usePagination - ? await client.getAllSearchResults(query, paginationOptions) - : await client.search(query, count, searchOptions); + const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); + const includeRaw = cmdOpts.jsonFull ?? false; + const searchOptions = { includeRaw }; + const paginationOptions = { includeRaw, maxPages, cursor: cmdOpts.cursor }; + const result = usePagination + ? await client.getAllSearchResults(query, paginationOptions) + : await client.search(query, count, searchOptions); - if (result.success) { - const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull); - ctx.printTweetsResult(result, { - json: isJson, - usePagination: Boolean(usePagination), - emptyMessage: 'No tweets found.', - }); - } else { - console.error(`${ctx.p('err')}Search failed: ${result.error}`); - process.exit(1); - } - }); + if (result.success) { + const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull); + ctx.printTweetsResult(result, { + json: isJson, + usePagination: Boolean(usePagination), + emptyMessage: 'No tweets found.', + }); + } else { + console.error(`${ctx.p('err')}Search failed: ${result.error}`); + process.exit(1); + } + }, + ); program .command('mentions') diff --git a/src/lib/twitter-client-search.ts b/src/lib/twitter-client-search.ts index 6e8eb2f..5c87d17 100644 --- a/src/lib/twitter-client-search.ts +++ b/src/lib/twitter-client-search.ts @@ -68,7 +68,11 @@ export function withSearch> return this.searchPaged(query, Number.POSITIVE_INFINITY, options); } - private async searchPaged(query: string, limit: number, options: SearchPaginationOptions = {}): Promise { + private async searchPaged( + query: string, + limit: number, + options: SearchPaginationOptions = {}, + ): Promise { const features = buildSearchFeatures(); const pageSize = 20; const seen = new Set();