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