/ src / all.js
all.js
  1  #!/usr/bin/env node
  2  
  3  /**
  4   * All Stages Orchestrator
  5   * Runs the complete pipeline from keywords to replies
  6   */
  7  
  8  import { runKeywordsStage } from './stages/keywords.js';
  9  import { runSerpsStage } from './stages/serps.js';
 10  import { runAssetsStage } from './stages/assets.js';
 11  import { runScoringStage } from './stages/scoring.js';
 12  import { runRescoringStage } from './stages/rescoring.js';
 13  import { runEnrichmentStage } from './stages/enrich.js';
 14  import { runProposalsStage } from './stages/proposals.js';
 15  import { runOutreachStage } from './stages/outreach.js';
 16  import { runFollowupGenerationStage } from './stages/followup-generator.js';
 17  import { runRepliesStage } from './stages/replies.js';
 18  import { parseFlags } from './utils/flag-parser.js';
 19  import { generatePipelineCompletion } from './utils/summary-generator.js';
 20  import Logger from './utils/logger.js';
 21  
 22  const logger = new Logger('all');
 23  
 24  /**
 25   * Pipeline stages in execution order
 26   */
 27  const STAGES = [
 28    { name: 'keywords', fn: runKeywordsStage, description: 'Keyword selection and prioritization' },
 29    { name: 'serps', fn: runSerpsStage, description: 'SERP scraping for selected keywords' },
 30    { name: 'assets', fn: runAssetsStage, description: 'Screenshot capture for sites' },
 31    { name: 'scoring', fn: runScoringStage, description: 'Initial AI conversion scoring' },
 32    {
 33      name: 'rescoring',
 34      fn: runRescoringStage,
 35      description: 'Rescore low-scoring sites (B- and below)',
 36    },
 37    {
 38      name: 'enrich',
 39      fn: runEnrichmentStage,
 40      description: 'Enrich contact details from key pages',
 41    },
 42    { name: 'proposals', fn: runProposalsStage, description: 'Generate personalized proposals' },
 43    { name: 'outreach', fn: runOutreachStage, description: 'Multi-channel outreach delivery' },
 44    { name: 'followup', fn: runFollowupGenerationStage, description: 'Generate follow-up messages for non-responders' },
 45    { name: 'replies', fn: runRepliesStage, description: 'Process inbound replies' },
 46  ];
 47  
 48  /**
 49   * Run the complete pipeline
 50   */
 51  async function runPipeline() {
 52    const pipelineStartTime = Date.now();
 53  
 54    // Parse command line flags
 55    const flags = parseFlags();
 56    const { skip, limit, force } = flags;
 57  
 58    // Merge SKIP_STAGES from environment variable
 59    const envSkipStages = (process.env.SKIP_STAGES || '')
 60      .split(',')
 61      .map(s => s.trim().toLowerCase())
 62      .filter(s => s.length > 0);
 63  
 64    envSkipStages.forEach(stage => skip.add(stage));
 65  
 66    // Display banner
 67    console.log('\n╔══════════════════════════════════════════════════════════════════════╗');
 68    console.log('║                  333 Method Automation Pipeline                     ║');
 69    console.log('╚══════════════════════════════════════════════════════════════════════╝\n');
 70  
 71    if (skip.size > 0) {
 72      logger.warn(`Skipping stages: ${Array.from(skip).join(', ')}`);
 73    }
 74  
 75    if (limit) {
 76      logger.info(`Limit: ${limit} items per stage`);
 77    }
 78  
 79    // Store results for final summary
 80    const stageResults = [];
 81  
 82    // Run each stage
 83    for (const stage of STAGES) {
 84      // Skip if requested
 85      if (skip.has(stage.name)) {
 86        logger.info(`\n⏭️  Skipping ${stage.name} stage`);
 87        continue;
 88      }
 89  
 90      // Display stage header
 91      console.log(`\n${'═'.repeat(70)}`);
 92      console.log(`Stage: ${stage.name.toUpperCase()}`);
 93      console.log(`Description: ${stage.description}`);
 94      console.log(`${'═'.repeat(70)}\n`);
 95  
 96      const stageStartTime = Date.now();
 97  
 98      try {
 99        // Run stage with options
100        const options = {
101          limit: limit || undefined,
102          force: force || undefined,
103        };
104  
105        const stats = await stage.fn(options);
106  
107        // Store results
108        stageResults.push({
109          stage: stage.name,
110          stats,
111          duration: Date.now() - stageStartTime,
112        });
113  
114        // Check for errors
115        if (stats.failed > 0) {
116          logger.warn(`Stage completed with ${stats.failed} failures`);
117        } else {
118          logger.success(`Stage completed successfully`);
119        }
120      } catch (err) {
121        logger.error(`Stage failed: ${err.message}`, err);
122  
123        // Store failed result
124        stageResults.push({
125          stage: stage.name,
126          stats: {
127            processed: 0,
128            succeeded: 0,
129            failed: 1,
130            skipped: 0,
131          },
132          duration: Date.now() - stageStartTime,
133        });
134  
135        // Decide whether to continue
136        if (!force) {
137          logger.error('Stopping pipeline due to stage failure. Use --force to continue on errors.');
138          break;
139        } else {
140          logger.warn('Continuing pipeline despite error (--force enabled)');
141        }
142      }
143    }
144  
145    // Display final summary
146    const totalDuration = Date.now() - pipelineStartTime;
147    generatePipelineCompletion(stageResults, totalDuration);
148  
149    // Calculate overall success
150    const totalFailed = stageResults.reduce((sum, r) => sum + (r.stats.failed || 0), 0);
151    if (totalFailed === 0) {
152      logger.success('\n✅ Pipeline completed successfully!');
153    } else {
154      logger.warn(`\n⚠️  Pipeline completed with ${totalFailed} total failures`);
155    }
156  }
157  
158  /**
159   * Main entry point
160   */
161  async function main() {
162    try {
163      await runPipeline();
164      logger.close();
165      process.exit(0);
166    } catch (err) {
167      logger.error(`Pipeline failed: ${err.message}`, err);
168      console.error(err.stack);
169      logger.close();
170      process.exit(1);
171    }
172  }
173  
174  // Run if called directly
175  if (import.meta.url === `file://${process.argv[1]}`) {
176    main();
177  }
178  
179  export { runPipeline };