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
70 lines
1.8 KiB
TypeScript
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`;
|
|
}
|
|
}
|