#!/usr/bin/env bun import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs' import { join } from 'path' const root = process.cwd() const outRoot = join(root, 'content_population_exports') const dataforseoRoot = join(root, 'data', 'sources', 'dataforseo') const asinMapPath = join(dataforseoRoot, 'asin-map.csv') const asinCandidatesPath = join(dataforseoRoot, 'asin-candidates.csv') mkdirSync(outRoot, { recursive: true }) mkdirSync(dataforseoRoot, { recursive: true }) function csv(rows: any[], headers: string[]) { const esc = (value: any) => { if (value === undefined || value === null) value = '' if (Array.isArray(value)) value = value.join(' | ') value = String(value) return /[",\n\r]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value } return [headers.join(','), ...rows.map(row => headers.map(header => esc(row[header])).join(','))].join('\n') + '\n' } function parseCsv(text: string) { const rows: string[][] = [] let row: string[] = [] let cell = '' let quoted = false for (let index = 0; index < text.length; index += 1) { const char = text[index] const next = text[index + 1] if (quoted) { if (char === '"' && next === '"') { cell += '"'; index += 1 } else if (char === '"') quoted = false else cell += char } else if (char === '"') quoted = true else if (char === ',') { row.push(cell); cell = '' } else if (char === '\n') { row.push(cell); rows.push(row); row = []; cell = '' } else if (char !== '\r') cell += char } if (cell || row.length) { row.push(cell); rows.push(row) } const headers = rows.shift()?.map(header => header.trim()) || [] return rows.filter(values => values.some(value => value.trim())).map(values => Object.fromEntries(headers.map((header, index) => [header, (values[index] || '').trim()]))) } function url(base: string, params: Record) { const search = new URLSearchParams(params) return `${base}?${search.toString()}` } const generatedAt = new Date().toISOString() const asinRows = existsSync(asinMapPath) ? parseCsv(readFileSync(asinMapPath, 'utf8')) : [] const candidateRows = existsSync(asinCandidatesPath) ? parseCsv(readFileSync(asinCandidatesPath, 'utf8')) : [] const candidateBySku = new Map(candidateRows.map((row: any) => [row.SKU, row])) const queue = asinRows.map((row: any) => { const product = row.Product || row.SKU const query = `Just Vitamins ${product}`.replace(/\s+/g, ' ').trim() const asin = row.ASIN || '' const productUrl = row.ProductUrl || '' const candidate: any = candidateBySku.get(row.SKU) || {} const status = asin ? 'asin_present' : 'needs_manual_lookup' return { SKU: row.SKU, Product: product, Priority: row.Priority, Status: status, CurrentASIN: asin, CurrentProductUrl: productUrl, CandidateASIN: candidate.CandidateASIN || '', CandidateConfidence: candidate.Confidence || '', CandidateMatchStatus: candidate.MatchStatus || '', CandidateEvidenceUrl: candidate.EvidenceUrl || '', AmazonSearchUrl: url('https://www.amazon.co.uk/s', { k: query }), AmazonBrandSearchUrl: url('https://www.amazon.co.uk/s', { k: `Just Vitamins ${row.SKU}` }), WebSearchQuery: `${query} site:amazon.co.uk/dp OR site:amazon.co.uk/Just-Vitamins`, SuggestedAction: asin ? 'Verify ASIN/ProductUrl still match the target product before paid task creation.' : candidate.CandidateASIN ? 'Open CandidateEvidenceUrl and CandidateProductUrl, confirm exact Amazon product match, then paste ASIN/ProductUrl into asin-map.csv only if confidence is high.' : 'Open AmazonSearchUrl, confirm the exact Just Vitamins product, then paste ASIN and ProductUrl into data/sources/dataforseo/asin-map.csv.' } }) const ready = queue.length > 0 && queue.every(row => row.CurrentASIN) const manifest = { generatedAt, ready, counts: { targets: queue.length, asinPresent: queue.filter(row => row.CurrentASIN).length, missingAsins: queue.filter(row => !row.CurrentASIN).length }, guardrails: [ 'Manual lookup only; this script does not call Amazon, DataForSEO, or Apify.', 'Only accept an ASIN when the product title/brand clearly matches Just Vitamins and the target SKU/product.', 'Do not create a DataForSEO paid task until every Stage 1 ASIN has been reviewed.', 'Candidate ASINs from third-party pages are lookup aids only; they do not count as ready until confirmed on Amazon.' ], files: { asinMap: 'data/sources/dataforseo/asin-map.csv', asinCandidates: 'data/sources/dataforseo/asin-candidates.csv', asinCandidateReport: 'content_population_exports/dataforseo_asin_candidate_report.md', lookupQueueCsv: 'content_population_exports/dataforseo_asin_lookup_queue.csv', lookupQueueJson: 'content_population_exports/dataforseo_asin_lookup_queue.json' }, queue } writeFileSync(join(outRoot, 'dataforseo_asin_lookup_queue.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8') writeFileSync(join(outRoot, 'dataforseo_asin_lookup_queue.csv'), csv(queue, ['SKU', 'Product', 'Priority', 'Status', 'CurrentASIN', 'CurrentProductUrl', 'CandidateASIN', 'CandidateConfidence', 'CandidateMatchStatus', 'CandidateEvidenceUrl', 'AmazonSearchUrl', 'AmazonBrandSearchUrl', 'WebSearchQuery', 'SuggestedAction']), 'utf8') writeFileSync(join(outRoot, 'dataforseo_asin_lookup_queue.md'), [ '# DataForSEO ASIN lookup queue', '', `Generated: ${generatedAt}`, '', 'This is a no-spend prep queue for the Stage 1 DataForSEO pilot. It does not call Amazon, DataForSEO, or Apify.', '', `- Targets: ${manifest.counts.targets}`, `- ASINs present: ${manifest.counts.asinPresent}`, `- Missing ASINs: ${manifest.counts.missingAsins}`, '', '## Workflow', '1. Open `dataforseo_asin_lookup_queue.csv`.', '2. For each missing row, open the Amazon search URL. If a CandidateASIN is present, use it only as a shortcut to find/verify the Amazon product page.', '3. Paste confirmed ASIN and product URL into `data/sources/dataforseo/asin-map.csv` only after exact Amazon product match is confirmed.', '4. Rerun `bun run dataforseo:pilot`; the task payload should then show ready rows.', '5. Review the payload before making any paid DataForSEO call.', '', '## Guardrails', ...manifest.guardrails.map(item => `- ${item}`), '', '## Targets', ...queue.map(row => `- ${row.CurrentASIN ? '[ready]' : '[missing]'} ${row.SKU} - ${row.Product} (${row.Priority})`), '' ].join('\n'), 'utf8') console.log(`DataForSEO ASIN lookup ready: ${manifest.ready}`) console.log(`Targets: ${manifest.counts.targets}`) console.log(`ASINs present: ${manifest.counts.asinPresent}`) console.log(`Missing ASINs: ${manifest.counts.missingAsins}`)