← 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
A scripts/_cork-apply-template.mjsA scripts/apply-cork-tags-SELFCONTAINED.mjsA scripts/gen-selfcontained-prod.mjs
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 →