← back to Flock Fix Viewer

trim-desc.mjs

49 lines

// Reduce product descriptions to ONE paragraph (keep the first). Standing rule for any description.
// Scope: flock (vendor Phillipe Romano flock + tag Flock Velvet). Snapshot + ledger. Modes: --dry | --apply
import fs from 'fs'; import os from 'os'; import path from 'path';
const SHOP='designer-laboratory-sandbox.myshopify.com';
const TOKEN=fs.readFileSync(new URL('./.token',import.meta.url),'utf8').trim();
const LEDGER=path.join(os.homedir(),'.claude/yolo-queue/executed-reversible/ledger.jsonl');
const SNAP=new URL('./desc-snapshots/',import.meta.url); try{fs.mkdirSync(SNAP,{recursive:true});}catch(e){}
const DRY=!process.argv.includes('--apply');
const API=`https://${SHOP}/admin/api/2024-10`;
const H={'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'};
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const gql=async(q,v={})=>(await (await fetch(`${API}/graphql.json`,{method:'POST',headers:H,body:JSON.stringify({query:q,variables:v})})).json());

// keep the FIRST paragraph only
function oneParagraph(html){
  if(!html) return html;
  let m=html.match(/<p\b[^>]*>[\s\S]*?<\/p>/i);
  if(m){ // has <p> blocks -> keep first non-empty one
    const ps=html.match(/<p\b[^>]*>[\s\S]*?<\/p>/gi)||[];
    for(const p of ps){ if(p.replace(/<[^>]+>/g,'').trim()) return p; }
    return ps[0]||html;
  }
  // no <p>: split on <br><br> or double newline; keep first block
  const first=html.split(/(?:<br\s*\/?>\s*){2,}|\n\s*\n/i)[0].trim();
  return first;
}

(async()=>{
  let cur=null,all=[];
  const Q=`query($c:String){ products(first:100, query:"vendor:'Phillipe Romano' (tag:'Flock Velvet' OR title:flock OR handle:flock)", after:$c){ pageInfo{hasNextPage endCursor} edges{node{ id handle descriptionHtml }}}}`;
  do{const r=await gql(Q,{c:cur});const p=r?.data?.products;if(!p)break;all=all.concat(p.edges);cur=p.pageInfo.hasNextPage?p.pageInfo.endCursor:null;}while(cur);
  let multi=0,done=0;
  for(const e of all){
    const n=e.node, orig=n.descriptionHtml||'';
    const trimmed=oneParagraph(orig);
    if(trimmed===orig || !trimmed) continue;   // already one paragraph
    multi++;
    if(DRY){ if(multi<=8) console.log(`  ${n.handle.slice(0,40)} : ${orig.replace(/<[^>]+>/g,' ').length} -> ${trimmed.replace(/<[^>]+>/g,' ').length} chars`); continue; }
    fs.writeFileSync(new URL(n.handle.replace(/[^a-z0-9-]/gi,'_')+'.html',SNAP), orig);
    const r=await gql('mutation($id:ID!,$b:String!){productUpdate(input:{id:$id,descriptionHtml:$b}){userErrors{field message}}}',{id:n.id,b:trimmed});
    const errs=r?.data?.productUpdate?.userErrors;
    if(errs&&errs.length){console.log('  FAIL',n.handle,JSON.stringify(errs));continue;}
    done++;
    fs.appendFileSync(LEDGER,JSON.stringify({ts:new Date().toISOString(),agent:'main:flock-desc-1para',ticket:'flock-desc',action:`trim description to 1 paragraph on ${n.handle}`,blast_radius:1,undo_cmd:`restore desc-snapshots/${n.handle}.html`,verify:`description is one paragraph`})+'\n');
    await sleep(220);
  }
  console.log(`${DRY?'DRY':'DONE'} · multi-paragraph flock: ${multi} · trimmed: ${done} · total flock: ${all.length}`);
})();