#!/usr/bin/env node import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); class ScriptInfo { static dir() { return path.dirname(__filename); } static cwd() { return process.cwd(); } } class ConfigLoader { static load(filename = 'config.jsonc') { const configPath = path.join(ScriptInfo.dir(), filename); if (!fs.existsSync(configPath)) { console.error(` ⚠ Konfigurationsdatei nicht gefunden: ${configPath}`); process.exit(1); } const raw = fs.readFileSync(configPath, 'utf8'); const clean = raw .split('\n') .filter(line => !line.trim().startsWith('//')) .join(''); return JSON.parse(clean); } } class ArgParser { static parse() { const argv = process.argv.slice(2); const result = { scan: null, ignore: [], reset: false, hilfe: false }; while (argv.length) { const arg = argv.shift(); if (!arg) break; if (arg === '-h' || arg === '--hilfe') { result.hilfe = true; } else if (arg === '--reset') { result.reset = true; } else if ((arg === '-s' || arg === '--scan') && argv.length) { result.scan = argv.shift().split(',').map(x => x.trim()).filter(Boolean); } else if ((arg === '-x' || arg === '--ignore') && argv.length) { result.ignore = argv.shift().split(',').map(x => x.trim()).filter(Boolean); } else { console.warn(` ⚠ Unbekannter Parameter: ${arg}`); result.hilfe = true; break; } } return result; } static showHelp() { console.log(` (C) 2025 - Adam Skotarczak (ionivation.com) 🛈 Tool das Verzeichnisse nach Markdown-Dateien durchsucht und diese an eine definierte Liste anhängt als Markdown-Link. Verwendung: ts-node link_collector.ts [OPTIONEN] -s, --scan Kommagetrennte Liste von Verzeichnissen zum Durchsuchen (relativ zum Aufrufpfad) -x, --ignore Kommagetrennte Liste von Verzeichnissen, die ignoriert werden sollen --reset Löscht das Logfile 'processed.log' und beendet sich -h, --hilfe Zeigt diese Hilfe Beispiel: ts-node link_collector.ts -s docs,notes -x "docs/alt" `); } } class FileScanner { static *find(rootDirs, ignoreDirs, extensions) { for (const root of rootDirs) { const absRoot = path.resolve(root); const stack = [absRoot]; while (stack.length) { const current = stack.pop(); const rel = path.relative(ScriptInfo.cwd(), current).replace(/\\/g, '/'); if (ignoreDirs.some(ignored => rel.startsWith(ignored))) continue; const entries = fs.readdirSync(current, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(current, entry.name); if (entry.isDirectory()) { stack.push(fullPath); } else if (extensions.some(ext => entry.name.endsWith(ext))) { yield fullPath; } } } } } } class TitleExtractor { static extract(filepath) { try { const content = fs.readFileSync(filepath, 'utf8'); for (const line of content.split('\n')) { if (line.trim().startsWith('#')) { return line.replace(/^#+/, '').trim(); } } } catch (e) { console.error(`⚠ Fehler beim Lesen von ${filepath}: ${e}`); } return path.basename(filepath, path.extname(filepath)); } } class ProcessedLog { constructor(filename) { this.logPath = path.join(ScriptInfo.dir(), filename); this.entries = new Set(); this.load(); } load() { if (!fs.existsSync(this.logPath)) return; const lines = fs.readFileSync(this.logPath, 'utf8').split('\n').map(x => x.trim()).filter(Boolean); this.entries = new Set(lines.map(p => p.replace(/\\/g, '/'))); } has(posixPath) { return this.entries.has(posixPath); } update(newPaths) { const content = newPaths.map(p => p.replace(/\\/g, '/')).join('\n') + '\n'; fs.appendFileSync(this.logPath, content, 'utf8'); } reset() { if (fs.existsSync(this.logPath)) { fs.unlinkSync(this.logPath); console.log('🧹 Logfile gelöscht:', this.logPath); } else { console.log('ℹ Logfile existierte nicht:', this.logPath); } } } class MarkdownAppender { static append(outputFile, links) { const content = links.join('\n') + '\n'; fs.appendFileSync(outputFile, content, 'utf8'); } } // Einstiegspunkt function main() { const config = ConfigLoader.load(); const args = ArgParser.parse(); if (args.hilfe) { ArgParser.showHelp(); return; } const log = new ProcessedLog(config.processed_log || 'processed.log'); if (args.reset) { log.reset(); return; } const cwd = ScriptInfo.cwd(); const outputFile = path.resolve(cwd, config.output_file || 'output.md'); const rootDirs = (args.scan || config.root_dirs || []).map(d => path.resolve(d)); const ignoreDirs = (args.ignore || []).map(d => path.relative(cwd, path.resolve(d)).replace(/\\/g, '/')); const extensions = config.extensions || ['.md']; const newLinks = []; const newProcessed = []; for (const absPath of FileScanner.find(rootDirs, ignoreDirs, extensions)) { const relPath = path.relative(cwd, absPath).replace(/\\/g, '/'); if (path.resolve(absPath) === outputFile || log.has(relPath)) continue; const title = TitleExtractor.extract(absPath); newLinks.push(`- [${title}](${relPath})`); newProcessed.push(relPath); } if (newLinks.length > 0) { MarkdownAppender.append(outputFile, newLinks); log.update(newProcessed); console.log(`✔ ${newLinks.length} neue Links hinzugefügt.`); } else { console.log(' ℹ Keine neuen Dateien gefunden.'); } } main();