← back to Fentucci Theme Note

apply-quote-note.mjs

109 lines

#!/usr/bin/env node
// TK-10311 / TK-00034 — inject the DTD-approved (6/6 A) site-wide quote-only "specs confirmed
// with your sample" trust note into the LIVE theme's snippets/product-description-meta.liquid.
// Steve APPROVED the content 2026-08-08; Steve in-session GO 2026-08-31 to execute now that the
// full-access token carries write_themes. Writes to the CURRENT role=main theme (the memo's
// 144396058675 is now unpublished; the live/published theme moved to a new id).
//
// Reversible: backs up the exact current asset first (backups/), re-PUT that file to revert.
// Idempotent: refuses if the block is already present.
import fs from 'node:fs';

const SECRETS = '/Users/macstudio3/Projects/secrets-manager/.env';
const STORE = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = fs.readFileSync(SECRETS, 'utf8').match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m)[1].trim();
const REST = `https://${STORE}/admin/api/2024-10`;
const H = { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' };
const KEY = 'snippets/product-description-meta.liquid';
const APPLY = process.argv.includes('--apply');
const BACKDIR = new URL('./backups/', import.meta.url).pathname;
const LEDGER = `${process.env.HOME}/.claude/yolo-queue/executed-reversible/ledger.jsonl`;
fs.mkdirSync(BACKDIR, { recursive: true });

// DTD-approved block (positive framing, site-wide quote-only condition) + minimal CSS.
const CSS_MARK = '.spec-note--quote{';
const CSS = `\n<style>.spec-note--quote{font-size:.85em;color:#6b6b6b;font-style:italic;margin-top:6px}</style>\n`;
const BLOCK_MARK = 'spec-note--quote';
const BLOCK = `
  {%- comment -%} TK-10311/TK-00034: site-wide quote-only "specs confirmed at sample" trust note (DTD 6/6 A, Steve-approved 2026-08-08) {%- endcomment -%}
  {%- if product.metafields.custom.price_mode == 'quote_only' or product.tags contains 'quotes' -%}
    <p class="spec-note spec-note--quote">Exact width &amp; specifications are confirmed with your complimentary sample.</p>
  {%- endif -%}
`;

async function getAsset() {
  const r = await fetch(`${REST}/themes/${MAIN}/assets.json?asset[key]=${encodeURIComponent(KEY)}`, { headers: H });
  const j = await r.json();
  return j.asset;
}
async function mainThemeId() {
  const r = await fetch(`${REST}/themes.json?fields=id,role,name`, { headers: H });
  const j = await r.json();
  const m = (j.themes || []).find(t => t.role === 'main');
  if (!m) throw new Error('no role=main theme found');
  return m;
}

let MAIN;
async function main() {
  const m = await mainThemeId();
  MAIN = m.id;
  console.log(`live main theme: ${MAIN} "${m.name}"`);
  const asset = await getAsset();
  if (!asset || asset.value == null) throw new Error(`${KEY} not found on theme ${MAIN}`);
  const orig = asset.value;
  // backup
  const bak = `${BACKDIR}apply-src-MAIN-${MAIN}-${new Date().toISOString().replace(/[:.]/g, '-')}.liquid.bak`;
  fs.writeFileSync(bak, orig);
  console.log(`backed up ${orig.length} bytes -> ${bak}`);

  if (orig.includes(BLOCK_MARK)) { console.log('IDEMPOTENT: quote-note block already present — nothing to do.'); return; }

  // 1) inject the liquid block: right after the AI Rooms {% endif %} that closes the spec rows,
  //    inside .dw-specs-more (anchor = the "</div>\n  </details>" that closes the specs panel).
  const anchor = '</div>\n  </details>';
  let updated;
  if (orig.includes(anchor)) {
    updated = orig.replace(anchor, `${BLOCK}    </div>\n  </details>`);
  } else {
    // fallback: inject before the closing of the top-level specs container "</div>\n{% endif %}"
    const fb = '</div>\n{% endif %}';
    if (!orig.includes(fb)) throw new Error('no injection anchor found — refusing to write');
    updated = orig.replace(fb, `${BLOCK}</div>\n{% endif %}`);
  }
  // 2) inject CSS once (prepend near top)
  if (!updated.includes(CSS_MARK)) updated = CSS + updated;

  if (updated === orig) throw new Error('no change produced — refusing');
  console.log(`change: +${updated.length - orig.length} bytes; block injected=${updated.includes(BLOCK_MARK)}; css injected=${updated.includes(CSS_MARK)}`);

  if (!APPLY) {
    const preview = `${BACKDIR}preview-MAIN-${MAIN}.liquid`;
    fs.writeFileSync(preview, updated);
    console.log(`DRY-RUN. proposed asset written to ${preview} (not PUT).`);
    return;
  }

  const put = await fetch(`${REST}/themes/${MAIN}/assets.json`, {
    method: 'PUT', headers: H, body: JSON.stringify({ asset: { key: KEY, value: updated } }),
  });
  const pj = await put.json();
  if (!put.ok || !pj.asset) throw new Error(`PUT failed ${put.status}: ${JSON.stringify(pj).slice(0, 200)}`);
  console.log(`PUT ok: ${pj.asset.key} @ ${pj.asset.updated_at} size ${pj.asset.size}`);

  // verify by re-GET
  const after = await getAsset();
  const ok = (after.value || '').includes(BLOCK_MARK) && (after.value || '').includes(CSS_MARK);
  console.log(`VERIFY re-GET: block present=${(after.value||'').includes(BLOCK_MARK)} css present=${(after.value||'').includes(CSS_MARK)} => ${ok ? 'OK' : 'FAIL'}`);

  fs.appendFileSync(LEDGER, JSON.stringify({
    ts: new Date().toISOString(), agent: 'vp-dw-commerce', ticket: 'TK-10311',
    action: `inject quote-only specs-confirmed-at-sample note into ${KEY} on live main theme ${MAIN}`,
    blast_radius: 1,
    undo_cmd: `node ~/Projects/fentucci-theme-note/revert-quote-note.mjs "${bak}"`,
    verify: `re-GET ${KEY} contains spec-note--quote = ${ok}`,
  }) + '\n');
  if (!ok) process.exit(2);
}
main().catch(e => { console.error('FATAL', e.message); process.exit(1); });