← 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); });