← back to All Designerwallcoverings
scripts/live-scrape.js
92 lines
#!/usr/bin/env node
// live-scrape.js — the METERED /livestock --live worker for all.designerwallcoverings.com.
//
// Now a thin DISPATCHER over the generalized per-vendor adapter registry (scripts/adapters/).
// Spawned by server.js on an explicit "Check Live" click for ONE SKU; the server passes the
// resolved adapter key (from data/live-stock-coverage.json). Prints ONE JSON line to stdout;
// all diagnostics go to stderr. Each adapter owns its own login (creds read from the DB — never
// hardcoded/echoed) and returns PUBLIC-SAFE availability (+ our-retail price only where defined).
//
// node scripts/live-scrape.js <dw_sku|mfr_sku> [adapterKey]
//
// If adapterKey is omitted (standalone CLI use), the worker resolves it from the SKU's display
// vendor via the committed coverage file — so `node scripts/live-scrape.js <sku>` still works.
const fs = require('fs');
const os = require('os');
const path = require('path');
const { Pool } = require('pg');
const adapters = require('./adapters');
const SKU = (process.argv[2] || '').trim();
let ADAPTER_KEY = (process.argv[3] || '').trim();
const CATALOG_ARG = (process.argv[4] || '').trim(); // server passes the coverage catalog_table for public-stock adapters
const DSN = process.env.DATABASE_URL || 'postgresql:///dw_unified?host=/tmp';
const COVERAGE = path.join(__dirname, '..', 'data', 'live-stock-coverage.json');
// Browserbase session helper — the self-contained project copy on prod, or the skill copy on dev.
const LOCAL_LIB = path.join(__dirname, '..', 'lib', 'romo-lib.js');
const BB_LIB = process.env.BROWSERBASE_LIB
|| (fs.existsSync(LOCAL_LIB) ? LOCAL_LIB : path.join(os.homedir(), '.claude', 'skills', 'browserbase', 'romo-lib.js'));
const emit = (o) => process.stdout.write(JSON.stringify(o) + '\n');
const log = (...a) => process.stderr.write(a.join(' ') + '\n');
// Resolve the SKU → display vendor → adapter (+ its catalog table) from the coverage file.
async function resolveAdapter(pool, sku) {
let vendor = null;
try {
const { rows } = await pool.query(
`SELECT vendor FROM shopify_products
WHERE upper(coalesce(dw_sku,''))=upper($1) OR upper(coalesce(variant_sku,''))=upper($1) OR upper(coalesce(sku,''))=upper($1)
LIMIT 1`, [sku]);
vendor = rows[0] && rows[0].vendor;
} catch { /* ignore */ }
if (!vendor) return { key: null, catalogTable: null, vendor: null };
try {
const cov = JSON.parse(fs.readFileSync(COVERAGE, 'utf8'));
const e = cov.vendors[vendor];
if (e && e.live_capable && e.adapter) return { key: e.adapter, catalogTable: e.catalog_table || null, vendor };
} catch { /* coverage optional */ }
return { key: null, catalogTable: null, vendor };
}
// The catalog table an adapter should read for this SKU (server passes argv[4]; else per-key default).
function catalogForKey(key) {
if (CATALOG_ARG && /^[a-z0-9_]+$/.test(CATALOG_ARG)) return CATALOG_ARG;
if (key === 'wallquest') return 'wallquest_catalog';
if (key === 'romo') return 'romo_catalog';
return null;
}
(async () => {
if (!SKU) return emit({ available: false, reason: 'sku required' });
const pool = new Pool({ connectionString: DSN, max: 1 });
try {
let catalogTable = catalogForKey(ADAPTER_KEY);
// No adapter key given → standalone resolve from the SKU's vendor.
if (!ADAPTER_KEY) {
const r = await resolveAdapter(pool, SKU);
ADAPTER_KEY = r.key; catalogTable = r.catalogTable || catalogTable;
if (!ADAPTER_KEY) return emit({ available: false, reason: 'live check unavailable for this vendor' });
}
// Load the Browserbase helpers (present only where the backend is installed).
// DB adapters ($0, in-Postgres reads — no browser session) run WITHOUT the Browserbase lib:
// its absence only blocks adapters that actually need a metered session.
const DB_ADAPTERS = new Set(['kravet-db']);
let newSession, romoLogin;
try { const lib = require(BB_LIB); newSession = lib.newSession; romoLogin = lib.login; }
catch {
if (!DB_ADAPTERS.has(ADAPTER_KEY)) return emit({ available: false, reason: 'live scrape backend not available on this host' });
newSession = undefined; romoLogin = undefined;
}
const result = await adapters.run(ADAPTER_KEY, { sku: SKU, pool, newSession, romoLogin, catalogTable, log });
return emit(result);
} catch (e) {
return emit({ available: false, reason: 'live scrape error: ' + (e.message || e) });
} finally {
try { await pool.end(); } catch {}
setTimeout(() => process.exit(0), 200);
}
})();