diff --git a/.env.example b/.env.example index 046cf284..fd5d2e46 100644 --- a/.env.example +++ b/.env.example @@ -1,21 +1,24 @@ -# Copia este archivo a `.env.local` si quieres personalizar el studio manualmente. -# Tambien puedes ejecutar `bun run studio:init`: si `.env.local` no existe, el script lo crea automaticamente. +# Copy this file to `.env.local` if you want to customize the studio manually. +# You can also run `bun run studio:init`: if `.env.local` does not exist, the script creates it automatically. # -# Ruta por defecto en Windows. -# Para macOS o Linux usa una ruta absoluta local, por ejemplo: -# /Users//AI-Studio-Library -# /home//AI-Studio-Library +# Default path on Windows. +# For macOS or Linux, use a local absolute path, for example: +# /Users//AI-Studio-Library +# /home//AI-Studio-Library STUDIO_LIBRARY_DIR=D:\AI-Studio-Library -# Puerto HTTP del backend local. -STUDIO_SERVER_PORT=4317 +# HTTP port for the local backend. +STUDIO_SERVER_PORT=17223 -# Puerto WebSocket usado por codex app-server. -STUDIO_CODEX_WS_PORT=4318 +# WebSocket port used by codex app-server. +STUDIO_CODEX_WS_PORT=17224 -# Cantidad maxima de jobs Codex procesados en paralelo por el worker local. +# Optional UI override for the local backend base URL used by Vite. +VITE_STUDIO_API_BASE=http://localhost:17223 + +# Maximum number of Codex jobs processed in parallel by the local worker. STUDIO_MAX_CONCURRENT_CODEX_JOBS=4 -# Modelo y esfuerzo por defecto para turns de imagegen en codex app-server. +# Default model and reasoning effort for imagegen turns in codex app-server. CODEX_IMAGEGEN_MODEL=gpt-5.4-mini CODEX_IMAGEGEN_REASONING_EFFORT=low diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..a09087fb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,54 @@ +name: Bug report +description: Report a reproducible problem in Codex Studio. +title: '[Bug]: ' +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for helping improve Codex Studio. Please include enough detail to reproduce the issue locally. + - type: input + id: os + attributes: + label: Operating system + placeholder: Windows 11, macOS 15, Ubuntu 24.04 + validations: + required: true + - type: input + id: bun + attributes: + label: Bun version + placeholder: bun --version + validations: + required: true + - type: input + id: codex + attributes: + label: Codex version + placeholder: codex --version + validations: + required: true + - type: input + id: command + attributes: + label: Command used + placeholder: bun run dev + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs or screenshots + description: Attach relevant logs from your Studio Library logs folder or `logs/tooling/`. Remove secrets before posting. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..7630ad8d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Security report + url: https://github.com/ + about: Please report vulnerabilities privately through the repository owner's preferred security channel. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..36694352 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,32 @@ +name: Feature request +description: Propose an improvement for Codex Studio. +title: '[Feature]: ' +labels: [enhancement] +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What user problem would this solve? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposal + description: What should Codex Studio do differently? + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: What other options did you consider? + - type: checkboxes + id: scope + attributes: + label: Scope + options: + - label: This keeps the main flow local-first. + - label: This does not require API keys for the default Codex flow. + - label: This improves onboarding, operability, traceability, or maintainability. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..65697ae8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +## Summary + +- + +## Validation + +- [ ] `bun run fmt:check` +- [ ] `bun run lint` +- [ ] `bun run check` +- [ ] `bun run test` +- [ ] `bun run build` + +## Notes + +- Link related issues, ADRs, or docs. +- Mention any skipped validation with the reason and risk. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..9788b6a3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + - master + +jobs: + validate: + name: Validate + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.13 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Check + run: bun run check + + - name: Test + run: bun run test + + - name: Build + run: bun run build diff --git a/.gitignore b/.gitignore index 196966b8..d25a11a9 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,8 @@ components/recipes/styles/defaults/.locks/ components/recipes/styleRuntimeData.generated.check.*.tmp.ts components/recipes/styleRuntimePacks.generated/*.check.*.tmp.ts components/recipes/styleRuntimePacks.generated/**/*.check.*.tmp.ts +react-doctor-*.json +!react-doctor.config.json # Editor directories and files .vscode/* diff --git a/.tmp_audit_styles.cjs b/.tmp_audit_styles.cjs new file mode 100644 index 00000000..662039b1 --- /dev/null +++ b/.tmp_audit_styles.cjs @@ -0,0 +1,90 @@ +const fs = require('fs'); +const path = require('path'); +const YAML = require('yaml'); + +const packs = ['pack_01','pack_02','pack_03','pack_04','pack_05','pack_07','pack_09','pack_11','pack_12','pack_13','pack_14','pack_15','pack_16']; +const packDir = path.join('components','recipes','styles','manifests','packs'); +const presetDir = path.join('components','recipes','styles','manifests','presets'); + +function readYaml(p){ return YAML.parse(fs.readFileSync(p,'utf8')); } +function walk(dir){ + let out=[]; + for(const e of fs.readdirSync(dir,{withFileTypes:true})){ + const full = path.join(dir,e.name); + if(e.isDirectory()) out=out.concat(walk(full)); + else if(/\.ya?ml$/i.test(e.name)) out.push(full); + } + return out; +} + +const packAudit=[]; +let reorganizedPresetRefs = new Set(); +for(const p of packs){ + const f = path.join(packDir,`${p}.yaml`); + if(!fs.existsSync(f)) { packAudit.push({pack:p,error:'missing'}); continue; } + const y = readYaml(f) || {}; + const categories = Array.isArray(y.categories)?y.categories:[]; + const catRows = categories.map(c=>({id:c?.id||'(no-id)',count:Array.isArray(c?.presetRefs)?c.presetRefs.length:0,zero:!Array.isArray(c?.presetRefs)||c.presetRefs.length===0})); + const totalPresetRefs = Array.isArray(y.presetRefs)?y.presetRefs.length:0; + packAudit.push({pack:p,totalPresetRefs,categories:catRows,zeroCategories:catRows.filter(r=>r.zero).map(r=>r.id)}); + for(const ref of (Array.isArray(y.presetRefs)?y.presetRefs:[])) reorganizedPresetRefs.add(ref); +} + +const legacySlugs = ['videojuegos','videojuegos-originals-vault','mythic_noir','solarpunk_dreamscapes','core_anime','slice_of_life_school_music','samurai_medieval']; +const presetFiles = walk(presetDir); +const legacyCounts = Object.fromEntries(legacySlugs.map(s=>[s,{count:0,files:[]}])) +for(const f of presetFiles){ + const txt = fs.readFileSync(f,'utf8').toLowerCase(); + for(const s of legacySlugs){ + if(txt.includes(s)){ + legacyCounts[s].count++; + if(legacyCounts[s].files.length<5) legacyCounts[s].files.push(f.replace(/\\/g,'/')); + } + } +} + +function collectStrings(v, arr){ + if(v==null) return; + if(typeof v==='string') arr.push(v); + else if(Array.isArray(v)) v.forEach(x=>collectStrings(x,arr)); + else if(typeof v==='object') Object.values(v).forEach(x=>collectStrings(x,arr)); +} +const esTokenRegex = /\b(de|la|el|los|las|y|con|para|en|del|una|un|estilo|fotografia|fotografía|cine|retrato|paisaje)\b/i; +const tildeRegex = /[áéíóúñü¿¡]/i; +let esHits = 0; +const esFiles = []; +for(const ref of reorganizedPresetRefs){ + const p = path.join(presetDir,ref); + if(!fs.existsSync(p)) continue; + let y; + try{ y = readYaml(p) || {}; }catch{ continue; } + const values = []; + collectStrings(y.name, values); + collectStrings(y.category, values); + collectStrings(y.tags, values); + collectStrings(y.taxonomy, values); + const joined = values.join(' | '); + if(tildeRegex.test(joined) || esTokenRegex.test(joined)){ + esHits++; + if(esFiles.length<20) esFiles.push(ref); + } +} + +console.log('===PACK_AUDIT==='); +for(const p of packAudit){ + if(p.error){ console.log(`${p.pack}: MISSING`); continue; } + console.log(`${p.pack}: totalPresetRefs=${p.totalPresetRefs}`); + for(const c of p.categories){ + console.log(` - ${c.id}: ${c.count}${c.zero?' [ZERO]':''}`); + } +} +console.log('===LEGACY_SLUGS==='); +for(const s of legacySlugs){ + const v=legacyCounts[s]; + console.log(`${s}: ${v.count}`); + if(v.files.length) console.log(` samples: ${v.files.join(', ')}`); +} +console.log('===SPANISH_DETECTION==='); +console.log(`reorganizedPresetFilesChecked=${reorganizedPresetRefs.size}`); +console.log(`filesWithSpanishSignal=${esHits}`); +if(esFiles.length) console.log(`samples: ${esFiles.join(', ')}`); diff --git a/.tmp_scene_analysis.js b/.tmp_scene_analysis.js new file mode 100644 index 00000000..4f7bdd32 --- /dev/null +++ b/.tmp_scene_analysis.js @@ -0,0 +1,155 @@ +const fs = require('fs'); +const path = require('path'); +let yaml = null; +try { yaml = require('js-yaml'); } catch (e) { + try { yaml = require('yaml'); } catch (e2) {} +} +if (!yaml) { + console.error('No YAML parser found (js-yaml or yaml).'); + process.exit(1); +} +const root = path.resolve('components/recipes/styles/manifests/presets'); +function walk(dir, out=[]) { + if (!fs.existsSync(dir)) return out; + for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, ent.name); + if (ent.isDirectory()) walk(p, out); + else if (ent.isFile() && /\.ya?ml$/i.test(ent.name)) out.push(p); + } + return out; +} +const files = walk(root); +const presets = []; +function traverse(node, ctx, file) { + if (Array.isArray(node)) { + node.forEach((v,i)=>traverse(v,{...ctx,index:i},file)); + return; + } + if (!node || typeof node !== 'object') return; + const next = { ...ctx }; + if (typeof node.pack === 'string') next.pack = node.pack; + if (typeof node.category === 'string') next.category = node.category; + if (typeof node.id === 'string') next.id = node.id; + if (typeof node.presetId === 'string') next.id = node.presetId; + if (typeof node.slug === 'string') next.id = node.slug; + if (typeof node.name === 'string' && !next.id) next.id = node.name; + + if (typeof node.visualDna === 'string') { + const rel = path.relative(root, file).replace(/\\/g,'/'); + const seg = rel.split('/'); + const packFromPath = seg.length > 1 ? seg[0] : 'unknown'; + const catFromPath = seg.length > 2 ? seg[1] : 'unknown'; + presets.push({ + id: next.id || path.basename(file, path.extname(file)), + visualDna: node.visualDna, + pack: next.pack || packFromPath, + category: next.category || catFromPath, + file: rel + }); + } + for (const [k,v] of Object.entries(node)) { + if (k === 'visualDna') continue; + traverse(v, next, file); + } +} +for (const f of files) { + try { + const txt = fs.readFileSync(f, 'utf8'); + const doc = yaml.load ? yaml.load(txt) : yaml.parse(txt); + traverse(doc, {}, f); + } catch (e) { + // ignore parse failures + } +} + +function norm(s){return s.toLowerCase().replace(/[^a-z0-9\s-]/g,' ').replace(/\s+/g,' ').trim();} +function tokens(s){return norm(s).split(' ').filter(Boolean);} +const stop = new Set(['the','a','an','and','or','with','of','to','for','from','by','as','is','are','on','in','at','into','over','under','through','within','without','this','that','these','those','it','its']); +const sceneKeywords = { + 'scene':3,'city':3,'street':3,'forest':3,'room':3,'battle':4,'warrior':3,'castle':3,'sunset':3, + 'night':2,'neon':2,'village':2,'temple':2,'mountain':2,'ocean':2,'river':2,'desert':2,'sky':2, + 'alley':2,'corridor':2,'landscape':3,'interior':3,'exterior':3,'town':2,'market':2,'bridge':2, + 'park':2,'station':2,'rooftop':2,'streetlight':2,'ruins':2,'battlefield':4,'throne':2,'palace':2, + 'harbor':2,'dock':2,'cave':2,'dungeon':2,'cathedral':2,'plaza':2,'district':2 +}; +const phraseWeights = { + 'in a':2,'in an':2,'in the':2,'at night':4,'on a':2,'on the':2,'inside a':3,'inside the':3, + 'outside a':3,'outside the':3,'set in':3,'located in':3,'during sunset':4,'neon city':5, + 'city street':4,'forest clearing':4,'battle scene':5,'wide shot':2,'establishing shot':4, + 'in front of':3,'background':2 +}; + +const wordCounts = new Map(); +const ngramCounts = new Map(); +const presetScores = []; +for (const p of presets) { + const tks = tokens(p.visualDna); + tks.forEach(t=>{ if(!stop.has(t) && t.length>2){ wordCounts.set(t,(wordCounts.get(t)||0)+1);} }); + for (let n=2;n<=5;n++) { + for (let i=0;i<=tks.length-n;i++) { + const ng = tks.slice(i,i+n).join(' '); + if (/^\d+$/.test(ng)) continue; + if (ng.split(' ').every(w=>stop.has(w))) continue; + ngramCounts.set(ng,(ngramCounts.get(ng)||0)+1); + } + } + + const text = norm(p.visualDna); + let score = 0; + const reasons = []; + const preps = (text.match(/\b(in|at|on|inside|outside|within|amid|under|over|near|beside|between)\b/g)||[]).length; + if (preps) { const s=Math.min(6,preps); score+=s; reasons.push(`prepositions:${preps}`); } + for (const [k,w] of Object.entries(sceneKeywords)) { + const m = text.match(new RegExp(`\\b${k}\\b`,'g')); + if (m) { score += m.length*w; reasons.push(`${k}x${m.length}`); } + } + for (const [ph,w] of Object.entries(phraseWeights)) { + const m = text.match(new RegExp(ph.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'),'g')); + if (m) { score += m.length*w; reasons.push(`${ph}x${m.length}`); } + } + presetScores.push({ ...p, score, reasons: reasons.slice(0,8) }); +} + +const sceneTerms = [...wordCounts.entries()] + .filter(([w,c])=> (sceneKeywords[w] || c>=5)) + .sort((a,b)=>b[1]-a[1]).slice(0,40) + .map(([term,count])=>({term,count})); + +const topNgrams = [...ngramCounts.entries()] + .filter(([ng,c])=>c>=3 && ng.split(' ').length>=2 && ng.split(' ').length<=5) + .sort((a,b)=> b[1]-a[1] || b[0].split(' ').length-a[0].split(' ').length) + .slice(0,60) + .map(([ngram,count])=>({ngram,count,n:ngram.split(' ').length})); + +const problematic = [...presetScores].sort((a,b)=>b.score-a.score).slice(0,30).map(x=>({ + id:x.id, pack:x.pack, category:x.category, score:x.score, reasons:x.reasons, + visualDna:x.visualDna.length>170?x.visualDna.slice(0,170)+'...':x.visualDna +})); + +function groupAvg(arr, key) { + const m = new Map(); + for (const p of arr) { + const k = p[key] || 'unknown'; + const cur = m.get(k) || {sum:0,count:0}; + cur.sum += p.score; cur.count += 1; m.set(k, cur); + } + return [...m.entries()].map(([k,v])=>({[key]:k,count:v.count,avgScore:+(v.sum/v.count).toFixed(2)})) + .sort((a,b)=>b.avgScore-a.avgScore || b.count-a.count); +} +const byPack = groupAvg(presetScores,'pack'); +const byCategory = groupAvg(presetScores,'category'); + +const examples = [...presetScores].filter(p=>p.score>=8).sort((a,b)=>b.score-a.score).slice(0,20).map(p=>({ + id:p.id, score:p.score, visualDna:p.visualDna.length>130?p.visualDna.slice(0,130)+'...':p.visualDna +})); + +const result = { + scannedYamlFiles: files.length, + totalPresets: presets.length, + sceneTermsTop: sceneTerms, + repeatedNgramsTop: topNgrams, + problematicTop30: problematic, + scoreDistribution: { byPack, byCategory }, + sceneExamples: examples +}; +console.log(JSON.stringify(result, null, 2)); diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..709011db --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,27 @@ +# Código de conducta + +## Nuestro compromiso + +Nos comprometemos a que participar en Codex Studio sea una experiencia respetuosa, inclusiva y libre de acoso para todas las personas. + +## Comportamiento esperado + +- Trato respetuoso y constructivo. +- Asumir buena intención, aceptando feedback cuando el impacto no coincide con la intención. +- Mantener foco en el proyecto y en quienes lo usan. +- Priorizar reportes claros, ejemplos reproducibles y propuestas concretas. + +## Comportamiento no aceptable + +- Acoso, amenazas, insultos o lenguaje discriminatorio. +- Contenido sexualizado en espacios del proyecto. +- Publicación de información privada sin permiso explícito. +- Interrupción sostenida de issues, PRs, discusiones o revisiones. + +## Aplicación + +El equipo mantenedor puede moderar comentarios, cerrar issues, bloquear usuarios o tomar otras acciones razonables cuando se incumpla este código. + +Si necesitas reportar un problema, usa el canal privado indicado por la persona o equipo propietario del repositorio. + +Este código está adaptado de Contributor Covenant v2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5bf12a7e..c1572f0b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,14 +1,15 @@ -# Contribuir a Codex Studio +# Guía de contribución para Codex Studio -Gracias por ayudar a convertir este repo en un producto open-source mas solido y mas facil de instalar. +Gracias por ayudar a convertir este repo en un proyecto open-source más robusto, claro e instalable. -## Antes de abrir una PR +## Ruta rápida para contribuir -- Revisa el contexto del producto en `README.md` y `ROADMAP.md`. -- Si tu cambio toca arquitectura o sincronizacion local, mira tambien `docs/ARCHITECTURE.md` y `docs/SERVICES.md`. -- Si agregas o modificas una receta, revisa `docs/DEV_GUIDE.md`. +1. Lee `README.md` y `ROADMAP.md`. +2. Levanta el entorno local con `bun run studio:init` + `bun run dev`. +3. Haz un cambio pequeño, validable y con contexto. +4. Corre checks mínimos antes del PR. -## Setup local recomendado +## Setup recomendado ```bash bun install @@ -16,22 +17,20 @@ bun run studio:init bun run dev ``` -Si prefieres separar procesos: +Opcional por separado: ```bash bun run dev:server bun run dev:ui ``` -## Requisitos para contribuir bien +## Requisitos -- Tener **Bun** disponible en PATH. -- Tener **Codex CLI** instalado y autenticado localmente. -- No depender de API keys para el flujo principal del producto. +- Bun disponible en PATH. +- Codex CLI instalado y autenticado localmente. +- No depender de API keys para el flujo principal. -## Checklist minima para cambios de codigo - -Antes de abrir una PR, intenta dejar esto en verde: +## Checklist antes de abrir PR ```bash bun run fmt:check @@ -41,41 +40,37 @@ bun run test bun run build ``` -Si tu cambio toca onboarding, setup o DX, actualiza la documentacion correspondiente en la misma PR. +## Convenciones importantes -## Convenciones importantes del repo +- No commitear assets generados, logs, DBs SQLite ni contenido local de librería. +- No commitear `.env.local` ni secretos reales. +- Mantener experiencia local-first funcional sin `OPENAI_API_KEY`. +- Documentar variables nuevas y scripts públicos en `README.md`. +- Si cambias decisiones estructurales, deja rastro en `docs/adr/`. -- No subir assets generados, logs, bases SQLite ni contenido de la biblioteca local. -- No commitear `.env.local`, `.env` con datos reales ni rutas especificas de tu maquina. -- Mantener la UI local-first: la experiencia principal debe seguir funcionando sin `OPENAI_API_KEY`. -- Si agregas una nueva variable de entorno o script publico, documentalo en `README.md`. -- Si cambias una decision estructural relevante, deja evidencia en `docs/adr/` o al menos en la documentacion tecnica afectada. -- Las tareas de calidad principales deben seguir escribiendo logs en `logs/tooling/`. -- Las nuevas pruebas unitarias deben escribirse con `vite-plus/test`. +## Cómo reportar bugs útiles -## Como reportar bugs utiles +Incluye: -Cuando abras un issue o describas un problema, incluye: +- SO +- versión de Bun (`bun --version`) +- versión de Codex (`codex --version`) +- comando ejecutado +- resultado esperado vs real +- logs relevantes (`logs/tooling/` o logs de Studio Library) -- sistema operativo; -- version de Bun; -- version de Codex (`codex --version`); -- version de Bun (`bun --version`); -- comando usado (`bun run dev`, `bun run dev:server`, etc.); -- que esperabas que ocurriera; -- que ocurrio realmente; -- logs o capturas relevantes de la carpeta `logs/` dentro de tu Studio Library (por ejemplo `%USERPROFILE%\AI-Studio-Library\logs` en Windows), `logs/tooling/` o de tu directorio equivalente. +## Qué aportes tienen más impacto hoy -## Cambios especialmente valiosos ahora mismo +- onboarding y mensajes de error +- compatibilidad Windows/macOS/Linux +- trazabilidad de jobs y assets +- documentación pública +- claridad de copy/UX en la UI -Durante esta etapa de preparacion open-source, ayudan mucho las PRs que mejoren: +## Estilo de contribución -- onboarding y mensajes de error; -- compatibilidad Windows/macOS/Linux; -- trazabilidad de jobs y assets; -- documentacion publica; -- limpieza de copy, naming y affordances de la UI. +Preferimos cambios pequeños, explicables y fáciles de verificar. Menos heroicidad; más claridad. -## Estilo de contribucion +## Código de conducta -Preferimos cambios pequeños, explicables y faciles de validar. Si vas a hacer una limpieza grande o una reorganizacion fuerte, abre primero una issue o deja una nota de enfoque para alinear la direccion. Menos heroics, mas claridad. +Este proyecto sigue [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md). diff --git a/README.md b/README.md index 5cfd08c2..c339a132 100644 --- a/README.md +++ b/README.md @@ -1,171 +1,131 @@ # Codex Studio -Studio local-first para generar, revisar y administrar imagenes con la sesion autenticada de Codex/ChatGPT del usuario a traves de `codex app-server`. +> Estudio de imágenes local-first que usa tu sesión autenticada de Codex/ChatGPT — sin `OPENAI_API_KEY` para el flujo principal. -> Estado actual: **preview open-source temprana**. La base tecnica ya es util localmente; ahora el objetivo es pulir onboarding, documentacion y ergonomia para que mas gente pueda instalarla sin conocer el historial interno del repo. +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) +[![Bun](https://img.shields.io/badge/runtime-Bun-black?logo=bun)](https://bun.sh) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue?logo=typescript)](https://www.typescriptlang.org/) -## Que hace interesante a este proyecto +**Estado actual: preview open-source temprana.** La base técnica ya funciona bien en local. El foco ahora es dejar onboarding, documentación y DX en nivel “instalable en minutos”. -- **No depende de `OPENAI_API_KEY`** para el flujo principal. -- **Usa la sesion local de Codex/ChatGPT** ya autenticada en la maquina. -- **Mantiene una cola persistente** con jobs locales y trazabilidad en SQLite. -- **Guarda assets, logs y transcripts fuera del repo**, en una biblioteca local configurable. -- **Conserva la UI creativa original** con recetas, workspaces, grid visual y herramientas de revision. +## Ruta rápida -## Como funciona +1. Instala dependencias y prepara la librería local: + - `bun install` + - `bun run studio:init` +2. Arranca el entorno: + - `bun run dev` +3. Verifica que todo responde: + - UI: + - API local: -1. La UI React/Vite recibe prompts, recetas, referencias y acciones del usuario. -2. El backend local Bun/Hono crea y supervisa jobs persistentes. -3. `codex app-server` ejecuta los turns reales de Codex para generar o editar imagenes. -4. La biblioteca local guarda assets, SQLite, transcripts y logs operativos. -5. La UI hace catch-up por HTTP, escucha actividad viva por SSE y mantiene un cache visual compatible para que el studio siga siendo usable aunque la generacion ocurra fuera del navegador. - -## Modelo de datos actual - -- **SQLite + Image Catalog** son la fuente duradera de verdad para jobs, imagenes catalogadas, libraries y logs. -- **IndexedDB** sigue guardando `GenerationBatch[]` en `catalog-cache` como cache visual de compatibilidad para el grid actual. -- **`GET /api/events`** distribuye eventos vivos de jobs, logs y assets para el frontend. -- **`/api/codex/session`** es la lectura canonica de la sesion local Codex/ChatGPT usada por onboarding y diagnosticos. - -## Requisitos previos - -Antes de levantar el studio conviene tener esto listo: - -- **Bun** instalado y disponible en PATH. -- **Codex CLI** instalado y autenticado con **ChatGPT login** en la misma maquina. -- Soporte para `codex app-server` desde esa instalacion de Codex. -- Un navegador moderno con soporte para IndexedDB. - -Si falta Codex o la sesion local no esta disponible, la UI puede iniciar pero la generacion real no va a completar. En ese caso revisa [`docs/TROUBLESHOOTING.md`](./docs/TROUBLESHOOTING.md). - -## Inicio rapido - -```bash -bun install -bun run studio:init -bun run dev -``` +## ¿Por qué llama la atención este proyecto? -Despues de eso deberias tener: +- **No exige API key** para el flujo principal con Codex. +- **Aprovecha tu sesión local** autenticada de Codex/ChatGPT. +- **Cola persistente de jobs** con trazabilidad sobre SQLite. +- **Assets, logs y transcripts fuera del repo**, en una Studio Library configurable. +- **UI creativa completa**: recetas, workspaces, grid visual y herramientas de revisión. +- **Arquitectura extensible** con frontera de proveedores (Codex-first). -- UI: -- API local: -- Biblioteca por defecto: `~/AI-Studio-Library` (en Windows normalmente `%USERPROFILE%\AI-Studio-Library`) -- Logs: `~/AI-Studio-Library/logs` -- SQLite: `~/AI-Studio-Library/db/studio.sqlite` +## Cómo funciona -### Primer arranque - -`bun run studio:init` crea la estructura de la biblioteca local, inicializa SQLite, genera un proyecto default y crea `.env.local` si todavia no existe. - -El repositorio ahora tambien incluye un `.env` base con placeholders seguros para que herramientas, tareas y nuevos entornos tengan variables explicitas desde el primer clone. Los valores específicos de tu máquina deben seguir yendo en `.env.local`. +1. La UI React/Vite recibe prompts, recetas e imágenes de referencia. +2. El backend local Bun/Hono crea y supervisa jobs persistentes. +3. `codex app-server` ejecuta turns reales para generar/editar imágenes. +4. La Studio Library guarda assets, SQLite, transcripts y logs. +5. La UI sincroniza por HTTP + SSE y mantiene compatibilidad visual para seguir operativa. -La UI abre automaticamente una guia de primer arranque para verificar backend local, Codex CLI, `codex app-server` y la ruta de biblioteca. Tambien puedes reabrirla desde el boton `Setup` del header. +## Requisitos -Si solo quieres levantar una parte del sistema: +- **Bun** en PATH — [bun.sh](https://bun.sh) +- **Codex CLI** instalado y autenticado con login de ChatGPT en la misma máquina. +- Soporte de `codex app-server` en esa instalación. +- Navegador moderno con IndexedDB. -- `bun run dev:ui` — UI Vite sin backend local. -- `bun run dev:server` — backend Hono + supervisor de `codex app-server`. -- `bun run dev:electron` — shell desktop Electron sobre el flujo local de desarrollo. +Si falta Codex o la sesión local, la UI puede abrir pero no completará generaciones reales. Ver [`docs/TROUBLESHOOTING.md`](./docs/TROUBLESHOOTING.md). -## Configuracion local +## Configuración local -El backend carga valores desde `.env.local`. Puedes dejar que `bun run studio:init` lo cree automaticamente o copiar la plantilla desde `.env.example`. +El backend toma variables desde `.env.local`. Puedes dejar que `bun run studio:init` lo cree o copiar `.env.example`. -Variables disponibles: +Variables principales: - `STUDIO_LIBRARY_DIR` - `STUDIO_SERVER_PORT` - `STUDIO_CODEX_WS_PORT` +- `VITE_STUDIO_API_BASE` -Variables opcionales para la shell Electron: +Variables opcionales para shell de Electron: -- `STUDIO_ELECTRON_API_BASE` — reutiliza un backend local ya corriendo si no quieres usar `http://localhost:4317`. -- `STUDIO_ELECTRON_RENDERER_URL` — apunta la shell desktop a un dev server Vite distinto del default `http://localhost:3000`. +- `STUDIO_ELECTRON_API_BASE` +- `STUDIO_ELECTRON_RENDERER_URL` -Ejemplos de ruta para la biblioteca: +Ejemplos de ruta de librería: - Windows: `%USERPROFILE%\AI-Studio-Library` - macOS: `/Users//AI-Studio-Library` - Linux: `/home//AI-Studio-Library` -## Scripts utiles +## Scripts útiles ```bash -bun run dev # backend local + UI integrada -bun run dev:server # Hono API + codex app-server supervisor -bun run dev:ui # UI Vite+ (vp dev) -bun run dev:electron # shell Electron para desarrollo local -bun run studio:init # crea biblioteca, SQLite y proyecto default -bun run styles:validate # valida manifests granulares de estilos -bun run fmt # formato con Oxfmt via Vite+ -bun run lint # lint con Oxlint via Vite+ -bun run check # formato + lint + type-check unificados via Vite+ -bun run test # suite de unit tests con Vitest via Vite+ -bun run test:unit # subset rapido para iteracion -bun run test:coverage # cobertura HTML + resumen en consola -bun run validate:fast # loop rapido: unit tests + verificacion server -bun run validate:full # gate completo: check + tests + build -bun run build # build UI (Vite+/Rolldown) + verificacion backend -bun run preview:electron # prueba la shell Electron cargando `dist/` -bun run tooling:logs # abre `logs/tooling` con los ultimos logs de comandos +bun run dev +bun run dev:server +bun run dev:ui +bun run dev:electron +bun run studio:init +bun run fmt +bun run lint +bun run check +bun run test +bun run build +bun run validate:fast +bun run validate:full ``` -Para iterar durante un refactor grande, usa `bun run validate:fast` y deja `bun run validate:full` para el cierre final. `bun run check` ahora corre el loop unificado de formato, lint y type-check sobre Vite+, asi que es el comando recomendado para validar cambios de forma local. - -Tambien hay tareas de VS Code en `.vscode/tasks.json` para inicializar, levantar, validar y abrir los logs de la biblioteca. +## Detalles clave -### Logs de tooling - -Los comandos de calidad y build (`fmt`, `lint`, `check`, `test`, `build`, `validate:*`) escriben logs persistentes en `logs/tooling/`. - -- cada ejecucion genera un archivo timestamped; -- ademas se actualiza un `*.latest.log` por tarea; -- esto facilita depurar fallos intermitentes sin tener que repetir una corrida solo para leer la consola. +| Tema | Decisión | +|------|----------| +| Fuente de verdad durable | `SQLite + Image Catalog` | +| Cache visual compatible | `GenerationBatch[]` en IndexedDB (solo compatibilidad) | +| Eventos en vivo | `GET /api/events` (SSE) | +| Sesión local canónica | `/api/codex/session` | +| Filosofía de producto | Codex-first, local-first, library-backed | ## Estructura del repositorio ```text . -├─ apps/local-server/ # backend local Bun/Hono y worker -├─ components/ # UI principal del studio -├─ contexts/ # estado global y de generacion -├─ docs/ # arquitectura, servicios, ADRs y guias -├─ hooks/ # sincronizacion, pipeline y persistencia -├─ packages/shared/ # tipos compartidos entre UI y backend -├─ scripts/ # scripts de inicializacion y utilidades internas -└─ services/ # adaptadores frontend hacia el backend local +├─ apps/local-server/ +├─ components/ +├─ contexts/ +├─ docs/ +├─ hooks/ +├─ packages/shared/ +├─ scripts/ +└─ services/ ``` -### Style preset manifests - -Los packs legacy monoliticos estan retirados. El loader principal consume la estructura granular: - -- `components/recipes/styles/manifests/packs/*.yaml` — Style Pack Manifests livianos, categorias y referencias. -- `components/recipes/styles/manifests/presets//.yaml` — Style Preset Manifests editables uno por archivo. - -Edita los manifests granulares en `components/recipes/styles/manifests/`. `bun run styles:split` se niega a sobrescribirlos desde YAML legacy. `styles:source:verify` falla si reaparece YAML legacy o si el runtime vuelve a usar exports de pack retirados. La UI consume `StyleRuntimePack` mediante loaders compactos generados desde los manifests granulares. - -## Documentacion recomendada +## Documentación principal -- [`CONTEXT.md`](./CONTEXT.md) — lenguaje y terminos canonicos del proyecto. -- [`AGENTS.md`](./AGENTS.md) — reglas para agentes y colaboradores automaticos. -- [`SKILLS.md`](./SKILLS.md) — workflows especializados para providers, recetas, presets, settings y outputs. -- [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) — vista general del sistema. -- [`docs/SERVICES.md`](./docs/SERVICES.md) — mapa de servicios y puntos de integracion. -- [`docs/DEV_GUIDE.md`](./docs/DEV_GUIDE.md) — convenciones para extender recetas y UI. -- [`docs/TOOLING.md`](./docs/TOOLING.md) — stack de tooling actual, comandos y logs. -- [`docs/ELECTRON.md`](./docs/ELECTRON.md) — estrategia y restricciones para una futura build desktop. -- [`docs/TROUBLESHOOTING.md`](./docs/TROUBLESHOOTING.md) — errores comunes de setup y ejecucion. -- [`docs/IMPLEMENTATION_LOG.md`](./docs/IMPLEMENTATION_LOG.md) — registro de tareas aplicadas en la puesta al dia. -- [`docs/TECHNICAL_DEBT.md`](./docs/TECHNICAL_DEBT.md) — deuda tecnica conocida y siguientes focos. -- [`docs/adr/0001-local-codex-studio.md`](./docs/adr/0001-local-codex-studio.md) — decision arquitectonica fundacional. -- [`ROADMAP.md`](./ROADMAP.md) — prioridades del producto para la etapa open-source. +- [`CONTEXT.md`](./CONTEXT.md) — vocabulario canónico del dominio. +- [`AGENTS.md`](./AGENTS.md) — reglas operativas para agentes. +- [`SKILLS.md`](./SKILLS.md) — flujos especializados. +- [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md) — arquitectura vigente. +- [`docs/SERVICES.md`](./docs/SERVICES.md) — mapa de servicios e integraciones. +- [`docs/DEV_GUIDE.md`](./docs/DEV_GUIDE.md) — convenciones de desarrollo. +- [`docs/TOOLING.md`](./docs/TOOLING.md) — comandos y calidad. +- [`docs/TROUBLESHOOTING.md`](./docs/TROUBLESHOOTING.md) — diagnóstico rápido. -## Contribuir +## Checklist de validación rápida -Si quieres ayudar a preparar el proyecto para un release open-source mas solido, revisa [`CONTRIBUTING.md`](./CONTRIBUTING.md). +- [ ] `bun run studio:init` completa sin errores. +- [ ] `bun run dev` levanta UI + backend. +- [ ] `GET /api/health` responde correctamente. +- [ ] Puedes abrir la UI y ver el estado de readiness. -## Licencia +## Próximo paso -Este repositorio se distribuye bajo la licencia [MIT](./LICENSE). +Si quieres contribuir, empieza por [`CONTRIBUTING.md`](./CONTRIBUTING.md). Si quieres entender prioridades de producto, sigue en [`ROADMAP.md`](./ROADMAP.md). diff --git a/ROADMAP.md b/ROADMAP.md index 67ca0e1f..c7d31a27 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,231 +1,55 @@ -# Roadmap +# Hoja de ruta -Este roadmap describe la direccion del producto mientras preparamos una version open-source mas madura de Codex Studio. No es un contrato cerrado; es una guia operativa para decidir que construir, que consolidar y que dejar explicitamente fuera de foco. +Esta hoja de ruta define la dirección del producto mientras Codex Studio se prepara para una preview open-source más sólida. -## Estado actual - -La direccion general sigue bien alineada con el producto real: - -- **Local-first**: el studio sigue priorizando la ejecucion local, la biblioteca local y la sesion autenticada del usuario. -- **Codex-native**: el camino principal gira alrededor de `codex app-server`, no de API keys obligatorias. -- **Trazable**: jobs, logs, estado y artefactos ya tienen una base consistente en backend local + SQLite + UI. -- **Portable**: la biblioteca externa, la inicializacion local y la separacion repo/runtime siguen siendo pilares correctos. - -Lo que cambio es el nivel de madurez: el producto ya no esta en una etapa donde alcanza con una lista corta de intenciones. Ahora necesitamos un plan mas concreto que refleje lo ya construido, cierre deuda estructural y ordene el camino hacia un release open-source mas robusto. - -## Lo que ya esta resuelto o encaminado - -Durante las ultimas tandas de trabajo, el studio avanzo en frentes que el roadmap anterior todavia no reflejaba: - -- mejora del shell visual de recetas, incluida la experiencia de `recipes/styles`; -- rebranding de la app a **Codex Studio**; -- mejor salud inicial del entorno y onboarding operativo; -- widget de usage disponible en la toolbar superior; -- indicadores de estado para backend local, Codex CLI y app-server; -- accion de reset completo de workspace + base de datos desde la UI; -- documentacion, tooling y validacion mas consistentes para iterar con menos friccion. - -En otras palabras: ya no estamos solo preparando el terreno. Ya tenemos una base usable y el foco ahora pasa a **consolidacion, claridad de producto y eliminacion de deuda de arquitectura**. - -## Donde aun no estamos alineados del todo - -Hay varias areas donde el producto real y el roadmap previo todavia no encajaban bien: - -- **Deuda del shell principal**: `components/AppContent.tsx` sigue concentrando demasiada orquestacion, lo que afecta mantenibilidad, testabilidad y velocidad para agregar features. -- **Transicion incompleta hacia el catalogo durable**: la UI todavia conserva compatibilidad fuerte con `GenerationBatch` e IndexedDB, aunque el backend/catalogo ya existe como base duradera. -- **Falta de una capa operativa de producto**: ya tenemos health, jobs persistentes, logs y reset, pero falta convertir eso en una experiencia mas clara de activity feed, recovery y diagnosticos accionables. -- **Criterios de salida poco concretos para open source**: no alcanza con “mejorar docs” o “hacer onboarding mas claro”; necesitamos hitos verificables para saber cuando una preview publica esta lista. - -## Principios rectores - -- **Local-first**: el studio debe seguir siendo util aunque no exista un backend cloud propio. -- **Codex-native**: la integracion principal gira alrededor de `codex app-server` y la sesion autenticada del usuario. -- **Trazable**: cada job debe dejar logs, estado, diagnosticos y artefactos inspeccionables. -- **Portable**: la biblioteca local debe poder moverse, resetearse, respaldarse y reconfigurarse sin reescribir el producto. -- **Operable**: un usuario nuevo debe poder diagnosticar su instalacion sin leer medio repositorio. -- **Evolutivo**: nuevas recetas, adapters y superficies desktop deben poder agregarse sin aumentar el acoplamiento del shell principal. - -## Plan maestro - -### Fase 0 — Consolidar el shell actual - -Objetivo: estabilizar la app tal como existe hoy antes de sumar demasiadas capas nuevas. - -#### Fase 0 — Alta prioridad - -- [ ] **Descomponer `AppContent.tsx`** — extraer modulos tipo `StudioShell`, `RecipeShell`, `DiagnosticsController` y `OverlayController`; reducir prop drilling; separar mejor routing, estado visual y side-effects. - -- [ ] **Unificar el estado de diagnosticos del studio** — consolidar health, backend connectivity, account usage y app-server status en una capa dedicada para evitar duplicacion entre onboarding, toolbar, panel system y sync hooks. - -- [ ] **Reemplazar affordances demasiado internos en la UI** — revisar copy, labels y mensajes con mirada de usuario nuevo, manteniendo personalidad pero bajando jerga demasiado interna o criptica. - -- [ ] **Mejorar la UX de acciones destructivas** — reemplazar confirmaciones primitivas por modales coherentes con la UI y explicar con precision que se resetea, que se conserva y como recuperar datos antes de borrar. - -#### Fase 0 — Prioridad media - -- [ ] **Logging frontend uniforme** — reducir usos directos de `console.*` y usar un adapter comun para UI con niveles, contexto y mensajes consistentes. - -- [ ] **Tests de integracion del shell** — agregar cobertura para `HeaderToolbar`, `RightSystemPanel`, `StudioPage`, `useLocalStudioSync` y flujo de reset. - -#### Fase 0 — Criterio de salida - -- el shell principal queda modularizado sin romper el flujo de generacion; -- status, usage y onboarding comparten una misma fuente de verdad; -- las acciones destructivas se entienden mejor y son testeables. - -### Fase 1 — Cerrar la brecha entre UI legacy y catalogo durable - -Objetivo: hacer que el modelo de datos visible en la UI represente mejor la fuente durable real. - -#### Fase 1 — Alta prioridad - -- [ ] **Migrar la experiencia visual hacia el catalogo como fuente principal** — reducir la dependencia de `GenerationBatch` como modelo dominante y usar SQLite/catalogo como fuente durable, dejando IndexedDB como cache de UX donde tenga sentido. - -- [ ] **Revisar import/export de workspaces y biblioteca** — definir con precision que exporta el vault y que pertenece a la biblioteca local, mejorando la consistencia entre importacion, trash, favoritos, metadata y catalogo. - -- [ ] **Mejor recovery de assets y jobs huerfanos** — fortalecer la deteccion de estados parciales y exponer recovery de forma clara desde la UI, no solo como comportamiento interno. - -#### Fase 1 — Prioridad media - -- [ ] **Backups mas claros de biblioteca local** — documentar y eventualmente exponer flujos de respaldo/restauracion, distinguiendo entre reset visual, reset de DB y resguardo de assets. - -- [ ] **Roadmap de migracion de datos documentado** — dejar explicito como transiciona la app desde cache visual legacy a indexacion durable. - -#### Fase 1 — Criterio de salida - -- la UI deja de depender fuertemente de un estado duplicado o ambiguo; -- importar, exportar, resetear y recuperar tiene reglas mas previsibles; -- el catalogo pasa a ser la referencia conceptual principal del studio. - -### Fase 2 — Operabilidad real del studio - -Objetivo: que el studio no solo funcione, sino que tambien sea facil de operar y diagnosticar. +## Ruta rápida -#### Fase 2 — Alta prioridad +1. Consolidar shell y experiencia de primer uso. +2. Completar transición catalog-first. +3. Fortalecer operabilidad (diagnóstico + jobs + recuperación). +4. Cerrar brecha de portabilidad y estrategia desktop. +5. Publicar un release candidate presentable. -- [ ] **Centro de actividad mas claro** — mejorar la visualizacion de jobs persistentes, distinguir mejor cola efimera vs backend y hacer mas evidente que esta esperando, corriendo, fallando o listo para recovery. - -- [ ] **Detalle de ejecucion mas util** — enriquecer job detail con eventos, transcript entries, errores accionables y artefactos relacionados para facilitar inspeccion sin obligar al usuario a abrir logs crudos. - -- [ ] **Mensajes de error y recovery accionables** — cuando Codex CLI, app-server, puertos o biblioteca fallen, mostrar pasos concretos de recuperacion y evitar estados silenciosos o ambiguos. - -#### Fase 2 — Prioridad media - -- [ ] **Dry-run y smoke-check visibles desde la UI** — facilitar un check rapido de instalacion sin consumo real de generacion y usarlo como parte del onboarding y soporte. - -- [ ] **Export de diagnosticos** — permitir descargar un bundle minimo con health, logs recientes y contexto tecnico para soporte o debugging. - -#### Fase 2 — Criterio de salida - -- un usuario puede entender el estado del sistema sin leer la consola; -- los fallos mas comunes tienen recovery visible; -- inspeccionar una ejecucion deja de sentirse como trabajo de arqueologia. - -### Fase 3 — Setup, portabilidad y camino desktop - -Objetivo: bajar friccion de instalacion y preparar una experiencia mas pulida fuera del entorno del repo. - -#### Fase 3 — Alta prioridad - -- [ ] **Mejor setup cross-platform** — pulir rutas, defaults y mensajes para Windows, macOS y Linux; mejorar deteccion y validacion de biblioteca local; reducir supuestos ocultos sobre puertos, PATH y estructura de entorno. - -- [ ] **Onboarding verdaderamente autosuficiente** — hacer que el primer arranque detecte problemas, sugiera correcciones y revalide sin obligar a salir a docs demasiado pronto. - -- [ ] **Definir mejor el camino desktop** — mantener el renderer desacoplado de Electron, limitar la superficie del `preload` y sostener una arquitectura segura y reversible. - -#### Fase 3 — Prioridad media - -- [ ] **Configuracion visual de runtime** — exponer mejor configuracion de biblioteca, puertos y endpoints locales sin convertir la UI en un panel de infra hostil. - -- [ ] **Empaquetado para usuarios no tecnicos** — explorar pasos concretos para una distribucion mas simple cuando la base ya este mas estable. - -#### Fase 3 — Criterio de salida - -- una persona nueva puede instalar y verificar el studio con mucha menos ayuda externa; -- la build desktop deja de ser una exploracion adyacente y pasa a tener una estrategia clara. - -### Fase 4 — Release candidate open-source - -Objetivo: cerrar brechas para una preview publica mas seria y mantenible. - -#### Fase 4 — Alta prioridad - -- [ ] **Auditoria final de artefactos y repo hygiene** — validar que no reaparezcan muestras locales, prompts de trabajo o archivos ajenos al release y revisar arbol trackeado e historial relevante antes del corte. - -- [ ] **Cobertura minima de calidad para release** — usar `validate:fast` en iteracion diaria y `validate:full` como gate de cierre, reforzando pruebas en shell, sincronizacion local y flujos criticos de usuario. - -- [ ] **Documentacion de operador y contribucion final** — README, troubleshooting, arquitectura y contribucion deben reflejar el producto real, no un estado historico. - -#### Fase 4 — Prioridad media - -- [ ] **Checklist de smoke test manual** — cubrir primer arranque, health, generacion o dry-run, import/export, reset y recuperacion de fallos basicos. - -#### Fase 4 — Criterio de salida - -- el repositorio puede presentarse como preview open-source util sin esconder demasiadas advertencias operativas; -- el camino de instalacion, validacion y soporte queda razonablemente defendido. - -## Mejoras concretas adicionales que vale la pena incorporar - -Ademas del plan por fases, estas son mejoras puntuales que hoy tendrian muy buena relacion impacto/esfuerzo: - -### UX del producto - -- [ ] convertir el reset completo en un flujo de confirmacion mas rico y menos abrupto; -- [ ] agregar estados vacios, loading y error mas expresivos en paneles de jobs, logs y diagnosticos; -- [ ] revisar responsive del header superior para que usage, workspaces y navegacion convivan mejor en pantallas medianas; -- [ ] hacer mas clara la diferencia entre “workspace visual”, “vault exportado” y “biblioteca local”. - -### Confiabilidad - -- [ ] consolidar revalidacion de health, Local Codex Session y reconexion SSE; -- [ ] hacer mas visible cuando el backend se desconecta y cuando vuelve; -- [ ] dejar mejor definido que significa un reset exitoso del lado frontend y backend; -- [ ] agregar mas pruebas sobre reconexion, cancelacion y jobs persistentes. - -### Datos y trazabilidad - -- [ ] mejorar descubrimiento de transcripts, logs y artefactos por job; -- [ ] revisar politicas de retencion local para no crecer sin control; -- [ ] ofrecer caminos mas claros para backup/restore antes de operaciones destructivas. - -### Performance +## Estado actual -- [ ] hacer code-splitting de recetas pesadas con `React.lazy()` donde tenga sentido; -- [ ] medir si el grid y el shell necesitan mas memoizacion o virtualizacion cuando el catalogo crece; -- [ ] reducir renders derivados de un shell demasiado centralizado. +Codex Studio ya está alineado en pilares clave: -### Calidad y mantenimiento +- **Local-first**: estado, assets y logs viven en una Studio Library local. +- **Codex-first**: flujo principal vía `codex app-server`. +- **Trazable**: jobs, eventos, transcripts y catálogo permiten inspección real. +- **Portable**: la librería está separada del repo y es configurable. +- **Extensible**: `Generation Task` y `Generation Provider` son conceptos separados. -- [ ] ampliar cobertura de tests de UI; -- [ ] revisar dependencias del stack periodicamente y validar compatibilidad real del ecosistema; -- [ ] seguir agregando JSDoc y contratos explicitos en servicios/hook criticos. +## Fases -## Orden recomendado de ejecucion +| Fase | Objetivo | Resultado esperado | +|------|----------|--------------------| +| 0 | Estabilizar shell actual | Navegación y estados globales más claros | +| 1 | Cerrar transición catalog-first | UI alineada con SQLite/Image Catalog | +| 2 | Mejorar operabilidad | Fallos comunes con diagnóstico accionable | +| 3 | Setup y portabilidad | Instalación más simple en Win/macOS/Linux | +| 4 | Release candidate OSS | Repo listo para exposición pública | -Si hoy hubiera que priorizar en serio, el orden recomendado seria este: +## Prioridades cercanas -1. **Descomponer `AppContent.tsx` y unificar diagnosticos**. -2. **Cerrar la brecha entre batches legacy e indexacion durable en catalogo**. -3. **Mejorar activity feed, job detail y recovery**. -4. **Pulir onboarding cross-platform y configuracion local**. -5. **Reforzar testing y criterios de release**. -6. **Recien despues** empujar fuerte desktop packaging o extensibilidad avanzada. +- Mejorar onboarding y mensajes de error. +- Reforzar recuperación y detalle de jobs. +- Reducir deuda de orquestación en shell. +- Mantener calidad con `validate:fast` y `validate:full`. -## No objetivos inmediatos +## Checklist de salida para release candidate -- convertir el producto en un servicio cloud administrado; -- reintroducir dependencias obligatorias de API keys para el flujo principal; -- publicarlo como paquete npm reutilizable; el foco sigue siendo el studio local, no una libreria; -- perseguir demasiadas features nuevas de superficie sin antes consolidar shell, datos y operabilidad. +- [ ] Instalación reproducible en máquinas nuevas. +- [ ] Diagnóstico básico disponible desde UI. +- [ ] Documentación de setup, troubleshooting y contribución alineada. +- [ ] Sin artefactos sensibles/versionados por error (DBs, logs, assets locales, secretos). -## Regla de oro para las proximas iteraciones +## No-objetivos (por ahora) -Cada mejora nueva deberia responder al menos a una de estas preguntas: +- Convertirlo en SaaS multiusuario. +- Hacer API keys obligatorias para el flujo principal. +- Empaquetar como librería npm reutilizable. -- ¿reduce friccion real para instalar o usar el studio? -- ¿aclara mejor el estado del sistema y los jobs? -- ¿reduce deuda estructural del shell o del modelo de datos? -- ¿prepara mejor el release open-source sin meter mas complejidad de la que saca? +## Próximo paso -Si la respuesta es “no”, probablemente esa mejora puede esperar. +Usar este documento junto con `docs/active/professionalization-roadmap.md` para priorizar slices semanales. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..59d005d5 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,24 @@ +# Política de seguridad + +## Versiones soportadas + +Codex Studio está en etapa de preview open-source. Los fixes de seguridad se aplican sobre la rama principal mientras no existan releases estables. + +## Reporte de vulnerabilidades + +No abras issues públicos para vulnerabilidades que involucren archivos locales, credenciales, Provider Secrets o exposición de assets. + +Reporta por canal privado del mantenedor e incluye: + +- commit o versión afectada +- sistema operativo +- pasos para reproducir +- impacto esperado/observado +- logs saneados (sin secretos) + +## Notas de seguridad local-first + +- Los secretos de proveedor deben permanecer fuera de Studio Settings persistidos en SQLite. +- Nunca commitees `.env.local`, bases SQLite, logs, transcripts ni carpetas de librería local. +- Trata las rutas de Studio Library como datos controlados por usuario. +- Evita operaciones destructivas sobre rutas arbitrarias: registra/importa External Output Sources primero. diff --git a/SKILLS.md b/SKILLS.md index b7b7ad2b..ad52cb11 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -1,8 +1,8 @@ -# Codex Studio Workflow Skills +# Skills de flujo de trabajo de Codex Studio -This file describes repo-local workflows for humans and agents. It is not the glossary; use `CONTEXT.md` for terms. +Este archivo describe flujos de trabajo locales del repo para personas y agentes. No es un glosario; para términos canónicos usa `CONTEXT.md`. -## Add Or Change A Generation Provider +## Agregar o cambiar un Generation Provider 1. Keep Codex-first product semantics. 2. Add provider capability/config behind Provider Boundary. @@ -20,9 +20,9 @@ This file describes repo-local workflows for humans and agents. It is not the gl 14. For fal.ai task assets, use `apps/local-server/src/providers/falAssetInputs.ts`. Hosted `sourceUrl` refs can map to `image_url`, `mask_url`, `control_image_url`, and `reference_image_urls`; local `localPath` refs upload through `apps/local-server/src/providers/falStorageUpload.ts`; inline asset bytes are intentionally omitted from compact Provider Inputs and must be imported as `localPath` or `sourceUrl` before execution. 15. For fal.ai `image_edit`, require an `input` or `external_output` asset before network execution. Do not let edit jobs run as text-only generation by accident. -Do not add provider-specific task names such as `fal_spritesheet` or `comfy_texture`. Use provider-independent Generation Tasks plus provider config. +No agregues nombres de task específicos de proveedor como `fal_spritesheet` o `comfy_texture`. Usa Generation Tasks agnósticos de proveedor + configuración del provider. -## Add Or Change A Recipe Module +## Agregar o cambiar un Recipe Module 1. Update `lib/recipeModules.ts` metadata: title, description, parameter descriptors, default task, supported tasks, supported providers. 2. Define parameter schema details there too: group, control, default, options, min/max/step, and required flags. @@ -36,7 +36,7 @@ Do not add provider-specific task names such as `fal_spritesheet` or `comfy_text 10. Let providers compile task specs into their own payloads. 11. Store rich task spec for traceability; send compact provider input for execution. -Recipe Module is not a React page. React page can host recipe UI, but the workflow contract must be pure and testable. +Un Recipe Module no es una página React. La página puede alojar la UI, pero el contrato de workflow debe ser puro y testeable. Run `bun run recipes:catalog -- --query= --limit=20` to inspect Recipe Modules by text. Add `--task=`, `--provider=`, `--parameter=`, `--examples`, or `--json` for agent-ready output. Run `bun run recipes:verify` before closing broad recipe work. It checks Recipe Module catalog coverage, default task support, provider coverage, duplicate parameter ids, enum options, slider types, required/default conflicts, provider-independent examples, React surface boundaries, and prompt evaluation. @@ -44,19 +44,20 @@ Run `bun run recipes:examples:verify` when changing future asset-task blueprints Run `bun run recipes:source:verify` when changing recipe UI or module plumbing. It blocks React recipe surfaces from importing task-spec builders, Recipe Context builders, Recipe Provider Directives, or provider compilers. Run `bun run recipes:evaluate:live -- --recipe= --out=logs/recipe-prompt-quality` to plan a live Codex quality comparison without creating jobs. Add `--execute` to queue real legacy-vs-directives jobs through the local backend, record JSON + Markdown review templates, and keep only job/catalog refs plus transcript paths in the repo-local report. -## Add Or Change A Style Preset +## Agregar o cambiar un Style Preset 1. Prefer `bun run styles:scaffold -- --preset= --pack= --category= --name= --template=style|sprite|texture` when creating a new preset. It is dry-run by default; pass `--write` to mutate files and optionally `--default-image=/assets/...` to prefill a real default image path. 2. Edit `components/recipes/styles/manifests/presets//.yaml` first. `styles:scaffold` uses `components/recipes/styles/manifests/templates/style-preset.template.yaml`, `sprite-sheet-preset.template.yaml`, or `texture-preset.template.yaml` under the hood for new presets. 3. Preserve stable `id`, `packId`, `name`, `category`, visual DNA, avoid rules, asset refs, supported tasks, tags, version. -4. Maintain editorial taxonomy (`packId`, `packName`, `categoryId`, `categoryName`, domain, tags, supported tasks, default image state) so agents can query presets without scanning compatibility packs. -5. Register the preset in both the matching category `presetRefs` and the pack-level `presetRefs`; refs must stay inside the same pack namespace. -6. Keep each preset visually distinct from neighboring presets. -7. Do not collapse motif/avoid constraints into generic prompt text. -8. Validate the edited preset file and any catalog index generated from it. -9. Import manifest-authoring contracts from `components/recipes/styles/manifestTypes.ts` and runtime/UI contracts from `components/recipes/styles/runtimeTypes.ts`. Do not use the retired `components/recipes/styles/types` path. +4. Keep taxonomy source language in English for durable ids/tags/labels, and keep `category.id` in `kebab-case` (no `snake_case`, no `videojuegos` legacy slugs). +5. Maintain editorial taxonomy (`packId`, `packName`, `categoryId`, `categoryName`, domain, tags, supported tasks, default image state) so agents can query presets without scanning compatibility packs. +6. Register the preset in both the matching category `presetRefs` and the pack-level `presetRefs`; refs must stay inside the same pack namespace. +7. Keep each preset visually distinct from neighboring presets. +8. Do not collapse motif/avoid constraints into generic prompt text. +9. Validate the edited preset file and any catalog index generated from it. +10. Import manifest-authoring contracts from `components/recipes/styles/manifestTypes.ts` and runtime/UI contracts from `components/recipes/styles/runtimeTypes.ts`. Do not use the retired `components/recipes/styles/types` path. Run `bun run styles:validate -- --preset=` after editing one granular preset, or `bun run styles:validate -- --pack=` after editing a pack. Use `--coverage` to see taxonomy and default-image coverage by pack, and `--strict-taxonomy` when intentionally requiring persisted taxonomy in the edited scope. Run `bun run styles:scaffold -- --preset= --pack= --category= --name= --template=style|sprite|texture` to preview the new preset file plus pack/category ref changes before writing them. Add `--write` to apply the scaffold. If `--default-image` is omitted, the scaffold uses `/assets/recipes/styles/defaults/.webp` and leaves `taxonomy.hasDefaultImage: false` until the asset exists. @@ -67,11 +68,11 @@ Run `bun run styles:runtime:check` to verify the generated runtime file is curre Run `bun run styles:templates:verify` after changing files in `components/recipes/styles/manifests/templates/`. It checks required image, sprite sheet, and texture templates plus their task coverage. Run `bun run styles:source:verify` when changing Styles runtime/scripts. It blocks accidental runtime imports from legacy pack YAML, generated runtime check temp files, presets that exist only in legacy pack YAML, YAML files recreated in the retired `components/recipes/styles/packs` directory, retired runtime pack aliases/exports, and imports from the retired `styles/types` path; only source-audit and compatibility-test seams should mention old legacy pack internals. Run `bun run styles:render:verify` after changing Styles UI grouping, pack runtime data, or virtualized rendering. It reports mounted/eager/placeholder sections plus eager/planned card budgets per pack from `styleBrowserRenderPlan`, keeping large packs from mounting every preset at once. For major Styles UI changes, verify `pack_05` in browser and compare collapsed/expanded DOM counts against this report. -Run `bun run styles:browser:verify -- --url=http://localhost:3000/#recipe-styles` (or the active dev URL) after major Styles UI changes when you want the reusable browser gate instead of a manual pass. If the gate should validate a clean dev console on Windows, start the UI with `VITE_ENABLE_REACT_SCAN=false bun run dev:ui` first. +Run `bun run styles:browser:verify -- --url=http://localhost:17222/#recipe-styles` (or the active dev URL) after major Styles UI changes when you want the reusable browser gate instead of a manual pass. If the gate should validate a clean dev console on Windows, start the UI with `VITE_ENABLE_REACT_SCAN=false bun run dev:ui` first. -Legacy pack YAML is retired. `bun run styles:split`, `scripts/expand-pack-02-pack-05.ts`, and `scripts/reorder-style-packs.ts` intentionally refuse to mutate granular manifests from old pack YAML flows. Use `StyleRuntimePack`, `StyleRuntimePreset`, `composeStyleRuntimePacksFromManifests()`, `STYLE_RUNTIME_PACK_SUMMARIES`, `loadStyleRuntimePack()`, `loadStyleRuntimePacks()`, and manifest/catalog types for new code. `styles:source:verify` blocks retired runtime aliases/exports outside the source-audit guard. +El pack YAML legacy está retirado. `bun run styles:split`, `scripts/expand-pack-02-pack-05.ts` y `scripts/reorder-style-packs.ts` rechazan mutar manifiestos granulares desde flujos antiguos. Usa `StyleRuntimePack`, `StyleRuntimePreset`, `composeStyleRuntimePacksFromManifests()`, `STYLE_RUNTIME_PACK_SUMMARIES`, `loadStyleRuntimePack()`, `loadStyleRuntimePacks()` y tipos de manifest/catalog para código nuevo. `styles:source:verify` bloquea aliases/export legacy fuera del guard de source-audit. -## Audit Token Usage +## Auditar uso de tokens 1. Compare Generation Task Spec size vs Compiled Provider Input size. 2. Move stable boilerplate into Provider Session Contract. @@ -81,8 +82,10 @@ Legacy pack YAML is retired. `bun run styles:split`, `scripts/expand-pack-02-pac 6. Keep Codex imagegen stable instructions in `apps/local-server/src/codex/imagegenContract.ts`; provider compilers and Codex threads should reuse that contract instead of copying boilerplate. 7. Prefer Recipe Provider Directives over legacy Recipe Context only after a focused test proves the compact payload still carries required task detail. 8. Prefer filtered tooling commands (`bun run test -- `, `bun run check -- `) while iterating so broad gates run only at closeout. +9. Use `createProviderInputMetrics()` when adding token/size diagnostics so source spec size, compiled input size, compiled payload size, asset count, inline-asset state, and Provider Session Contract id stay consistent. +10. Treat inline asset bytes as a preflight signal, not debug data; never serialize inline image bytes into Compiled Provider Input logs. -Token savings should come from better compilation, not weaker recipes. +El ahorro de tokens debe venir de mejor compilación, no de debilitar recetas. Run `bun run providers:audit` to inspect Recipe Module and provider conformance rows, including source spec size, compiled payload size, prompt estimates, Recipe Provider Directives coverage, and inline-data/secret leak checks. Run `bun run providers:verify` before changing provider compilers or removing legacy Recipe Context metadata. Run `bun run providers:source:verify` after backend route/worker/provider-boundary changes. It blocks route handlers and non-provider backend modules from importing provider compilers, shared hosted result internals, or concrete hosted/local executors. @@ -91,7 +94,22 @@ Use `apps/local-server/src/providers/externalProvider.ts` as the adapter shell f Use `apps/local-server/src/providers/externalProviderResults.ts` for hosted image result handling before adding provider-specific download/transcript code. Keep retry policy, image URL extraction, mime/ext inference, asset writes, transcript writes, and secret redaction shared unless a provider truly needs a different contract. For ComfyUI, use `apps/local-server/src/providers/comfyExecutor.ts`. It requires `COMFY_API_URL` or `COMFYUI_API_URL` plus `COMFY_WORKFLOW_TEMPLATE_PATH`. The template must be a Comfy workflow JSON with `{{prompt}}` and optional `{{negativePrompt}}` placeholders. The executor submits `/prompt`, polls `/history/{prompt_id}`, imports the first `/view` image into the Studio Library, and records only compact no-secret diagnostics. -## Add Settings UI Or Config +## Endurecer cola de generación + +1. Validate `Generation Task Spec` before enqueue when a job carries `sourceSpec`. +2. Local queued jobs with `metadata.workspaceId` or `metadata.batchId` must use `batch-*` batch ids and `spec-batch-*` spec ids. +3. After backend reference persistence, provider execution must not receive inline `data:image/*;base64` task assets; assets should be hydrated to `localPath` or accepted `sourceUrl`. +4. Return normalized validation fields (`code`, `field`, `reason`, `issues`) so UI and agents can diagnose without backend stack traces. +5. Keep reference persistence in `apps/local-server/src/referenceManager.ts`; do not let route handlers or providers write ad-hoc reference files. + +Run focused coverage first: + +```bash +bun run test -- packages/shared/src/generationContracts.test.ts apps/local-server/src/jobRoutes.test.ts +bun run check -- packages/shared/src/generationContracts.ts packages/shared/src/generationContracts.test.ts apps/local-server/src/jobRoutes.ts apps/local-server/src/jobRoutes.test.ts +``` + +## Agregar UI o configuración de Settings 1. Ask: is this Bootstrap Configuration, Studio Settings, or Provider Secret? 2. Bootstrap Configuration: `.env.local`, ports, initial library path, dev flags. @@ -103,7 +121,7 @@ For ComfyUI, use `apps/local-server/src/providers/comfyExecutor.ts`. It requires 8. Keep Studio Library roots clean: internal state belongs in `.studio/`, generated images and exports belong in `outputs/`. 9. Output organization preferences may control subfolders by date/provider/model/recipe and file-name templates; keep defaults readable and avoid exposing secret/provider endpoint values. -## Work With Output Folders +## Trabajar con carpetas de salida 1. Detect likely External Output Sources. 2. Let user register source. @@ -112,7 +130,7 @@ For ComfyUI, use `apps/local-server/src/providers/comfyExecutor.ts`. It requires 5. Run catalog operations only on managed Local Assets. 6. Preserve original external folder unless user explicitly asks for cleanup. -## Move Toward Catalog-First UI +## Avanzar hacia una UI catalog-first 1. Treat Image Catalog / Catalog Entries as durable read model. 2. Keep Visual Batch as a compatibility adapter only while legacy grid surfaces need it. @@ -121,7 +139,7 @@ For ComfyUI, use `apps/local-server/src/providers/comfyExecutor.ts`. It requires 5. Do not read `catalog-cache`, `GlobalContext`, or `useIndexedDBStorage` from `useCatalog`. 6. Run `bun run catalog:source:verify` after changing catalog/grid read paths. -## UI Cleanup +## Limpieza de UI 1. Global status and entry points go to Command Center. 2. Heavy detail panels become Demand-Mounted Surfaces. diff --git a/apps/local-server/src/appFactory.ts b/apps/local-server/src/appFactory.ts index d2db9853..adaf7a55 100644 --- a/apps/local-server/src/appFactory.ts +++ b/apps/local-server/src/appFactory.ts @@ -1,10 +1,6 @@ -import { existsSync } from 'node:fs'; -import { spawnSync } from 'node:child_process'; import { randomUUID } from 'node:crypto'; -import path from 'node:path'; import { Hono } from 'hono'; import { cors } from 'hono/cors'; -import { streamSSE } from 'hono/streaming'; import { getCodexWsUrl, getEnvLocalPath, getSettings, hasEnvLocalFile } from './config'; import { resolveCodexInvocation } from './codexExecutable'; import { createCatalogCommands } from './catalogCommands'; @@ -23,19 +19,22 @@ import { type StudioSettingsStorage, } from './studioSettingsStore'; import { createWorkerController, type WorkerController, type WorkerStatus } from './worker'; +import { resolveJobCatalogContext } from './workerCatalogContext'; +import { resolveWorkerRuntimeTarget } from './workerRouting'; +import { getCodexAccountStatus } from './codex/accountStatus'; import { - getCodexAccountStatus, ensureAppServer, getAppServerDiagnostics, - getCodexModelCatalog, - getLocalCodexSession, isAppServerRunning, -} from './codex'; +} from './codex/processSupervisor'; +import { getCodexModelCatalog } from './codex/modelCatalog'; +import { getLocalCodexSession } from './codex/localCodexSession'; import { embedMetadata } from './metadataEmbedder'; import { getJobDetail } from './jobDetails'; import { hydrateSourceSpecAssetPaths, processReferences, + type ProcessedReference, ReferenceProcessingError, } from './referenceManager'; import { createWorkspaceRoutes } from './workspaceRoutes'; @@ -46,20 +45,22 @@ import { resolveAssetCacheSeconds, resolveThumbnailMaxEdge, } from './libraryAssetVariants'; -import { - detectExternalOutputSourceCandidates, - importExternalOutputSourceFiles, - listExternalOutputSourceFiles, - readExternalOutputSourceRegistry, - registerExternalOutputSource, -} from './outputSources'; import { getProviderExecutionBlocker, readProviderCapabilities } from './providerCapabilities'; -import { readExternalProviderRuntimePreflights } from './providers/runtimeConfig'; +import { createOutputSourceRoutes } from './outputSourceRoutes'; +import { createProviderRoutes } from './providerRoutes'; +import { createSettingsRoutes } from './settingsRoutes'; +import { createCodexRoutes } from './codexRoutes'; +import { createLibrariesRoutes } from './librariesRoutes'; +import { createProjectRoutes } from './projectRoutes'; +import { createJobRoutes } from './jobRoutes'; +import { createAssetLogRoutes } from './assetLogRoutes'; +import { createRuntimeRoutes } from './runtimeRoutes'; +import { createStudioControlRoutes } from './studioControlRoutes'; +import { createEventStreamRoutes } from './eventStreamRoutes'; +import { createLibraryRoutes } from './libraryRoutes'; import type { AppServerEnsureReason, CodexModelCatalogResponse, - CreateJobRequest, - GenerationTaskSpec, LocalCodexSessionResponse, } from '../../../packages/shared/src'; @@ -110,7 +111,13 @@ export async function createStudioApp( setSetting: setSettingValue, }; const workerController = - options.dependencies?.worker ?? createWorkerController({ logger: appLogger }); + options.dependencies?.worker ?? + createWorkerController({ + logger: appLogger, + readEditableStudioSettings, + resolveJobCatalogContext, + resolveWorkerRuntimeTarget, + }); const catalogCommands = createCatalogCommands({ updateCatalogImage: (...args) => catalogStore.updateCatalogImage(...args), softDeleteCatalogImage: (...args) => catalogStore.softDeleteCatalogImage(...args), @@ -121,294 +128,138 @@ export async function createStudioApp( app.use('*', cors()); - app.get('/api/health', (c) => { - const settings = getSettings(); - const library = inspectLibrary(); - const [command, ...args] = resolveCodexInvocation(['--version']); - const codex = spawnSync(command, args, { encoding: 'utf8' }); - const codexAvailable = codex.status === 0; - const appServerDiagnostics = readAppServerDiagnostics(); - const libraryReady = library.exists && library.writable && library.missingFolders.length === 0; - const appServerRunning = isLocalAppServerRunning(); - - return c.json({ - ok: true, - checkedAt: new Date().toISOString(), - libraryDir: settings.libraryDir, - runtime: { - platform: process.platform, - arch: process.arch, - bunVersion: Bun.version, - nodeVersion: process.versions.node, - cwd: process.cwd(), - envLocalPath: getEnvLocalPath(), - envLocalPresent: hasEnvLocalFile(), - }, - config: { - serverPort: settings.serverPort, - codexWsPort: settings.codexWsPort, - }, - library: { - exists: library.exists, - writable: library.writable, - readmePresent: library.readmePresent, - missingFolders: library.missingFolders, - }, - codexCli: { - available: codexAvailable, - version: codexAvailable ? codex.stdout.trim() : null, - command: [command, ...args].join(' '), - }, - appServer: { - running: appServerRunning, - wsUrl: getCodexWsUrl(), - pid: appServerDiagnostics.pid, - lastExitCode: appServerDiagnostics.lastExitCode, - lastExitAt: appServerDiagnostics.lastExitAt, - lastInvocation: appServerDiagnostics.lastInvocation?.join(' ') ?? null, - lastStartAt: appServerDiagnostics.lastStartAt, - lastStartError: appServerDiagnostics.lastStartError, - lastEnsureAt: appServerDiagnostics.lastEnsureAt, - lastEnsureReason: appServerDiagnostics.lastEnsureReason, - }, - checks: { - libraryReady, - codexReady: codexAvailable, - onboardingReady: libraryReady && codexAvailable && appServerRunning, - }, - worker: workerController.getWorkerStatus(), - }); - }); - - app.post('/api/app-server/start', (c) => { - ensureLocalAppServer('user'); - const diagnostics = readAppServerDiagnostics(); - return c.json({ - running: isLocalAppServerRunning(), - wsUrl: getCodexWsUrl(), - pid: diagnostics.pid, - lastStartError: diagnostics.lastStartError, - }); - }); - - app.get('/api/bootstrap-config', (c) => c.json(getSettings())); - - app.get('/api/settings', (c) => c.json(readEditableStudioSettings(settingsStorage))); - - app.patch('/api/settings', async (c) => { - const body = await c.req.json().catch(() => ({})); - return c.json(updateEditableStudioSettings(settingsStorage, body)); - }); - - app.get('/api/providers', (c) => - c.json(readProviderCapabilities(readEditableStudioSettings(settingsStorage))), + app.route( + '/api', + createRuntimeRoutes({ + readSettings: getSettings, + inspectLibrary, + resolveCodexInvocation, + getCodexWsUrl, + getEnvLocalPath, + hasEnvLocalFile, + ensureAppServer: ensureLocalAppServer, + readAppServerDiagnostics, + isAppServerRunning: isLocalAppServerRunning, + readWorkerStatus: () => workerController.getWorkerStatus(), + }), ); - app.get('/api/providers/preflight', (c) => - c.json({ providers: readExternalProviderRuntimePreflights() }), + app.route( + '/api/settings', + createSettingsRoutes({ + readSettings: () => readEditableStudioSettings(settingsStorage), + updateSettings: (patch) => updateEditableStudioSettings(settingsStorage, patch), + }), ); - app.get('/api/output-sources', (c) => { - const settings = readEditableStudioSettings(settingsStorage); - return c.json({ - registry: readExternalOutputSourceRegistry(settingsStorage), - candidates: detectExternalOutputSourceCandidates({ - libraryDir: getSettings().libraryDir, - settings, - }), - }); - }); - - app.post('/api/output-sources', async (c) => { - const body = await c.req.json().catch(() => ({})); - const result = registerExternalOutputSource({ - storage: settingsStorage, - libraryDir: getSettings().libraryDir, - input: body, - }); - - if (!result.ok) { - return c.json({ error: result.reason }, 400); - } - - publishEvent('output-source.registered', result.source); - return c.json(result.source, 201); - }); - - app.get('/api/output-sources/:id/files', (c) => { - const url = new URL(c.req.url); - const result = listExternalOutputSourceFiles({ - storage: settingsStorage, - sourceId: c.req.param('id'), - limit: Number(url.searchParams.get('limit') || 100), - }); - if (!result.ok) return c.json({ error: result.reason }, 404); - return c.json({ source: result.source, files: result.files }); - }); + app.route( + '/api/providers', + createProviderRoutes({ + readSettings: readEditableStudioSettings(settingsStorage), + }), + ); - app.post('/api/output-sources/:id/import', async (c) => { - const body = await c.req.json().catch(() => ({})); - const result = importExternalOutputSourceFiles({ - storage: settingsStorage, - sourceId: c.req.param('id'), - libraryDir: getSettings().libraryDir, - input: body, + app.route( + '/api/output-sources', + createOutputSourceRoutes({ + settingsStorage, + readSettings: () => readEditableStudioSettings(settingsStorage), + readConfig: getSettings, registerCatalogImage: (...args) => catalogStore.registerCatalogImage(...args), - }); - if (!result.ok) return c.json({ error: result.reason }, 400); - publishEvent('output-source.imported', result.result); - return c.json(result.result, 201); - }); - - app.get('/api/codex/models', async (c) => { - return c.json(await readCodexModelCatalog()); - }); - - app.get('/api/codex/session', async (c) => { - return c.json(await readLocalCodexSession()); - }); - - app.get('/api/codex/account', async (c) => { - return c.json(await getCodexAccountStatus()); - }); - - app.post('/api/studio/reset', async (c) => { - return c.json(await resetStudioData(workerController)); - }); - - app.get('/api/projects', (c) => c.json(dbStore.listProjects())); - - app.post('/api/projects', async (c) => { - const body = await c.req.json().catch(() => ({})); - const project = dbStore.createProject( - body.name || 'Untitled Project', - body.description || null, - ); - publishEvent('project.created', project); - appLogger('info', 'api', `Project created: ${project.name}`); - return c.json(project, 201); - }); - - app.get('/api/jobs', (c) => c.json(dbStore.listJobs())); - - app.get('/api/jobs/:id', async (c) => { - const detail = await getJobDetail(c.req.param('id')); - if (!detail) return c.json({ error: 'Job not found' }, 404); - return c.json(detail); - }); - - app.post('/api/jobs/:id/cancel', (c) => { - const jobId = c.req.param('id'); - const job = dbStore.getJob(jobId); - if (!job) return c.json({ error: 'Job not found' }, 404); - - if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') { - return c.json(job); - } + publishEvent, + }), + ); - const updatedJob = workerController.cancelQueuedOrRunningJob(jobId); - if (!updatedJob) { - return c.json({ error: 'Job cannot be cancelled right now' }, 409); - } + app.route( + '/api/codex', + createCodexRoutes({ + readCodexModelCatalog, + readLocalCodexSession, + readCodexAccountStatus: getCodexAccountStatus, + }), + ); - return c.json(updatedJob); - }); + app.route( + '/api/studio', + createStudioControlRoutes({ + resetStudioData, + worker: workerController, + }), + ); - app.post('/api/jobs', async (c) => { - const body = (await c.req.json()) as CreateJobRequest; - const projectId = body.projectId || dbStore.ensureDefaultProject().id; - const prompt = (body.prompt || body.sourceSpec?.prompt || '').trim(); - if (!prompt) return c.json({ error: 'Prompt is required' }, 400); - const jobId = randomUUID(); - // Legacy clients may still post only { kind, prompt }. Keep the local runtime - // Codex-first by normalizing those jobs onto the current provider boundary. - const providerId = - body.kind === 'dry_run' - ? 'dry_run' - : (body.providerId ?? body.sourceSpec?.providerId ?? 'codex'); - let sourceSpec: GenerationTaskSpec | null = body.sourceSpec - ? { - ...body.sourceSpec, - providerId: body.sourceSpec.providerId ?? providerId, - assets: body.sourceSpec.assets.map((asset) => ({ ...asset })), - } - : null; - const capabilityReport = readProviderCapabilities(readEditableStudioSettings(settingsStorage)); - const providerBlocker = getProviderExecutionBlocker(capabilityReport, providerId); - if (providerBlocker) { - return c.json(providerBlocker, 400); - } + app.route( + '/api/projects', + createProjectRoutes({ + listProjects: () => dbStore.listProjects(), + createProject: (name, description) => dbStore.createProject(name, description), + publishEvent, + logProjectCreated: (projectName) => + appLogger('info', 'api', `Project created: ${projectName}`), + }), + ); - let finalPrompt = prompt; - try { - const processedReferences = await processReferences( - jobId, - prompt, - body.references || [], - getSettings().libraryDir, - ); - finalPrompt = processedReferences.augmentedPrompt; - sourceSpec = hydrateSourceSpecAssetPaths( - sourceSpec, - body.references || [], - processedReferences.persistedRefs, - ); - } catch (error) { - if (error instanceof ReferenceProcessingError) { - return c.json( - { error: error.message, referenceName: error.referenceName, reason: error.reason }, - 400, + app.route( + '/api/jobs', + createJobRoutes({ + listJobs: () => dbStore.listJobs(), + getJob: (jobId) => dbStore.getJob(jobId), + getJobDetail, + cancelQueuedOrRunningJob: (jobId) => workerController.cancelQueuedOrRunningJob(jobId), + ensureDefaultProjectId: () => dbStore.ensureDefaultProject().id, + createJobId: () => randomUUID(), + createJob: (input) => + dbStore.createJob({ + id: input.id, + projectId: input.projectId, + kind: input.kind, + providerId: input.providerId, + sourceSpec: input.sourceSpec, + prompt: input.prompt, + execution: input.execution, + }), + updateJobFinalPrompt: (jobId, finalPrompt) => + dbStore.updateJobFinalPrompt(jobId, finalPrompt), + processReferences: (jobId, prompt, references, libraryDir) => + processReferences(jobId, prompt, references ?? [], libraryDir), + hydrateSourceSpecAssetPaths: (sourceSpec, references, persistedRefs) => + hydrateSourceSpecAssetPaths( + sourceSpec, + references ?? [], + persistedRefs as ProcessedReference[], + ), + readLibraryDir: () => getSettings().libraryDir, + resolveProviderExecutionBlocker: (providerId) => { + const capabilityReport = readProviderCapabilities( + readEditableStudioSettings(settingsStorage), ); - } - throw error; - } - - const job = dbStore.createJob({ - id: jobId, - projectId, - kind: body.kind, - providerId, - sourceSpec, - prompt, - execution: body.execution ?? null, - }); - const queuedJob = - finalPrompt === prompt ? job : (dbStore.updateJobFinalPrompt(job.id, finalPrompt) ?? job); - publishEvent('job.created', queuedJob); - appLogger('info', 'api', `Job created: ${queuedJob.kind}`, queuedJob.id); - workerController.enqueueJob(queuedJob); - return c.json(queuedJob, 201); - }); - - app.get('/api/assets', (c) => c.json(dbStore.listAssets())); - - app.get('/api/logs', (c) => c.json(dbStore.listLogs())); - - app.get('/api/libraries', (c) => c.json(listLibraries())); - - app.post('/api/libraries', async (c) => { - const body = await c.req.json().catch(() => ({})); - const library = registerLibrary({ - name: body.name || 'Untitled Library', - path: body.path, - isDefault: Boolean(body.isDefault), - }); - publishEvent('library.created', library); - return c.json(library, 201); - }); + return getProviderExecutionBlocker(capabilityReport, providerId); + }, + isReferenceProcessingError: (error): error is ReferenceProcessingError => + error instanceof ReferenceProcessingError, + publishEvent, + logJobCreated: (kind, jobId) => appLogger('info', 'api', `Job created: ${kind}`, jobId), + enqueueJob: (job) => workerController.enqueueJob(job), + }), + ); - app.put('/api/libraries/:id/default', (c) => { - const library = setDefaultLibrary(c.req.param('id')); - if (!library) return c.json({ error: 'Library not found' }, 404); - publishEvent('library.default', library); - return c.json(library); - }); + app.route( + '/api', + createAssetLogRoutes({ + listAssets: () => dbStore.listAssets(), + listLogs: () => dbStore.listLogs(), + }), + ); - app.delete('/api/libraries/:id', (c) => { - if (!removeLibrary(c.req.param('id'))) - return c.json({ error: 'Library not found or default library cannot be removed' }, 400); - return c.json({ ok: true }); - }); + app.route( + '/api/libraries', + createLibrariesRoutes({ + listLibraries, + registerLibrary, + setDefaultLibrary, + removeLibrary, + publishEvent, + }), + ); app.route( '/api/catalog', @@ -421,90 +272,24 @@ export async function createStudioApp( app.route('/api/workspaces', createWorkspaceRoutes()); - app.get('/api/events', (c) => { - c.header('X-Accel-Buffering', 'no'); - - return streamSSE(c, async (stream) => { - let cleanedUp = false; - - const send = (event: unknown) => { - void stream.writeSSE({ - data: JSON.stringify(event), - }); - }; - - const unsubscribe = subscribeEvents(send); - - const cleanup = () => { - if (cleanedUp) return; - cleanedUp = true; - unsubscribe(); - }; - - const abort = () => { - cleanup(); - if (!stream.aborted) { - stream.abort(); - } - }; - - c.req.raw.signal.addEventListener('abort', abort, { once: true }); - - try { - await stream.writeSSE({ - data: JSON.stringify({ - type: 'server.connected', - payload: { ok: true }, - createdAt: new Date().toISOString(), - }), - }); - - while (!stream.aborted) { - if (stream.aborted) { - break; - } - - await stream.sleep(10_000); - await stream.write(`: keep-alive ${Date.now()}\n\n`); - } - } finally { - cleanup(); - c.req.raw.signal.removeEventListener('abort', abort); - } - }); - }); - - app.get('/library/*', async (c) => { - const encoded = c.req.path.replace('/library/', ''); - const relative = decodeURIComponent(encoded); - if (relative.includes('..')) return c.notFound(); - const filePath = resolvePublicLibraryPath(relative); - if (!existsSync(filePath)) return c.notFound(); - - const url = new URL(c.req.url); - const variant = url.searchParams.get('variant'); - let servedPath = filePath; - - if (variant === 'thumb') { - try { - servedPath = await ensureThumbnailVariant(filePath, { - maxEdge: resolveThumbnailMaxEdge(url.searchParams.get('max')), - }); - } catch (error) { - appLogger( - 'warn', - 'library', - `Thumbnail generation failed for ${path.basename(filePath)}: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } + app.route( + '/api', + createEventStreamRoutes({ + subscribeEvents, + }), + ); - return new Response(Bun.file(servedPath), { - headers: buildLibraryAssetHeaders(servedPath, { - cacheSeconds: resolveAssetCacheSeconds(variant), - }), - }); - }); + app.route( + '/', + createLibraryRoutes({ + resolvePublicLibraryPath, + ensureThumbnailVariant, + buildLibraryAssetHeaders, + resolveAssetCacheSeconds, + resolveThumbnailMaxEdge, + logger: appLogger, + }), + ); return { app, diff --git a/apps/local-server/src/assetLogRoutes.test.ts b/apps/local-server/src/assetLogRoutes.test.ts new file mode 100644 index 00000000..361756e9 --- /dev/null +++ b/apps/local-server/src/assetLogRoutes.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vite-plus/test'; +import type { Asset, SystemLog } from '../../../packages/shared/src'; +import { createAssetLogRoutes } from './assetLogRoutes'; + +describe('assetLogRoutes', () => { + it('returns assets and logs through the route seam', async () => { + const assets: Asset[] = [ + { + id: 'asset-1', + projectId: 'project-1', + jobId: 'job-1', + filePath: 'D:/library/outputs/a.png', + thumbnailPath: null, + publicUrl: '/library/outputs/a.png', + prompt: 'a', + width: null, + height: null, + mimeType: 'image/png', + createdAt: '2026-05-29T00:00:00.000Z', + deletedAt: null, + }, + ]; + + const logs: SystemLog[] = [ + { + id: 1, + level: 'info', + scope: 'worker', + message: 'created', + jobId: 'job-1', + createdAt: '2026-05-29T00:00:00.000Z', + }, + ]; + + const routes = createAssetLogRoutes({ + listAssets: () => assets, + listLogs: () => logs, + }); + + const assetsResponse = await routes.request('/assets'); + expect(assetsResponse.status).toBe(200); + await expect(assetsResponse.json()).resolves.toEqual(assets); + + const logsResponse = await routes.request('/logs'); + expect(logsResponse.status).toBe(200); + await expect(logsResponse.json()).resolves.toEqual(logs); + }); +}); diff --git a/apps/local-server/src/assetLogRoutes.ts b/apps/local-server/src/assetLogRoutes.ts new file mode 100644 index 00000000..2f0a2d7c --- /dev/null +++ b/apps/local-server/src/assetLogRoutes.ts @@ -0,0 +1,16 @@ +import { Hono } from 'hono'; +import type { Asset, SystemLog } from '../../../packages/shared/src'; + +interface AssetLogRoutesDependencies { + listAssets: () => Asset[]; + listLogs: () => SystemLog[]; +} + +export function createAssetLogRoutes({ listAssets, listLogs }: AssetLogRoutesDependencies) { + const routes = new Hono(); + + routes.get('/assets', (c) => c.json(listAssets())); + routes.get('/logs', (c) => c.json(listLogs())); + + return routes; +} diff --git a/apps/local-server/src/codex/rpcClient.ts b/apps/local-server/src/codex/rpcClient.ts index dec9ca53..c43d92d1 100644 --- a/apps/local-server/src/codex/rpcClient.ts +++ b/apps/local-server/src/codex/rpcClient.ts @@ -44,6 +44,7 @@ export class CodexRpcClient { { resolve: (value: any) => void; reject: (error: Error) => void } >(); private notifications: JsonRpcMessage[] = []; + private notificationWaiters = new Set<(error: Error) => void>(); constructor({ ensureAppServer: ensureAppServerFn = ensureAppServer, @@ -88,6 +89,15 @@ export class CodexRpcClient { socket.addEventListener('message', (event) => this.handleMessage(String(event.data))); socket.addEventListener('close', () => { this.socket = null; + const error = new Error('Codex app-server socket closed'); + for (const pending of this.pending.values()) { + pending.reject(error); + } + this.pending.clear(); + for (const reject of this.notificationWaiters) { + reject(error); + } + this.notificationWaiters.clear(); }); resolve(); }); @@ -120,18 +130,26 @@ export class CodexRpcClient { return new Promise((resolve, reject) => { const started = Date.now(); + const handleSocketClose = (error: Error) => { + clearInterval(interval); + this.notificationWaiters.delete(handleSocketClose); + reject(error); + }; const interval = setInterval(() => { const match = this.notifications.find(predicate); if (match) { clearInterval(interval); + this.notificationWaiters.delete(handleSocketClose); resolve(match); return; } if (Date.now() - started > timeoutMs) { clearInterval(interval); + this.notificationWaiters.delete(handleSocketClose); reject(new Error('Timed out waiting for Codex notification')); } }, 250); + this.notificationWaiters.add(handleSocketClose); }); } diff --git a/apps/local-server/src/codex/sessionIdentity.ts b/apps/local-server/src/codex/sessionIdentity.ts new file mode 100644 index 00000000..246f741d --- /dev/null +++ b/apps/local-server/src/codex/sessionIdentity.ts @@ -0,0 +1,52 @@ +export interface CodexImagegenSessionIdentity { + sessionKey: string; + reusable: boolean; +} + +function normalizeSessionKeyToken(value: string | null | undefined) { + return (value ?? 'unknown') + .trim() + .toLowerCase() + .replace(/[^a-z0-9_-]+/g, '_') + .replace(/^_+|_+$/g, ''); +} + +export function resolveCodexImagegenSessionIdentity(params: { + jobId: string; + prompt: string; + requestedSessionKey?: string | null; + hasImageInputs?: boolean; + getSessionKey: (prompt: string) => string; +}): CodexImagegenSessionIdentity { + const explicitSessionKey = normalizeSessionKeyToken(params.requestedSessionKey); + if (explicitSessionKey && explicitSessionKey !== 'unknown') { + return { + sessionKey: explicitSessionKey, + reusable: true, + }; + } + + if (params.hasImageInputs) { + return { + sessionKey: normalizeSessionKeyToken(`job_${params.jobId}`) || 'job_unknown', + reusable: false, + }; + } + + const derivedSessionKey = normalizeSessionKeyToken(params.getSessionKey(params.prompt)); + if ( + derivedSessionKey && + derivedSessionKey !== 'unknown' && + derivedSessionKey !== 'unknown_pack' + ) { + return { + sessionKey: derivedSessionKey, + reusable: true, + }; + } + + return { + sessionKey: normalizeSessionKeyToken(`job_${params.jobId}`) || 'job_unknown', + reusable: false, + }; +} diff --git a/apps/local-server/src/codex/sessionPool.ts b/apps/local-server/src/codex/sessionPool.ts index 681ef760..acb806c0 100644 --- a/apps/local-server/src/codex/sessionPool.ts +++ b/apps/local-server/src/codex/sessionPool.ts @@ -84,7 +84,9 @@ export function createSessionPool({ function savePersistedImagegenSessions(sessions: Map) { const imagegenSessionRegistryPath = getImagegenSessionRegistryPath(); mkdirSync(path.dirname(imagegenSessionRegistryPath), { recursive: true }); - const entries = [...sessions.values()].sort((a, b) => a.sessionKey.localeCompare(b.sessionKey)); + const entries = Array.from(sessions.values()).toSorted((a, b) => + a.sessionKey.localeCompare(b.sessionKey), + ); writeFileSync(imagegenSessionRegistryPath, `${JSON.stringify(entries, null, 2)}\n`, 'utf8'); } diff --git a/apps/local-server/src/codex/turn.test.ts b/apps/local-server/src/codex/turn.test.ts new file mode 100644 index 00000000..fe9bb455 --- /dev/null +++ b/apps/local-server/src/codex/turn.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vite-plus/test'; + +import { resolveCodexImagegenSessionIdentity } from './sessionIdentity'; + +describe('resolveCodexImagegenSessionIdentity', () => { + it('falls back to an isolated per-job session when the derived session would be unknown_pack', () => { + expect( + resolveCodexImagegenSessionIdentity({ + jobId: 'job-123', + prompt: 'Generate something', + getSessionKey: () => 'unknown_pack', + }), + ).toEqual({ + sessionKey: 'job_job-123', + reusable: false, + }); + }); + + it('keeps reusable pack sessions when the prompt resolves to a stable pack key', () => { + expect( + resolveCodexImagegenSessionIdentity({ + jobId: 'job-123', + prompt: 'PACK: pack_01', + getSessionKey: () => 'pack_01', + }), + ).toEqual({ + sessionKey: 'pack_01', + reusable: true, + }); + }); + + it('uses an isolated per-job session when the turn includes image inputs', () => { + expect( + resolveCodexImagegenSessionIdentity({ + jobId: 'job-456', + prompt: 'PACK: pack_01', + hasImageInputs: true, + getSessionKey: () => 'pack_01', + }), + ).toEqual({ + sessionKey: 'job_job-456', + reusable: false, + }); + }); + + it('honors explicit session keys from callers that intentionally want reuse', () => { + expect( + resolveCodexImagegenSessionIdentity({ + jobId: 'job-123', + prompt: 'Generate something', + requestedSessionKey: 'Manual Session', + getSessionKey: () => 'unknown_pack', + }), + ).toEqual({ + sessionKey: 'manual_session', + reusable: true, + }); + }); +}); diff --git a/apps/local-server/src/codex/turn.ts b/apps/local-server/src/codex/turn.ts index 28a85d58..2ed8366f 100644 --- a/apps/local-server/src/codex/turn.ts +++ b/apps/local-server/src/codex/turn.ts @@ -5,6 +5,7 @@ import { log } from '../logger'; import { resolvePlatformPath } from '../platformPaths'; import { createAssetExtractor, type AssetExtractor } from './assetExtractor'; import { resolveJobExecutionOptions } from './executionOptions'; +import { resolveCodexImagegenSessionIdentity } from './sessionIdentity'; import { buildCodexImagegenTurnInput } from './turnInput'; import { closeImagegenSession, @@ -252,6 +253,7 @@ async function runImagegenJob( id: string; prompt: string; projectId: string; + sessionKey?: string; execution?: JobExecutionOptions | null; compiledInput?: CodexImagegenCompiledInput | null; signal?: AbortSignal; @@ -262,7 +264,14 @@ async function runImagegenJob( const transcriptDir = dependencies.resolveLibraryPath('transcripts', job.id); mkdirSync(transcriptDir, { recursive: true }); const transcriptPath = path.join(transcriptDir, 'events.jsonl'); - const sessionKey = dependencies.getSessionKey(job.prompt); + const sessionIdentity = resolveCodexImagegenSessionIdentity({ + jobId: job.id, + prompt: job.prompt, + requestedSessionKey: job.sessionKey, + hasImageInputs: (job.compiledInput?.payload.imageInputs.length ?? 0) > 0, + getSessionKey: dependencies.getSessionKey, + }); + const { sessionKey, reusable: reusableSession } = sessionIdentity; let lastError: unknown = null; for (let attempt = 1; attempt <= 2; attempt += 1) { @@ -295,13 +304,19 @@ async function runImagegenJob( try { await run; + if (!reusableSession) { + dependencies.closeSession(sessionKey, { invalidatePersistedThread: true }); + } return runResult; } catch (error) { + if (!reusableSession) { + dependencies.closeSession(sessionKey, { invalidatePersistedThread: true }); + } lastError = error; if (isAbortError(error)) throw error; const message = error instanceof Error ? error.message : String(error); const retryable = - /stream disconnected|Timed out waiting for Codex notification|thread.+not found|unknown thread|invalid thread/i.test( + /stream disconnected|Timed out waiting for Codex notification|thread.+not found|unknown thread|invalid thread|socket is not open|socket closed|websocket/i.test( message, ); if (!retryable || attempt === 2) throw error; @@ -355,6 +370,7 @@ export function createCodexTurn({ id: params.jobId, projectId: params.projectId, prompt: params.prompt, + sessionKey: params.sessionKey, execution: params.execution, compiledInput: params.compiledInput ?? null, signal: params.signal, diff --git a/apps/local-server/src/codexRoutes.test.ts b/apps/local-server/src/codexRoutes.test.ts new file mode 100644 index 00000000..4f3cda2e --- /dev/null +++ b/apps/local-server/src/codexRoutes.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it, vi } from "vite-plus/test"; +import { createCodexRoutes } from "./codexRoutes"; + +describe("codexRoutes", () => { + it("returns model catalog snapshot", async () => { + const readCodexModelCatalog = vi.fn(async () => ({ + models: [], + authMode: null, + planType: null, + recommendedDefaultModel: null, + source: "fallback" as const, + fetchedAt: "2026-05-29T00:00:00.000Z", + error: null, + })); + const readLocalCodexSession = vi.fn(async () => { + throw new Error("readLocalCodexSession should not be called"); + }); + const readCodexAccountStatus = vi.fn(async () => { + throw new Error("readCodexAccountStatus should not be called"); + }); + + const routes = createCodexRoutes({ + readCodexModelCatalog, + readLocalCodexSession, + readCodexAccountStatus, + }); + + const response = await routes.request("/models"); + expect(response.status).toBe(200); + + const payload = (await response.json()) as { models?: unknown[] }; + expect(Array.isArray(payload.models)).toBe(true); + expect(readCodexModelCatalog).toHaveBeenCalledTimes(1); + }); + + it("returns codex session and compatibility account snapshots", async () => { + const sessionPayload = { + authMode: "chatgpt" as const, + planType: "plus", + usage: null, + source: "app-server" as const, + fetchedAt: "2026-05-29T00:00:00.000Z", + error: null, + authLabel: "ChatGPT login", + state: "ready" as const, + reason: null, + isChatgptLogin: true, + isSupportedAuthMode: true, + canRunLocalJobs: true, + }; + + const readCodexModelCatalog = vi.fn(async () => { + throw new Error("readCodexModelCatalog should not be called"); + }); + const readLocalCodexSession = vi.fn(async () => sessionPayload); + const readCodexAccountStatus = vi.fn(async () => ({ + ...sessionPayload, + source: "fallback" as const, + })); + + const routes = createCodexRoutes({ + readCodexModelCatalog, + readLocalCodexSession, + readCodexAccountStatus, + }); + + const sessionResponse = await routes.request("/session"); + expect(sessionResponse.status).toBe(200); + await expect(sessionResponse.json()).resolves.toEqual(sessionPayload); + + const accountResponse = await routes.request("/account"); + expect(accountResponse.status).toBe(200); + await expect(accountResponse.json()).resolves.toEqual( + expect.objectContaining({ source: "fallback" }), + ); + + expect(readLocalCodexSession).toHaveBeenCalledTimes(1); + expect(readCodexAccountStatus).toHaveBeenCalledTimes(1); + }); +}); \ No newline at end of file diff --git a/apps/local-server/src/codexRoutes.ts b/apps/local-server/src/codexRoutes.ts new file mode 100644 index 00000000..2e43d7eb --- /dev/null +++ b/apps/local-server/src/codexRoutes.ts @@ -0,0 +1,28 @@ +import { Hono } from "hono"; +import type { + CodexAccountStatusResponse, + CodexModelCatalogResponse, + LocalCodexSessionResponse, +} from "../../../packages/shared/src"; + +interface CodexRoutesDependencies { + readCodexModelCatalog: () => Promise; + readLocalCodexSession: () => Promise; + readCodexAccountStatus: () => Promise; +} + +export function createCodexRoutes({ + readCodexModelCatalog, + readLocalCodexSession, + readCodexAccountStatus, +}: CodexRoutesDependencies) { + const routes = new Hono(); + + routes.get("/models", async (c) => c.json(await readCodexModelCatalog())); + + routes.get("/session", async (c) => c.json(await readLocalCodexSession())); + + routes.get("/account", async (c) => c.json(await readCodexAccountStatus())); + + return routes; +} \ No newline at end of file diff --git a/apps/local-server/src/config.ts b/apps/local-server/src/config.ts index 8df1ad52..27f875f5 100644 --- a/apps/local-server/src/config.ts +++ b/apps/local-server/src/config.ts @@ -7,8 +7,8 @@ const DEFAULT_LIBRARY_DIR = path.join( process.env.USERPROFILE?.trim() || process.env.HOME?.trim() || os.homedir() || process.cwd(), 'AI-Studio-Library', ); -const DEFAULT_SERVER_PORT = 4317; -const DEFAULT_CODEX_WS_PORT = 4318; +const DEFAULT_SERVER_PORT = 17223; +const DEFAULT_CODEX_WS_PORT = 17224; const DEFAULT_CODEX_IMAGEGEN_MODEL = 'gpt-5.4-mini'; const DEFAULT_CODEX_IMAGEGEN_REASONING_EFFORT: StudioSettings['codexImagegenReasoningEffort'] = 'low'; diff --git a/apps/local-server/src/eventStreamRoutes.test.ts b/apps/local-server/src/eventStreamRoutes.test.ts new file mode 100644 index 00000000..34489c7f --- /dev/null +++ b/apps/local-server/src/eventStreamRoutes.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it, vi } from 'vite-plus/test'; +import { createEventStreamRoutes } from './eventStreamRoutes'; + +describe('eventStreamRoutes', () => { + it('returns SSE handshake payload and no-buffering header', async () => { + const subscribeEvents = vi.fn(() => () => true); + const routes = createEventStreamRoutes({ + subscribeEvents, + }); + + const response = await routes.request('/events'); + + expect(response.status).toBe(200); + expect(response.headers.get('X-Accel-Buffering')).toBe('no'); + expect(response.headers.get('content-type')?.toLowerCase()).toContain('text/event-stream'); + + const reader = response.body?.getReader(); + expect(reader).toBeDefined(); + + const firstChunk = await reader!.read(); + const text = new TextDecoder().decode(firstChunk.value); + + expect(text).toContain('server.connected'); + expect(subscribeEvents).toHaveBeenCalledTimes(1); + + await reader!.cancel(); + }); +}); diff --git a/apps/local-server/src/eventStreamRoutes.ts b/apps/local-server/src/eventStreamRoutes.ts new file mode 100644 index 00000000..848555ea --- /dev/null +++ b/apps/local-server/src/eventStreamRoutes.ts @@ -0,0 +1,66 @@ +import { Hono } from 'hono'; +import { streamSSE } from 'hono/streaming'; +import type { subscribeEvents } from './events'; + +interface EventStreamRoutesDependencies { + subscribeEvents: typeof subscribeEvents; +} + +export function createEventStreamRoutes({ subscribeEvents }: EventStreamRoutesDependencies) { + const routes = new Hono(); + + routes.get('/events', (c) => { + c.header('X-Accel-Buffering', 'no'); + + return streamSSE(c, async (stream) => { + let cleanedUp = false; + + const send = (event: unknown) => { + void stream.writeSSE({ + data: JSON.stringify(event), + }); + }; + + const unsubscribe = subscribeEvents(send); + + const cleanup = () => { + if (cleanedUp) return; + cleanedUp = true; + unsubscribe(); + }; + + const abort = () => { + cleanup(); + if (!stream.aborted) { + stream.abort(); + } + }; + + c.req.raw.signal.addEventListener('abort', abort, { once: true }); + + try { + await stream.writeSSE({ + data: JSON.stringify({ + type: 'server.connected', + payload: { ok: true }, + createdAt: new Date().toISOString(), + }), + }); + + while (!stream.aborted) { + if (stream.aborted) { + break; + } + + await stream.sleep(10_000); + await stream.write(`: keep-alive ${Date.now()}\n\n`); + } + } finally { + cleanup(); + c.req.raw.signal.removeEventListener('abort', abort); + } + }); + }); + + return routes; +} diff --git a/apps/local-server/src/init.ts b/apps/local-server/src/init.ts index 2c924f64..69f160d3 100644 --- a/apps/local-server/src/init.ts +++ b/apps/local-server/src/init.ts @@ -24,6 +24,7 @@ export function initStudio() { `STUDIO_LIBRARY_DIR=${settings.libraryDir}`, `STUDIO_SERVER_PORT=${settings.serverPort}`, `STUDIO_CODEX_WS_PORT=${settings.codexWsPort}`, + `VITE_STUDIO_API_BASE=http://localhost:${settings.serverPort}`, `STUDIO_MAX_CONCURRENT_CODEX_JOBS=${settings.codexMaxConcurrentJobs}`, `CODEX_IMAGEGEN_MODEL=${settings.codexImagegenModel}`, `CODEX_IMAGEGEN_REASONING_EFFORT=${settings.codexImagegenReasoningEffort}`, diff --git a/apps/local-server/src/jobRoutes.test.ts b/apps/local-server/src/jobRoutes.test.ts new file mode 100644 index 00000000..7b513e38 --- /dev/null +++ b/apps/local-server/src/jobRoutes.test.ts @@ -0,0 +1,307 @@ +import { describe, expect, it, vi } from 'vite-plus/test'; +import { + createGenerationTaskSpec, + type GenerationTaskSpec, + type Job, + type JobDetailResponse, + type StudioEvent, +} from '../../../packages/shared/src'; +import { createJobRoutes } from './jobRoutes'; + +function isReferenceProcessingError( + error: unknown, +): error is { message: string; referenceName: string | null; reason: string } { + void error; + return false; +} + +function publishEvent(type: string, payload: unknown): StudioEvent { + return { + type, + payload, + createdAt: '2026-05-29T00:00:00.000Z', + }; +} + +function createJob(overrides: Partial = {}): Job { + return { + id: overrides.id ?? 'job-1', + projectId: overrides.projectId ?? 'project-1', + kind: overrides.kind ?? 'codex_imagegen', + providerId: overrides.providerId ?? 'codex', + sourceSpec: overrides.sourceSpec ?? null, + status: overrides.status ?? 'queued', + execution: overrides.execution ?? null, + originalPrompt: overrides.originalPrompt ?? 'orig', + expandedPrompt: overrides.expandedPrompt ?? null, + finalPromptUsed: overrides.finalPromptUsed ?? 'orig', + error: overrides.error ?? null, + createdAt: overrides.createdAt ?? '2026-05-29T00:00:00.000Z', + updatedAt: overrides.updatedAt ?? '2026-05-29T00:00:00.000Z', + completedAt: overrides.completedAt ?? null, + }; +} + +function createSourceSpec(overrides: Partial = {}): GenerationTaskSpec { + return createGenerationTaskSpec({ + id: overrides.id ?? 'spec-1', + task: overrides.task ?? 'image_generate', + providerId: overrides.providerId ?? 'codex', + prompt: overrides.prompt ?? 'draw a lighthouse', + negativePrompt: overrides.negativePrompt, + recipeId: overrides.recipeId, + recipeParams: overrides.recipeParams, + stylePresetId: overrides.stylePresetId, + assets: overrides.assets, + output: overrides.output, + metadata: overrides.metadata, + }); +} + +function createJobDetail(job: Job): JobDetailResponse { + return { + job, + events: [], + turn: null, + transcriptEntries: [], + catalogImages: [], + metrics: { + timings: [], + tokenUsage: null, + estimatedPromptTokens: 0, + }, + }; +} + +describe('jobRoutes', () => { + it('lists jobs and returns detail when found', async () => { + const job = createJob({ id: 'job-1' }); + const routes = createJobRoutes({ + listJobs: () => [job], + getJob: () => null, + getJobDetail: async (jobId) => (jobId === 'job-1' ? createJobDetail(job) : null), + cancelQueuedOrRunningJob: () => null, + ensureDefaultProjectId: () => 'project-1', + createJobId: () => 'job-new', + createJob: () => job, + updateJobFinalPrompt: () => null, + processReferences: async () => ({ augmentedPrompt: 'x', persistedRefs: [] }), + hydrateSourceSpecAssetPaths: (sourceSpec) => sourceSpec, + readLibraryDir: () => 'D:/library', + resolveProviderExecutionBlocker: () => null, + isReferenceProcessingError, + publishEvent, + logJobCreated: () => {}, + enqueueJob: () => {}, + }); + + const listResponse = await routes.request('/'); + expect(listResponse.status).toBe(200); + await expect(listResponse.json()).resolves.toEqual([job]); + + const detailResponse = await routes.request('/job-1'); + expect(detailResponse.status).toBe(200); + await expect(detailResponse.json()).resolves.toEqual(expect.objectContaining({ job })); + + const missingResponse = await routes.request('/missing'); + expect(missingResponse.status).toBe(404); + }); + + it('cancels queued jobs and keeps terminal jobs unchanged', async () => { + const queued = createJob({ id: 'job-q', status: 'queued' }); + const cancelled = createJob({ id: 'job-q', status: 'cancelled' }); + const completed = createJob({ id: 'job-done', status: 'completed' }); + + const getJob = vi.fn<(...args: [string]) => Job | null>().mockImplementation((jobId) => { + if (jobId === 'job-q') return queued; + if (jobId === 'job-done') return completed; + return null; + }); + + const cancelQueuedOrRunningJob = vi + .fn<(...args: [string]) => Job | null>() + .mockImplementation((jobId) => (jobId === 'job-q' ? cancelled : null)); + + const routes = createJobRoutes({ + listJobs: () => [], + getJob, + getJobDetail: async () => null, + cancelQueuedOrRunningJob, + ensureDefaultProjectId: () => 'project-1', + createJobId: () => 'job-new', + createJob: () => queued, + updateJobFinalPrompt: () => null, + processReferences: async () => ({ augmentedPrompt: 'x', persistedRefs: [] }), + hydrateSourceSpecAssetPaths: (sourceSpec) => sourceSpec, + readLibraryDir: () => 'D:/library', + resolveProviderExecutionBlocker: () => null, + isReferenceProcessingError, + publishEvent, + logJobCreated: () => {}, + enqueueJob: () => {}, + }); + + const cancelledResponse = await routes.request('/job-q/cancel', { method: 'POST' }); + expect(cancelledResponse.status).toBe(200); + await expect(cancelledResponse.json()).resolves.toEqual(cancelled); + + const terminalResponse = await routes.request('/job-done/cancel', { method: 'POST' }); + expect(terminalResponse.status).toBe(200); + await expect(terminalResponse.json()).resolves.toEqual(completed); + + const missingResponse = await routes.request('/missing/cancel', { method: 'POST' }); + expect(missingResponse.status).toBe(404); + }); + + it('creates jobs with reference processing and provider blocker checks', async () => { + const publishEvent = vi.fn(); + const enqueueJob = vi.fn(); + const logJobCreated = vi.fn(); + const created = createJob({ id: 'job-new', kind: 'codex_imagegen' }); + + const routes = createJobRoutes({ + listJobs: () => [], + getJob: () => null, + getJobDetail: async () => null, + cancelQueuedOrRunningJob: () => null, + ensureDefaultProjectId: () => 'project-default', + createJobId: () => 'job-new', + createJob: () => created, + updateJobFinalPrompt: () => created, + processReferences: async () => ({ + augmentedPrompt: 'draw a lighthouse with refs', + persistedRefs: [{ id: 'ref-1' }], + }), + hydrateSourceSpecAssetPaths: (sourceSpec) => sourceSpec, + readLibraryDir: () => 'D:/library', + resolveProviderExecutionBlocker: (providerId) => + providerId === 'blocked' ? { error: 'provider_blocked' } : null, + isReferenceProcessingError: ( + error, + ): error is { message: string; referenceName: string | null; reason: string } => + typeof error === 'object' && error !== null && 'reason' in error, + publishEvent, + logJobCreated, + enqueueJob, + }); + + const blockedResponse = await routes.request('/', { + method: 'POST', + body: JSON.stringify({ kind: 'codex_imagegen', prompt: 'x', providerId: 'blocked' }), + headers: { 'Content-Type': 'application/json' }, + }); + expect(blockedResponse.status).toBe(400); + + const createResponse = await routes.request('/', { + method: 'POST', + body: JSON.stringify({ + kind: 'codex_imagegen', + prompt: 'draw a lighthouse', + sourceSpec: createSourceSpec(), + }), + headers: { 'Content-Type': 'application/json' }, + }); + + expect(createResponse.status).toBe(201); + await expect(createResponse.json()).resolves.toEqual(created); + expect(publishEvent).toHaveBeenCalledWith('job.created', created); + expect(logJobCreated).toHaveBeenCalledWith('codex_imagegen', 'job-new'); + expect(enqueueJob).toHaveBeenCalledWith(created); + + const invalidPromptResponse = await routes.request('/', { + method: 'POST', + body: JSON.stringify({ kind: 'codex_imagegen', prompt: ' ' }), + headers: { 'Content-Type': 'application/json' }, + }); + expect(invalidPromptResponse.status).toBe(400); + }); + + it('rejects invalid local queued batch ids before enqueue', async () => { + const enqueueJob = vi.fn(); + const routes = createJobRoutes({ + listJobs: () => [], + getJob: () => null, + getJobDetail: async () => null, + cancelQueuedOrRunningJob: () => null, + ensureDefaultProjectId: () => 'project-default', + createJobId: () => 'job-new', + createJob: () => createJob({ id: 'job-new' }), + updateJobFinalPrompt: () => null, + processReferences: async () => ({ + augmentedPrompt: 'draw a lighthouse', + persistedRefs: [], + }), + hydrateSourceSpecAssetPaths: (sourceSpec) => sourceSpec, + readLibraryDir: () => 'D:/library', + resolveProviderExecutionBlocker: () => null, + isReferenceProcessingError, + publishEvent, + logJobCreated: () => {}, + enqueueJob, + }); + + const response = await routes.request('/', { + method: 'POST', + body: JSON.stringify({ + kind: 'image_generate', + prompt: 'draw a lighthouse', + sourceSpec: createSourceSpec({ + id: 'spec-legacy', + metadata: { workspaceId: 'workspace-1', batchId: '1234-invalid' }, + }), + }), + headers: { 'Content-Type': 'application/json' }, + }); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + code: 'invalid_spec_id', + error: 'Invalid Generation Task Spec', + issues: expect.arrayContaining([expect.objectContaining({ code: 'invalid_batch_id' })]), + }); + expect(enqueueJob).not.toHaveBeenCalled(); + }); + + it('rejects provider mismatch between job request and source spec', async () => { + const enqueueJob = vi.fn(); + const routes = createJobRoutes({ + listJobs: () => [], + getJob: () => null, + getJobDetail: async () => null, + cancelQueuedOrRunningJob: () => null, + ensureDefaultProjectId: () => 'project-default', + createJobId: () => 'job-new', + createJob: () => createJob({ id: 'job-new' }), + updateJobFinalPrompt: () => null, + processReferences: async () => ({ + augmentedPrompt: 'draw a lighthouse', + persistedRefs: [], + }), + hydrateSourceSpecAssetPaths: (sourceSpec) => sourceSpec, + readLibraryDir: () => 'D:/library', + resolveProviderExecutionBlocker: () => null, + isReferenceProcessingError, + publishEvent, + logJobCreated: () => {}, + enqueueJob, + }); + + const response = await routes.request('/', { + method: 'POST', + body: JSON.stringify({ + kind: 'image_generate', + providerId: 'google', + prompt: 'draw a lighthouse', + sourceSpec: createSourceSpec({ providerId: 'codex' }), + }), + headers: { 'Content-Type': 'application/json' }, + }); + + expect(response.status).toBe(400); + await expect(response.json()).resolves.toMatchObject({ + code: 'invalid_provider', + field: 'sourceSpec.providerId', + }); + expect(enqueueJob).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/local-server/src/jobRoutes.ts b/apps/local-server/src/jobRoutes.ts new file mode 100644 index 00000000..66690533 --- /dev/null +++ b/apps/local-server/src/jobRoutes.ts @@ -0,0 +1,212 @@ +import { Hono } from 'hono'; +import type { + CreateJobRequest, + GenerationTaskSpec, + Job, + JobDetailResponse, +} from '../../../packages/shared/src'; +import { validateGenerationTaskSpec } from '../../../packages/shared/src'; +import type { publishEvent } from './events'; + +interface ProcessReferencesResult { + augmentedPrompt: string; + persistedRefs: unknown[]; +} + +interface ReferenceProcessingErrorLike { + message: string; + referenceName: string | null; + reason: string; +} + +interface JobRoutesDependencies { + listJobs: () => Job[]; + getJob: (jobId: string) => Job | null; + getJobDetail: (jobId: string) => Promise; + cancelQueuedOrRunningJob: (jobId: string) => Job | null; + ensureDefaultProjectId: () => string; + createJobId: () => string; + createJob: (input: { + id: string; + projectId: string; + kind: Job['kind']; + providerId: Job['providerId']; + sourceSpec: GenerationTaskSpec | null; + prompt: string; + execution: Job['execution']; + }) => Job; + updateJobFinalPrompt: (jobId: string, finalPrompt: string) => Job | null; + processReferences: ( + jobId: string, + prompt: string, + references: CreateJobRequest['references'], + libraryDir: string, + ) => Promise; + hydrateSourceSpecAssetPaths: ( + sourceSpec: GenerationTaskSpec | null, + references: CreateJobRequest['references'], + persistedRefs: unknown[], + ) => GenerationTaskSpec | null; + readLibraryDir: () => string; + resolveProviderExecutionBlocker: (providerId: string) => unknown; + isReferenceProcessingError: (error: unknown) => error is ReferenceProcessingErrorLike; + publishEvent: typeof publishEvent; + logJobCreated: (kind: string, jobId: string) => void; + enqueueJob: (job: Job) => void; +} + +function shouldRequireLocalRunIds(sourceSpec: GenerationTaskSpec | null) { + const metadata = + sourceSpec?.metadata && + typeof sourceSpec.metadata === 'object' && + !Array.isArray(sourceSpec.metadata) + ? sourceSpec.metadata + : {}; + return Boolean( + sourceSpec && + (typeof metadata.batchId === 'string' || typeof metadata.workspaceId === 'string'), + ); +} + +function createValidationErrorResponse( + sourceSpec: GenerationTaskSpec, + providerId: Job['providerId'], +) { + const issues = validateGenerationTaskSpec(sourceSpec, { + requireLocalRunIds: shouldRequireLocalRunIds(sourceSpec), + requireHydratedAssets: true, + expectedProviderId: providerId, + }); + if (issues.length === 0) return null; + + return { + error: 'Invalid Generation Task Spec', + code: issues[0].code, + field: issues[0].field, + reason: issues[0].message, + issues, + }; +} + +export function createJobRoutes({ + listJobs, + getJob, + getJobDetail, + cancelQueuedOrRunningJob, + ensureDefaultProjectId, + createJobId, + createJob, + updateJobFinalPrompt, + processReferences, + hydrateSourceSpecAssetPaths, + readLibraryDir, + resolveProviderExecutionBlocker, + isReferenceProcessingError, + publishEvent, + logJobCreated, + enqueueJob, +}: JobRoutesDependencies) { + const routes = new Hono(); + + routes.get('/', (c) => c.json(listJobs())); + + routes.get('/:id', async (c) => { + const detail = await getJobDetail(c.req.param('id')); + if (!detail) return c.json({ error: 'Job not found' }, 404); + return c.json(detail); + }); + + routes.post('/:id/cancel', (c) => { + const jobId = c.req.param('id'); + const job = getJob(jobId); + if (!job) return c.json({ error: 'Job not found' }, 404); + + if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') { + return c.json(job); + } + + const updatedJob = cancelQueuedOrRunningJob(jobId); + if (!updatedJob) { + return c.json({ error: 'Job cannot be cancelled right now' }, 409); + } + + return c.json(updatedJob); + }); + + routes.post('/', async (c) => { + const body = (await c.req.json()) as CreateJobRequest; + const projectId = body.projectId || ensureDefaultProjectId(); + const prompt = (body.prompt || body.sourceSpec?.prompt || '').trim(); + if (!prompt) return c.json({ error: 'Prompt is required' }, 400); + const jobId = createJobId(); + + const providerId = + body.kind === 'dry_run' + ? 'dry_run' + : (body.providerId ?? body.sourceSpec?.providerId ?? 'codex'); + + let sourceSpec: GenerationTaskSpec | null = body.sourceSpec + ? { + ...body.sourceSpec, + providerId: body.sourceSpec.providerId ?? providerId, + assets: body.sourceSpec.assets.map((asset) => ({ ...asset })), + } + : null; + + const providerBlocker = resolveProviderExecutionBlocker(providerId); + if (providerBlocker) { + return c.json(providerBlocker, 400); + } + + let finalPrompt = prompt; + try { + const processedReferences = await processReferences( + jobId, + prompt, + body.references || [], + readLibraryDir(), + ); + finalPrompt = processedReferences.augmentedPrompt; + sourceSpec = hydrateSourceSpecAssetPaths( + sourceSpec, + body.references || [], + processedReferences.persistedRefs, + ); + } catch (error) { + if (isReferenceProcessingError(error)) { + return c.json( + { error: error.message, referenceName: error.referenceName, reason: error.reason }, + 400, + ); + } + throw error; + } + + if (sourceSpec) { + const validationError = createValidationErrorResponse(sourceSpec, providerId); + if (validationError) { + return c.json(validationError, 400); + } + } + + const job = createJob({ + id: jobId, + projectId, + kind: body.kind, + providerId, + sourceSpec, + prompt, + execution: body.execution ?? null, + }); + + const queuedJob = + finalPrompt === prompt ? job : (updateJobFinalPrompt(job.id, finalPrompt) ?? job); + + publishEvent('job.created', queuedJob); + logJobCreated(queuedJob.kind, queuedJob.id); + enqueueJob(queuedJob); + return c.json(queuedJob, 201); + }); + + return routes; +} diff --git a/apps/local-server/src/librariesRoutes.test.ts b/apps/local-server/src/librariesRoutes.test.ts new file mode 100644 index 00000000..11b093cf --- /dev/null +++ b/apps/local-server/src/librariesRoutes.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it, vi } from "vite-plus/test"; +import type { StudioLibrary } from "./libraries"; +import { createLibrariesRoutes } from "./librariesRoutes"; + +function makeLibrary(overrides: Partial = {}): StudioLibrary { + return { + id: overrides.id ?? "library-1", + name: overrides.name ?? "Library 1", + path: overrides.path ?? "D:/Library-1", + isDefault: overrides.isDefault ?? true, + createdAt: overrides.createdAt ?? "2026-05-29T00:00:00.000Z", + }; +} + +describe("librariesRoutes", () => { + it("lists and registers libraries through the route seam", async () => { + const listed = [makeLibrary()]; + const created = makeLibrary({ id: "library-2", isDefault: false, name: "New Library" }); + const listLibraries = vi.fn(() => listed); + const registerLibrary = vi.fn(() => created); + const setDefaultLibrary = vi.fn(() => null); + const removeLibrary = vi.fn(() => false); + const publishEvent = vi.fn(); + + const routes = createLibrariesRoutes({ + listLibraries, + registerLibrary, + setDefaultLibrary, + removeLibrary, + publishEvent, + }); + + const listResponse = await routes.request("/"); + expect(listResponse.status).toBe(200); + await expect(listResponse.json()).resolves.toEqual(listed); + + const createResponse = await routes.request("/", { + method: "POST", + body: JSON.stringify({ name: "New Library", path: "D:/new-library", isDefault: false }), + headers: { "Content-Type": "application/json" }, + }); + + expect(createResponse.status).toBe(201); + await expect(createResponse.json()).resolves.toEqual(created); + expect(registerLibrary).toHaveBeenCalledWith({ + name: "New Library", + path: "D:/new-library", + isDefault: false, + }); + expect(publishEvent).toHaveBeenCalledWith("library.created", created); + }); + + it("handles set-default and delete responses", async () => { + const listLibraries = vi.fn(() => []); + const registerLibrary = vi.fn(() => makeLibrary()); + const setDefaultLibrary = vi + .fn<(...args: [string]) => StudioLibrary | null>() + .mockReturnValueOnce(makeLibrary({ id: "library-2", isDefault: true })) + .mockReturnValueOnce(null); + const removeLibrary = vi + .fn<(...args: [string]) => boolean>() + .mockReturnValueOnce(true) + .mockReturnValueOnce(false); + const publishEvent = vi.fn(); + + const routes = createLibrariesRoutes({ + listLibraries, + registerLibrary, + setDefaultLibrary, + removeLibrary, + publishEvent, + }); + + const setDefaultOk = await routes.request("/library-2/default", { method: "PUT" }); + expect(setDefaultOk.status).toBe(200); + await expect(setDefaultOk.json()).resolves.toEqual(expect.objectContaining({ id: "library-2" })); + + const setDefaultMissing = await routes.request("/missing/default", { method: "PUT" }); + expect(setDefaultMissing.status).toBe(404); + + const deleteOk = await routes.request("/library-2", { method: "DELETE" }); + expect(deleteOk.status).toBe(200); + await expect(deleteOk.json()).resolves.toEqual({ ok: true }); + + const deleteMissing = await routes.request("/missing", { method: "DELETE" }); + expect(deleteMissing.status).toBe(400); + + expect(publishEvent).toHaveBeenCalledWith( + "library.default", + expect.objectContaining({ id: "library-2" }), + ); + }); +}); \ No newline at end of file diff --git a/apps/local-server/src/librariesRoutes.ts b/apps/local-server/src/librariesRoutes.ts new file mode 100644 index 00000000..8b3e94d6 --- /dev/null +++ b/apps/local-server/src/librariesRoutes.ts @@ -0,0 +1,50 @@ +import { Hono } from "hono"; +import type { registerLibrary, listLibraries, removeLibrary, setDefaultLibrary } from "./libraries"; +import type { publishEvent } from "./events"; + +interface LibrariesRoutesDependencies { + listLibraries: typeof listLibraries; + registerLibrary: typeof registerLibrary; + setDefaultLibrary: typeof setDefaultLibrary; + removeLibrary: typeof removeLibrary; + publishEvent: typeof publishEvent; +} + +export function createLibrariesRoutes({ + listLibraries, + registerLibrary, + setDefaultLibrary, + removeLibrary, + publishEvent, +}: LibrariesRoutesDependencies) { + const routes = new Hono(); + + routes.get("/", (c) => c.json(listLibraries())); + + routes.post("/", async (c) => { + const body = await c.req.json().catch(() => ({})); + const library = registerLibrary({ + name: body.name || "Untitled Library", + path: body.path, + isDefault: Boolean(body.isDefault), + }); + publishEvent("library.created", library); + return c.json(library, 201); + }); + + routes.put("/:id/default", (c) => { + const library = setDefaultLibrary(c.req.param("id")); + if (!library) return c.json({ error: "Library not found" }, 404); + publishEvent("library.default", library); + return c.json(library); + }); + + routes.delete("/:id", (c) => { + if (!removeLibrary(c.req.param("id"))) { + return c.json({ error: "Library not found or default library cannot be removed" }, 400); + } + return c.json({ ok: true }); + }); + + return routes; +} \ No newline at end of file diff --git a/apps/local-server/src/libraryRoutes.test.ts b/apps/local-server/src/libraryRoutes.test.ts new file mode 100644 index 00000000..aa64421d --- /dev/null +++ b/apps/local-server/src/libraryRoutes.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it, vi } from 'vite-plus/test'; +import { createLibraryRoutes } from './libraryRoutes'; + +describe('libraryRoutes', () => { + function createTestHeaders(contentType: string) { + return { + 'Content-Type': contentType, + 'Cache-Control': 'private, max-age=60', + 'Content-Length': '2', + 'Last-Modified': 'Wed, 29 May 2026 00:00:00 GMT', + ETag: 'W/"2-1"', + }; + } + + it('returns 404 for traversal attempts', async () => { + const routes = createLibraryRoutes({ + resolvePublicLibraryPath: (relative) => `D:/library/${relative}`, + ensureThumbnailVariant: vi.fn(async (filePath) => filePath), + buildLibraryAssetHeaders: () => createTestHeaders('image/png'), + resolveAssetCacheSeconds: () => 60, + resolveThumbnailMaxEdge: () => 512, + fileExists: () => true, + createFileResponse: () => new Response('ok'), + }); + + const response = await routes.request('/library/../secret.png'); + expect(response.status).toBe(404); + }); + + it('returns 404 when file does not exist', async () => { + const routes = createLibraryRoutes({ + resolvePublicLibraryPath: () => 'D:/library/outputs/missing.png', + ensureThumbnailVariant: vi.fn(async (filePath) => filePath), + buildLibraryAssetHeaders: () => createTestHeaders('image/png'), + resolveAssetCacheSeconds: () => 60, + resolveThumbnailMaxEdge: () => 512, + fileExists: () => false, + createFileResponse: () => new Response('ok'), + }); + + const response = await routes.request('/library/outputs/missing.png'); + expect(response.status).toBe(404); + }); + + it('serves thumbnail variant and applies cache headers', async () => { + const ensureThumbnailVariant = vi.fn( + async () => 'D:/library/outputs/thumbnails/img-thumb.webp', + ); + const createFileResponse = vi.fn(() => new Response('thumbnail')); + + const routes = createLibraryRoutes({ + resolvePublicLibraryPath: () => 'D:/library/outputs/img.png', + ensureThumbnailVariant, + buildLibraryAssetHeaders: () => createTestHeaders('image/webp'), + resolveAssetCacheSeconds: (variant) => (variant === 'thumb' ? 86400 : 3600), + resolveThumbnailMaxEdge: () => 640, + fileExists: () => true, + createFileResponse, + }); + + const response = await routes.request( + 'http://local.test/library/outputs/img.png?variant=thumb&max=640', + ); + + expect(response.status).toBe(200); + await expect(response.text()).resolves.toBe('thumbnail'); + expect(ensureThumbnailVariant).toHaveBeenCalledWith('D:/library/outputs/img.png', { + maxEdge: 640, + }); + expect(createFileResponse).toHaveBeenCalledWith('D:/library/outputs/thumbnails/img-thumb.webp'); + expect(response.headers.get('Content-Type')).toBe('image/webp'); + }); +}); diff --git a/apps/local-server/src/libraryRoutes.ts b/apps/local-server/src/libraryRoutes.ts new file mode 100644 index 00000000..6f7a4b93 --- /dev/null +++ b/apps/local-server/src/libraryRoutes.ts @@ -0,0 +1,74 @@ +import { existsSync } from 'node:fs'; +import path from 'node:path'; +import { Hono } from 'hono'; +import type { resolvePublicLibraryPath } from './library'; +import { + buildLibraryAssetHeaders, + ensureThumbnailVariant, + resolveAssetCacheSeconds, + resolveThumbnailMaxEdge, +} from './libraryAssetVariants'; +import type { log } from './logger'; + +interface LibraryRoutesDependencies { + resolvePublicLibraryPath: typeof resolvePublicLibraryPath; + ensureThumbnailVariant: typeof ensureThumbnailVariant; + buildLibraryAssetHeaders: typeof buildLibraryAssetHeaders; + resolveAssetCacheSeconds: typeof resolveAssetCacheSeconds; + resolveThumbnailMaxEdge: typeof resolveThumbnailMaxEdge; + fileExists?: (filePath: string) => boolean; + createFileResponse?: (filePath: string) => Response; + logger?: typeof log; +} + +export function createLibraryRoutes({ + resolvePublicLibraryPath, + ensureThumbnailVariant, + buildLibraryAssetHeaders, + resolveAssetCacheSeconds, + resolveThumbnailMaxEdge, + fileExists = existsSync, + createFileResponse = (filePath: string) => new Response(Bun.file(filePath)), + logger, +}: LibraryRoutesDependencies) { + const routes = new Hono(); + + routes.get('/library/*', async (c) => { + const encoded = c.req.path.replace('/library/', ''); + const relative = decodeURIComponent(encoded); + if (relative.includes('..')) return c.notFound(); + const filePath = resolvePublicLibraryPath(relative); + if (!fileExists(filePath)) return c.notFound(); + + const url = new URL(c.req.url); + const variant = url.searchParams.get('variant'); + let servedPath = filePath; + + if (variant === 'thumb') { + try { + servedPath = await ensureThumbnailVariant(filePath, { + maxEdge: resolveThumbnailMaxEdge(url.searchParams.get('max')), + }); + } catch (error) { + logger?.( + 'warn', + 'library', + `Thumbnail generation failed for ${path.basename(filePath)}: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + const fileResponse = createFileResponse(servedPath); + const headers = buildLibraryAssetHeaders(servedPath, { + cacheSeconds: resolveAssetCacheSeconds(variant), + }); + + return new Response(fileResponse.body, { + status: fileResponse.status, + statusText: fileResponse.statusText, + headers, + }); + }); + + return routes; +} diff --git a/apps/local-server/src/outputSourceRoutes.test.ts b/apps/local-server/src/outputSourceRoutes.test.ts new file mode 100644 index 00000000..6009f855 --- /dev/null +++ b/apps/local-server/src/outputSourceRoutes.test.ts @@ -0,0 +1,163 @@ +import { describe, expect, it, vi } from "vite-plus/test"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { + createDefaultEditableStudioSettings, + type CatalogImage, +} from "../../../packages/shared/src"; +import type { StudioSettingsStorage } from "./studioSettingsStore"; +import { createOutputSourceRoutes } from "./outputSourceRoutes"; + +function createMemoryStorage(initial?: Record): StudioSettingsStorage { + const values = new Map(Object.entries(initial ?? {})); + return { + getSetting(key) { + return values.get(key) ?? null; + }, + setSetting(key, value) { + values.set(key, value); + }, + }; +} + +describe("outputSourceRoutes", () => { + it("registers and lists output source files through the route seam", async () => { + const root = mkdtempSync(path.join(os.tmpdir(), "output-source-routes-")); + const sourceDir = path.join(root, "source"); + const libraryDir = path.join(root, "library"); + + try { + mkdirSync(sourceDir, { recursive: true }); + mkdirSync(libraryDir, { recursive: true }); + writeFileSync(path.join(sourceDir, "one.png"), "png"); + + const storage = createMemoryStorage(); + const publishEvent = vi.fn(); + + const routes = createOutputSourceRoutes({ + settingsStorage: storage, + readSettings: () => createDefaultEditableStudioSettings(), + readConfig: () => ({ libraryDir }) as ReturnType, + registerCatalogImage: () => { + throw new Error("registerCatalogImage should not be called in this test"); + }, + publishEvent, + }); + + const createResponse = await routes.request("/", { + method: "POST", + body: JSON.stringify({ + label: "external source", + path: sourceDir, + providerId: "comfy", + }), + headers: { "Content-Type": "application/json" }, + }); + + expect(createResponse.status).toBe(201); + const created = (await createResponse.json()) as { id: string }; + expect(created.id).toBeTruthy(); + + const filesResponse = await routes.request(`/${created.id}/files?limit=10`); + expect(filesResponse.status).toBe(200); + const filesPayload = (await filesResponse.json()) as { files: Array<{ fileName: string }> }; + expect(filesPayload.files).toEqual([expect.objectContaining({ fileName: "one.png" })]); + expect(publishEvent).toHaveBeenCalledWith( + "output-source.registered", + expect.objectContaining({ id: created.id }), + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("imports files and publishes imported event", async () => { + const root = mkdtempSync(path.join(os.tmpdir(), "output-source-routes-import-")); + const sourceDir = path.join(root, "source"); + const libraryDir = path.join(root, "library"); + + try { + mkdirSync(sourceDir, { recursive: true }); + mkdirSync(libraryDir, { recursive: true }); + writeFileSync(path.join(sourceDir, "hero.webp"), "webp"); + + const storage = createMemoryStorage(); + const publishEvent = vi.fn(); + const registerCatalogImage = vi.fn((input: any) => { + const image = { + id: "catalog-1", + libraryId: "library-1", + filePath: input.filePath, + thumbnailPath: null, + publicUrl: "/library/fake", + thumbnailUrl: null, + prompt: input.prompt ?? null, + negativePrompt: null, + aspectRatio: null, + imageSize: null, + width: null, + height: null, + mimeType: input.mimeType, + fileSizeBytes: input.fileSizeBytes ?? null, + jobId: null, + workspaceId: input.workspaceId ?? null, + batchId: null, + recipeId: null, + isFavorite: false, + isDeleted: false, + deletedAt: null, + tags: input.tags ?? [], + generationConfig: input.generationConfig ?? null, + createdAt: "2026-05-25T00:00:00.000Z", + } satisfies CatalogImage; + return image; + }); + + const routes = createOutputSourceRoutes({ + settingsStorage: storage, + readSettings: () => createDefaultEditableStudioSettings(), + readConfig: () => ({ libraryDir }) as ReturnType, + registerCatalogImage, + publishEvent, + }); + + const createResponse = await routes.request("/", { + method: "POST", + body: JSON.stringify({ + label: "external source", + path: sourceDir, + providerId: "comfy", + }), + headers: { "Content-Type": "application/json" }, + }); + + const created = (await createResponse.json()) as { id: string }; + + const importResponse = await routes.request(`/${created.id}/import`, { + method: "POST", + body: JSON.stringify({ files: ["hero.webp"], workspaceId: "workspace-1" }), + headers: { "Content-Type": "application/json" }, + }); + + expect(importResponse.status).toBe(201); + const payload = (await importResponse.json()) as { + imported: Array<{ sourceFile: string; catalogId: string }>; + }; + + expect(payload.imported).toEqual([ + expect.objectContaining({ sourceFile: "hero.webp", catalogId: "catalog-1" }), + ]); + expect(registerCatalogImage).toHaveBeenCalled(); + expect(publishEvent).toHaveBeenCalledWith( + "output-source.imported", + expect.objectContaining({ + imported: [expect.objectContaining({ sourceFile: "hero.webp", catalogId: "catalog-1" })], + }), + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); +}); diff --git a/apps/local-server/src/outputSourceRoutes.ts b/apps/local-server/src/outputSourceRoutes.ts new file mode 100644 index 00000000..4cf45352 --- /dev/null +++ b/apps/local-server/src/outputSourceRoutes.ts @@ -0,0 +1,85 @@ +import { Hono } from "hono"; +import { + detectExternalOutputSourceCandidates, + importExternalOutputSourceFiles, + listExternalOutputSourceFiles, + readExternalOutputSourceRegistry, + registerExternalOutputSource, +} from "./outputSources"; +import type { StudioSettingsStorage } from "./studioSettingsStore"; +import type { registerCatalogImage } from "./catalog"; +import type { publishEvent } from "./events"; +import type { getSettings } from "./config"; +import type { EditableStudioSettings } from "../../../packages/shared/src"; + +interface OutputSourceRoutesDependencies { + settingsStorage: StudioSettingsStorage; + readSettings: () => EditableStudioSettings; + readConfig: typeof getSettings; + registerCatalogImage: typeof registerCatalogImage; + publishEvent: typeof publishEvent; +} + +export function createOutputSourceRoutes({ + settingsStorage, + readSettings, + readConfig, + registerCatalogImage, + publishEvent, +}: OutputSourceRoutesDependencies) { + const routes = new Hono(); + + routes.get("/", (c) => { + const settings = readSettings(); + return c.json({ + registry: readExternalOutputSourceRegistry(settingsStorage), + candidates: detectExternalOutputSourceCandidates({ + libraryDir: readConfig().libraryDir, + settings, + }), + }); + }); + + routes.post("/", async (c) => { + const body = await c.req.json().catch(() => ({})); + const result = registerExternalOutputSource({ + storage: settingsStorage, + libraryDir: readConfig().libraryDir, + input: body, + }); + + if (!result.ok) { + return c.json({ error: result.reason }, 400); + } + + publishEvent("output-source.registered", result.source); + return c.json(result.source, 201); + }); + + routes.get("/:id/files", (c) => { + const url = new URL(c.req.url); + const result = listExternalOutputSourceFiles({ + storage: settingsStorage, + sourceId: c.req.param("id"), + limit: Number(url.searchParams.get("limit") || 100), + }); + if (!result.ok) return c.json({ error: result.reason }, 404); + return c.json({ source: result.source, files: result.files }); + }); + + routes.post("/:id/import", async (c) => { + const body = await c.req.json().catch(() => ({})); + const result = importExternalOutputSourceFiles({ + storage: settingsStorage, + sourceId: c.req.param("id"), + libraryDir: readConfig().libraryDir, + input: body, + registerCatalogImage, + }); + if (!result.ok) return c.json({ error: result.reason }, 400); + publishEvent("output-source.imported", result.result); + return c.json(result.result, 201); + }); + + return routes; +} diff --git a/apps/local-server/src/projectRoutes.test.ts b/apps/local-server/src/projectRoutes.test.ts new file mode 100644 index 00000000..2664a037 --- /dev/null +++ b/apps/local-server/src/projectRoutes.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it, vi } from "vite-plus/test"; +import type { Project } from "../../../packages/shared/src"; +import { createProjectRoutes } from "./projectRoutes"; + +function makeProject(overrides: Partial = {}): Project { + return { + id: overrides.id ?? "project-1", + name: overrides.name ?? "Default Project", + description: overrides.description ?? null, + createdAt: overrides.createdAt ?? "2026-05-29T00:00:00.000Z", + updatedAt: overrides.updatedAt ?? "2026-05-29T00:00:00.000Z", + }; +} + +describe("projectRoutes", () => { + it("lists projects and creates new projects through the seam", async () => { + const listed = [makeProject()]; + const created = makeProject({ id: "project-2", name: "New Project", description: "desc" }); + + const listProjects = vi.fn(() => listed); + const createProject = vi.fn(() => created); + const publishEvent = vi.fn(); + const logProjectCreated = vi.fn(); + + const routes = createProjectRoutes({ + listProjects, + createProject, + publishEvent, + logProjectCreated, + }); + + const listResponse = await routes.request("/"); + expect(listResponse.status).toBe(200); + await expect(listResponse.json()).resolves.toEqual(listed); + + const createResponse = await routes.request("/", { + method: "POST", + body: JSON.stringify({ name: "New Project", description: "desc" }), + headers: { "Content-Type": "application/json" }, + }); + + expect(createResponse.status).toBe(201); + await expect(createResponse.json()).resolves.toEqual(created); + expect(createProject).toHaveBeenCalledWith("New Project", "desc"); + expect(publishEvent).toHaveBeenCalledWith("project.created", created); + expect(logProjectCreated).toHaveBeenCalledWith("New Project"); + }); +}); \ No newline at end of file diff --git a/apps/local-server/src/projectRoutes.ts b/apps/local-server/src/projectRoutes.ts new file mode 100644 index 00000000..cd754967 --- /dev/null +++ b/apps/local-server/src/projectRoutes.ts @@ -0,0 +1,31 @@ +import { Hono } from "hono"; +import type { publishEvent } from "./events"; +import type { Project } from "../../../packages/shared/src"; + +interface ProjectRoutesDependencies { + listProjects: () => Project[]; + createProject: (name: string, description: string | null) => Project; + publishEvent: typeof publishEvent; + logProjectCreated: (projectName: string) => void; +} + +export function createProjectRoutes({ + listProjects, + createProject, + publishEvent, + logProjectCreated, +}: ProjectRoutesDependencies) { + const routes = new Hono(); + + routes.get("/", (c) => c.json(listProjects())); + + routes.post("/", async (c) => { + const body = await c.req.json().catch(() => ({})); + const project = createProject(body.name || "Untitled Project", body.description || null); + publishEvent("project.created", project); + logProjectCreated(project.name); + return c.json(project, 201); + }); + + return routes; +} \ No newline at end of file diff --git a/apps/local-server/src/providerRoutes.test.ts b/apps/local-server/src/providerRoutes.test.ts new file mode 100644 index 00000000..5666433a --- /dev/null +++ b/apps/local-server/src/providerRoutes.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vite-plus/test"; +import { createDefaultEditableStudioSettings } from "../../../packages/shared/src"; +import { createProviderRoutes } from "./providerRoutes"; + +describe("providerRoutes", () => { + it("returns provider capabilities from Studio Settings", async () => { + const routes = createProviderRoutes({ + readSettings: createDefaultEditableStudioSettings(), + }); + + const response = await routes.request("/"); + expect(response.status).toBe(200); + + const payload = (await response.json()) as { providers?: unknown[] }; + expect(Array.isArray(payload.providers)).toBe(true); + }); + + it("returns runtime preflight providers snapshot", async () => { + const routes = createProviderRoutes({ + readSettings: createDefaultEditableStudioSettings(), + }); + + const response = await routes.request("/preflight"); + expect(response.status).toBe(200); + + const payload = (await response.json()) as { providers?: unknown }; + expect(payload).toHaveProperty("providers"); + }); +}); diff --git a/apps/local-server/src/providerRoutes.ts b/apps/local-server/src/providerRoutes.ts new file mode 100644 index 00000000..47abe84e --- /dev/null +++ b/apps/local-server/src/providerRoutes.ts @@ -0,0 +1,18 @@ +import { Hono } from "hono"; +import { readExternalProviderRuntimePreflights } from "./providers/runtimeConfig"; +import { readProviderCapabilities } from "./providerCapabilities"; +import type { readEditableStudioSettings } from "./studioSettingsStore"; + +interface ProviderRoutesDependencies { + readSettings: ReturnType; +} + +export function createProviderRoutes({ readSettings }: ProviderRoutesDependencies) { + const routes = new Hono(); + + routes.get("/", (c) => c.json(readProviderCapabilities(readSettings))); + + routes.get("/preflight", (c) => c.json({ providers: readExternalProviderRuntimePreflights() })); + + return routes; +} diff --git a/apps/local-server/src/providers/codexProvider.test.ts b/apps/local-server/src/providers/codexProvider.test.ts index be4327c2..caca2a4c 100644 --- a/apps/local-server/src/providers/codexProvider.test.ts +++ b/apps/local-server/src/providers/codexProvider.test.ts @@ -242,6 +242,29 @@ describe('codexProvider', () => { ]); }); + it('adds a variation brief when the source spec requests fresher output diversity', () => { + const sourceSpec = createGenerationTaskSpec({ + id: 'spec-var', + task: 'image_generate', + providerId: 'codex', + prompt: 'retro sci-fi heroine portrait', + metadata: { + variationBrief: 'Treat this as a fresh interpretation of the brief.', + }, + }); + + const compiled = compileCodexImagegenInput({ + id: 'job-var', + projectId: 'project-1', + prompt: 'fallback', + execution: null, + sourceSpec, + }); + + expect(compiled.payload.text).toContain('Variation brief:'); + expect(compiled.payload.text).toContain('fresh interpretation'); + }); + it('delegates execution to the Codex Product Runtime with compiled input text', async () => { const calls: TurnParams[] = []; const turn: CodexTurn = { diff --git a/apps/local-server/src/providers/codexProvider.ts b/apps/local-server/src/providers/codexProvider.ts index 923b5ee2..754205a0 100644 --- a/apps/local-server/src/providers/codexProvider.ts +++ b/apps/local-server/src/providers/codexProvider.ts @@ -111,6 +111,10 @@ function buildCodexPromptText(sourceSpec: GenerationTaskSpec) { const parts = [`Task: ${sourceSpec.task}`, '', 'Prompt:', sourceSpec.prompt]; const recipeProviderDirectives = sourceSpec.metadata.recipeProviderDirectives; const recipeContext = sourceSpec.metadata.recipeContext; + const variationBrief = + typeof sourceSpec.metadata.variationBrief === 'string' + ? sourceSpec.metadata.variationBrief.trim() + : ''; const assetLines = buildCodexAssetLines(sourceSpec); if (isRecipeProviderDirectives(recipeProviderDirectives)) { @@ -123,6 +127,10 @@ function buildCodexPromptText(sourceSpec: GenerationTaskSpec) { parts.push('', 'Recipe instructions:', recipeContext.trim()); } + if (variationBrief) { + parts.push('', 'Variation brief:', variationBrief); + } + if (sourceSpec.negativePrompt) { parts.push('', 'Avoid:', sourceSpec.negativePrompt); } diff --git a/apps/local-server/src/providers/externalProviderInputs.test.ts b/apps/local-server/src/providers/externalProviderInputs.test.ts index 4196664b..da0d6026 100644 --- a/apps/local-server/src/providers/externalProviderInputs.test.ts +++ b/apps/local-server/src/providers/externalProviderInputs.test.ts @@ -145,6 +145,30 @@ describe('external provider input compilers', () => { expect(compiled.audit.estimatedPromptChars).toBe(compiled.payload.prompt.length); }); + it('adds a variation brief to hosted API prompts when requested in metadata', () => { + const sourceSpec = createGenerationTaskSpec({ + id: 'spec-google-var', + task: 'image_generate', + providerId: 'google', + prompt: 'moody cyberpunk alley', + metadata: { + variationBrief: 'Make this noticeably different from earlier attempts.', + }, + }); + + const compiled = compileGoogleImageApiInput({ + id: 'job-google-var', + projectId: 'project-1', + providerId: 'google', + prompt: 'fallback', + execution: null, + sourceSpec, + }); + + expect(compiled.payload.prompt).toContain('Variation brief:'); + expect(compiled.payload.prompt).toContain('noticeably different'); + }); + it('compiles Comfy local workflow input for adapter conformance fixtures', () => { const sourceSpec = createGenerationTaskSpec({ id: 'spec-comfy-1', diff --git a/apps/local-server/src/providers/externalProviderInputs.ts b/apps/local-server/src/providers/externalProviderInputs.ts index 64e30c71..f1a49214 100644 --- a/apps/local-server/src/providers/externalProviderInputs.ts +++ b/apps/local-server/src/providers/externalProviderInputs.ts @@ -212,16 +212,31 @@ function createProviderPayloadMetadata(sourceSpec: GenerationTaskSpec) { function buildProviderPrompt(sourceSpec: GenerationTaskSpec) { const recipeProviderDirectives = sourceSpec.metadata.recipeProviderDirectives; + const variationBrief = + typeof sourceSpec.metadata.variationBrief === 'string' + ? sourceSpec.metadata.variationBrief.trim() + : ''; + const sections = [sourceSpec.prompt]; + if (!isRecipeProviderDirectives(recipeProviderDirectives)) { - return sourceSpec.prompt; + if (variationBrief) { + sections.push('', 'Variation brief:', variationBrief); + } + + return sections.join('\n'); } - return [ - sourceSpec.prompt, + sections.push( '', 'Recipe directives:', serializeRecipeProviderDirectives(recipeProviderDirectives), - ].join('\n'); + ); + + if (variationBrief) { + sections.push('', 'Variation brief:', variationBrief); + } + + return sections.join('\n'); } function estimatePromptChars(sourceSpec: GenerationTaskSpec, prompt: string) { diff --git a/apps/local-server/src/referenceManager.test.ts b/apps/local-server/src/referenceManager.test.ts index cde4c7ba..766b4d4d 100644 --- a/apps/local-server/src/referenceManager.test.ts +++ b/apps/local-server/src/referenceManager.test.ts @@ -4,7 +4,7 @@ import { createGenerationTaskSpec } from '../../../packages/shared/src'; import { hydrateSourceSpecAssetPaths } from './referenceManager'; describe('referenceManager', () => { - it('hydrates inline source spec assets with persisted local paths in request order', () => { + it('hydrates every inline task asset with its persisted local path', () => { const sourceSpec = createGenerationTaskSpec({ id: 'spec-1', task: 'image_edit', @@ -62,24 +62,73 @@ describe('referenceManager', () => { { role: 'input', name: 'input-image.png', - dataUrl: 'data:image/png;base64,AAA', + dataUrl: undefined, localPath: 'D:/AI-Studio-Library/references/job-1/input-image.png', strength: 1, }, { role: 'mask', name: 'input-mask.png', - dataUrl: 'data:image/png;base64,BBB', + dataUrl: undefined, localPath: 'D:/AI-Studio-Library/references/job-1/input-mask.png', strength: 1, }, { role: 'reference', name: 'moodboard.png', - dataUrl: 'data:image/png;base64,CCC', + dataUrl: undefined, localPath: 'D:/AI-Studio-Library/references/job-1/moodboard.png', strength: 0.4, }, ]); }); + + it('still hydrates references when non-reference assets with inline data appear first', () => { + const sourceSpec = createGenerationTaskSpec({ + id: 'spec-2', + task: 'image_edit', + providerId: 'codex', + prompt: 'Stylize this image with a reference.', + assets: [ + { + role: 'input', + name: 'base.png', + dataUrl: 'data:image/png;base64,AAA', + strength: 1, + }, + { + role: 'mask', + name: 'mask.png', + dataUrl: 'data:image/png;base64,BBB', + strength: 1, + }, + { + role: 'reference', + name: 'moodboard.png', + dataUrl: 'data:image/png;base64,CCC', + strength: 0.65, + }, + ], + }); + + const hydrated = hydrateSourceSpecAssetPaths( + sourceSpec, + [{ name: 'moodboard.png', dataUrl: 'data:image/png;base64,CCC', strength: 0.65 }], + [ + { + name: 'moodboard.png', + path: 'D:/AI-Studio-Library/references/job-2/moodboard.png', + strength: 0.65, + }, + ], + ); + + expect(hydrated?.assets.at(2)).toEqual( + expect.objectContaining({ + role: 'reference', + dataUrl: undefined, + localPath: 'D:/AI-Studio-Library/references/job-2/moodboard.png', + }), + ); + }); }); diff --git a/apps/local-server/src/referenceManager.ts b/apps/local-server/src/referenceManager.ts index 9773df86..a3a6162d 100644 --- a/apps/local-server/src/referenceManager.ts +++ b/apps/local-server/src/referenceManager.ts @@ -125,25 +125,26 @@ export function hydrateSourceSpecAssetPaths( return sourceSpec ?? null; } - let referenceIndex = 0; + const usedReferenceIndexes = new Set(); const hydratedAssets = sourceSpec.assets.map((asset) => { if (!asset.dataUrl) { return asset; } - while (referenceIndex < references.length) { - const rawReference = references[referenceIndex]; - const persistedRef = persistedRefs[referenceIndex] ?? null; - referenceIndex += 1; + for (const [index, rawReference] of references.entries()) { + if (usedReferenceIndexes.has(index)) continue; + const persistedRef = persistedRefs[index] ?? null; const nameMatches = !asset.name || !rawReference?.name || asset.name === rawReference.name; const strengthMatches = asset.strength == null || rawReference?.strength == null || Math.abs(asset.strength - rawReference.strength) < 0.000_001; + const dataUrlMatches = + !asset.dataUrl || !rawReference?.dataUrl || asset.dataUrl === rawReference.dataUrl; - if (!nameMatches || !strengthMatches) { + if (!nameMatches || !strengthMatches || !dataUrlMatches) { continue; } @@ -151,8 +152,10 @@ export function hydrateSourceSpecAssetPaths( return asset; } + usedReferenceIndexes.add(index); return { ...asset, + dataUrl: undefined, localPath: persistedRef.path, strength: asset.strength ?? rawReference.strength ?? persistedRef.strength, }; diff --git a/apps/local-server/src/reset.ts b/apps/local-server/src/reset.ts index 287bb56b..88a364c7 100644 --- a/apps/local-server/src/reset.ts +++ b/apps/local-server/src/reset.ts @@ -1,6 +1,6 @@ import { existsSync, rmSync } from 'node:fs'; import { getSettings } from './config'; -import { stopAppServer } from './codex'; +import { stopAppServer } from './codex/processSupervisor'; import { closeDb, ensureDefaultProject, migrateDb } from './db'; import { LIBRARY_FOLDERS, ensureLibrary, resolveLibraryPath } from './library'; import { ensureDefaultLibrary } from './libraries'; diff --git a/apps/local-server/src/runtimeRoutes.test.ts b/apps/local-server/src/runtimeRoutes.test.ts new file mode 100644 index 00000000..d2bd38fe --- /dev/null +++ b/apps/local-server/src/runtimeRoutes.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it, vi } from "vite-plus/test"; +import { createRuntimeRoutes } from "./runtimeRoutes"; + +describe("runtimeRoutes", () => { + it("returns health snapshot and bootstrap config", async () => { + const ensureAppServer = vi.fn(); + const routes = createRuntimeRoutes({ + readSettings: () => ({ + libraryDir: "D:/library", + serverPort: 17223, + codexWsPort: 17224, + codexImagegenModel: "gpt-image-1", + codexImagegenReasoningEffort: "medium", + codexImagegenServiceTier: null, + codexMaxConcurrentJobs: 1, + }), + inspectLibrary: () => ({ + exists: true, + writable: true, + readmePresent: true, + missingFolders: [], + }), + resolveCodexInvocation: () => ["node", "-e", "process.stdout.write('codex-test')"], + getCodexWsUrl: () => "ws://127.0.0.1:17224", + getEnvLocalPath: () => "D:/repo/.env.local", + hasEnvLocalFile: () => true, + ensureAppServer, + readAppServerDiagnostics: () => ({ + pid: 123, + lastExitCode: null, + lastExitAt: null, + lastInvocation: ["codex", "app-server"], + lastStartAt: null, + lastStartError: null, + lastEnsureAt: null, + lastEnsureReason: null, + }), + isAppServerRunning: () => true, + readWorkerStatus: () => ({ currentJobId: null, queueLength: 0, status: "idle" }), + }); + + const healthResponse = await routes.request("/health"); + expect(healthResponse.status).toBe(200); + const healthPayload = (await healthResponse.json()) as { + ok: boolean; + checks: { onboardingReady: boolean }; + appServer: { running: boolean }; + }; + expect(healthPayload.ok).toBe(true); + expect(healthPayload.checks.onboardingReady).toBe(true); + expect(healthPayload.appServer.running).toBe(true); + + const bootstrapResponse = await routes.request("/bootstrap-config"); + expect(bootstrapResponse.status).toBe(200); + await expect(bootstrapResponse.json()).resolves.toEqual( + expect.objectContaining({ libraryDir: "D:/library", serverPort: 17223 }), + ); + }); + + it("starts app-server and returns diagnostics", async () => { + const ensureAppServer = vi.fn(); + const routes = createRuntimeRoutes({ + readSettings: () => ({ + libraryDir: "D:/library", + serverPort: 17223, + codexWsPort: 17224, + codexImagegenModel: "gpt-image-1", + codexImagegenReasoningEffort: "medium", + codexImagegenServiceTier: null, + codexMaxConcurrentJobs: 1, + }), + inspectLibrary: () => ({ + exists: true, + writable: true, + readmePresent: true, + missingFolders: [], + }), + resolveCodexInvocation: () => ["node", "-e", "process.stdout.write('codex-test')"], + getCodexWsUrl: () => "ws://127.0.0.1:17224", + getEnvLocalPath: () => "D:/repo/.env.local", + hasEnvLocalFile: () => true, + ensureAppServer, + readAppServerDiagnostics: () => ({ + pid: 456, + lastExitCode: null, + lastExitAt: null, + lastInvocation: ["codex", "app-server"], + lastStartAt: null, + lastStartError: null, + lastEnsureAt: null, + lastEnsureReason: "user", + }), + isAppServerRunning: () => true, + readWorkerStatus: () => ({ currentJobId: null, queueLength: 0, status: "idle" }), + }); + + const response = await routes.request("/app-server/start", { method: "POST" }); + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual( + expect.objectContaining({ + running: true, + wsUrl: "ws://127.0.0.1:17224", + pid: 456, + }), + ); + expect(ensureAppServer).toHaveBeenCalledWith("user"); + }); +}); \ No newline at end of file diff --git a/apps/local-server/src/runtimeRoutes.ts b/apps/local-server/src/runtimeRoutes.ts new file mode 100644 index 00000000..b40d9ed6 --- /dev/null +++ b/apps/local-server/src/runtimeRoutes.ts @@ -0,0 +1,115 @@ +import { spawnSync } from "node:child_process"; +import { Hono } from "hono"; +import type { AppServerEnsureReason } from "../../../packages/shared/src"; +import type { getSettings } from "./config"; +import type { resolveCodexInvocation } from "./codexExecutable"; +import type { getAppServerDiagnostics } from "./codex/processSupervisor"; +import type { inspectLibrary } from "./library"; +import type { WorkerStatus } from "./worker"; + +interface RuntimeRoutesDependencies { + readSettings: () => ReturnType; + inspectLibrary: () => ReturnType; + resolveCodexInvocation: typeof resolveCodexInvocation; + getCodexWsUrl: () => string; + getEnvLocalPath: () => string; + hasEnvLocalFile: () => boolean; + ensureAppServer: (reason?: AppServerEnsureReason) => void; + readAppServerDiagnostics: typeof getAppServerDiagnostics; + isAppServerRunning: () => boolean; + readWorkerStatus: () => WorkerStatus; +} + +export function createRuntimeRoutes({ + readSettings, + inspectLibrary, + resolveCodexInvocation, + getCodexWsUrl, + getEnvLocalPath, + hasEnvLocalFile, + ensureAppServer, + readAppServerDiagnostics, + isAppServerRunning, + readWorkerStatus, +}: RuntimeRoutesDependencies) { + const routes = new Hono(); + + const bunVersion = + typeof globalThis === "object" && "Bun" in globalThis + ? ((globalThis as { Bun?: { version?: string } }).Bun?.version ?? null) + : null; + + routes.get("/health", (c) => { + const settings = readSettings(); + const library = inspectLibrary(); + const [command, ...args] = resolveCodexInvocation(["--version"]); + const codex = spawnSync(command, args, { encoding: "utf8" }); + const codexAvailable = codex.status === 0; + const appServerDiagnostics = readAppServerDiagnostics(); + const libraryReady = library.exists && library.writable && library.missingFolders.length === 0; + const appServerRunning = isAppServerRunning(); + + return c.json({ + ok: true, + checkedAt: new Date().toISOString(), + libraryDir: settings.libraryDir, + runtime: { + platform: process.platform, + arch: process.arch, + bunVersion, + nodeVersion: process.versions.node, + cwd: process.cwd(), + envLocalPath: getEnvLocalPath(), + envLocalPresent: hasEnvLocalFile(), + }, + config: { + serverPort: settings.serverPort, + codexWsPort: settings.codexWsPort, + }, + library: { + exists: library.exists, + writable: library.writable, + readmePresent: library.readmePresent, + missingFolders: library.missingFolders, + }, + codexCli: { + available: codexAvailable, + version: codexAvailable ? codex.stdout.trim() : null, + command: [command, ...args].join(" "), + }, + appServer: { + running: appServerRunning, + wsUrl: getCodexWsUrl(), + pid: appServerDiagnostics.pid, + lastExitCode: appServerDiagnostics.lastExitCode, + lastExitAt: appServerDiagnostics.lastExitAt, + lastInvocation: appServerDiagnostics.lastInvocation?.join(" ") ?? null, + lastStartAt: appServerDiagnostics.lastStartAt, + lastStartError: appServerDiagnostics.lastStartError, + lastEnsureAt: appServerDiagnostics.lastEnsureAt, + lastEnsureReason: appServerDiagnostics.lastEnsureReason, + }, + checks: { + libraryReady, + codexReady: codexAvailable, + onboardingReady: libraryReady && codexAvailable && appServerRunning, + }, + worker: readWorkerStatus(), + }); + }); + + routes.post("/app-server/start", (c) => { + ensureAppServer("user"); + const diagnostics = readAppServerDiagnostics(); + return c.json({ + running: isAppServerRunning(), + wsUrl: getCodexWsUrl(), + pid: diagnostics.pid, + lastStartError: diagnostics.lastStartError, + }); + }); + + routes.get("/bootstrap-config", (c) => c.json(readSettings())); + + return routes; +} \ No newline at end of file diff --git a/apps/local-server/src/settingsRoutes.test.ts b/apps/local-server/src/settingsRoutes.test.ts new file mode 100644 index 00000000..939d5da6 --- /dev/null +++ b/apps/local-server/src/settingsRoutes.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vite-plus/test"; +import { createDefaultEditableStudioSettings } from "../../../packages/shared/src"; +import { createSettingsRoutes } from "./settingsRoutes"; +import { + readEditableStudioSettings, + updateEditableStudioSettings, + type StudioSettingsStorage, +} from "./studioSettingsStore"; + +function createMemoryStorage(initial?: Record): StudioSettingsStorage { + const values = new Map(Object.entries(initial ?? {})); + return { + getSetting(key) { + return values.get(key) ?? null; + }, + setSetting(key, value) { + values.set(key, value); + }, + }; +} + +describe("settingsRoutes", () => { + it("returns editable Studio Settings through the route seam", async () => { + const storage = createMemoryStorage(); + const routes = createSettingsRoutes({ + readSettings: () => readEditableStudioSettings(storage), + updateSettings: (patch) => updateEditableStudioSettings(storage, patch), + }); + + const response = await routes.request("/"); + expect(response.status).toBe(200); + + const payload = (await response.json()) as ReturnType< + typeof createDefaultEditableStudioSettings + >; + expect(payload.defaultProviderId).toBe("codex"); + expect(payload.defaultOutputMode).toBe("studio_library"); + }); + + it("updates editable settings and keeps the new value for subsequent reads", async () => { + const storage = createMemoryStorage(); + const routes = createSettingsRoutes({ + readSettings: () => readEditableStudioSettings(storage), + updateSettings: (patch) => updateEditableStudioSettings(storage, patch), + }); + + const patchResponse = await routes.request("/", { + method: "PATCH", + body: JSON.stringify({ commandCenterCompactMode: true }), + headers: { "Content-Type": "application/json" }, + }); + + expect(patchResponse.status).toBe(200); + const patched = (await patchResponse.json()) as { commandCenterCompactMode: boolean }; + expect(patched.commandCenterCompactMode).toBe(true); + + const readBack = await routes.request("/"); + const readBackPayload = (await readBack.json()) as { commandCenterCompactMode: boolean }; + expect(readBackPayload.commandCenterCompactMode).toBe(true); + }); +}); \ No newline at end of file diff --git a/apps/local-server/src/settingsRoutes.ts b/apps/local-server/src/settingsRoutes.ts new file mode 100644 index 00000000..eb9f0da1 --- /dev/null +++ b/apps/local-server/src/settingsRoutes.ts @@ -0,0 +1,23 @@ +import { Hono } from "hono"; +import type { EditableStudioSettings } from "../../../packages/shared/src"; + +interface SettingsRoutesDependencies { + readSettings: () => EditableStudioSettings; + updateSettings: (patch: unknown) => EditableStudioSettings; +} + +export function createSettingsRoutes({ + readSettings, + updateSettings, +}: SettingsRoutesDependencies) { + const routes = new Hono(); + + routes.get("/", (c) => c.json(readSettings())); + + routes.patch("/", async (c) => { + const body = await c.req.json().catch(() => ({})); + return c.json(updateSettings(body)); + }); + + return routes; +} \ No newline at end of file diff --git a/apps/local-server/src/studioControlRoutes.test.ts b/apps/local-server/src/studioControlRoutes.test.ts new file mode 100644 index 00000000..fd9acbc9 --- /dev/null +++ b/apps/local-server/src/studioControlRoutes.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it, vi } from "vite-plus/test"; +import { createStudioControlRoutes } from "./studioControlRoutes"; + +describe("studioControlRoutes", () => { + it("delegates reset to resetStudioData with the provided worker seam", async () => { + const worker = { + resetWorkerState: vi.fn(async () => {}), + }; + const resetStudioData = vi.fn(async () => ({ + ok: true, + resetAt: "2026-05-29T00:00:00.000Z", + libraryDir: "D:/library", + defaultProjectId: "project-1", + })); + + const routes = createStudioControlRoutes({ resetStudioData, worker }); + + const response = await routes.request("/reset", { method: "POST" }); + expect(response.status).toBe(200); + await expect(response.json()).resolves.toEqual( + expect.objectContaining({ ok: true, defaultProjectId: "project-1" }), + ); + expect(resetStudioData).toHaveBeenCalledWith(worker); + }); +}); \ No newline at end of file diff --git a/apps/local-server/src/studioControlRoutes.ts b/apps/local-server/src/studioControlRoutes.ts new file mode 100644 index 00000000..ba3252a0 --- /dev/null +++ b/apps/local-server/src/studioControlRoutes.ts @@ -0,0 +1,20 @@ +import { Hono } from "hono"; +import type { resetStudioData } from "./reset"; + +interface StudioControlRoutesDependencies { + resetStudioData: typeof resetStudioData; + worker: { resetWorkerState(): Promise }; +} + +export function createStudioControlRoutes({ + resetStudioData, + worker, +}: StudioControlRoutesDependencies) { + const routes = new Hono(); + + routes.post("/reset", async (c) => { + return c.json(await resetStudioData(worker)); + }); + + return routes; +} \ No newline at end of file diff --git a/apps/local-server/src/worker.ts b/apps/local-server/src/worker.ts index b7d1859b..52b68fad 100644 --- a/apps/local-server/src/worker.ts +++ b/apps/local-server/src/worker.ts @@ -1,7 +1,7 @@ -import { existsSync, mkdirSync, renameSync, statSync, writeFileSync } from 'node:fs'; -import path from 'node:path'; -import { getSettings } from './config'; -import { registerCatalogImage } from './catalog'; +import { mkdirSync, statSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { getSettings } from "./config"; +import { registerCatalogImage } from "./catalog"; import { addAsset, addJobEvent, @@ -10,22 +10,24 @@ import { setSettingValue, updateJobStatus, upsertCodexTurn, -} from './db'; -import { publishEvent } from './events'; -import { resolveLibraryPath, toPublicAssetUrl } from './library'; -import { log } from './logger'; -import { createCodexTurn, resolveJobExecutionOptions } from './codex'; -import { createCodexGenerationProvider } from './providers/codexProvider'; -import { createExternalGenerationProvider } from './providers/externalProvider'; -import type { GenerationProvider } from './providers/types'; -import { embedMetadata } from './metadataEmbedder'; -import { parsePromptTransport } from '../../../packages/shared/src/promptTransport'; -import type { Job } from '../../../packages/shared/src/types'; -import type { CodexTurn } from './codex'; -import { buildOutputAssetRelativePath } from './outputOrganization'; -import { readEditableStudioSettings } from './studioSettingsStore'; -import { resolveJobCatalogContext } from './workerCatalogContext'; -import { resolveWorkerRuntimeTarget } from './workerRouting'; +} from "./db"; +import { publishEvent } from "./events"; +import { resolveLibraryPath, toPublicAssetUrl } from "./library"; +import { log } from "./logger"; +import { createCodexTurn } from "./codex/turn"; +import type { CodexTurn } from "./codex/turn"; +import { resolveJobExecutionOptions } from "./codex/executionOptions"; +import { createCodexGenerationProvider } from "./providers/codexProvider"; +import { createExternalGenerationProvider } from "./providers/externalProvider"; +import type { GenerationProvider } from "./providers/types"; +import { embedMetadata } from "./metadataEmbedder"; +import { parsePromptTransport } from "../../../packages/shared/src/promptTransport"; +import type { Job } from "../../../packages/shared/src/types"; +import { readEditableStudioSettings } from "./studioSettingsStore"; +import { resolveJobCatalogContext } from "./workerCatalogContext"; +import { resolveWorkerRuntimeTarget } from "./workerRouting"; +import { createWorkerAssetPathing, inferGeneratedAssetMimeType } from "./workerAssetPathing"; +import { createWorkerAssetFinalizer } from "./workerAssetFinalizer"; export interface WorkerStatus { maxConcurrentJobs: number; @@ -59,16 +61,19 @@ export interface CreateWorkerControllerDependencies { parsePromptTransport?: typeof parsePromptTransport; createGenerationProvider?: () => GenerationProvider; createExternalProvider?: () => GenerationProvider; + readEditableStudioSettings?: typeof readEditableStudioSettings; + resolveJobCatalogContext?: typeof resolveJobCatalogContext; + resolveWorkerRuntimeTarget?: typeof resolveWorkerRuntimeTarget; } function createAbortError() { - const error = new Error('Operation cancelled by user'); - error.name = 'AbortError'; + const error = new Error("Operation cancelled by user"); + error.name = "AbortError"; return error; } function isAbortError(error: unknown) { - return error instanceof Error && error.name === 'AbortError'; + return error instanceof Error && error.name === "AbortError"; } function throwIfAborted(signal?: AbortSignal) { @@ -83,25 +88,25 @@ function waitWithAbort(durationMs: number, signal?: AbortSignal) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { - signal.removeEventListener('abort', handleAbort); + signal.removeEventListener("abort", handleAbort); resolve(); }, durationMs); const handleAbort = () => { clearTimeout(timeout); - signal.removeEventListener('abort', handleAbort); + signal.removeEventListener("abort", handleAbort); reject(createAbortError()); }; - signal.addEventListener('abort', handleAbort, { once: true }); + signal.addEventListener("abort", handleAbort, { once: true }); }); } function svgForPrompt(prompt: string) { const safePrompt = prompt - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>') + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") .slice(0, 180); return ` @@ -117,16 +122,6 @@ function svgForPrompt(prompt: string) { `; } -function resolveUniquePath(filePath: string) { - if (!existsSync(filePath)) return filePath; - const parsed = path.parse(filePath); - for (let index = 2; index < 1000; index += 1) { - const candidate = path.join(parsed.dir, `${parsed.name}-${index}${parsed.ext}`); - if (!existsSync(candidate)) return candidate; - } - return path.join(parsed.dir, `${parsed.name}-${Date.now()}${parsed.ext}`); -} - export function createWorkerController({ createTurn = createCodexTurn, getSettings: getSettingsFn = getSettings, @@ -145,6 +140,9 @@ export function createWorkerController({ parsePromptTransport: parsePromptTransportFn = parsePromptTransport, createGenerationProvider, createExternalProvider, + readEditableStudioSettings: readEditableStudioSettingsFn = readEditableStudioSettings, + resolveJobCatalogContext: resolveJobCatalogContextFn = resolveJobCatalogContext, + resolveWorkerRuntimeTarget: resolveWorkerRuntimeTargetFn = resolveWorkerRuntimeTarget, }: CreateWorkerControllerDependencies = {}): WorkerController { const runningJobs = new Set(); const jobQueue: Job[] = []; @@ -155,40 +153,18 @@ export function createWorkerController({ createGenerationProvider?.() ?? createCodexGenerationProvider({ turn: createTurn() }); const externalGenerationProvider = createExternalProvider?.() ?? createExternalGenerationProvider(); + const assetPathing = createWorkerAssetPathing({ + resolveExecutionOptions, + readEditableStudioSettings: readEditableStudioSettingsFn, + getSetting: getSettingValue, + setSetting: setSettingValue, + resolveLibraryPath: resolveLibraryPathFn, + }); function getMaxConcurrentJobs() { return getSettingsFn().codexMaxConcurrentJobs; } - function resolveGeneratedAssetTargetPath(job: Job, providerId: string | null, extension: string) { - const executionOptions = resolveExecutionOptions(job.execution); - const settings = readEditableStudioSettings({ - getSetting: getSettingValue, - setSetting: setSettingValue, - }); - const relativePath = buildOutputAssetRelativePath(settings, { - jobId: job.id, - providerId, - model: executionOptions.model, - recipeId: job.sourceSpec?.recipeId ?? null, - extension, - }); - return resolveUniquePath(resolveLibraryPathFn(...relativePath.split(/[\\/]/))); - } - - function organizeGeneratedAssetPath(job: Job, filePath: string, providerId: string | null) { - const ext = path.extname(filePath).toLowerCase() || '.png'; - const targetPath = resolveGeneratedAssetTargetPath(job, providerId, ext); - - if (path.resolve(filePath) === path.resolve(targetPath)) return filePath; - mkdirSync(path.dirname(targetPath), { recursive: true }); - if (existsSync(filePath)) { - renameSync(filePath, targetPath); - return targetPath; - } - return filePath; - } - function buildCatalogGenerationConfig(prompt: string) { const parsedPrompt = parsePromptTransportFn(prompt); const executionOptions = resolveExecutionOptions(); @@ -203,10 +179,10 @@ export function createWorkerController({ imageSize: parsedPrompt.imageSize, negativePrompt: parsedPrompt.negativePrompt, temperature: 0.8, - model: 'codex-imagegen', + model: "codex-imagegen", executionModel: executionOptions.model, executionReasoningEffort: executionOptions.reasoningEffort, - executionSpeed: executionOptions.serviceTier ?? 'standard', + executionSpeed: executionOptions.serviceTier ?? "standard", batchCount: 1, useThinkingAndSearch: false, }; @@ -216,7 +192,7 @@ export function createWorkerController({ if (job.sourceSpec) { const executionOptions = resolveExecutionOptions(job.execution); const recipeContext = - typeof job.sourceSpec.metadata.recipeContext === 'string' + typeof job.sourceSpec.metadata.recipeContext === "string" ? job.sourceSpec.metadata.recipeContext : null; @@ -230,10 +206,10 @@ export function createWorkerController({ imageSize: job.sourceSpec.output.imageSize, negativePrompt: job.sourceSpec.negativePrompt, temperature: 0.8, - model: 'codex-imagegen', + model: "codex-imagegen", executionModel: executionOptions.model, executionReasoningEffort: executionOptions.reasoningEffort, - executionSpeed: executionOptions.serviceTier ?? 'standard', + executionSpeed: executionOptions.serviceTier ?? "standard", batchCount: job.sourceSpec.output.count, useThinkingAndSearch: false, }; @@ -252,120 +228,42 @@ export function createWorkerController({ imageSize: parsedPrompt.imageSize, negativePrompt: parsedPrompt.negativePrompt, temperature: 0.8, - model: 'codex-imagegen', + model: "codex-imagegen", executionModel: executionOptions.model, executionReasoningEffort: executionOptions.reasoningEffort, - executionSpeed: executionOptions.serviceTier ?? 'standard', + executionSpeed: executionOptions.serviceTier ?? "standard", batchCount: 1, useThinkingAndSearch: false, }; } - async function finalizeJobAsset( - job: Job, - catalogContext: ReturnType, - discoveredImagePath: string, - providerId: string, - options: { - logPrefix: string; - embedMetadata?: boolean; - executionOptions?: ReturnType; - }, - ) { - const ext = path.extname(discoveredImagePath).toLowerCase(); - const mimeType = - ext === '.jpg' || ext === '.jpeg' - ? 'image/jpeg' - : ext === '.webp' - ? 'image/webp' - : 'image/png'; - const organizedImagePath = organizeGeneratedAssetPath(job, discoveredImagePath, providerId); - const asset = addAssetFn({ - projectId: job.projectId, - jobId: job.id, - filePath: organizedImagePath, - thumbnailPath: null, - publicUrl: toPublicAssetUrlFn(discoveredImagePath), - prompt: job.finalPromptUsed, - width: null, - height: null, - mimeType, - }); - const parsedPrompt = job.sourceSpec - ? { - prompt: job.sourceSpec.prompt, - negativePrompt: job.sourceSpec.negativePrompt, - aspectRatio: job.sourceSpec.output.aspectRatio, - imageSize: job.sourceSpec.output.imageSize, - recipeId: job.sourceSpec.recipeId, - } - : parsePromptTransportFn(job.finalPromptUsed); - const catalogImage = registerCatalogImageFn({ - filePath: asset.filePath, - thumbnailPath: asset.thumbnailPath, - prompt: asset.prompt, - negativePrompt: parsedPrompt.negativePrompt || null, - aspectRatio: parsedPrompt.aspectRatio, - imageSize: parsedPrompt.imageSize, - width: asset.width, - height: asset.height, - mimeType: asset.mimeType, - fileSizeBytes: statSync(asset.filePath).size, - jobId: asset.jobId, - workspaceId: catalogContext.workspaceId, - batchId: catalogContext.batchId, - recipeId: parsedPrompt.recipeId, - generationConfig: buildCatalogGenerationConfigFromJob(job), - }); - - if (options.embedMetadata && options.executionOptions) { - void embedMetadataFn(asset.filePath, { - prompt: job.finalPromptUsed, - negativePrompt: parsedPrompt.negativePrompt || null, - aspectRatio: parsedPrompt.aspectRatio, - imageSize: parsedPrompt.imageSize, - model: options.executionOptions.model, - recipe: parsedPrompt.recipeId, - batchId: catalogContext.batchId ?? job.id, - generatedAt: new Date().toISOString(), - studioVersion: '0.0.0', - libraryId: catalogImage.libraryId, - catalogId: catalogImage.id, - }).catch((error) => { - logger( - 'warn', - 'metadata', - `Metadata embed failed: ${error instanceof Error ? error.message : String(error)}`, - job.id, - ); - }); - } - - addJobEventFn(job.id, 'asset.created', `${options.logPrefix} asset imported.`, { - assetId: asset.id, - }); - publishEventFn('asset.created', asset); - publishEventFn('catalog.created', catalogImage); - updateJobStatusFn(job.id, 'completed'); - publishEventFn('job.completed', getJobFn(job.id)); - logger( - 'info', - 'worker', - `${options.logPrefix} job completed. Asset: ${path.basename(asset.filePath)}`, - job.id, - ); - } + const assetFinalizer = createWorkerAssetFinalizer({ + registerCatalogImage: registerCatalogImageFn, + addAsset: addAssetFn, + addJobEvent: addJobEventFn, + updateJobStatus: updateJobStatusFn, + publishEvent: publishEventFn, + getJob: getJobFn, + toPublicAssetUrl: toPublicAssetUrlFn, + logger, + embedMetadata: embedMetadataFn, + parsePromptTransport: parsePromptTransportFn, + resolveExecutionOptions, + resolveCatalogGenerationConfig: buildCatalogGenerationConfigFromJob, + organizeGeneratedAssetPath: assetPathing.organizeGeneratedAssetPath, + inferGeneratedAssetMimeType, + }); async function runDryJob(job: Job, signal?: AbortSignal) { const startedAt = Date.now(); - addJobEventFn(job.id, 'dry_run.started', 'Dry run asset creation started.'); - logger('info', 'worker', 'Dry run job started.', job.id); + addJobEventFn(job.id, "dry_run.started", "Dry run asset creation started."); + logger("info", "worker", "Dry run job started.", job.id); await waitWithAbort(500, signal); throwIfAborted(signal); - const filePath = resolveGeneratedAssetTargetPath(job, 'dry_run', '.svg'); + const filePath = assetPathing.resolveGeneratedAssetTargetPath(job, "dry_run", ".svg"); mkdirSync(path.dirname(filePath), { recursive: true }); - writeFileSync(filePath, svgForPrompt(job.finalPromptUsed), 'utf8'); + writeFileSync(filePath, svgForPrompt(job.finalPromptUsed), "utf8"); const asset = addAssetFn({ projectId: job.projectId, jobId: job.id, @@ -375,9 +273,9 @@ export function createWorkerController({ prompt: job.finalPromptUsed, width: 1200, height: 800, - mimeType: 'image/svg+xml', + mimeType: "image/svg+xml", }); - const catalogContext = resolveJobCatalogContext(job); + const catalogContext = resolveJobCatalogContextFn(job); const parsedPrompt = parsePromptTransportFn(job.finalPromptUsed); const catalogImage = registerCatalogImageFn({ filePath: asset.filePath, @@ -396,41 +294,41 @@ export function createWorkerController({ recipeId: parsedPrompt.recipeId, generationConfig: buildCatalogGenerationConfig(job.finalPromptUsed), }); - addJobEventFn(job.id, 'dry_run.completed', 'Dry run asset creation completed.', { + addJobEventFn(job.id, "dry_run.completed", "Dry run asset creation completed.", { durationMs: Date.now() - startedAt, assetCount: 1, }); - addJobEventFn(job.id, 'asset.created', 'Dry run asset created.', { assetId: asset.id }); - publishEventFn('asset.created', asset); - publishEventFn('catalog.created', catalogImage); - updateJobStatusFn(job.id, 'completed'); - publishEventFn('job.completed', getJobFn(job.id)); + addJobEventFn(job.id, "asset.created", "Dry run asset created.", { assetId: asset.id }); + publishEventFn("asset.created", asset); + publishEventFn("catalog.created", catalogImage); + updateJobStatusFn(job.id, "completed"); + publishEventFn("job.completed", getJobFn(job.id)); logger( - 'info', - 'worker', + "info", + "worker", `Dry run job completed. Asset: ${path.basename(asset.filePath)}`, job.id, ); } async function runCodexJob(job: Job, signal?: AbortSignal) { - addJobEventFn(job.id, 'codex.started', 'Codex image generation started.'); - logger('info', 'worker', 'Codex imagegen job started.', job.id); - const turnRecordId = upsertCodexTurnFn({ jobId: job.id, status: 'running' }); - const catalogContext = resolveJobCatalogContext(job); + addJobEventFn(job.id, "codex.started", "Codex image generation started."); + logger("info", "worker", "Codex imagegen job started.", job.id); + const turnRecordId = upsertCodexTurnFn({ jobId: job.id, status: "running" }); + const catalogContext = resolveJobCatalogContextFn(job); const executionOptions = resolveExecutionOptions(job.execution); const result = await codexGenerationProvider.run({ id: job.id, projectId: job.projectId, prompt: job.finalPromptUsed, execution: job.execution, - providerId: job.providerId ?? job.sourceSpec?.providerId ?? 'codex', + providerId: job.providerId ?? job.sourceSpec?.providerId ?? "codex", sourceSpec: job.sourceSpec, signal, }); throwIfAborted(signal); - addJobEventFn(job.id, 'codex.completed', 'Codex image generation completed.', { + addJobEventFn(job.id, "codex.completed", "Codex image generation completed.", { durationMs: result.durationMs, assetCount: result.assets.length, threadId: result.threadId, @@ -443,34 +341,40 @@ export function createWorkerController({ codexThreadId: result.threadId, codexTurnId: result.turnId, transcriptPath: result.transcript, - status: result.assets.length > 0 ? 'completed' : 'needs_review', + status: result.assets.length > 0 ? "completed" : "needs_review", }); const discoveredImagePath = result.assets[0]?.sourcePath ?? null; if (!discoveredImagePath) { - updateJobStatusFn(job.id, 'needs_review'); - publishEventFn('job.progress', getJobFn(job.id)); + updateJobStatusFn(job.id, "needs_review"); + publishEventFn("job.progress", getJobFn(job.id)); logger( - 'warn', - 'worker', + "warn", + "worker", `Codex turn completed but no image file was discovered. Transcript: ${result.transcript}`, job.id, ); return; } - await finalizeJobAsset(job, catalogContext, discoveredImagePath, 'codex', { - logPrefix: 'Codex', - embedMetadata: true, - executionOptions, + await assetFinalizer.finalizeJobAsset({ + job, + catalogContext, + discoveredImagePath, + providerId: "codex", + options: { + logPrefix: "Codex", + embedMetadata: true, + executionOptions, + }, }); } async function runExternalJob(job: Job, signal?: AbortSignal) { - const providerId = job.providerId ?? job.sourceSpec?.providerId ?? 'unknown'; - addJobEventFn(job.id, 'external.started', `External provider job started: ${providerId}.`); - logger('info', 'worker', `External provider job started: ${providerId}.`, job.id); - const catalogContext = resolveJobCatalogContext(job); + const providerId = job.providerId ?? job.sourceSpec?.providerId ?? "unknown"; + addJobEventFn(job.id, "external.started", `External provider job started: ${providerId}.`); + logger("info", "worker", `External provider job started: ${providerId}.`, job.id); + const catalogContext = resolveJobCatalogContextFn(job); const result = await externalGenerationProvider.run({ id: job.id, @@ -484,7 +388,7 @@ export function createWorkerController({ throwIfAborted(signal); - addJobEventFn(job.id, 'external.completed', 'External provider execution completed.', { + addJobEventFn(job.id, "external.completed", "External provider execution completed.", { transcript: result.transcript, durationMs: result.durationMs, assetCount: result.assets.length, @@ -492,19 +396,25 @@ export function createWorkerController({ const discoveredImagePath = result.assets[0]?.sourcePath ?? null; if (!discoveredImagePath) { - updateJobStatusFn(job.id, 'needs_review'); - publishEventFn('job.progress', getJobFn(job.id)); + updateJobStatusFn(job.id, "needs_review"); + publishEventFn("job.progress", getJobFn(job.id)); logger( - 'warn', - 'worker', + "warn", + "worker", `External provider completed but no image file was discovered. Transcript: ${result.transcript}`, job.id, ); return; } - await finalizeJobAsset(job, catalogContext, discoveredImagePath, providerId, { - logPrefix: 'External provider', + await assetFinalizer.finalizeJobAsset({ + job, + catalogContext, + discoveredImagePath, + providerId, + options: { + logPrefix: "External provider", + }, }); } @@ -513,35 +423,35 @@ export function createWorkerController({ runningJobControllers.set(job.id, controller); try { - addJobEventFn(job.id, 'job.started', 'Job execution started.', { + addJobEventFn(job.id, "job.started", "Job execution started.", { startedAt: new Date().toISOString(), }); - updateJobStatusFn(job.id, 'running'); - publishEventFn('job.running', getJobFn(job.id)); - const runtimeTarget = resolveWorkerRuntimeTarget(job); + updateJobStatusFn(job.id, "running"); + publishEventFn("job.running", getJobFn(job.id)); + const runtimeTarget = resolveWorkerRuntimeTargetFn(job); - if (runtimeTarget === 'dry_run') { + if (runtimeTarget === "dry_run") { await runDryJob(job, controller.signal); - } else if (runtimeTarget === 'codex') { + } else if (runtimeTarget === "codex") { await runCodexJob(job, controller.signal); - } else if (runtimeTarget === 'external') { + } else if (runtimeTarget === "external") { await runExternalJob(job, controller.signal); } else { throw new Error( - `Unsupported job kind received by worker: kind=${job.kind} provider=${job.providerId ?? job.sourceSpec?.providerId ?? 'null'} sourceTask=${job.sourceSpec?.task ?? 'null'}`, + `Unsupported job kind received by worker: kind=${job.kind} provider=${job.providerId ?? job.sourceSpec?.providerId ?? "null"} sourceTask=${job.sourceSpec?.task ?? "null"}`, ); } } catch (error) { if (isAbortError(error)) { - addJobEventFn(job.id, 'job.cancelled', 'Job cancelled by user.'); - updateJobStatusFn(job.id, 'cancelled'); - publishEventFn('job.cancelled', getJobFn(job.id)); - logger('info', 'worker', 'Job cancelled by user.', job.id); + addJobEventFn(job.id, "job.cancelled", "Job cancelled by user."); + updateJobStatusFn(job.id, "cancelled"); + publishEventFn("job.cancelled", getJobFn(job.id)); + logger("info", "worker", "Job cancelled by user.", job.id); } else { const message = error instanceof Error ? error.message : String(error); - updateJobStatusFn(job.id, 'failed', message); - publishEventFn('job.failed', getJobFn(job.id)); - logger('error', 'worker', message, job.id); + updateJobStatusFn(job.id, "failed", message); + publishEventFn("job.failed", getJobFn(job.id)); + logger("error", "worker", message, job.id); } } finally { runningJobControllers.delete(job.id); @@ -580,18 +490,18 @@ export function createWorkerController({ if (queuedIndex >= 0) { jobQueue.splice(queuedIndex, 1); runningJobs.delete(jobId); - addJobEventFn(jobId, 'job.cancelled', 'Queued job cancelled before execution.'); - const job = updateJobStatusFn(jobId, 'cancelled'); - publishEventFn('job.cancelled', job); - logger('info', 'worker', 'Queued job cancelled before execution.', jobId); + addJobEventFn(jobId, "job.cancelled", "Queued job cancelled before execution."); + const job = updateJobStatusFn(jobId, "cancelled"); + publishEventFn("job.cancelled", job); + logger("info", "worker", "Queued job cancelled before execution.", jobId); return job; } const controller = runningJobControllers.get(jobId); if (controller) { - addJobEventFn(jobId, 'job.cancel.requested', 'Cancellation requested for running job.'); + addJobEventFn(jobId, "job.cancel.requested", "Cancellation requested for running job."); controller.abort(); - logger('info', 'worker', 'Cancellation requested for running job.', jobId); + logger("info", "worker", "Cancellation requested for running job.", jobId); return getJobFn(jobId); } @@ -610,14 +520,14 @@ export function createWorkerController({ for (const queuedJob of queuedJobs) { runningJobs.delete(queuedJob.id); - addJobEventFn(queuedJob.id, 'job.cancelled', 'Queued job cancelled during studio reset.'); - updateJobStatusFn(queuedJob.id, 'cancelled'); - publishEventFn('job.cancelled', getJobFn(queuedJob.id)); + addJobEventFn(queuedJob.id, "job.cancelled", "Queued job cancelled during studio reset."); + updateJobStatusFn(queuedJob.id, "cancelled"); + publishEventFn("job.cancelled", getJobFn(queuedJob.id)); } for (const [jobId, controller] of runningJobControllers.entries()) { if (!controller.signal.aborted) { - addJobEventFn(jobId, 'job.cancel.requested', 'Studio reset requested cancellation.'); + addJobEventFn(jobId, "job.cancel.requested", "Studio reset requested cancellation."); controller.abort(); } } diff --git a/apps/local-server/src/workerAssetFinalizer.test.ts b/apps/local-server/src/workerAssetFinalizer.test.ts new file mode 100644 index 00000000..11c3e643 --- /dev/null +++ b/apps/local-server/src/workerAssetFinalizer.test.ts @@ -0,0 +1,116 @@ +import { describe, expect, it, vi } from "vite-plus/test"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { Job } from "../../../packages/shared/src"; +import { createWorkerAssetFinalizer } from "./workerAssetFinalizer"; + +function createJob(overrides: Partial = {}): Job { + return { + id: overrides.id ?? "job-finalizer-1", + projectId: overrides.projectId ?? "project-1", + kind: overrides.kind ?? "image_generate", + providerId: overrides.providerId ?? "codex", + sourceSpec: overrides.sourceSpec ?? null, + status: overrides.status ?? "running", + execution: overrides.execution ?? null, + originalPrompt: overrides.originalPrompt ?? "prompt", + expandedPrompt: overrides.expandedPrompt ?? null, + finalPromptUsed: overrides.finalPromptUsed ?? "prompt", + error: overrides.error ?? null, + createdAt: overrides.createdAt ?? new Date().toISOString(), + updatedAt: overrides.updatedAt ?? new Date().toISOString(), + completedAt: overrides.completedAt ?? null, + }; +} + +describe("workerAssetFinalizer", () => { + it("finalizes asset using organized path for file and public URL", async () => { + const tempRoot = mkdtempSync(path.join(os.tmpdir(), "worker-asset-finalizer-")); + const organizedPath = path.join(tempRoot, "outputs", "final.png"); + mkdirSync(path.dirname(organizedPath), { recursive: true }); + writeFileSync(organizedPath, "png", "utf8"); + + const addAsset = vi.fn(() => ({ + id: "asset-1", + projectId: "project-1", + jobId: "job-finalizer-1", + filePath: organizedPath, + thumbnailPath: null, + publicUrl: "/library/outputs/final.png", + prompt: "prompt", + width: null, + height: null, + mimeType: "image/png", + createdAt: new Date().toISOString(), + deletedAt: null, + })); + const registerCatalogImage = vi.fn(() => ({ + id: "catalog-1", + libraryId: "library-1", + })); + const publishEvent = vi.fn(); + const updateJobStatus = vi.fn(); + const getJob = vi.fn(() => createJob()); + const toPublicAssetUrl = vi.fn(() => "/library/outputs/final.png"); + const addJobEvent = vi.fn(); + const logger = vi.fn(); + + const finalizer = createWorkerAssetFinalizer({ + registerCatalogImage, + addAsset, + addJobEvent, + updateJobStatus, + publishEvent, + getJob, + toPublicAssetUrl, + logger, + embedMetadata: vi.fn(async () => {}), + parsePromptTransport: vi.fn(() => ({ + prompt: "prompt", + negativePrompt: null, + aspectRatio: "1:1", + imageSize: "1024x1024", + recipeId: null, + recipeContext: null, + })), + resolveExecutionOptions: vi.fn(() => ({ + model: "gpt-5.4-mini", + reasoningEffort: "medium", + serviceTier: null, + })), + resolveCatalogGenerationConfig: vi.fn(() => ({ + prompt: "prompt", + })), + organizeGeneratedAssetPath: vi.fn(() => organizedPath), + inferGeneratedAssetMimeType: vi.fn(() => "image/png"), + }); + + try { + await finalizer.finalizeJobAsset({ + job: createJob(), + catalogContext: { + workspaceId: "workspace-1", + batchId: "batch-1", + }, + discoveredImagePath: "D:/tmp/discovered.png", + providerId: "codex", + options: { + logPrefix: "Codex", + }, + }); + + expect(toPublicAssetUrl).toHaveBeenCalledWith(organizedPath); + expect(addAsset).toHaveBeenCalledWith( + expect.objectContaining({ + filePath: organizedPath, + publicUrl: "/library/outputs/final.png", + }), + ); + expect(updateJobStatus).toHaveBeenCalledWith("job-finalizer-1", "completed"); + expect(publishEvent).toHaveBeenCalledWith("job.completed", expect.anything()); + } finally { + rmSync(tempRoot, { recursive: true, force: true }); + } + }); +}); diff --git a/apps/local-server/src/workerAssetFinalizer.ts b/apps/local-server/src/workerAssetFinalizer.ts new file mode 100644 index 00000000..54f85d4c --- /dev/null +++ b/apps/local-server/src/workerAssetFinalizer.ts @@ -0,0 +1,149 @@ +import { statSync } from "node:fs"; +import path from "node:path"; +import type { registerCatalogImage } from "./catalog"; +import type { addAsset, addJobEvent, getJob, updateJobStatus } from "./db"; +import type { publishEvent } from "./events"; +import type { toPublicAssetUrl } from "./library"; +import type { log } from "./logger"; +import type { embedMetadata } from "./metadataEmbedder"; +import type { parsePromptTransport } from "../../../packages/shared/src/promptTransport"; +import type { Job } from "../../../packages/shared/src/types"; +import type { resolveJobExecutionOptions } from "./codex/executionOptions"; +import type { resolveJobCatalogContext } from "./workerCatalogContext"; + +interface WorkerAssetFinalizerDependencies { + registerCatalogImage: typeof registerCatalogImage; + addAsset: typeof addAsset; + addJobEvent: typeof addJobEvent; + updateJobStatus: typeof updateJobStatus; + publishEvent: typeof publishEvent; + getJob: typeof getJob; + toPublicAssetUrl: typeof toPublicAssetUrl; + logger: typeof log; + embedMetadata: typeof embedMetadata; + parsePromptTransport: typeof parsePromptTransport; + resolveExecutionOptions: typeof resolveJobExecutionOptions; + resolveCatalogGenerationConfig: (job: Job) => Record; + organizeGeneratedAssetPath: (job: Job, filePath: string, providerId: string | null) => string; + inferGeneratedAssetMimeType: (filePath: string) => string; +} + +interface FinalizeWorkerAssetOptions { + logPrefix: string; + embedMetadata?: boolean; + executionOptions?: ReturnType; +} + +export function createWorkerAssetFinalizer({ + registerCatalogImage, + addAsset, + addJobEvent, + updateJobStatus, + publishEvent, + getJob, + toPublicAssetUrl, + logger, + embedMetadata, + parsePromptTransport, + resolveCatalogGenerationConfig, + organizeGeneratedAssetPath, + inferGeneratedAssetMimeType, +}: WorkerAssetFinalizerDependencies) { + async function finalizeJobAsset({ + job, + catalogContext, + discoveredImagePath, + providerId, + options, + }: { + job: Job; + catalogContext: ReturnType; + discoveredImagePath: string; + providerId: string; + options: FinalizeWorkerAssetOptions; + }) { + const mimeType = inferGeneratedAssetMimeType(discoveredImagePath); + const organizedImagePath = organizeGeneratedAssetPath(job, discoveredImagePath, providerId); + + const asset = addAsset({ + projectId: job.projectId, + jobId: job.id, + filePath: organizedImagePath, + thumbnailPath: null, + publicUrl: toPublicAssetUrl(organizedImagePath), + prompt: job.finalPromptUsed, + width: null, + height: null, + mimeType, + }); + + const parsedPrompt = job.sourceSpec + ? { + prompt: job.sourceSpec.prompt, + negativePrompt: job.sourceSpec.negativePrompt, + aspectRatio: job.sourceSpec.output.aspectRatio, + imageSize: job.sourceSpec.output.imageSize, + recipeId: job.sourceSpec.recipeId, + } + : parsePromptTransport(job.finalPromptUsed); + + const catalogImage = registerCatalogImage({ + filePath: asset.filePath, + thumbnailPath: asset.thumbnailPath, + prompt: asset.prompt, + negativePrompt: parsedPrompt.negativePrompt || null, + aspectRatio: parsedPrompt.aspectRatio, + imageSize: parsedPrompt.imageSize, + width: asset.width, + height: asset.height, + mimeType: asset.mimeType, + fileSizeBytes: statSync(asset.filePath).size, + jobId: asset.jobId, + workspaceId: catalogContext.workspaceId, + batchId: catalogContext.batchId, + recipeId: parsedPrompt.recipeId, + generationConfig: resolveCatalogGenerationConfig(job), + }); + + if (options.embedMetadata && options.executionOptions) { + void embedMetadata(asset.filePath, { + prompt: job.finalPromptUsed, + negativePrompt: parsedPrompt.negativePrompt || null, + aspectRatio: parsedPrompt.aspectRatio, + imageSize: parsedPrompt.imageSize, + model: options.executionOptions.model, + recipe: parsedPrompt.recipeId, + batchId: catalogContext.batchId ?? job.id, + generatedAt: new Date().toISOString(), + studioVersion: "0.0.0", + libraryId: catalogImage.libraryId, + catalogId: catalogImage.id, + }).catch((error) => { + logger( + "warn", + "metadata", + `Metadata embed failed: ${error instanceof Error ? error.message : String(error)}`, + job.id, + ); + }); + } + + addJobEvent(job.id, "asset.created", `${options.logPrefix} asset imported.`, { + assetId: asset.id, + }); + publishEvent("asset.created", asset); + publishEvent("catalog.created", catalogImage); + updateJobStatus(job.id, "completed"); + publishEvent("job.completed", getJob(job.id)); + logger( + "info", + "worker", + `${options.logPrefix} job completed. Asset: ${path.basename(asset.filePath)}`, + job.id, + ); + } + + return { + finalizeJobAsset, + }; +} diff --git a/apps/local-server/src/workerAssetPathing.test.ts b/apps/local-server/src/workerAssetPathing.test.ts new file mode 100644 index 00000000..91da3d32 --- /dev/null +++ b/apps/local-server/src/workerAssetPathing.test.ts @@ -0,0 +1,96 @@ +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vite-plus/test"; +import { createDefaultEditableStudioSettings, type Job } from "../../../packages/shared/src"; +import { createWorkerAssetPathing, inferGeneratedAssetMimeType } from "./workerAssetPathing"; + +function createJob(overrides: Partial = {}): Job { + return { + id: overrides.id ?? "job-asset-pathing", + projectId: overrides.projectId ?? "project-1", + kind: overrides.kind ?? "image_generate", + providerId: overrides.providerId ?? "codex", + sourceSpec: overrides.sourceSpec ?? null, + status: overrides.status ?? "queued", + execution: overrides.execution ?? null, + originalPrompt: overrides.originalPrompt ?? "prompt", + expandedPrompt: overrides.expandedPrompt ?? null, + finalPromptUsed: overrides.finalPromptUsed ?? "prompt", + error: overrides.error ?? null, + createdAt: overrides.createdAt ?? new Date().toISOString(), + updatedAt: overrides.updatedAt ?? new Date().toISOString(), + completedAt: overrides.completedAt ?? null, + }; +} + +describe("workerAssetPathing", () => { + it("infers generated asset mime type from extension", () => { + expect(inferGeneratedAssetMimeType("x.png")).toBe("image/png"); + expect(inferGeneratedAssetMimeType("x.jpg")).toBe("image/jpeg"); + expect(inferGeneratedAssetMimeType("x.jpeg")).toBe("image/jpeg"); + expect(inferGeneratedAssetMimeType("x.webp")).toBe("image/webp"); + expect(inferGeneratedAssetMimeType("x.svg")).toBe("image/svg+xml"); + }); + + it("organizes discovered files into output paths and keeps bytes intact", () => { + const tempRoot = mkdtempSync(path.join(os.tmpdir(), "worker-asset-pathing-")); + + try { + const pathing = createWorkerAssetPathing({ + resolveExecutionOptions: () => ({ + model: "gpt-5.4-mini", + reasoningEffort: "medium", + serviceTier: null, + }), + readEditableStudioSettings: () => createDefaultEditableStudioSettings(), + getSetting: () => null, + setSetting: () => {}, + resolveLibraryPath: (...segments: string[]) => path.join(tempRoot, ...segments), + }); + + const sourcePath = path.join(tempRoot, "incoming", "image.png"); + mkdirSync(path.dirname(sourcePath), { recursive: true }); + writeFileSync(sourcePath, "pixel-data", "utf8"); + + const job = createJob(); + const organizedPath = pathing.organizeGeneratedAssetPath(job, sourcePath, "codex"); + + expect(organizedPath).not.toBe(sourcePath); + expect(organizedPath).toContain(`${path.sep}outputs${path.sep}`); + expect(existsSync(organizedPath)).toBe(true); + expect(existsSync(sourcePath)).toBe(false); + expect(readFileSync(organizedPath, "utf8")).toBe("pixel-data"); + } finally { + rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + it("resolves unique target paths when the generated path is already taken", () => { + const tempRoot = mkdtempSync(path.join(os.tmpdir(), "worker-asset-pathing-unique-")); + const fixedTarget = path.join(tempRoot, "outputs", "fixed-file.png"); + + try { + const pathing = createWorkerAssetPathing({ + resolveExecutionOptions: () => ({ + model: "gpt-5.4-mini", + reasoningEffort: "medium", + serviceTier: null, + }), + readEditableStudioSettings: () => createDefaultEditableStudioSettings(), + getSetting: () => null, + setSetting: () => {}, + resolveLibraryPath: () => fixedTarget, + }); + + mkdirSync(path.dirname(fixedTarget), { recursive: true }); + writeFileSync(fixedTarget, "occupied", "utf8"); + + const resolved = pathing.resolveGeneratedAssetTargetPath(createJob(), "codex", ".png"); + expect(resolved).not.toBe(fixedTarget); + expect(path.basename(resolved)).toBe("fixed-file-2.png"); + } finally { + rmSync(tempRoot, { recursive: true, force: true }); + } + }); +}); diff --git a/apps/local-server/src/workerAssetPathing.ts b/apps/local-server/src/workerAssetPathing.ts new file mode 100644 index 00000000..e4bd3640 --- /dev/null +++ b/apps/local-server/src/workerAssetPathing.ts @@ -0,0 +1,76 @@ +import { existsSync, mkdirSync, renameSync } from "node:fs"; +import path from "node:path"; +import { buildOutputAssetRelativePath } from "./outputOrganization"; +import type { Job } from "../../../packages/shared/src/types"; +import type { resolveJobExecutionOptions } from "./codex/executionOptions"; +import type { readEditableStudioSettings } from "./studioSettingsStore"; +import type { getSettingValue, setSettingValue } from "./db"; +import type { resolveLibraryPath } from "./library"; + +function resolveUniquePath(filePath: string) { + if (!existsSync(filePath)) return filePath; + const parsed = path.parse(filePath); + for (let index = 2; index < 1000; index += 1) { + const candidate = path.join(parsed.dir, `${parsed.name}-${index}${parsed.ext}`); + if (!existsSync(candidate)) return candidate; + } + return path.join(parsed.dir, `${parsed.name}-${Date.now()}${parsed.ext}`); +} + +export function inferGeneratedAssetMimeType(filePath: string) { + const ext = path.extname(filePath).toLowerCase(); + if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg"; + if (ext === ".webp") return "image/webp"; + if (ext === ".svg") return "image/svg+xml"; + return "image/png"; +} + +interface CreateWorkerAssetPathingDependencies { + resolveExecutionOptions: typeof resolveJobExecutionOptions; + readEditableStudioSettings: typeof readEditableStudioSettings; + getSetting: typeof getSettingValue; + setSetting: typeof setSettingValue; + resolveLibraryPath: typeof resolveLibraryPath; +} + +export function createWorkerAssetPathing({ + resolveExecutionOptions, + readEditableStudioSettings, + getSetting, + setSetting, + resolveLibraryPath, +}: CreateWorkerAssetPathingDependencies) { + function resolveGeneratedAssetTargetPath(job: Job, providerId: string | null, extension: string) { + const executionOptions = resolveExecutionOptions(job.execution); + const settings = readEditableStudioSettings({ + getSetting, + setSetting, + }); + const relativePath = buildOutputAssetRelativePath(settings, { + jobId: job.id, + providerId, + model: executionOptions.model, + recipeId: job.sourceSpec?.recipeId ?? null, + extension, + }); + return resolveUniquePath(resolveLibraryPath(...relativePath.split(/[\\/]/))); + } + + function organizeGeneratedAssetPath(job: Job, filePath: string, providerId: string | null) { + const ext = path.extname(filePath).toLowerCase() || ".png"; + const targetPath = resolveGeneratedAssetTargetPath(job, providerId, ext); + + if (path.resolve(filePath) === path.resolve(targetPath)) return filePath; + mkdirSync(path.dirname(targetPath), { recursive: true }); + if (existsSync(filePath)) { + renameSync(filePath, targetPath); + return targetPath; + } + return filePath; + } + + return { + resolveGeneratedAssetTargetPath, + organizeGeneratedAssetPath, + }; +} diff --git a/artifacts/marca-ficticia-roperia.svg b/artifacts/marca-ficticia-roperia.svg new file mode 100644 index 00000000..7cc7198f --- /dev/null +++ b/artifacts/marca-ficticia-roperia.svg @@ -0,0 +1,41 @@ + + Logo de marca de ropa ficticia + Marca ficticia: NOVA THREADS + + + + + + + + + + + + + + + + + + + + + + + + NOVA + THREADS + + Ropa urbana consciente + + + + + + + + + + Colección "Horizonte" · Edición 2026 + diff --git a/assets/recipes/styles/defaults/SP06-101.webp b/assets/recipes/styles/defaults/SP06-101.webp new file mode 100644 index 00000000..0882aa3f Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-101.webp differ diff --git a/assets/recipes/styles/defaults/SP06-102.webp b/assets/recipes/styles/defaults/SP06-102.webp new file mode 100644 index 00000000..e6e0ed2d Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-102.webp differ diff --git a/assets/recipes/styles/defaults/SP06-103.webp b/assets/recipes/styles/defaults/SP06-103.webp new file mode 100644 index 00000000..72f95b3e Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-103.webp differ diff --git a/assets/recipes/styles/defaults/SP06-104.webp b/assets/recipes/styles/defaults/SP06-104.webp new file mode 100644 index 00000000..88471a65 Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-104.webp differ diff --git a/assets/recipes/styles/defaults/SP06-105.webp b/assets/recipes/styles/defaults/SP06-105.webp new file mode 100644 index 00000000..47b56015 Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-105.webp differ diff --git a/assets/recipes/styles/defaults/SP06-106.webp b/assets/recipes/styles/defaults/SP06-106.webp new file mode 100644 index 00000000..cdbb15e7 Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-106.webp differ diff --git a/assets/recipes/styles/defaults/SP06-107.webp b/assets/recipes/styles/defaults/SP06-107.webp new file mode 100644 index 00000000..c6428147 Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-107.webp differ diff --git a/assets/recipes/styles/defaults/SP06-108.webp b/assets/recipes/styles/defaults/SP06-108.webp new file mode 100644 index 00000000..f6f0d835 Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-108.webp differ diff --git a/assets/recipes/styles/defaults/SP06-109.webp b/assets/recipes/styles/defaults/SP06-109.webp new file mode 100644 index 00000000..f56f8dd3 Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-109.webp differ diff --git a/assets/recipes/styles/defaults/SP06-110.webp b/assets/recipes/styles/defaults/SP06-110.webp new file mode 100644 index 00000000..fec146ff Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-110.webp differ diff --git a/assets/recipes/styles/defaults/SP06-111.webp b/assets/recipes/styles/defaults/SP06-111.webp new file mode 100644 index 00000000..6afc76c8 Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-111.webp differ diff --git a/assets/recipes/styles/defaults/SP06-112.webp b/assets/recipes/styles/defaults/SP06-112.webp new file mode 100644 index 00000000..2cee8112 Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-112.webp differ diff --git a/assets/recipes/styles/defaults/SP06-113.webp b/assets/recipes/styles/defaults/SP06-113.webp new file mode 100644 index 00000000..94086b1e Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-113.webp differ diff --git a/assets/recipes/styles/defaults/SP06-114.webp b/assets/recipes/styles/defaults/SP06-114.webp new file mode 100644 index 00000000..da7988be Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-114.webp differ diff --git a/assets/recipes/styles/defaults/SP06-115.webp b/assets/recipes/styles/defaults/SP06-115.webp new file mode 100644 index 00000000..7059e2be Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-115.webp differ diff --git a/assets/recipes/styles/defaults/SP06-116.webp b/assets/recipes/styles/defaults/SP06-116.webp new file mode 100644 index 00000000..99fad4fb Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-116.webp differ diff --git a/assets/recipes/styles/defaults/SP06-117.webp b/assets/recipes/styles/defaults/SP06-117.webp new file mode 100644 index 00000000..73ba4c0c Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-117.webp differ diff --git a/assets/recipes/styles/defaults/SP06-118.webp b/assets/recipes/styles/defaults/SP06-118.webp new file mode 100644 index 00000000..b0d19de4 Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-118.webp differ diff --git a/assets/recipes/styles/defaults/SP06-119.webp b/assets/recipes/styles/defaults/SP06-119.webp new file mode 100644 index 00000000..60e6971c Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-119.webp differ diff --git a/assets/recipes/styles/defaults/SP06-120.webp b/assets/recipes/styles/defaults/SP06-120.webp new file mode 100644 index 00000000..fc9cb8b9 Binary files /dev/null and b/assets/recipes/styles/defaults/SP06-120.webp differ diff --git a/assets/recipes/styles/defaults/SP12-001.webp b/assets/recipes/styles/defaults/SP12-001.webp new file mode 100644 index 00000000..2fc77a40 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-001.webp differ diff --git a/assets/recipes/styles/defaults/SP12-002.webp b/assets/recipes/styles/defaults/SP12-002.webp new file mode 100644 index 00000000..b8ecd082 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-002.webp differ diff --git a/assets/recipes/styles/defaults/SP12-003.webp b/assets/recipes/styles/defaults/SP12-003.webp new file mode 100644 index 00000000..087b64c1 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-003.webp differ diff --git a/assets/recipes/styles/defaults/SP12-004.webp b/assets/recipes/styles/defaults/SP12-004.webp new file mode 100644 index 00000000..4dddc09e Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-004.webp differ diff --git a/assets/recipes/styles/defaults/SP12-005.webp b/assets/recipes/styles/defaults/SP12-005.webp new file mode 100644 index 00000000..dbe498e7 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-005.webp differ diff --git a/assets/recipes/styles/defaults/SP12-006.webp b/assets/recipes/styles/defaults/SP12-006.webp new file mode 100644 index 00000000..2e2c8302 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-006.webp differ diff --git a/assets/recipes/styles/defaults/SP12-007.webp b/assets/recipes/styles/defaults/SP12-007.webp new file mode 100644 index 00000000..a02bcaa6 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-007.webp differ diff --git a/assets/recipes/styles/defaults/SP12-008.webp b/assets/recipes/styles/defaults/SP12-008.webp new file mode 100644 index 00000000..0db1be16 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-008.webp differ diff --git a/assets/recipes/styles/defaults/SP12-009.webp b/assets/recipes/styles/defaults/SP12-009.webp new file mode 100644 index 00000000..5a6250c0 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-009.webp differ diff --git a/assets/recipes/styles/defaults/SP12-010.webp b/assets/recipes/styles/defaults/SP12-010.webp new file mode 100644 index 00000000..6c3a2edd Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-010.webp differ diff --git a/assets/recipes/styles/defaults/SP12-011.webp b/assets/recipes/styles/defaults/SP12-011.webp new file mode 100644 index 00000000..e9a9e99f Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-011.webp differ diff --git a/assets/recipes/styles/defaults/SP12-012.webp b/assets/recipes/styles/defaults/SP12-012.webp new file mode 100644 index 00000000..0c0514aa Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-012.webp differ diff --git a/assets/recipes/styles/defaults/SP12-013.webp b/assets/recipes/styles/defaults/SP12-013.webp new file mode 100644 index 00000000..be2f9236 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-013.webp differ diff --git a/assets/recipes/styles/defaults/SP12-014.webp b/assets/recipes/styles/defaults/SP12-014.webp new file mode 100644 index 00000000..793ab01f Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-014.webp differ diff --git a/assets/recipes/styles/defaults/SP12-015.webp b/assets/recipes/styles/defaults/SP12-015.webp new file mode 100644 index 00000000..24c90814 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-015.webp differ diff --git a/assets/recipes/styles/defaults/SP12-016.webp b/assets/recipes/styles/defaults/SP12-016.webp new file mode 100644 index 00000000..b3b40213 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-016.webp differ diff --git a/assets/recipes/styles/defaults/SP12-017.webp b/assets/recipes/styles/defaults/SP12-017.webp new file mode 100644 index 00000000..e81d1f89 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-017.webp differ diff --git a/assets/recipes/styles/defaults/SP12-018.webp b/assets/recipes/styles/defaults/SP12-018.webp new file mode 100644 index 00000000..9d6f6e21 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-018.webp differ diff --git a/assets/recipes/styles/defaults/SP12-019.webp b/assets/recipes/styles/defaults/SP12-019.webp new file mode 100644 index 00000000..afe9a16a Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-019.webp differ diff --git a/assets/recipes/styles/defaults/SP12-020.webp b/assets/recipes/styles/defaults/SP12-020.webp new file mode 100644 index 00000000..9621069d Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-020.webp differ diff --git a/assets/recipes/styles/defaults/SP12-021.webp b/assets/recipes/styles/defaults/SP12-021.webp new file mode 100644 index 00000000..e70c4457 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-021.webp differ diff --git a/assets/recipes/styles/defaults/SP12-022.webp b/assets/recipes/styles/defaults/SP12-022.webp new file mode 100644 index 00000000..9972db38 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-022.webp differ diff --git a/assets/recipes/styles/defaults/SP12-023.webp b/assets/recipes/styles/defaults/SP12-023.webp new file mode 100644 index 00000000..dac2ba7a Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-023.webp differ diff --git a/assets/recipes/styles/defaults/SP12-024.webp b/assets/recipes/styles/defaults/SP12-024.webp new file mode 100644 index 00000000..6a99ce04 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-024.webp differ diff --git a/assets/recipes/styles/defaults/SP12-025.webp b/assets/recipes/styles/defaults/SP12-025.webp new file mode 100644 index 00000000..34ff7eb7 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-025.webp differ diff --git a/assets/recipes/styles/defaults/SP12-026.webp b/assets/recipes/styles/defaults/SP12-026.webp new file mode 100644 index 00000000..b9e4944a Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-026.webp differ diff --git a/assets/recipes/styles/defaults/SP12-027.webp b/assets/recipes/styles/defaults/SP12-027.webp new file mode 100644 index 00000000..a8e8e1e8 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-027.webp differ diff --git a/assets/recipes/styles/defaults/SP12-028.webp b/assets/recipes/styles/defaults/SP12-028.webp new file mode 100644 index 00000000..d9846ded Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-028.webp differ diff --git a/assets/recipes/styles/defaults/SP12-029.webp b/assets/recipes/styles/defaults/SP12-029.webp new file mode 100644 index 00000000..6c971d60 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-029.webp differ diff --git a/assets/recipes/styles/defaults/SP12-030.webp b/assets/recipes/styles/defaults/SP12-030.webp new file mode 100644 index 00000000..3a1410e6 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-030.webp differ diff --git a/assets/recipes/styles/defaults/SP12-031.webp b/assets/recipes/styles/defaults/SP12-031.webp new file mode 100644 index 00000000..3db5774a Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-031.webp differ diff --git a/assets/recipes/styles/defaults/SP12-032.webp b/assets/recipes/styles/defaults/SP12-032.webp new file mode 100644 index 00000000..b579a51f Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-032.webp differ diff --git a/assets/recipes/styles/defaults/SP12-033.webp b/assets/recipes/styles/defaults/SP12-033.webp new file mode 100644 index 00000000..5369812e Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-033.webp differ diff --git a/assets/recipes/styles/defaults/SP12-034.webp b/assets/recipes/styles/defaults/SP12-034.webp new file mode 100644 index 00000000..b14113fd Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-034.webp differ diff --git a/assets/recipes/styles/defaults/SP12-035.webp b/assets/recipes/styles/defaults/SP12-035.webp new file mode 100644 index 00000000..fcc33837 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-035.webp differ diff --git a/assets/recipes/styles/defaults/SP12-036.webp b/assets/recipes/styles/defaults/SP12-036.webp new file mode 100644 index 00000000..7cca4aa2 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-036.webp differ diff --git a/assets/recipes/styles/defaults/SP12-037.webp b/assets/recipes/styles/defaults/SP12-037.webp new file mode 100644 index 00000000..f35634a2 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-037.webp differ diff --git a/assets/recipes/styles/defaults/SP12-038.webp b/assets/recipes/styles/defaults/SP12-038.webp new file mode 100644 index 00000000..3fef260b Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-038.webp differ diff --git a/assets/recipes/styles/defaults/SP12-039.webp b/assets/recipes/styles/defaults/SP12-039.webp new file mode 100644 index 00000000..18cf8cce Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-039.webp differ diff --git a/assets/recipes/styles/defaults/SP12-040.webp b/assets/recipes/styles/defaults/SP12-040.webp new file mode 100644 index 00000000..b52409be Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-040.webp differ diff --git a/assets/recipes/styles/defaults/SP12-041.webp b/assets/recipes/styles/defaults/SP12-041.webp new file mode 100644 index 00000000..8aa60aa2 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-041.webp differ diff --git a/assets/recipes/styles/defaults/SP12-042.webp b/assets/recipes/styles/defaults/SP12-042.webp new file mode 100644 index 00000000..7c9b9849 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-042.webp differ diff --git a/assets/recipes/styles/defaults/SP12-043.webp b/assets/recipes/styles/defaults/SP12-043.webp new file mode 100644 index 00000000..65ff99cd Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-043.webp differ diff --git a/assets/recipes/styles/defaults/SP12-044.webp b/assets/recipes/styles/defaults/SP12-044.webp new file mode 100644 index 00000000..a5175cc8 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-044.webp differ diff --git a/assets/recipes/styles/defaults/SP12-045.webp b/assets/recipes/styles/defaults/SP12-045.webp new file mode 100644 index 00000000..a8310339 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-045.webp differ diff --git a/assets/recipes/styles/defaults/SP12-046.webp b/assets/recipes/styles/defaults/SP12-046.webp new file mode 100644 index 00000000..808aa523 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-046.webp differ diff --git a/assets/recipes/styles/defaults/SP12-047.webp b/assets/recipes/styles/defaults/SP12-047.webp new file mode 100644 index 00000000..30bfb169 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-047.webp differ diff --git a/assets/recipes/styles/defaults/SP12-048.webp b/assets/recipes/styles/defaults/SP12-048.webp new file mode 100644 index 00000000..5cfce844 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-048.webp differ diff --git a/assets/recipes/styles/defaults/SP12-049.webp b/assets/recipes/styles/defaults/SP12-049.webp new file mode 100644 index 00000000..251da993 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-049.webp differ diff --git a/assets/recipes/styles/defaults/SP12-050.webp b/assets/recipes/styles/defaults/SP12-050.webp new file mode 100644 index 00000000..8fc07338 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-050.webp differ diff --git a/assets/recipes/styles/defaults/SP12-051.webp b/assets/recipes/styles/defaults/SP12-051.webp new file mode 100644 index 00000000..3bd52583 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-051.webp differ diff --git a/assets/recipes/styles/defaults/SP12-052.webp b/assets/recipes/styles/defaults/SP12-052.webp new file mode 100644 index 00000000..20352780 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-052.webp differ diff --git a/assets/recipes/styles/defaults/SP12-053.webp b/assets/recipes/styles/defaults/SP12-053.webp new file mode 100644 index 00000000..35e5694f Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-053.webp differ diff --git a/assets/recipes/styles/defaults/SP12-054.webp b/assets/recipes/styles/defaults/SP12-054.webp new file mode 100644 index 00000000..ca02e305 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-054.webp differ diff --git a/assets/recipes/styles/defaults/SP12-055.webp b/assets/recipes/styles/defaults/SP12-055.webp new file mode 100644 index 00000000..f5811945 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-055.webp differ diff --git a/assets/recipes/styles/defaults/SP12-056.webp b/assets/recipes/styles/defaults/SP12-056.webp new file mode 100644 index 00000000..55583ea9 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-056.webp differ diff --git a/assets/recipes/styles/defaults/SP12-057.webp b/assets/recipes/styles/defaults/SP12-057.webp new file mode 100644 index 00000000..b83058a3 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-057.webp differ diff --git a/assets/recipes/styles/defaults/SP12-058.webp b/assets/recipes/styles/defaults/SP12-058.webp new file mode 100644 index 00000000..037e8776 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-058.webp differ diff --git a/assets/recipes/styles/defaults/SP12-059.webp b/assets/recipes/styles/defaults/SP12-059.webp new file mode 100644 index 00000000..0fa80edc Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-059.webp differ diff --git a/assets/recipes/styles/defaults/SP12-060.webp b/assets/recipes/styles/defaults/SP12-060.webp new file mode 100644 index 00000000..2f89300b Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-060.webp differ diff --git a/assets/recipes/styles/defaults/SP12-061.webp b/assets/recipes/styles/defaults/SP12-061.webp new file mode 100644 index 00000000..19fcfa6d Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-061.webp differ diff --git a/assets/recipes/styles/defaults/SP12-062.webp b/assets/recipes/styles/defaults/SP12-062.webp new file mode 100644 index 00000000..ccf9080e Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-062.webp differ diff --git a/assets/recipes/styles/defaults/SP12-063.webp b/assets/recipes/styles/defaults/SP12-063.webp new file mode 100644 index 00000000..c2de3c0b Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-063.webp differ diff --git a/assets/recipes/styles/defaults/SP12-064.webp b/assets/recipes/styles/defaults/SP12-064.webp new file mode 100644 index 00000000..07486cf6 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-064.webp differ diff --git a/assets/recipes/styles/defaults/SP12-065.webp b/assets/recipes/styles/defaults/SP12-065.webp new file mode 100644 index 00000000..c3e4fe5f Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-065.webp differ diff --git a/assets/recipes/styles/defaults/SP12-066.webp b/assets/recipes/styles/defaults/SP12-066.webp new file mode 100644 index 00000000..47161ce4 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-066.webp differ diff --git a/assets/recipes/styles/defaults/SP12-067.webp b/assets/recipes/styles/defaults/SP12-067.webp new file mode 100644 index 00000000..c7c0950b Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-067.webp differ diff --git a/assets/recipes/styles/defaults/SP12-068.webp b/assets/recipes/styles/defaults/SP12-068.webp new file mode 100644 index 00000000..2189af51 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-068.webp differ diff --git a/assets/recipes/styles/defaults/SP12-069.webp b/assets/recipes/styles/defaults/SP12-069.webp new file mode 100644 index 00000000..6e65c309 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-069.webp differ diff --git a/assets/recipes/styles/defaults/SP12-070.webp b/assets/recipes/styles/defaults/SP12-070.webp new file mode 100644 index 00000000..7a9ee349 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-070.webp differ diff --git a/assets/recipes/styles/defaults/SP12-071.webp b/assets/recipes/styles/defaults/SP12-071.webp new file mode 100644 index 00000000..075b8ebe Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-071.webp differ diff --git a/assets/recipes/styles/defaults/SP12-072.webp b/assets/recipes/styles/defaults/SP12-072.webp new file mode 100644 index 00000000..cc89e20f Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-072.webp differ diff --git a/assets/recipes/styles/defaults/SP12-073.webp b/assets/recipes/styles/defaults/SP12-073.webp new file mode 100644 index 00000000..8c0d0065 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-073.webp differ diff --git a/assets/recipes/styles/defaults/SP12-074.webp b/assets/recipes/styles/defaults/SP12-074.webp new file mode 100644 index 00000000..ed2b443a Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-074.webp differ diff --git a/assets/recipes/styles/defaults/SP12-075.webp b/assets/recipes/styles/defaults/SP12-075.webp new file mode 100644 index 00000000..f8e652d0 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-075.webp differ diff --git a/assets/recipes/styles/defaults/SP12-076.webp b/assets/recipes/styles/defaults/SP12-076.webp new file mode 100644 index 00000000..86ae2973 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-076.webp differ diff --git a/assets/recipes/styles/defaults/SP12-077.webp b/assets/recipes/styles/defaults/SP12-077.webp new file mode 100644 index 00000000..34a9f424 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-077.webp differ diff --git a/assets/recipes/styles/defaults/SP12-078.webp b/assets/recipes/styles/defaults/SP12-078.webp new file mode 100644 index 00000000..8cd711fc Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-078.webp differ diff --git a/assets/recipes/styles/defaults/SP12-079.webp b/assets/recipes/styles/defaults/SP12-079.webp new file mode 100644 index 00000000..bdfaac1e Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-079.webp differ diff --git a/assets/recipes/styles/defaults/SP12-080.webp b/assets/recipes/styles/defaults/SP12-080.webp new file mode 100644 index 00000000..fbe6cad3 Binary files /dev/null and b/assets/recipes/styles/defaults/SP12-080.webp differ diff --git a/assets/recipes/styles/defaults/SP13-001.webp b/assets/recipes/styles/defaults/SP13-001.webp new file mode 100644 index 00000000..f5161614 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-001.webp differ diff --git a/assets/recipes/styles/defaults/SP13-002.webp b/assets/recipes/styles/defaults/SP13-002.webp new file mode 100644 index 00000000..dfec9142 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-002.webp differ diff --git a/assets/recipes/styles/defaults/SP13-003.webp b/assets/recipes/styles/defaults/SP13-003.webp new file mode 100644 index 00000000..e901b6a6 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-003.webp differ diff --git a/assets/recipes/styles/defaults/SP13-004.webp b/assets/recipes/styles/defaults/SP13-004.webp new file mode 100644 index 00000000..a0ebf04d Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-004.webp differ diff --git a/assets/recipes/styles/defaults/SP13-005.webp b/assets/recipes/styles/defaults/SP13-005.webp new file mode 100644 index 00000000..87a683f1 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-005.webp differ diff --git a/assets/recipes/styles/defaults/SP13-006.webp b/assets/recipes/styles/defaults/SP13-006.webp new file mode 100644 index 00000000..f03535de Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-006.webp differ diff --git a/assets/recipes/styles/defaults/SP13-007.webp b/assets/recipes/styles/defaults/SP13-007.webp new file mode 100644 index 00000000..9c435361 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-007.webp differ diff --git a/assets/recipes/styles/defaults/SP13-008.webp b/assets/recipes/styles/defaults/SP13-008.webp new file mode 100644 index 00000000..0e27b4b5 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-008.webp differ diff --git a/assets/recipes/styles/defaults/SP13-009.webp b/assets/recipes/styles/defaults/SP13-009.webp new file mode 100644 index 00000000..2b884ad6 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-009.webp differ diff --git a/assets/recipes/styles/defaults/SP13-010.webp b/assets/recipes/styles/defaults/SP13-010.webp new file mode 100644 index 00000000..72c9f41b Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-010.webp differ diff --git a/assets/recipes/styles/defaults/SP13-011.webp b/assets/recipes/styles/defaults/SP13-011.webp new file mode 100644 index 00000000..ba609bf9 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-011.webp differ diff --git a/assets/recipes/styles/defaults/SP13-012.webp b/assets/recipes/styles/defaults/SP13-012.webp new file mode 100644 index 00000000..2adcc801 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-012.webp differ diff --git a/assets/recipes/styles/defaults/SP13-013.webp b/assets/recipes/styles/defaults/SP13-013.webp new file mode 100644 index 00000000..5a33d1af Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-013.webp differ diff --git a/assets/recipes/styles/defaults/SP13-014.webp b/assets/recipes/styles/defaults/SP13-014.webp new file mode 100644 index 00000000..48869e92 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-014.webp differ diff --git a/assets/recipes/styles/defaults/SP13-015.webp b/assets/recipes/styles/defaults/SP13-015.webp new file mode 100644 index 00000000..e9fd4866 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-015.webp differ diff --git a/assets/recipes/styles/defaults/SP13-016.webp b/assets/recipes/styles/defaults/SP13-016.webp new file mode 100644 index 00000000..b4395abf Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-016.webp differ diff --git a/assets/recipes/styles/defaults/SP13-017.webp b/assets/recipes/styles/defaults/SP13-017.webp new file mode 100644 index 00000000..7e2e31e3 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-017.webp differ diff --git a/assets/recipes/styles/defaults/SP13-018.webp b/assets/recipes/styles/defaults/SP13-018.webp new file mode 100644 index 00000000..949ddb69 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-018.webp differ diff --git a/assets/recipes/styles/defaults/SP13-019.webp b/assets/recipes/styles/defaults/SP13-019.webp new file mode 100644 index 00000000..717ca05f Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-019.webp differ diff --git a/assets/recipes/styles/defaults/SP13-020.webp b/assets/recipes/styles/defaults/SP13-020.webp new file mode 100644 index 00000000..4112d4f5 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-020.webp differ diff --git a/assets/recipes/styles/defaults/SP13-021.webp b/assets/recipes/styles/defaults/SP13-021.webp new file mode 100644 index 00000000..ad491348 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-021.webp differ diff --git a/assets/recipes/styles/defaults/SP13-022.webp b/assets/recipes/styles/defaults/SP13-022.webp new file mode 100644 index 00000000..0c4082fe Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-022.webp differ diff --git a/assets/recipes/styles/defaults/SP13-023.webp b/assets/recipes/styles/defaults/SP13-023.webp new file mode 100644 index 00000000..024112f6 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-023.webp differ diff --git a/assets/recipes/styles/defaults/SP13-024.webp b/assets/recipes/styles/defaults/SP13-024.webp new file mode 100644 index 00000000..6e056626 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-024.webp differ diff --git a/assets/recipes/styles/defaults/SP13-025.webp b/assets/recipes/styles/defaults/SP13-025.webp new file mode 100644 index 00000000..40d1b948 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-025.webp differ diff --git a/assets/recipes/styles/defaults/SP13-026.webp b/assets/recipes/styles/defaults/SP13-026.webp new file mode 100644 index 00000000..f89a4c9c Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-026.webp differ diff --git a/assets/recipes/styles/defaults/SP13-027.webp b/assets/recipes/styles/defaults/SP13-027.webp new file mode 100644 index 00000000..2ea3416a Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-027.webp differ diff --git a/assets/recipes/styles/defaults/SP13-028.webp b/assets/recipes/styles/defaults/SP13-028.webp new file mode 100644 index 00000000..ed4f5890 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-028.webp differ diff --git a/assets/recipes/styles/defaults/SP13-029.webp b/assets/recipes/styles/defaults/SP13-029.webp new file mode 100644 index 00000000..4e614f4c Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-029.webp differ diff --git a/assets/recipes/styles/defaults/SP13-030.webp b/assets/recipes/styles/defaults/SP13-030.webp new file mode 100644 index 00000000..04f98045 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-030.webp differ diff --git a/assets/recipes/styles/defaults/SP13-031.webp b/assets/recipes/styles/defaults/SP13-031.webp new file mode 100644 index 00000000..6cef154b Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-031.webp differ diff --git a/assets/recipes/styles/defaults/SP13-032.webp b/assets/recipes/styles/defaults/SP13-032.webp new file mode 100644 index 00000000..55ac3d73 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-032.webp differ diff --git a/assets/recipes/styles/defaults/SP13-033.webp b/assets/recipes/styles/defaults/SP13-033.webp new file mode 100644 index 00000000..006eac7b Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-033.webp differ diff --git a/assets/recipes/styles/defaults/SP13-034.webp b/assets/recipes/styles/defaults/SP13-034.webp new file mode 100644 index 00000000..e2187bda Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-034.webp differ diff --git a/assets/recipes/styles/defaults/SP13-035.webp b/assets/recipes/styles/defaults/SP13-035.webp new file mode 100644 index 00000000..58ccfb92 Binary files /dev/null and b/assets/recipes/styles/defaults/SP13-035.webp differ diff --git a/assets/recipes/styles/defaults/SP14-001.webp b/assets/recipes/styles/defaults/SP14-001.webp new file mode 100644 index 00000000..37db9e85 Binary files /dev/null and b/assets/recipes/styles/defaults/SP14-001.webp differ diff --git a/assets/recipes/styles/defaults/SP14-002.webp b/assets/recipes/styles/defaults/SP14-002.webp new file mode 100644 index 00000000..8d934e5b Binary files /dev/null and b/assets/recipes/styles/defaults/SP14-002.webp differ diff --git a/assets/recipes/styles/defaults/SP14-003.webp b/assets/recipes/styles/defaults/SP14-003.webp new file mode 100644 index 00000000..9d48161e Binary files /dev/null and b/assets/recipes/styles/defaults/SP14-003.webp differ diff --git a/assets/recipes/styles/defaults/SP14-004.webp b/assets/recipes/styles/defaults/SP14-004.webp new file mode 100644 index 00000000..c04ecf22 Binary files /dev/null and b/assets/recipes/styles/defaults/SP14-004.webp differ diff --git a/assets/recipes/styles/defaults/SP14-005.webp b/assets/recipes/styles/defaults/SP14-005.webp new file mode 100644 index 00000000..52ac6daa Binary files /dev/null and b/assets/recipes/styles/defaults/SP14-005.webp differ diff --git a/assets/recipes/styles/defaults/SP14-006.webp b/assets/recipes/styles/defaults/SP14-006.webp new file mode 100644 index 00000000..9695a8d9 Binary files /dev/null and b/assets/recipes/styles/defaults/SP14-006.webp differ diff --git a/assets/recipes/styles/defaults/SP14-007.webp b/assets/recipes/styles/defaults/SP14-007.webp new file mode 100644 index 00000000..49463502 Binary files /dev/null and b/assets/recipes/styles/defaults/SP14-007.webp differ diff --git a/assets/recipes/styles/defaults/SP14-008.webp b/assets/recipes/styles/defaults/SP14-008.webp new file mode 100644 index 00000000..c5f1b473 Binary files /dev/null and b/assets/recipes/styles/defaults/SP14-008.webp differ diff --git a/assets/recipes/styles/defaults/SP15-001.webp b/assets/recipes/styles/defaults/SP15-001.webp new file mode 100644 index 00000000..7c8501cf Binary files /dev/null and b/assets/recipes/styles/defaults/SP15-001.webp differ diff --git a/assets/recipes/styles/defaults/SP15-002.webp b/assets/recipes/styles/defaults/SP15-002.webp new file mode 100644 index 00000000..62840027 Binary files /dev/null and b/assets/recipes/styles/defaults/SP15-002.webp differ diff --git a/assets/recipes/styles/defaults/SP15-003.webp b/assets/recipes/styles/defaults/SP15-003.webp new file mode 100644 index 00000000..bbc6648c Binary files /dev/null and b/assets/recipes/styles/defaults/SP15-003.webp differ diff --git a/assets/recipes/styles/defaults/SP15-004.webp b/assets/recipes/styles/defaults/SP15-004.webp new file mode 100644 index 00000000..0f631d1a Binary files /dev/null and b/assets/recipes/styles/defaults/SP15-004.webp differ diff --git a/assets/recipes/styles/defaults/SP15-005.webp b/assets/recipes/styles/defaults/SP15-005.webp new file mode 100644 index 00000000..1f19ca45 Binary files /dev/null and b/assets/recipes/styles/defaults/SP15-005.webp differ diff --git a/assets/recipes/styles/defaults/SP15-006.webp b/assets/recipes/styles/defaults/SP15-006.webp new file mode 100644 index 00000000..4c6d05d0 Binary files /dev/null and b/assets/recipes/styles/defaults/SP15-006.webp differ diff --git a/assets/recipes/styles/defaults/SP15-007.webp b/assets/recipes/styles/defaults/SP15-007.webp new file mode 100644 index 00000000..f74df5ee Binary files /dev/null and b/assets/recipes/styles/defaults/SP15-007.webp differ diff --git a/assets/recipes/styles/defaults/SP15-008.webp b/assets/recipes/styles/defaults/SP15-008.webp new file mode 100644 index 00000000..3d314d2d Binary files /dev/null and b/assets/recipes/styles/defaults/SP15-008.webp differ diff --git a/assets/recipes/styles/defaults/failures-pack_12.json b/assets/recipes/styles/defaults/failures-pack_12.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/assets/recipes/styles/defaults/failures-pack_12.json @@ -0,0 +1 @@ +[] diff --git a/assets/recipes/styles/defaults/failures-pack_13.json b/assets/recipes/styles/defaults/failures-pack_13.json new file mode 100644 index 00000000..3d575964 --- /dev/null +++ b/assets/recipes/styles/defaults/failures-pack_13.json @@ -0,0 +1,182 @@ +[ + { + "presetId": "SP13-027", + "presetName": "Crimson Clan Battlefield", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "3. Samurais & Medieval", + "error": "GENERIC_PRESET_MOTIFS is not defined", + "failedAt": "2026-05-28T15:46:08.351Z" + }, + { + "presetId": "SP13-028", + "presetName": "Medieval Knight Oath", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "3. Samurais & Medieval", + "error": "GENERIC_PRESET_MOTIFS is not defined", + "failedAt": "2026-05-28T15:46:08.351Z" + }, + { + "presetId": "SP13-029", + "presetName": "Castle Siege Emberstorm", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "3. Samurais & Medieval", + "error": "GENERIC_PRESET_MOTIFS is not defined", + "failedAt": "2026-05-28T15:46:08.352Z" + }, + { + "presetId": "SP13-030", + "presetName": "Blademonk Moon Courtyard", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "3. Samurais & Medieval", + "error": "GENERIC_PRESET_MOTIFS is not defined", + "failedAt": "2026-05-28T15:46:08.352Z" + }, + { + "presetId": "SP13-031", + "presetName": "Haunted School Corridor", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "4. Horror", + "error": "GENERIC_PRESET_MOTIFS is not defined", + "failedAt": "2026-05-28T15:46:08.352Z" + }, + { + "presetId": "SP13-032", + "presetName": "Crimson Moon Apparition", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "4. Horror", + "error": "GENERIC_PRESET_MOTIFS is not defined", + "failedAt": "2026-05-28T15:46:08.352Z" + }, + { + "presetId": "SP13-033", + "presetName": "Flesh Puppet Theater", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "4. Horror", + "error": "GENERIC_PRESET_MOTIFS is not defined", + "failedAt": "2026-05-28T15:46:08.352Z" + }, + { + "presetId": "SP13-034", + "presetName": "Deep Well Whisper", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "4. Horror", + "error": "GENERIC_PRESET_MOTIFS is not defined", + "failedAt": "2026-05-28T15:46:08.353Z" + }, + { + "presetId": "SP13-035", + "presetName": "Oni Mask Ritual Night", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "4. Horror", + "error": "GENERIC_PRESET_MOTIFS is not defined", + "failedAt": "2026-05-28T15:46:08.353Z" + }, + { + "presetId": "SP13-003", + "presetName": "Soft Shojo Spring", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "error": "Job 6cc2a7e1-ad61-4fbc-aef4-5a164ae65f5e ended as failed: Timed out waiting for Codex notification", + "failedAt": "2026-05-28T16:20:19.443Z" + }, + { + "presetId": "SP13-004", + "presetName": "Mecha Hangar Ignition", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "error": "Job 01590fff-c624-4436-85ff-a866bac29428 ended as failed: Codex app-server socket is not open", + "failedAt": "2026-05-28T16:31:09.566Z" + }, + { + "presetId": "SP13-006", + "presetName": "Spirit Shrine Twilight", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "error": "Unable to connect. Is the computer able to access the url?", + "failedAt": "2026-05-28T16:36:32.474Z" + }, + { + "presetId": "SP13-007", + "presetName": "Sports Climax Arena", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "error": "Unable to connect. Is the computer able to access the url?", + "failedAt": "2026-05-28T16:38:27.793Z" + }, + { + "presetId": "SP13-008", + "presetName": "Gothic Vampire Manor", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "error": "Unable to connect. Is the computer able to access the url?", + "failedAt": "2026-05-28T16:40:23.044Z" + }, + { + "presetId": "SP13-009", + "presetName": "Magical Girl Prism Burst", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "error": "Unable to connect. Is the computer able to access the url?", + "failedAt": "2026-05-28T16:42:18.339Z" + }, + { + "presetId": "SP13-010", + "presetName": "Isekai Forest Caravan", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "error": "Unable to connect. Is the computer able to access the url?", + "failedAt": "2026-05-28T16:44:13.616Z" + }, + { + "presetId": "SP13-011", + "presetName": "Meikyuu Dungeon Glow", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "error": "Unable to connect. Is the computer able to access the url?", + "failedAt": "2026-05-28T16:46:08.875Z" + }, + { + "presetId": "SP13-012", + "presetName": "Retro Mecha VHS Grain", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "error": "Unable to connect. Is the computer able to access the url?", + "failedAt": "2026-05-28T16:48:04.123Z" + }, + { + "presetId": "SP13-013", + "presetName": "Rainy Idol Backstage", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "error": "Unable to connect. Is the computer able to access the url?", + "failedAt": "2026-05-28T16:49:59.363Z" + }, + { + "presetId": "SP13-014", + "presetName": "Ronin Alley Duel", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "error": "Unable to connect. Is the computer able to access the url?", + "failedAt": "2026-05-28T16:51:54.640Z" + } +] diff --git a/assets/recipes/styles/defaults/failures-pack_14.json b/assets/recipes/styles/defaults/failures-pack_14.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/assets/recipes/styles/defaults/failures-pack_14.json @@ -0,0 +1 @@ +[] diff --git a/assets/recipes/styles/defaults/failures-pack_15.json b/assets/recipes/styles/defaults/failures-pack_15.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/assets/recipes/styles/defaults/failures-pack_15.json @@ -0,0 +1 @@ +[] diff --git a/assets/recipes/styles/defaults/manifest-pack_06.json b/assets/recipes/styles/defaults/manifest-pack_06.json index 929ac6e6..dd96d79d 100644 --- a/assets/recipes/styles/defaults/manifest-pack_06.json +++ b/assets/recipes/styles/defaults/manifest-pack_06.json @@ -1384,5 +1384,243 @@ "model": "gpt-5.4-mini", "reasoningEffort": "low", "generatedAt": "2026-05-24T01:01:34.575Z" + }, + { + "presetId": "SP06-104", + "presetName": "Cyberpunk HUD Interface", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-104.webp", + "jobId": "d0d42fa8-cd62-4605-b6cc-412429dffea3", + "sourceAsset": "assets/recipes/styles/defaults/SP06-104.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T21:42:52.654Z" + }, + { + "presetId": "SP06-105", + "presetName": "Retro Fighting Game Portrait", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-105.webp", + "jobId": "e0012211-613a-4869-8b90-f917cab2f88b", + "sourceAsset": "assets/recipes/styles/defaults/SP06-105.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T21:45:23.232Z" + }, + { + "presetId": "SP06-106", + "presetName": "Isometric Strategy Terrain", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-106.webp", + "jobId": "7ce15a8d-a3ad-4231-846f-6e7b4668b402", + "sourceAsset": "assets/recipes/styles/defaults/SP06-106.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T21:46:27.314Z" + }, + { + "presetId": "SP06-107", + "presetName": "MOBA Splash Art Heroic", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-107.webp", + "jobId": "747537fa-cd91-4245-a712-799faab86e4c", + "sourceAsset": "assets/recipes/styles/defaults/SP06-107.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T21:49:02.544Z" + }, + { + "presetId": "SP06-108", + "presetName": "Visual Novel Neon Classroom", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-108.webp", + "jobId": "5293f766-5c19-43d0-9755-472b1acf6be8", + "sourceAsset": "assets/recipes/styles/defaults/SP06-108.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T21:50:15.275Z" + }, + { + "presetId": "SP06-109", + "presetName": "Soulslike Dark Citadel", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-109.webp", + "jobId": "a528df0d-d254-4314-8f19-d0a5b045b078", + "sourceAsset": "assets/recipes/styles/defaults/SP06-109.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T21:52:18.926Z" + }, + { + "presetId": "SP06-110", + "presetName": "Chibi Platformer Character Sheet", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-110.webp", + "jobId": "6093d19f-8a33-47c5-8f78-065a16aa8e93", + "sourceAsset": "assets/recipes/styles/defaults/SP06-110.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T21:53:39.996Z" + }, + { + "presetId": "SP06-111", + "presetName": "Battle Royale Storm Arena", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-111.webp", + "jobId": "00b22581-a7d7-4a83-853c-9a0dc436f733", + "sourceAsset": "assets/recipes/styles/defaults/SP06-111.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T21:54:55.384Z" + }, + { + "presetId": "SP06-112", + "presetName": "Sci Fi Shooter Weapon Icons", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-112.webp", + "jobId": "20be3676-a525-4344-ab50-8e793ecb9741", + "sourceAsset": "assets/recipes/styles/defaults/SP06-112.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T21:56:09.661Z" + }, + { + "presetId": "SP06-113", + "presetName": "Fantasy MMO World Map", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-113.webp", + "jobId": "58e1e2eb-d7c5-4e35-bafb-8a06e5ebd751", + "sourceAsset": "assets/recipes/styles/defaults/SP06-113.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T21:57:31.451Z" + }, + { + "presetId": "SP06-114", + "presetName": "Anime Gacha Card Frame", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-114.webp", + "jobId": "8fb45fc2-a0de-42cf-a9ec-744076d1e49d", + "sourceAsset": "assets/recipes/styles/defaults/SP06-114.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T21:58:50.346Z" + }, + { + "presetId": "SP06-115", + "presetName": "Horror Survival Safe Room", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-115.webp", + "jobId": "16a3582f-c745-402e-b191-45b33d31913f", + "sourceAsset": "assets/recipes/styles/defaults/SP06-115.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:00:28.231Z" + }, + { + "presetId": "SP06-116", + "presetName": "Stealth Game Night City Rooftops", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-116.webp", + "jobId": "eac84d9e-7fec-425c-9282-b464a2866256", + "sourceAsset": "assets/recipes/styles/defaults/SP06-116.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:01:44.000Z" + }, + { + "presetId": "SP06-117", + "presetName": "Arcade Racing Track Neon", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-117.webp", + "jobId": "16d05436-275f-494c-b658-36f973bd46b0", + "sourceAsset": "assets/recipes/styles/defaults/SP06-117.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:03:15.288Z" + }, + { + "presetId": "SP06-118", + "presetName": "RPG Item Pack Pixel Icons", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "1. Videojuegos", + "file": "assets/recipes/styles/defaults/SP06-118.webp", + "jobId": "41553cec-c03d-439d-b63d-2d3dd2bff808", + "sourceAsset": "assets/recipes/styles/defaults/SP06-118.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:04:59.194Z" + }, + { + "presetId": "SP06-119", + "presetName": "Cozy Farm Sim Seasonal Pack", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "7. Game Art Directions & UI", + "file": "assets/recipes/styles/defaults/SP06-119.webp", + "jobId": "dfe97f36-bb3b-48ee-b9e9-cb3efadb3ceb", + "sourceAsset": "assets/recipes/styles/defaults/SP06-119.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:20:07.549Z" + }, + { + "presetId": "SP06-120", + "presetName": "Boss Encounter Cinematic Key Art", + "packId": "pack_06", + "packName": "Essential Art Styles", + "category": "7. Game Art Directions & UI", + "file": "assets/recipes/styles/defaults/SP06-120.webp", + "jobId": "b72803ef-e02c-46d8-a0cc-7a7391506986", + "sourceAsset": "assets/recipes/styles/defaults/SP06-120.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:21:08.345Z" } ] diff --git a/assets/recipes/styles/defaults/manifest-pack_12.json b/assets/recipes/styles/defaults/manifest-pack_12.json new file mode 100644 index 00000000..a3e393a5 --- /dev/null +++ b/assets/recipes/styles/defaults/manifest-pack_12.json @@ -0,0 +1,1122 @@ +[ + { + "presetId": "SP12-001", + "presetName": "Neon Samurai District", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "1. Neon Urban & Night Ops", + "file": "assets/recipes/styles/defaults/SP12-001.webp", + "jobId": "recovered-local-file", + "sourceAsset": "assets/recipes/styles/defaults/SP12-001.webp", + "generationMode": "text-to-image", + "model": "recovered-local-file", + "reasoningEffort": "unknown", + "generatedAt": "2026-05-28T21:05:13.796Z" + }, + { + "presetId": "SP12-002", + "presetName": "Bioluminescent Jungle Raid", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "6. Wilderness Hunts & Harsh Frontiers", + "file": "assets/recipes/styles/defaults/SP12-002.webp", + "jobId": "recovered-local-file", + "sourceAsset": "assets/recipes/styles/defaults/SP12-002.webp", + "generationMode": "text-to-image", + "model": "recovered-local-file", + "reasoningEffort": "unknown", + "generatedAt": "2026-05-28T21:09:54.478Z" + }, + { + "presetId": "SP12-003", + "presetName": "Desert Mech Convoy", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "3. Sci-Fi Frontiers & Mech Zones", + "file": "assets/recipes/styles/defaults/SP12-003.webp", + "jobId": "recovered-local-file", + "sourceAsset": "assets/recipes/styles/defaults/SP12-003.webp", + "generationMode": "text-to-image", + "model": "recovered-local-file", + "reasoningEffort": "unknown", + "generatedAt": "2026-05-28T21:22:55.806Z" + }, + { + "presetId": "SP12-004", + "presetName": "Clockwork Sky Armada", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "8. Puzzle Chambers & Adventure Setpieces", + "file": "assets/recipes/styles/defaults/SP12-004.webp", + "jobId": "9e22ad61-f49f-42cf-a982-f679d1a38fb3", + "sourceAsset": "assets/recipes/styles/defaults/SP12-004.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T21:47:17.095Z" + }, + { + "presetId": "SP12-005", + "presetName": "Moonbase Breach Alarm", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "3. Sci-Fi Frontiers & Mech Zones", + "file": "assets/recipes/styles/defaults/SP12-005.webp", + "jobId": "1ece5b9e-07f8-4030-b7c3-818af06e3a22", + "sourceAsset": "assets/recipes/styles/defaults/SP12-005.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:20:12.460Z" + }, + { + "presetId": "SP12-006", + "presetName": "Arcane Library Boss Arena", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "2. Arcane Temples & Mythic Realms", + "file": "assets/recipes/styles/defaults/SP12-006.webp", + "jobId": "f604a92d-04e5-4203-a1c3-13519762f718", + "sourceAsset": "assets/recipes/styles/defaults/SP12-006.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:21:30.154Z" + }, + { + "presetId": "SP12-007", + "presetName": "Glacier Fortress Assault", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "4. Sieges, Warfronts & Last Stands", + "file": "assets/recipes/styles/defaults/SP12-007.webp", + "jobId": "f300d0b8-c74c-4fba-a4a3-419528d8bc61", + "sourceAsset": "assets/recipes/styles/defaults/SP12-007.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:22:39.033Z" + }, + { + "presetId": "SP12-008", + "presetName": "Holographic Grand Prix Night", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "5. Speed, Sport & Competitive Arenas", + "file": "assets/recipes/styles/defaults/SP12-008.webp", + "jobId": "30bf0639-cc44-496c-a86d-3b7365ee24a1", + "sourceAsset": "assets/recipes/styles/defaults/SP12-008.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:24:29.863Z" + }, + { + "presetId": "SP12-009", + "presetName": "Ruined Cathedral Co-op Siege", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "4. Sieges, Warfronts & Last Stands", + "file": "assets/recipes/styles/defaults/SP12-009.webp", + "jobId": "29436538-0b51-443b-acba-7757e1914fa3", + "sourceAsset": "assets/recipes/styles/defaults/SP12-009.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:25:37.282Z" + }, + { + "presetId": "SP12-010", + "presetName": "Volcanic Forge Dungeon", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "6. Wilderness Hunts & Harsh Frontiers", + "file": "assets/recipes/styles/defaults/SP12-010.webp", + "jobId": "e3316c36-c889-40fc-99af-bff2331d2604", + "sourceAsset": "assets/recipes/styles/defaults/SP12-010.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:26:50.956Z" + }, + { + "presetId": "SP12-011", + "presetName": "Underwater Research Collapse", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "3. Sci-Fi Frontiers & Mech Zones", + "file": "assets/recipes/styles/defaults/SP12-011.webp", + "jobId": "ec9b7aba-6a85-41d8-b5dd-8189c6112f35", + "sourceAsset": "assets/recipes/styles/defaults/SP12-011.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:27:58.701Z" + }, + { + "presetId": "SP12-012", + "presetName": "Pixel Tavern Quest Hub", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "8. Puzzle Chambers & Adventure Setpieces", + "file": "assets/recipes/styles/defaults/SP12-012.webp", + "jobId": "ca2bdf26-fccc-45bd-a808-0a6667cbe3af", + "sourceAsset": "assets/recipes/styles/defaults/SP12-012.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:29:19.747Z" + }, + { + "presetId": "SP12-013", + "presetName": "Crystal Desert Shrine", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "2. Arcane Temples & Mythic Realms", + "file": "assets/recipes/styles/defaults/SP12-013.webp", + "jobId": "0fa0b662-7dc2-43b0-91ce-1518051a21fd", + "sourceAsset": "assets/recipes/styles/defaults/SP12-013.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:30:35.798Z" + }, + { + "presetId": "SP12-014", + "presetName": "Urban Parkour Rooftop Wars", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "1. Neon Urban & Night Ops", + "file": "assets/recipes/styles/defaults/SP12-014.webp", + "jobId": "9d6823e8-9729-40d0-8423-f5e38e62ac81", + "sourceAsset": "assets/recipes/styles/defaults/SP12-014.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:31:57.394Z" + }, + { + "presetId": "SP12-015", + "presetName": "Ancient Mecha Temple", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "2. Arcane Temples & Mythic Realms", + "file": "assets/recipes/styles/defaults/SP12-015.webp", + "jobId": "8defca20-4d8b-48d7-ba51-6f4fb2ce2136", + "sourceAsset": "assets/recipes/styles/defaults/SP12-015.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:33:29.352Z" + }, + { + "presetId": "SP12-016", + "presetName": "Shadow Opera Assassin Court", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "7. Heists, Horror & Underworld Runs", + "file": "assets/recipes/styles/defaults/SP12-016.webp", + "jobId": "674018f8-2d23-4dd2-a665-432dfefc3bfa", + "sourceAsset": "assets/recipes/styles/defaults/SP12-016.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:35:00.307Z" + }, + { + "presetId": "SP12-017", + "presetName": "Lava Skate Arena", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "5. Speed, Sport & Competitive Arenas", + "file": "assets/recipes/styles/defaults/SP12-017.webp", + "jobId": "9d144994-e503-4327-bb04-845fbbcc5c6a", + "sourceAsset": "assets/recipes/styles/defaults/SP12-017.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:37:52.498Z" + }, + { + "presetId": "SP12-018", + "presetName": "Storm Citadel Defense Grid", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "3. Sci-Fi Frontiers & Mech Zones", + "file": "assets/recipes/styles/defaults/SP12-018.webp", + "jobId": "7246fbf4-51a4-41d3-a271-cc989a18cdde", + "sourceAsset": "assets/recipes/styles/defaults/SP12-018.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:39:22.861Z" + }, + { + "presetId": "SP12-019", + "presetName": "Forgotten Subway Mutation Zone", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "7. Heists, Horror & Underworld Runs", + "file": "assets/recipes/styles/defaults/SP12-019.webp", + "jobId": "f9870823-968e-4315-8d3a-3d4839d7cb64", + "sourceAsset": "assets/recipes/styles/defaults/SP12-019.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:42:02.517Z" + }, + { + "presetId": "SP12-020", + "presetName": "Celestial Harbor Trade Wars", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "2. Arcane Temples & Mythic Realms", + "file": "assets/recipes/styles/defaults/SP12-020.webp", + "jobId": "21ede8c0-01ac-4a90-8a71-ffee299901ec", + "sourceAsset": "assets/recipes/styles/defaults/SP12-020.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:44:43.042Z" + }, + { + "presetId": "SP12-021", + "presetName": "Drift Kingdom Sandstorm Cup", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "5. Speed, Sport & Competitive Arenas", + "file": "assets/recipes/styles/defaults/SP12-021.webp", + "jobId": "c1c15fdc-11a8-43c1-a703-e4b30436cc09", + "sourceAsset": "assets/recipes/styles/defaults/SP12-021.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:46:06.011Z" + }, + { + "presetId": "SP12-022", + "presetName": "Frozen Bazaar Survival Night", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "6. Wilderness Hunts & Harsh Frontiers", + "file": "assets/recipes/styles/defaults/SP12-022.webp", + "jobId": "582ab2dc-c4d7-4782-afa3-b760f2eb1340", + "sourceAsset": "assets/recipes/styles/defaults/SP12-022.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:47:47.474Z" + }, + { + "presetId": "SP12-023", + "presetName": "Orbital Garden Colony Builder", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "3. Sci-Fi Frontiers & Mech Zones", + "file": "assets/recipes/styles/defaults/SP12-023.webp", + "jobId": "9ebc2a11-5ec3-4f6e-90e2-2e2cafa2a716", + "sourceAsset": "assets/recipes/styles/defaults/SP12-023.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:49:14.356Z" + }, + { + "presetId": "SP12-024", + "presetName": "Temple Runner Trap Gauntlet", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "7. Heists, Horror & Underworld Runs", + "file": "assets/recipes/styles/defaults/SP12-024.webp", + "jobId": "ca865230-2fdf-4dac-a43e-0c62e35ea640", + "sourceAsset": "assets/recipes/styles/defaults/SP12-024.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:50:46.324Z" + }, + { + "presetId": "SP12-025", + "presetName": "Neon Underpass Brawler", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "1. Neon Urban & Night Ops", + "file": "assets/recipes/styles/defaults/SP12-025.webp", + "jobId": "8185c700-019d-483d-867a-042cac3a88fe", + "sourceAsset": "assets/recipes/styles/defaults/SP12-025.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:52:21.518Z" + }, + { + "presetId": "SP12-026", + "presetName": "Verdant Ruins Tactical RPG", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "4. Sieges, Warfronts & Last Stands", + "file": "assets/recipes/styles/defaults/SP12-026.webp", + "jobId": "7448aa1f-917d-4a67-b20d-a22cf69f5e14", + "sourceAsset": "assets/recipes/styles/defaults/SP12-026.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:53:55.804Z" + }, + { + "presetId": "SP12-027", + "presetName": "Deep Mine Co-op Extraction", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "6. Wilderness Hunts & Harsh Frontiers", + "file": "assets/recipes/styles/defaults/SP12-027.webp", + "jobId": "5ab2e828-276d-4471-acbd-8c40bedde26d", + "sourceAsset": "assets/recipes/styles/defaults/SP12-027.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:55:33.746Z" + }, + { + "presetId": "SP12-028", + "presetName": "Sky Monastery Duel", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "2. Arcane Temples & Mythic Realms", + "file": "assets/recipes/styles/defaults/SP12-028.webp", + "jobId": "d373f98d-d425-4870-925e-913eed75239c", + "sourceAsset": "assets/recipes/styles/defaults/SP12-028.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T22:57:17.359Z" + }, + { + "presetId": "SP12-029", + "presetName": "Coral Reef Underkingdom", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "6. Wilderness Hunts & Harsh Frontiers", + "file": "assets/recipes/styles/defaults/SP12-029.webp", + "jobId": "d767d8fe-b28d-464c-9ceb-93331e3bd6c9", + "sourceAsset": "assets/recipes/styles/defaults/SP12-029.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:00:48.683Z" + }, + { + "presetId": "SP12-030", + "presetName": "Cursed Carnival Showdown", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "7. Heists, Horror & Underworld Runs", + "file": "assets/recipes/styles/defaults/SP12-030.webp", + "jobId": "f42320fc-9dc1-4af5-9372-65f2116a2d70", + "sourceAsset": "assets/recipes/styles/defaults/SP12-030.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:03:26.134Z" + }, + { + "presetId": "SP12-031", + "presetName": "Astral Chess Battlefield", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "5. Speed, Sport & Competitive Arenas", + "file": "assets/recipes/styles/defaults/SP12-031.webp", + "jobId": "3239179e-3c3d-438a-8e56-0509b3c3486f", + "sourceAsset": "assets/recipes/styles/defaults/SP12-031.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:05:15.714Z" + }, + { + "presetId": "SP12-032", + "presetName": "Harbor Smuggler Night Heist", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "1. Neon Urban & Night Ops", + "file": "assets/recipes/styles/defaults/SP12-032.webp", + "jobId": "4d30f700-b69c-49c1-b165-3a69d36e8130", + "sourceAsset": "assets/recipes/styles/defaults/SP12-032.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:07:16.993Z" + }, + { + "presetId": "SP12-033", + "presetName": "Robot Orchard Defense", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "3. Sci-Fi Frontiers & Mech Zones", + "file": "assets/recipes/styles/defaults/SP12-033.webp", + "jobId": "1fa4e509-60fd-4a03-9440-43249800ee4c", + "sourceAsset": "assets/recipes/styles/defaults/SP12-033.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:09:16.708Z" + }, + { + "presetId": "SP12-034", + "presetName": "Crimson Canyon Sniper Run", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "6. Wilderness Hunts & Harsh Frontiers", + "file": "assets/recipes/styles/defaults/SP12-034.webp", + "jobId": "379dd4d0-96b3-4d1f-b634-cc4fcf9b4458", + "sourceAsset": "assets/recipes/styles/defaults/SP12-034.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:11:05.049Z" + }, + { + "presetId": "SP12-035", + "presetName": "Mythic Train Defense", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "4. Sieges, Warfronts & Last Stands", + "file": "assets/recipes/styles/defaults/SP12-035.webp", + "jobId": "21b14bcd-d9a3-444c-98d8-5f09e79952e3", + "sourceAsset": "assets/recipes/styles/defaults/SP12-035.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:13:15.377Z" + }, + { + "presetId": "SP12-036", + "presetName": "Lunar Monolith Puzzle Chamber", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "8. Puzzle Chambers & Adventure Setpieces", + "file": "assets/recipes/styles/defaults/SP12-036.webp", + "jobId": "6392bc3d-ece0-4ee3-8b6c-d88e50f08dd4", + "sourceAsset": "assets/recipes/styles/defaults/SP12-036.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:14:27.295Z" + }, + { + "presetId": "SP12-037", + "presetName": "Mushroom Kingdom Frontier", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "8. Puzzle Chambers & Adventure Setpieces", + "file": "assets/recipes/styles/defaults/SP12-037.webp", + "jobId": "6563dc99-3b02-45cc-9313-fe3dd484a4bf", + "sourceAsset": "assets/recipes/styles/defaults/SP12-037.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:16:02.997Z" + }, + { + "presetId": "SP12-038", + "presetName": "Iron Reef Naval Skirmish", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "4. Sieges, Warfronts & Last Stands", + "file": "assets/recipes/styles/defaults/SP12-038.webp", + "jobId": "50771243-9919-42a1-bae8-d3a7434cf6e3", + "sourceAsset": "assets/recipes/styles/defaults/SP12-038.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:17:34.607Z" + }, + { + "presetId": "SP12-039", + "presetName": "Phantom Theater Rhythm Battle", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "5. Speed, Sport & Competitive Arenas", + "file": "assets/recipes/styles/defaults/SP12-039.webp", + "jobId": "380ec3fb-f6cb-49b5-8460-a0e4d37d0a76", + "sourceAsset": "assets/recipes/styles/defaults/SP12-039.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:18:41.833Z" + }, + { + "presetId": "SP12-040", + "presetName": "Thunder Plains Beast Hunt", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "6. Wilderness Hunts & Harsh Frontiers", + "file": "assets/recipes/styles/defaults/SP12-040.webp", + "jobId": "f1c312ee-800b-49a6-b563-f229e488c32a", + "sourceAsset": "assets/recipes/styles/defaults/SP12-040.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:19:46.521Z" + }, + { + "presetId": "SP12-041", + "presetName": "Emberwood Ranger Outpost", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "6. Wilderness Hunts & Harsh Frontiers", + "file": "assets/recipes/styles/defaults/SP12-041.webp", + "jobId": "768af0ed-ce3c-4a3c-b7f2-12c8be9e41e1", + "sourceAsset": "assets/recipes/styles/defaults/SP12-041.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:20:59.126Z" + }, + { + "presetId": "SP12-042", + "presetName": "Quantum Laboratory Rift", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "3. Sci-Fi Frontiers & Mech Zones", + "file": "assets/recipes/styles/defaults/SP12-042.webp", + "jobId": "9ef64142-75d3-47b3-b928-f27fc94ed3e7", + "sourceAsset": "assets/recipes/styles/defaults/SP12-042.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:22:12.254Z" + }, + { + "presetId": "SP12-043", + "presetName": "Harbor Kaiju Evacuation", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "1. Neon Urban & Night Ops", + "file": "assets/recipes/styles/defaults/SP12-043.webp", + "jobId": "d21c76d3-efe3-4d25-8ec3-f9b39b731061", + "sourceAsset": "assets/recipes/styles/defaults/SP12-043.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:23:45.673Z" + }, + { + "presetId": "SP12-044", + "presetName": "Mirage Palace Stealth Gala", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "2. Arcane Temples & Mythic Realms", + "file": "assets/recipes/styles/defaults/SP12-044.webp", + "jobId": "06cac210-0607-4038-a331-52d8ca9b95b3", + "sourceAsset": "assets/recipes/styles/defaults/SP12-044.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:26:21.557Z" + }, + { + "presetId": "SP12-045", + "presetName": "Alloy Forest Mech Hunt", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "3. Sci-Fi Frontiers & Mech Zones", + "file": "assets/recipes/styles/defaults/SP12-045.webp", + "jobId": "dfc404e9-566e-4860-b8d4-320e798695e4", + "sourceAsset": "assets/recipes/styles/defaults/SP12-045.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:27:32.791Z" + }, + { + "presetId": "SP12-046", + "presetName": "Solar Rail Nomad Camp", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "3. Sci-Fi Frontiers & Mech Zones", + "file": "assets/recipes/styles/defaults/SP12-046.webp", + "jobId": "ca670253-468e-459a-a19e-a641998c663d", + "sourceAsset": "assets/recipes/styles/defaults/SP12-046.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:28:54.478Z" + }, + { + "presetId": "SP12-047", + "presetName": "Obsidian Arena Champion Trial", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "5. Speed, Sport & Competitive Arenas", + "file": "assets/recipes/styles/defaults/SP12-047.webp", + "jobId": "24eea74f-74a2-4dd7-90bf-9c963ab8bcce", + "sourceAsset": "assets/recipes/styles/defaults/SP12-047.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:30:10.320Z" + }, + { + "presetId": "SP12-048", + "presetName": "Crystal Metro Hoverline", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "1. Neon Urban & Night Ops", + "file": "assets/recipes/styles/defaults/SP12-048.webp", + "jobId": "a0d41408-daf4-4fc0-bcd7-8fd1538e900e", + "sourceAsset": "assets/recipes/styles/defaults/SP12-048.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:31:41.319Z" + }, + { + "presetId": "SP12-049", + "presetName": "Thorn Castle Moon Raid", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "7. Heists, Horror & Underworld Runs", + "file": "assets/recipes/styles/defaults/SP12-049.webp", + "jobId": "8f68c6f4-1498-4465-9968-f9c6ad50737a", + "sourceAsset": "assets/recipes/styles/defaults/SP12-049.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:33:06.599Z" + }, + { + "presetId": "SP12-050", + "presetName": "Polar Signal Tower Outbreak", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "7. Heists, Horror & Underworld Runs", + "file": "assets/recipes/styles/defaults/SP12-050.webp", + "jobId": "9b827773-327d-4c83-947a-b1a5d9849c81", + "sourceAsset": "assets/recipes/styles/defaults/SP12-050.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:34:29.617Z" + }, + { + "presetId": "SP12-051", + "presetName": "Sapphire Bazaar Deckbuilder Hub", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "8. Puzzle Chambers & Adventure Setpieces", + "file": "assets/recipes/styles/defaults/SP12-051.webp", + "jobId": "dff9a443-c98e-49fc-821a-de521b38deab", + "sourceAsset": "assets/recipes/styles/defaults/SP12-051.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:37:10.727Z" + }, + { + "presetId": "SP12-052", + "presetName": "Rift Bridge Capture Point", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "4. Sieges, Warfronts & Last Stands", + "file": "assets/recipes/styles/defaults/SP12-052.webp", + "jobId": "02bfc4e7-de8d-4468-9546-f80ff5982617", + "sourceAsset": "assets/recipes/styles/defaults/SP12-052.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:38:36.179Z" + }, + { + "presetId": "SP12-053", + "presetName": "Marsh Witch Coven Arena", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "2. Arcane Temples & Mythic Realms", + "file": "assets/recipes/styles/defaults/SP12-053.webp", + "jobId": "d8f73bf6-6002-40b4-84c7-05d605a77026", + "sourceAsset": "assets/recipes/styles/defaults/SP12-053.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:40:19.755Z" + }, + { + "presetId": "SP12-054", + "presetName": "Copper Canyon Train Robbery", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "7. Heists, Horror & Underworld Runs", + "file": "assets/recipes/styles/defaults/SP12-054.webp", + "jobId": "55c3547f-ba78-44a9-b11f-f13b4a0012fd", + "sourceAsset": "assets/recipes/styles/defaults/SP12-054.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:41:39.217Z" + }, + { + "presetId": "SP12-055", + "presetName": "Orchid Palace Puzzle Gardens", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "2. Arcane Temples & Mythic Realms", + "file": "assets/recipes/styles/defaults/SP12-055.webp", + "jobId": "6684a0ce-20be-44c3-bacf-584a08e75c98", + "sourceAsset": "assets/recipes/styles/defaults/SP12-055.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:43:26.949Z" + }, + { + "presetId": "SP12-056", + "presetName": "Carbon Megacity Rooftop Chase", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "1. Neon Urban & Night Ops", + "file": "assets/recipes/styles/defaults/SP12-056.webp", + "jobId": "9f77a7c3-b323-440f-805d-7e0ab0a1d2ab", + "sourceAsset": "assets/recipes/styles/defaults/SP12-056.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:44:52.665Z" + }, + { + "presetId": "SP12-057", + "presetName": "Verdigris Harbor Pirate Skies", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "7. Heists, Horror & Underworld Runs", + "file": "assets/recipes/styles/defaults/SP12-057.webp", + "jobId": "9a09ee9e-faa6-4b4a-9428-70ee6539b1b0", + "sourceAsset": "assets/recipes/styles/defaults/SP12-057.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:46:26.218Z" + }, + { + "presetId": "SP12-058", + "presetName": "Echo Cavern Sound Puzzle", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "8. Puzzle Chambers & Adventure Setpieces", + "file": "assets/recipes/styles/defaults/SP12-058.webp", + "jobId": "4639a47f-a251-47d5-95fc-134a8d45e3ca", + "sourceAsset": "assets/recipes/styles/defaults/SP12-058.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:49:13.552Z" + }, + { + "presetId": "SP12-059", + "presetName": "Prismatic Arena Hero Draft", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "5. Speed, Sport & Competitive Arenas", + "file": "assets/recipes/styles/defaults/SP12-059.webp", + "jobId": "b61acd1d-902e-4352-b915-42f72746cdd3", + "sourceAsset": "assets/recipes/styles/defaults/SP12-059.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:51:01.358Z" + }, + { + "presetId": "SP12-060", + "presetName": "Hollow Basilica Final Stand", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "4. Sieges, Warfronts & Last Stands", + "file": "assets/recipes/styles/defaults/SP12-060.webp", + "jobId": "8fd95e5b-304a-4c34-befb-b6f96012a59b", + "sourceAsset": "assets/recipes/styles/defaults/SP12-060.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:52:41.484Z" + }, + { + "presetId": "SP12-061", + "presetName": "Jade Volcano Shrine Run", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "2. Arcane Temples & Mythic Realms", + "file": "assets/recipes/styles/defaults/SP12-061.webp", + "jobId": "ed747cb6-50da-4413-9f00-61960dc7474a", + "sourceAsset": "assets/recipes/styles/defaults/SP12-061.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:54:33.925Z" + }, + { + "presetId": "SP12-062", + "presetName": "Neon Koi River District", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "1. Neon Urban & Night Ops", + "file": "assets/recipes/styles/defaults/SP12-062.webp", + "jobId": "a51a5383-3492-4f9e-af6a-d4c83224ff5d", + "sourceAsset": "assets/recipes/styles/defaults/SP12-062.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:56:14.959Z" + }, + { + "presetId": "SP12-063", + "presetName": "Obelisk Desert Relic Race", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "5. Speed, Sport & Competitive Arenas", + "file": "assets/recipes/styles/defaults/SP12-063.webp", + "jobId": "07a2b046-b618-48a9-aa91-b87a11a905da", + "sourceAsset": "assets/recipes/styles/defaults/SP12-063.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:58:12.796Z" + }, + { + "presetId": "SP12-064", + "presetName": "Iron Orchard Defense Night", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "4. Sieges, Warfronts & Last Stands", + "file": "assets/recipes/styles/defaults/SP12-064.webp", + "jobId": "7188521c-b21b-4f39-888f-7a999ed6bc77", + "sourceAsset": "assets/recipes/styles/defaults/SP12-064.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:00:37.094Z" + }, + { + "presetId": "SP12-065", + "presetName": "Crystal Crown Duel Hall", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "5. Speed, Sport & Competitive Arenas", + "file": "assets/recipes/styles/defaults/SP12-065.webp", + "jobId": "eb9a7717-f931-4aac-9bd7-0a152c75c4ae", + "sourceAsset": "assets/recipes/styles/defaults/SP12-065.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:02:24.681Z" + }, + { + "presetId": "SP12-066", + "presetName": "Abyss Rail Horror Transit", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "7. Heists, Horror & Underworld Runs", + "file": "assets/recipes/styles/defaults/SP12-066.webp", + "jobId": "dff266a0-bb1f-4712-b726-882985333053", + "sourceAsset": "assets/recipes/styles/defaults/SP12-066.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:04:31.245Z" + }, + { + "presetId": "SP12-067", + "presetName": "Bronze Marsh Siege Camp", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "6. Wilderness Hunts & Harsh Frontiers", + "file": "assets/recipes/styles/defaults/SP12-067.webp", + "jobId": "17a6809d-96a5-42bb-9c69-592e247f106d", + "sourceAsset": "assets/recipes/styles/defaults/SP12-067.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:05:58.023Z" + }, + { + "presetId": "SP12-068", + "presetName": "Skyforge Dragon Dock", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "3. Sci-Fi Frontiers & Mech Zones", + "file": "assets/recipes/styles/defaults/SP12-068.webp", + "jobId": "dd7e0626-b043-496c-8d27-8f9f0942fae2", + "sourceAsset": "assets/recipes/styles/defaults/SP12-068.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:08:16.617Z" + }, + { + "presetId": "SP12-069", + "presetName": "Static Dune Radio Wars", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "7. Heists, Horror & Underworld Runs", + "file": "assets/recipes/styles/defaults/SP12-069.webp", + "jobId": "393e121a-e026-4992-9e13-36a359ae29c3", + "sourceAsset": "assets/recipes/styles/defaults/SP12-069.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:09:27.132Z" + }, + { + "presetId": "SP12-070", + "presetName": "Moonlit Shrine Archer Trials", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "2. Arcane Temples & Mythic Realms", + "file": "assets/recipes/styles/defaults/SP12-070.webp", + "jobId": "6e7b841b-3994-4469-b230-17dc596a0d56", + "sourceAsset": "assets/recipes/styles/defaults/SP12-070.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:10:33.119Z" + }, + { + "presetId": "SP12-071", + "presetName": "Verdant Metro Rebellion", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "1. Neon Urban & Night Ops", + "file": "assets/recipes/styles/defaults/SP12-071.webp", + "jobId": "7e75f117-17a7-42e8-b97d-f7f65cd50924", + "sourceAsset": "assets/recipes/styles/defaults/SP12-071.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:11:41.955Z" + }, + { + "presetId": "SP12-072", + "presetName": "Dust Cathedral Rally Raid", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "8. Puzzle Chambers & Adventure Setpieces", + "file": "assets/recipes/styles/defaults/SP12-072.webp", + "jobId": "be965453-5e46-447f-90d7-6a542863cdbc", + "sourceAsset": "assets/recipes/styles/defaults/SP12-072.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:13:20.035Z" + }, + { + "presetId": "SP12-073", + "presetName": "Titan Orchard Colossus Hunt", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "6. Wilderness Hunts & Harsh Frontiers", + "file": "assets/recipes/styles/defaults/SP12-073.webp", + "jobId": "75773bc6-dd9e-4d12-98a9-e1a99860df1f", + "sourceAsset": "assets/recipes/styles/defaults/SP12-073.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:14:31.381Z" + }, + { + "presetId": "SP12-074", + "presetName": "Prism Alley Card Duel", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "8. Puzzle Chambers & Adventure Setpieces", + "file": "assets/recipes/styles/defaults/SP12-074.webp", + "jobId": "49626624-9cd6-4935-b620-2f95fd294d43", + "sourceAsset": "assets/recipes/styles/defaults/SP12-074.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:15:37.758Z" + }, + { + "presetId": "SP12-075", + "presetName": "Cobalt Docks Mechball League", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "5. Speed, Sport & Competitive Arenas", + "file": "assets/recipes/styles/defaults/SP12-075.webp", + "jobId": "d208a356-91c9-4d12-a843-04b3010f93f6", + "sourceAsset": "assets/recipes/styles/defaults/SP12-075.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:16:44.153Z" + }, + { + "presetId": "SP12-076", + "presetName": "Aurora Bastion Siege", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "4. Sieges, Warfronts & Last Stands", + "file": "assets/recipes/styles/defaults/SP12-076.webp", + "jobId": "2a4ff4e4-49a8-4346-aa1e-db33112a3b62", + "sourceAsset": "assets/recipes/styles/defaults/SP12-076.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:17:58.891Z" + }, + { + "presetId": "SP12-077", + "presetName": "Basilisk Quarry Escape", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "8. Puzzle Chambers & Adventure Setpieces", + "file": "assets/recipes/styles/defaults/SP12-077.webp", + "jobId": "23019a50-2ed1-4605-b8d6-27d6e163ffca", + "sourceAsset": "assets/recipes/styles/defaults/SP12-077.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:19:26.494Z" + }, + { + "presetId": "SP12-078", + "presetName": "Midnight Lotus Ninja Heist", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "1. Neon Urban & Night Ops", + "file": "assets/recipes/styles/defaults/SP12-078.webp", + "jobId": "19cb8291-c242-4e52-aa55-79c9d898cddc", + "sourceAsset": "assets/recipes/styles/defaults/SP12-078.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:20:53.865Z" + }, + { + "presetId": "SP12-079", + "presetName": "Radiant Citadel Co-op Hold", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "4. Sieges, Warfronts & Last Stands", + "file": "assets/recipes/styles/defaults/SP12-079.webp", + "jobId": "c602b3e1-21d1-4cb1-96d4-ae41ce66043f", + "sourceAsset": "assets/recipes/styles/defaults/SP12-079.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:22:12.493Z" + }, + { + "presetId": "SP12-080", + "presetName": "Endgame Eclipse Throne Room", + "packId": "pack_12", + "packName": "Video Game Originals Vault", + "category": "8. Puzzle Chambers & Adventure Setpieces", + "file": "assets/recipes/styles/defaults/SP12-080.webp", + "jobId": "2d2ecd32-13ed-40cb-b5e7-2a659ffc3859", + "sourceAsset": "assets/recipes/styles/defaults/SP12-080.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:23:58.563Z" + } +] diff --git a/assets/recipes/styles/defaults/manifest-pack_13.json b/assets/recipes/styles/defaults/manifest-pack_13.json new file mode 100644 index 00000000..ad09bbfd --- /dev/null +++ b/assets/recipes/styles/defaults/manifest-pack_13.json @@ -0,0 +1,422 @@ +[ + { + "presetId": "SP13-001", + "presetName": "Cel Heroic Dawn", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-001.webp", + "jobId": "b9a2e6a3-3b86-466c-9cfd-fa49b3d8dea5", + "sourceAsset": "assets/recipes/styles/defaults/SP13-001.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T15:26:26.323Z" + }, + { + "presetId": "SP13-002", + "presetName": "Neon City Vigil", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-002.webp", + "jobId": "03531fbe-8b10-44c1-8bb8-ca9e301b99a1", + "sourceAsset": "assets/recipes/styles/defaults/SP13-002.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T15:40:13.736Z" + }, + { + "presetId": "SP13-006", + "presetName": "Spirit Shrine Twilight", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-006.webp", + "jobId": "f25656af-bf21-4fd5-8dab-94259d3b285b", + "sourceAsset": "assets/recipes/styles/defaults/SP13-006.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:49:54.597Z" + }, + { + "presetId": "SP13-007", + "presetName": "Sports Climax Arena", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-007.webp", + "jobId": "f5441cf3-4303-4a71-aa27-3a6682205e40", + "sourceAsset": "assets/recipes/styles/defaults/SP13-007.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:51:36.905Z" + }, + { + "presetId": "SP13-008", + "presetName": "Gothic Vampire Manor", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-008.webp", + "jobId": "b6a36941-6d66-4d54-b195-9a71d188ec6d", + "sourceAsset": "assets/recipes/styles/defaults/SP13-008.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:53:07.454Z" + }, + { + "presetId": "SP13-009", + "presetName": "Magical Girl Prism Burst", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-009.webp", + "jobId": "4dc48525-1057-44e0-8c9d-67d11f5f58bb", + "sourceAsset": "assets/recipes/styles/defaults/SP13-009.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:54:46.079Z" + }, + { + "presetId": "SP13-010", + "presetName": "Isekai Forest Caravan", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-010.webp", + "jobId": "3f2677c8-97a6-4800-babf-c8961ca441a1", + "sourceAsset": "assets/recipes/styles/defaults/SP13-010.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:56:38.113Z" + }, + { + "presetId": "SP13-011", + "presetName": "Meikyuu Dungeon Glow", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-011.webp", + "jobId": "3d455a66-f23a-4afb-bb80-4a4971948fc0", + "sourceAsset": "assets/recipes/styles/defaults/SP13-011.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:58:40.594Z" + }, + { + "presetId": "SP13-012", + "presetName": "Retro Mecha VHS Grain", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-012.webp", + "jobId": "1e4991c5-72d8-4c98-a3aa-e8a14b4f3943", + "sourceAsset": "assets/recipes/styles/defaults/SP13-012.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T18:00:12.332Z" + }, + { + "presetId": "SP13-013", + "presetName": "Rainy Idol Backstage", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-013.webp", + "jobId": "2cb8b524-18df-41ee-b06b-c6c81be96697", + "sourceAsset": "assets/recipes/styles/defaults/SP13-013.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T18:03:19.880Z" + }, + { + "presetId": "SP13-014", + "presetName": "Ronin Alley Duel", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-014.webp", + "jobId": "e5098526-def0-4479-ab58-46a469b110cf", + "sourceAsset": "assets/recipes/styles/defaults/SP13-014.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T18:05:08.045Z" + }, + { + "presetId": "SP13-015", + "presetName": "Dessert Cafe Comedy", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-015.webp", + "jobId": "6666efa4-f622-43e2-89d4-a0e8e26ca7f9", + "sourceAsset": "assets/recipes/styles/defaults/SP13-015.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T18:06:56.515Z" + }, + { + "presetId": "SP13-016", + "presetName": "Battle Mage Stormcast", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-016.webp", + "jobId": "ac71b151-0c3e-4460-b046-ad04efcb8a2c", + "sourceAsset": "assets/recipes/styles/defaults/SP13-016.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T18:08:45.997Z" + }, + { + "presetId": "SP13-017", + "presetName": "Ink Noir Detective", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-017.webp", + "jobId": "f98176a6-2655-4c4e-9cb5-ab72ce88aedf", + "sourceAsset": "assets/recipes/styles/defaults/SP13-017.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T18:10:50.393Z" + }, + { + "presetId": "SP13-018", + "presetName": "Festival Lantern Summer", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-018.webp", + "jobId": "2c6b04fe-d7df-4a1d-87cb-c72bd192e581", + "sourceAsset": "assets/recipes/styles/defaults/SP13-018.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T18:13:22.448Z" + }, + { + "presetId": "SP13-019", + "presetName": "Forest Spirit Courier", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-019.webp", + "jobId": "730d7a6e-812d-4005-a1e8-8fb4512497c2", + "sourceAsset": "assets/recipes/styles/defaults/SP13-019.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T18:15:12.311Z" + }, + { + "presetId": "SP13-020", + "presetName": "Final Episode Skyline", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "1. Anime", + "file": "assets/recipes/styles/defaults/SP13-020.webp", + "jobId": "ef25a701-5ba9-4a31-85bd-a1f7db4df492", + "sourceAsset": "assets/recipes/styles/defaults/SP13-020.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T18:17:09.060Z" + }, + { + "presetId": "SP13-022", + "presetName": "Skyblade Midair Clash", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "2. Acción", + "file": "assets/recipes/styles/defaults/SP13-022.webp", + "jobId": "8ce12245-1858-490c-b90c-489e4c2dd9c2", + "sourceAsset": "assets/recipes/styles/defaults/SP13-022.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:23:45.322Z" + }, + { + "presetId": "SP13-023", + "presetName": "Railgun Rooftop Strike", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "2. Acción", + "file": "assets/recipes/styles/defaults/SP13-023.webp", + "jobId": "0d667c17-026f-42f1-a86f-05fa8abbb02a", + "sourceAsset": "assets/recipes/styles/defaults/SP13-023.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:26:00.674Z" + }, + { + "presetId": "SP13-024", + "presetName": "Arena Combo Finisher", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "2. Acción", + "file": "assets/recipes/styles/defaults/SP13-024.webp", + "jobId": "7d068841-d955-45c7-9b1a-b5e2df81a37b", + "sourceAsset": "assets/recipes/styles/defaults/SP13-024.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:28:10.086Z" + }, + { + "presetId": "SP13-025", + "presetName": "Thunder Gauntlet Charge", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "2. Acción", + "file": "assets/recipes/styles/defaults/SP13-025.webp", + "jobId": "0a653976-81dd-4e74-8ac6-7feaff5e1052", + "sourceAsset": "assets/recipes/styles/defaults/SP13-025.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:30:54.168Z" + }, + { + "presetId": "SP13-027", + "presetName": "Crimson Clan Battlefield", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "3. Samurais & Medieval", + "file": "assets/recipes/styles/defaults/SP13-027.webp", + "jobId": "4c999866-245c-4528-8010-3000d3703b24", + "sourceAsset": "assets/recipes/styles/defaults/SP13-027.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:35:34.224Z" + }, + { + "presetId": "SP13-028", + "presetName": "Medieval Knight Oath", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "3. Samurais & Medieval", + "file": "assets/recipes/styles/defaults/SP13-028.webp", + "jobId": "cf03af58-0ff6-4d24-849a-c68bf7ef121c", + "sourceAsset": "assets/recipes/styles/defaults/SP13-028.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:37:15.497Z" + }, + { + "presetId": "SP13-029", + "presetName": "Castle Siege Emberstorm", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "3. Samurais & Medieval", + "file": "assets/recipes/styles/defaults/SP13-029.webp", + "jobId": "1bc699ff-ab38-42f8-b2d0-90d1596c3282", + "sourceAsset": "assets/recipes/styles/defaults/SP13-029.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:38:41.185Z" + }, + { + "presetId": "SP13-030", + "presetName": "Blademonk Moon Courtyard", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "3. Samurais & Medieval", + "file": "assets/recipes/styles/defaults/SP13-030.webp", + "jobId": "dd32e1e9-0be3-4d67-ad57-2fa603f17b2a", + "sourceAsset": "assets/recipes/styles/defaults/SP13-030.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:40:35.787Z" + }, + { + "presetId": "SP13-031", + "presetName": "Haunted School Corridor", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "4. Horror", + "file": "assets/recipes/styles/defaults/SP13-031.webp", + "jobId": "3673762a-2fac-4eb4-a2ed-aca286a33d69", + "sourceAsset": "assets/recipes/styles/defaults/SP13-031.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:42:11.278Z" + }, + { + "presetId": "SP13-032", + "presetName": "Crimson Moon Apparition", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "4. Horror", + "file": "assets/recipes/styles/defaults/SP13-032.webp", + "jobId": "7fed2a6a-ae70-4972-aa10-4f4f0df59d2d", + "sourceAsset": "assets/recipes/styles/defaults/SP13-032.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:43:42.588Z" + }, + { + "presetId": "SP13-033", + "presetName": "Flesh Puppet Theater", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "4. Horror", + "file": "assets/recipes/styles/defaults/SP13-033.webp", + "jobId": "950d6599-d880-47ad-8aa5-5195dd101c08", + "sourceAsset": "assets/recipes/styles/defaults/SP13-033.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:45:19.399Z" + }, + { + "presetId": "SP13-034", + "presetName": "Deep Well Whisper", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "4. Horror", + "file": "assets/recipes/styles/defaults/SP13-034.webp", + "jobId": "22d31f39-f30c-46b7-a422-fe0278a633ac", + "sourceAsset": "assets/recipes/styles/defaults/SP13-034.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:46:41.829Z" + }, + { + "presetId": "SP13-035", + "presetName": "Oni Mask Ritual Night", + "packId": "pack_13", + "packName": "Anime Expansion Vault", + "category": "4. Horror", + "file": "assets/recipes/styles/defaults/SP13-035.webp", + "jobId": "a5553e07-2706-4378-b012-affbf5051c90", + "sourceAsset": "assets/recipes/styles/defaults/SP13-035.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T17:48:02.251Z" + } +] diff --git a/assets/recipes/styles/defaults/manifest-pack_14.json b/assets/recipes/styles/defaults/manifest-pack_14.json new file mode 100644 index 00000000..6984086d --- /dev/null +++ b/assets/recipes/styles/defaults/manifest-pack_14.json @@ -0,0 +1,114 @@ +[ + { + "presetId": "SP14-001", + "presetName": "Cathedral Eclipse Procession", + "packId": "pack_14", + "packName": "Mythic Noir Curated Vault", + "category": "1. Mythic Noir", + "file": "assets/recipes/styles/defaults/SP14-001.webp", + "jobId": "30fe8a75-5a71-4a34-a023-a74316b6f1c9", + "sourceAsset": "assets/recipes/styles/defaults/SP14-001.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:51:22.808Z" + }, + { + "presetId": "SP14-002", + "presetName": "Velvet Relic Revenant", + "packId": "pack_14", + "packName": "Mythic Noir Curated Vault", + "category": "1. Mythic Noir", + "file": "assets/recipes/styles/defaults/SP14-002.webp", + "jobId": "827d2661-6342-4d18-ae4b-0c4534d8303b", + "sourceAsset": "assets/recipes/styles/defaults/SP14-002.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:53:34.490Z" + }, + { + "presetId": "SP14-003", + "presetName": "Saint of Broken Neon", + "packId": "pack_14", + "packName": "Mythic Noir Curated Vault", + "category": "1. Mythic Noir", + "file": "assets/recipes/styles/defaults/SP14-003.webp", + "jobId": "b144590e-f47f-4556-8b3a-2c8ff04cfd48", + "sourceAsset": "assets/recipes/styles/defaults/SP14-003.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:54:53.713Z" + }, + { + "presetId": "SP14-004", + "presetName": "Warden of Glass Thorns", + "packId": "pack_14", + "packName": "Mythic Noir Curated Vault", + "category": "1. Mythic Noir", + "file": "assets/recipes/styles/defaults/SP14-004.webp", + "jobId": "3c6d3403-619c-4b9a-ac21-6e511147b99a", + "sourceAsset": "assets/recipes/styles/defaults/SP14-004.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:57:23.285Z" + }, + { + "presetId": "SP14-005", + "presetName": "Marble Wraith Banquet", + "packId": "pack_14", + "packName": "Mythic Noir Curated Vault", + "category": "1. Mythic Noir", + "file": "assets/recipes/styles/defaults/SP14-005.webp", + "jobId": "06320e29-810f-427e-a952-f99325e3c0d3", + "sourceAsset": "assets/recipes/styles/defaults/SP14-005.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:58:31.767Z" + }, + { + "presetId": "SP14-006", + "presetName": "Crowned Ash Oracle", + "packId": "pack_14", + "packName": "Mythic Noir Curated Vault", + "category": "1. Mythic Noir", + "file": "assets/recipes/styles/defaults/SP14-006.webp", + "jobId": "67637361-673c-4b7c-a75e-1965c0386396", + "sourceAsset": "assets/recipes/styles/defaults/SP14-006.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:00:11.532Z" + }, + { + "presetId": "SP14-007", + "presetName": "Moonlit Reliquary Hunt", + "packId": "pack_14", + "packName": "Mythic Noir Curated Vault", + "category": "1. Mythic Noir", + "file": "assets/recipes/styles/defaults/SP14-007.webp", + "jobId": "e6501da1-0b69-441a-bbf0-b6209603bc16", + "sourceAsset": "assets/recipes/styles/defaults/SP14-007.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:03:19.424Z" + }, + { + "presetId": "SP14-008", + "presetName": "Funeral Rose Cavalier", + "packId": "pack_14", + "packName": "Mythic Noir Curated Vault", + "category": "1. Mythic Noir", + "file": "assets/recipes/styles/defaults/SP14-008.webp", + "jobId": "32078ade-d2e2-4c9f-8814-e462353d70db", + "sourceAsset": "assets/recipes/styles/defaults/SP14-008.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:05:39.816Z" + } +] diff --git a/assets/recipes/styles/defaults/manifest-pack_15.json b/assets/recipes/styles/defaults/manifest-pack_15.json new file mode 100644 index 00000000..54230878 --- /dev/null +++ b/assets/recipes/styles/defaults/manifest-pack_15.json @@ -0,0 +1,114 @@ +[ + { + "presetId": "SP15-001", + "presetName": "Terraced Sky Harvest", + "packId": "pack_15", + "packName": "Solarpunk Dreamscapes Vault", + "category": "1. Solarpunk Dreamscapes", + "file": "assets/recipes/styles/defaults/SP15-001.webp", + "jobId": "be8f803a-40c3-444f-8a77-873247f4dd74", + "sourceAsset": "assets/recipes/styles/defaults/SP15-001.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:52:33.783Z" + }, + { + "presetId": "SP15-002", + "presetName": "Wind Garden Monorail", + "packId": "pack_15", + "packName": "Solarpunk Dreamscapes Vault", + "category": "1. Solarpunk Dreamscapes", + "file": "assets/recipes/styles/defaults/SP15-002.webp", + "jobId": "f99aff61-4506-4c4e-aa5e-2712eacae942", + "sourceAsset": "assets/recipes/styles/defaults/SP15-002.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:53:37.215Z" + }, + { + "presetId": "SP15-003", + "presetName": "Coral Transit Canopy", + "packId": "pack_15", + "packName": "Solarpunk Dreamscapes Vault", + "category": "1. Solarpunk Dreamscapes", + "file": "assets/recipes/styles/defaults/SP15-003.webp", + "jobId": "ebb15b84-10bf-4fbc-b706-4a9a54ad8c1b", + "sourceAsset": "assets/recipes/styles/defaults/SP15-003.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:54:18.947Z" + }, + { + "presetId": "SP15-004", + "presetName": "Fog Orchard Habitat", + "packId": "pack_15", + "packName": "Solarpunk Dreamscapes Vault", + "category": "1. Solarpunk Dreamscapes", + "file": "assets/recipes/styles/defaults/SP15-004.webp", + "jobId": "fb695050-dd8b-4438-99cb-c4ec7399f444", + "sourceAsset": "assets/recipes/styles/defaults/SP15-004.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:55:29.916Z" + }, + { + "presetId": "SP15-005", + "presetName": "Sun Reservoir Commons", + "packId": "pack_15", + "packName": "Solarpunk Dreamscapes Vault", + "category": "1. Solarpunk Dreamscapes", + "file": "assets/recipes/styles/defaults/SP15-005.webp", + "jobId": "302088ac-5538-437e-b00d-b79bef158204", + "sourceAsset": "assets/recipes/styles/defaults/SP15-005.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:56:39.683Z" + }, + { + "presetId": "SP15-006", + "presetName": "Seed Vault Festival", + "packId": "pack_15", + "packName": "Solarpunk Dreamscapes Vault", + "category": "1. Solarpunk Dreamscapes", + "file": "assets/recipes/styles/defaults/SP15-006.webp", + "jobId": "bfef14a1-35da-4aca-9b3e-a6004429f0d3", + "sourceAsset": "assets/recipes/styles/defaults/SP15-006.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-28T23:58:29.690Z" + }, + { + "presetId": "SP15-007", + "presetName": "Vertical Meadow Clinic", + "packId": "pack_15", + "packName": "Solarpunk Dreamscapes Vault", + "category": "1. Solarpunk Dreamscapes", + "file": "assets/recipes/styles/defaults/SP15-007.webp", + "jobId": "b6eeb24a-bc7d-41a4-ae9a-1aa4e1ef3719", + "sourceAsset": "assets/recipes/styles/defaults/SP15-007.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:01:20.209Z" + }, + { + "presetId": "SP15-008", + "presetName": "Rooftop Rain Choir", + "packId": "pack_15", + "packName": "Solarpunk Dreamscapes Vault", + "category": "1. Solarpunk Dreamscapes", + "file": "assets/recipes/styles/defaults/SP15-008.webp", + "jobId": "00852702-5715-4a38-a06f-5b5ccd27757b", + "sourceAsset": "assets/recipes/styles/defaults/SP15-008.webp", + "generationMode": "text-to-image", + "model": "gpt-5.4-mini", + "reasoningEffort": "low", + "generatedAt": "2026-05-29T00:03:48.896Z" + } +] diff --git a/bun.lock b/bun.lock index b786c732..9d7485b8 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,6 @@ "jszip": "^3.10.1", "lucide-react": "^1.16.0", "react": "^19.2.6", - "react-doctor": "^0.2.8", "react-dom": "^19.2.6", "tailwind-merge": "^3.6.0", "three": "^0.184.0", @@ -30,7 +29,7 @@ "@types/react-dom": "^19.2.3", "@types/three": "^0.184.1", "@vitejs/plugin-react": "^6.0.2", - "electron": "^42.2.0", + "electron": "^42.3.0", "playwright": "^1.60.0", "react-scan": "^0.5.6", "sharp": "^0.34.5", @@ -78,8 +77,6 @@ "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], - "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.70", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.70" } }, "sha512-3VXuL63IDmq13We+ApRKn2JW3Rb9g5gj1YEmfb8u2b73norur1VsIJ/pRE4qjShevg19dQYi2JsLawSZ6gApug=="], - "@electron/get": ["@electron/get@5.0.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^3.0.0", "graceful-fs": "^4.2.11", "progress": "^2.0.3", "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "optionalDependencies": { "undici": "^7.24.4" } }, "sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA=="], "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], @@ -140,36 +137,10 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - - "@eslint/config-array": ["@eslint/config-array@0.23.5", "", { "dependencies": { "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.6.0", "", { "dependencies": { "@eslint/core": "^1.2.1" } }, "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA=="], - - "@eslint/core": ["@eslint/core@1.2.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ=="], - - "@eslint/object-schema": ["@eslint/object-schema@3.0.5", "", {}, "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw=="], - - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], - "@fal-ai/client": ["@fal-ai/client@1.10.1", "", { "dependencies": { "@msgpack/msgpack": "^3.0.0-beta2", "eventsource-parser": "^1.1.2", "robot3": "^0.4.1" } }, "sha512-c3AVeH31OioiI2J1BfW8Cryi1DhUYldnY3X35nv6xLMq3fU2NQOo+eYaR5mL2O8MoHHh+HzXdQuIyanIyeq+ug=="], "@gsap/react": ["@gsap/react@2.1.2", "", { "peerDependencies": { "gsap": "^3.12.5", "react": ">=17" } }, "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw=="], - "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], - - "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], - - "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - - "@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="], - "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], @@ -232,110 +203,12 @@ "@msgpack/msgpack": ["@msgpack/msgpack@3.1.3", "", {}, "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA=="], - "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], - - "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], - - "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="], - - "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="], - - "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="], - - "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.132.0", "", { "os": "android", "cpu": "arm" }, "sha512-KrLaPWa5c9Y7LkW+rKkaUE3y7DBDrQtaf7rlsSDfv6KAHUjgzAIRA761Lrrp6//Yd/Rlie/yEOt9YENCoJnOcw=="], - - "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.132.0", "", { "os": "android", "cpu": "arm64" }, "sha512-SThDrSeamB/kG2+NxcJ5/wSLcV6dUqDknrPLqFYQ0ST/55mtBP4M7Q/f3QbubH6aAd11wpzZn/nwbVRSdobOpg=="], - - "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.132.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Lc0f/TYoKBghE5/2Gsv7bLXk+TJZunx2Tf61X8hG4ARXdc8UYI26dCGccFSd1AyFbK3jfaNXtMnupggDbjPXdQ=="], - - "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.132.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-RG2eJIpf7C21z9HSSXFw1bTArdpKe7Y4fwcJTwRq1yCSe1vSavaN9GA1sm9KqzemTLAGVktQ+7qBTGp0vQeUZg=="], - - "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.132.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-wQIPntPLtJ8NcBpvKPbEv3NqzV6k8eP8tP/jE9Rg8HTg/j7urZGFSsTCPCW5k77Qfw2DM4vRvc9p3I4yq/Shvw=="], - - "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.132.0", "", { "os": "linux", "cpu": "arm" }, "sha512-PixKEpeSe3yxQWqNyOCBALRYc72+Tj7ILDofUl3iXo25cVOzLA6jHUhmOINRtWIPh7dbUie3QNeabwaQpZTw6w=="], - - "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.132.0", "", { "os": "linux", "cpu": "arm" }, "sha512-sCR+DzGHlyHKnbA2z9zWjTUhIo8Sy0enJl4RDsBwPmkxYynPatpwOAWe8W5127SlW0boqUWHGtr1NWn5UwIhXQ=="], - - "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.132.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-sQBix5P2cW+IpzTcCwYxnh9yALrKSIkKJThspBvMGcygSMnbzkSvhN7SfuX1hvBk8y1XEChsdkU3ET0V5DmzUw=="], - - "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.132.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WozHg3Kc//8Sk756HXXgMbEAvqtG+Lzb9JOojwQzIGDtN78Az2dLttkb71akWYUF/8IgYfDSlfKh4Uot8is5Vw=="], - - "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.132.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CmX/ulNBOEwWTyVRmcpYKAcAizW6+OjtLJgo7fXoL9OqQvjF4VER8tPomv44vwzfSCy1BHbsB0ZlZYzYJNj4cA=="], - - "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.132.0", "", { "os": "linux", "cpu": "none" }, "sha512-j9oQS+hM90SdhviNGWbPgT4+Rlq+ac++q/zjgwPD1mVHgxHzATvoRGtDx0sXGmFOQ9J9YkwAhYGb5MAHL6TAsA=="], - - "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.132.0", "", { "os": "linux", "cpu": "none" }, "sha512-bLz+Xi+Agnfmd7kWPEsSVwCn2k4EyIalZkNBcQ0OGIv9rqn8VgCPLNd03tM9mKX/5TdlvDXalz0q71BIrOPNqg=="], - - "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.132.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-U6t2qbJU0ypTfyj9QV3W1Y6mITDTL8ai/OR6NUn85vyHthOvobKWgXzU4tu0EskSzlpuVFz1g0jFGulDIUKHxQ=="], - - "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.132.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WcEaSNHFk8yz5YFlQQAlhq6jOFmZBB/RKE7uzhyCIf+pF1Lmv9gUH4221mle2Gd9iHyWT3ySNph8yZgb1xYdWg=="], - - "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.132.0", "", { "os": "linux", "cpu": "x64" }, "sha512-iQrV4iJzQgRwK3BWRmQl1C3C6g3wYpXN2WLdQdyR+efoUnncdShZAVp9OgcojtlD3MDRbuOMGG3SjxF4fL4nlQ=="], - - "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.132.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FWzmUGrZ6GUby4U7WIwcCtab6tdmlTO3xTRRKyb5kjIJVEiaUAT8animUG/nK8ZCA8gkRkPOTId4rl6uTqUmJQ=="], - - "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.132.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-TlbMppxJI5CjWDes0QaP6G3aneVg1yikBu5QYI+DUShF9WDL66ccgKFNNGmi/Wybtszw6hxwAvv76T4DaPKnHw=="], - - "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.132.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-RH/NbFjGKqdUAUi7Oh3LQPxUk2hsWFEEQ38HSnbRQT8QjBZFKqL1fMbmsB3N4jy/KPh9iX94+9dmkEMBBbambw=="], - - "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.132.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-JUr4jQY9jxoIB/YTLXr6XofSi5xikj6p5/Ns1h0VOBDT0j1jKU+kMsv2xxv51RwnETcXpA1Yw/9oUAfcqfaqEA=="], - - "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.132.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2dapgHpA5X8DSXF4AU36hJWYf6zP0tKjMXFRAZFBD62pkevW/uhFDXoFH9Y/3Fd2EtDrw5ByNnR1wVE9X9y0SQ=="], - "@oxc-project/runtime": ["@oxc-project/runtime@0.129.0", "", {}, "sha512-0+S67blQakgeNqoKGozOUp5rQBrz2ynXZ2QIINXZPiafsD0YL0UogB9hAWc1S7k6VSNwKYC/N7MqT0V6IzpHkQ=="], "@oxc-project/types": ["@oxc-project/types@0.129.0", "", {}, "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg=="], - "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.19.1", "", { "os": "android", "cpu": "arm" }, "sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg=="], - - "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.19.1", "", { "os": "android", "cpu": "arm64" }, "sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA=="], - - "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.19.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ=="], - - "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.19.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ=="], - - "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.19.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw=="], - - "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1", "", { "os": "linux", "cpu": "arm" }, "sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A=="], - - "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.19.1", "", { "os": "linux", "cpu": "arm" }, "sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ=="], - - "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.19.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig=="], - - "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.19.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew=="], - - "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.19.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ=="], - - "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.19.1", "", { "os": "linux", "cpu": "none" }, "sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w=="], - - "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.19.1", "", { "os": "linux", "cpu": "none" }, "sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw=="], - - "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.19.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA=="], - - "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.19.1", "", { "os": "linux", "cpu": "x64" }, "sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ=="], - - "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.19.1", "", { "os": "linux", "cpu": "x64" }, "sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw=="], - - "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.19.1", "", { "os": "none", "cpu": "arm64" }, "sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA=="], - - "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.19.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg=="], - - "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.19.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ=="], - - "@oxc-resolver/binding-win32-ia32-msvc": ["@oxc-resolver/binding-win32-ia32-msvc@11.19.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA=="], - - "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.19.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw=="], - "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.48.0", "", { "os": "android", "cpu": "arm" }, "sha512-uwqk+/KhQvBIpULD8SMM/zAafMRC/+DV/xsEQjkkIsJ/kLmEI/2bxonVowcYTiXqqZ/a0FEW8DPkZY3VvwELDA=="], "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.48.0", "", { "os": "android", "cpu": "arm64" }, "sha512-VUCiKuXK5+McVssgHEJdrcGK7hRJzrRb36zm9/jwzMholyYt4BgXhw5Nm1V1DX6Ce717Zi/1jk432b/tgmQgtQ=="], @@ -510,16 +383,12 @@ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], - "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/file-saver": ["@types/file-saver@2.0.7", "", {}, "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A=="], "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="], "@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="], @@ -532,12 +401,8 @@ "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="], - "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="], "@voidzero-dev/vite-plus-core": ["@voidzero-dev/vite-plus-core@0.1.22", "", { "dependencies": { "@oxc-project/runtime": "=0.129.0", "@oxc-project/types": "=0.129.0", "lightningcss": "^1.30.2", "postcss": "^8.5.6" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@tsdown/css": "0.22.0", "@tsdown/exe": "0.22.0", "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "typescript": "^5.0.0 || ^6.0.0", "unplugin-unused": "^0.5.0", "unrun": "*", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@tsdown/css", "@tsdown/exe", "@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "yaml"] }, "sha512-OC7tChagbJCoY7YKzD5MuyxJO1km5IF42B3ltZoQ9Twc8UuPrMuWZrVoP984tJKYd/gFJuQFM/lrbNtBm9kyDg=="], @@ -562,32 +427,16 @@ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "agent-install": ["agent-install@0.0.5", "", { "dependencies": { "@iarna/toml": "^2.2.5", "commander": "^14.0.0", "jsonc-parser": "^3.3.1", "picocolors": "^1.1.1", "prompts": "^2.4.2", "yaml": "^2.8.3" }, "bin": { "agent-install": "bin/agent-install.mjs" } }, "sha512-nHlms9BkP8ZiY79HrwCGiA2DcNaXrAaJrCM/BEqQ7MEsSKyCk+2A76xPGylIfASZSZE0SaU3T0bNSg4rBPIJAQ=="], - - "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - - "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "atomically": ["atomically@2.1.1", "", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="], - - "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.10.27", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA=="], "bippy": ["bippy@0.5.39", "", { "peerDependencies": { "react": ">=17.0.1" } }, "sha512-8hE8rKSl8JWyeaY+JjpnmceWAZPpLEyzOZQpWXM5Rc7861c5WotMJHy2aRZKZrGA8nMpvLNF01t4yQQ+HcZG3w=="], - "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], @@ -606,31 +455,17 @@ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "conf": ["conf@15.1.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - - "deslop-js": ["deslop-js@0.0.13", "", { "dependencies": { "@oxc-project/types": "^0.132.0", "fast-glob": "^3.3.3", "minimatch": "^10.2.5", "oxc-parser": "^0.132.0", "oxc-resolver": "^11.19.1", "typescript": "^6.0.3" } }, "sha512-gIXD+wY2/NHkZHpNrb8MWS6NJs3ee0XunAenBCvwHJlWnedDbIrg70hdNR7bDzYZLe9LGJZMLEI1w2CC72Gk5A=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="], - - "effect": ["effect@4.0.0-beta.70", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-8AwGTRiNriirHGEYHrOS0E9fzdhIqCdZjiHP1YXmNo2UyPGS43ILsymsSHT7V0DJS+8dvlKq2RxnrDBUhDNZHg=="], - - "electron": ["electron@42.2.0", "", { "dependencies": { "@electron/get": "^5.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-b2Tc7sIKiZEl0tBVwFM5GJ+FT5KYhmy9QJHjx8BGVZPVW2SctXWEvrE959ElB56qw7H05dBkhlikDA1DmpaAMw=="], + "electron": ["electron@42.3.0", "", { "dependencies": { "@electron/get": "^5.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-9ZiLdRXk+WDxW1OgIUz8J2rIQ5TYU9o629gCOjU48Q3dQiOmym7osWsH5Ubs/Jh4uuFLn6m6SBD2rmRXLAPz9g=="], "electron-to-chromium": ["electron-to-chromium@1.5.352", "", {}, "sha512-9wHk8x6dyuimoe18EdiDPWKExNdxYqo4fn4FwOVVper6RxT3cmpBwBkWWfSOCYJjQdIco/nPhJhNLmn4Ufg1Yg=="], @@ -646,66 +481,20 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@10.4.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ=="], - - "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="], - - "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], - - "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="], - - "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "eventsource-parser": ["eventsource-parser@1.1.2", "", {}, "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA=="], "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], - "fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], - - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - "file-saver": ["file-saver@2.0.5", "", {}, "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="], - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], - "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], "fzf": ["fzf@0.5.2", "", {}, "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q=="], @@ -716,42 +505,24 @@ "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "gsap": ["gsap@3.15.0", "", {}, "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A=="], - "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], - - "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], - "hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="], "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - "inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="], - "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "jiti": ["jiti@2.6.1", "", { "bin": "lib/jiti-cli.mjs" }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -760,28 +531,14 @@ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], - - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -808,8 +565,6 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -818,32 +573,16 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - "meshoptimizer": ["meshoptimizer@1.1.1", "", {}, "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g=="], - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="], - - "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], - - "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - - "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], - "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], @@ -852,34 +591,18 @@ "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - "ora": ["ora@9.4.0", "", { "dependencies": { "chalk": "^5.6.2", "cli-cursor": "^5.0.0", "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" } }, "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ=="], - "oxc-parser": ["oxc-parser@0.132.0", "", { "dependencies": { "@oxc-project/types": "^0.132.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.132.0", "@oxc-parser/binding-android-arm64": "0.132.0", "@oxc-parser/binding-darwin-arm64": "0.132.0", "@oxc-parser/binding-darwin-x64": "0.132.0", "@oxc-parser/binding-freebsd-x64": "0.132.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.132.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.132.0", "@oxc-parser/binding-linux-arm64-gnu": "0.132.0", "@oxc-parser/binding-linux-arm64-musl": "0.132.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.132.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.132.0", "@oxc-parser/binding-linux-riscv64-musl": "0.132.0", "@oxc-parser/binding-linux-s390x-gnu": "0.132.0", "@oxc-parser/binding-linux-x64-gnu": "0.132.0", "@oxc-parser/binding-linux-x64-musl": "0.132.0", "@oxc-parser/binding-openharmony-arm64": "0.132.0", "@oxc-parser/binding-wasm32-wasi": "0.132.0", "@oxc-parser/binding-win32-arm64-msvc": "0.132.0", "@oxc-parser/binding-win32-ia32-msvc": "0.132.0", "@oxc-parser/binding-win32-x64-msvc": "0.132.0" } }, "sha512-+0LAPHaqtfQlvWdpaAa09SmOaZZgP8C552xosEkGJ4+ruEwP1Vgx+sqBgcBCNfR6KDCmagGOZTde8wmAvcI/Hg=="], - - "oxc-resolver": ["oxc-resolver@11.19.1", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.19.1", "@oxc-resolver/binding-android-arm64": "11.19.1", "@oxc-resolver/binding-darwin-arm64": "11.19.1", "@oxc-resolver/binding-darwin-x64": "11.19.1", "@oxc-resolver/binding-freebsd-x64": "11.19.1", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.19.1", "@oxc-resolver/binding-linux-arm-musleabihf": "11.19.1", "@oxc-resolver/binding-linux-arm64-gnu": "11.19.1", "@oxc-resolver/binding-linux-arm64-musl": "11.19.1", "@oxc-resolver/binding-linux-ppc64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-musl": "11.19.1", "@oxc-resolver/binding-linux-s390x-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-musl": "11.19.1", "@oxc-resolver/binding-openharmony-arm64": "11.19.1", "@oxc-resolver/binding-wasm32-wasi": "11.19.1", "@oxc-resolver/binding-win32-arm64-msvc": "11.19.1", "@oxc-resolver/binding-win32-ia32-msvc": "11.19.1", "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg=="], - "oxfmt": ["oxfmt@0.48.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.48.0", "@oxfmt/binding-android-arm64": "0.48.0", "@oxfmt/binding-darwin-arm64": "0.48.0", "@oxfmt/binding-darwin-x64": "0.48.0", "@oxfmt/binding-freebsd-x64": "0.48.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.48.0", "@oxfmt/binding-linux-arm-musleabihf": "0.48.0", "@oxfmt/binding-linux-arm64-gnu": "0.48.0", "@oxfmt/binding-linux-arm64-musl": "0.48.0", "@oxfmt/binding-linux-ppc64-gnu": "0.48.0", "@oxfmt/binding-linux-riscv64-gnu": "0.48.0", "@oxfmt/binding-linux-riscv64-musl": "0.48.0", "@oxfmt/binding-linux-s390x-gnu": "0.48.0", "@oxfmt/binding-linux-x64-gnu": "0.48.0", "@oxfmt/binding-linux-x64-musl": "0.48.0", "@oxfmt/binding-openharmony-arm64": "0.48.0", "@oxfmt/binding-win32-arm64-msvc": "0.48.0", "@oxfmt/binding-win32-ia32-msvc": "0.48.0", "@oxfmt/binding-win32-x64-msvc": "0.48.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-AVaLh+7XeGx+R1zfFV+f6VV61nT2MWVJXVUDhbTm5LBWGyNt64xAyh3NYYyjeY2WykNt9AvqSQLPHcbWquYF9g=="], "oxlint": ["oxlint@1.63.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.63.0", "@oxlint/binding-android-arm64": "1.63.0", "@oxlint/binding-darwin-arm64": "1.63.0", "@oxlint/binding-darwin-x64": "1.63.0", "@oxlint/binding-freebsd-x64": "1.63.0", "@oxlint/binding-linux-arm-gnueabihf": "1.63.0", "@oxlint/binding-linux-arm-musleabihf": "1.63.0", "@oxlint/binding-linux-arm64-gnu": "1.63.0", "@oxlint/binding-linux-arm64-musl": "1.63.0", "@oxlint/binding-linux-ppc64-gnu": "1.63.0", "@oxlint/binding-linux-riscv64-gnu": "1.63.0", "@oxlint/binding-linux-riscv64-musl": "1.63.0", "@oxlint/binding-linux-s390x-gnu": "1.63.0", "@oxlint/binding-linux-x64-gnu": "1.63.0", "@oxlint/binding-linux-x64-musl": "1.63.0", "@oxlint/binding-openharmony-arm64": "1.63.0", "@oxlint/binding-win32-arm64-msvc": "1.63.0", "@oxlint/binding-win32-ia32-msvc": "1.63.0", "@oxlint/binding-win32-x64-msvc": "1.63.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-9TGXetdjgIHOJ9OiReomP7nnrMkV9HxC1xM2ramJSLQpzxjsAJtQwa4wqkJN2f/uCrqZuJseFuSlWDdvcruveg=="], - "oxlint-plugin-react-doctor": ["oxlint-plugin-react-doctor@0.2.8", "", { "dependencies": { "@typescript-eslint/types": "^8.59.3", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-bqfY9RNfJWWaHI61Tkn5FUdJ9Tq3mTZdpNgufkVwoyT85bfRCLY8QNH20Vr7zpwFoQIfN05IpJk+VS373K4m1w=="], - "oxlint-tsgolint": ["oxlint-tsgolint@0.22.1", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "0.22.1", "@oxlint-tsgolint/darwin-x64": "0.22.1", "@oxlint-tsgolint/linux-arm64": "0.22.1", "@oxlint-tsgolint/linux-x64": "0.22.1", "@oxlint-tsgolint/win32-arm64": "0.22.1", "@oxlint-tsgolint/win32-x64": "0.22.1" }, "bin": { "tsgolint": "bin/tsgolint.js" } }, "sha512-YUSGSLUnoolsu8gxISEDio3q1rtsCozwfOzASUn3DT2mR2EeQ93uEEnen7s+6LpF+lyTQFln1pQfqwBh/fsVEg=="], - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], @@ -898,8 +621,6 @@ "preact": ["preact@10.29.1", "", {}, "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg=="], - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], @@ -908,16 +629,8 @@ "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="], - "react-doctor": ["react-doctor@0.2.8", "", { "dependencies": { "@effect/platform-node-shared": "4.0.0-beta.70", "agent-install": "0.0.5", "conf": "^15.1.0", "deslop-js": "^0.0.13", "effect": "4.0.0-beta.70", "eslint-plugin-react-hooks": "^7.1.1", "oxlint": "^1.66.0", "oxlint-plugin-react-doctor": "0.2.8", "prompts": "^2.4.2", "typescript": ">=5.0.4 <7" }, "bin": { "react-doctor": "bin/react-doctor.js" } }, "sha512-WpikJI6/boNzl1ddNaO4HrnS6V/zg/wT/I3MT3lz8ho36ANCZzG6wGZBtW8L1gIeWIritjyE0Nzxre609+GzoA=="], - "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], "react-grab": ["react-grab@0.1.33", "", { "dependencies": { "@react-grab/cli": "0.1.33", "bippy": "^0.5.39" }, "peerDependencies": { "react": ">=17.0.0" }, "optionalPeers": ["react"], "bin": { "react-grab": "bin/cli.js" } }, "sha512-ER919JMsE4TTrb2CpEivqsIjNMSycD4HtS8v7mS3pq67U7WL1K3+C8m9AYOwW4dpuYh+EanC2eJBmfuczHJZ0A=="], @@ -926,18 +639,12 @@ "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - "robot3": ["robot3@0.4.1", "", {}, "sha512-hzjy826lrxzx8eRgv80idkf8ua1JAepRc9Efdtj03N3KNJuznQCPlyCJ7gnUmDFwZCLQjxy567mQVKmdv2BsXQ=="], "rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -948,10 +655,6 @@ "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], @@ -972,14 +675,8 @@ "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - "stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="], - - "stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="], - "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], - "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], - "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], "tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], @@ -996,24 +693,14 @@ "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="], - "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tsx": ["tsx@4.22.3", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg=="], - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - - "type-fest": ["type-fest@5.6.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA=="], - "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], - "undici": ["undici@7.25.0", "", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], @@ -1022,24 +709,14 @@ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="], - "vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="], "vite-plus": ["vite-plus@0.1.22", "", { "dependencies": { "@oxc-project/types": "=0.129.0", "@oxlint/plugins": "=1.61.0", "@voidzero-dev/vite-plus-core": "0.1.22", "@voidzero-dev/vite-plus-test": "0.1.22", "oxfmt": "=0.48.0", "oxlint": "=1.63.0", "oxlint-tsgolint": "=0.22.1" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.1.22", "@voidzero-dev/vite-plus-darwin-x64": "0.1.22", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.1.22", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.1.22", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.1.22", "@voidzero-dev/vite-plus-linux-x64-musl": "0.1.22", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.1.22", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.1.22" }, "bin": { "oxfmt": "bin/oxfmt", "oxlint": "bin/oxlint", "vp": "bin/vp" } }, "sha512-fCCmEKjI+Hv74PdL/MKcrBkdYPHFNcqD5568KxwN0sa4SGxtcbs55i/577LxKs0w5zIjuLRZZ0zQPu9MO+9itg=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], - "when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], @@ -1050,20 +727,12 @@ "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], - "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - - "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -1080,22 +749,8 @@ "@voidzero-dev/vite-plus-core/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "deslop-js/@oxc-project/types": ["@oxc-project/types@0.132.0", "", {}, "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ=="], - "electron/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], - "eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], - - "eslint/glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - - "oxc-parser/@oxc-project/types": ["@oxc-project/types@0.132.0", "", {}, "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ=="], - - "react-doctor/oxlint": ["oxlint@1.67.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.67.0", "@oxlint/binding-android-arm64": "1.67.0", "@oxlint/binding-darwin-arm64": "1.67.0", "@oxlint/binding-darwin-x64": "1.67.0", "@oxlint/binding-freebsd-x64": "1.67.0", "@oxlint/binding-linux-arm-gnueabihf": "1.67.0", "@oxlint/binding-linux-arm-musleabihf": "1.67.0", "@oxlint/binding-linux-arm64-gnu": "1.67.0", "@oxlint/binding-linux-arm64-musl": "1.67.0", "@oxlint/binding-linux-ppc64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-gnu": "1.67.0", "@oxlint/binding-linux-riscv64-musl": "1.67.0", "@oxlint/binding-linux-s390x-gnu": "1.67.0", "@oxlint/binding-linux-x64-gnu": "1.67.0", "@oxlint/binding-linux-x64-musl": "1.67.0", "@oxlint/binding-openharmony-arm64": "1.67.0", "@oxlint/binding-win32-arm64-msvc": "1.67.0", "@oxlint/binding-win32-ia32-msvc": "1.67.0", "@oxlint/binding-win32-x64-msvc": "1.67.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-blwwaHPdoH8piQ5/z0KHeoHFR7FZgl12WluKJfu4qFLPkZl6mK04PkLE45Fw1NxfBRSlh40Gu7MkxHUw++ociQ=="], - "rolldown/@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="], "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="], @@ -1107,45 +762,5 @@ "@types/yauzl/@types/node/undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "electron/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "react-doctor/oxlint/@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.67.0", "", { "os": "android", "cpu": "arm" }, "sha512-VrSi571rDv1N8HaEDM+DEX8nmT0y9jJo8tzzW13vsOWTx59xQczCIJx68n2zWOXRT5YKZsOZXp4qkHN/10x4mw=="], - - "react-doctor/oxlint/@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.67.0", "", { "os": "android", "cpu": "arm64" }, "sha512-l6+NdYxMoRohix5r5bbigW16LPicceCwGcQ6LKKuE1kUdjgFfQolJjrJsQYPFetIs78Gxj/G/f5TEGoTCwj9nQ=="], - - "react-doctor/oxlint/@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.67.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jOzXxS1AxFxhImLIRbtGIMrEwaXcgMw3gR57WB1cRk8ai+vpr6726kxXqVvlNsrXtJ/FrmOm8RxlC0m8SW24Qg=="], - - "react-doctor/oxlint/@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.67.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-3DFAVY94OqjIZHXIPz37yGRSWwOFTAqChQ64/M69GYLawzP0KiwdhDNfqdKKYT0bTR/DNxmMnQsj3ns+8+X/Lg=="], - - "react-doctor/oxlint/@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.67.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-e4dDKZuLu8TR9DEBssWSDahlPgZBwojTTHZUvnjBRJfJJbpxYCjfjKfi0Z1+CSLMiJBwI2yCDtRM1XJQaARjmg=="], - - "react-doctor/oxlint/@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BKytFdcQzbITV3xlnzDUDTEDtbUMCCiC4EaNTDZ4FyT8gdNvBC4gfiLucXp/sQl0XU3p7syTlorUWVVVBZab2g=="], - - "react-doctor/oxlint/@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.67.0", "", { "os": "linux", "cpu": "arm" }, "sha512-XYAv0esBDX7BpTzRDjVX2Vdj+zndd8ll2dFQiaeQ6zTZr7A8GRDTN7fH3FP3jU+O0vCDx85oH/EtG7BzPgAXuw=="], - - "react-doctor/oxlint/@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zizRMjA0i6u/2B0evgda04iycu+MoNuf1pBy6Eh+1CjC5wMEG7qN5zdDKTCvFc0KSYSDM9QTG3gjZHirgtQuKg=="], - - "react-doctor/oxlint/@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.67.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zB/Tf6sUjmmvvbva9Gj3JTJ8rJ9t4I8/U0o6vSRtd0DRIsIuyegBwJAzhSUFQHdMijIRJkW0exs/yBhpw2S20w=="], - - "react-doctor/oxlint/@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.67.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-kgU40Gt74CK0TCsF51KZymkIwN9U0BajKsMijB52zPqOeZU9NAHkA/NSQkZDHEaCakx42DxhXkODiAqf2b4Gug=="], - - "react-doctor/oxlint/@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.67.0", "", { "os": "linux", "cpu": "none" }, "sha512-tOYhkk/iaG9aD3FvGpBFd1Lrw0x0RaVoJBxjUkfNzS50rC5NS5BteNCwgr8A2zCdADrIIoze6D7u6U5Ic++/iQ=="], - - "react-doctor/oxlint/@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.67.0", "", { "os": "linux", "cpu": "none" }, "sha512-sEtywrPb+0b+tHYl1SDCrw903fiC4eyKoNqzP3v+f2JT3Xcv4NEYG+P8rj+eEnX7IWhqV/xj8/JmcmVj21CXaA=="], - - "react-doctor/oxlint/@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.67.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-BvR8Moa0zCLxroOx4vZaZN9nUfwAUpSTwjZdxZyKy4bv3PrzrXrxKR/ZQ0L9wNSvlPhnMJeZfa3q5w6ZCTuN6Q=="], - - "react-doctor/oxlint/@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-mm2cxM6fksOpq6l0uFws8BUGKAR4dNa/cZCn37Npq7PFbhD5HDJqWfnoIvTaeRKMy5XdS2tO0MA0qbHDrnXAAA=="], - - "react-doctor/oxlint/@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.67.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WmbMuLapKyDlobMkXAaAL0Y+Uczh4LETfIfQsUpbId4Ip8Ai82/jqeYTOoUCkuuhBFapgqP253+d83tLKOksJg=="], - - "react-doctor/oxlint/@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.67.0", "", { "os": "none", "cpu": "arm64" }, "sha512-9g/PqxYJelzzTAOR5Y+RiRqdeydhEuXv2KxNeFcAKQ7UsvnWSY1OP4MsuPMbTO2Pf70tz7mFhl1j13H3fyh+8g=="], - - "react-doctor/oxlint/@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.67.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-2VhwE6Gatb0vJGnN0TBuQMbKCOiZlSQ/zJvVWYLK4a9d4iDiJOen/yVQkGpmsJ90MuH66fzi0kEKI0jRQMDxGA=="], - - "react-doctor/oxlint/@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.67.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-EQ3VExXfeM1InbE5+JjufhZZTWy+kHUwgt3yZR7gQ47Je/mE0WspQPan0OJznh493L5anM210YNJtH1PXjTSFg=="], - - "react-doctor/oxlint/@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.67.0", "", { "os": "win32", "cpu": "x64" }, "sha512-bw24y+/1MHS4QDkons3YyHkPT9uCMoLHHgQhb+mb8NOjTYwub1CZ+K9Ngr8aO5DMrDrkqHwTzlTwFP2vS8Y/ZQ=="], } } diff --git a/components/AppContent.tsx b/components/AppContent.tsx index 6c658327..f00a662f 100644 --- a/components/AppContent.tsx +++ b/components/AppContent.tsx @@ -39,20 +39,19 @@ export const AppContent: React.FC = () => { {shell.headerToolbar.isVisible && } -
{ if (e.key === 'Enter' || e.key === ' ') shell.root.onMainClick(); }} - role="button" - tabIndex={0} >
-
+ diff --git a/components/DashboardModal.tsx b/components/DashboardModal.tsx index bd43a5e2..40234fba 100644 --- a/components/DashboardModal.tsx +++ b/components/DashboardModal.tsx @@ -7,7 +7,7 @@ interface DashboardModalProps { onClose: () => void; imagesCount: number; workspaces: Workspace[]; - onExportWorkspaceSnapshot: () => void; + onExportLegacyVisualBatchSnapshot: () => void; onDeepScan: () => void; } @@ -16,7 +16,7 @@ export const DashboardModal: React.FC = ({ onClose, imagesCount, workspaces, - onExportWorkspaceSnapshot, + onExportLegacyVisualBatchSnapshot, onDeepScan, }) => { if (!isOpen) return null; @@ -94,19 +94,19 @@ export const DashboardModal: React.FC = ({

- Workspace Snapshot + Legacy Workspace Snapshot

+
+ )} + +
+ onToggleFavorite(currentImage.id)} + icon={} + label={currentImage?.isFavorite ? 'Unpin from top' : 'Pin to top'} + isActive={currentImage?.isFavorite} + /> + + ) : ( + + ) + } + label="Copy Prompt" + /> + onLoadConfig(currentImage.config)} + icon={} + label="Load Recipe" + /> +
+ +
+ onAddToContext(currentImage)} + icon={} + label="To Context" + /> +
+ } label="Save Local" /> +
+
+ +
+ onRegenerate(currentImage.config)} + icon={} + label="Re-Synthesize" + variant="primary" + /> + onDelete(currentImage.id)} + icon={} + label="Purge" + variant="danger" + /> +
+ + + + ); +} + +export const ImageCarousel: React.FC = ({ activeImage, allImages, activeGenerationConfig, @@ -239,20 +358,24 @@ const ImageCarousel: React.FC = ({ lastSetIndexRef.current = activeIndex; } - const [direction, setDirection] = useState(0); - const [isSliding, setIsSliding] = useState(false); - const [isFullscreen, setIsFullscreen] = useState(false); - const [copiedPrompt, setCopiedPrompt] = useState(false); + const [carouselState, setCarouselState] = useState({ + direction: 0, + isSliding: false, + isFullscreen: false, + copiedPrompt: false, + isComparing: false, + }); + const { direction, isSliding, isFullscreen, copiedPrompt, isComparing } = carouselState; const timeoutRef = useRef(null); React.useEffect(() => { + const timeout = timeoutRef.current; return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); + if (timeout) { + clearTimeout(timeout); } }; }, []); - const [isComparing, setIsComparing] = useState(false); const isProcessingDownloadRef = useRef(false); const containerRef = useRef(null); @@ -268,9 +391,12 @@ const ImageCarousel: React.FC = ({ (index: number) => { if (index === lastSetIndexRef.current || isSliding || index < 0 || index >= allImages.length) return; - setDirection(index > lastSetIndexRef.current ? 1 : -1); - setIsSliding(true); - setIsComparing(false); + setCarouselState((prev) => ({ + ...prev, + direction: index > lastSetIndexRef.current ? 1 : -1, + isSliding: true, + isComparing: false, + })); lastSetIndexRef.current = index; onActiveImageChange(allImages[index].id); @@ -308,10 +434,11 @@ const ImageCarousel: React.FC = ({ if (e.key === 'ArrowRight') handleNextRef.current(); if (e.key === 'ArrowLeft') handlePrevRef.current(); if (e.key === 'Escape' && !isFullscreenRef.current) onCloseRef.current(); - if (e.code === 'Space' && !e.repeat) setIsComparing(true); + if (e.code === 'Space' && !e.repeat) + setCarouselState((prev) => ({ ...prev, isComparing: true })); }; const handleKeyUp = (e: KeyboardEvent) => { - if (e.code === 'Space') setIsComparing(false); + if (e.code === 'Space') setCarouselState((prev) => ({ ...prev, isComparing: false })); }; window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); @@ -351,9 +478,12 @@ const ImageCarousel: React.FC = ({ const handleCopyPrompt = () => { if (!currentImage || copiedPrompt) return; void navigator.clipboard.writeText(currentImage.config.prompt || ''); - setCopiedPrompt(true); + setCarouselState((prev) => ({ ...prev, copiedPrompt: true })); if (timeoutRef.current) clearTimeout(timeoutRef.current); - timeoutRef.current = window.setTimeout(() => setCopiedPrompt(false), 2000); + timeoutRef.current = window.setTimeout( + () => setCarouselState((prev) => ({ ...prev, copiedPrompt: false })), + 2000, + ); }; const hasReference = @@ -410,7 +540,7 @@ const ImageCarousel: React.FC = ({ onClick={() => { if (!document.fullscreenElement) void containerRef.current?.requestFullscreen(); else void document.exitFullscreen(); - setIsFullscreen(!isFullscreen); + setCarouselState((prev) => ({ ...prev, isFullscreen: !prev.isFullscreen })); }} className="p-2 rounded-xl bg-white/5 hover:bg-white/10 text-zinc-500 hover:text-white transition-all cursor-pointer" > @@ -419,7 +549,7 @@ const ImageCarousel: React.FC = ({ @@ -464,7 +594,9 @@ const ImageCarousel: React.FC = ({ initial="enter" animate="center" exit="exit" - onAnimationComplete={() => setIsSliding(false)} + onAnimationComplete={() => + setCarouselState((prev) => ({ ...prev, isSliding: false })) + } className="absolute inset-0 size-full flex items-center justify-center will-change-transform pointer-events-auto" > = ({ - -
-
-

- {currentImage.config.prompt || 'Generated image'} -

-
- - {currentImage.config.model.split('-').slice(0, 2).join(' ').toUpperCase()} - - - {currentImage.config.aspectRatio} OUTPUT - -
-
- -
- {/* COMPARE BUTTON */} - {hasReference && ( -
- -
- )} - -
- onToggleFavorite(currentImage.id)} - icon={} - label={currentImage?.isFavorite ? 'Unpin from top' : 'Pin to top'} - isActive={currentImage?.isFavorite} - /> - - ) : ( - - ) - } - label="Copy Prompt" - /> - onLoadConfig(currentImage.config)} - icon={} - label="Load Recipe" - /> -
- - {/* DOWNLOAD WITH OPTIONS */} -
- onAddToContext(currentImage)} - icon={} - label="To Context" - /> - -
- } - label="Save Local" - /> -
-
- -
- onRegenerate(currentImage.config)} - icon={} - label="Re-Synthesize" - variant="primary" - /> - onDelete(currentImage.id)} - icon={} - label="Purge" - variant="danger" - /> -
-
-
-
+ setCarouselState((prev) => ({ ...prev, isComparing: true }))} + onCompareEnd={() => setCarouselState((prev) => ({ ...prev, isComparing: false }))} + onCopyPrompt={handleCopyPrompt} + onDownload={handleDownloadClick} + onToggleFavorite={onToggleFavorite} + onLoadConfig={onLoadConfig} + onAddToContext={onAddToContext} + onRegenerate={onRegenerate} + onDelete={onDelete} + /> ); }; diff --git a/components/ImageEditorModal.tsx b/components/ImageEditorModal.tsx index dd261a6d..aa6a5cfa 100644 --- a/components/ImageEditorModal.tsx +++ b/components/ImageEditorModal.tsx @@ -12,6 +12,104 @@ interface ImageEditorModalProps { isGenerating: boolean; } +interface ImageEditorControlsPanelProps { + editPrompt: string; + onEditPromptChange: (v: string) => void; + textareaRef: React.RefObject; + brushSize: number; + onBrushSizeChange: (v: number) => void; + historyIndex: number; + isGenerating: boolean; + onUndo: () => void; + onReset: () => void; + onGenerate: () => void; +} + +function ImageEditorControlsPanel({ + editPrompt, + onEditPromptChange, + textareaRef, + brushSize, + onBrushSizeChange, + historyIndex, + isGenerating, + onUndo, + onReset, + onGenerate, +}: ImageEditorControlsPanelProps) { + return ( +
+
+ +