aiplang 2.11.3 → 2.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/aiplang.js CHANGED
@@ -5,7 +5,7 @@ const fs = require('fs')
5
5
  const path = require('path')
6
6
  const http = require('http')
7
7
 
8
- const VERSION = '2.11.3'
8
+ const VERSION = '2.11.5'
9
9
  const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
10
10
  const cmd = process.argv[2]
11
11
  const args = process.argv.slice(3)
@@ -60,6 +60,12 @@ if (!cmd||cmd==='--help'||cmd==='-h') {
60
60
  {{year}} current year
61
61
 
62
62
  Customization:
63
+ # Bancos de dados suportados:
64
+ # ~db sqlite ./app.db (padrão — sem configuração)
65
+ # ~db pg $DATABASE_URL (PostgreSQL)
66
+ # ~db mysql $MYSQL_URL (MySQL / MariaDB)
67
+ # ~db mongodb $MONGODB_URL (MongoDB)
68
+ # ~db redis $REDIS_URL (Redis — cache/session)
63
69
  ~theme accent=#7c3aed radius=1.5rem font=Syne bg=#000 text=#fff
64
70
  hero{...} animate:fade-up
65
71
  row3{...} class:my-class animate:stagger
@@ -680,7 +686,7 @@ function generateTypes(app, srcFile) {
680
686
  }
681
687
 
682
688
  lines.push(`// ── aiplang version ──────────────────────────────────────────`)
683
- lines.push(`export const AIPLANG_VERSION = '2.11.3'`)
689
+ lines.push(`export const AIPLANG_VERSION = '2.11.5'`)
684
690
  lines.push(``)
685
691
  return lines.join('\n')
686
692
  }
@@ -1034,6 +1040,10 @@ function parseBlock(line) {
1034
1040
  return{kind:'pricing',plans,extraClass,animate,variant,style,bg}
1035
1041
  }
1036
1042
 
