import chalk from 'chalk'; import type { Page, Space } from './confluence-client/types.js'; import type { SyncDiff } from './sync/sync-engine.js'; /** * Base formatter interface */ export interface Formatter { formatSpaces(spaces: Space[]): string; formatPages(pages: Page[], spaceKey: string): string; formatSyncDiff(diff: SyncDiff): string; formatStatus(status: StatusInfo): string; formatTree(nodes: TreeNode[], depth?: number): string; } export interface StatusInfo { connected: boolean; configured: boolean; initialized: boolean; confluenceUrl?: string; email?: string; spaceKey?: string; spaceName?: string; lastSync?: string; pageCount?: number; pendingChanges?: { added: number; modified: number; deleted: number; }; } export interface TreeNode { id: string; title: string; children: TreeNode[]; depth?: number; } /** * XML escape helper */ export function escapeXml(text: string): string { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } /** * Human-readable formatter with colors */ export class HumanFormatter implements Formatter { formatSpaces(spaces: Space[]): string { if (spaces.length === 0) { return chalk.yellow('No spaces found.'); } const lines = [chalk.bold('Spaces:'), '']; for (const space of spaces) { lines.push(` ${chalk.cyan(space.key)} - ${space.name}`); if (space.description?.plain?.value) { lines.push(chalk.gray(` ${space.description.plain.value.substring(0, 80)}...`)); } } return lines.join('\n'); } formatPages(pages: Page[], spaceKey: string): string { if (pages.length === 0) { return chalk.yellow(`No pages found in space ${spaceKey}.`); } const lines = [chalk.bold(`Pages in ${spaceKey}:`), '']; for (const page of pages) { lines.push(` ${chalk.cyan(page.id)} - ${page.title}`); } lines.push('', chalk.gray(`Total: ${pages.length} pages`)); return lines.join('\n'); } formatSyncDiff(diff: SyncDiff): string { const lines: string[] = []; if (diff.added.length > 0) { lines.push(chalk.green.bold('Added:')); for (const change of diff.added) { lines.push(chalk.green(` + ${change.title}`)); } lines.push(''); } if (diff.modified.length > 0) { lines.push(chalk.yellow.bold('Modified:')); for (const change of diff.modified) { lines.push(chalk.yellow(` ~ ${change.title}`)); } lines.push(''); } if (diff.deleted.length > 0) { lines.push(chalk.red.bold('Deleted:')); for (const change of diff.deleted) { lines.push(chalk.red(` - ${change.title}`)); } lines.push(''); } if (diff.added.length === 0 && diff.modified.length === 0 && diff.deleted.length === 0) { lines.push(chalk.gray('No changes detected.')); } else { lines.push( chalk.gray( `Summary: ${diff.added.length} added, ${diff.modified.length} modified, ${diff.deleted.length} deleted`, ), ); } return lines.join('\n'); } formatStatus(status: StatusInfo): string { const lines: string[] = []; if (!status.configured) { lines.push(chalk.red('Not configured.')); lines.push(chalk.gray('Run "cn setup" to configure Confluence credentials.')); return lines.join('\n'); } lines.push(chalk.bold('Configuration:')); lines.push(` URL: ${chalk.cyan(status.confluenceUrl || 'N/A')}`); lines.push(` Email: ${chalk.cyan(status.email || 'N/A')}`); lines.push(''); if (status.connected) { lines.push(chalk.green('✓ Connected to Confluence')); } else { lines.push(chalk.red('✗ Not connected')); } lines.push(''); if (!status.initialized) { lines.push(chalk.yellow('No space initialized in current directory.')); lines.push(chalk.gray('Run "cn sync --init " to initialize.')); } else { lines.push(chalk.bold('Space:')); lines.push(` Key: ${chalk.cyan(status.spaceKey || 'N/A')}`); lines.push(` Name: ${status.spaceName || 'N/A'}`); lines.push(` Last Sync: ${status.lastSync || 'Never'}`); lines.push(` Pages: ${status.pageCount || 0}`); if (status.pendingChanges) { const { added, modified, deleted } = status.pendingChanges; if (added > 0 || modified > 0 || deleted > 0) { lines.push(''); lines.push(chalk.bold('Pending Changes:')); if (added > 0) lines.push(chalk.green(` + ${added} new`)); if (modified > 0) lines.push(chalk.yellow(` ~ ${modified} modified`)); if (deleted > 0) lines.push(chalk.red(` - ${deleted} deleted`)); } } } return lines.join('\n'); } formatTree(nodes: TreeNode[], depth = 0): string { const lines: string[] = []; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; const isLast = i === nodes.length - 1; const prefix = depth === 0 ? '' : ' '.repeat(depth - 1) + (isLast ? '└── ' : '├── '); lines.push(`${prefix}${node.title}`); if (node.children.length > 0) { lines.push(this.formatTree(node.children, depth + 1)); } } return lines.join('\n'); } } /** * XML formatter for LLM consumption per ADR-0011 */ export class XmlFormatter implements Formatter { formatSpaces(spaces: Space[]): string { const lines = ['']; for (const space of spaces) { lines.push(` `); lines.push(` ${escapeXml(space.name)}`); if (space.description?.plain?.value) { lines.push(` ${escapeXml(space.description.plain.value)}`); } lines.push(' '); } lines.push(''); return lines.join('\n'); } formatPages(pages: Page[], spaceKey: string): string { const lines = [``]; for (const page of pages) { lines.push(` `); lines.push(` ${escapeXml(page.title)}`); if (page.parentId) { lines.push(` ${escapeXml(page.parentId)}`); } lines.push(' '); } lines.push(''); return lines.join('\n'); } formatSyncDiff(diff: SyncDiff): string { const lines = [ ``, ]; if (diff.added.length > 0) { lines.push(' '); for (const change of diff.added) { lines.push(` `); } lines.push(' '); } if (diff.modified.length > 0) { lines.push(' '); for (const change of diff.modified) { lines.push(` `); } lines.push(' '); } if (diff.deleted.length > 0) { lines.push(' '); for (const change of diff.deleted) { lines.push(` `); } lines.push(' '); } lines.push(''); return lines.join('\n'); } formatStatus(status: StatusInfo): string { const lines = [ ``, ]; if (status.configured) { lines.push(' '); if (status.confluenceUrl) lines.push(` ${escapeXml(status.confluenceUrl)}`); if (status.email) lines.push(` ${escapeXml(status.email)}`); lines.push(' '); } if (status.initialized) { lines.push(' '); if (status.spaceKey) lines.push(` ${escapeXml(status.spaceKey)}`); if (status.spaceName) lines.push(` ${escapeXml(status.spaceName)}`); if (status.lastSync) lines.push(` ${escapeXml(status.lastSync)}`); if (status.pageCount !== undefined) lines.push(` ${status.pageCount}`); lines.push(' '); if (status.pendingChanges) { const { added, modified, deleted } = status.pendingChanges; lines.push(` `); } } lines.push(''); return lines.join('\n'); } formatTree(nodes: TreeNode[], depth = 0): string { if (depth === 0) { const lines = ['']; lines.push(this.formatTreeNodes(nodes)); lines.push(''); return lines.join('\n'); } return this.formatTreeNodes(nodes); } private formatTreeNodes(nodes: TreeNode[]): string { const lines: string[] = []; for (const node of nodes) { if (node.children.length > 0) { lines.push(` `); lines.push(this.formatTreeNodes(node.children)); lines.push(' '); } else { lines.push(` `); } } return lines.join('\n'); } } /** * Get the appropriate formatter based on output mode */ export function getFormatter(xml: boolean): Formatter { return xml ? new XmlFormatter() : new HumanFormatter(); }