Skip to main content
Glama
joelmnz

Article Manager MCP Server

by joelmnz
import-articles.ts13.3 kB
#!/usr/bin/env bun /** * CLI utility for importing markdown articles to database * Usage: bun scripts/import-articles.ts <directory> [options] */ import { importService } from '../src/backend/services/import.js'; import { databaseInit } from '../src/backend/services/databaseInit.js'; import { readlineSync } from './utils/readline.js'; const commands = { validate: 'Validate import without making changes', preview: 'Show detailed preview of what would be imported', import: 'Import articles to database', stats: 'Show import statistics', } as const; interface CliOptions { command: keyof typeof commands; directory: string; preserveFolders?: boolean; useFilenameAsSlug?: boolean; conflictResolution?: 'skip' | 'rename' | 'overwrite'; batchSize?: number; dryRun?: boolean; } function parseArgs(): CliOptions { const args = process.argv.slice(2); if (args.length < 2) { showUsage(); process.exit(1); } const command = args[0] as keyof typeof commands; const directory = args[1]; if (!Object.keys(commands).includes(command)) { console.error(`Unknown command: ${command}`); showUsage(); process.exit(1); } const options: CliOptions = { command, directory, preserveFolders: args.includes('--preserve-folders'), useFilenameAsSlug: !args.includes('--use-title-slug'), // Default to filename dryRun: args.includes('--dry-run'), }; // Parse conflict resolution const conflictIndex = args.indexOf('--conflict'); if (conflictIndex >= 0 && conflictIndex + 1 < args.length) { const resolution = args[conflictIndex + 1]; if (['skip', 'rename', 'overwrite', 'interactive'].includes(resolution)) { options.conflictResolution = resolution as 'skip' | 'rename' | 'overwrite'; } } // Parse batch size const batchIndex = args.indexOf('--batch-size'); if (batchIndex >= 0 && batchIndex + 1 < args.length) { const batchSize = parseInt(args[batchIndex + 1], 10); if (!isNaN(batchSize) && batchSize > 0) { options.batchSize = batchSize; } } return options; } function showUsage() { console.log('Article Import CLI'); console.log('Usage: bun scripts/import-articles.ts <command> <directory> [options]'); console.log(''); console.log('Commands:'); Object.entries(commands).forEach(([cmd, desc]) => { console.log(` ${cmd.padEnd(10)} - ${desc}`); }); console.log(''); console.log('Options:'); console.log(' --preserve-folders Preserve directory structure as folders'); console.log(' --use-title-slug Generate slugs from titles instead of filenames'); console.log(' --conflict <action> How to handle conflicts: skip, rename, overwrite, interactive'); console.log(' --batch-size <n> Number of files to process per batch (default: 50)'); console.log(' --dry-run Show what would be imported without making changes'); console.log(''); console.log('Examples:'); console.log(' bun scripts/import-articles.ts validate ./my-articles'); console.log(' bun scripts/import-articles.ts preview ./my-articles --preserve-folders'); console.log(' bun scripts/import-articles.ts import ./my-articles --conflict interactive --dry-run'); console.log(' bun scripts/import-articles.ts import ./my-articles --preserve-folders --batch-size 25'); } async function validateCommand(options: CliOptions) { console.log(`Validating import from: ${options.directory}`); console.log('Options:', { preserveFolders: options.preserveFolders, useFilenameAsSlug: options.useFilenameAsSlug, }); console.log(''); const result = await importService.validateImport(options.directory, { preserveFolderStructure: options.preserveFolders, useFilenameAsSlug: options.useFilenameAsSlug, }); console.log('Validation Results:'); console.log(` Total files: ${result.totalFiles}`); console.log(` Valid: ${result.valid ? 'Yes' : 'No'}`); console.log(` Conflicts: ${result.conflicts.length}`); console.log(` Errors: ${result.errors.length}`); if (result.conflicts.length > 0) { console.log('\nConflicts:'); result.conflicts.forEach(conflict => { console.log(` - ${conflict.sourceFilename}: ${conflict.type} conflict`); console.log(` Existing: "${conflict.existingTitle}" (${conflict.existingSlug})`); console.log(` New: "${conflict.newTitle}" (${conflict.newSlug})`); }); } if (result.errors.length > 0) { console.log('\nErrors:'); result.errors.forEach(error => { console.log(` - ${error.sourceFilename}: ${error.error} (${error.type})`); }); } } async function previewCommand(options: CliOptions) { console.log(`Generating preview for: ${options.directory}`); console.log(''); const preview = await importService.getDetailedImportPreview(options.directory, { preserveFolderStructure: options.preserveFolders, useFilenameAsSlug: options.useFilenameAsSlug, }); console.log('Import Preview:'); console.log(` Total files: ${preview.summary.totalFiles}`); console.log(` Valid files: ${preview.summary.validFiles}`); console.log(` Conflicts: ${preview.summary.conflicts}`); console.log(` Errors: ${preview.summary.errors}`); console.log(''); if (preview.files.length > 0) { console.log('Files to import:'); preview.files.forEach(file => { const status = file.parseError ? '❌ ERROR' : file.hasConflict ? '⚠️ CONFLICT' : '✅ OK'; const folder = file.folder ? ` [${file.folder}]` : ' [root]'; console.log(` ${status} ${file.sourceFilename}`); console.log(` Title: "${file.title}"`); console.log(` Slug: ${file.slug}${folder}`); if (file.parseError) { console.log(` Error: ${file.parseError}`); } if (file.hasConflict) { console.log(` Conflict: ${file.conflictType} already exists`); } console.log(''); }); } } async function statsCommand(options: CliOptions) { console.log(`Getting statistics for: ${options.directory}`); console.log(''); const stats = await importService.getImportStats(options.directory, { preserveFolderStructure: options.preserveFolders, useFilenameAsSlug: options.useFilenameAsSlug, }); console.log('Import Statistics:'); console.log(` Total files found: ${stats.totalFiles}`); console.log(` Valid files: ${stats.validFiles}`); console.log(` Files with conflicts: ${stats.conflicts}`); console.log(` Files with errors: ${stats.errors}`); const successRate = stats.totalFiles > 0 ? ((stats.validFiles - stats.conflicts - stats.errors) / stats.totalFiles * 100).toFixed(1) : 0; console.log(` Success rate: ${successRate}%`); } async function importCommand(options: CliOptions) { const isDryRun = options.dryRun; console.log(`${isDryRun ? 'Dry run' : 'Importing'} from: ${options.directory}`); console.log('Options:', { preserveFolders: options.preserveFolders, useFilenameAsSlug: options.useFilenameAsSlug, conflictResolution: options.conflictResolution || 'interactive', batchSize: options.batchSize || 50, dryRun: isDryRun, }); console.log(''); // First, validate the import to check for conflicts console.log('Validating import...'); const validation = await importService.validateImport(options.directory, { preserveFolderStructure: options.preserveFolders, useFilenameAsSlug: options.useFilenameAsSlug, }); if (validation.errors.length > 0) { console.log('\n❌ Import validation failed:'); validation.errors.forEach(error => { console.log(` - ${error.sourceFilename}: ${error.error} (${error.type})`); }); const continueAnyway = await readlineSync.askYesNo( 'There are validation errors. Do you want to continue anyway?', false ); if (!continueAnyway) { console.log('Import cancelled.'); return; } } // Handle conflicts interactively if needed let conflictResolution = options.conflictResolution || 'interactive'; if (validation.conflicts.length > 0 && conflictResolution === 'interactive') { console.log(`\n⚠️ Found ${validation.conflicts.length} conflicts:`); validation.conflicts.forEach((conflict, index) => { console.log(`\n${index + 1}. ${conflict.sourceFilename}`); console.log(` Existing: "${conflict.existingTitle}" (${conflict.existingSlug})`); console.log(` New: "${conflict.newTitle}" (${conflict.newSlug})`); console.log(` Conflict type: ${conflict.type}`); }); console.log('\nHow would you like to handle these conflicts?'); const choice = await readlineSync.askChoice( 'Choose conflict resolution strategy:', [ 'Skip conflicting files (recommended for safety)', 'Overwrite existing articles (WARNING: will replace existing content)', 'Rename new articles (append number to title and slug)', 'Cancel import' ], 0 ); switch (choice) { case 0: conflictResolution = 'skip'; break; case 1: const confirmOverwrite = await readlineSync.confirmAction( 'overwrite existing articles', validation.conflicts.map(c => `${c.sourceFilename} → ${c.existingTitle}`), 'This will permanently replace existing article content!' ); if (!confirmOverwrite) { console.log('Import cancelled.'); return; } conflictResolution = 'overwrite'; break; case 2: conflictResolution = 'rename'; break; case 3: default: console.log('Import cancelled.'); return; } } // Final confirmation for non-dry-run imports if (!isDryRun) { const importDetails = [ `${validation.totalFiles} total files`, `${validation.totalFiles - validation.errors.length - validation.conflicts.length} will be imported`, `${validation.conflicts.length} conflicts (${conflictResolution})`, `${validation.errors.length} errors (will be skipped)` ]; const confirmed = await readlineSync.confirmAction( 'import articles to database', importDetails ); if (!confirmed) { console.log('Import cancelled.'); return; } } const startTime = Date.now(); const result = await importService.importFromDirectory(options.directory, { preserveFolderStructure: options.preserveFolders, useFilenameAsSlug: options.useFilenameAsSlug, conflictResolution: conflictResolution as 'skip' | 'overwrite', batchSize: options.batchSize || 50, dryRun: isDryRun, continueOnError: true, progressCallback: (progress) => { const percent = progress.total > 0 ? ((progress.processed / progress.total) * 100).toFixed(1) : '0.0'; console.log(` ${progress.phase}: ${progress.processed}/${progress.total} (${percent}%) - ${progress.currentFile}`); } }); const duration = ((Date.now() - startTime) / 1000).toFixed(2); console.log(''); console.log(`${isDryRun ? 'Dry run' : 'Import'} Results:`); console.log(` Imported: ${result.imported}`); console.log(` Skipped: ${result.skipped}`); console.log(` Conflicts: ${result.conflicts.length}`); console.log(` Errors: ${result.errors.length}`); console.log(` Duration: ${duration}s`); if (result.conflicts.length > 0) { console.log('\nConflicts:'); result.conflicts.forEach(conflict => { console.log(` - ${conflict.sourceFilename}: ${conflict.type} conflict with existing article`); }); } if (result.errors.length > 0) { console.log('\nErrors:'); result.errors.forEach(error => { console.log(` - ${error.sourceFilename}: ${error.error} (${error.type})`); }); } if (!isDryRun && result.imported > 0) { console.log(`\n✅ Successfully imported ${result.imported} articles to database`); } } async function main() { try { const options = parseArgs(); // Initialize database for all commands except validate (which might work without DB) if (options.command !== 'validate') { console.log('Initializing database connection...'); await databaseInit.initialize(); console.log(''); } switch (options.command) { case 'validate': await validateCommand(options); break; case 'preview': await previewCommand(options); break; case 'stats': await statsCommand(options); break; case 'import': await importCommand(options); break; default: console.error(`Unknown command: ${options.command}`); process.exit(1); } } catch (error) { console.error('\nCommand failed:', error instanceof Error ? error.message : error); process.exit(1); } finally { await databaseInit.shutdown(); } } // Graceful shutdown process.on('SIGINT', async () => { console.log('\nShutting down...'); await databaseInit.shutdown(); process.exit(0); }); process.on('SIGTERM', async () => { console.log('\nShutting down...'); await databaseInit.shutdown(); process.exit(0); }); // Run the CLI if (import.meta.main) { main(); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/joelmnz/mcp-markdown-manager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server