← back to Dw Daily Catchup 20260909
out/cleanup/revert-tier1.js
72 lines
'use strict';
/* TK-10446 Tier-1 REVERT (GATED). Sets ACTIVE -> DRAFT for the 892 live-verified
* true-orphan products the rotation published in the last 7 days (sample-only, no
* sellable roll). Reversible (re-publish / set ACTIVE).
*
* node revert-tier1.js tier1-revert-892-ids.txt # DRY-RUN (default): re-verifies each is
* # still ACTIVE + still orphan; NO writes.
* node revert-tier1.js tier1-revert-892-ids.txt --apply --yes-i-am-steve # GATED: sets status DRAFT.
*
* Safety: even in --apply it RE-CHECKS live that the product is ACTIVE and has NO
* variant priced > $4.25 before reverting — so a product that gained a roll variant
* (or was already changed) since the scan is SKIPPED, never wrongly un-published.
* Uses curl (node fetch is blocked in this sandbox). Cost $0 (Shopify Admin API).
*/
const fs = require('fs');
const { execFileSync } = require('child_process');
const SAMPLE = 4.25;
const ENV = fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1];
const URL = 'https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
const args = process.argv.slice(2);
const FILE = args.find(a => !a.startsWith('--'));
const APPLY = args.includes('--apply');
const CONFIRMED = args.includes('--yes-i-am-steve');
const ids = fs.readFileSync(FILE, 'utf8').trim().split('\n').filter(Boolean);
// GATE: refuse to apply without explicit confirmation BEFORE touching anything.
if (APPLY && !CONFIRMED) { console.error('REFUSED: --apply requires --yes-i-am-steve. No writes performed.'); process.exit(2); }
function gql(query, variables) {
const body = JSON.stringify({ query, variables });
for (let a = 0; a < 6; a++) {
try {
const out = execFileSync('curl', ['-s', '--max-time', '25', '-X', 'POST', URL,
'-H', `X-Shopify-Access-Token: ${TOKEN}`, '-H', 'Content-Type: application/json', '--data', body],
{ encoding: 'utf8', maxBuffer: 4e6 });
const j = JSON.parse(out);
if (j.errors && JSON.stringify(j.errors).includes('THROTTLED')) { sleep(1500); continue; }
if (j.errors) throw new Error(JSON.stringify(j.errors).slice(0, 200));
return j.data;
} catch (e) { if (a === 5) throw e; sleep(1000 * (a + 1)); }
}
}
function sleep(ms){ try{ execFileSync('sleep', [String(ms/1000)]); }catch{} }
let reverted = 0, skipped_hasRoll = 0, skipped_notActive = 0, failed = 0, wouldRevert = 0;
for (let i = 0; i < ids.length; i++) {
const gid = ids[i];
let d;
try { d = gql(`{ product(id:"${gid}"){ status variants(first:40){ nodes{ price } } } }`); }
catch (e) { failed++; continue; }
const p = d && d.product;
if (!p) { failed++; continue; }
if (String(p.status).toUpperCase() !== 'ACTIVE') { skipped_notActive++; continue; }
if (p.variants.nodes.some(v => parseFloat(v.price) > SAMPLE)) { skipped_hasRoll++; continue; } // gained a roll → leave
// qualifies: ACTIVE + orphan
if (!APPLY) { wouldRevert++; }
else {
try {
const r = gql(`mutation($in:ProductInput!){ productUpdate(input:$in){ userErrors{ message } } }`,
{ in: { id: gid, status: 'DRAFT' } });
const ue = r.productUpdate && r.productUpdate.userErrors;
if (ue && ue.length) { failed++; if (failed <= 5) console.error(' ERR', gid, ue[0].message); }
else reverted++;
} catch (e) { failed++; if (failed <= 5) console.error(' ERR', gid, e.message); }
}
if ((i + 1) % 100 === 0) process.stderr.write(` ...${i + 1}/${ids.length}\n`);
}
console.log(JSON.stringify({ mode: APPLY ? 'APPLY' : 'DRY-RUN', total: ids.length,
would_revert: wouldRevert, reverted, skipped_has_roll: skipped_hasRoll,
skipped_not_active: skipped_notActive, failed }, null, 2));