← back to Fashion Style Guides

agent/refresh.js

88 lines

#!/usr/bin/env node
// Style Guide Agent — re-checks each brand's public guidelines + Pantone announcements.
// Strategy: (1) HEAD-check each brand URL is reachable, (2) note last-checked timestamp,
// (3) check Pantone color-of-the-year for current year if blank, (4) write back to data/.
// DRY_RUN=1 prints planned changes without writing.

const fs = require('fs');
const path = require('path');
const https = require('https');

const ROOT = path.join(__dirname, '..');
const DATA_DIR = path.join(ROOT, 'data');
const LOG_FILE = path.join(__dirname, 'refresh.log');
const DRY = !!process.env.DRY_RUN;

const log = (msg) => {
  const line = `[${new Date().toISOString()}] ${msg}`;
  console.log(line);
  fs.appendFileSync(LOG_FILE, line + '\n');
};

function loadJson(file) { return JSON.parse(fs.readFileSync(path.join(DATA_DIR, file), 'utf8')); }
function saveJson(file, data) {
  if (DRY) { log(`DRY: would write ${file}`); return; }
  fs.writeFileSync(path.join(DATA_DIR, file), JSON.stringify(data, null, 2) + '\n');
  log(`wrote ${file}`);
}

function headCheck(url) {
  return new Promise((resolve) => {
    try {
      const u = new URL(url);
      const req = https.request({
        method: 'HEAD',
        hostname: u.hostname,
        path: u.pathname || '/',
        timeout: 8000,
        headers: { 'user-agent': 'FashionStyleGuidesAgent/0.1 (+steve@designerwallcoverings.com)' },
      }, (res) => resolve({ ok: res.statusCode >= 200 && res.statusCode < 500, status: res.statusCode }));
      req.on('timeout', () => { req.destroy(); resolve({ ok: false, status: 'timeout' }); });
      req.on('error', (e) => resolve({ ok: false, status: e.code || 'error' }));
      req.end();
    } catch (e) { resolve({ ok: false, status: 'invalid-url' }); }
  });
}

async function refreshBrands() {
  const data = loadJson('brands.json');
  log(`brand-refresh: checking ${data.brands.length} URLs`);
  let okCount = 0, failCount = 0;
  for (const b of data.brands) {
    if (!b.url) continue;
    const res = await headCheck(b.url);
    b.last_checked = new Date().toISOString();
    b.last_status = res.status;
    if (res.ok) okCount++; else failCount++;
    log(`  ${b.id.padEnd(18)} ${res.ok ? 'OK' : 'FAIL'} (${res.status}) ${b.url}`);
  }
  data.last_updated = new Date().toISOString().slice(0, 10);
  saveJson('brands.json', data);
  log(`brand-refresh complete: ${okCount} OK, ${failCount} fail`);
}

async function refreshPantone() {
  const data = loadJson('pantone.json');
  const thisYear = new Date().getFullYear();
  const entry = data.color_of_the_year.find(c => c.year === thisYear);
  if (entry && !entry.hex) {
    log(`pantone: ${thisYear} color of the year is still pending — agent has no live Pantone API; please update manually when announced.`);
  } else if (entry) {
    log(`pantone: ${thisYear} = ${entry.name} (${entry.pantone || 'n/a'})`);
  } else {
    log(`pantone: no entry for ${thisYear}; consider adding a TBA stub`);
    data.color_of_the_year.unshift({ year: thisYear, name: 'TBA', pantone: null, hex: null, note: 'Auto-stub by Style Guide Agent — populate when Pantone publishes.' });
    saveJson('pantone.json', data);
    return;
  }
  data.last_updated = new Date().toISOString().slice(0, 10);
  saveJson('pantone.json', data);
}

(async () => {
  log(`=== refresh start ${DRY ? '(DRY RUN)' : ''} ===`);
  try { await refreshBrands(); } catch (e) { log('brand-refresh ERROR: ' + e.message); }
  try { await refreshPantone(); } catch (e) { log('pantone-refresh ERROR: ' + e.message); }
  log('=== refresh end ===');
})();