/ scripts / npm-logger.js
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  });