diff --git a/README.md b/README.md index 8338137..835c9b5 100644 --- a/README.md +++ b/README.md @@ -369,6 +369,9 @@ confluence search "search term" # Limit results confluence search "search term" --limit 5 + +# Paginate results +confluence search "search term" --limit 5 --start 5 ``` ### List or Download Attachments @@ -697,7 +700,7 @@ confluence stats | `init` | Initialize CLI configuration | `--read-only` | | `read ` | Read page content | `--format ` | | `info ` | Get page information | `--format ` | -| `search ` | Search for pages | `--limit ` | +| `search ` | Search for pages | `--limit `, `--start ` | | `spaces` | List available spaces | `--limit `, `--all` | | `find ` | Find a page by its title | `--space <spaceKey>` | | `children <pageId>` | List child pages of a page | `--recursive`, `--max-depth <number>`, `--format <list\|tree\|json>`, `--show-url`, `--show-id` | diff --git a/bin/confluence.js b/bin/confluence.js index 5c2d178..782a3ca 100755 --- a/bin/confluence.js +++ b/bin/confluence.js @@ -166,9 +166,15 @@ program .command('search <query>') .description('Search for Confluence pages') .option('-l, --limit <limit>', 'Limit number of results', '10') + .option('--start <start>', 'Start index for results', '0') .option('--cql', 'Pass query as raw CQL instead of text search') .action(withClient('search', async ({ client, analytics }, query, options) => { - const results = await client.search(query, parseInt(options.limit), options.cql); + const start = parseInt(options.start, 10); + if (Number.isNaN(start) || start < 0) { + throw new Error('Start must be a non-negative number.'); + } + + const results = await client.search(query, parseInt(options.limit), options.cql, start); if (results.length === 0) { console.log(chalk.yellow('No results found.')); @@ -178,7 +184,7 @@ program console.log(chalk.blue(`Found ${results.length} results:`)); results.forEach((result, index) => { - console.log(`${index + 1}. ${chalk.green(result.title)} (ID: ${result.id})`); + console.log(`${start + index + 1}. ${chalk.green(result.title)} (ID: ${result.id})`); if (result.excerpt) { console.log(` ${chalk.gray(result.excerpt)}`); } diff --git a/lib/confluence-client.js b/lib/confluence-client.js index 1a0f60d..7668f12 100644 --- a/lib/confluence-client.js +++ b/lib/confluence-client.js @@ -415,12 +415,13 @@ class ConfluenceClient { /** * Search for pages */ - async search(query, limit = 10, rawCql = false) { + async search(query, limit = 10, rawCql = false, start = 0) { const cql = rawCql ? query : `text ~ "${this.escapeCql(query)}"`; const response = await this.client.get('/search', { params: { cql, - limit: limit + limit, + start } }); diff --git a/tests/cli-entry.test.js b/tests/cli-entry.test.js index f8ff53d..69105cf 100644 --- a/tests/cli-entry.test.js +++ b/tests/cli-entry.test.js @@ -16,6 +16,16 @@ describe('CLI entry point', () => { ).trim(); expect(output).toMatch(/^\d+\.\d+\.\d+$/); }); + + test('search --help documents start pagination option', () => { + const output = execFileSync( + process.execPath, + [path.resolve(__dirname, '../bin/index.js'), 'search', '--help'], + { encoding: 'utf8' } + ); + expect(output).toContain('--start <start>'); + expect(output).toContain('Start index for results'); + }); }); describe('create/create-child --type validation', () => { diff --git a/tests/confluence-client.test.js b/tests/confluence-client.test.js index 640d157..d7565cb 100644 --- a/tests/confluence-client.test.js +++ b/tests/confluence-client.test.js @@ -1134,6 +1134,18 @@ describe('ConfluenceClient', () => { mock.restore(); }); + test('should respect start parameter', async () => { + const mock = new MockAdapter(client.client); + mock.onGet('/search').reply((config) => { + expect(config.params.start).toBe(20); + return [200, { results: [] }]; + }); + + await client.search('test', 10, false, 20); + + mock.restore(); + }); + test('should escape backslashes before double quotes', async () => { const mock = new MockAdapter(client.client); mock.onGet('/search').reply((config) => {