← back to Prestige Car Wash

scripts/competitors-scan.js

69 lines

#!/usr/bin/env node
'use strict';
/**
 * competitors-scan.js — refresh the SFV car-wash competitor set.
 *
 * Two paths:
 *   1) If GOOGLE_PLACES_API_KEY is set, run a Places "Nearby/Text Search" for car washes
 *      across SFV and merge new finds into data/competitors.json (id/name/city/rating/reviews).
 *   2) Ad-platform signals (who's running Google/Meta/TikTok ads) come from the Claude-side
 *      `advertising-signals` / `ad-social-tracker` skills — this script leaves ad_platforms
 *      for that enrichment step and does not fabricate it.
 *
 * The seeded competitor set already reflects live Exa research; this keeps it fresh.
 * Usage: node scripts/competitors-scan.js
 */
const fs = require('fs');
const path = require('path');
const FILE = path.join(__dirname, '..', 'data', 'competitors.json');

function keyFrom(name) {
  if (process.env[name]) return process.env[name];
  try {
    const m = fs.readFileSync(path.join(process.env.HOME, 'Projects/secrets-manager/.env'), 'utf8')
      .match(new RegExp('^' + name + '=(.+)$', 'm'));
    return m ? m[1].trim() : '';
  } catch { return ''; }
}
const KEY = keyFrom('GOOGLE_PLACES_API_KEY');
const SFV = ['Sherman Oaks', 'Van Nuys', 'Encino', 'Northridge', 'Studio City', 'Reseda', 'Woodland Hills', 'North Hollywood', 'Tarzana'];
const slug = s => 'cmp-' + s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 32);

async function textSearch(city) {
  const r = await fetch('https://places.googleapis.com/v1/places:searchText', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-Goog-Api-Key': KEY,
      'X-Goog-FieldMask': 'places.displayName,places.formattedAddress,places.rating,places.userRatingCount,places.websiteUri' },
    body: JSON.stringify({ textQuery: `car wash in ${city}, CA` })
  });
  if (!r.ok) throw new Error(`Places ${r.status}`);
  return ((await r.json()).places || []);
}

async function main() {
  const existing = JSON.parse(fs.readFileSync(FILE, 'utf8'));
  if (!KEY) {
    console.log('⚠ GOOGLE_PLACES_API_KEY not set — cannot auto-discover new competitors.');
    console.log(`  Current seeded set: ${existing.length} competitors (from Exa research).`);
    console.log('  For ad-platform signals, run the `advertising-signals` skill per competitor domain.');
    return;
  }
  const byName = new Map(existing.map(c => [c.name.toLowerCase(), c]));
  let added = 0;
  for (const city of SFV) {
    try {
      for (const p of await textSearch(city)) {
        const name = p.displayName?.text; if (!name || byName.has(name.toLowerCase())) continue;
        const row = { id: slug(name), name, city, address: p.formattedAddress || '', format: '',
          rating: p.rating || null, reviews: p.userRatingCount || 0, price_signal: '', notes: '',
          ad_platforms: [], website: p.websiteUri || '', source: 'places-scan',
          created_at: new Date().toISOString() };
        existing.push(row); byName.set(name.toLowerCase(), row); added++;
      }
    } catch (e) { console.log(`  ✖ ${city}: ${e.message}`); }
  }
  fs.writeFileSync(FILE, JSON.stringify(existing, null, 2));
  console.log(`✔ competitors: +${added} new (total ${existing.length})`);
}
main().catch(e => { console.error(e); process.exit(1); });