pulse/modules/formatter/terminal.formatter.ts
Edo Limburg c79eb6d76d Add CLI entry point, RSS content extraction, and image support
Features:
- Add CLI with commands: start, add, remove, list, fetch, status, items
- Auto-detect RSS format when adding feeds
- Auto-run database migrations on startup
- Extract full HTML content from RSS description field (NOS-style feeds)
- Extract image URLs from RSS enclosure tags
- Display images in terminal output with emoji
- Include imageUrl in JSON formatter output

Database:
- Add image_url column to feed_items table
- Update storage layer to persist imageUrl field

Tests:
- Add 10 CLI integration tests
- Add 3 RSS parser tests for image/content extraction
- Add 2 storage tests for imageUrl persistence

Dependencies:
- Add commander for CLI framework

All 144 tests passing
2026-05-05 23:05:30 +02:00

70 lines
1.8 KiB
TypeScript

import type { FeedItem } from '../../interfaces/feed.types.js';
export class TerminalFormatter {
format(items: FeedItem[]): string {
if (items.length === 0) {
return '\n No items to display.\n';
}
const lines: string[] = [];
lines.push('');
lines.push(` ${this.bold(`Found ${items.length} item${items.length === 1 ? '' : 's'}`)}`);
lines.push('');
items.forEach((item, index) => {
const number = `${index + 1}.`.padStart(3);
lines.push(` ${this.dim(number)} ${this.cyan(item.source)}`);
lines.push(` ${this.bold(item.title)}`);
lines.push(` ${this.dim(this.formatDate(item.publishedAt))}`);
lines.push(` ${this.blue(item.url)}`);
if (item.summary) {
const truncated = this.truncate(item.summary, 80);
lines.push(` ${this.dim(truncated)}`);
}
if (item.imageUrl) {
lines.push(` ${this.dim('📷')} ${this.dim(this.truncate(item.imageUrl, 70))}`);
}
lines.push('');
});
return lines.join('\n');
}
private formatDate(date: Date): string {
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
private truncate(text: string, maxLength: number): string {
if (text.length <= maxLength) {
return text;
}
return text.substring(0, maxLength - 3) + '...';
}
// ANSI color codes
private bold(text: string): string {
return `\x1b[1m${text}\x1b[0m`;
}
private dim(text: string): string {
return `\x1b[2m${text}\x1b[0m`;
}
private cyan(text: string): string {
return `\x1b[36m${text}\x1b[0m`;
}
private blue(text: string): string {
return `\x1b[34m${text}\x1b[0m`;
}
}