/** * Carga las 33 piezas de Blue Parrot (oct 2026 - ene 2027) en creative_pipeline. * * Requiere: * 1. Haber aplicado sql/creative_pipeline.sql en Supabase. * 2. Haber descomprimido Blue-Parrot-creatividades-con-imagen.zip en una carpeta. * 3. .env.local con NEXT_PUBLIC_SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY reales * (los mismos que usa la app en producción — ejecutar esto en el propio VPS, * por ejemplo desde "Open Terminal" en Dokploy, nunca pegando la service * role key en un chat). * * Uso: * npx tsx scripts/seed_blue_parrot_pipeline.ts --dir /ruta/a/Blue-Parrot-creatividades-con-imagen \ * [--status revision_interna] [--dry-run] */ import { config } from 'dotenv'; import { resolve, join } from 'path'; import { existsSync, readFileSync } from 'fs'; import { createClient } from '@supabase/supabase-js'; config({ path: resolve(process.cwd(), '.env.local') }); // Mismo tenant_id que scripts/provision_victor_bps.ts const BLUE_PARROT_TENANT_ID = '8bec977a-6032-4759-aa7f-b5042081f450'; const BUCKET = 'tenant-assets'; type SeedItem = { id: string; format: 'post' | 'story' | 'reel' | 'carrusel'; month: string; category: string; title: string; }; function arg(name: string, fallback?: string): string | undefined { const i = process.argv.indexOf(`--${name}`); if (i === -1) return fallback; return process.argv[i + 1]; } const DRY_RUN = process.argv.includes('--dry-run'); async function main() { const url = process.env.NEXT_PUBLIC_SUPABASE_URL?.trim(); const key = process.env.SUPABASE_SERVICE_ROLE_KEY?.trim(); if (!url || !key) throw new Error('Faltan NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY en .env.local'); const dir = arg('dir'); if (!dir || !existsSync(dir)) { throw new Error('Pasa --dir /ruta/a/la/carpeta/descomprimida (el ZIP de las 33 piezas)'); } const status = arg('status', 'revision_agencia')!; const metaPath = resolve(__dirname, 'blue-parrot-pipeline-seed.json'); const items: SeedItem[] = JSON.parse(readFileSync(metaPath, 'utf-8')); const admin = createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } }); // Asegura el bucket (no falla si ya existe). const { data: buckets } = await admin.storage.listBuckets(); if (!buckets?.some((b) => b.id === BUCKET)) { await admin.storage.createBucket(BUCKET, { public: false }); } let ok = 0; let failed = 0; for (const it of items) { const isCarrusel = it.format === 'carrusel'; const coverLocal = isCarrusel ? join(dir, `${it.id}-carrusel`, `${it.id}-1.jpg`) : join(dir, `${it.id}-${it.format}.jpg`); if (!existsSync(coverLocal)) { console.error(`[SKIP] ${it.id}: no existe ${coverLocal}`); failed++; continue; } console.log(`[${it.id}] ${it.title}`); if (DRY_RUN) { ok++; continue; } // 1) upsert de la fila (sin imágenes todavía) para obtener el id. const { data: row, error: upErr } = await admin .from('creative_pipeline') .upsert( { tenant_id: BLUE_PARROT_TENANT_ID, external_ref: it.id, title: it.title, format: it.format, month: it.month, status, notes: it.category, }, { onConflict: 'tenant_id,external_ref' } ) .select('id') .single(); if (upErr || !row) { console.error(`[FAIL] ${it.id}: ${upErr?.message}`); failed++; continue; } const itemId = row.id as string; // 2) sube portada const coverPath = `${BLUE_PARROT_TENANT_ID}/pipeline/${itemId}/cover.jpg`; const coverBuf = readFileSync(coverLocal); const coverUp = await admin.storage.from(BUCKET).upload(coverPath, coverBuf, { contentType: 'image/jpeg', upsert: true, }); if (coverUp.error) { console.error(`[FAIL cover] ${it.id}: ${coverUp.error.message}`); failed++; continue; } // 3) sube slides (carrusel: 2,3,4,5 → 4 slides interiores + CTA) const slidePaths: string[] = []; if (isCarrusel) { for (let n = 2; n <= 5; n++) { const local = join(dir, `${it.id}-carrusel`, `${it.id}-${n}.jpg`); if (!existsSync(local)) continue; const path = `${BLUE_PARROT_TENANT_ID}/pipeline/${itemId}/slide-${n - 1}.jpg`; const buf = readFileSync(local); const up = await admin.storage.from(BUCKET).upload(path, buf, { contentType: 'image/jpeg', upsert: true, }); if (up.error) { console.error(`[FAIL slide ${n}] ${it.id}: ${up.error.message}`); continue; } slidePaths.push(path); } } // 4) cierra la fila con las rutas de storage const { error: patchErr } = await admin .from('creative_pipeline') .update({ cover_path: coverPath, cover_mime: 'image/jpeg', slide_paths: slidePaths }) .eq('id', itemId); if (patchErr) { console.error(`[FAIL patch] ${it.id}: ${patchErr.message}`); failed++; continue; } ok++; } console.log(`\nHecho: ${ok} ok, ${failed} con error, de ${items.length} piezas.`); if (failed > 0) process.exitCode = 1; } main().catch((e) => { console.error(e); process.exitCode = 1; });