aiplang 2.10.9 → 2.11.0
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 +103 -2
- package/package.json +1 -1
- package/runtime/aiplang-hydrate.js +183 -4
- package/server/server.js +89 -1
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.
|
|
8
|
+
const VERSION = '2.11.0'
|
|
9
9
|
const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
|
|
10
10
|
const cmd = process.argv[2]
|
|
11
11
|
const args = process.argv.slice(3)
|
|
@@ -36,6 +36,8 @@ if (!cmd||cmd==='--help'||cmd==='-h') {
|
|
|
36
36
|
npx aiplang init [name] --template my-custom use a saved custom template
|
|
37
37
|
npx aiplang serve [dir] dev server + hot reload
|
|
38
38
|
npx aiplang build [dir/file] compile → static HTML
|
|
39
|
+
npx aiplang validate <app.aip> validate syntax with AI-friendly errors
|
|
40
|
+
npx aiplang context [app.aip] dump minimal AI context (<500 tokens)
|
|
39
41
|
npx aiplang new <page> new page template
|
|
40
42
|
npx aiplang --version
|
|
41
43
|
|
|
@@ -441,6 +443,104 @@ if (cmd==='new') {
|
|
|
441
443
|
process.exit(0)
|
|
442
444
|
}
|
|
443
445
|
|
|
446
|
+
function validateAipSrc(source) {
|
|
447
|
+
const errors = []
|
|
448
|
+
const lines = source.split('\n')
|
|
449
|
+
const knownDirs = new Set(['db','auth','env','mail','s3','stripe','plan','admin','realtime','use','plugin','import','store','ssr','interval','mount','theme','guard','validate','unique','hash','check','cache','rateLimit','broadcast','soft-delete','belongs'])
|
|
450
|
+
for (let i=0; i<lines.length; i++) {
|
|
451
|
+
const line = lines[i].trim()
|
|
452
|
+
if (!line || line.startsWith('#')) continue
|
|
453
|
+
const dm = line.match(/^(guard|validate|unique|hash|check|cache|mount|store|ssr|interval|auth|db|env|use|plugin|import|theme|rateLimit|broadcast)\b/)
|
|
454
|
+
if (dm && !line.startsWith('~') && !line.startsWith('api ') && !line.startsWith('model ') && !line.startsWith('%')) {
|
|
455
|
+
errors.push({ line:i+1, code:line, message:`Missing ~ before '${dm[1]}'`, fix:`~${line}`, severity:'error' })
|
|
456
|
+
}
|
|
457
|
+
if (line.startsWith('api ') && !line.includes('{')) {
|
|
458
|
+
errors.push({ line:i+1, code:line, message:"api block missing '{'", fix:line+' { return {} }', severity:'error' })
|
|
459
|
+
}
|
|
460
|
+
if (/^[a-z_]+\s+-\s+[a-z]/.test(line) && !line.startsWith('api') && !line.startsWith('model')) {
|
|
461
|
+
errors.push({ line:i+1, code:line, message:"Use ':' not '-' in field definitions", fix:line.replace(/\s*-\s*/g,' : '), severity:'error' })
|
|
462
|
+
}
|
|
463
|
+
if (line.startsWith('~')) {
|
|
464
|
+
const dir = line.slice(1).split(/\s/)[0]
|
|
465
|
+
if (!knownDirs.has(dir)) errors.push({ line:i+1, code:line, message:`Unknown directive ~${dir}`, severity:'warning' })
|
|
466
|
+
}
|
|
467
|
+
if (/^table\s*\{/.test(line)) {
|
|
468
|
+
errors.push({ line:i+1, code:line, message:"table missing @binding — e.g.: table @users { Name:name | ... }", severity:'error' })
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return errors
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (cmd==='validate'||cmd==='check'||cmd==='lint') {
|
|
475
|
+
const file = args[0]
|
|
476
|
+
if (!file) { console.error('\n Usage: aiplang validate <app.aip>\n'); process.exit(1) }
|
|
477
|
+
if (!require('fs').existsSync(file)) { console.error(`\n ✗ File not found: ${file}\n`); process.exit(1) }
|
|
478
|
+
const src = require('fs').readFileSync(file,'utf8')
|
|
479
|
+
const errs = validateAipSrc(src)
|
|
480
|
+
if (!errs.length) { console.log('\n ✓ Syntax OK — safe to run\n'); process.exit(0) }
|
|
481
|
+
console.log(`\n ✗ ${errs.length} issue(s) found in ${file}:\n`)
|
|
482
|
+
errs.forEach(e => {
|
|
483
|
+
const icon = e.severity==='error' ? '✗' : '⚠'
|
|
484
|
+
console.log(` ${icon} Line ${e.line}: ${e.message}`)
|
|
485
|
+
console.log(` ${e.code}`)
|
|
486
|
+
if (e.fix) console.log(` Fix: ${e.fix}`)
|
|
487
|
+
})
|
|
488
|
+
console.log()
|
|
489
|
+
process.exit(errs.some(e=>e.severity==='error') ? 1 : 0)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (cmd==='context'||cmd==='ctx') {
|
|
493
|
+
const file = args[0] || 'app.aip'
|
|
494
|
+
const exists = require('fs').existsSync
|
|
495
|
+
const src = exists(file) ? require('fs').readFileSync(file,'utf8') : null
|
|
496
|
+
if (!src) { console.log('\n Usage: aiplang context [app.aip]\n Dumps minimal AI context (~200 tokens).\n'); process.exit(0) }
|
|
497
|
+
// Use server's parseApp for full app structure
|
|
498
|
+
const serverPath = require('path').join(__dirname,'../server/server.js')
|
|
499
|
+
let app = { models:[], apis:[], pages:[], db:null, auth:null }
|
|
500
|
+
try {
|
|
501
|
+
const srv = require(serverPath)
|
|
502
|
+
if (srv.parseApp) app = srv.parseApp(src)
|
|
503
|
+
} catch {
|
|
504
|
+
// Fallback: basic parse for models + routes
|
|
505
|
+
const modelRx = /^model\s+(\w+)/gm
|
|
506
|
+
const apiRx = /^api\s+(\w+)\s+(\S+)/gm
|
|
507
|
+
let m
|
|
508
|
+
while((m=modelRx.exec(src))) app.models.push({name:m[1],fields:[]})
|
|
509
|
+
while((m=apiRx.exec(src))) app.apis.push({method:m[1],path:m[2],guards:[]})
|
|
510
|
+
const pageRx = /^%(\w+)\s+(\w+)\s+(\S+)/gm
|
|
511
|
+
while((m=pageRx.exec(src))) app.pages.push({id:m[1],theme:m[2],route:m[3],state:{},queries:[]})
|
|
512
|
+
}
|
|
513
|
+
const out = [
|
|
514
|
+
`# aiplang app — ${file}`,
|
|
515
|
+
'# paste into AI for maintenance/customization',
|
|
516
|
+
'',
|
|
517
|
+
'## MODELS'
|
|
518
|
+
]
|
|
519
|
+
for (const m of app.models||[]) {
|
|
520
|
+
const fields = m.fields.map(f=>`${f.name}:${f.type}${f.modifiers?.length?':'+f.modifiers.join(':'):''}`).join(' ')
|
|
521
|
+
out.push(`model ${m.name} { ${fields} }`)
|
|
522
|
+
}
|
|
523
|
+
out.push('')
|
|
524
|
+
out.push('## ROUTES')
|
|
525
|
+
for (const r of app.apis||[]) {
|
|
526
|
+
const g = r.guards?.length ? ` [${r.guards.join(',')}]` : ''
|
|
527
|
+
const v = r.validate?.length ? ` validate:${r.validate.length}` : ''
|
|
528
|
+
out.push(`${r.method.padEnd(7)}${r.path}${g}${v}`)
|
|
529
|
+
}
|
|
530
|
+
out.push('')
|
|
531
|
+
out.push('## PAGES')
|
|
532
|
+
for (const p of app.pages||[]) {
|
|
533
|
+
const state = Object.keys(p.state||{}).map(k=>`@${k}`).join(' ')
|
|
534
|
+
const queries = (p.queries||[]).map(q=>`${q.trigger}:${q.path}`).join(' ')
|
|
535
|
+
out.push(`%${p.id} ${p.theme||'dark'} ${p.route} | state:${state||'none'} | queries:${queries||'none'}`)
|
|
536
|
+
}
|
|
537
|
+
if (app.db) { out.push(''); out.push(`## CONFIG\ndb:${app.db.driver} auth:${app.auth?'jwt':'none'}`) }
|
|
538
|
+
const ctx = out.join('\n')
|
|
539
|
+
console.log(ctx)
|
|
540
|
+
console.log(`\n# ~${Math.ceil(ctx.length/4)} tokens`)
|
|
541
|
+
process.exit(0)
|
|
542
|
+
}
|
|
543
|
+
|
|
444
544
|
if (cmd==='build') {
|
|
445
545
|
const outIdx=args.indexOf('--out')
|
|
446
546
|
const outDir=outIdx!==-1?args[outIdx+1]:'dist'
|
|
@@ -1060,7 +1160,8 @@ function rTable(b) {
|
|
|
1060
1160
|
const span=cols.length+((b.editPath||b.deletePath)?1:0)
|
|
1061
1161
|
const fallbackAttr=b.fallback?` data-fx-fallback="${esc(b.fallback)}"`:''
|
|
1062
1162
|
const retryAttr=b.retry?` data-fx-retry="${esc(b.retry)}"`:''
|
|
1063
|
-
|
|
1163
|
+
const exitAttr=b.animateExit?` data-fx-exit="${esc(b.animateExit)}"`:'';
|
|
1164
|
+
return `<div class="fx-table-wrap"><table class="fx-table"${exitAttr} data-fx-table="${esc(b.binding)}" data-fx-cols='${keys}' data-fx-col-map='${cm}'${ea}${da}${fallbackAttr}${retryAttr}><thead><tr>${ths}${at}</tr></thead><tbody class="fx-tbody"><tr><td colspan="${span}" class="fx-td-empty">${esc(b.empty)}</td></tr></tbody></table></div>\n`
|
|
1064
1165
|
}
|
|
1065
1166
|
|
|
1066
1167
|
function rForm(b) {
|
package/package.json
CHANGED
|
@@ -499,7 +499,7 @@ function hydrateTables() {
|
|
|
499
499
|
db.onclick = async () => {
|
|
500
500
|
if (!await confirm('Delete this record? This cannot be undone.')) return
|
|
501
501
|
const {ok,data} = await http('DELETE', resolvePath(delPath,_row), null)
|
|
502
|
-
if (ok) { set(key,(get(key)||[]).filter((_,j)=>j!==_i));
|
|
502
|
+
if (ok) { const _dtr=db.closest('tr');_animateExit(_dtr,'fx-exit',()=>set(key,(get(key)||[]).filter((_,j)=>j!==_i)));toast('Deleted','ok') }
|
|
503
503
|
else toast(data.message||'Error deleting','err')
|
|
504
504
|
}
|
|
505
505
|
actTd.appendChild(db)
|
|
@@ -539,7 +539,7 @@ function hydrateTables() {
|
|
|
539
539
|
const actTd = document.createElement('td')
|
|
540
540
|
actTd.className = 'fx-td fx-td-actions'; actTd.style.cssText = 'white-space:nowrap'
|
|
541
541
|
if (editPath) { const eb=document.createElement('button');eb.className='fx-action-btn fx-edit-btn';eb.textContent='✎ Edit';const _r=row,_i=idx;eb.onclick=async()=>{const upd=await editModal(_r,cols,editPath,editMethod,key);if(!upd)return;const arr=[...(get(key)||[])];arr[_i]={..._r,...upd};set(key,arr)};actTd.appendChild(eb) }
|
|
542
|
-
if (delPath) { const db=document.createElement('button');db.className='fx-action-btn fx-delete-btn';db.textContent='✕ Delete';const _r=row,_i=idx;db.onclick=async()=>{if(!await confirm('Delete?'))return;const{ok,data}=await http('DELETE',resolvePath(delPath,_r),null);if(ok){set(key,(get(key)||[]).filter((_,j)=>j!==_i));toast('Deleted','ok')}else toast(data.message||'Error','err')};actTd.appendChild(db) }
|
|
542
|
+
if (delPath) { const db=document.createElement('button');db.className='fx-action-btn fx-delete-btn';db.textContent='✕ Delete';const _r=row,_i=idx;db.onclick=async()=>{if(!await confirm('Delete?'))return;const{ok,data}=await http('DELETE',resolvePath(delPath,_r),null);if(ok){const _dtr2=db.closest('tr');_animateExit(_dtr2,'fx-exit',()=>set(key,(get(key)||[]).filter((_,j)=>j!==_i)));toast('Deleted','ok')}else toast(data.message||'Error','err')};actTd.appendChild(db) }
|
|
543
543
|
tr.appendChild(actTd)
|
|
544
544
|
}
|
|
545
545
|
return tr
|
|
@@ -752,6 +752,32 @@ function initAnimations() {
|
|
|
752
752
|
.fx-visible.fx-anim-stagger > *:nth-child(6) { animation-delay: .5s }
|
|
753
753
|
.fx-anim-bounce { animation: fx-bounce 1.5s ease-in-out infinite !important; opacity: 1 !important }
|
|
754
754
|
.fx-anim-pulse { animation: fx-pulse-ring 2s ease infinite !important; opacity: 1 !important }
|
|
755
|
+
|
|
756
|
+
/* ── Exit animations (AnimatePresence) ─────── */
|
|
757
|
+
@keyframes fx-exit-fade { to{opacity:0;transform:scale(.95)} }
|
|
758
|
+
@keyframes fx-exit-up { to{opacity:0;transform:translateY(-16px)} }
|
|
759
|
+
@keyframes fx-exit-down { to{opacity:0;transform:translateY(16px)} }
|
|
760
|
+
@keyframes fx-exit-left { to{opacity:0;transform:translateX(-20px)} }
|
|
761
|
+
@keyframes fx-exit-right { to{opacity:0;transform:translateX(20px)} }
|
|
762
|
+
@keyframes fx-exit-shrink { to{opacity:0;transform:scale(0);max-height:0;padding:0;margin:0} }
|
|
763
|
+
.fx-exit { pointer-events:none!important; animation: fx-exit-fade .25s ease forwards }
|
|
764
|
+
.fx-exit-up { pointer-events:none!important; animation: fx-exit-up .25s ease forwards }
|
|
765
|
+
.fx-exit-down { pointer-events:none!important; animation: fx-exit-down .25s ease forwards }
|
|
766
|
+
.fx-exit-left { pointer-events:none!important; animation: fx-exit-left .22s ease forwards }
|
|
767
|
+
.fx-exit-right { pointer-events:none!important; animation: fx-exit-right .22s ease forwards }
|
|
768
|
+
.fx-exit-shrink { pointer-events:none!important; animation: fx-exit-shrink .3s ease forwards; overflow:hidden }
|
|
769
|
+
|
|
770
|
+
/* ── Enter animations for new rows ─────────── */
|
|
771
|
+
@keyframes fx-enter-row { from{opacity:0;transform:translateX(-8px)} to{opacity:1;transform:none} }
|
|
772
|
+
.fx-row-enter { animation: fx-enter-row .25s ease both }
|
|
773
|
+
|
|
774
|
+
/* ── FLIP layout transition ─────────────────── */
|
|
775
|
+
.fx-flip-pending { transition: transform .3s cubic-bezier(.4,0,.2,1) !important }
|
|
776
|
+
|
|
777
|
+
/* ── Drag with inertia ───────────────────────── */
|
|
778
|
+
.fx-dragging { opacity:.7; cursor:grabbing!important; z-index:1000; position:relative }
|
|
779
|
+
.fx-drag-ghost { opacity:.3; transition:none }
|
|
780
|
+
.fx-drop-target { outline:2px dashed rgba(99,102,241,.5); outline-offset:2px }
|
|
755
781
|
`
|
|
756
782
|
document.head.appendChild(style)
|
|
757
783
|
|
|
@@ -772,8 +798,149 @@ function initAnimations() {
|
|
|
772
798
|
observer.observe(el)
|
|
773
799
|
})
|
|
774
800
|
|
|
775
|
-
|
|
776
|
-
|
|
801
|
+
// ── AnimatePresence — animate elements before DOM removal ──────────
|
|
802
|
+
// Like Framer Motion AnimatePresence. exitClass: 'fx-exit', 'fx-exit-left', etc.
|
|
803
|
+
function _animateExit(el, exitClass, callback) {
|
|
804
|
+
if (!el) { callback && callback(); return }
|
|
805
|
+
exitClass = exitClass || 'fx-exit'
|
|
806
|
+
el.classList.add(exitClass)
|
|
807
|
+
const done = () => { el.removeEventListener('animationend', done); callback && callback() }
|
|
808
|
+
el.addEventListener('animationend', done, { once: true })
|
|
809
|
+
// Fallback: force removal after 350ms max (safety net)
|
|
810
|
+
setTimeout(() => { el.classList.contains(exitClass) && callback && callback() }, 350)
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// ── FLIP layout animation ─────────────────────────────────────────
|
|
814
|
+
// Capture positions BEFORE a DOM change, then animate FROM old TO new
|
|
815
|
+
// Like Framer Motion layout prop — zero extra markup required
|
|
816
|
+
function _flipCapture(elements) {
|
|
817
|
+
const rects = new Map()
|
|
818
|
+
elements.forEach(el => rects.set(el, el.getBoundingClientRect()))
|
|
819
|
+
return rects
|
|
820
|
+
}
|
|
821
|
+
function _flipPlay(rects, duration) {
|
|
822
|
+
duration = duration || 300
|
|
823
|
+
rects.forEach((oldRect, el) => {
|
|
824
|
+
if (!el.isConnected) return
|
|
825
|
+
const newRect = el.getBoundingClientRect()
|
|
826
|
+
const dx = oldRect.left - newRect.left
|
|
827
|
+
const dy = oldRect.top - newRect.top
|
|
828
|
+
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) return
|
|
829
|
+
// Invert: move el back to where it was
|
|
830
|
+
el.style.transform = `translate(${dx}px,${dy}px)`
|
|
831
|
+
el.style.transition = 'none'
|
|
832
|
+
// Play: animate to final position
|
|
833
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
834
|
+
el.style.transition = `transform ${duration}ms cubic-bezier(.4,0,.2,1)`
|
|
835
|
+
el.style.transform = ''
|
|
836
|
+
const clean = () => { el.style.transform = ''; el.style.transition = '' }
|
|
837
|
+
el.addEventListener('transitionend', clean, { once: true })
|
|
838
|
+
}))
|
|
839
|
+
})
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ── Spring physics (improved — overshoot + settle) ────────────────
|
|
843
|
+
function _spring(el, prop, from, to, opts) {
|
|
844
|
+
const k = opts && opts.stiffness || 200
|
|
845
|
+
const b = opts && opts.damping || 22
|
|
846
|
+
const m = opts && opts.mass || 1
|
|
847
|
+
const unit = opts && opts.unit || 'px'
|
|
848
|
+
let pos = from, vel = 0, raf
|
|
849
|
+
const dt = 1/60
|
|
850
|
+
const tick = () => {
|
|
851
|
+
const F = -k*(pos-to) - b*vel
|
|
852
|
+
vel += (F/m)*dt; pos += vel*dt
|
|
853
|
+
el.style[prop] = pos + unit
|
|
854
|
+
if (Math.abs(pos-to) > 0.05 || Math.abs(vel) > 0.05) {
|
|
855
|
+
raf = requestAnimationFrame(tick)
|
|
856
|
+
} else {
|
|
857
|
+
el.style[prop] = to + unit
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
cancelAnimationFrame(raf)
|
|
861
|
+
requestAnimationFrame(tick)
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ── Drag with inertia ─────────────────────────────────────────────
|
|
865
|
+
// HTML5 drag is used for kanban. This adds pointer-based drag
|
|
866
|
+
// for any element with data-fx-draggable attribute.
|
|
867
|
+
// Inertia: on pointerup, continues movement with exponential decay.
|
|
868
|
+
function _initDragInertia(el, opts) {
|
|
869
|
+
opts = opts || {}
|
|
870
|
+
const onDrop = opts.onDrop
|
|
871
|
+
let startX, startY, lastX, lastY, velX = 0, velY = 0
|
|
872
|
+
let lastTime, raf, isDragging = false
|
|
873
|
+
|
|
874
|
+
el.style.cursor = 'grab'
|
|
875
|
+
el.style.userSelect = 'none'
|
|
876
|
+
el.style.touchAction = 'none'
|
|
877
|
+
|
|
878
|
+
const onDown = e => {
|
|
879
|
+
e.preventDefault()
|
|
880
|
+
isDragging = true
|
|
881
|
+
el.classList.add('fx-dragging')
|
|
882
|
+
startX = lastX = e.clientX
|
|
883
|
+
startY = lastY = e.clientY
|
|
884
|
+
lastTime = Date.now()
|
|
885
|
+
velX = velY = 0
|
|
886
|
+
cancelAnimationFrame(raf)
|
|
887
|
+
document.addEventListener('pointermove', onMove, { passive: false })
|
|
888
|
+
document.addEventListener('pointerup', onUp, { once: true })
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const onMove = e => {
|
|
892
|
+
if (!isDragging) return
|
|
893
|
+
const now = Date.now()
|
|
894
|
+
const dt = Math.max(now - lastTime, 1)
|
|
895
|
+
const dx = e.clientX - lastX
|
|
896
|
+
const dy = e.clientY - lastY
|
|
897
|
+
// Exponential moving average for velocity
|
|
898
|
+
velX = velX * 0.7 + (dx / dt) * 0.3 * 16
|
|
899
|
+
velY = velY * 0.7 + (dy / dt) * 0.3 * 16
|
|
900
|
+
lastX = e.clientX; lastY = e.clientY; lastTime = now
|
|
901
|
+
const totalDx = e.clientX - startX
|
|
902
|
+
const totalDy = e.clientY - startY
|
|
903
|
+
el.style.transform = `translate(${totalDx}px,${totalDy}px)`
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const onUp = e => {
|
|
907
|
+
isDragging = false
|
|
908
|
+
el.classList.remove('fx-dragging')
|
|
909
|
+
document.removeEventListener('pointermove', onMove)
|
|
910
|
+
const speed = Math.sqrt(velX*velX + velY*velY)
|
|
911
|
+
if (speed < 0.5) {
|
|
912
|
+
el.style.transition = 'transform .3s cubic-bezier(.4,0,.2,1)'
|
|
913
|
+
el.style.transform = ''
|
|
914
|
+
onDrop && onDrop(e)
|
|
915
|
+
return
|
|
916
|
+
}
|
|
917
|
+
// Inertia: continue with decay
|
|
918
|
+
let dx = parseFloat(el.style.transform.match(/translate\(([^,]+)/)?.[1]) || 0
|
|
919
|
+
let dy = parseFloat(el.style.transform.match(/,([^)]+)/)?.[1]) || 0
|
|
920
|
+
const decay = () => {
|
|
921
|
+
velX *= 0.88; velY *= 0.88
|
|
922
|
+
dx += velX; dy += velY
|
|
923
|
+
el.style.transform = `translate(${dx}px,${dy}px)`
|
|
924
|
+
if (Math.abs(velX) > 0.5 || Math.abs(velY) > 0.5) {
|
|
925
|
+
raf = requestAnimationFrame(decay)
|
|
926
|
+
} else {
|
|
927
|
+
el.style.transition = 'transform .2s cubic-bezier(.4,0,.2,1)'
|
|
928
|
+
el.style.transform = ''
|
|
929
|
+
onDrop && onDrop(e)
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
raf = requestAnimationFrame(decay)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
el.addEventListener('pointerdown', onDown)
|
|
936
|
+
return () => el.removeEventListener('pointerdown', onDown)
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
window.aiplang = window.aiplang || {}
|
|
940
|
+
window.aiplang.animateExit = _animateExit
|
|
941
|
+
window.aiplang.flip = { capture: _flipCapture, play: _flipPlay }
|
|
942
|
+
window.aiplang.draggable = _initDragInertia
|
|
943
|
+
window.aiplang.spring = function(el, prop, from, to, opts = {}) {
|
|
777
944
|
const k = opts.stiffness || 180
|
|
778
945
|
const b = opts.damping || 22
|
|
779
946
|
const m = opts.mass || 1
|
|
@@ -1204,6 +1371,17 @@ function hydrateTableErrors() {
|
|
|
1204
1371
|
}
|
|
1205
1372
|
}
|
|
1206
1373
|
|
|
1374
|
+
function initDraggables() {
|
|
1375
|
+
document.querySelectorAll('[data-fx-draggable]').forEach(el => {
|
|
1376
|
+
_initDragInertia(el, {
|
|
1377
|
+
onDrop: e => {
|
|
1378
|
+
const action = el.getAttribute('data-fx-drop-action')
|
|
1379
|
+
if (action) applyAction({}, null, action)
|
|
1380
|
+
}
|
|
1381
|
+
})
|
|
1382
|
+
})
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1207
1385
|
function boot() {
|
|
1208
1386
|
loadSSRData()
|
|
1209
1387
|
injectActionCSS()
|
|
@@ -1221,6 +1399,7 @@ function boot() {
|
|
|
1221
1399
|
hydrateCharts()
|
|
1222
1400
|
hydrateKanban()
|
|
1223
1401
|
hydrateEditors()
|
|
1402
|
+
initDraggables()
|
|
1224
1403
|
mountQueries()
|
|
1225
1404
|
}
|
|
1226
1405
|
|
package/server/server.js
CHANGED
|
@@ -312,6 +312,60 @@ setInterval(() => {
|
|
|
312
312
|
const _mpEnc=(()=>{const te=new TextEncoder();function encode(v){const b=[];_w(v,b);return Buffer.from(b)}function _w(v,b){if(v===null||v===undefined){b.push(0xc0);return}if(v===false){b.push(0xc2);return}if(v===true){b.push(0xc3);return}const t=typeof v;if(t==='number'){if(Number.isInteger(v)&&v>=0&&v<=127){b.push(v);return}if(Number.isInteger(v)&&v>=-32&&v<0){b.push(v+256);return}if(Number.isInteger(v)&&v>=0&&v<=65535){b.push(0xcd,v>>8,v&0xff);return}const dv=new DataView(new ArrayBuffer(8));dv.setFloat64(0,v);b.push(0xcb,...new Uint8Array(dv.buffer));return}if(t==='string'){const e=te.encode(v);const n=e.length;if(n<=31)b.push(0xa0|n);else if(n<=255)b.push(0xd9,n);else b.push(0xda,n>>8,n&0xff);b.push(...e);return}if(Array.isArray(v)){const n=v.length;if(n<=15)b.push(0x90|n);else b.push(0xdc,n>>8,n&0xff);v.forEach(x=>_w(x,b));return}if(t==='object'){const ks=Object.keys(v);const n=ks.length;if(n<=15)b.push(0x80|n);else b.push(0xde,n>>8,n&0xff);ks.forEach(k=>{_w(k,b);_w(v[k],b)});return}}return{encode}})()
|
|
313
313
|
|
|
314
314
|
|
|
315
|
+
// ── AI-optimized .aip validator with fix suggestions ──────────────
|
|
316
|
+
function validateAip(source) {
|
|
317
|
+
const errors = []
|
|
318
|
+
const lines = source.split('\n')
|
|
319
|
+
const knownDirectives = ['db','auth','env','mail','s3','stripe','plan','admin','realtime','use','plugin','import','store','ssr','interval','mount','theme','guard','validate','unique','hash','check','cache','rateLimit','broadcast','soft-delete','belongs']
|
|
320
|
+
const knownBlocks = ['nav','hero','stats','row','row2','row3','sect','foot','table','form','btn','pricing','faq','testimonial','gallery','each','chart','kanban','editor','card','cols','spacer','html','divider','badge','select','if','raw']
|
|
321
|
+
const knownApiOps = ['insert','update','delete','return','~guard','~validate','~unique','~hash','~check','~cache','~rateLimit','~broadcast']
|
|
322
|
+
|
|
323
|
+
for (let i=0; i<lines.length; i++) {
|
|
324
|
+
const line = lines[i].trim()
|
|
325
|
+
if (!line || line.startsWith('#')) continue
|
|
326
|
+
|
|
327
|
+
// Detect missing ~ on directives
|
|
328
|
+
const directiveMatch = line.match(/^(guard|validate|unique|hash|check|cache|mount|store|ssr|interval|realtime|auth|db|env|use|plugin|import|theme|rateLimit|broadcast)\s/)
|
|
329
|
+
if (directiveMatch && !line.startsWith('~') && !line.startsWith('api ') && !line.startsWith('model ') && !line.startsWith('%')) {
|
|
330
|
+
errors.push({
|
|
331
|
+
line: i+1, code: line,
|
|
332
|
+
message: `Directive '${directiveMatch[1]}' missing ~ prefix`,
|
|
333
|
+
fix: `~${line}`,
|
|
334
|
+
severity: 'error'
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Detect api without { }
|
|
339
|
+
if (line.startsWith('api ') && !line.includes('{')) {
|
|
340
|
+
errors.push({
|
|
341
|
+
line: i+1, code: line,
|
|
342
|
+
message: 'api block missing opening {',
|
|
343
|
+
fix: line + ' { return {} }',
|
|
344
|
+
severity: 'error'
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Detect unknown ~directive
|
|
349
|
+
if (line.startsWith('~')) {
|
|
350
|
+
const dir = line.slice(1).split(/\s/)[0]
|
|
351
|
+
if (!knownDirectives.includes(dir) && !dir.match(/^[a-z]+$/)) {
|
|
352
|
+
errors.push({ line: i+1, code: line, message: `Unknown directive: ~${dir}`, severity: 'warning' })
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Detect model fields with wrong separator
|
|
357
|
+
if (/^[a-z_]+\s*-\s*[a-z]/.test(line) && !line.startsWith('api') && !line.startsWith('model')) {
|
|
358
|
+
errors.push({
|
|
359
|
+
line: i+1, code: line,
|
|
360
|
+
message: "Field definition uses '-' separator, should use ':'",
|
|
361
|
+
fix: line.replace(/\s*-\s*/g, ' : '),
|
|
362
|
+
severity: 'error'
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return errors
|
|
367
|
+
}
|
|
368
|
+
|
|
315
369
|
function cacheSet(key, value, ttlMs = 60000) {
|
|
316
370
|
_cache.set(key, { value, expires: Date.now() + ttlMs })
|
|
317
371
|
}
|
|
@@ -1941,8 +1995,42 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1941
1995
|
})
|
|
1942
1996
|
|
|
1943
1997
|
// Health
|
|
1998
|
+
// ── AI introspection: GET /__aip ──────────────────────────────────
|
|
1999
|
+
srv.addRoute('GET', '/__aip', (req, res) => {
|
|
2000
|
+
const modelInfo = {}
|
|
2001
|
+
for (const [name, M] of Object.entries(srv._models || {})) {
|
|
2002
|
+
modelInfo[name.toLowerCase()] = {
|
|
2003
|
+
fields: M.fields ? M.fields.map(f => ({
|
|
2004
|
+
name:f.name, type:f.type,
|
|
2005
|
+
required:!!(f.modifiers?.includes('required')),
|
|
2006
|
+
unique:!!(f.modifiers?.includes('unique')),
|
|
2007
|
+
hashed:!!(f.modifiers?.includes('hashed'))
|
|
2008
|
+
})) : [],
|
|
2009
|
+
count: (() => { try { return M.count() } catch { return null } })()
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
const routeList = (srv._routes||[]).map(r=>({method:r.method,path:r.path,guards:r.guards||[]}))
|
|
2013
|
+
res.json(200, {
|
|
2014
|
+
version: VERSION,
|
|
2015
|
+
models: modelInfo,
|
|
2016
|
+
routes: routeList,
|
|
2017
|
+
ai_hint: 'POST /__aip/validate with {source:"..."} to check .aip before applying.'
|
|
2018
|
+
})
|
|
2019
|
+
})
|
|
2020
|
+
|
|
2021
|
+
// POST /__aip/validate
|
|
2022
|
+
srv.addRoute('POST', '/__aip/validate', async (req, res) => {
|
|
2023
|
+
const { source } = req.body || {}
|
|
2024
|
+
if (!source) { res.error(400, 'Body: { source: "aip content" }'); return }
|
|
2025
|
+
try {
|
|
2026
|
+
const errs = validateAip(source)
|
|
2027
|
+
if (errs.length) res.json(422, { valid:false, errors:errs, fixes:errs.map(e=>e.fix).filter(Boolean) })
|
|
2028
|
+
else res.json(200, { valid:true, message:'Syntax OK. Safe to apply.' })
|
|
2029
|
+
} catch(e) { res.error(500, e.message) }
|
|
2030
|
+
})
|
|
2031
|
+
|
|
1944
2032
|
srv.addRoute('GET', '/health', (req, res) => res.json(200, {
|
|
1945
|
-
status:'ok', version:'2.
|
|
2033
|
+
status:'ok', version:'2.11.0',
|
|
1946
2034
|
models: app.models.map(m=>m.name),
|
|
1947
2035
|
routes: app.apis.length, pages: app.pages.length,
|
|
1948
2036
|
admin: app.admin?.prefix || null,
|