npm-logger.js
1 #!/usr/bin/env node 2 3 /** 4 * NPM script wrapper with logging 5 * Runs any command and logs output to daily log files 6 * 7 * Usage: node scripts/npm-logger.js <script-name> <command> [args...] 8 * Example: node scripts/npm-logger.js keywords node src/cli/keywords.js --limit 10 9 */ 10 11 import { spawn } from 'child_process'; 12 import fs from 'fs'; 13 import path from 'path'; 14 15 // Parse arguments 16 const args = process.argv.slice(2); 17 if (args.length < 2) { 18 console.error('Usage: npm-logger.js <script-name> <command> [args...]'); 19 process.exit(1); 20 } 21 22 const scriptName = args[0]; 23 const command = args[1]; 24 const commandArgs = args.slice(2); 25 26 // Map script names to consolidated domain log files 27 function getLogDomain(scriptName) { 28 const domainMap = { 29 // Pipeline stages 30 keywords: 'pipeline', 31 serps: 'pipeline', 32 assets: 'pipeline', 33 scoring: 'pipeline', 34 score: 'pipeline', 35 rescoring: 'pipeline', 36 rescore: 'pipeline', 37 enrich: 'pipeline', 38 proposals: 'pipeline', 39 40 // Outreach channels 41 outreach: 'outreach', 42 'outreach-sms': 'outreach', 43 'outreach-email': 'outreach', 44 'outreach-form': 'outreach', 45 'outreach-x': 'outreach', 46 'outreach-linkedin': 'outreach', 47 sms: 'outreach', 48 email: 'outreach', 49 form: 'outreach', 50 51 // Inbound processing 52 replies: 'inbound', 53 'inbound-sms': 'inbound', 54 'inbound-email': 'inbound', 55 56 // Dashboard (all pages) 57 dashboard: 'dashboard', 58 'dashboard.overview': 'dashboard', 59 'dashboard.pipeline': 'dashboard', 60 'dashboard.pipeline_health': 'dashboard', 61 'dashboard.outreach': 'dashboard', 62 'dashboard.conversations': 'dashboard', 63 'dashboard.compliance': 'dashboard', 64 'dashboard.coverage': 'dashboard', 65 'dashboard.system_health': 'dashboard', 66 'dashboard.cron_jobs': 'dashboard', 67 68 // Cron jobs 69 cron: 'cron', 70 'cron-list': 'cron', 71 'daily-log-rotation': 'cron', 72 'weekly-repricing': 'cron', 73 'sync-email-events': 'cron', 74 'sync-unsubscribes': 'cron', 75 76 // Utilities 77 dedupe: 'utils', 78 'dedupe-locale': 'utils', 79 backfill: 'utils', 80 'image-optimizer': 'utils', 81 'error-handler': 'utils', 82 'keyword-manager': 'utils', 83 'site-filters': 'utils', 84 85 // Tests 86 test: 'tests', 87 'test:unit': 'tests', 88 'test:integration': 'tests', 89 'test:watch': 'tests', 90 91 // All other pipeline stages 92 all: 'pipeline', 93 poc: 'pipeline', 94 mvp: 'pipeline', 95 process: 'pipeline', 96 }; 97 98 // Return mapped domain or lowercase script name as fallback 99 return domainMap[scriptName.toLowerCase()] || scriptName.toLowerCase(); 100 } 101 102 // Setup log directory 103 const logDir = './logs'; 104 if (!fs.existsSync(logDir)) { 105 fs.mkdirSync(logDir, { recursive: true }); 106 } 107 108 // Generate log filename with domain and date 109 const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD 110 const logDomain = getLogDomain(scriptName); 111 const logFile = path.join(logDir, `${logDomain}-${date}.log`); 112 113 // Open log file in append mode 114 const logStream = fs.createWriteStream(logFile, { flags: 'a' }); 115 116 // Helper to write log lines 117 function logLine(level, message) { 118 const timestamp = new Date().toISOString(); 119 const line = `[${timestamp}] [${scriptName}] [${level}] ${message}\n`; 120 logStream.write(line); 121 } 122 123 // Write start marker 124 logStream.write('\n'); 125 logLine('INFO', '=========================================='); 126 logLine('INFO', `Command: ${command} ${commandArgs.join(' ')}`); 127 logLine('INFO', `PID: ${process.pid}`); 128 logLine('INFO', `CWD: ${process.cwd()}`); 129 logLine('INFO', '=========================================='); 130 131 // Spawn the command 132 const child = spawn(command, commandArgs, { 133 stdio: ['inherit', 'pipe', 'pipe'], 134 shell: false, 135 }); 136 137 // Capture stdout 138 child.stdout.on('data', data => { 139 const output = data.toString(); 140 // Write to console 141 process.stdout.write(output); 142 // Write to log file with timestamp 143 const lines = output.split('\n').filter(l => l.length > 0); 144 lines.forEach(line => { 145 const timestamp = new Date().toISOString(); 146 logStream.write(`[${timestamp}] [${scriptName}] [OUTPUT] ${line}\n`); 147 }); 148 }); 149 150 // Capture stderr 151 child.stderr.on('data', data => { 152 const output = data.toString(); 153 // Write to console 154 process.stderr.write(output); 155 // Write to log file with timestamp 156 const lines = output.split('\n').filter(l => l.length > 0); 157 lines.forEach(line => { 158 const timestamp = new Date().toISOString(); 159 logStream.write(`[${timestamp}] [${scriptName}] [STDERR] ${line}\n`); 160 }); 161 }); 162 163 // Handle process exit 164 child.on('close', code => { 165 logLine('INFO', '=========================================='); 166 if (code === 0) { 167 logLine('SUCCESS', 'Completed successfully'); 168 } else { 169 logLine('ERROR', `Failed with exit code: ${code}`); 170 } 171 logLine('INFO', '=========================================='); 172 logStream.write('\n'); 173 174 logStream.end(() => { 175 process.exit(code); 176 }); 177 }); 178 179 // Handle errors 180 child.on('error', err => { 181 logLine('ERROR', `Failed to start command: ${err.message}`); 182 logStream.end(() => { 183 process.exit(1); 184 }); 185 }); 186 187 // Handle SIGINT (Ctrl+C) gracefully 188 process.on('SIGINT', () => { 189 logLine('WARN', 'Received SIGINT, terminating child process'); 190 child.kill('SIGINT'); 191 }); 192 193 // Handle SIGTERM gracefully 194 process.on('SIGTERM', () => { 195 logLine('WARN', 'Received SIGTERM, terminating child process'); 196 child.kill('SIGTERM'); 197 });