import { Injectable, Logger } from '@nestjs/common'; import * as fs from 'fs'; import * as path from 'path'; export interface HelpArticle { id: string; title: string | Record; description?: string | Record; } export interface HelpArticlesJson { articles: HelpArticle[]; } @Injectable() export class HelpService { private readonly logger = new Logger(HelpService.name); private readonly helpDir: string; private cachedArticles: HelpArticle[] | null = null; constructor() { this.helpDir = path.resolve(process.cwd(), 'help'); } getArticles(): HelpArticle[] { if (this.cachedArticles) return this.cachedArticles; if (!fs.existsSync(this.helpDir)) { this.logger.warn(`Help directory not found: ${this.helpDir}`); this.cachedArticles = []; return this.cachedArticles; } // Try articles.json first const jsonPath = path.join(this.helpDir, 'articles.json'); if (fs.existsSync(jsonPath)) { try { const raw = fs.readFileSync(jsonPath, 'utf-8'); const parsed: HelpArticlesJson = JSON.parse(raw); if (parsed.articles && Array.isArray(parsed.articles)) { this.cachedArticles = parsed.articles; return this.cachedArticles; } } catch (e: any) { this.logger.error(`Failed to parse articles.json: ${e.message}`); } } // Fallback: auto-discover from .md files return this.discoverArticlesFromFiles(); } getArticleContent(id: string, lang: string): string | null { if (!fs.existsSync(this.helpDir)) return null; // Try lang-specific file first: help/{id}.{lang}.md const langPath = path.join(this.helpDir, `${id}.${lang}.md`); if (fs.existsSync(langPath)) { return fs.readFileSync(langPath, 'utf-8'); } // Fallback to .en.md const enPath = path.join(this.helpDir, `${id}.en.md`); if (fs.existsSync(enPath)) { return fs.readFileSync(enPath, 'utf-8'); } // Fallback to plain {id}.md const plainPath = path.join(this.helpDir, `${id}.md`); if (fs.existsSync(plainPath)) { return fs.readFileSync(plainPath, 'utf-8'); } // Fallback to any matching file try { const files = fs.readdirSync(this.helpDir); const match = files.find(f => f.startsWith(`${id}.`) && f.endsWith('.md')); if (match) { return fs.readFileSync(path.join(this.helpDir, match), 'utf-8'); } } catch (e) {} return null; } clearCache(): void { this.cachedArticles = null; } private discoverArticlesFromFiles(): HelpArticle[] { try { const files = fs.readdirSync(this.helpDir).filter(f => f.endsWith('.md')); const seen = new Set(); const articles: HelpArticle[] = []; for (const file of files) { // Pattern: {id}.{lang}.md or {id}.md const baseName = file.replace(/\.md$/, ''); const parts = baseName.split('.'); let id: string; if (parts.length >= 2 && parts[parts.length - 1].length === 2) { // Has language suffix: getting-started.en.md id = parts.slice(0, -1).join('.'); } else { // Plain name: getting-started.md id = baseName; } if (seen.has(id)) continue; seen.add(id); // Extract title from first h1 const content = fs.readFileSync(path.join(this.helpDir, file), 'utf-8'); const titleMatch = content.match(/^#\s+(.+)$/m); const title = titleMatch ? titleMatch[1].trim() : id; const descMatch = content.match(/^>\s+(.+)$/m); const description = descMatch ? descMatch[1].trim() : undefined; articles.push({ id, title, ...(description ? { description } : {}) }); } return articles; } catch (e: any) { this.logger.error(`Failed to discover help articles: ${e.message}`); return []; } } }