diff --git a/bin/confluence.js b/bin/confluence.js index e4d24f9..2623843 100755 --- a/bin/confluence.js +++ b/bin/confluence.js @@ -8,11 +8,6 @@ const { getConfig, initConfig, listProfiles, setActiveProfile, deleteProfile, is const Analytics = require('../lib/analytics'); const pkg = require('../package.json'); -function buildPageUrl(config, path) { - const protocol = config.protocol || 'https'; - return `${protocol}://${config.domain}${path}`; -} - function assertWritable(config) { if (config.readOnly) { console.error(chalk.red('Error: This profile is in read-only mode. Write operations are not allowed.')); @@ -240,8 +235,8 @@ program console.log(`Title: ${chalk.blue(result.title)}`); console.log(`ID: ${chalk.blue(result.id)}`); console.log(`Space: ${chalk.blue(result.space.name)} (${result.space.key})`); - console.log(`URL: ${chalk.gray(`${buildPageUrl(config, `/wiki${result._links.webui}`)}`)}`); - + console.log(`URL: ${chalk.gray(`${client.buildUrl(`${client.webUrlPrefix}${result._links.webui}`)}`)}`); + analytics.track('create', true); } catch (error) { analytics.track('create', false); @@ -289,8 +284,8 @@ program console.log(`ID: ${chalk.blue(result.id)}`); console.log(`Parent: ${chalk.blue(parentInfo.title)} (${parentId})`); console.log(`Space: ${chalk.blue(result.space.name)} (${result.space.key})`); - console.log(`URL: ${chalk.gray(`${buildPageUrl(config, `/wiki${result._links.webui}`)}`)}`); - + console.log(`URL: ${chalk.gray(`${client.buildUrl(`${client.webUrlPrefix}${result._links.webui}`)}`)}`); + analytics.track('create_child', true); } catch (error) { analytics.track('create_child', false); @@ -337,8 +332,8 @@ program console.log(`Title: ${chalk.blue(result.title)}`); console.log(`ID: ${chalk.blue(result.id)}`); console.log(`Version: ${chalk.blue(result.version.number)}`); - console.log(`URL: ${chalk.gray(`${buildPageUrl(config, `/wiki${result._links.webui}`)}`)}`); - + console.log(`URL: ${chalk.gray(`${client.buildUrl(`${client.webUrlPrefix}${result._links.webui}`)}`)}`); + analytics.track('update', true); } catch (error) { analytics.track('update', false); @@ -365,7 +360,7 @@ program console.log(`ID: ${chalk.blue(result.id)}`); console.log(`New Parent: ${chalk.blue(newParentId)}`); console.log(`Version: ${chalk.blue(result.version.number)}`); - console.log(`URL: ${chalk.gray(`${buildPageUrl(config, `/wiki${result._links.webui}`)}`)}`); + console.log(`URL: ${chalk.gray(`${client.buildUrl(`${client.webUrlPrefix}${result._links.webui}`)}`)}`); analytics.track('move', true); } catch (error) { @@ -473,8 +468,8 @@ program console.log(`Title: ${chalk.green(pageInfo.title)}`); console.log(`ID: ${chalk.green(pageInfo.id)}`); console.log(`Space: ${chalk.green(pageInfo.space.name)} (${pageInfo.space.key})`); - console.log(`URL: ${chalk.gray(`${buildPageUrl(config, `/wiki${pageInfo.url}`)}`)}`); - + console.log(`URL: ${chalk.gray(`${client.buildUrl(`${client.webUrlPrefix}${pageInfo.url}`)}`)}`); + analytics.track('find', true); } catch (error) { analytics.track('find', false); @@ -1713,7 +1708,7 @@ program console.log(` - ...and ${result.failures.length - 10} more`); } } - console.log(`URL: ${chalk.gray(`${buildPageUrl(config, `/wiki${result.rootPage._links.webui}`)}`)}`); + console.log(`URL: ${chalk.gray(`${client.buildUrl(`${client.webUrlPrefix}${result.rootPage._links.webui}`)}`)}`); if (options.failOnError && result.failures?.length) { analytics.track('copy_tree', false); console.error(chalk.red('Completed with failures and --fail-on-error is set.')); @@ -1775,7 +1770,7 @@ program type: page.type, status: page.status, spaceKey: page.space?.key, - url: `${buildPageUrl(config, `/wiki/spaces/${page.space?.key}/pages/${page.id}`)}`, + url: `${client.buildUrl(`${client.webUrlPrefix}/spaces/${page.space?.key}/pages/${page.id}`)}`, parentId: page.parentId || resolvedPageId })) }; @@ -1787,7 +1782,7 @@ program // Build tree structure const tree = buildTree(children, resolvedPageId); - printTree(tree, config, options, 1); + printTree(tree, client, config, options, 1); console.log(''); console.log(chalk.gray(`Total: ${children.length} child page${children.length === 1 ? '' : 's'}`)); @@ -1804,7 +1799,7 @@ program } if (options.showUrl) { - const url = `${buildPageUrl(config, `/wiki/spaces/${page.space?.key}/pages/${page.id}`)}`; + const url = `${client.buildUrl(`${client.webUrlPrefix}/spaces/${page.space?.key}/pages/${page.id}`)}`; output += `\n ${chalk.gray(url)}`; } @@ -1856,7 +1851,7 @@ function buildTree(pages, rootId) { } // Helper function to print tree -function printTree(nodes, config, options, depth = 1) { +function printTree(nodes, client, config, options, depth = 1) { nodes.forEach((node, index) => { const isLast = index === nodes.length - 1; const indent = ' '.repeat(depth - 1); @@ -1869,14 +1864,14 @@ function printTree(nodes, config, options, depth = 1) { } if (options.showUrl) { - const url = `${buildPageUrl(config, `/wiki/spaces/${node.space?.key}/pages/${node.id}`)}`; + const url = `${client.buildUrl(`${client.webUrlPrefix}/spaces/${node.space?.key}/pages/${node.id}`)}`; output += `\n${indent}${isLast ? ' ' : '│ '}${chalk.gray(url)}`; } console.log(output); - + if (node.children && node.children.length > 0) { - printTree(node.children, config, options, depth + 1); + printTree(node.children, client, config, options, depth + 1); } }); } diff --git a/lib/confluence-client.js b/lib/confluence-client.js index f3a2e65..3239abf 100644 --- a/lib/confluence-client.js +++ b/lib/confluence-client.js @@ -35,6 +35,7 @@ class ConfluenceClient { this.email = config.email; this.authType = (config.authType || (this.email ? 'basic' : 'bearer')).toLowerCase(); this.apiPath = this.sanitizeApiPath(config.apiPath); + this.webUrlPrefix = this.apiPath.startsWith('/wiki/') ? '/wiki' : ''; this.baseURL = `${this.protocol}://${this.domain}${this.apiPath}`; this.markdown = new MarkdownIt(); this.setupConfluenceMarkdownExtensions(); @@ -412,7 +413,7 @@ class ConfluenceClient { const webui = page._links?.webui || ''; return { title: page.title, - url: webui ? this.buildUrl(`/wiki${webui}`) : '' + url: webui ? this.buildUrl(`${this.webUrlPrefix}${webui}`) : '' }; } return null; @@ -506,7 +507,7 @@ class ConfluenceClient { // Format: - [Page Title](URL) const childPagesList = childPages.map(page => { const webui = page._links?.webui || ''; - const url = webui ? this.buildUrl(`/wiki${webui}`) : ''; + const url = webui ? this.buildUrl(`${this.webUrlPrefix}${webui}`) : ''; if (url) { return `- [${page.title}](${url})`; } else { @@ -1351,11 +1352,11 @@ class ConfluenceClient { // Try to build a proper URL - if spaceKey starts with ~, it's a user space if (spaceKey.startsWith('~')) { const spacePath = `display/${spaceKey}/${encodeURIComponent(title)}`; - return `\n> 📄 **${labels.includePage}**: [${title}](${this.buildUrl(`/wiki/${spacePath}`)})\n`; + return `\n> 📄 **${labels.includePage}**: [${title}](${this.buildUrl(`${this.webUrlPrefix}/${spacePath}`)})\n`; } else { // For non-user spaces, we cannot construct a valid link without the page ID. // Document that manual correction is required. - return `\n> 📄 **${labels.includePage}**: [${title}](${this.buildUrl(`/wiki/spaces/${spaceKey}/pages/[PAGE_ID_HERE]`)}) _(manual link correction required)_\n`; + return `\n> 📄 **${labels.includePage}**: [${title}](${this.buildUrl(`${this.webUrlPrefix}/spaces/${spaceKey}/pages/[PAGE_ID_HERE]`)}) _(manual link correction required)_\n`; } }); diff --git a/tests/confluence-client.test.js b/tests/confluence-client.test.js index 9e8b921..83b99e1 100644 --- a/tests/confluence-client.test.js +++ b/tests/confluence-client.test.js @@ -126,6 +126,45 @@ describe('ConfluenceClient', () => { expect(customClient.baseURL).toBe('https://cloud.example/wiki/rest/api'); }); + + test('sets webUrlPrefix to /wiki when apiPath starts with /wiki/', () => { + const cloudClient = new ConfluenceClient({ + domain: 'test.atlassian.net', + token: 'cloud-token', + apiPath: '/wiki/rest/api' + }); + + expect(cloudClient.webUrlPrefix).toBe('/wiki'); + }); + + test('sets webUrlPrefix to empty string when apiPath does not start with /wiki/', () => { + const serverClient = new ConfluenceClient({ + domain: 'confluence.example.com', + token: 'server-token', + apiPath: '/rest/api' + }); + + expect(serverClient.webUrlPrefix).toBe(''); + }); + + test('sets webUrlPrefix to empty string when apiPath is not provided', () => { + const defaultClient = new ConfluenceClient({ + domain: 'example.com', + token: 'default-token' + }); + + expect(defaultClient.webUrlPrefix).toBe(''); + }); + + test('sets webUrlPrefix to /wiki when apiPath is wiki/rest/api/ (missing leading slash)', () => { + const clientWithMissingSlash = new ConfluenceClient({ + domain: 'confluence.example.com', + token: 'test-token', + apiPath: 'wiki/rest/api/' + }); + + expect(clientWithMissingSlash.webUrlPrefix).toBe('/wiki'); + }); }); describe('authentication setup', () => {