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