lecodes-cli 2.0.13 → 2.1.1

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.
@@ -1,5 +1,5 @@
1
1
  var Dn=Object.defineProperty;var Bn=(e,t,n)=>t in e?Dn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var je=(e,t,n)=>Bn(e,typeof t!="symbol"?t+"":t,n);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const s of i)if(s.type==="childList")for(const o of s.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(i){const s={};return i.integrity&&(s.integrity=i.integrity),i.referrerPolicy&&(s.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?s.credentials="include":i.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function r(i){if(i.ep)return;i.ep=!0;const s=n(i);fetch(i.href,s)}})();const Vn=e=>{if(typeof e=="string")return e;if(e instanceof Error)return e.stack?`${String(e)}
2
- ${e.stack}`:String(e);try{return JSON.stringify(e)}catch{return String(e)}},zn=(e,t)=>{const n=o=>{fetch(`${e}/log`,{method:"POST",body:JSON.stringify(o),headers:{"content-type":"application/json"},keepalive:!0}).catch(()=>{})};let r=-1;const i=async()=>{try{const{v:o}=await(await fetch(`${e}/version`,{cache:"no-store"})).json();typeof o=="number"&&t(o)}catch{}setTimeout(i,2500)};setTimeout(i,2500);const s=()=>{let o;try{o=new WebSocket(e.replace(/^http/,"ws")+"/ws")}catch{return}o.onopen=()=>o.send(JSON.stringify({type:"hello",v:r})),o.onmessage=l=>{let d={};try{d=JSON.parse(String(l.data))}catch{return}d.type==="reload"&&typeof d.v=="number"&&t(d.v)},o.onclose=()=>{setTimeout(s,2e3)}};return s(),{bundleUrl:o=>`${e}/bundle.js?t=web&v=${o}`,versionOf(o,l){const d=Number(o.headers.get("x-lecodes-v"));return o.headers.has("x-lecodes-v")&&Number.isFinite(d)?d:l},ran(o){r=o},log:(o,l)=>n({type:"log",level:o,text:l.map(Vn).join(" ")}),error:o=>n({type:"error",text:o})}},Wn=(e,t)=>{try{const n=new URL(e,t);return n.origin===new URL(t).origin&&(n.protocol==="http:"||n.protocol==="https:")?n.href:null}catch{return null}},Gn="modulepreload",Hn=function(e,t){return new URL(e,t).href},Et={},ce=function(t,n,r){let i=Promise.resolve();if(n&&n.length>0){let o=function(y){return Promise.all(y.map(k=>Promise.resolve(k).then(A=>({status:"fulfilled",value:A}),A=>({status:"rejected",reason:A}))))};const l=document.getElementsByTagName("link"),d=document.querySelector("meta[property=csp-nonce]"),f=(d==null?void 0:d.nonce)||(d==null?void 0:d.getAttribute("nonce"));i=o(n.map(y=>{if(y=Hn(y,r),y in Et)return;Et[y]=!0;const k=y.endsWith(".css"),A=k?'[rel="stylesheet"]':"";if(!!r)for(let E=l.length-1;E>=0;E--){const b=l[E];if(b.href===y&&(!k||b.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${y}"]${A}`))return;const p=document.createElement("link");if(p.rel=k?"stylesheet":Gn,k||(p.as="script"),p.crossOrigin="",p.href=y,f&&p.setAttribute("nonce",f),document.head.appendChild(p),k)return new Promise((E,b)=>{p.addEventListener("load",E),p.addEventListener("error",()=>b(new Error(`Unable to preload CSS for ${y}`)))})}))}function s(o){const l=new Event("vite:preloadError",{cancelable:!0});if(l.payload=o,window.dispatchEvent(l),!l.defaultPrevented)throw o}return i.then(o=>{for(const l of o||[])l.status==="rejected"&&s(l.reason);return t().catch(s)})},wt="2.0.13",Ke=e=>{const t=e==null?void 0:e.match(/^(\d+)\.\d+\.\d+/);return t?Number(t[1]):null},jn=(e,t)=>Ke(e)!==null&&Ke(e)===Ke(t),Kn=(e,t="woff2")=>{const n=/^\/\/ fonts: (.+)$/m.exec(e);if(!n)return[];const r=[];for(const i of n[1].split(";")){const s=i.indexOf("=");if(s===-1)continue;const o=/^(.+)@(\d+)(i?)$/.exec(i.slice(0,s));o&&r.push({family:o[1],weight:parseInt(o[2],10),italic:o[3]==="i",url:i.slice(s+1).replace("{fmt}",t)})}return r},Yn=async(e,t,n=2e3)=>{const r=t??(typeof document>"u"?void 0:document);if(!(r!=null&&r.fonts)||e.length===0)return;const i=[];for(const s of e)try{const o=new FontFace(s.family,`url("${s.url}")`,{weight:String(s.weight),style:s.italic?"italic":"normal"});r.fonts.add(o),i.push(o.load().catch(()=>{}))}catch{}await Promise.race([Promise.all(i),new Promise(s=>setTimeout(s,n))])},qn=e=>{var d,f,y,k;const t=e.slice(0,2e3),n=new Set,r=t.match(/\/\/ preload: (.+)/);if(r)for(const A of r[1].split(";"))n.add(A);const i=t.includes("// gl: disable")?"off":t.includes("// gl: defer")?"defer":"on",s=((d=t.match(/^\/\/ sdk: (\d+\.\d+\.\d+\S*)/m))==null?void 0:d[1])??null,o=((y=(f=t.match(/^\/\/ name: (.+)$/m))==null?void 0:f[1])==null?void 0:y.trim())??null,l=new Set((((k=t.match(/^\/\/ plugins: (.+)$/m))==null?void 0:k[1])??"").split(";").map(A=>A.trim()).filter(Boolean));return{sdk:s,name:o,preloads:n,gl:i,is2d:t.includes("// 2d: enable"),plugins:l,bootFonts:Kn(t)}},Xn=1,Jn=2,Qn=3,Zn=4,er=5,tr=6,St=7,nr=8,_t=9,rr=10,or=11,sr=12,ir=13,ar=14,lr=15,cr=16,kt=17,ur=18,fr=19,Oe=20,ai=21,li=22,ci=23,ui=24,fi=25,dr=26,pr=27,hr=28,mr=29,Tt=30,Lt=31,gr=32,br=33,xt=34,Rt=35,Ct=36,Pe=1,yr=2,vr=4,de=8,Er=16,Ye=32,wr=64,qe=128,Sr=256,_r=512,kr=1024,Tr=2048,Lr=4096,At=8192,di=16384,xr=32768,Rr=1<<18,Cr=255,$t=256,Ot=512,Ar=1024,Q=0,$r=1,Or=2,Ur=3,De=4,be=255,un=e=>{const t=e.clientWidth;if(!t)return 1;const n=e.getBoundingClientRect().width;return n>0?n/t:1},Ut={text:{type:"text"},password:{type:"password"},search:{type:"search"},tel:{type:"tel"},email:{type:"email"},url:{type:"url"},number:{type:"text",mode:"numeric"},decimal:{type:"text",mode:"decimal"},date:{type:"date"},time:{type:"time"}},Nt=(e,t)=>e==null?t:e===!0||e==="true"||e==="1"||typeof e=="number"&&e>.5,oe=(e,t,n)=>{n===null?e.removeAttribute(t):e.getAttribute(t)!==n&&e.setAttribute(t,n)},Nr=(e,t)=>{if(e.type!=="input"&&e.type!=="textarea")return;const n=e.el,r=t("placeholder");if(oe(n,"placeholder",r==null?null:String(r)),e.type==="input"){const l=Ut[String(t("type")??"text")]??Ut.text,d=n;d.type!==l.type&&(d.type=l.type),oe(n,"inputmode",l.mode??null);const f=t("enterKey");oe(n,"enterkeyhint",f==null?null:String(f))}const i=t("autocapitalize");oe(n,"autocapitalize",i==null?null:String(i));const s=t("autocorrect");s==null?(oe(n,"autocorrect",null),oe(n,"spellcheck",null)):(oe(n,"autocorrect",Nt(s,!0)?"on":"off"),oe(n,"spellcheck",Nt(s,!0)?"true":"false"));const o=Number(t("maxLength")??0);oe(n,"maxlength",o>0?String(Math.round(o)):null)},Be={width:"layout",height:"layout",minWidth:"layout",minHeight:"layout",maxWidth:"layout",maxHeight:"layout",flexBasis:"layout",flexGrow:"layout",flexShrink:"layout",aspectRatio:"layout",gap:"layout",rowGap:"layout",columnGap:"layout",padding:"layout",paddingTop:"layout",paddingRight:"layout",paddingBottom:"layout",paddingLeft:"layout",paddingHorizontal:"layout",paddingVertical:"layout",paddingStart:"layout",paddingEnd:"layout",margin:"layout",marginTop:"layout",marginRight:"layout",marginBottom:"layout",marginLeft:"layout",marginHorizontal:"layout",marginVertical:"layout",marginStart:"layout",marginEnd:"layout",left:"layout",right:"layout",top:"layout",bottom:"layout",start:"layout",end:"layout",inset:"layout",flexDirection:"enum",justifyContent:"enum",alignItems:"enum",alignSelf:"enum",position:"enum",flexWrap:"enum",boxSizing:"enum",display:"enum",overflow:"enum",scrollDirection:"enum",borderWidth:"len",borderTopWidth:"len",borderRightWidth:"len",borderBottomWidth:"len",borderLeftWidth:"len",borderHorizontalWidth:"len",borderVerticalWidth:"len",borderStartWidth:"len",borderEndWidth:"len",color:"color",backgroundColor:"color",borderColor:"color",borderTopColor:"color",borderRightColor:"color",borderBottomColor:"color",borderLeftColor:"color",placeholderColor:"color",refreshControlColor:"color",tintColor:"color",rippleColor:"color",overlayColor:"color",backgroundImage:"str",backgroundSize:"enum",backgroundGradient:"str",borderRadius:"len",borderTopLeftRadius:"len",borderTopRightRadius:"len",borderBottomRightRadius:"len",borderBottomLeftRadius:"len",opacity:"num",transform:"str",transformOrigin:"str",pointerEvents:"enum",fontSize:"len",fontWeight:"num",fontStyle:"enum",fontFamily:"str",lineHeight:"len",letterSpacing:"len",textAlign:"enum",textDecoration:"enum",lineClamp:"num",textOverflow:"enum",objectFit:"enum",placeholder:"str",type:"enum",enterKey:"enum",autocapitalize:"enum",autocorrect:"num",maxLength:"num",keyboardShrink:"num",keyboardDismiss:"num",showScrollbar:"num",overscrollMode:"enum",keyboardDismissMode:"enum",snap:"enum",sheetDetents:"str",sheetDetent:"num",sheetDismissible:"num",dim:"num"},Mr={p:"padding",pt:"paddingTop",pr:"paddingRight",pb:"paddingBottom",pl:"paddingLeft",px:"paddingHorizontal",py:"paddingVertical",m:"margin",mt:"marginTop",mr:"marginRight",mb:"marginBottom",ml:"marginLeft",mx:"marginHorizontal",my:"marginVertical",bgColor:"backgroundColor",bgImage:"backgroundImage",bgSize:"backgroundSize",bgGradient:"backgroundGradient"},Fr=["placeholderColor","refreshControlColor","tintColor","rippleColor","overlayColor","backgroundGradient"],Pr={display:"flex",overflow:"visible",scrollDirection:"vertical",borderWidth:0,borderTopWidth:0,borderRightWidth:0,borderBottomWidth:0,borderLeftWidth:0,borderHorizontalWidth:0,borderVerticalWidth:0,borderStartWidth:0,borderEndWidth:0,backgroundColor:0,borderColor:0,borderTopColor:0,borderRightColor:0,borderBottomColor:0,borderLeftColor:0,backgroundImage:"",backgroundSize:"cover",borderRadius:0,borderTopLeftRadius:0,borderTopRightRadius:0,borderBottomRightRadius:0,borderBottomLeftRadius:0,opacity:1,transform:"identity",transformOrigin:"50% 50%",pointerEvents:"all",fontSize:14,fontWeight:400,fontStyle:"normal",lineHeight:"normal",letterSpacing:0,textAlign:"start",textDecoration:"none",lineClamp:0,textOverflow:"ellipsis",objectFit:"cover",placeholder:"",type:"text",enterKey:"done",autocapitalize:"sentences",autocorrect:1,maxLength:0,keyboardShrink:1,keyboardDismiss:1,showScrollbar:1,overscrollMode:"default",keyboardDismissMode:"interactive",snap:"none",sheetDetents:"",sheetDetent:0,sheetDismissible:1,dim:0},Ir=["duration","delay","layer","commit","loop","loopMode","name"];function fn(e){let t=e.trim();/\sxmlns\s*=/.test(t)||(t=t.replace(/<svg\b/,'<svg xmlns="http://www.w3.org/2000/svg"'));const n=new TextEncoder().encode(t);let r="";for(const i of n)r+=String.fromCharCode(i);return`data:image/svg+xml;base64,${btoa(r)}`}function Dr(e){const t=e.match(/viewBox\s*=\s*["']([^"']+)["']/);return t?t[1]:null}const Br=/\b(?:calc|min|max|clamp)\(/,Vr=e=>{if(!Br.test(e))return e;let t="";for(let n=0;n<e.length;n++){if(e.startsWith("var(",n)){let i=0;const s=n;for(;n<e.length;n++)if(e[n]==="(")i++;else if(e[n]===")"&&--i===0)break;t+=e.slice(s,Math.min(n+1,e.length));continue}const r=e[n];if(r==="+"||r==="-"){const i=t[t.length-1]??"";if(!(r==="-"&&(i==="e"&&"btlr".includes(e[n+1]??"")||e.slice(Math.max(0,n-7),n)==="comfort"))&&/[\w%.)]/.test(i)){for(t=t.trimEnd()+" "+r+" ";e[n+1]===" ";)n++;continue}}t+=r}return t.replace(/\d*\.\d+|\d+/g,(n,r,i)=>{const s=i.slice(0,r).trimEnd(),o=i.slice(r+n.length).trimStart();return/[\w.%]$/.test(s)||/^[\w.%]/.test(o)||/[*/]$/.test(s)||/^[*/]/.test(o)?n:n+"px"})},zr={top:"12px",bottom:"4px",left:"16px",right:"16px"},dn=e=>`max(var(--safe-${e}, 0px), calc(var(--safe-${e}-bar, 0px) + var(--comfort-${e}, ${zr[e]})))`,Wr={vw:"cqw",vh:"cqh",vmin:"cqmin",vmax:"cqmax"},Re=e=>Vr(e).replace(new RegExp("(?<![-\\w])comfort-(top|bottom|left|right)\\b","g"),(t,n)=>dn(n)).replace(new RegExp("(?<![-\\w])safe-(top|bottom|left|right)\\b","g"),(t,n)=>`var(--safe-${n}, 0px)`).replace(/(\d)(vmin|vmax|vw|vh)\b/g,(t,n,r)=>n+Wr[r]),pt=/^-?\d*\.?\d+$/,se=e=>{if(typeof e=="number")return e+"px";const t=String(e).trim();return pt.test(t)?t+"px":Re(t)},Ce=e=>{if(typeof e=="number")return"#"+(e>>>0).toString(16).padStart(e<=16777215?6:8,"0");const t=String(e);return t==="clear"?"transparent":t},Gr=/^(rotate|skew)/,Hr=/^translate/,jr=e=>{const t=String(e).trim();return t===""||t==="identity"?"none":t.replace(/([a-zA-Z0-9]+)\s*\(([^)]*)\)/g,(n,r,i)=>{const s=Hr.test(r)?"px":Gr.test(r)?"deg":null;return s===null?n:`${r}(${i.split(",").map(o=>{const l=o.trim();return pt.test(l)?l+s:Re(l)}).join(", ")})`})},pn=e=>typeof e=="number"?e+"px":String(e).trim().split(/\s+/).map(t=>pt.test(t)?t+"px":Re(t)).join(" "),ne=0,re=1,_e=2,Ie=["top","right","bottom","left"],Kr=(e,t)=>e==="safe-all"?`var(--safe-${t}, 0px)`:e==="comfort-all"||e==="comfort-x"||e==="comfort-y"?dn(t):se(e),at=(e,t)=>{const n=(i,s)=>({rank:s,to:o=>i.map(l=>[t(l),Kr(o,l)])}),r=i=>({rank:re,to:s=>[[t(i),se(s)]]});return{[e]:n(Ie,ne),[`${e}Horizontal`]:n(["left","right"],re),[`${e}Vertical`]:n(["top","bottom"],re),[`${e}Start`]:r("inline-start"),[`${e}End`]:r("inline-end"),[`${e}Top`]:n(["top"],_e),[`${e}Right`]:n(["right"],_e),[`${e}Bottom`]:n(["bottom"],_e),[`${e}Left`]:n(["left"],_e)}},Yr=()=>{const e=at("border",n=>`border-${n}-width`),t={};for(const n of Object.keys(e))t[`${n}Width`]=e[n];return t},ht=e=>{const t=[];let n=0,r="";for(const i of e.trim()){if(i==="("&&n++,i===")"&&n--,/\s/.test(i)&&n===0){r&&t.push(r),r="";continue}r+=i}return r&&t.push(r),t},qr=new Set(["none","solid","dashed","dotted","double","hidden"]),Xr=/^-?\d*\.?\d+([a-z%]*)$|^(calc|min|max|clamp)\(/,we=(e,t)=>({rank:t,to:n=>{let r=null,i=null,s=null;if(typeof n=="number")r=n+"px";else for(const l of ht(String(n)))qr.has(l)?s=l:Xr.test(l)?r=se(l):i=Ce(l);const o=[];for(const l of e)o.push([`border-${l}-width`,r??(i?"1px":"0px")]),o.push([`border-${l}-color`,i??"transparent"]),s&&o.push([`border-${l}-style`,s]);return o}}),Jr=e=>{const t=String(e).trim(),n=(s,o,l)=>[["flex-grow",s],["flex-shrink",o],["flex-basis",l]];if(t==="none")return n("0","0","auto");if(t==="auto")return n("1","1","auto");if(t==="initial")return n("0","1","auto");const r=ht(t),i=s=>s!==void 0&&/^-?\d*\.?\d+$/.test(s);return i(r[0])?r.length===1?n(r[0],"1","0%"):i(r[1])?n(r[0],r[1],r[2]!==void 0?se(r[2]):"0%"):n(r[0],"1",se(r[1])):n("1","1",se(r[0]??"auto"))},hn=e=>{var t;return typeof e=="object"&&e!==null&&"_h"in e&&typeof((t=e._h)==null?void 0:t.id)=="number"},Qr=e=>e.replace(/<svg\b[^>]*>/,t=>t.replace(/\spreserveAspectRatio\s*=\s*("[^"]*"|'[^']*')/,"").replace(/^<svg\b/,'<svg preserveAspectRatio="none"')),mn=(e,t)=>{if(hn(e))return`url("${t.bufferUrl(e._h.id)}")`;const n=String(e??"");return n===""?"none":n.startsWith("id:")?`url("${t.bufferUrl(Number(n.slice(3)))}")`:n.startsWith("svg:")?`url("${fn(Qr(n.slice(4)))}")`:`url("${n.replace(/["\\\n]/g,r=>"\\"+r)}")`},Zr=/^\s*(repeating-)?(linear|radial|conic)-gradient\(/,gn=e=>{const t=String(e??"").trim();return t===""?"none":Re(t)},eo=(e,t)=>{const n=typeof e=="string"&&Zr.test(e),r=hn(e)||typeof e=="string"&&/^(id:|https?:|data:|blob:|\.{0,2}\/)/.test(e);return[["--lc-bg-grad",n?gn(e):"none"],["--lc-bg-img",r?mn(e,t):"none"],["background-color",n||r?"transparent":Ce(e)]]},to=e=>{const t=typeof e=="number"?[e+"px"]:ht(String(e)).map(o=>pn(o)),[n,r=n,i=n,s=r]=t;return[["border-top-left-radius",n??"0px"],["border-top-right-radius",r??"0px"],["border-bottom-right-radius",i??"0px"],["border-bottom-left-radius",s??"0px"]]},no=new Set(["serif","sans-serif","monospace","cursive","fantasy","system-ui","ui-monospace","ui-sans-serif","ui-serif","inherit"]),ro=e=>{const t=String(e).trim();return t==="monospaced"?"monospace":t.startsWith("var(")||t.includes(",")||/^["']/.test(t)||no.has(t)?t:`"${t.replace(/"/g,'\\"')}"`},oo=e=>e===!0||e==="true"||typeof e=="number"&&e>.5||e==="1",so=e=>t=>e[String(t)]??String(t),Ae={width:{css:"width",as:"len"},height:{css:"height",as:"len"},minWidth:{css:"min-width",as:"len"},minHeight:{css:"min-height",as:"len"},maxWidth:{css:"max-width",as:"len"},maxHeight:{css:"max-height",as:"len"},flexBasis:{css:"flex-basis",as:"len"},flexGrow:{css:"flex-grow",as:"num"},flexShrink:{css:"flex-shrink",as:"num"},aspectRatio:{css:"aspect-ratio",as:"num"},gap:{css:["row-gap","column-gap"],as:"len",rank:ne},rowGap:{css:"row-gap",as:"len"},columnGap:{css:"column-gap",as:"len"},...at("padding",e=>`padding-${e}`),...at("margin",e=>`margin-${e}`),inset:{css:Ie,as:"len",rank:ne},top:{css:"top",as:"len"},right:{css:"right",as:"len"},bottom:{css:"bottom",as:"len"},left:{css:"left",as:"len"},start:{css:"inset-inline-start",as:"len",rank:re},end:{css:"inset-inline-end",as:"len",rank:re},flexDirection:{css:"flex-direction",as:"raw"},justifyContent:{css:"justify-content",as:"raw"},alignItems:{css:"align-items",as:"raw"},alignSelf:{css:"align-self",as:"raw"},position:{css:"position",as:"raw"},flexWrap:{css:"flex-wrap",as:"raw"},boxSizing:{css:"box-sizing",as:"raw"},display:{css:"display",as:"raw"},overflow:{rank:ne,to:e=>{const t=e==="scroll"?"auto":String(e);return[["overflow-x",t],["overflow-y",t]]}},scrollDirection:{to:e=>e==="horizontal"?[["overflow-x","auto"],["overflow-y","hidden"],["flex-direction","row"]]:e==="all"?[["overflow-x","auto"],["overflow-y","auto"]]:[["overflow-x","hidden"],["overflow-y","auto"],["flex-direction","column"]],rank:re},...Yr(),color:{css:"color"},backgroundColor:{css:"background-color"},borderColor:{css:Ie.map(e=>`border-${e}-color`),rank:ne},borderTopColor:{css:"border-top-color"},borderRightColor:{css:"border-right-color"},borderBottomColor:{css:"border-bottom-color"},borderLeftColor:{css:"border-left-color"},placeholderColor:{css:"--lc-placeholder"},refreshControlColor:{css:"--lc-refresh"},tintColor:{css:"--lc-tint"},overlayColor:{css:"--lc-overlay"},rippleColor:{none:"a browser has no ripple: the press feedback of a page is `$pressed`"},backgroundImage:{to:(e,t)=>[["--lc-bg-img",mn(e,t)]]},backgroundGradient:{to:e=>[["--lc-bg-grad",gn(e)]]},backgroundSize:{to:e=>e==="tile"?[["background-size","auto"],["background-repeat","repeat"],["background-position","0 0"]]:[["background-size",e==="fill"?"100% 100%":String(e)],["background-repeat","no-repeat"],["background-position","center"]]},borderRadius:{to,rank:ne},borderTopLeftRadius:{css:"border-top-left-radius"},borderTopRightRadius:{css:"border-top-right-radius"},borderBottomRightRadius:{css:"border-bottom-right-radius"},borderBottomLeftRadius:{css:"border-bottom-left-radius"},opacity:{css:"opacity"},transform:{to:e=>[["transform",jr(e)]]},transformOrigin:{css:"transform-origin",as:"lens"},pointerEvents:{to:e=>[["pointer-events",e==="none"?"none":"auto"]]},dim:{css:"--lc-dim"},fontSize:{css:"font-size"},fontWeight:{css:"font-weight",as:"raw"},fontStyle:{to:e=>[["font-style",e===2?"italic":e===0?"normal":String(e)]]},fontFamily:{to:e=>[["font-family",ro(e)]]},lineHeight:{to:e=>[["line-height",e==="normal"?"1.2":se(e)]]},letterSpacing:{css:"letter-spacing"},textAlign:{css:"text-align"},textDecoration:{css:"text-decoration-line"},lineClamp:{rank:ne,to:e=>Number(e)>0?[["display","-webkit-box"],["-webkit-box-orient","vertical"],["-webkit-line-clamp",String(Math.round(Number(e)))],["overflow-x","hidden"],["overflow-y","hidden"]]:[["-webkit-line-clamp","none"]]},textOverflow:{css:"text-overflow"},objectFit:{to:e=>[["object-fit",String(e)],["--lc-fit",e==="fill"?"100% 100%":String(e)]]},placeholder:{attr:!0},type:{attr:!0},enterKey:{attr:!0},autocapitalize:{attr:!0},autocorrect:{attr:!0},maxLength:{attr:!0},keyboardShrink:{attr:!0},keyboardDismiss:{attr:!0},showScrollbar:{to:e=>[["scrollbar-width",oo(e)?"thin":"none"]]},overscrollMode:{to:e=>[["overscroll-behavior",so({default:"auto",none:"none",absorb:"contain"})(e)]]},keyboardDismissMode:{attr:!0},snap:{to:e=>e==="none"?[["scroll-snap-type","none"],["--lc-snap","none"]]:[["scroll-snap-type","both mandatory"],["--lc-snap",String(e)]]},sheetDetents:{attr:!0},sheetDetent:{attr:!0},sheetDismissible:{attr:!0},background:{to:eo,rank:ne},border:we(Ie,ne),borderTop:we(["top"],re),borderRight:we(["right"],re),borderBottom:we(["bottom"],re),borderLeft:we(["left"],re),flex:{to:Jr,rank:ne}},Xe=e=>Mr[e]??e,io=e=>e in Ae,bn=e=>{const t=Ae[e];return!!t&&"attr"in t},ao={overflow:"overflow-x",pointerEvents:"pointer-events",fontFamily:"font-family",fontStyle:"font-style",lineHeight:"line-height",objectFit:"object-fit",transform:"transform",borderRadius:"border-top-left-radius",borderWidth:"border-top-width",borderColor:"border-top-color",backgroundImage:"--lc-bg-img",backgroundGradient:"--lc-bg-grad",backgroundSize:"background-size",showScrollbar:"scrollbar-width",overscrollMode:"overscroll-behavior",snap:"--lc-snap",lineClamp:"-webkit-line-clamp",borderTopWidth:"border-top-width",borderRightWidth:"border-right-width",borderBottomWidth:"border-bottom-width",borderLeftWidth:"border-left-width",borderHorizontalWidth:"border-left-width",borderVerticalWidth:"border-top-width",borderStartWidth:"border-inline-start-width",borderEndWidth:"border-inline-end-width"},lo=e=>{const t=Ae[e];return t&&"css"in t&&typeof t.css=="string"?t.css:ao[e]??null},co={len:se,lens:pn,num:e=>String(e),raw:e=>String(e),color:Ce},uo={color:"color",len:"len",enum:"raw",num:"num",str:"raw",layout:"raw"},Mt=e=>{const t=Ae[e];return t&&"rank"in t&&t.rank!==void 0?t.rank:_e},Je=(e,t,n)=>{const r=[...e].filter(([,s])=>s!=null);r.sort((s,o)=>Mt(s[0])-Mt(o[0]));const i=new Map;for(const[s,o]of r){const l=Ae[s];if(!l){n==null||n(s);continue}if("attr"in l||"none"in l)continue;if("to"in l){for(const[y,k]of l.to(o,t))i.set(y,k);continue}const d=co[l.as??uo[Be[s]??"layout"]],f=d(o);if(typeof l.css=="string")i.set(l.css,f);else for(const y of l.css)i.set(y,f)}return i},Ft=e=>[...e].sort((t,n)=>t[0]<n[0]?-1:t[0]>n[0]?1:0).map(([t,n])=>`${t}:${n}`).join(";"),yn=0,vn=1,Le=2,xe=3,En=4,me=65,Ve=1.70158,Ue=Ve*1.525,Pt=Ve+1,It=2*Math.PI/3,Dt=2*Math.PI/4.5,Ne=e=>e<1/2.75?7.5625*e*e:e<2/2.75?7.5625*(e-=1.5/2.75)*e+.75:e<2.5/2.75?7.5625*(e-=2.25/2.75)*e+.9375:7.5625*(e-=2.625/2.75)*e+.984375,lt=[["linear",e=>e],["inSine",e=>1-Math.cos(e*Math.PI/2)],["outSine",e=>Math.sin(e*Math.PI/2)],["inOutSine",e=>-(Math.cos(Math.PI*e)-1)/2],["inQuad",e=>e*e],["outQuad",e=>1-(1-e)*(1-e)],["inOutQuad",e=>e<.5?2*e*e:1-(-2*e+2)**2/2],["inCubic",e=>e*e*e],["outCubic",e=>1-(1-e)**3],["inOutCubic",e=>e<.5?4*e*e*e:1-(-2*e+2)**3/2],["inQuart",e=>e**4],["outQuart",e=>1-(1-e)**4],["inOutQuart",e=>e<.5?8*e**4:1-(-2*e+2)**4/2],["inExpo",e=>e===0?0:2**(10*e-10)],["outExpo",e=>e===1?1:1-2**(-10*e)],["inOutExpo",e=>e===0?0:e===1?1:e<.5?2**(20*e-10)/2:(2-2**(-20*e+10))/2],["inCirc",e=>1-Math.sqrt(1-e*e)],["outCirc",e=>Math.sqrt(1-(e-1)**2)],["inOutCirc",e=>e<.5?(1-Math.sqrt(1-(2*e)**2))/2:(Math.sqrt(1-(-2*e+2)**2)+1)/2],["inBack",e=>Pt*e*e*e-Ve*e*e],["outBack",e=>1+Pt*(e-1)**3+Ve*(e-1)**2],["inOutBack",e=>e<.5?(2*e)**2*((Ue+1)*2*e-Ue)/2:((2*e-2)**2*((Ue+1)*(e*2-2)+Ue)+2)/2],["inElastic",e=>e===0?0:e===1?1:-(2**(10*e-10))*Math.sin((e*10-10.75)*It)],["outElastic",e=>e===0?0:e===1?1:2**(-10*e)*Math.sin((e*10-.75)*It)+1],["inOutElastic",e=>e===0?0:e===1?1:e<.5?-(2**(20*e-10)*Math.sin((20*e-11.125)*Dt))/2:2**(-20*e+10)*Math.sin((20*e-11.125)*Dt)/2+1],["inBounce",e=>1-Ne(1-e)],["outBounce",Ne],["inOutBounce",e=>e<.5?(1-Ne(1-2*e))/2:(1+Ne(2*e-1))/2],["smoothstep",e=>e*e*(3-2*e)]],fo={in:"inQuad",out:"outQuad",inOut:"inOutQuad",easeIn:"inQuad",easeOut:"outQuad",easeInOut:"inOutQuad","ease-in":"inQuad","ease-out":"outQuad","ease-in-out":"inOutQuad"},po=e=>{const t=fo[e]??e;for(let n=0;n<lt.length;n++)if(lt[n][0]===t)return n;return-1},ho=[.25,.1,.25,1],Me={kind:yn,p:[0,0,0,0]},mo=e=>{const t=new Float32Array(me);for(let n=0;n<me;n++)t[n]=e(n/(me-1));return t[0]=0,t[me-1]=1,t},go=e=>{if(e===void 0)return Me;if(typeof e=="function")return{kind:En,p:[0,0,0,0],table:mo(e)};if(Array.isArray(e)){const[i,s,o,l]=e;return{kind:Le,p:[i,s,o,l]}}const t=e.trim();if(t==="linear")return Me;if(t==="ease")return{kind:Le,p:ho};if(t==="steps")return{kind:xe,p:[1,0,0,0]};let n=/^steps\(\s*(\d+)\s*(?:,\s*(start|end|jump-start|jump-end)\s*)?\)$/.exec(t);if(n)return{kind:xe,p:[Math.max(1,parseInt(n[1],10)),n[2]==="start"||n[2]==="jump-start"?1:0,0,0]};if(n=/^cubic-bezier\(\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*\)$/.exec(t),n)return{kind:Le,p:[parseFloat(n[1]),parseFloat(n[2]),parseFloat(n[3]),parseFloat(n[4])]};const r=po(t);return r===0?Me:r>0?{kind:vn,p:[r,0,0,0]}:(console.warn(`[tween] unknown easing "${t}" — using linear`),Me)},bo=(e,t,n,r,i)=>{const s=1-3*r+3*t,o=3*r-6*t,l=3*t,d=1-3*i+3*n,f=3*i-6*n,y=3*n;let k=e;for(let A=0;A<8;A++){const _=((s*k+o)*k+l)*k-e,p=(3*s*k+2*o)*k+l;if(Math.abs(_)<1e-6||Math.abs(p)<1e-6)break;k-=_/p}return k=Math.min(1,Math.max(0,k)),((d*k+f)*k+y)*k},yo=(e,t)=>{if(t<=0)return e.kind===xe&&e.p[1]===1?1/e.p[0]:0;if(t>=1)return 1;switch(e.kind){case vn:return lt[e.p[0]][1](t);case Le:return bo(t,e.p[0],e.p[1],e.p[2],e.p[3]);case xe:{const n=e.p[0];return e.p[1]===1?Math.min(1,Math.ceil(t*n)/n):Math.floor(t*n)/n}case En:{const n=e.table,r=t*(me-1),i=Math.floor(r),s=n[i],o=n[Math.min(i+1,me-1)];return s+(o-s)*(r-i)}default:return t}},Bt=24,Vt=e=>String(Math.round(e*1e4)/1e4),vo="cubic-bezier(0.3333, 0, 0.6667, 1)",Eo=e=>{switch(e.kind){case yn:return"linear";case Le:return`cubic-bezier(${e.p.map(Vt).join(", ")})`;case xe:return`steps(${e.p[0]}, ${e.p[1]===1?"jump-start":"jump-end"})`}const t=[];for(let n=0;n<=Bt;n++)t.push(Vt(yo(e,n/Bt)));return`linear(${t.join(", ")})`},wo=e=>e===void 0||e==="smoothstep"?vo:Eo(go(e)),wn=["lc-base","lc-bag","lc-own","lc-orient","lc-class","lc-hover","lc-press","lc-focus"],So=["--lc-bg-grad","--lc-bg-img","--lc-tint","--lc-overlay","--lc-refresh","--lc-fit","--lc-src","--lc-src-tint","--lc-iw","--lc-ih","--lc-ir","--lc-crop-size","--lc-crop-pos"],Sn="lc-app",_o=`
2
+ ${e.stack}`:String(e);try{return JSON.stringify(e)}catch{return String(e)}},zn=(e,t)=>{const n=o=>{fetch(`${e}/log`,{method:"POST",body:JSON.stringify(o),headers:{"content-type":"application/json"},keepalive:!0}).catch(()=>{})};let r=-1;const i=async()=>{try{const{v:o}=await(await fetch(`${e}/version`,{cache:"no-store"})).json();typeof o=="number"&&t(o)}catch{}setTimeout(i,2500)};setTimeout(i,2500);const s=()=>{let o;try{o=new WebSocket(e.replace(/^http/,"ws")+"/ws")}catch{return}o.onopen=()=>o.send(JSON.stringify({type:"hello",v:r})),o.onmessage=l=>{let d={};try{d=JSON.parse(String(l.data))}catch{return}d.type==="reload"&&typeof d.v=="number"&&t(d.v)},o.onclose=()=>{setTimeout(s,2e3)}};return s(),{bundleUrl:o=>`${e}/bundle.js?t=web&v=${o}`,versionOf(o,l){const d=Number(o.headers.get("x-lecodes-v"));return o.headers.has("x-lecodes-v")&&Number.isFinite(d)?d:l},ran(o){r=o},log:(o,l)=>n({type:"log",level:o,text:l.map(Vn).join(" ")}),error:o=>n({type:"error",text:o})}},Wn=(e,t)=>{try{const n=new URL(e,t);return n.origin===new URL(t).origin&&(n.protocol==="http:"||n.protocol==="https:")?n.href:null}catch{return null}},Gn="modulepreload",Hn=function(e,t){return new URL(e,t).href},Et={},ce=function(t,n,r){let i=Promise.resolve();if(n&&n.length>0){let o=function(y){return Promise.all(y.map(k=>Promise.resolve(k).then(A=>({status:"fulfilled",value:A}),A=>({status:"rejected",reason:A}))))};const l=document.getElementsByTagName("link"),d=document.querySelector("meta[property=csp-nonce]"),f=(d==null?void 0:d.nonce)||(d==null?void 0:d.getAttribute("nonce"));i=o(n.map(y=>{if(y=Hn(y,r),y in Et)return;Et[y]=!0;const k=y.endsWith(".css"),A=k?'[rel="stylesheet"]':"";if(!!r)for(let E=l.length-1;E>=0;E--){const b=l[E];if(b.href===y&&(!k||b.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${y}"]${A}`))return;const p=document.createElement("link");if(p.rel=k?"stylesheet":Gn,k||(p.as="script"),p.crossOrigin="",p.href=y,f&&p.setAttribute("nonce",f),document.head.appendChild(p),k)return new Promise((E,b)=>{p.addEventListener("load",E),p.addEventListener("error",()=>b(new Error(`Unable to preload CSS for ${y}`)))})}))}function s(o){const l=new Event("vite:preloadError",{cancelable:!0});if(l.payload=o,window.dispatchEvent(l),!l.defaultPrevented)throw o}return i.then(o=>{for(const l of o||[])l.status==="rejected"&&s(l.reason);return t().catch(s)})},wt="2.1.1",Ke=e=>{const t=e==null?void 0:e.match(/^(\d+)\.\d+\.\d+/);return t?Number(t[1]):null},jn=(e,t)=>Ke(e)!==null&&Ke(e)===Ke(t),Kn=(e,t="woff2")=>{const n=/^\/\/ fonts: (.+)$/m.exec(e);if(!n)return[];const r=[];for(const i of n[1].split(";")){const s=i.indexOf("=");if(s===-1)continue;const o=/^(.+)@(\d+)(i?)$/.exec(i.slice(0,s));o&&r.push({family:o[1],weight:parseInt(o[2],10),italic:o[3]==="i",url:i.slice(s+1).replace("{fmt}",t)})}return r},Yn=async(e,t,n=2e3)=>{const r=t??(typeof document>"u"?void 0:document);if(!(r!=null&&r.fonts)||e.length===0)return;const i=[];for(const s of e)try{const o=new FontFace(s.family,`url("${s.url}")`,{weight:String(s.weight),style:s.italic?"italic":"normal"});r.fonts.add(o),i.push(o.load().catch(()=>{}))}catch{}await Promise.race([Promise.all(i),new Promise(s=>setTimeout(s,n))])},qn=e=>{var d,f,y,k;const t=e.slice(0,2e3),n=new Set,r=t.match(/\/\/ preload: (.+)/);if(r)for(const A of r[1].split(";"))n.add(A);const i=t.includes("// gl: disable")?"off":t.includes("// gl: defer")?"defer":"on",s=((d=t.match(/^\/\/ sdk: (\d+\.\d+\.\d+\S*)/m))==null?void 0:d[1])??null,o=((y=(f=t.match(/^\/\/ name: (.+)$/m))==null?void 0:f[1])==null?void 0:y.trim())??null,l=new Set((((k=t.match(/^\/\/ plugins: (.+)$/m))==null?void 0:k[1])??"").split(";").map(A=>A.trim()).filter(Boolean));return{sdk:s,name:o,preloads:n,gl:i,is2d:t.includes("// 2d: enable"),plugins:l,bootFonts:Kn(t)}},Xn=1,Jn=2,Qn=3,Zn=4,er=5,tr=6,St=7,nr=8,_t=9,rr=10,or=11,sr=12,ir=13,ar=14,lr=15,cr=16,kt=17,ur=18,fr=19,Oe=20,ai=21,li=22,ci=23,ui=24,fi=25,dr=26,pr=27,hr=28,mr=29,Tt=30,Lt=31,gr=32,br=33,xt=34,Rt=35,Ct=36,Pe=1,yr=2,vr=4,de=8,Er=16,Ye=32,wr=64,qe=128,Sr=256,_r=512,kr=1024,Tr=2048,Lr=4096,At=8192,di=16384,xr=32768,Rr=1<<18,Cr=255,$t=256,Ot=512,Ar=1024,Q=0,$r=1,Or=2,Ur=3,De=4,be=255,un=e=>{const t=e.clientWidth;if(!t)return 1;const n=e.getBoundingClientRect().width;return n>0?n/t:1},Ut={text:{type:"text"},password:{type:"password"},search:{type:"search"},tel:{type:"tel"},email:{type:"email"},url:{type:"url"},number:{type:"text",mode:"numeric"},decimal:{type:"text",mode:"decimal"},date:{type:"date"},time:{type:"time"}},Nt=(e,t)=>e==null?t:e===!0||e==="true"||e==="1"||typeof e=="number"&&e>.5,oe=(e,t,n)=>{n===null?e.removeAttribute(t):e.getAttribute(t)!==n&&e.setAttribute(t,n)},Nr=(e,t)=>{if(e.type!=="input"&&e.type!=="textarea")return;const n=e.el,r=t("placeholder");if(oe(n,"placeholder",r==null?null:String(r)),e.type==="input"){const l=Ut[String(t("type")??"text")]??Ut.text,d=n;d.type!==l.type&&(d.type=l.type),oe(n,"inputmode",l.mode??null);const f=t("enterKey");oe(n,"enterkeyhint",f==null?null:String(f))}const i=t("autocapitalize");oe(n,"autocapitalize",i==null?null:String(i));const s=t("autocorrect");s==null?(oe(n,"autocorrect",null),oe(n,"spellcheck",null)):(oe(n,"autocorrect",Nt(s,!0)?"on":"off"),oe(n,"spellcheck",Nt(s,!0)?"true":"false"));const o=Number(t("maxLength")??0);oe(n,"maxlength",o>0?String(Math.round(o)):null)},Be={width:"layout",height:"layout",minWidth:"layout",minHeight:"layout",maxWidth:"layout",maxHeight:"layout",flexBasis:"layout",flexGrow:"layout",flexShrink:"layout",aspectRatio:"layout",gap:"layout",rowGap:"layout",columnGap:"layout",padding:"layout",paddingTop:"layout",paddingRight:"layout",paddingBottom:"layout",paddingLeft:"layout",paddingHorizontal:"layout",paddingVertical:"layout",paddingStart:"layout",paddingEnd:"layout",margin:"layout",marginTop:"layout",marginRight:"layout",marginBottom:"layout",marginLeft:"layout",marginHorizontal:"layout",marginVertical:"layout",marginStart:"layout",marginEnd:"layout",left:"layout",right:"layout",top:"layout",bottom:"layout",start:"layout",end:"layout",inset:"layout",flexDirection:"enum",justifyContent:"enum",alignItems:"enum",alignSelf:"enum",position:"enum",flexWrap:"enum",boxSizing:"enum",display:"enum",overflow:"enum",scrollDirection:"enum",borderWidth:"len",borderTopWidth:"len",borderRightWidth:"len",borderBottomWidth:"len",borderLeftWidth:"len",borderHorizontalWidth:"len",borderVerticalWidth:"len",borderStartWidth:"len",borderEndWidth:"len",color:"color",backgroundColor:"color",borderColor:"color",borderTopColor:"color",borderRightColor:"color",borderBottomColor:"color",borderLeftColor:"color",placeholderColor:"color",refreshControlColor:"color",tintColor:"color",rippleColor:"color",overlayColor:"color",backgroundImage:"str",backgroundSize:"enum",backgroundGradient:"str",borderRadius:"len",borderTopLeftRadius:"len",borderTopRightRadius:"len",borderBottomRightRadius:"len",borderBottomLeftRadius:"len",opacity:"num",transform:"str",transformOrigin:"str",pointerEvents:"enum",fontSize:"len",fontWeight:"num",fontStyle:"enum",fontFamily:"str",lineHeight:"len",letterSpacing:"len",textAlign:"enum",textDecoration:"enum",lineClamp:"num",textOverflow:"enum",objectFit:"enum",placeholder:"str",type:"enum",enterKey:"enum",autocapitalize:"enum",autocorrect:"num",maxLength:"num",keyboardShrink:"num",keyboardDismiss:"num",showScrollbar:"num",overscrollMode:"enum",keyboardDismissMode:"enum",snap:"enum",sheetDetents:"str",sheetDetent:"num",sheetDismissible:"num",dim:"num"},Mr={p:"padding",pt:"paddingTop",pr:"paddingRight",pb:"paddingBottom",pl:"paddingLeft",px:"paddingHorizontal",py:"paddingVertical",m:"margin",mt:"marginTop",mr:"marginRight",mb:"marginBottom",ml:"marginLeft",mx:"marginHorizontal",my:"marginVertical",bgColor:"backgroundColor",bgImage:"backgroundImage",bgSize:"backgroundSize",bgGradient:"backgroundGradient"},Fr=["placeholderColor","refreshControlColor","tintColor","rippleColor","overlayColor","backgroundGradient"],Pr={display:"flex",overflow:"visible",scrollDirection:"vertical",borderWidth:0,borderTopWidth:0,borderRightWidth:0,borderBottomWidth:0,borderLeftWidth:0,borderHorizontalWidth:0,borderVerticalWidth:0,borderStartWidth:0,borderEndWidth:0,backgroundColor:0,borderColor:0,borderTopColor:0,borderRightColor:0,borderBottomColor:0,borderLeftColor:0,backgroundImage:"",backgroundSize:"cover",borderRadius:0,borderTopLeftRadius:0,borderTopRightRadius:0,borderBottomRightRadius:0,borderBottomLeftRadius:0,opacity:1,transform:"identity",transformOrigin:"50% 50%",pointerEvents:"all",fontSize:14,fontWeight:400,fontStyle:"normal",lineHeight:"normal",letterSpacing:0,textAlign:"start",textDecoration:"none",lineClamp:0,textOverflow:"ellipsis",objectFit:"cover",placeholder:"",type:"text",enterKey:"done",autocapitalize:"sentences",autocorrect:1,maxLength:0,keyboardShrink:1,keyboardDismiss:1,showScrollbar:1,overscrollMode:"default",keyboardDismissMode:"interactive",snap:"none",sheetDetents:"",sheetDetent:0,sheetDismissible:1,dim:0},Ir=["duration","delay","layer","commit","loop","loopMode","name"];function fn(e){let t=e.trim();/\sxmlns\s*=/.test(t)||(t=t.replace(/<svg\b/,'<svg xmlns="http://www.w3.org/2000/svg"'));const n=new TextEncoder().encode(t);let r="";for(const i of n)r+=String.fromCharCode(i);return`data:image/svg+xml;base64,${btoa(r)}`}function Dr(e){const t=e.match(/viewBox\s*=\s*["']([^"']+)["']/);return t?t[1]:null}const Br=/\b(?:calc|min|max|clamp)\(/,Vr=e=>{if(!Br.test(e))return e;let t="";for(let n=0;n<e.length;n++){if(e.startsWith("var(",n)){let i=0;const s=n;for(;n<e.length;n++)if(e[n]==="(")i++;else if(e[n]===")"&&--i===0)break;t+=e.slice(s,Math.min(n+1,e.length));continue}const r=e[n];if(r==="+"||r==="-"){const i=t[t.length-1]??"";if(!(r==="-"&&(i==="e"&&"btlr".includes(e[n+1]??"")||e.slice(Math.max(0,n-7),n)==="comfort"))&&/[\w%.)]/.test(i)){for(t=t.trimEnd()+" "+r+" ";e[n+1]===" ";)n++;continue}}t+=r}return t.replace(/\d*\.\d+|\d+/g,(n,r,i)=>{const s=i.slice(0,r).trimEnd(),o=i.slice(r+n.length).trimStart();return/[\w.%]$/.test(s)||/^[\w.%]/.test(o)||/[*/]$/.test(s)||/^[*/]/.test(o)?n:n+"px"})},zr={top:"12px",bottom:"4px",left:"16px",right:"16px"},dn=e=>`max(var(--safe-${e}, 0px), calc(var(--safe-${e}-bar, 0px) + var(--comfort-${e}, ${zr[e]})))`,Wr={vw:"cqw",vh:"cqh",vmin:"cqmin",vmax:"cqmax"},Re=e=>Vr(e).replace(new RegExp("(?<![-\\w])comfort-(top|bottom|left|right)\\b","g"),(t,n)=>dn(n)).replace(new RegExp("(?<![-\\w])safe-(top|bottom|left|right)\\b","g"),(t,n)=>`var(--safe-${n}, 0px)`).replace(/(\d)(vmin|vmax|vw|vh)\b/g,(t,n,r)=>n+Wr[r]),pt=/^-?\d*\.?\d+$/,se=e=>{if(typeof e=="number")return e+"px";const t=String(e).trim();return pt.test(t)?t+"px":Re(t)},Ce=e=>{if(typeof e=="number")return"#"+(e>>>0).toString(16).padStart(e<=16777215?6:8,"0");const t=String(e);return t==="clear"?"transparent":t},Gr=/^(rotate|skew)/,Hr=/^translate/,jr=e=>{const t=String(e).trim();return t===""||t==="identity"?"none":t.replace(/([a-zA-Z0-9]+)\s*\(([^)]*)\)/g,(n,r,i)=>{const s=Hr.test(r)?"px":Gr.test(r)?"deg":null;return s===null?n:`${r}(${i.split(",").map(o=>{const l=o.trim();return pt.test(l)?l+s:Re(l)}).join(", ")})`})},pn=e=>typeof e=="number"?e+"px":String(e).trim().split(/\s+/).map(t=>pt.test(t)?t+"px":Re(t)).join(" "),ne=0,re=1,_e=2,Ie=["top","right","bottom","left"],Kr=(e,t)=>e==="safe-all"?`var(--safe-${t}, 0px)`:e==="comfort-all"||e==="comfort-x"||e==="comfort-y"?dn(t):se(e),at=(e,t)=>{const n=(i,s)=>({rank:s,to:o=>i.map(l=>[t(l),Kr(o,l)])}),r=i=>({rank:re,to:s=>[[t(i),se(s)]]});return{[e]:n(Ie,ne),[`${e}Horizontal`]:n(["left","right"],re),[`${e}Vertical`]:n(["top","bottom"],re),[`${e}Start`]:r("inline-start"),[`${e}End`]:r("inline-end"),[`${e}Top`]:n(["top"],_e),[`${e}Right`]:n(["right"],_e),[`${e}Bottom`]:n(["bottom"],_e),[`${e}Left`]:n(["left"],_e)}},Yr=()=>{const e=at("border",n=>`border-${n}-width`),t={};for(const n of Object.keys(e))t[`${n}Width`]=e[n];return t},ht=e=>{const t=[];let n=0,r="";for(const i of e.trim()){if(i==="("&&n++,i===")"&&n--,/\s/.test(i)&&n===0){r&&t.push(r),r="";continue}r+=i}return r&&t.push(r),t},qr=new Set(["none","solid","dashed","dotted","double","hidden"]),Xr=/^-?\d*\.?\d+([a-z%]*)$|^(calc|min|max|clamp)\(/,we=(e,t)=>({rank:t,to:n=>{let r=null,i=null,s=null;if(typeof n=="number")r=n+"px";else for(const l of ht(String(n)))qr.has(l)?s=l:Xr.test(l)?r=se(l):i=Ce(l);const o=[];for(const l of e)o.push([`border-${l}-width`,r??(i?"1px":"0px")]),o.push([`border-${l}-color`,i??"transparent"]),s&&o.push([`border-${l}-style`,s]);return o}}),Jr=e=>{const t=String(e).trim(),n=(s,o,l)=>[["flex-grow",s],["flex-shrink",o],["flex-basis",l]];if(t==="none")return n("0","0","auto");if(t==="auto")return n("1","1","auto");if(t==="initial")return n("0","1","auto");const r=ht(t),i=s=>s!==void 0&&/^-?\d*\.?\d+$/.test(s);return i(r[0])?r.length===1?n(r[0],"1","0%"):i(r[1])?n(r[0],r[1],r[2]!==void 0?se(r[2]):"0%"):n(r[0],"1",se(r[1])):n("1","1",se(r[0]??"auto"))},hn=e=>{var t;return typeof e=="object"&&e!==null&&"_h"in e&&typeof((t=e._h)==null?void 0:t.id)=="number"},Qr=e=>e.replace(/<svg\b[^>]*>/,t=>t.replace(/\spreserveAspectRatio\s*=\s*("[^"]*"|'[^']*')/,"").replace(/^<svg\b/,'<svg preserveAspectRatio="none"')),mn=(e,t)=>{if(hn(e))return`url("${t.bufferUrl(e._h.id)}")`;const n=String(e??"");return n===""?"none":n.startsWith("id:")?`url("${t.bufferUrl(Number(n.slice(3)))}")`:n.startsWith("svg:")?`url("${fn(Qr(n.slice(4)))}")`:`url("${n.replace(/["\\\n]/g,r=>"\\"+r)}")`},Zr=/^\s*(repeating-)?(linear|radial|conic)-gradient\(/,gn=e=>{const t=String(e??"").trim();return t===""?"none":Re(t)},eo=(e,t)=>{const n=typeof e=="string"&&Zr.test(e),r=hn(e)||typeof e=="string"&&/^(id:|https?:|data:|blob:|\.{0,2}\/)/.test(e);return[["--lc-bg-grad",n?gn(e):"none"],["--lc-bg-img",r?mn(e,t):"none"],["background-color",n||r?"transparent":Ce(e)]]},to=e=>{const t=typeof e=="number"?[e+"px"]:ht(String(e)).map(o=>pn(o)),[n,r=n,i=n,s=r]=t;return[["border-top-left-radius",n??"0px"],["border-top-right-radius",r??"0px"],["border-bottom-right-radius",i??"0px"],["border-bottom-left-radius",s??"0px"]]},no=new Set(["serif","sans-serif","monospace","cursive","fantasy","system-ui","ui-monospace","ui-sans-serif","ui-serif","inherit"]),ro=e=>{const t=String(e).trim();return t==="monospaced"?"monospace":t.startsWith("var(")||t.includes(",")||/^["']/.test(t)||no.has(t)?t:`"${t.replace(/"/g,'\\"')}"`},oo=e=>e===!0||e==="true"||typeof e=="number"&&e>.5||e==="1",so=e=>t=>e[String(t)]??String(t),Ae={width:{css:"width",as:"len"},height:{css:"height",as:"len"},minWidth:{css:"min-width",as:"len"},minHeight:{css:"min-height",as:"len"},maxWidth:{css:"max-width",as:"len"},maxHeight:{css:"max-height",as:"len"},flexBasis:{css:"flex-basis",as:"len"},flexGrow:{css:"flex-grow",as:"num"},flexShrink:{css:"flex-shrink",as:"num"},aspectRatio:{css:"aspect-ratio",as:"num"},gap:{css:["row-gap","column-gap"],as:"len",rank:ne},rowGap:{css:"row-gap",as:"len"},columnGap:{css:"column-gap",as:"len"},...at("padding",e=>`padding-${e}`),...at("margin",e=>`margin-${e}`),inset:{css:Ie,as:"len",rank:ne},top:{css:"top",as:"len"},right:{css:"right",as:"len"},bottom:{css:"bottom",as:"len"},left:{css:"left",as:"len"},start:{css:"inset-inline-start",as:"len",rank:re},end:{css:"inset-inline-end",as:"len",rank:re},flexDirection:{css:"flex-direction",as:"raw"},justifyContent:{css:"justify-content",as:"raw"},alignItems:{css:"align-items",as:"raw"},alignSelf:{css:"align-self",as:"raw"},position:{css:"position",as:"raw"},flexWrap:{css:"flex-wrap",as:"raw"},boxSizing:{css:"box-sizing",as:"raw"},display:{css:"display",as:"raw"},overflow:{rank:ne,to:e=>{const t=e==="scroll"?"auto":String(e);return[["overflow-x",t],["overflow-y",t]]}},scrollDirection:{to:e=>e==="horizontal"?[["overflow-x","auto"],["overflow-y","hidden"],["flex-direction","row"]]:e==="all"?[["overflow-x","auto"],["overflow-y","auto"]]:[["overflow-x","hidden"],["overflow-y","auto"],["flex-direction","column"]],rank:re},...Yr(),color:{css:"color"},backgroundColor:{css:"background-color"},borderColor:{css:Ie.map(e=>`border-${e}-color`),rank:ne},borderTopColor:{css:"border-top-color"},borderRightColor:{css:"border-right-color"},borderBottomColor:{css:"border-bottom-color"},borderLeftColor:{css:"border-left-color"},placeholderColor:{css:"--lc-placeholder"},refreshControlColor:{css:"--lc-refresh"},tintColor:{css:"--lc-tint"},overlayColor:{css:"--lc-overlay"},rippleColor:{none:"a browser has no ripple: the press feedback of a page is `$pressed`"},backgroundImage:{to:(e,t)=>[["--lc-bg-img",mn(e,t)]]},backgroundGradient:{to:e=>[["--lc-bg-grad",gn(e)]]},backgroundSize:{to:e=>e==="tile"?[["background-size","auto"],["background-repeat","repeat"],["background-position","0 0"]]:[["background-size",e==="fill"?"100% 100%":String(e)],["background-repeat","no-repeat"],["background-position","center"]]},borderRadius:{to,rank:ne},borderTopLeftRadius:{css:"border-top-left-radius"},borderTopRightRadius:{css:"border-top-right-radius"},borderBottomRightRadius:{css:"border-bottom-right-radius"},borderBottomLeftRadius:{css:"border-bottom-left-radius"},opacity:{css:"opacity"},transform:{to:e=>[["transform",jr(e)]]},transformOrigin:{css:"transform-origin",as:"lens"},pointerEvents:{to:e=>[["pointer-events",e==="none"?"none":"auto"]]},dim:{css:"--lc-dim"},fontSize:{css:"font-size"},fontWeight:{css:"font-weight",as:"raw"},fontStyle:{to:e=>[["font-style",e===2?"italic":e===0?"normal":String(e)]]},fontFamily:{to:e=>[["font-family",ro(e)]]},lineHeight:{to:e=>[["line-height",e==="normal"?"1.2":se(e)]]},letterSpacing:{css:"letter-spacing"},textAlign:{css:"text-align"},textDecoration:{css:"text-decoration-line"},lineClamp:{rank:ne,to:e=>Number(e)>0?[["display","-webkit-box"],["-webkit-box-orient","vertical"],["-webkit-line-clamp",String(Math.round(Number(e)))],["overflow-x","hidden"],["overflow-y","hidden"]]:[["-webkit-line-clamp","none"]]},textOverflow:{css:"text-overflow"},objectFit:{to:e=>[["object-fit",String(e)],["--lc-fit",e==="fill"?"100% 100%":String(e)]]},placeholder:{attr:!0},type:{attr:!0},enterKey:{attr:!0},autocapitalize:{attr:!0},autocorrect:{attr:!0},maxLength:{attr:!0},keyboardShrink:{attr:!0},keyboardDismiss:{attr:!0},showScrollbar:{to:e=>[["scrollbar-width",oo(e)?"thin":"none"]]},overscrollMode:{to:e=>[["overscroll-behavior",so({default:"auto",none:"none",absorb:"contain"})(e)]]},keyboardDismissMode:{attr:!0},snap:{to:e=>e==="none"?[["scroll-snap-type","none"],["--lc-snap","none"]]:[["scroll-snap-type","both mandatory"],["--lc-snap",String(e)]]},sheetDetents:{attr:!0},sheetDetent:{attr:!0},sheetDismissible:{attr:!0},background:{to:eo,rank:ne},border:we(Ie,ne),borderTop:we(["top"],re),borderRight:we(["right"],re),borderBottom:we(["bottom"],re),borderLeft:we(["left"],re),flex:{to:Jr,rank:ne}},Xe=e=>Mr[e]??e,io=e=>e in Ae,bn=e=>{const t=Ae[e];return!!t&&"attr"in t},ao={overflow:"overflow-x",pointerEvents:"pointer-events",fontFamily:"font-family",fontStyle:"font-style",lineHeight:"line-height",objectFit:"object-fit",transform:"transform",borderRadius:"border-top-left-radius",borderWidth:"border-top-width",borderColor:"border-top-color",backgroundImage:"--lc-bg-img",backgroundGradient:"--lc-bg-grad",backgroundSize:"background-size",showScrollbar:"scrollbar-width",overscrollMode:"overscroll-behavior",snap:"--lc-snap",lineClamp:"-webkit-line-clamp",borderTopWidth:"border-top-width",borderRightWidth:"border-right-width",borderBottomWidth:"border-bottom-width",borderLeftWidth:"border-left-width",borderHorizontalWidth:"border-left-width",borderVerticalWidth:"border-top-width",borderStartWidth:"border-inline-start-width",borderEndWidth:"border-inline-end-width"},lo=e=>{const t=Ae[e];return t&&"css"in t&&typeof t.css=="string"?t.css:ao[e]??null},co={len:se,lens:pn,num:e=>String(e),raw:e=>String(e),color:Ce},uo={color:"color",len:"len",enum:"raw",num:"num",str:"raw",layout:"raw"},Mt=e=>{const t=Ae[e];return t&&"rank"in t&&t.rank!==void 0?t.rank:_e},Je=(e,t,n)=>{const r=[...e].filter(([,s])=>s!=null);r.sort((s,o)=>Mt(s[0])-Mt(o[0]));const i=new Map;for(const[s,o]of r){const l=Ae[s];if(!l){n==null||n(s);continue}if("attr"in l||"none"in l)continue;if("to"in l){for(const[y,k]of l.to(o,t))i.set(y,k);continue}const d=co[l.as??uo[Be[s]??"layout"]],f=d(o);if(typeof l.css=="string")i.set(l.css,f);else for(const y of l.css)i.set(y,f)}return i},Ft=e=>[...e].sort((t,n)=>t[0]<n[0]?-1:t[0]>n[0]?1:0).map(([t,n])=>`${t}:${n}`).join(";"),yn=0,vn=1,Le=2,xe=3,En=4,me=65,Ve=1.70158,Ue=Ve*1.525,Pt=Ve+1,It=2*Math.PI/3,Dt=2*Math.PI/4.5,Ne=e=>e<1/2.75?7.5625*e*e:e<2/2.75?7.5625*(e-=1.5/2.75)*e+.75:e<2.5/2.75?7.5625*(e-=2.25/2.75)*e+.9375:7.5625*(e-=2.625/2.75)*e+.984375,lt=[["linear",e=>e],["inSine",e=>1-Math.cos(e*Math.PI/2)],["outSine",e=>Math.sin(e*Math.PI/2)],["inOutSine",e=>-(Math.cos(Math.PI*e)-1)/2],["inQuad",e=>e*e],["outQuad",e=>1-(1-e)*(1-e)],["inOutQuad",e=>e<.5?2*e*e:1-(-2*e+2)**2/2],["inCubic",e=>e*e*e],["outCubic",e=>1-(1-e)**3],["inOutCubic",e=>e<.5?4*e*e*e:1-(-2*e+2)**3/2],["inQuart",e=>e**4],["outQuart",e=>1-(1-e)**4],["inOutQuart",e=>e<.5?8*e**4:1-(-2*e+2)**4/2],["inExpo",e=>e===0?0:2**(10*e-10)],["outExpo",e=>e===1?1:1-2**(-10*e)],["inOutExpo",e=>e===0?0:e===1?1:e<.5?2**(20*e-10)/2:(2-2**(-20*e+10))/2],["inCirc",e=>1-Math.sqrt(1-e*e)],["outCirc",e=>Math.sqrt(1-(e-1)**2)],["inOutCirc",e=>e<.5?(1-Math.sqrt(1-(2*e)**2))/2:(Math.sqrt(1-(-2*e+2)**2)+1)/2],["inBack",e=>Pt*e*e*e-Ve*e*e],["outBack",e=>1+Pt*(e-1)**3+Ve*(e-1)**2],["inOutBack",e=>e<.5?(2*e)**2*((Ue+1)*2*e-Ue)/2:((2*e-2)**2*((Ue+1)*(e*2-2)+Ue)+2)/2],["inElastic",e=>e===0?0:e===1?1:-(2**(10*e-10))*Math.sin((e*10-10.75)*It)],["outElastic",e=>e===0?0:e===1?1:2**(-10*e)*Math.sin((e*10-.75)*It)+1],["inOutElastic",e=>e===0?0:e===1?1:e<.5?-(2**(20*e-10)*Math.sin((20*e-11.125)*Dt))/2:2**(-20*e+10)*Math.sin((20*e-11.125)*Dt)/2+1],["inBounce",e=>1-Ne(1-e)],["outBounce",Ne],["inOutBounce",e=>e<.5?(1-Ne(1-2*e))/2:(1+Ne(2*e-1))/2],["smoothstep",e=>e*e*(3-2*e)]],fo={in:"inQuad",out:"outQuad",inOut:"inOutQuad",easeIn:"inQuad",easeOut:"outQuad",easeInOut:"inOutQuad","ease-in":"inQuad","ease-out":"outQuad","ease-in-out":"inOutQuad"},po=e=>{const t=fo[e]??e;for(let n=0;n<lt.length;n++)if(lt[n][0]===t)return n;return-1},ho=[.25,.1,.25,1],Me={kind:yn,p:[0,0,0,0]},mo=e=>{const t=new Float32Array(me);for(let n=0;n<me;n++)t[n]=e(n/(me-1));return t[0]=0,t[me-1]=1,t},go=e=>{if(e===void 0)return Me;if(typeof e=="function")return{kind:En,p:[0,0,0,0],table:mo(e)};if(Array.isArray(e)){const[i,s,o,l]=e;return{kind:Le,p:[i,s,o,l]}}const t=e.trim();if(t==="linear")return Me;if(t==="ease")return{kind:Le,p:ho};if(t==="steps")return{kind:xe,p:[1,0,0,0]};let n=/^steps\(\s*(\d+)\s*(?:,\s*(start|end|jump-start|jump-end)\s*)?\)$/.exec(t);if(n)return{kind:xe,p:[Math.max(1,parseInt(n[1],10)),n[2]==="start"||n[2]==="jump-start"?1:0,0,0]};if(n=/^cubic-bezier\(\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*,\s*([-\d.e]+)\s*\)$/.exec(t),n)return{kind:Le,p:[parseFloat(n[1]),parseFloat(n[2]),parseFloat(n[3]),parseFloat(n[4])]};const r=po(t);return r===0?Me:r>0?{kind:vn,p:[r,0,0,0]}:(console.warn(`[tween] unknown easing "${t}" — using linear`),Me)},bo=(e,t,n,r,i)=>{const s=1-3*r+3*t,o=3*r-6*t,l=3*t,d=1-3*i+3*n,f=3*i-6*n,y=3*n;let k=e;for(let A=0;A<8;A++){const _=((s*k+o)*k+l)*k-e,p=(3*s*k+2*o)*k+l;if(Math.abs(_)<1e-6||Math.abs(p)<1e-6)break;k-=_/p}return k=Math.min(1,Math.max(0,k)),((d*k+f)*k+y)*k},yo=(e,t)=>{if(t<=0)return e.kind===xe&&e.p[1]===1?1/e.p[0]:0;if(t>=1)return 1;switch(e.kind){case vn:return lt[e.p[0]][1](t);case Le:return bo(t,e.p[0],e.p[1],e.p[2],e.p[3]);case xe:{const n=e.p[0];return e.p[1]===1?Math.min(1,Math.ceil(t*n)/n):Math.floor(t*n)/n}case En:{const n=e.table,r=t*(me-1),i=Math.floor(r),s=n[i],o=n[Math.min(i+1,me-1)];return s+(o-s)*(r-i)}default:return t}},Bt=24,Vt=e=>String(Math.round(e*1e4)/1e4),vo="cubic-bezier(0.3333, 0, 0.6667, 1)",Eo=e=>{switch(e.kind){case yn:return"linear";case Le:return`cubic-bezier(${e.p.map(Vt).join(", ")})`;case xe:return`steps(${e.p[0]}, ${e.p[1]===1?"jump-start":"jump-end"})`}const t=[];for(let n=0;n<=Bt;n++)t.push(Vt(yo(e,n/Bt)));return`linear(${t.join(", ")})`},wo=e=>e===void 0||e==="smoothstep"?vo:Eo(go(e)),wn=["lc-base","lc-bag","lc-own","lc-orient","lc-class","lc-hover","lc-press","lc-focus"],So=["--lc-bg-grad","--lc-bg-img","--lc-tint","--lc-overlay","--lc-refresh","--lc-fit","--lc-src","--lc-src-tint","--lc-iw","--lc-ih","--lc-ir","--lc-crop-size","--lc-crop-pos"],Sn="lc-app",_o=`
3
3
  @layer ${wn.join(", ")};
4
4
 
5
5
  @layer lc-base {
@@ -8,9 +8,10 @@ import { compileShell, type BundleResource } from "../compile"
8
8
  import { CliError, UsageError, bool, c, defineCommand, log, note, promptHidden, str, success, warn, type FlagValues } from "../../cli"
9
9
  import { upsertDotEnv } from "../../project/env"
10
10
  import {
11
- type AppConfig, SDK_VARIANTS, VERSION_RE, childEnv, cmpVersion, ensureShellIgnored, fetchLatestSdkVersion,
11
+ type AppConfig, ANDROID_SDK_VARIANTS, type AndroidSdkVariant, VERSION_RE, androidSdkArtifact, androidVariantEngines,
12
+ childEnv, cmpVersion, ensureShellIgnored, fetchLatestSdkVersion,
12
13
  projectRoot, readJson, resolvePlugin, resolveSdkVersion, resolveVariant, runTool, sdkSatisfies, slugify,
13
- writeJson, type LockEntry,
14
+ writeJson, type LockEntry, resolveUpdateUrl,
14
15
  } from "./shared"
15
16
  import { loadIconMaster, restampManifestIcon, writeAndroidIcons } from "./icon"
16
17
  import {
@@ -19,10 +20,9 @@ import {
19
20
  appManifestTemplate, appViewModelTemplate, gradleWrapperProperties, mainActivityTemplate,
20
21
  pluginBuildGradleTemplate, rootBuildGradleTemplate, runtimeBuildGradleTemplate,
21
22
  runtimeKotlinTemplate, settingsGradleTemplate, stringsXmlTemplate, themesXmlTemplate,
22
- type AndroidRuntimePlugin, type AndroidShellIdentity,
23
+ type AndroidRuntimePlugin, type AndroidShellIdentity, runtimeManifestTemplate,
23
24
  } from "./templates/android"
24
25
  import { GRADLEW_BAT, GRADLEW_SH } from "./templates/gradlew"
25
- import { type SdkVariant } from "./templates/ios"
26
26
 
27
27
  /* The Android arm of `lecodes app` — the mirror of the iOS flows in index.ts, over Gradle
28
28
  * instead of Xcode: init / sync / open / doctor checks as functions the shared command nodes call,
@@ -33,20 +33,18 @@ import { type SdkVariant } from "./templates/ios"
33
33
 
34
34
  export const ANDROID_SDK_REPO = "letary/lecodes-android-sdk"
35
35
 
36
- /** Oldest lecodes-android-sdk a generated shell can use — 1.2.0 moved system-back wiring into
37
- * engine.attach() and the template dropped its own dispatcher block, so an older SDK under
38
- * this template would leave the back button dead. */
39
- export const ANDROID_SDK_MIN_VERSION = "1.2.0"
36
+ /** Oldest lecodes-android-sdk a generated shell can use — 2.0.13 = the first 2.x release (the fat
37
+ * AAR of hosts/android's :lecodes over runtime/native: the tree of truth, the host tables,
38
+ * :lecodes-ui). A 2.x bundle is refused by a 1.x runtime (`_creatorApp.run()` gates on the major),
39
+ * so a shell pinned below this would install and show nothing; `lecodes app update` moves a 1.x
40
+ * shell here. (1.2.0 was the 1.x floor: system back moved into engine.attach().) */
41
+ export const ANDROID_SDK_MIN_VERSION = "2.0.13"
40
42
 
41
43
  /** First lecodes-android-sdk release that publishes the variant matrix (lecodes-sdk-3d / -2d /
42
- * -core next to the full lecodes-sdk asset). Older pins always link the full artifact. */
44
+ * -core next to the full lecodes-sdk asset). Older pins always link the full artifact. The vulkan
45
+ * artifact's floor is ANDROID_SDK_VULKAN_MIN (shared.ts); androidSdkArtifact holds it. */
43
46
  export const ANDROID_SDK_VARIANTS_MIN = "1.3.0"
44
47
 
45
- /** Release-asset / ivy artifact name for an SDK variant — full keeps the historic plain name so
46
- * pre-variant pins and shells keep resolving. */
47
- const variantArtifact = (variant: SdkVariant): string =>
48
- variant === "full" ? "lecodes-sdk" : `lecodes-sdk-${variant}`
49
-
50
48
  const ANDROID_SDK_URL_DEFAULT = `https://github.com/${ANDROID_SDK_REPO}/releases/download`
51
49
 
52
50
  /** The ivy base URL shells resolve the SDK AAR from. `LECODES_ANDROID_SDK_URL` overrides for
@@ -109,8 +107,8 @@ export const initAndroid = async (opts: AndroidInitOptions) => {
109
107
  const existing = readJson<AppConfig>(join(root, "app.json")) ?? {}
110
108
 
111
109
  const variant = opts.variant ?? existing.android?.variant ?? "auto"
112
- if (variant !== "auto" && !SDK_VARIANTS.includes(variant as SdkVariant)) {
113
- throw new CliError(`--variant must be auto, ${SDK_VARIANTS.join(", ")} (got "${variant}")`)
110
+ if (variant !== "auto" && !ANDROID_SDK_VARIANTS.includes(variant as AndroidSdkVariant)) {
111
+ throw new CliError(`--variant must be auto, ${ANDROID_SDK_VARIANTS.join(", ")} (got "${variant}")`)
114
112
  }
115
113
 
116
114
  // The native SDK: a new shell starts on the newest published release, pinned into app.json so
@@ -182,8 +180,8 @@ export const initAndroid = async (opts: AndroidInitOptions) => {
182
180
  write("app/src/main/res/drawable/ic_launcher.xml", IC_LAUNCHER_XML)
183
181
  // Like iOS init: no compiled bundle yet, so an "auto" variant starts on core — the first sync
184
182
  // re-resolves from what the bundle actually uses. Pre-variant SDK pins get the full AAR.
185
- const initVariant: SdkVariant = variant === "auto" ? "core" : variant as SdkVariant
186
- const initArtifact = cmpVersion(sdkVersion, ANDROID_SDK_VARIANTS_MIN) >= 0 ? variantArtifact(initVariant) : "lecodes-sdk"
183
+ const initVariant: AndroidSdkVariant = variant === "auto" ? "core" : variant as AndroidSdkVariant
184
+ const initArtifact = cmpVersion(sdkVersion, ANDROID_SDK_VARIANTS_MIN) >= 0 ? androidSdkArtifact(initVariant, sdkVersion) : "lecodes-sdk"
187
185
  write("lecodes-runtime/build.gradle.kts", runtimeBuildGradleTemplate(sdkVersion, [], initArtifact))
188
186
  write("lecodes-runtime/src/main/kotlin/io/letary/lecodes/runtime/LeCodesRuntime.kt", runtimeKotlinTemplate([]))
189
187
 
@@ -247,16 +245,19 @@ export const syncAndroid = async (root: string, config: AppConfig, prior: SyncAr
247
245
 
248
246
  // ---- variant: the smallest SDK artifact covering what the bundle uses (or the forced one) ----
249
247
  const requestedVariant = config.android?.variant ?? "auto"
250
- if (requestedVariant !== "auto" && !SDK_VARIANTS.includes(requestedVariant as SdkVariant)) {
251
- throw new CliError(`app.json android.variant must be "auto" or one of ${SDK_VARIANTS.join(", ")} (got "${requestedVariant}").`)
248
+ if (requestedVariant !== "auto" && !ANDROID_SDK_VARIANTS.includes(requestedVariant as AndroidSdkVariant)) {
249
+ throw new CliError(`app.json android.variant must be "auto" or one of ${ANDROID_SDK_VARIANTS.join(", ")} (got "${requestedVariant}").`)
252
250
  }
253
- const { variant, detected } = resolveVariant(requestedVariant, bundleJs, "android.variant")
254
- let sdkArtifact = variantArtifact(variant)
251
+ // vulkan is full's engines on another backend: the coverage check runs as full, the artifact is its own.
252
+ const { variant: covered, detected } = resolveVariant(
253
+ requestedVariant === "auto" ? "auto" : androidVariantEngines(requestedVariant as AndroidSdkVariant), bundleJs, "android.variant")
254
+ const variant: AndroidSdkVariant = requestedVariant === "vulkan" ? "vulkan" : covered
255
+ let sdkArtifact = androidSdkArtifact(variant, sdkVersion)
255
256
  if (sdkArtifact !== "lecodes-sdk" && cmpVersion(sdkVersion, ANDROID_SDK_VARIANTS_MIN) < 0) {
256
257
  warn(`lecodes-android-sdk ${sdkVersion} predates the variant matrix (${ANDROID_SDK_VARIANTS_MIN}) — linking the full lecodes-sdk AAR; run \`lecodes app update\` to pick up variant builds.`)
257
258
  sdkArtifact = "lecodes-sdk"
258
259
  }
259
- const WHY: Record<SdkVariant, string> = {
260
+ const WHY: Record<string, string> = {
260
261
  full: "3D + 2D engines in use", "3d": "3D in use, no 2D", "2d": "2D in use, no 3D", core: "no 3D/2D engine use detected",
261
262
  }
262
263
  log(`${c.green("variant")} ${variant} → ${sdkArtifact}${requestedVariant === "auto" ? c.dim(` (auto: ${WHY[detected]})`) : c.dim(" (forced by android.variant)")}`)
@@ -376,6 +377,11 @@ export const syncAndroid = async (root: string, config: AppConfig, prior: SyncAr
376
377
  const runtimeKt = join(androidDir, "lecodes-runtime", "src", "main", "kotlin", "io", "letary", "lecodes", "runtime", "LeCodesRuntime.kt")
377
378
  mkdirSync(dirname(runtimeKt), { recursive: true })
378
379
  writeFileSync(runtimeKt, runtimeKotlinTemplate(runtimePlugins, shellUuid))
380
+ // ---- OTA: the update URL rides the runtime module's manifest (merged into the app's by AGP) and
381
+ // LeCodesShell.boot reads it — app.json "update" resolved like the iOS plist key ----
382
+ const updateUrl = resolveUpdateUrl(config.update, root, sdkVersion)
383
+ writeFileSync(join(androidDir, "lecodes-runtime", "src", "main", "AndroidManifest.xml"), runtimeManifestTemplate(updateUrl))
384
+ if (updateUrl) log(`${c.green("update")} OTA source ${updateUrl} — checked natively at launch (LeCodesShell.boot), new code applies next launch`)
379
385
 
380
386
  if (pluginSpecs.length > 0 || existsSync(lockPath)) writeJson(lockPath, { plugins: newLock })
381
387
 
@@ -390,11 +396,6 @@ export const syncAndroid = async (root: string, config: AppConfig, prior: SyncAr
390
396
  restampManifestIcon(androidDir)
391
397
  }
392
398
 
393
- // ---- OTA: not on Android yet ----
394
- if (config.update) {
395
- note(`app.json "update" is iOS-only for now — the Android runtime has no native updater, so it's ignored for this shell.`)
396
- }
397
-
398
399
  log("")
399
400
  success(`Android shell synced: app.js + ${runtimePlugins.length} plugin(s), SDK ${sdkVersion}, v${config.version ?? "1.0.0"} (${config.android?.versionCode ?? 1}).`)
400
401
  note("Build & run from Android Studio (`lecodes app open android`) — user files in android/app/ were not touched.")
@@ -969,8 +970,8 @@ export const doctorAndroidChecks = async (
969
970
  check(hasSdkHome, "Android SDK location", "set ANDROID_HOME or create android/local.properties")
970
971
 
971
972
  const requestedVariant = config.android?.variant ?? "auto"
972
- const validVariant = requestedVariant === "auto" || SDK_VARIANTS.includes(requestedVariant as SdkVariant)
973
- check(validVariant, `android.variant "${requestedVariant}"`, `use "auto" or one of ${SDK_VARIANTS.join(", ")}`)
973
+ const validVariant = requestedVariant === "auto" || ANDROID_SDK_VARIANTS.includes(requestedVariant as AndroidSdkVariant)
974
+ check(validVariant, `android.variant "${requestedVariant}"`, `use "auto" or one of ${ANDROID_SDK_VARIANTS.join(", ")}`)
974
975
 
975
976
  const wrapperJar = join(androidDir, "gradle", "wrapper", "gradle-wrapper.jar")
976
977
  check(existsSync(wrapperJar) && statSync(wrapperJar).size > 0, "Gradle wrapper jar",
@@ -991,6 +992,20 @@ export const doctorAndroidChecks = async (
991
992
  } catch { /* offline — not a preflight failure */ }
992
993
  }
993
994
 
995
+ if (config.update) {
996
+ // The manifest meta-data is necessary but not sufficient: the user-owned AppViewModel must boot
997
+ // through LeCodesShell.boot (the updater's check + the newest local bundle). A shell generated
998
+ // before Android OTA existed reads assets/app.js directly — downloads would land but never run,
999
+ // and sync can't rewrite app/. Doctor is the only place that can catch it.
1000
+ const manifest = join(androidDir, "lecodes-runtime", "src", "main", "AndroidManifest.xml")
1001
+ check(existsSync(manifest) && readFileSync(manifest, "utf8").includes("io.letary.lecodes.UPDATE_URL"), "OTA update URL in the runtime manifest", "run `lecodes app sync`", false)
1002
+ const appSrc = join(androidDir, "app", "src", "main", "java")
1003
+ const kt = existsSync(appSrc) ? readdirSync(appSrc, { recursive: true }).map(String).filter((f) => f.endsWith(".kt")) : []
1004
+ const boots = kt.some((f) => readFileSync(join(appSrc, f), "utf8").includes("LeCodesShell.boot"))
1005
+ check(boots, "App code launches via the updater (LeCodesShell.boot)",
1006
+ "no app/src/main/java/**/*.kt calls LeCodesShell.boot(engine, context) — downloaded updates would never run; see \"Over-the-air updates\" in android/README.md for the AppViewModel snippet", false)
1007
+ }
1008
+
994
1009
  const signing = resolveAndroidSigning(androidDir)
995
1010
  check(!!signing, "Release signing key", "`lecodes app keystore create` or `keystore use <file>` (release builds are refused without one)", false)
996
1011
  if (signing) {
@@ -2,8 +2,7 @@ import { execFileSync } from "node:child_process"
2
2
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"
3
3
  import { homedir } from "node:os"
4
4
  import { basename, dirname, join, resolve } from "node:path"
5
- import { readManifest, type Manifest } from "../../project/manifest"
6
- import { loadConfig, requireApiUrl } from "../../platform/config"
5
+ import { readManifest } from "../../project/manifest"
7
6
  import { compileShell, type BundleResource } from "../compile"
8
7
  import { CliError, UsageError, bool, c, defineCommand, log, note, str, success, warn } from "../../cli"
9
8
  import {
@@ -15,7 +14,7 @@ import {
15
14
  import {
16
15
  FIRST_PARTY_PLUGINS, SDK_VARIANTS, cmpVersion, ensureShellIgnored,
17
16
  fetchLatestSdkVersion, isLocalPluginSpec, latestPluginTag, projectRoot, readJson, resolvePlugin, resolveSdkVersion, sdkSatisfies,
18
- resolveVariant, runTool, slugify, writeJson, VERSION_RE, type AppConfig, type LockEntry,
17
+ resolveVariant, runTool, slugify, writeJson, VERSION_RE, type AppConfig, type LockEntry, resolveUpdateUrl,
19
18
  } from "./shared"
20
19
  import {
21
20
  ANDROID_SDK_MIN_VERSION, ANDROID_SDK_REPO, androidBuild, doctorAndroidChecks, initAndroid,
@@ -195,7 +194,7 @@ const init = defineCommand({
195
194
  "bundle-id": str("iOS bundle identifier (default com.example.<slug>)", { value: "<id>" }),
196
195
  "app-id": str("Android applicationId (default com.example.<slug>)", { value: "<id>" }),
197
196
  team: str("Apple development team id", { value: "<id>" }),
198
- variant: str("SDK variant: auto (default — sync picks the smallest of core/2d/3d/full covering what the bundle uses), or one of those to pin", { value: "<v>" }),
197
+ variant: str("SDK variant: auto (default — sync picks the smallest of core/2d/3d/full covering what the bundle uses), or one of those to pin; Android also vulkan (full on Vulkan, arm64 only)", { value: "<v>" }),
199
198
  sdk: str("native SDK release (default: the newest one, pinned in app.json); 'latest' floats it — every sync re-resolves", { value: "<v|latest>" }),
200
199
  fullscreen: bool("(desktop) borderless fullscreen on the primary monitor — kiosks"),
201
200
  },
@@ -212,27 +211,6 @@ const init = defineCommand({
212
211
 
213
212
  /* --------------------------------------- sync --------------------------------------- */
214
213
 
215
- /** Resolve app.json "update" to the URL the shell's native updater checks (LeCodesUpdater):
216
- * absent or `true` = the project's live bundle on the platform — `/code/<uuid>.js?sdk=<version>`,
217
- * the launcher's own URL, so a pushed project updates its shells with no server of its own (a
218
- * local-only project has none: unconfigured); `false` = no OTA; a string / { url } = custom
219
- * hosting, passed through. Warns on http: ATS blocks it on device. */
220
- const resolveUpdateUrl = (update: AppConfig["update"], root: string, sdkVersion: string): string | undefined => {
221
- if (update === false) return undefined
222
- let url = typeof update === "string" ? update : typeof update === "object" ? update.url : undefined
223
- if (!url) {
224
- let manifest: Manifest | null = null
225
- try { manifest = readManifest(root) } catch { /* not a project dir */ }
226
- if (!manifest) {
227
- if (update) warn(`app.json "update": true needs a server-backed project — use "update": {"url": "https://…/app.js"} for custom hosting. OTA left unconfigured.`)
228
- return undefined
229
- }
230
- url = `${requireApiUrl(loadConfig())}/code/${manifest.uuid}.js?sdk=${sdkVersion}`
231
- }
232
- if (!url.startsWith("https://")) warn(`update URL is not https — iOS blocks it on device (ATS): ${url}`)
233
- return url
234
- }
235
-
236
214
  /** `lecodes app sync [ios|android]` — no platform syncs every shell dir that exists. A
237
215
  * dual-shell project compiles the bundle ONCE (the iOS pass) and the Android pass reuses
238
216
  * the byte-identical output. `app update` and `app build ios` run it too. */
@@ -461,7 +439,7 @@ const sync = defineCommand({
461
439
  name: "sync",
462
440
  summary: "Make every existing shell match the project: bundle + assets + SDK variant + plugins + CLI-owned files",
463
441
  usage: "[ios|android]",
464
- description: "Compiles the bundle and embeds the assets it references (ios/App/Resources/ resp. the android assets/; offline by default, also for server projects — app.json \"resources\": {\"remote\": [globs]} keeps heavy assets streaming), picks the SDK variant from the bundle's engine use, vendors the app.json plugins (pinned in plugins.lock.json) and regenerates the CLI-owned shell files. User files (ios/App/*.swift, android/app/) are never touched.\n\nNative OTA JS updates at launch (iOS): on by default for a pushed project (its bundle on the platform, the launcher's own URL), app.json \"update\": {\"url\": …} for custom hosting, \"update\": false turns it off. app.json \"icon\" (a square ≥1024px image) renders the app icons every sync (\"iconBackground\": the adaptive / flatten backdrop). No platform = every shell that exists; a dual-shell project compiles once.",
442
+ description: "Compiles the bundle and embeds the assets it references (ios/App/Resources/ resp. the android assets/; offline by default, also for server projects — app.json \"resources\": {\"remote\": [globs]} keeps heavy assets streaming), picks the SDK variant from the bundle's engine use, vendors the app.json plugins (pinned in plugins.lock.json) and regenerates the CLI-owned shell files. User files (ios/App/*.swift, android/app/) are never touched.\n\nNative OTA JS updates at launch (both shells; the current launch runs the newest LOCAL bundle, a download applies on the next): on by default for a pushed project (its bundle on the platform, the launcher's own URL), app.json \"update\": {\"url\": …} for custom hosting, \"update\": false turns it off. A download runs only when it is of the shell's SDK major and not older than the bundle running. app.json \"icon\" (a square ≥1024px image) renders the app icons every sync (\"iconBackground\": the adaptive / flatten backdrop). No platform = every shell that exists; a dual-shell project compiles once.",
465
443
  examples: ["lecodes app sync", "lecodes app sync android"],
466
444
  run: ({ args }) => runSync(args[0]),
467
445
  })
@@ -746,14 +724,25 @@ const doctorIosChecks = async (root: string, config: AppConfig, check: Check) =>
746
724
  const plist = readFileSync(join(root, "ios", "App", "Info.plist"), "utf8")
747
725
  check(plist.includes("<key>LeCodesUpdateURL</key>"), "OTA update URL in Info.plist", "run `lecodes app sync`", false)
748
726
  // The plist key is necessary but not sufficient: the user-owned ViewController must route
749
- // the launch through LeCodesUpdater. Shells generated before OTA existed read Bundle.main
750
- // directly — downloads land but never run, and sync can't rewrite App/*.swift. Doctor is
751
- // the only place that can catch it.
727
+ // the launch through the updater — a 2.x shell by subclassing LeCodesAppViewController (its
728
+ // boot takes the newest local bundle), a 1.x one by calling LeCodesUpdater.activeBundle()
729
+ // itself. Shells generated before OTA existed read Bundle.main directly — downloads land but
730
+ // never run, and sync can't rewrite App/*.swift. Doctor is the only place that can catch it.
752
731
  const appDir = join(root, "ios", "App")
753
- const callsUpdater = readdirSync(appDir).some(f =>
754
- f.endsWith(".swift") && readFileSync(join(appDir, f), "utf8").includes("LeCodesUpdater.activeBundle"))
755
- check(callsUpdater, "App code launches via LeCodesUpdater",
756
- `no App/*.swift calls LeCodesUpdater.activeBundle() — downloaded updates would never run; see "Over-the-air updates" in ios/README.md for the ViewController snippet`, false)
732
+ const swift = readdirSync(appDir).filter(f => f.endsWith(".swift")).map(f => readFileSync(join(appDir, f), "utf8"))
733
+ const launchesViaUpdater = swift.some(t => t.includes("LeCodesAppViewController") || t.includes("LeCodesUpdater.activeBundle"))
734
+ check(launchesViaUpdater, "App code launches via the updater",
735
+ `no App/*.swift subclasses LeCodesAppViewController or calls LeCodesUpdater.activeBundle() — downloaded updates would never run; see "Over-the-air updates" in ios/README.md`, false)
736
+ }
737
+ // A 1.x shell moved to a 2.x SDK by \`lecodes app update\`: its CLI-owned files are regenerated, but
738
+ // App/*.swift are the user's and keep the 1.x shape — a ViewController over LeCodesViewController
739
+ // whose init needs an engine, so SceneDelegate's \`ViewController()\` no longer compiles. The 2.x
740
+ // shape is LeCodesAppViewController (LeCodesRuntime); the three files are the init templates.
741
+ {
742
+ const vc = join(root, "ios", "App", "ViewController.swift")
743
+ const is2x = !existsSync(vc) || readFileSync(vc, "utf8").includes("LeCodesAppViewController")
744
+ check(is2x, "App/ViewController.swift is the 2.x shell (LeCodesAppViewController)",
745
+ "a 1.x shell's App/*.swift: replace AppDelegate.swift, SceneDelegate.swift and ViewController.swift with the ones `lecodes app init ios` writes today (your own code goes into ViewController.configure(_:))", false)
757
746
  }
758
747
 
759
748
  const asc = readJson<{ keyPath?: string }>(join(homedir(), ".lecodes", "asc.json"))
@@ -2,7 +2,8 @@ import { execFileSync } from "node:child_process"
2
2
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
3
3
  import { homedir } from "node:os"
4
4
  import { dirname, join, resolve } from "node:path"
5
- import { findProjectRoot } from "../../project/manifest"
5
+ import { findProjectRoot, readManifest, type Manifest } from "../../project/manifest"
6
+ import { loadConfig, requireApiUrl } from "../../platform/config"
6
7
  import { DEFAULT_IGNORE_FILE } from "../../project/ignore"
7
8
  import { CliError, c, log, note, warn } from "../../cli"
8
9
  import { pascal, readManifest as readPluginManifest, type IosPackage, type PluginManifest as PluginManifest2 } from "../../plugin/manifest"
@@ -49,7 +50,9 @@ export interface AppConfig {
49
50
  * (natively, before the JS engine — offline is a no-op, new code applies next launch).
50
51
  * `true` = this project's live bundle on the platform (server-backed only); or an https
51
52
  * URL to a compiled bundle on any hosting (string or `{ "url": … }`). Absent = no OTA.
52
- * iOS-only for now — the Android runtime has no native updater yet. */
53
+ * Both shells: iOS reads it from Info.plist (LeCodesUpdater), Android from the manifest
54
+ * meta-data the CLI-owned lecodes-runtime module carries (LeCodesShell.boot). A download runs
55
+ * only when it is of the shell's SDK major and not older than the bundle running. */
53
56
  update?: boolean | string | { url?: string }
54
57
  /** `variant`: SDK product — "auto" (default: sync picks the smallest one covering the engines
55
58
  * the compiled bundle actually uses) or core|2d|3d|full to force one.
@@ -61,7 +64,8 @@ export interface AppConfig {
61
64
  * shared `version`). `sdk`: an exact lecodes-android-sdk release or "latest", like ios.sdk.
62
65
  * `variant`: SDK artifact — "auto" (default: sync picks the smallest one covering the engines
63
66
  * the compiled bundle actually uses) or core|2d|3d|full to force one, like ios.variant
64
- * (needs lecodes-android-sdk >= 1.3.0 — older releases ship only the full AAR).
67
+ * (needs lecodes-android-sdk >= 1.3.0 — older releases ship only the full AAR); "vulkan" = full
68
+ * on Filament's Vulkan backend, arm64 only, never auto-picked (lecodes-android-sdk >= 2.1.0).
65
69
  * `signing`: keystore METADATA only (written by `lecodes app keystore create|use`) — the
66
70
  * path + passwords live in the project .env as LECODES_ANDROID_* (or android/
67
71
  * keystore.properties for Studio-only builds), never in app.json.
@@ -248,6 +252,28 @@ export interface LockEntry { repo: string, tag: string, commit: string, version:
248
252
  export const isLocalPluginSpec = (spec: string): boolean =>
249
253
  spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("/") || spec.startsWith("file:")
250
254
 
255
+ /** Resolve app.json "update" to the URL a shell's native updater checks (LeCodesUpdater on both
256
+ * platforms): absent or `true` = the project's live bundle on the platform —
257
+ * `/code/<uuid>.js?sdk=<version>`, the launcher's own URL, so a pushed project updates its shells
258
+ * with no server of its own (a local-only project has none: unconfigured); `false` = no OTA; a
259
+ * string / { url } = custom hosting, passed through (one file: the updater refuses a bundle of
260
+ * another SDK major). Warns on http: iOS blocks it on device (ATS), Android's cleartext policy too. */
261
+ export const resolveUpdateUrl = (update: AppConfig["update"], root: string, sdkVersion: string): string | undefined => {
262
+ if (update === false) return undefined
263
+ let url = typeof update === "string" ? update : typeof update === "object" ? update.url : undefined
264
+ if (!url) {
265
+ let manifest: Manifest | null = null
266
+ try { manifest = readManifest(root) } catch { /* not a project dir */ }
267
+ if (!manifest) {
268
+ if (update) warn(`app.json "update": true needs a server-backed project — use "update": {"url": "https://…/app.js"} for custom hosting. OTA left unconfigured.`)
269
+ return undefined
270
+ }
271
+ url = `${requireApiUrl(loadConfig())}/code/${manifest.uuid}.js?sdk=${sdkVersion}`
272
+ }
273
+ if (!url.startsWith("https://")) warn(`update URL is not https — the phones block it on device (ATS / cleartext policy): ${url}`)
274
+ return url
275
+ }
276
+
251
277
  /** Does the SDK `version` a shell links satisfy a plugin manifest's `sdk` range (`^2.0.0`: the
252
278
  * same major, not older; `~2.1.0`: the same major.minor, not older; `2.0.0`: exactly)? */
253
279
  export const sdkSatisfies = (version: string, range: string): boolean => {
@@ -350,6 +376,33 @@ export const resolveVariant = (requested: string, bundleJs: string, configKey =
350
376
  return { variant, detected }
351
377
  }
352
378
 
379
+ /* ------------------------------ Android: the vulkan variant ------------------------------ */
380
+
381
+ // Android alone has a fifth artifact: lecodes-sdk-vulkan = full's engines with Filament's Vulkan backend
382
+ // as the default (OpenGL ES the fallback of the engine's device probe), arm64-v8a only. Auto-selection
383
+ // never picks it — a 3D game's app.json asks for it (android.variant: "vulkan"); iOS has nothing of the
384
+ // kind (Metal). The viewer and the GL AARs carry no Vulkan code (2026-10-10).
385
+ export const ANDROID_SDK_VARIANTS = [...SDK_VARIANTS, "vulkan"] as const
386
+ export type AndroidSdkVariant = (typeof ANDROID_SDK_VARIANTS)[number]
387
+
388
+ /** First lecodes-android-sdk release whose GitHub release carries lecodes-sdk-vulkan. */
389
+ export const ANDROID_SDK_VULKAN_MIN = "2.1.0"
390
+
391
+ /** Release-asset / ivy artifact name of an Android variant — full keeps the historic plain name so
392
+ * pre-variant pins and shells keep resolving. vulkan refuses a release that has no such asset: Gradle
393
+ * would fail much later, on a 404 from the ivy url, with no word about why. */
394
+ export const androidSdkArtifact = (variant: AndroidSdkVariant, sdkVersion?: string): string => {
395
+ if (variant === "vulkan" && sdkVersion && cmpVersion(sdkVersion, ANDROID_SDK_VULKAN_MIN) < 0) {
396
+ throw new CliError(`android.variant "vulkan" needs lecodes-android-sdk >= ${ANDROID_SDK_VULKAN_MIN} (pinned: ${sdkVersion}) — run \`lecodes app update\` or set android.sdk.`)
397
+ }
398
+ return variant === "full" ? "lecodes-sdk" : `lecodes-sdk-${variant}`
399
+ }
400
+
401
+ /** The engine set a variant links, for the coverage check of resolveVariant: vulkan is full's set on
402
+ * another backend. */
403
+ export const androidVariantEngines = (variant: AndroidSdkVariant): SdkVariantShared =>
404
+ variant === "vulkan" ? "full" : variant
405
+
353
406
  /* ----------------------------------- SDK version ------------------------------------ */
354
407
 
355
408
  export const VERSION_RE = /^\d+\.\d+\.\d+$/