/** * Icon Pipeline — orchestrates the full icon generation workflow * * Steps: * 1. Vectorize — convert PNG/JPG to SVG (skipped if no raster files or --skip-vectorize) * 2. Validate — check SVG quality (exits on errors) * 3. Normalize — normalize SVG viewBox dimensions to 32x32 * 4. Generate — emit light.tsx + dark.tsx + meta.ts per logo * 5. Avatars — emit avatar.tsx + index.tsx + barrel + catalog * * Usage: * tsx scripts/pipeline.ts --dir=providers * tsx scripts/pipeline.ts --dir=models --skip-vectorize * tsx scripts/pipeline.ts --dir=providers --force */ import { spawn } from 'child_process' import fs from 'fs/promises' import path from 'path' type SourceDir = 'providers' | 'models' interface PipelineArgs { dir: SourceDir skipVectorize: boolean force: boolean } interface StepResult { name: string command: string status: 'success' | 'skipped' | 'failed' durationMs: number exitCode?: number } function parseArgs(): PipelineArgs { const args = process.argv.slice(2) let dir: SourceDir | undefined let skipVectorize = false let force = false for (const arg of args) { if (arg.startsWith('--dir=')) { const value = arg.split('=')[1] if (value === 'providers' || value === 'models') { dir = value } else { console.error(`Invalid --dir value: ${value}. Use "providers" or "models".`) process.exit(1) } } else if (arg === '--skip-vectorize') { skipVectorize = true } else if (arg === '--force') { force = true } } if (!dir) { console.error('Usage: tsx scripts/pipeline.ts --dir=providers|models [--skip-vectorize] [--force]') process.exit(1) } return { dir, skipVectorize, force } } /** * Check if a directory contains any raster files (PNG/JPG/JPEG). */ async function hasRasterFiles(dir: string): Promise { const rasterExts = new Set(['.png', '.jpg', '.jpeg']) try { const entries = await fs.readdir(dir) return entries.some((f) => rasterExts.has(path.extname(f).toLowerCase())) } catch { return false } } /** * Run a command as a child process, streaming stdout/stderr. * Returns the exit code. */ function runStep(command: string, args: string[], cwd: string): Promise { return new Promise((resolve, reject) => { const child = spawn(command, args, { cwd, stdio: 'inherit', shell: false }) child.on('error', reject) child.on('close', (code) => resolve(code ?? 1)) }) } async function main() { const { dir, skipVectorize, force } = parseArgs() const uiRoot = path.join(__dirname, '..') const iconsDir = path.join(uiRoot, 'icons', dir) console.log(`\n========================================`) console.log(` Icon Pipeline: ${dir}`) console.log(`========================================\n`) const results: StepResult[] = [] const steps: Array<{ name: string args: string[] skip?: () => Promise | boolean skipReason?: string }> = [ { name: 'Vectorize', args: ['scripts/vectorize-logo.ts', `--dir=${dir}`], skip: async () => skipVectorize || !(await hasRasterFiles(iconsDir)), skipReason: skipVectorize ? '--skip-vectorize flag' : 'no raster files found' }, { name: 'Validate', args: ['scripts/validate-svgs.ts', `--dir=${dir}`] }, { name: 'Normalize', args: ['scripts/normalize-viewbox.ts', `--dir=${dir}`] }, { name: 'Generate', args: ['scripts/generate-icons.ts', `--type=${dir}`, ...(force ? ['--force'] : [])] }, { name: 'Avatars', args: ['scripts/generate-avatars.ts', `--type=${dir}`] } ] const pipelineStart = Date.now() for (const step of steps) { const shouldSkip = step.skip ? await step.skip() : false if (shouldSkip) { const reason = step.skipReason || 'condition not met' console.log(`[${step.name}] Skipped (${reason})\n`) results.push({ name: step.name, command: `tsx ${step.args.join(' ')}`, status: 'skipped', durationMs: 0 }) continue } const cmd = `tsx ${step.args.join(' ')}` console.log(`[${step.name}] Running: ${cmd}`) console.log('─'.repeat(50)) const stepStart = Date.now() try { const exitCode = await runStep('tsx', step.args, uiRoot) const durationMs = Date.now() - stepStart if (exitCode !== 0) { console.log(`\n[${step.name}] Failed with exit code ${exitCode} (${durationMs}ms)\n`) results.push({ name: step.name, command: cmd, status: 'failed', durationMs, exitCode }) break } console.log(`\n[${step.name}] Done (${durationMs}ms)\n`) results.push({ name: step.name, command: cmd, status: 'success', durationMs }) } catch (error) { const durationMs = Date.now() - stepStart console.error(`\n[${step.name}] Error: ${error}\n`) results.push({ name: step.name, command: cmd, status: 'failed', durationMs, exitCode: 1 }) break } } const totalMs = Date.now() - pipelineStart // Summary console.log('\n========================================') console.log(' Summary') console.log('========================================\n') const nameWidth = Math.max(...results.map((r) => r.name.length), 4) console.log(` ${'Step'.padEnd(nameWidth)} Status Duration`) console.log(` ${'─'.repeat(nameWidth)} ──────── ────────`) for (const r of results) { const statusIcon = r.status === 'success' ? 'OK' : r.status === 'skipped' ? 'SKIP' : 'FAIL' const duration = r.status === 'skipped' ? '-' : `${r.durationMs}ms` console.log(` ${r.name.padEnd(nameWidth)} ${statusIcon.padEnd(8)} ${duration}`) } console.log(`\n Total: ${totalMs}ms`) const failed = results.some((r) => r.status === 'failed') if (failed) { console.log('\n Pipeline failed. Fix the errors above and re-run.\n') process.exit(1) } console.log('\n Pipeline completed successfully.\n') } void main()