[object Object]

← back to Corkwallcovering

auto-save: 2026-07-01T14:06:35 (3 files) — scripts/_cork-apply-template.mjs scripts/apply-cork-tags-SELFCONTAINED.mjs scripts/gen-selfcontained-prod.mjs

bbf65d97e36e51599c00b2331d577e3b8f7164bb · 2026-07-01 14:06:38 -0700 · Steve Abrams

Files touched

Diff

commit bbf65d97e36e51599c00b2331d577e3b8f7164bb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 1 14:06:38 2026 -0700

    auto-save: 2026-07-01T14:06:35 (3 files) — scripts/_cork-apply-template.mjs scripts/apply-cork-tags-SELFCONTAINED.mjs scripts/gen-selfcontained-prod.mjs
---
 scripts/_cork-apply-template.mjs          |  96 ++++++++++
 scripts/apply-cork-tags-SELFCONTAINED.mjs | 302 ++++++++++++++++++++++++++++++
 scripts/gen-selfcontained-prod.mjs        |  47 +++++
 3 files changed, 445 insertions(+)

diff --git a/scripts/_cork-apply-template.mjs b/scripts/_cork-apply-template.mjs
new file mode 100644
index 0000000..f906ceb
--- /dev/null
+++ b/scripts/_cork-apply-template.mjs
@@ -0,0 +1,96 @@
+#!/usr/bin/env node
+/**
+ * SELF-CONTAINED cork PROD tag-apply. The __PLAN__ literal below is injected by
+ * gen-selfcontained-prod.mjs (which computes Scope A + Scope B on a git checkout).
+ * Needs ONLY: DW_PROD_STORE_DOMAIN, DW_PROD_SHOPIFY_TOKEN, DW_UNIFIED_DSN (env) + pg.
+ * No git / repo / data files. Additive-only, idempotent, PG-first, kill-switch + cap.
+ *   node apply-cork-tags-SELFCONTAINED.mjs                       # DRY-RUN (read-only)
+ *   ...creds... node apply-cork-tags-SELFCONTAINED.mjs --apply   # LIVE (PG→Shopify)
+ */
+import { existsSync } from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+
+const PLAN = __PLAN__;
+
+const APPLY = process.argv.includes('--apply');
+const KILL = path.join(os.homedir(), '.dw-fixer-stop');
+const CAP = 200, API = '2024-10';
+const STORE = process.env.DW_PROD_STORE_DOMAIN || null;
+const TOKEN = process.env.DW_PROD_SHOPIFY_TOKEN || null;
+const DSN = process.env.DW_UNIFIED_DSN || null;
+
+async function gql(query, variables) {
+  const r = await fetch(`https://${STORE}/admin/api/${API}/graphql.json`, {
+    method: 'POST', headers: { 'content-type': 'application/json', 'X-Shopify-Access-Token': TOKEN },
+    body: JSON.stringify({ query, variables }),
+  });
+  const j = await r.json();
+  if (j.errors) throw new Error(JSON.stringify(j.errors).slice(0, 300));
+  return j.data;
+}
+async function resolveByHandle(handle) {
+  const d = await gql(`query($q:String!){ products(first:1, query:$q){ edges{ node{ id title handle tags } } } }`, { q: `handle:${handle}` });
+  const n = d?.products?.edges?.[0]?.node;
+  return n ? { id: n.id, tags: n.tags || [] } : null;
+}
+async function applyShopifyTags(id, tags) {
+  const d = await gql(`mutation($input:ProductInput!){ productUpdate(input:$input){ product{ id tags } userErrors{ field message } } }`, { input: { id, tags } });
+  const ue = d?.productUpdate?.userErrors || [];
+  if (ue.length) throw new Error(ue.map(e => e.message).join('; '));
+  return d.productUpdate.product.tags;
+}
+let PgClient = null;
+async function pgConnect() {
+  if (!DSN) throw new Error('DW_UNIFIED_DSN not set');
+  try { ({ Client: PgClient } = await import('pg')); } catch { throw new Error("node 'pg' package not available — install pg to run --apply"); }
+  const c = new PgClient({ connectionString: DSN }); await c.connect(); return c;
+}
+async function pgAddTags(pg, handle, fresh) {
+  const sel = await pg.query('SELECT id, tags FROM shopify_products WHERE handle = $1 LIMIT 1', [handle]);
+  if (!sel.rows.length) return { matched: false };
+  const row = sel.rows[0];
+  const existing = String(row.tags || '').split(',').map(s => s.trim()).filter(Boolean);
+  const have = new Set(existing.map(t => t.toLowerCase()));
+  const add = fresh.filter(t => !have.has(t.toLowerCase()));
+  if (!add.length) return { matched: true, wrote: false };
+  await pg.query('UPDATE shopify_products SET tags = $1 WHERE id = $2', [[...existing, ...add].join(', '), row.id]);
+  return { matched: true, wrote: true, added: add };
+}
+
+if (existsSync(KILL)) { console.error('REFUSED: kill-switch present at ' + KILL); process.exit(2); }
+console.log(`Mode: ${APPLY ? 'LIVE APPLY (PG-first → Shopify)' : 'DRY-RUN (read-only)'}`);
+console.log(`Store: ${STORE || '(DW_PROD_STORE_DOMAIN unset)'}  ·  dw_unified: ${DSN ? 'DSN present' : '(unset)'}`);
+console.log(`In-scope products: ${PLAN.length} (baked plan)  cap=${CAP}  ·  cost $0\n`);
+if (APPLY) {
+  if (!STORE || !TOKEN) { console.error('REFUSED --apply: DW_PROD_STORE_DOMAIN and DW_PROD_SHOPIFY_TOKEN required.'); process.exit(3); }
+  if (!DSN) { console.error('REFUSED --apply: DW_UNIFIED_DSN required (PG-first ordering is mandatory).'); process.exit(3); }
+}
+
+let resolved = 0, wouldChange = 0, noChange = 0, notFound = 0, failed = 0, applied = 0;
+let pg = null; if (APPLY) pg = await pgConnect();
+for (const { handle, add: wantTags } of PLAN.slice(0, CAP)) {
+  try {
+    let prod = null;
+    if (STORE && TOKEN) prod = await resolveByHandle(handle);
+    if (STORE && TOKEN && !prod) { console.log(`  ✗ ${handle}: NOT FOUND in store`); notFound++; continue; }
+    resolved += (prod ? 1 : 0);
+    const storeTags = prod ? prod.tags : [];
+    const have = new Set(storeTags.map(t => t.toLowerCase()));
+    const fresh = wantTags.filter(t => !have.has(t.toLowerCase()));
+    if (!fresh.length) { if (prod) { console.log(`  · ${handle}: already has ${wantTags.join(', ')} — skip`); noChange++; } else { console.log(`  ~ ${handle}: would add ${wantTags.join(', ')} (unresolved)`); wouldChange++; } continue; }
+    wouldChange++;
+    if (APPLY) {
+      const pgRes = await pgAddTags(pg, handle, fresh);
+      if (!pgRes.matched) { console.log(`  ✗ ${handle}: no dw_unified row — SKIP (no Shopify write)`); failed++; continue; }
+      const now = await applyShopifyTags(prod.id, [...storeTags, ...fresh]);
+      console.log(`  ✓ ${handle} → PG+${(pgRes.added || []).join(',') || '∅'} · Shopify +${fresh.join(', ')} (now ${now.length})`);
+      applied++;
+    } else {
+      console.log(`  ~ ${handle} [${prod ? prod.id.split('/').pop() : '?'}] would add +${fresh.join(', ')} (store has ${storeTags.length})`);
+    }
+  } catch (e) { console.log(`  ✗ ${handle}: ${e.message}`); failed++; }
+}
+if (pg) await pg.end();
+console.log(`\n── ${APPLY ? 'APPLIED' : 'DRY-RUN'} SUMMARY ──`);
+console.log(`in-scope=${PLAN.length}  resolved=${resolved}  wouldChange=${wouldChange}  alreadyPresent=${noChange}  notFound=${notFound}  failed=${failed}${APPLY ? '  applied=' + applied : ''}`);
diff --git a/scripts/apply-cork-tags-SELFCONTAINED.mjs b/scripts/apply-cork-tags-SELFCONTAINED.mjs
new file mode 100644
index 0000000..360d491
--- /dev/null
+++ b/scripts/apply-cork-tags-SELFCONTAINED.mjs
@@ -0,0 +1,302 @@
+#!/usr/bin/env node
+/**
+ * SELF-CONTAINED cork PROD tag-apply — generated ee5ed29 plan baked in.
+ * Needs ONLY: DW_PROD_STORE_DOMAIN, DW_PROD_SHOPIFY_TOKEN, DW_UNIFIED_DSN (env) + pg.
+ * No git / repo / data files. Additive-only, idempotent, PG-first, kill-switch + cap.
+ *   node apply-cork-tags-SELFCONTAINED.mjs            # DRY-RUN (read-only)
+ *   ...creds... node apply-cork-tags-SELFCONTAINED.mjs --apply   # LIVE (PG→Shopify)
+ */
+import { existsSync } from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+
+const PLAN = [
+  {
+    "handle": "dwtt-71313-designer-wallcoverings-los-angeles",
+    "add": [
+      "Cork"
+    ]
+  },
+  {
+    "handle": "dwta-65433designerwallcoverings-los-angeles",
+    "add": [
+      "Cork",
+      "Contemporary",
+      "Geometric"
+    ]
+  },
+  {
+    "handle": "dwtt-71303-designer-wallcoverings-los-angeles",
+    "add": [
+      "Cork"
+    ]
+  },
+  {
+    "handle": "dwtt-71287-designer-wallcoverings-los-angeles",
+    "add": [
+      "Cork"
+    ]
+  },
+  {
+    "handle": "mazarin-by-innovations-usa-dwc-maz-04",
+    "add": [
+      "Brown"
+    ]
+  },
+  {
+    "handle": "dwjj-40820-andrea",
+    "add": [
+      "Contemporary"
+    ]
+  },
+  {
+    "handle": "billion-dollar-mica-prg-47801",
+    "add": [
+      "Hollywood Regency"
+    ]
+  },
+  {
+    "handle": "billion-dollar-mica-prg-47812",
+    "add": [
+      "Rustic"
+    ]
+  },
+  {
+    "handle": "clara-house-gold-mica-fxx-3201",
+    "add": [
+      "Contemporary"
+    ]
+  },
+  {
+    "handle": "caluvoto-gilded-cork-cork-72016",
+    "add": [
+      "Hollywood Regency"
+    ]
+  },
+  {
+    "handle": "caluvoto-gilded-cork-cork-72015",
+    "add": [
+      "Rustic"
+    ]
+  },
+  {
+    "handle": "caluvoto-gilded-cork-cork-72012",
+    "add": [
+      "Contemporary"
+    ]
+  },
+  {
+    "handle": "caluvoto-gilded-cork-cork-72010",
+    "add": [
+      "Contemporary"
+    ]
+  },
+  {
+    "handle": "caluvoto-gilded-cork-cork-72006",
+    "add": [
+      "Contemporary"
+    ]
+  },
+  {
+    "handle": "caluvoto-gilded-cork-cork-72004",
+    "add": [
+      "Rustic"
+    ]
+  },
+  {
+    "handle": "caluvoto-gilded-cork-cork-72001",
+    "add": [
+      "Hollywood Regency"
+    ]
+  },
+  {
+    "handle": "faguli-real-cork-cork-34183",
+    "add": [
+      "Rustic"
+    ]
+  },
+  {
+    "handle": "faguli-real-cork-cork-34181",
+    "add": [
+      "Rustic"
+    ]
+  },
+  {
+    "handle": "faguli-real-cork-cork-34180",
+    "add": [
+      "Rustic"
+    ]
+  },
+  {
+    "handle": "faguli-real-cork-cork-34179",
+    "add": [
+      "Rustic"
+    ]
+  },
+  {
+    "handle": "faguli-real-cork-cork-34182",
+    "add": [
+      "Rustic"
+    ]
+  },
+  {
+    "handle": "la-corka-prt-74040",
+    "add": [
+      "Industrial"
+    ]
+  },
+  {
+    "handle": "la-corka-prt-74044",
+    "add": [
+      "Rustic"
+    ]
+  },
+  {
+    "handle": "caluvoto-gilded-cork-cork-72000",
+    "add": [
+      "Contemporary"
+    ]
+  },
+  {
+    "handle": "la-corka-prt-74057",
+    "add": [
+      "Rustic"
+    ]
+  },
+  {
+    "handle": "la-corka-prt-74039",
+    "add": [
+      "Rustic"
+    ]
+  },
+  {
+    "handle": "la-corka-prt-74038",
+    "add": [
+      "Rustic"
+    ]
+  },
+  {
+    "handle": "la-corka-prt-74047",
+    "add": [
+      "Rustic"
+    ]
+  },
+  {
+    "handle": "la-corka-prt-74046",
+    "add": [
+      "Rustic"
+    ]
+  },
+  {
+    "handle": "dwk-29031-tak-ca01-08-designer-wallcoverings-los-angeles",
+    "add": [
+      "Textured"
+    ]
+  },
+  {
+    "handle": "dwk-29027-tak-ca01-04-designer-wallcoverings-los-angeles",
+    "add": [
+      "Textured"
+    ]
+  },
+  {
+    "handle": "dwk-29025-tak-ca01-02-designer-wallcoverings-los-angeles",
+    "add": [
+      "Textured"
+    ]
+  },
+  {
+    "handle": "dwhf-70377-sample-designer-wallcoverings-los-angeles",
+    "add": [
+      "Modern"
+    ]
+  },
+  {
+    "handle": "dwhf-70378-sample-designer-wallcoverings-los-angeles",
+    "add": [
+      "Modern"
+    ]
+  }
+];
+
+const APPLY = process.argv.includes('--apply');
+const KILL = path.join(os.homedir(), '.dw-fixer-stop');
+const CAP = 200, API = '2024-10';
+const STORE = process.env.DW_PROD_STORE_DOMAIN || null;
+const TOKEN = process.env.DW_PROD_SHOPIFY_TOKEN || null;
+const DSN = process.env.DW_UNIFIED_DSN || null;
+
+async function gql(query, variables) {
+  const r = await fetch(\`https://\${STORE}/admin/api/\${API}/graphql.json\`, {
+    method: 'POST', headers: { 'content-type': 'application/json', 'X-Shopify-Access-Token': TOKEN },
+    body: JSON.stringify({ query, variables }),
+  });
+  const j = await r.json();
+  if (j.errors) throw new Error(JSON.stringify(j.errors).slice(0, 300));
+  return j.data;
+}
+async function resolveByHandle(handle) {
+  const d = await gql(\`query($q:String!){ products(first:1, query:$q){ edges{ node{ id title handle tags } } } }\`, { q: \`handle:\${handle}\` });
+  const n = d?.products?.edges?.[0]?.node;
+  return n ? { id: n.id, tags: n.tags || [] } : null;
+}
+async function applyShopifyTags(id, tags) {
+  const d = await gql(\`mutation($input:ProductInput!){ productUpdate(input:$input){ product{ id tags } userErrors{ field message } } }\`, { input: { id, tags } });
+  const ue = d?.productUpdate?.userErrors || [];
+  if (ue.length) throw new Error(ue.map(e => e.message).join('; '));
+  return d.productUpdate.product.tags;
+}
+let PgClient = null;
+async function pgConnect() {
+  if (!DSN) throw new Error('DW_UNIFIED_DSN not set');
+  try { ({ Client: PgClient } = await import('pg')); } catch { throw new Error("node 'pg' package not available — install pg to run --apply"); }
+  const c = new PgClient({ connectionString: DSN }); await c.connect(); return c;
+}
+async function pgAddTags(pg, handle, fresh) {
+  const sel = await pg.query('SELECT id, tags FROM shopify_products WHERE handle = $1 LIMIT 1', [handle]);
+  if (!sel.rows.length) return { matched: false };
+  const row = sel.rows[0];
+  const existing = String(row.tags || '').split(',').map(s => s.trim()).filter(Boolean);
+  const have = new Set(existing.map(t => t.toLowerCase()));
+  const add = fresh.filter(t => !have.has(t.toLowerCase()));
+  if (!add.length) return { matched: true, wrote: false };
+  await pg.query('UPDATE shopify_products SET tags = $1 WHERE id = $2', [[...existing, ...add].join(', '), row.id]);
+  return { matched: true, wrote: true, added: add };
+}
+
+if (existsSync(KILL)) { console.error('REFUSED: kill-switch present at ' + KILL); process.exit(2); }
+console.log(\`Mode: \${APPLY ? 'LIVE APPLY (PG-first → Shopify)' : 'DRY-RUN (read-only)'}\`);
+console.log(\`Store: \${STORE || '(DW_PROD_STORE_DOMAIN unset)'}  ·  dw_unified: \${DSN ? 'DSN present' : '(unset)'}\`);
+console.log(\`In-scope products: \${PLAN.length} (baked plan)  cap=\${CAP}  ·  cost $0\\n\`);
+if (APPLY) {
+  if (!STORE || !TOKEN) { console.error('REFUSED --apply: DW_PROD_STORE_DOMAIN and DW_PROD_SHOPIFY_TOKEN required.'); process.exit(3); }
+  if (!DSN) { console.error('REFUSED --apply: DW_UNIFIED_DSN required (PG-first ordering is mandatory).'); process.exit(3); }
+}
+
+let resolved = 0, wouldChange = 0, noChange = 0, notFound = 0, failed = 0, applied = 0;
+let pg = null; if (APPLY) pg = await pgConnect();
+for (const { handle, add: wantTags } of PLAN.slice(0, CAP)) {
+  try {
+    let prod = null;
+    if (STORE && TOKEN) prod = await resolveByHandle(handle);
+    if (STORE && TOKEN && !prod) { console.log(\`  ✗ \${handle}: NOT FOUND in store\`); notFound++; continue; }
+    resolved += (prod ? 1 : 0);
+    const storeTags = prod ? prod.tags : [];
+    const have = new Set(storeTags.map(t => t.toLowerCase()));
+    const fresh = wantTags.filter(t => !have.has(t.toLowerCase()));
+    if (!fresh.length) { if (prod) { console.log(\`  · \${handle}: already has \${wantTags.join(', ')} — skip\`); noChange++; } else { console.log(\`  ~ \${handle}: would add \${wantTags.join(', ')} (unresolved)\`); wouldChange++; } continue; }
+    wouldChange++;
+    if (APPLY) {
+      const pgRes = await pgAddTags(pg, handle, fresh);
+      if (!pgRes.matched) { console.log(\`  ✗ \${handle}: no dw_unified row — SKIP (no Shopify write)\`); failed++; continue; }
+      const now = await applyShopifyTags(prod.id, [...storeTags, ...fresh]);
+      console.log(\`  ✓ \${handle} → PG+\${(pgRes.added || []).join(',') || '∅'} · Shopify +\${fresh.join(', ')} (now \${now.length})\`);
+      applied++;
+    } else {
+      console.log(\`  ~ \${handle} [\${prod ? prod.id.split('/').pop() : '?'}] would add +\${fresh.join(', ')} (store has \${storeTags.length})\`);
+    }
+  } catch (e) { console.log(\`  ✗ \${handle}: \${e.message}\`); failed++; }
+}
+if (pg) await pg.end();
+console.log(\`\\n── \${APPLY ? 'APPLIED' : 'DRY-RUN'} SUMMARY ──\`);
+console.log(\`in-scope=\${PLAN.length}  resolved=\${resolved}  wouldChange=\${wouldChange}  alreadyPresent=\${noChange}  notFound=\${notFound}  failed=\${failed}\${APPLY ? '  applied=' + applied : ''}\`);
diff --git a/scripts/gen-selfcontained-prod.mjs b/scripts/gen-selfcontained-prod.mjs
new file mode 100644
index 0000000..233071d
--- /dev/null
+++ b/scripts/gen-selfcontained-prod.mjs
@@ -0,0 +1,47 @@
+#!/usr/bin/env node
+/**
+ * Generator (runs on Mac2, where git history exists): computes the exact cork PROD
+ * tag-apply plan (Scope A + Scope B, same logic as apply-shopify-tags-PROD.mjs) and
+ * injects it into scripts/_cork-apply-template.mjs at the __PLAN__ marker, emitting
+ * a fully SELF-CONTAINED runner (scripts/apply-cork-tags-SELFCONTAINED.mjs).
+ *
+ * The emitted runner needs ONLY the 3 prod creds (env) + the `pg` package — no git,
+ * no data/products.json, no reports/ — so it runs on Kamatera's /var/www copy
+ * (which has no .git) with zero repo dependencies.
+ */
+import fs from 'node:fs/promises';
+import { execFileSync } from 'node:child_process';
+
+const STYLE_VOCAB   = ['Traditional','Contemporary','Modern','Coastal','Tropical','Art Deco','Mid-Century Modern','Bohemian','Transitional','Victorian','Chinoiserie','Minimalist','Maximalist','Rustic','Industrial','Scandinavian','Hollywood Regency','Japandi','Farmhouse'];
+const PATTERN_VOCAB = ['Botanical','Floral','Geometric','Damask','Palm','Trellis','Stripe','Abstract','Toile','Scenic','Paisley','Plaid','Animal Print','Medallion','Chevron','Herringbone','Solid','Textured','Faux','Mural'];
+const SCOPE_B_VOCAB = new Set([...STYLE_VOCAB, ...PATTERN_VOCAB]);
+const ENRICH_COMMIT = 'ee5ed29';
+
+const products = JSON.parse(await fs.readFile('data/products.json', 'utf8'));
+const plan = new Map(); // handle -> Set(tags)
+const want = (h, t) => { if (!plan.has(h)) plan.set(h, new Set()); plan.get(h).add(t); };
+
+// Scope A — deterministic adds from the staged diff
+const diff = JSON.parse(await fs.readFile('reports/cork-tag-diff.json', 'utf8'));
+let scopeA = 0;
+for (const r of diff.rows) { if (!r.add_tags?.length) continue; const h = r.handle || r.sku; for (const t of r.add_tags) { want(h, t); scopeA++; } }
+
+// Scope B — Gemini-enriched vocab style/pattern tags: diff vs pre-enrich version (git)
+let scopeB = 0;
+const beforeRaw = execFileSync('git', ['show', `${ENRICH_COMMIT}~1:data/products.json`], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
+const before = JSON.parse(beforeRaw);
+const beforeTags = new Map(before.map(p => [p.handle || p.sku, new Set((p.tags || []).map(String))]));
+for (const p of products) {
+  const h = p.handle || p.sku;
+  const prev = beforeTags.get(h) || new Set();
+  for (const t of (p.tags || []).map(String)) { if (!prev.has(t) && SCOPE_B_VOCAB.has(t)) { want(h, t); scopeB++; } }
+}
+
+const PLAN = [...plan.entries()].map(([handle, set]) => ({ handle, add: [...set] }));
+console.error(`plan: ${PLAN.length} products  (Scope A adds=${scopeA}, Scope B adds=${scopeB})`);
+
+const template = await fs.readFile('scripts/_cork-apply-template.mjs', 'utf8');
+if (!template.includes('__PLAN__')) throw new Error('template missing __PLAN__ marker');
+const out = template.replace('__PLAN__', JSON.stringify(PLAN, null, 2));
+await fs.writeFile('scripts/apply-cork-tags-SELFCONTAINED.mjs', out);
+console.error('wrote scripts/apply-cork-tags-SELFCONTAINED.mjs');

← a663b22 cork: PROD tag-apply accepts BEFORE_PRODUCTS_JSON baseline (  ·  back to Corkwallcovering  ·  chore: lint (HTTP guard) + refactor (dead code, idiomatic co 850fc15 →