1043
+ if(line.startsWith('code{')) { const m=line.match(/^code\{([^}]*)\}/);if(m){const pts=m[1].split('|');const vm=line.match(/variant:(\S+)/);const am=line.match(/animate:(\S+)/);return{kind:'code',lang:pts[0]?.trim()||'aip',lines:pts.slice(1).map(l=>l.trim()),variant:vm?.[1],animate:am?.[1]}} }
1044
+ if(line.startsWith('benchmark{')) { const m=line.match(/^benchmark\{([^}]*)\}/);if(m){const vm=line.match(/variant:(\S+)/);const am=line.match(/animate:(\S+)/);return{kind:'benchmark',items:m[1].split('|').map(it=>{const p=it.trim().split(':');return{num:p[0]?.trim(),label:p[1]?.trim(),vs:p[2]?.trim(),pct:parseInt(p[3])||0}}),variant:vm?.[1],animate:am?.[1]}} }
1045
+ if(line.startsWith('install{')) { const m=line.match(/^install\{([^}]*)\}/);if(m){const vm=line.match(/variant:(\S+)/);return{kind:'install',cmds:m[1].split('|').map(c=>c.trim()).filter(Boolean),variant:vm?.[1]}} }
1046
+ if(line.startsWith('feature{')) { const b=parseBlock(line.replace(/^feature/,'row3'));if(b){b.variant='feature'};return b }
1037
1047
  if(line.startsWith('faq{')) {
1038
1048
  const body=line.slice(4,line.lastIndexOf('}')).trim()
1039
1049
  const items=body.split('|').map(i=>{const idx=i.indexOf('>');return{q:i.slice(0,idx).trim(),a:i.slice(idx+1).trim()}}).filter(i=>i.q&&i.a)
@@ -1262,6 +1272,10 @@ function renderBlock(b, page) {
1262
1272
  case 'select': return rSelectBlock(b)
1263
1273
  case 'pricing': return rPricing(b)
1264
1274
  case 'faq': return rFaq(b)
1275
+ case 'code': return rCode(b)
1276
+ case 'benchmark': return rBenchmark(b)
1277
+ case 'install': return rInstall(b)
1278
+ case 'feature': return rRow(b)
1265
1279
  case 'testimonial': return rTestimonial(b)
1266
1280
  case 'gallery': return rGallery(b)
1267
1281
  case 'raw': return (b.html||'')+'\n'
@@ -1341,6 +1355,49 @@ function rNav(b) {
1341
1355
  return `<nav class="fx-nav">${brand}<button class="fx-hamburger" onclick="this.classList.toggle('open');document.querySelector('.fx-nav-links').classList.toggle('open')" aria-label="Menu"><span></span><span></span><span></span></button><div class="fx-nav-links">${links}</div></nav>\n`
1342
1356
  }
1343
1357
 
1358
+
1359
+
1360
+ // ── Parser: code{lang|linha1|linha2} ─────────────────────────────
1361
+ function parseCode(line) {
1362
+ const m = line.match(/^code\{([^}]*)\}/)
1363
+ if (!m) return null
1364
+ const parts = m[1].split('|')
1365
+ const lang = parts[0]?.trim() || 'aip'
1366
+ const lines = parts.slice(1).map(l => l.trim())
1367
+ const b = parseBlockMeta(line)
1368
+ return { ...b, kind:'code', lang, lines }
1369
+ }
1370
+
1371
+ // ── Parser: benchmark{Num:Label:vs texto|...} ─────────────────────
1372
+ function parseBenchmark(line) {
1373
+ const m = line.match(/^benchmark\{([^}]*)\}/)
1374
+ if (!m) return null
1375
+ const items = m[1].split('|').map(item => {
1376
+ const parts = item.trim().split(':')
1377
+ return { num: parts[0]?.trim(), label: parts[1]?.trim(), vs: parts[2]?.trim(), pct: parts[3]?.trim() }
1378
+ })
1379
+ const b = parseBlockMeta(line)
1380
+ return { ...b, kind:'benchmark', items }
1381
+ }
1382
+
1383
+ // ── Parser: install{cmd1|cmd2|...} ───────────────────────────────
1384
+ function parseInstall(line) {
1385
+ const m = line.match(/^install\{([^}]*)\}/)
1386
+ if (!m) return null
1387
+ const cmds = m[1].split('|').map(c => c.trim()).filter(Boolean)
1388
+ const b = parseBlockMeta(line)
1389
+ return { ...b, kind:'install', cmds }
1390
+ }
1391
+
1392
+ // ── Parser: feature{emoji>Título>Desc | ...} (alias row3 otimizado)
1393
+ function parseFeature(line) {
1394
+ // Converte feature{} em row{} com cols=3 e variant=feature
1395
+ const inner = line.replace(/^feature/, 'row3')
1396
+ const b = parseRow(inner)
1397
+ if (b) b.variant = 'feature'
1398
+ return b
1399
+ }
1400
+
1344
1401
  function rHero(b) {
1345
1402
  let h1='',sub='',img='',ctas=''
1346
1403
  for(const item of (b.items||[])) for(const f of item){
@@ -1352,6 +1409,10 @@ function rHero(b) {
1352
1409
  const v = b.variant || (img ? 'split' : 'centered')
1353
1410
  const bgStyle = b.bg ? ` style="background:${b.bg}"` : b.style ? ` style="${b.style.replace(/,/g,';')}"` : ''
1354
1411
  const inlineStyle = b.style && !b.bg ? ` style="${b.style.replace(/,/g,';')}"` : ''
1412
+ if (v === 'landing') {
1413
+ return `<section class="fx-hero fx-hero-landing"${bgStyle}><div class="fx-hero-inner">${h1}${sub}${ctas}</div></section>
1414
+ `
1415
+ }
1355
1416
  if (v === 'minimal') {
1356
1417
  return `<section class="fx-hero fx-hero-minimal"${bgStyle}><div class="fx-hero-inner">${h1}${sub}${ctas}</div></section>\n`
1357
1418
  }
@@ -1424,13 +1485,102 @@ function rSect(b) {
1424
1485
  return `<section class="fx-sect${cls}"${bgStyle}>${inner}</section>\n`
1425
1486
  }
1426
1487
 
1488
+
1489
+ // ── rCode: code window com syntax highlight ───────────────────────
1490
+ function rCode(b) {
1491
+ const lang = b.lang || 'aip'
1492
+ const id = 'code_' + Math.random().toString(36).slice(2,7)
1493
+ const highlighted = (b.lines||[]).map(line => {
1494
+ let l = esc(line)
1495
+ if(lang==='aip'||lang==='aiplang') {
1496
+ l = l.replace(/^(~\w+)/g,'<span class="fx-kw">$1</span>')
1497
+ l = l.replace(/\$([\w.]+)/g,'<span class="fx-nb">$$$1</span>')
1498
+ l = l.replace(/(\{[^}]*\})/g,'<span class="fx-op">$1</span>')
1499
+ l = l.replace(/(#[\w-]+(?:\.\w+)*)/g,'<span class="fx-comment">$1</span>')
1500
+ } else if(lang==='bash'||lang==='sh') {
1501
+ l = l.replace(/^(\$ )/,'<span class="fx-kw">$1</span>')
1502
+ l = l.replace(/(#.*$)/,'<span class="fx-comment">$1</span>')
1503
+ l = l.replace(/(npx aiplang \w+)/g,'<span class="fx-fn">$1</span>')
1504
+ } else if(lang==='js'||lang==='ts') {
1505
+ l = l.replace(/\b(const|let|var|function|async|await|return|import|from|export|if|else|for)\b/g,'<span class="fx-kw">$1</span>')
1506
+ l = l.replace(/(\/\/.*$)/,'<span class="fx-comment">$1</span>')
1507
+ l = l.replace(/(['"`][^'"`]*['"`])/g,'<span class="fx-st">$1</span>')
1508
+ }
1509
+ return `<div class="fx-code-line">${l}</div>`
1510
+ }).join('')
1511
+ const copyBtn = `<button class="fx-code-copy" onclick="(function(b){navigator.clipboard&&navigator.clipboard.writeText(b.querySelectorAll('.fx-code-line').length?Array.from(b.querySelectorAll('.fx-code-line')).map(l=>l.innerText).join('\\n'):'');var t=b.querySelector('.fx-code-copy');t&&(t.textContent='copiado!',setTimeout(()=>t.textContent='copiar',1500))})(this.closest('.fx-code-window'))">copiar</button>`
1512
+ return `<div class="fx-code-window"><div class="fx-code-bar"><div class="fx-dots"><span></span><span></span><span></span></div><span class="fx-code-lang">${esc(lang)}</span>${copyBtn}</div><div class="fx-code-body" id="${id}">${highlighted}</div></div>\n`
1513
+ }
1514
+
1515
+ // ── rBenchmark: cards de benchmark com número + barra ─────────────
1516
+ function rBenchmark(b) {
1517
+ const cards = (b.items||[]).map(item => {
1518
+ const pct = item.pct || (item.num && item.num.includes('%') ? parseInt(item.num) : 85)
1519
+ const n = item.num || ''
1520
+ const isLow = pct < 20
1521
+ return `<div class="fx-bench-card">
1522
+ <div class="fx-bench-label">${esc(item.label||'')}</div>
1523
+ <div class="fx-bench-num">${esc(n)}</div>
1524
+ ${item.vs ? `<div class="fx-bench-vs">${esc(item.vs)}</div>` : ''}
1525
+ <div class="fx-bench-bar"><div class="fx-bench-fill" style="width:${isLow?pct+'%':pct+'%'}"></div></div>
1526
+ </div>`
1527
+ }).join('')
1528
+ return `<div class="fx-benchmark">${cards}</div>\n`
1529
+ }
1530
+
1531
+ // ── rInstall: multi-step code box com botões ──────────────────────
1532
+ function rInstall(b) {
1533
+ const steps = (b.cmds||[]).map((cmd,i) => {
1534
+ const isComment = cmd.startsWith('#')
1535
+ if(isComment) return `<div class="fx-install-comment">${esc(cmd.slice(1).trim())}</div>`
1536
+ return `<div class="fx-install-line"><span class="fx-install-prompt">$</span><span class="fx-install-cmd">${esc(cmd)}</span></div>`
1537
+ }).join('')
1538
+ const firstCmd = (b.cmds||[]).find(c => !c.startsWith('#')) || ''
1539
+ const copy = `navigator.clipboard&&navigator.clipboard.writeText(${JSON.stringify(firstCmd)})`
1540
+ return `<div class="fx-install-wrap">
1541
+ <div class="fx-code-bar"><div class="fx-dots"><span></span><span></span><span></span></div><span class="fx-code-lang">terminal</span><button class="fx-code-copy" onclick="${copy};var t=this;t.textContent='copiado!';setTimeout(()=>t.textContent='copiar',1500)">copiar</button></div>
1542
+ <div class="fx-install-body">${steps}</div>
1543
+ </div>\n`
1544
+ }
1545
+
1546
+ // ── rStats upgrade: suporte a subtítulo via "val:label:vs" ────────
1547
+ function rStatsUpgraded(b) {
1548
+ const cells = (b.items||[]).map(item => {
1549
+ const raw = item[0]?.text || ''
1550
+ const parts = raw.split(':')
1551
+ const val = parts[0]?.trim()
1552
+ const lbl = parts[1]?.trim()
1553
+ const vs = parts[2]?.trim()
1554
+ const bind = isDyn(val) ? ` data-fx-bind="${esc(val)}"` : ''
1555
+ return `<div class="fx-stat">
1556
+ <div class="fx-stat-val"${bind}>${esc(val)}</div>
1557
+ <div class="fx-stat-lbl">${esc(lbl||'')}</div>
1558
+ ${vs ? `<div class="fx-stat-vs">${esc(vs)}</div>` : ''}
1559
+ </div>`
1560
+ }).join('')
1561
+ return `<div class="fx-stats">${cells}</div>\n`
1562
+ }
1563
+
1427
1564
  function rFoot(b) {
1565
+ let brand='', links='', note=''
1566
+ let itemIdx = 0
1567
+ for(const item of (b.items||[])) for(const f of item){
1568
+ if(f.isLink) links+=`<a href="${esc(f.path)}" class="fx-footer-link">${esc(f.label)}</a>`
1569
+ else if(itemIdx===0) { brand=`<span class="fx-footer-brand">${esc(f.text)}</span>`; itemIdx++ }
1570
+ else note=`<span class="fx-footer-note">${esc(f.text)}</span>`
1571
+ }
1572
+ if(brand||links){
1573
+ return `<footer class="fx-footer"><div class="fx-footer-inner">${brand}<div class="fx-footer-links">${links}</div>${note}</div></footer>
1574
+ `
1575
+ }
1576
+ // fallback centrado
1428
1577
  let inner=''
1429
1578
  for(const item of (b.items||[])) for(const f of item){
1430
1579
  if(f.isLink) inner+=`<a href="${esc(f.path)}" class="fx-footer-link">${esc(f.label)}</a>`
1431
1580
  else inner+=`<p class="fx-footer-text">${esc(f.text)}</p>`
1432
1581
  }
1433
- return `<footer class="fx-footer">${inner}</footer>\n`
1582
+ return `<footer class="fx-footer">${inner}</footer>
1583
+ `
1434
1584
  }
1435
1585
 
1436
1586
  function rTable(b) {
@@ -1551,6 +1701,51 @@ function genThemeVarCSS(t) {
1551
1701
 
1552
1702
  function css(theme) {
1553
1703
  const base=`*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html{scroll-behavior:smooth}body{font-family:-apple-system,'Segoe UI',system-ui,sans-serif;-webkit-font-smoothing:antialiased;min-height:100vh}a{text-decoration:none;color:inherit}input,button,select{font-family:inherit}img{max-width:100%;height:auto}.fx-nav{display:flex;align-items:center;justify-content:space-between;padding:1rem 2.5rem;position:sticky;top:0;z-index:50;backdrop-filter:blur(12px);flex-wrap:wrap;gap:.5rem}.fx-brand{font-size:1.25rem;font-weight:800;letter-spacing:-.03em}.fx-nav-links{display:flex;align-items:center;gap:1.75rem}.fx-nav-link{font-size:.875rem;font-weight:500;opacity:.65;transition:opacity .15s}.fx-nav-link:hover{opacity:1}.fx-hamburger{display:none;flex-direction:column;gap:5px;background:none;border:none;cursor:pointer;padding:.25rem}.fx-hamburger span{display:block;width:22px;height:2px;background:currentColor;transition:all .2s;border-radius:1px}.fx-hamburger.open span:nth-child(1){transform:rotate(45deg) translate(5px,5px)}.fx-hamburger.open span:nth-child(2){opacity:0}.fx-hamburger.open span:nth-child(3){transform:rotate(-45deg) translate(5px,-5px)}@media(max-width:640px){.fx-hamburger{display:flex}.fx-nav-links{display:none;width:100%;flex-direction:column;align-items:flex-start;gap:.75rem;padding:.75rem 0}.fx-nav-links.open{display:flex}}.fx-hero{display:flex;align-items:center;justify-content:center;min-height:92vh;padding:4rem 1.5rem}.fx-hero-split{display:grid;grid-template-columns:1fr 1fr;gap:3rem;align-items:center;padding:4rem 2.5rem;min-height:70vh}@media(max-width:768px){.fx-hero-split{grid-template-columns:1fr}}.fx-hero-img{width:100%;border-radius:1.25rem;object-fit:cover;max-height:500px}.fx-hero-inner{max-width:56rem;text-align:center;display:flex;flex-direction:column;align-items:center;gap:1.5rem}.fx-hero-split .fx-hero-inner{text-align:left;align-items:flex-start;max-width:none}.fx-title{font-size:clamp(2.5rem,8vw,5.5rem);font-weight:900;letter-spacing:-.04em;line-height:1}.fx-sub{font-size:clamp(1rem,2vw,1.25rem);line-height:1.75;max-width:40rem}.fx-cta{display:inline-flex;align-items:center;padding:.875rem 2.5rem;border-radius:.75rem;font-weight:700;font-size:1rem;letter-spacing:-.01em;transition:transform .15s;margin:.25rem}.fx-cta:hover{transform:translateY(-1px)}.fx-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:3rem;padding:5rem 2.5rem;text-align:center}.fx-stat-val{font-size:clamp(2.5rem,5vw,4rem);font-weight:900;letter-spacing:-.04em;line-height:1}.fx-stat-lbl{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.1em;margin-top:.5rem}.fx-grid{display:grid;gap:1.25rem;padding:1rem 2.5rem 5rem}.fx-grid-2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}.fx-grid-3{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}.fx-grid-4{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.fx-card{border-radius:1rem;padding:1.75rem;transition:transform .2s,box-shadow .2s}.fx-card:hover{transform:translateY(-2px)}.fx-card-img{width:100%;border-radius:.75rem;object-fit:cover;height:180px;margin-bottom:1rem}.fx-icon{font-size:2rem;margin-bottom:1rem}.fx-card-title{font-size:1.0625rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.5rem}.fx-card-body{font-size:.875rem;line-height:1.65}.fx-card-link{font-size:.8125rem;font-weight:600;display:inline-block;margin-top:1rem;opacity:.6;transition:opacity .15s}.fx-card-link:hover{opacity:1}.fx-sect{padding:5rem 2.5rem}.fx-sect-title{font-size:clamp(1.75rem,4vw,3rem);font-weight:800;letter-spacing:-.04em;margin-bottom:1.5rem;text-align:center}.fx-sect-body{font-size:1rem;line-height:1.75;text-align:center;max-width:48rem;margin:0 auto}.fx-form-wrap{padding:3rem 2.5rem;display:flex;justify-content:center}.fx-form{width:100%;max-width:28rem;border-radius:1.25rem;padding:2.5rem}.fx-field{margin-bottom:1.25rem}.fx-label{display:block;font-size:.8125rem;font-weight:600;margin-bottom:.5rem}.fx-input{width:100%;padding:.75rem 1rem;border-radius:.625rem;font-size:.9375rem;outline:none;transition:box-shadow .15s}.fx-input:focus{box-shadow:0 0 0 3px rgba(37,99,235,.35)}.fx-btn{width:100%;padding:.875rem 1.5rem;border:none;border-radius:.625rem;font-size:.9375rem;font-weight:700;cursor:pointer;margin-top:.5rem;transition:transform .15s,opacity .15s;letter-spacing:-.01em}.fx-btn:hover{transform:translateY(-1px)}.fx-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.fx-btn-wrap{padding:0 2.5rem 1.5rem}.fx-standalone-btn{width:auto;padding:.75rem 2rem;margin-top:0}.fx-form-msg{font-size:.8125rem;padding:.5rem 0;min-height:1.5rem;text-align:center}.fx-form-err{color:#f87171}.fx-form-ok{color:#4ade80}.fx-table-wrap{overflow-x:auto;padding:0 2.5rem 4rem}.fx-table{width:100%;border-collapse:collapse;font-size:.875rem}.fx-th{text-align:left;padding:.875rem 1.25rem;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em}.fx-th-actions{opacity:.6}.fx-tr{transition:background .1s}.fx-td{padding:.875rem 1.25rem}.fx-td-empty{padding:2rem 1.25rem;text-align:center;opacity:.4}.fx-td-actions{white-space:nowrap;padding:.5rem 1rem!important}.fx-action-btn{border:none;cursor:pointer;font-size:.75rem;font-weight:600;padding:.3rem .75rem;border-radius:.375rem;margin-right:.375rem;font-family:inherit;transition:opacity .15s}.fx-action-btn:hover{opacity:.85}.fx-edit-btn{background:#1e40af;color:#93c5fd}.fx-delete-btn{background:#7f1d1d;color:#fca5a5}.fx-select-wrap{padding:.5rem 2.5rem}.fx-select-block{width:auto;min-width:200px;margin-top:0}.fx-pricing{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1.5rem;padding:2rem 2.5rem 5rem;align-items:start}.fx-pricing-card{border-radius:1.25rem;padding:2rem;position:relative;transition:transform .2s}.fx-pricing-featured{transform:scale(1.03)}.fx-pricing-badge{position:absolute;top:-12px;left:50%;transform:translateX(-50%);background:#2563eb;color:#fff;font-size:.7rem;font-weight:700;padding:.25rem .875rem;border-radius:999px;white-space:nowrap;letter-spacing:.05em}.fx-pricing-name{font-size:.875rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-bottom:.5rem;opacity:.7}.fx-pricing-price{font-size:3rem;font-weight:900;letter-spacing:-.05em;line-height:1;margin-bottom:.75rem}.fx-pricing-desc{font-size:.875rem;line-height:1.65;margin-bottom:1.5rem;opacity:.7}.fx-pricing-cta{display:block;text-align:center;padding:.75rem;border-radius:.625rem;font-weight:700;font-size:.9rem;transition:opacity .15s}.fx-pricing-cta:hover{opacity:.85}.fx-faq{max-width:48rem;margin:0 auto}.fx-faq-item{border-radius:.75rem;margin-bottom:.625rem;cursor:pointer;overflow:hidden;transition:background .15s}.fx-faq-q{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.25rem;font-size:.9375rem;font-weight:600}.fx-faq-arrow{transition:transform .2s;font-size:.75rem;opacity:.5}.fx-faq-item.open .fx-faq-arrow{transform:rotate(90deg)}.fx-faq-a{max-height:0;overflow:hidden;padding:0 1.25rem;font-size:.875rem;line-height:1.7;transition:max-height .3s,padding .3s}.fx-faq-item.open .fx-faq-a{max-height:300px;padding:.75rem 1.25rem 1.25rem}.fx-testi-wrap{padding:5rem 2.5rem;display:flex;justify-content:center}.fx-testi{max-width:42rem;text-align:center;display:flex;flex-direction:column;align-items:center;gap:1.25rem}.fx-testi-img{width:64px;height:64px;border-radius:50%;object-fit:cover}.fx-testi-avatar{width:64px;height:64px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.5rem;font-weight:700;background:#1e293b}.fx-testi-quote{font-size:1.25rem;line-height:1.7;font-style:italic;opacity:.9}.fx-testi-author{font-size:.875rem;font-weight:600;opacity:.5}.fx-gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.75rem;padding:1rem 2.5rem 4rem}.fx-gallery-item{border-radius:.75rem;overflow:hidden;aspect-ratio:4/3}.fx-gallery-item img{width:100%;height:100%;object-fit:cover;transition:transform .3s}.fx-gallery-item:hover img{transform:scale(1.04)}.fx-if-wrap{display:contents}.fx-footer{padding:3rem 2.5rem;text-align:center}.fx-footer-text{font-size:.8125rem}.fx-footer-link{font-size:.8125rem;margin:0 .75rem;opacity:.5;transition:opacity .15s}.fx-footer-link:hover{opacity:1}
1704
+ /* ── code window ── */
1705
+ .fx-code-window{border-radius:.875rem;overflow:hidden;border:1px solid rgba(255,255,255,.08);margin:0 2.5rem 2rem}
1706
+ .fx-code-bar{display:flex;align-items:center;gap:.75rem;padding:.625rem 1rem;background:rgba(255,255,255,.04);border-bottom:1px solid rgba(255,255,255,.06)}
1707
+ .fx-dots{display:flex;gap:.375rem}
1708
+ .fx-dots span{width:10px;height:10px;border-radius:50%;background:rgba(255,255,255,.15)}
1709
+ .fx-dots span:nth-child(1){background:#ff5f57}.fx-dots span:nth-child(2){background:#febc2e}.fx-dots span:nth-child(3){background:#28c840}
1710
+ .fx-code-lang{font-size:.62rem;letter-spacing:.1em;text-transform:uppercase;opacity:.35;font-family:monospace;margin-left:.25rem}
1711
+ .fx-code-copy{margin-left:auto;font-family:monospace;font-size:.62rem;letter-spacing:.1em;text-transform:uppercase;background:none;border:1px solid rgba(255,255,255,.15);color:rgba(255,255,255,.4);padding:.2rem .625rem;border-radius:.3rem;cursor:pointer;transition:all .15s}
1712
+ .fx-code-copy:hover{border-color:rgba(255,255,255,.3);color:rgba(255,255,255,.7)}
1713
+ .fx-code-body{padding:1.375rem 1.5rem;overflow-x:auto}
1714
+ .fx-code-line{font-family:"JetBrains Mono","Fira Code","Courier New",monospace;font-size:.8rem;line-height:1.75;color:#8899aa;white-space:pre}
1715
+ .fx-kw{color:#c792ea}.fx-st{color:#c3e88d}.fx-fn{color:#82aaff}.fx-nb{color:#f78c6c}.fx-op{color:var(--accent,#ff5722)}.fx-comment{color:#3d5166;font-style:italic}
1716
+ /* ── benchmark ── */
1717
+ .fx-benchmark{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:1rem;padding:1rem 2.5rem 4rem}
1718
+ .fx-bench-card{border-radius:1rem;padding:1.5rem;border:1px solid rgba(255,255,255,.06);background:rgba(255,255,255,.02);position:relative;overflow:hidden}
1719
+ .fx-bench-card::before{content:"";position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--accent,#ff5722),transparent)}
1720
+ .fx-bench-label{font-size:.6rem;letter-spacing:.12em;text-transform:uppercase;opacity:.5;margin-bottom:.5rem;font-family:monospace}
1721
+ .fx-bench-num{font-size:clamp(2rem,5vw,3.5rem);font-weight:900;letter-spacing:-.04em;line-height:1;color:var(--accent,#ff5722)}
1722
+ .fx-bench-vs{font-size:.7rem;opacity:.4;margin-top:.25rem;font-family:monospace}
1723
+ .fx-bench-bar{margin-top:1rem;height:5px;background:rgba(255,255,255,.06);border-radius:3px;overflow:hidden}
1724
+ .fx-bench-fill{height:100%;border-radius:3px;background:var(--accent,#ff5722);transition:width 1.5s cubic-bezier(.4,0,.2,1)}
1725
+ /* ── install ── */
1726
+ .fx-install-wrap{border-radius:.875rem;overflow:hidden;border:1px solid rgba(255,255,255,.08);margin:0 2.5rem 2rem;max-width:540px}
1727
+ .fx-install-body{padding:1.25rem 1.5rem}
1728
+ .fx-install-line{display:flex;gap:.75rem;padding:.25rem 0;font-family:"JetBrains Mono","Courier New",monospace;font-size:.8rem;line-height:1.7}
1729
+ .fx-install-prompt{color:var(--accent,#ff5722);flex-shrink:0}
1730
+ .fx-install-cmd{color:rgba(255,255,255,.85)}
1731
+ .fx-install-comment{font-family:"JetBrains Mono","Courier New",monospace;font-size:.72rem;color:rgba(255,255,255,.25);padding:.25rem 0;font-style:italic}
1732
+ /* ── stats upgrade ── */
1733
+ .fx-stat-vs{font-size:.65rem;opacity:.35;margin-top:.2rem;letter-spacing:.02em}
1734
+ /* ── hero landing (dark variant) grid + glow ── */
1735
+ .fx-hero-landing{position:relative;overflow:hidden}
1736
+ .fx-hero-landing::before{content:"";position:absolute;inset:0;background:linear-gradient(rgba(255,255,255,.05) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.05) 1px,transparent 1px);background-size:60px 60px;mask-image:radial-gradient(ellipse 70% 60% at 50% 50%,black,transparent);pointer-events:none}
1737
+ .fx-hero-landing::after{content:"";position:absolute;width:700px;height:500px;border-radius:50%;background:radial-gradient(ellipse,rgba(255,87,34,.13) 0%,transparent 70%);left:50%;top:50%;transform:translate(-50%,-50%);pointer-events:none;animation:fx-breathe 6s ease-in-out infinite}
1738
+ @keyframes fx-breathe{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-50%,-50%) scale(1.1)}}
1739
+ /* ── feature grid ── */
1740
+ .fx-grid-feature{gap:1rem}
1741
+ .fx-grid-feature .fx-card{transition:border-color .2s,transform .15s}
1742
+ .fx-grid-feature .fx-card:hover{border-color:rgba(255,87,34,.25)}
1743
+ .fx-grid-feature .fx-icon{width:44px;height:44px;border-radius:.75rem;display:flex;align-items:center;justify-content:center;background:rgba(255,87,34,.08);border:1px solid rgba(255,87,34,.15)}
1744
+ /* ── footer upgrade ── */
1745
+ .fx-footer-inner{max-width:1100px;margin:0 auto;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:1rem}
1746
+ .fx-footer-brand{font-size:1rem;font-weight:800;letter-spacing:-.02em}
1747
+ .fx-footer-links{display:flex;gap:1.5rem;flex-wrap:wrap}
1748
+ .fx-footer-note{font-size:.72rem;opacity:.3;font-family:monospace}
1554
1749
  .fx-hero-minimal{min-height:50vh!important}
1555
1750
  .fx-hero-minimal .fx-hero-inner{gap:1rem}
1556
1751
  .fx-hero-tall{min-height:98vh!important}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.11.3",
3
+ "version": "2.11.5",
4
4
  "description": "AI-first web language. One .aip file = complete app. Frontend + backend + database + auth.",
5
5
  "keywords": [
6
6
  "aiplang",
@@ -43,12 +43,18 @@
43
43
  "@sqlite.org/sqlite-wasm": "^3.51.2-build8",
44
44
  "bcryptjs": "^2.4.3",
45
45
  "better-sqlite3": "^12.8.0",
46
+ "ioredis": "^5.3.2",
46
47
  "jsonwebtoken": "^9.0.2",
48
+ "mongodb": "^6.5.0",
49
+ "mysql2": "^3.9.0",
47
50
  "nodemailer": "^8.0.3",
48
51
  "pg": "^8.11.0",
49
52
  "sql.js": "^1.10.3",
50
53
  "stripe": "^14.0.0",
51
54
  "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.44.0",
52
55
  "ws": "^8.16.0"
56
+ },
57
+ "optionalDependencies": {
58
+ "better-sqlite3": "^9.4.0"
53
59
  }
54
60
  }
package/server/server.js CHANGED
@@ -32,6 +32,12 @@ let SQL, DB_FILE, _db = null
32
32
  let _pgPool = null // PostgreSQL connection pool
33
33
  let _dbDriver = 'sqlite' // 'sqlite' | 'postgres'
34
34
  let _useBetter = false // true when better-sqlite3 is available
35
+ let _mysqlPool = null // MySQL/MariaDB (mysql2)
36
+ let _mongoClient= null // MongoDB client
37
+ let _mongoDB = null // MongoDB database
38
+ let _redisClient= null // Redis (ioredis)
39
+ let _useMongo = false
40
+ let _useRedis = false
35
41
 
36
42
  async function getDB(dbConfig = { driver: 'sqlite', dsn: ':memory:' }) {
37
43
  if (_db || _pgPool) return _db || _pgPool
@@ -39,16 +45,72 @@ async function getDB(dbConfig = { driver: 'sqlite', dsn: ':memory:' }) {
39
45
  const dsn = dbConfig.dsn || ':memory:'
40
46
  _dbDriver = driver
41
47
 
42
- if (driver === 'postgres' || driver === 'postgresql' || dsn.startsWith('postgres')) {
48
+ // ── PostgreSQL ────────────────────────────────────────────────
49
+ if (driver === 'postgres' || dsn.startsWith('postgres')) {
43
50
  try {
44
51
  const { Pool } = require('pg')
45
52
  _pgPool = new Pool({ connectionString: dsn, ssl: dsn.includes('ssl=true') ? { rejectUnauthorized: false } : false })
46
- await _pgPool.query('SELECT 1') // test connection
53
+ await _pgPool.query('SELECT 1')
47
54
  console.log('[aiplang] DB: PostgreSQL ✓')
48
55
  return _pgPool
49
56
  } catch (e) {
50
- console.error('[aiplang] PostgreSQL connection failed:', e.message)
51
- console.log('[aiplang] Falling back to SQLite :memory:')
57
+ console.error('[aiplang] PostgreSQL falhou:', e.message)
58
+ _dbDriver = 'sqlite'
59
+ }
60
+ }
61
+
62
+ // ── MySQL / MariaDB ───────────────────────────────────────────
63
+ if (driver === 'mysql' || dsn.startsWith('mysql') || dsn.startsWith('mariadb')) {
64
+ try {
65
+ const mysql = require('mysql2/promise')
66
+ _mysqlPool = await mysql.createPool({
67
+ uri: dsn.startsWith('mariadb') ? dsn.replace('mariadb://', 'mysql://') : dsn,
68
+ waitForConnections: true,
69
+ connectionLimit: 10,
70
+ queueLimit: 0,
71
+ ssl: dsn.includes('ssl=true') ? { rejectUnauthorized: false } : undefined
72
+ })
73
+ await _mysqlPool.query('SELECT 1')
74
+ _dbDriver = 'mysql'
75
+ console.log('[aiplang] DB: MySQL/MariaDB ✓')
76
+ return _mysqlPool
77
+ } catch (e) {
78
+ console.error('[aiplang] MySQL falhou:', e.message)
79
+ _dbDriver = 'sqlite'
80
+ }
81
+ }
82
+
83
+ // ── MongoDB ───────────────────────────────────────────────────
84
+ if (driver === 'mongodb' || dsn.startsWith('mongodb')) {
85
+ try {
86
+ const { MongoClient } = require('mongodb')
87
+ _mongoClient = new MongoClient(dsn)
88
+ await _mongoClient.connect()
89
+ // Extrair nome do banco da URL
90
+ const dbName = dsn.split('/').pop()?.split('?')[0] || 'aiplang'
91
+ _mongoDB = _mongoClient.db(dbName)
92
+ _useMongo = true
93
+ _dbDriver = 'mongodb'
94
+ console.log('[aiplang] DB: MongoDB ✓ (' + dbName + ')')
95
+ return _mongoDB
96
+ } catch (e) {
97
+ console.error('[aiplang] MongoDB falhou:', e.message)
98
+ _dbDriver = 'sqlite'
99
+ }
100
+ }
101
+
102
+ // ── Redis ─────────────────────────────────────────────────────
103
+ if (driver === 'redis' || dsn.startsWith('redis')) {
104
+ try {
105
+ const Redis = require('ioredis')
106
+ _redisClient = new Redis(dsn, { lazyConnect: false, maxRetriesPerRequest: 3 })
107
+ await _redisClient.ping()
108
+ _useRedis = true
109
+ _dbDriver = 'redis'
110
+ console.log('[aiplang] DB: Redis ✓')
111
+ return _redisClient
112
+ } catch (e) {
113
+ console.error('[aiplang] Redis falhou:', e.message)
52
114
  _dbDriver = 'sqlite'
53
115
  }
54
116
  }
@@ -89,7 +151,48 @@ function persistDB() {
89
151
  }
90
152
  let _dirty = false, _persistTimer = null
91
153
 
154
+ // ── MongoDB helpers ───────────────────────────────────────────────
155
+ function _sqlWhereToMongo(sql, params) {
156
+ // Converte WHERE simples: id = ? / email = ? → {id: val, email: val}
157
+ const filter = {}
158
+ const whereMatch = sql.match(/WHERE\s+(.+?)(?:ORDER|LIMIT|$)/is)
159
+ if (!whereMatch) return filter
160
+ const conditions = whereMatch[1].trim()
161
+ const parts = conditions.split(/\s+AND\s+/i)
162
+ let paramIdx = 0
163
+ for (const part of parts) {
164
+ const m = part.trim().match(/^(\w+)\s*=\s*\?$/)
165
+ if (m && paramIdx < params.length) {
166
+ filter[m[1]] = params[paramIdx++]
167
+ }
168
+ }
169
+ return filter
170
+ }
171
+
172
+ function _mongoToObj(doc) {
173
+ if (!doc) return null
174
+ const { _id, ...rest } = doc
175
+ return { id: _id?.toString() || rest.id, ...rest }
176
+ }
177
+
178
+ // ── MySQL: CREATE TABLE equivalente ──────────────────────────────
179
+ function _mysqlType(sqliteType) {
180
+ return {
181
+ 'TEXT': 'TEXT',
182
+ 'INTEGER': 'INT',
183
+ 'REAL': 'DOUBLE',
184
+ 'BLOB': 'BLOB'
185
+ }[sqliteType] || 'TEXT'
186
+ }
187
+
188
+
92
189
  function dbRun(sql, params = []) {
190
+ if (_mysqlPool) {
191
+ // MySQL: async, mas dbRun é chamado sync em alguns contextos
192
+ // Enfileirar de forma segura
193
+ _mysqlPool.execute(sql, params).catch(e => console.debug('[aiplang:mysql]', e?.message))
194
+ return
195
+ }
93
196
  if (_useBetter && !_pgPool) {
94
197
  // better-sqlite3: synchronous, native — 30x faster than sql.js writes
95
198
  _getStmt(sql).run(...params)
@@ -117,7 +220,8 @@ function convertPlaceholders(sql) {
117
220
  }
118
221
 
119
222
  async function dbRunAsync(sql, params = []) {
120
- if (_pgPool) return _pgPool.query(convertPlaceholders(sql), params)
223
+ if (_mysqlPool) return _mysqlPool.execute(sql, params)
224
+ if (_pgPool) return _pgPool.query(convertPlaceholders(sql), params)
121
225
  dbRun(sql, params)
122
226
  }
123
227
 
@@ -181,18 +285,18 @@ function _getStmt(sql) {
181
285
  }
182
286
 
183
287
  function dbAll(sql, params = [], _cacheKey = null, _cacheTables = null) {
184
- if (_pgPool) return []
288
+ if (_pgPool) return [] // PostgreSQL usa dbAllAsync
289
+ if (_mysqlPool) return [] // MySQL usa dbAllAsync
290
+ if (_useMongo) return [] // MongoDB usa dbAllAsync
185
291
  if (_cacheKey && !params.length) {
186
292
  const cached = _cacheGet(_cacheKey)
187
293
  if (cached !== null) return { __cached: true, __body: cached }
188
294
  }
189
295
  let rows
190
296
  if (_useBetter) {
191
- // better-sqlite3: synchronous, native, 7x faster
192
297
  const stmt = _getStmt(sql)
193
298
  rows = params.length ? stmt.all(...params) : stmt.all()
194
299
  } else {
195
- // sql.js fallback
196
300
  const stmt = _db.prepare(sql)
197
301
  stmt.bind(params); rows = []
198
302
  while (stmt.step()) rows.push(stmt.getAsObject())
@@ -202,12 +306,33 @@ function dbAll(sql, params = [], _cacheKey = null, _cacheTables = null) {
202
306
  return rows
203
307
  }
204
308
 
205
- async function dbAllAsync(sql, params = []) {
309
+ async function dbAllAsync(sql, params = [], _cacheKey = null, _cacheTables = null) {
310
+ // Cache check (universal)
311
+ if (_cacheKey && !params.length) {
312
+ const cached = _cacheGet(_cacheKey)
313
+ if (cached !== null) return { __cached: true, __body: cached }
314
+ }
315
+ let rows
206
316
  if (_pgPool) {
207
317
  const r = await _pgPool.query(convertPlaceholders(sql), params)
208
- return r.rows
318
+ rows = r.rows
319
+ } else if (_mysqlPool) {
320
+ const [result] = await _mysqlPool.execute(sql, params)
321
+ rows = result
322
+ } else if (_useMongo) {
323
+ // MongoDB: extrair collection do SQL "SELECT * FROM table WHERE..."
324
+ const tableMatch = sql.match(/FROM\s+(\w+)/i)
325
+ const collection = tableMatch ? tableMatch[1] : null
326
+ if (!collection) return []
327
+ const filter = _sqlWhereToMongo(sql, params)
328
+ rows = await _mongoDB.collection(collection).find(filter).toArray()
329
+ } else {
330
+ rows = dbAll(sql, params)
331
+ }
332
+ if (Array.isArray(rows) && _cacheKey && !params.length) {
333
+ _cacheSet(_cacheKey, JSON.stringify(rows), _cacheTables)
209
334
  }
210
- return dbAll(sql, params)
335
+ return rows
211
336
  }
212
337
 
213
338
  function dbGet(sql, params = []) { return dbAll(sql, params)[0] || null }
@@ -460,9 +585,15 @@ function validateAip(source) {
460
585
  }
461
586
 
462
587
  function cacheSet(key, value, ttlMs = 60000) {
588
+ // Redis: persistir em redis além do cache em memória
589
+ if (_useRedis) {
590
+ try { _redisClient.set('aip:' + key, JSON.stringify(value), 'PX', ttlMs).catch(() => {}) } catch {}
591
+ }
463
592
  _cache.set(key, { value, expires: Date.now() + ttlMs })
464
593
  }
465
594
  function cacheGet(key) {
595
+ // Redis: verificar redis se não tiver na memória
596
+ // (retorna null para busca assíncrona — o caller deve usar cacheGetAsync se precisar)
466
597
  const item = _cache.get(key)
467
598
  if (!item) return null
468
599
  if (item.expires < Date.now()) { _cache.delete(key); return null }
@@ -621,6 +752,19 @@ class Model {
621
752
 
622
753
  // ── Core queries ────────────────────────────────────────────────
623
754
  all(opts = {}) {
755
+ // MongoDB
756
+ if (_useMongo) {
757
+ const filter = this.softDelete ? { deleted_at: { $exists: false } } : {}
758
+ const mongoOpts = {}
759
+ if (opts.limit) mongoOpts.limit = parseInt(opts.limit)
760
+ if (opts.offset) mongoOpts.skip = parseInt(opts.offset)
761
+ if (opts.order) {
762
+ const p = String(opts.order).trim().split(/\s+/)
763
+ mongoOpts.sort = { [p[0]]: p[1]?.toLowerCase()==='desc' ? -1 : 1 }
764
+ }
765
+ return _mongoDB.collection(this.tableName).find(filter, mongoOpts).toArray()
766
+ .then(docs => docs.map(d => { const { _id, ...rest } = d; return { id: _id?.toString(), ...rest } }))
767
+ }
624
768
  let sql = `SELECT * FROM ${this.tableName}`
625
769
  const params = [], conditions = []
626
770
  if (this.softDelete) conditions.push('deleted_at IS NULL')
@@ -684,6 +828,11 @@ class Model {
684
828
  if (!row.updated_at) row.updated_at = now()
685
829
  }
686
830
  const keys = Object.keys(row), vals = Object.values(row)
831
+ // MongoDB: usar insertOne
832
+ if (_useMongo) {
833
+ _mongoDB.collection(this.tableName).insertOne({ ...row }).catch(e => console.debug('[aiplang:mongo]', e?.message))
834
+ return row
835
+ }
687
836
  dbRun(`INSERT INTO ${this.tableName} (${keys.join(',')}) VALUES (${keys.map(()=>'?').join(',')})`, vals)
688
837
  emit(`${this.modelName}.created`, row)
689
838
  return row
@@ -767,13 +916,39 @@ class Model {
767
916
  // MIGRATION
768
917
  // ═══════════════════════════════════════════════════════════════════
769
918
  function migrateModels(models) {
919
+ // ── Seleção de mapa de tipos por banco ───────────────────────────
920
+ const _sqliteTypes = { uuid:'TEXT',int:'INTEGER',integer:'INTEGER',float:'REAL',bool:'INTEGER',timestamp:'TEXT',date:'TEXT',json:'TEXT',enum:'TEXT',text:'TEXT',email:'TEXT',url:'TEXT',phone:'TEXT' }
921
+ const _mysqlTypes = { uuid:'VARCHAR(36)',int:'INT',integer:'INT',float:'DOUBLE',bool:'TINYINT(1)',timestamp:'DATETIME',date:'DATE',json:'JSON',enum:'TEXT',text:'TEXT',email:'VARCHAR(255)',url:'TEXT',phone:'VARCHAR(20)' }
922
+ const _pgTypes = { uuid:'UUID',int:'INTEGER',integer:'INTEGER',float:'DOUBLE PRECISION',bool:'BOOLEAN',timestamp:'TIMESTAMPTZ',date:'DATE',json:'JSONB',enum:'TEXT',text:'TEXT',email:'TEXT',url:'TEXT',phone:'TEXT' }
923
+ const typeMap = _mysqlPool ? _mysqlTypes : (_pgPool ? _pgTypes : _sqliteTypes)
924
+
770
925
  for (const model of models) {
771
926
  const table = toTable(model.name)
927
+
928
+ // ── MongoDB: criar collection com índices ──────────────────────
929
+ if (_useMongo) {
930
+ _mongoDB.collection(table) // cria collection implicitamente
931
+ for (const f of model.fields) {
932
+ const colName = toCol(f.name)
933
+ if (f.modifiers.includes('unique')) {
934
+ _mongoDB.collection(table).createIndex({ [colName]: 1 }, { unique: true }).catch(() => {})
935
+ }
936
+ }
937
+ console.log(`[aiplang] ✓ ${table} (MongoDB)`)
938
+ continue
939
+ }
940
+
772
941
  const cols = []
773
942
  for (const f of model.fields) {
774
- let sqlType = { uuid:'TEXT',int:'INTEGER',integer:'INTEGER',float:'REAL',bool:'INTEGER',timestamp:'TEXT',date:'TEXT',json:'TEXT',enum:'TEXT',text:'TEXT',email:'TEXT',url:'TEXT',phone:'TEXT' }[f.type] || 'TEXT'
943
+ let sqlType = typeMap[f.type] || 'TEXT'
775
944
  let def = `${toCol(f.name)} ${sqlType}`
776
- if (f.modifiers.includes('pk')) def += ' PRIMARY KEY'
945
+ if (f.modifiers.includes('pk')) {
946
+ if (_mysqlPool) def += ' PRIMARY KEY'
947
+ else def += ' PRIMARY KEY'
948
+ }
949
+ if (f.modifiers.includes('auto') && f.type !== 'uuid') {
950
+ if (_mysqlPool) def += ' AUTO_INCREMENT'
951
+ }
777
952
  if (f.modifiers.includes('required')) def += ' NOT NULL'
778
953
  if (f.modifiers.includes('unique')) def += ' UNIQUE'
779
954
  if (f.default !== null) def += ` DEFAULT '${f.default}'`
@@ -905,7 +1080,18 @@ function parseApp(src) {
905
1080
  }
906
1081
 
907
1082
  function parseEnvLine(s) { const p=s.split(/\s+/); const ev={name:'',required:false,default:null}; for(const x of p){if(x==='required')ev.required=true;else if(x.includes('=')){const[k,v]=x.split('=');ev.name=k;ev.default=v}else ev.name=x}; return ev }
908
- function parseDBLine(s) { const p=s.split(/\s+/); const d=p[0]||'sqlite'; return{driver:d==='pg'||d==='psql'?'postgres':d,dsn:p[1]||'./app.db'} }
1083
+ function parseDBLine(s) {
1084
+ const p = s.split(/\s+/)
1085
+ let d = (p[0]||'sqlite').toLowerCase()
1086
+ // Normalizar aliases
1087
+ if (d==='pg'||d==='psql'||d==='postgresql') d='postgres'
1088
+ if (d==='mariadb') d='mysql'
1089
+ if (d==='mongo') d='mongodb'
1090
+ if (d==='redis'||d==='cache') d='redis'
1091
+ if (d==='sqlite3') d='sqlite'
1092
+ const dsn = p[1] || (d==='sqlite'?'./app.db':d==='redis'?'redis://localhost:6379':'')
1093
+ return { driver:d, dsn }
1094
+ }
909
1095
  function parseAuthLine(s) { const p=s.split(/\s+/); const a={provider:'jwt',secret:p[1]||'$JWT_SECRET',expire:'7d',refresh:'30d'}; for(const x of p){if(x.startsWith('expire='))a.expire=x.slice(7);if(x.startsWith('refresh='))a.refresh=x.slice(8);if(x==='google')a.oauth=['google'];if(x==='github')a.oauth=[...(a.oauth||[]),'google']}; return a }
910
1096
  function parseMailLine(s) { const parts=s.split(/\s+/); const m={driver:parts[0]||'smtp'}; for(const x of parts.slice(1)){const[k,v]=x.split('='); m[k]=v}; return m }
911
1097
  function parseStripeLine(s) {
@@ -2246,7 +2432,7 @@ async function startServer(aipFile, port = 3000) {
2246
2432
  })
2247
2433
 
2248
2434
  srv.addRoute('GET', '/health', (req, res) => res.json(200, {
2249
- status:'ok', version:'2.11.3',
2435
+ status:'ok', version:'2.11.5',
2250
2436
  models: app.models.map(m=>m.name),
2251
2437
  routes: app.apis.length, pages: app.pages.length,
2252
2438
  admin: app.admin?.prefix || null,