#!/usr/bin/env node /** * setup.js - Downloads/updates the JAE Image Skill reference library. * * Fully dynamic: reads manifest.json first to discover all categories. * New/renamed/removed categories are handled automatically — no hardcoding. * * Usage: * node scripts/setup.js # Download missing files only * node scripts/setup.js --force # Force re-download all references * node scripts/setup.js --check # Auto-update if stale (> 24h) * * Override source if needed: * JAE_IMAGE_SKILL_BASE_URL=https://example.com/references node scripts/setup.js --force */ import { existsSync, mkdirSync, statSync, writeFileSync, readFileSync, readdirSync, unlinkSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const refsDir = join(__dirname, '..', 'references'); const stampFile = join(refsDir, '.last-updated'); const BASE_URL = (process.env.JAE_IMAGE_SKILL_BASE_URL || 'https://jaeswift.xyz/skills/JAE-image-skill/references').replace(/\/$/, ''); const STALE_HOURS = Number(process.env.JAE_IMAGE_SKILL_STALE_HOURS || 24); function isStale() { if (!existsSync(stampFile)) return true; const ts = parseInt(readFileSync(stampFile, 'utf8').trim(), 10); return (Date.now() - ts) / 1000 / 3600 > STALE_HOURS; } async function fetchText(url) { if (typeof fetch !== 'function') { throw new Error('This installer requires Node.js 18+ with global fetch. Node 20+ is recommended.'); } const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status} — ${url}`); return res.text(); } async function setup() { const args = process.argv.slice(2); const forceMode = args.includes('--force'); const checkMode = args.includes('--check'); if (checkMode && !isStale()) { return; } if (!existsSync(refsDir)) mkdirSync(refsDir, { recursive: true }); const label = forceMode ? 'Updating' : 'Downloading'; console.log(`[setup] ${label} JAE Image Skill reference library from ${BASE_URL} ...`); let categories; try { const manifestText = await fetchText(`${BASE_URL}/manifest.json`); const manifest = JSON.parse(manifestText); categories = manifest.categories; if (!Array.isArray(categories) || categories.length === 0) throw new Error('manifest has no categories'); writeFileSync(join(refsDir, 'manifest.json'), manifestText, 'utf8'); console.log(` manifest: ${categories.length} categories, ${manifest.totalPrompts} prompts total`); } catch (err) { console.warn(`[setup] Could not fetch manifest: ${err.message}`); console.warn('[setup] Falling back to existing local manifest...'); const localManifest = join(refsDir, 'manifest.json'); if (!existsSync(localManifest)) { console.error('[setup] No manifest available. Install from the bundled repo/archive or retry with --force.'); process.exit(0); } categories = JSON.parse(readFileSync(localManifest, 'utf8')).categories; } const validFiles = new Set([...categories.map(c => c.file), 'manifest.json', '.last-updated', '.gitkeep']); if (forceMode && existsSync(refsDir)) { for (const f of readdirSync(refsDir)) { if (!validFiles.has(f)) { unlinkSync(join(refsDir, f)); console.log(` removed stale: ${f}`); } } } let downloaded = 0, skipped = 0, failed = 0; for (const cat of categories) { const dest = join(refsDir, cat.file); if (!forceMode && existsSync(dest) && statSync(dest).size > 100) { skipped++; continue; } process.stdout.write(` → ${cat.file} (${cat.title}, ${cat.count} prompts) ... `); try { const text = await fetchText(`${BASE_URL}/${cat.file}`); JSON.parse(text); writeFileSync(dest, text, 'utf8'); console.log('✓'); downloaded++; } catch (err) { console.log(`✗ (${err.message})`); failed++; } } if (failed === 0) writeFileSync(stampFile, String(Date.now()), 'utf8'); if (downloaded > 0) { console.log(`[setup] Done! ${downloaded} file(s) ${forceMode ? 'updated' : 'downloaded'}. JAE Image Skill is ready.`); } else if (skipped === categories.length) { console.log('[setup] All references up to date. Use --force to refresh.'); } if (failed > 0) console.warn(`[setup] ${failed} file(s) failed. Run again to retry.`); } setup().catch(err => { console.warn('[setup] Warning (non-fatal):', err.message); process.exit(0); });