lecodes-cli 2.2.9 → 2.2.10
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/dist/index.js +7470 -14528
- package/package.json +8 -8
- package/runtime/materials/background.mat +52 -0
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +3 -3
- package/runtime/web/assets/ar-CHvaraG4.js +1 -0
- package/runtime/web/assets/audio-recorder-Bt-Caym7.js +1 -0
- package/runtime/web/assets/materials-BzTsWTWO.bin +0 -0
- package/runtime/web/assets/speech-DzDPVK_j.js +1 -0
- package/runtime/web/player.js +17 -17
- package/runtime/web-lite/assets/{index-BttBnvIf.js → index-By07xWGA.js} +1 -1
- package/runtime/web-lite/assets/{scene3d-D8LdhX0V.js → scene3d-D5R7Vq3f.js} +58 -58
- package/runtime/web-lite/features.json +2 -11
- package/runtime/web-lite/lite.js +7 -7
- package/runtime/web-lite/plugins/ar/index-CHvaraG4.js +1 -0
- package/runtime/web-lite/plugins/audio-recorder/index-Bt-Caym7.js +1 -0
- package/runtime/web-lite/plugins/speech/index-DzDPVK_j.js +1 -0
- package/runtime/web-lite/plugins.json +3 -0
- package/src/commands/app/templates/ios.ts +14 -15
- package/src/commands/init/index.ts +195 -195
- package/src/commands/render.ts +15 -0
- package/src/commands/shared.ts +79 -79
- package/src/commands/test.ts +2 -1
- package/src/commands/thumbs.ts +185 -185
- package/src/compile/screenEntry.ts +123 -123
- package/src/declarations/lecodes-headless.d.ts +2 -0
- package/src/dev/backendWorker.ts +3 -3
- package/src/plugin/contract.ts +29 -12
- package/src/plugin/emitKotlin.ts +33 -8
- package/src/plugin/emitSdk.ts +23 -9
- package/src/plugin/emitSwift.ts +33 -8
- package/src/plugin/emitWeb.ts +57 -1
- package/src/plugin/generate.ts +22 -1
- package/src/plugin/lower.ts +42 -6
- package/src/plugin/permissions.ts +52 -0
- package/src/project/ignore.ts +164 -164
- package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
- package/runtime/web-lite/assets/ar-BOdQ-wV4.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var nt=Object.defineProperty;var ot=(e,t,n)=>t in e?nt(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var P=(e,t,n)=>ot(e,typeof t!="symbol"?t+"":t,n);const it=(e,t)=>{const n=t.data instanceof Uint8Array?t.data.slice().buffer:t.data;return{systemId:e.putBuffer(n),name:t.name,size:n.byteLength}},l={present:e=>e!=null,f64:e=>typeof e=="number"&&Number.isFinite(e)?e:void 0,i32:e=>typeof e=="number"&&Number.isInteger(e)&&e>=-2147483648&&e<=2147483647?e:void 0,bool:e=>typeof e=="boolean"?e:void 0,string:e=>typeof e=="string"?e:void 0,json:e=>e,bytes:e=>Object.prototype.toString.call(e)==="[object Uint8Array]"?e.slice():void 0,object:e=>typeof e=="object"&&e!==null&&!Array.isArray(e)?e:void 0,array:(e,t)=>Array.isArray(e)&&(t===void 0||e.length===t)?e:void 0,list:(e,t)=>{if(!Array.isArray(e))return;const n=[];for(const o of e){const i=t(o);if(i===void 0)return;n.push(i)}return n},map:(e,t)=>{if(typeof e!="object"||e===null||Array.isArray(e))return;const n={};for(const[o,i]of Object.entries(e)){const r=t(i);if(r===void 0)return;n[o]=r}return n}};let $;const rt=e=>{const t=l.object(e),n=t?l.i32(t.systemId):void 0,o=n===void 0||$==null?void 0:$(n);return t&&o?{data:o,name:l.string(t.name)??""}:void 0},S=e=>typeof e=="number"&&Number.isInteger(e)&&e>=0&&e<=4294967295?e:void 0,st=e=>{const t=l.object(e);if(!t)return;let n;if(l.present(t.scene)){const r=S(t.scene);if(r===void 0)return;n=r}let o;if(l.present(t.camera)){const r=S(t.camera);if(r===void 0)return;o=r}const i={};return n!==void 0&&(i.scene=n),o!==void 0&&(i.camera=o),i},at=e=>e==="world"||e==="images"?e:void 0,ct=e=>e==="horizontal"||e==="vertical"||e==="both"||e==="none"?e:void 0,Y=e=>{const t=l.object(e);if(!t)return;const n=rt(t.image);if(n===void 0)return;const o=l.f64(t.physicalWidth);if(o===void 0)return;const i=S(t.entity);return i===void 0?void 0:{image:n,physicalWidth:o,entity:i}},dt=e=>e==="gravity"||e==="gravityAndHeading"?e:void 0,ut=e=>{const t=l.object(e);if(!t)return;let n;if(l.present(t.mode)){const s=at(t.mode);if(s===void 0)return;n=s}let o;if(l.present(t.planes)){const s=ct(t.planes);if(s===void 0)return;o=s}let i;if(l.present(t.images)){const s=l.list(t.images,u=>Y(u));if(s===void 0)return;i=s}let r;if(l.present(t.worldAlignment)){const s=dt(t.worldAlignment);if(s===void 0)return;r=s}let h;if(l.present(t.lightEstimation)){const s=l.bool(t.lightEstimation);if(s===void 0)return;h=s}let c;if(l.present(t.autoFocus)){const s=l.bool(t.autoFocus);if(s===void 0)return;c=s}const d={};return n!==void 0&&(d.mode=n),o!==void 0&&(d.planes=o),i!==void 0&&(d.images=i),r!==void 0&&(d.worldAlignment=r),h!==void 0&&(d.lightEstimation=h),c!==void 0&&(d.autoFocus=c),d},lt=e=>e==="planes"||e==="estimated"||e==="any"?e:void 0,ft=e=>{const t=l.object(e);if(!t)return;const n=l.f64(t.x);if(n===void 0)return;const o=l.f64(t.y);if(o===void 0)return;const i=l.f64(t.z);return i===void 0?void 0:{x:n,y:o,z:i}},ht=e=>{const t=l.object(e);if(!t)return;const n=l.f64(t.x);if(n===void 0)return;const o=l.f64(t.y);if(o===void 0)return;const i=l.f64(t.z);if(i===void 0)return;const r=l.f64(t.w);return r===void 0?void 0:{x:n,y:o,z:i,w:r}},mt=e=>{const t=l.object(e);if(!t)return;const n=ft(t.position);if(n===void 0)return;const o=ht(t.rotation);return o===void 0?void 0:{position:n,rotation:o}};class A extends Error{constructor(t){super(t),this.code=t}}const q={name:"ar",version:1},vt=(e,t)=>{$=n=>e.getBuffer(n),e.registerService(q.name,(n,o)=>{const i={tracking:c=>o.emit("tracking",c),plane:c=>o.emit("plane",c),image:c=>o.emit("image",c),anchor:c=>o.emit("anchor",c),light:c=>o.emit("light",c),error:c=>o.emit("error",c)},r=t(st(n)??{},i);return{call:async(c,d)=>{switch(c){case"start":{let s;if(l.present(d[0])){const u=ut(d[0]);if(u===void 0)throw new Error("bad arguments — AR.start: options");s=u}await r.start(s);return}case"stop":{await r.stop();return}case"setImages":{const s=l.list(d[0],u=>Y(u));if(s===void 0)throw new Error("bad arguments — AR.setImages: images");await r.setImages(s);return}case"hitTest":{const s=l.f64(d[0]);if(s===void 0)throw new Error("bad arguments — AR.hitTest: x");const u=l.f64(d[1]);if(u===void 0)throw new Error("bad arguments — AR.hitTest: y");let p;if(l.present(d[2])){const E=lt(d[2]);if(E===void 0)throw new Error("bad arguments — AR.hitTest: target");p=E}return await r.hitTest(s,u,p)}case"anchor":{const s=S(d[0]);if(s===void 0)throw new Error("bad arguments — AR.anchor: entity");const u=mt(d[1]);if(u===void 0)throw new Error("bad arguments — AR.anchor: pose");await r.anchor(s,u);return}case"release":{const s=S(d[0]);if(s===void 0)throw new Error("bad arguments — AR.release: entity");await r.release(s);return}case"captureFrame":{const s=await r.captureFrame();return it(e,s)}default:throw new Error(`Unknown method: ${c}`)}},close:()=>{var c;return(c=r.close)==null?void 0:c.call(r)}}},q.version)},F=Math.PI/180,D=(e,t)=>[e[3]*t[0]+e[0]*t[3]+e[1]*t[2]-e[2]*t[1],e[3]*t[1]-e[0]*t[2]+e[1]*t[3]+e[2]*t[0],e[3]*t[2]+e[0]*t[1]-e[1]*t[0]+e[2]*t[3],e[3]*t[3]-e[0]*t[0]-e[1]*t[1]-e[2]*t[2]],U=e=>{const t=Math.hypot(e[0],e[1],e[2],e[3])||1;return[e[0]/t,e[1]/t,e[2]/t,e[3]/t]},O=(e,t)=>{const n=Math.sin(t/2);return[e[0]*n,e[1]*n,e[2]*n,Math.cos(t/2)]},G=(e,t)=>{const[n,o,i,r]=e,h=2*(o*t[2]-i*t[1]),c=2*(i*t[0]-n*t[2]),d=2*(n*t[1]-o*t[0]);return[t[0]+r*h+(o*d-i*c),t[1]+r*c+(i*h-n*d),t[2]+r*d+(n*c-o*h)]},Q=(e,t,n)=>D(D(O([0,0,1],e*F),O([1,0,0],t*F)),O([0,1,0],n*F)),tt=e=>{const[t,n,o,i]=e;return[2*(t*o-i*n),2*(i*t+n*o),i*i-t*t-n*n+o*o]},wt=(e,t)=>{const n=e[0]*t[0]+e[1]*t[1]+e[2]*t[2];if(n<-.999999)return[1,0,0,0];const o=[e[1]*t[2]-e[2]*t[1],e[2]*t[0]-e[0]*t[2],e[0]*t[1]-e[1]*t[0]];return U([o[0],o[1],o[2],1+n])};class Z{constructor(t=1,n=.05){P(this,"q",[0,0,0,1]);P(this,"seeded",!1);P(this,"ix",0);P(this,"iy",0);P(this,"iz",0);this.kp=t,this.ki=n}get ready(){return this.seeded}seed(t){this.q=U(t),this.seeded=!0}update(t,n,o){const i=Math.hypot(n[0],n[1],n[2]);if(!this.seeded){if(i===0)return;this.seed(wt([n[0]/i,n[1]/i,n[2]/i],[0,0,1]));return}let r=t[0],h=t[1],c=t[2];if(i>0){const z=n[0]/i,j=n[1]/i,I=n[2]/i,b=tt(this.q),M=j*b[2]-I*b[1],T=I*b[0]-z*b[2],k=z*b[1]-j*b[0];this.ki>0&&(this.ix+=this.ki*M*o,this.iy+=this.ki*T*o,this.iz+=this.ki*k*o,r+=this.ix,h+=this.iy,c+=this.iz),r+=this.kp*M,h+=this.kp*T,c+=this.kp*k}const d=.5*o*r,s=.5*o*h,u=.5*o*c,[p,E,x,R]=this.q;this.q=U([p+R*d+E*u-x*s,E+R*s-p*u+x*d,x+R*u+p*s-E*d,R-p*d-E*s-x*u])}}const gt=O([1,0,0],-Math.PI/2),yt=(e,t)=>D(D(gt,e),O([0,0,1],-t*F)),pt=e=>{let[t,,n]=G(e,[0,0,-1]);return Math.hypot(t,n)<.3&&([t,,n]=G(e,[0,1,0])),O([0,1,0],-Math.atan2(-t,-n))},At=e=>{const[t,n,o,i]=e;return[1-2*(n*n+o*o),2*(t*n+i*o),2*(t*o-i*n),0,2*(t*n-i*o),1-2*(t*t+o*o),2*(n*o+i*t),0,2*(t*o+i*n),2*(n*o-i*t),1-2*(t*t+n*n),0,0,0,0,1]},Et=(e,t,n,o,i)=>{const r=Math.max(e,t)/2/Math.tan(i*F/2),h=Math.max(n/e,o/t);return 2*Math.atan(o/h/2/r)/F},bt=(e,t,n,o)=>{const i=1/Math.tan(e*F/2);return[i/t,0,0,0,0,i,0,0,0,0,(o+n)/(n-o),-1,0,0,2*o*n/(n-o),0]},xt=(e,t,n,o)=>{const i=Math.max(n/e,o/t),r=n/(e*i),h=o/(t*i);return[r,0,(1-r)/2,0,h,(1-h)/2]},Mt=68,J=.01,K=1e3,N=Math.PI/180,X=30,kt=e=>{var n;if(typeof navigator>"u"||!((n=navigator.mediaDevices)!=null&&n.getUserMedia))return;const t=e.gl;vt(e,(o,i)=>{const r=o.scene;let h=0,c=!1,d=!1,s=null,u=null,p=null,E="",x=new Z,R=!1,z=null,j=null,I=0,b=0,M=0,T=null,k=null;const W=a=>{a.alpha===null||a.beta===null||a.gamma===null||a.alpha===0&&a.beta===0&&a.gamma===0||(j=[a.beta,a.gamma],z=Q(a.alpha,a.beta,a.gamma))},B=a=>{const m=a.rotationRate,f=a.accelerationIncludingGravity;if(!m||!f||m.alpha===null||m.beta===null||m.gamma===null||f.x===null||f.y===null||f.z===null)return;const g=a.timeStamp;if(b||(b=g),M===0)if(j){const y=tt(Q(0,j[0],j[1])),v=Math.hypot(f.x,f.y,f.z)||1;M=(y[0]*f.x+y[1]*f.y+y[2]*f.z)/v<0?-1:1}else if(g-b>500)M=1;else return;const w=I?Math.min(.1,Math.max(0,(g-I)/1e3)):0;I=g,R||(R=!0,z&&x.seed(z)),x.update([m.beta*N,m.gamma*N,m.alpha*N],[f.x*M,f.y*M,f.z*M],w)},C=()=>{if(!u||r===void 0)return;const a=t.viewport(r),{width:m,height:f}=a,g=`${u.videoWidth}x${u.videoHeight}:${m}x${f}`;if(g!==E&&u.videoWidth>0&&m>0&&f>0){E=g;const v=Et(u.videoWidth,u.videoHeight,m,f,Mt);t.setProjection(r,bt(v,m/f,J,K),J,K),t.setBackground(r,u,xt(u.videoWidth,u.videoHeight,m,f))}const w=R?x.ready?x.q:null:z;if(!w)return;const y=yt(w,a.rotation);T??(T=pt(y)),t.setCameraMatrix(r,At(D(T,y)))},H=async()=>{const a=[window.DeviceMotionEvent,window.DeviceOrientationEvent];if(!a[1])return!1;let m=!0;for(const f of a)if(typeof(f==null?void 0:f.requestPermission)=="function")try{m=await f.requestPermission()==="granted"&&m}catch{m=!1}return m},V=()=>{k&&window.removeEventListener("pointerup",k,!0),k=null},et=()=>{k||(k=()=>{V(),H().then(a=>{a||console.warn("[ar] the page was not given the device's sensors: the camera of the scene stays as the app turned it")})},window.addEventListener("pointerup",k,!0))},_=()=>{h++,c=!1,d=!1,p==null||p(),p=null;for(const a of(s==null?void 0:s.getTracks())??[])a.stop();s=null,u&&(u.srcObject=null,r!==void 0&&(t.setBackground(r,null),t.clearProjection(r)),u.remove()),u=null,E="",V(),window.removeEventListener("deviceorientation",W),window.removeEventListener("devicemotion",B),x=new Z,R=!1,z=null,j=null,I=0,b=0,M=0,T=null};return{async start(a){var y;if(c||d)throw new A("busy");if(r===void 0||o.camera===void 0)throw console.warn("[ar] start: the session has no scene or camera to drive (ARScene opens it with both)"),new A("failed");((a==null?void 0:a.mode)==="images"||(y=a==null?void 0:a.images)!=null&&y.length)&&console.warn("[ar] the web finds no images in the camera's picture: their anchors are never tracked"),d=!0;const m=++h,f=H();let g;try{g=await navigator.mediaDevices.getUserMedia({audio:!1,video:{facingMode:{ideal:"environment"},width:{ideal:1280},height:{ideal:720}}})}catch(v){m===h&&(d=!1);const L=v instanceof DOMException?v.name:"";throw console.warn(`[ar] no camera: ${v instanceof Error?v.message:v}`),new A(L==="NotAllowedError"||L==="SecurityError"?"denied":L==="NotFoundError"||L==="OverconstrainedError"?"unavailable":"failed")}if(m!==h){for(const v of g.getTracks())v.stop();throw new A("failed")}s=g;const w=document.createElement("video");if(w.muted=!0,w.playsInline=!0,w.autoplay=!0,w.srcObject=g,u=w,await new Promise(v=>{if(w.readyState>=2)return v();w.addEventListener("loadeddata",()=>v(),{once:!0})}),await w.play().catch(()=>{}),m!==h)throw new A("failed");window.addEventListener("deviceorientation",W),window.addEventListener("devicemotion",B),p=t.onFrame(C),C(),d=!1,c=!0,i.tracking({state:"normal"}),f.then(v=>{!v&&m===h&&et()})},stop(){_()},setImages(a){if(!c)throw new A("notRunning");a.length&&console.warn("[ar] the web finds no images in the camera's picture: their anchors are never tracked")},hitTest(){if(!c)throw new A("notRunning");return[]},anchor(){if(!c)throw new A("notRunning")},release(){},async captureFrame(){const a=u;if(!c||!a)throw new A("notRunning");const m=h;for(let y=0;a.readyState<2||a.videoWidth===0;y++){if(y>=X)throw console.warn(`[ar] captureFrame: the camera gave no picture in ${X} frames`),new A("failed");if(await new Promise(v=>requestAnimationFrame(v)),m!==h)throw new A("notRunning")}const f=document.createElement("canvas");f.width=a.videoWidth,f.height=a.videoHeight;const g=f.getContext("2d");if(!g)throw new A("failed");g.drawImage(a,0,0);const w=await new Promise(y=>f.toBlob(y,"image/jpeg",.9));if(!w)throw new A("failed");return{data:await w.arrayBuffer(),name:"frame.jpg"}},close(){_()}}})};export{kt as register};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const j=(e,r)=>{const i=r.data instanceof Uint8Array?r.data.slice().buffer:r.data;return{systemId:e.putBuffer(i),name:r.name,size:i.byteLength}},m={present:e=>e!=null,f64:e=>typeof e=="number"&&Number.isFinite(e)?e:void 0,i32:e=>typeof e=="number"&&Number.isInteger(e)&&e>=-2147483648&&e<=2147483647?e:void 0,bool:e=>typeof e=="boolean"?e:void 0,string:e=>typeof e=="string"?e:void 0,json:e=>e,bytes:e=>Object.prototype.toString.call(e)==="[object Uint8Array]"?e.slice():void 0,object:e=>typeof e=="object"&&e!==null&&!Array.isArray(e)?e:void 0,array:(e,r)=>Array.isArray(e)&&(r===void 0||e.length===r)?e:void 0,list:(e,r)=>{if(!Array.isArray(e))return;const i=[];for(const d of e){const o=r(d);if(o===void 0)return;i.push(o)}return i},map:(e,r)=>{if(typeof e!="object"||e===null||Array.isArray(e))return;const i={};for(const[d,o]of Object.entries(e)){const t=r(o);if(t===void 0)return;i[d]=t}return i}},k=e=>!m.present(e)||m.object(e)?{}:void 0,U=e=>{const r=m.object(e);if(!r)return;let i;if(m.present(r.bitrate)){const c=m.f64(r.bitrate);if(c===void 0)return;i=c}let d;if(m.present(r.sampleRate)){const c=m.f64(r.sampleRate);if(c===void 0)return;d=c}let o;if(m.present(r.maxDuration)){const c=m.f64(r.maxDuration);if(c===void 0)return;o=c}const t={};return i!==void 0&&(t.bitrate=i),d!==void 0&&(t.sampleRate=d),o!==void 0&&(t.maxDuration=o),t},z=(e,r)=>({...r,file:j(e,r.file)});class w extends Error{constructor(r){super(r),this.code=r}}const x={name:"audioRecorder",version:1},B=(e,r)=>{e.registerService(x.name,(i,d)=>{const o={level:n=>d.emit("level",n),ended:n=>d.emit("ended",n)},t=r(k(i)??{},o);return{call:async(n,l)=>{switch(n){case"start":{let f;if(m.present(l[0])){const p=U(l[0]);if(p===void 0)throw new Error("bad arguments — AudioRecorder.start: options");f=p}await t.start(f);return}case"pause":{await t.pause();return}case"resume":{await t.resume();return}case"stop":{const f=await t.stop();return z(e,f)}case"cancel":{await t.cancel();return}default:throw new Error(`Unknown method: ${n}`)}},close:()=>{var n;return(n=t.close)==null?void 0:n.call(t)}}},x.version)},I=["audio/mp4","audio/webm;codecs=opus","audio/webm","audio/ogg;codecs=opus"],O=e=>e.startsWith("audio/mp4")?"m4a":e.startsWith("audio/ogg")?"ogg":"webm",P=e=>{var r;typeof MediaRecorder>"u"||typeof navigator>"u"||!((r=navigator.mediaDevices)!=null&&r.getUserMedia)||B(e,(i,d)=>{let o=null,t=null,c=[],n="",l=null,f=null,p=null,R=0,y=0,v=!1,b=null;const D=()=>{y&&(R+=performance.now()-y,y=0)},S=()=>{if(!(!o||f))try{l??(l=new AudioContext);const a=l.createAnalyser();a.fftSize=256,l.createMediaStreamSource(o).connect(a);const u=new Uint8Array(a.fftSize);f=setInterval(()=>{if(!y)return;a.getByteTimeDomainData(u);let s=0;for(const h of u){const M=(h-128)/128;s+=M*M}d.level({value:Math.min(1,Math.sqrt(s/u.length)*2)})},100)}catch{}},T=()=>{f&&clearInterval(f),f=null},g=()=>{if(b)return b;const a=t;return D(),T(),p&&clearTimeout(p),p=null,b=new Promise(u=>{if(!a||a.state==="inactive")return u();a.onstop=()=>u(),a.stop()}).then(()=>{o==null||o.getTracks().forEach(u=>u.stop()),o=null,l==null||l.close().catch(()=>{}),l=null}),b},A=()=>{t=null,c=[],v=!1,b=null},E=a=>{v||!t||(v=!0,g().then(()=>d.ended({reason:a})))};return{async start(a){var u;if(t)throw new w("busy");try{o=await navigator.mediaDevices.getUserMedia({audio:a!=null&&a.sampleRate?{sampleRate:a.sampleRate}:!0})}catch(s){const h=s instanceof Error?s.name:"";throw new w(h==="NotAllowedError"||h==="SecurityError"?"denied":"failed")}n=I.find(s=>MediaRecorder.isTypeSupported(s))??"";try{t=new MediaRecorder(o,{...n?{mimeType:n}:{},audioBitsPerSecond:((a==null?void 0:a.bitrate)??64)*1e3})}catch{throw o.getTracks().forEach(s=>s.stop()),o=null,new w("failed")}n=t.mimeType||n||"audio/webm",c=[],R=0,v=!1,b=null,t.ondataavailable=s=>{s.data.size&&c.push(s.data)},t.onerror=()=>E("failed"),(u=o.getAudioTracks()[0])==null||u.addEventListener("ended",()=>E("interrupted")),t.start(250),y=performance.now(),S(),a!=null&&a.maxDuration&&a.maxDuration>0&&(p=setTimeout(()=>E("maxDuration"),a.maxDuration))},async pause(){if(!t||v)throw new w("notRecording");t.state==="recording"&&(t.pause(),D())},async resume(){if(!t||v)throw new w("notRecording");t.state==="paused"&&(t.resume(),y=performance.now())},async stop(){if(!t)throw new w("notRecording");await g();const a=new Blob(c,{type:n}),u=R/1e3,s=n.split(";")[0];if(A(),a.size===0)throw new w("failed");return{file:{data:await a.arrayBuffer(),name:`recording.${O(s)}`},mimeType:s,duration:u}},async cancel(){t&&await g(),A()},close(){g(),A()}}})};export{P as register};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const l={present:e=>e!=null,f64:e=>typeof e=="number"&&Number.isFinite(e)?e:void 0,i32:e=>typeof e=="number"&&Number.isInteger(e)&&e>=-2147483648&&e<=2147483647?e:void 0,bool:e=>typeof e=="boolean"?e:void 0,string:e=>typeof e=="string"?e:void 0,json:e=>e,bytes:e=>Object.prototype.toString.call(e)==="[object Uint8Array]"?e.slice():void 0,object:e=>typeof e=="object"&&e!==null&&!Array.isArray(e)?e:void 0,array:(e,n)=>Array.isArray(e)&&(n===void 0||e.length===n)?e:void 0,list:(e,n)=>{if(!Array.isArray(e))return;const s=[];for(const t of e){const a=n(t);if(a===void 0)return;s.push(a)}return s},map:(e,n)=>{if(typeof e!="object"||e===null||Array.isArray(e))return;const s={};for(const[t,a]of Object.entries(e)){const i=n(a);if(i===void 0)return;s[t]=i}return s}},m=e=>!l.present(e)||l.object(e)?{}:void 0,y=e=>{const n=l.object(e);if(!n)return;let s;if(l.present(n.language)){const r=l.string(n.language);if(r===void 0)return;s=r}let t;if(l.present(n.continuous)){const r=l.bool(n.continuous);if(r===void 0)return;t=r}let a;if(l.present(n.interimResults)){const r=l.bool(n.interimResults);if(r===void 0)return;a=r}let i;if(l.present(n.onDevice)){const r=l.bool(n.onDevice);if(r===void 0)return;i=r}const o={};return s!==void 0&&(o.language=s),t!==void 0&&(o.continuous=t),a!==void 0&&(o.interimResults=a),i!==void 0&&(o.onDevice=i),o};class g extends Error{constructor(n){super(n),this.code=n}}const b={name:"speech",version:1},v=(e,n)=>{e.registerService(b.name,(s,t)=>{const a={result:r=>t.emit("result",r),speechStart:()=>t.emit("speechStart"),speechEnd:()=>t.emit("speechEnd"),end:()=>t.emit("end"),error:r=>t.emit("error",r)},i=n(m(s)??{},a);return{call:async(r,c)=>{switch(r){case"start":{let u;if(l.present(c[0])){const d=y(c[0]);if(d===void 0)throw new Error("bad arguments — Speech.start: options");u=d}await i.start(u);return}case"stop":{await i.stop();return}case"abort":{await i.abort();return}case"supportedLanguages":return await i.supportedLanguages();default:throw new Error(`Unknown method: ${r}`)}},close:()=>{var r;return(r=i.close)==null?void 0:r.call(i)}}},b.version)},h=()=>{const e=globalThis;return e.SpeechRecognition??e.webkitSpeechRecognition??null},w=e=>e==="no-speech"?"noSpeech":e==="audio-capture"?"audio":e==="network"?"network":e==="not-allowed"||e==="service-not-allowed"?"denied":e==="aborted"?"aborted":e==="language-not-supported"?"unavailable":"failed",S=e=>{h()&&v(e,(n,s)=>{let t=null,a=!1;const i=()=>{t&&(t.onresult=t.onend=t.onerror=t.onspeechstart=t.onspeechend=null,t=null)};return{async start(o){if(t)throw new g("busy");const r=h();if(!r)throw new g("unavailable");const c=new r;o!=null&&o.language&&(c.lang=o.language),c.continuous=(o==null?void 0:o.continuous)??!1,c.interimResults=(o==null?void 0:o.interimResults)??!0,c.maxAlternatives=1,a=!1,c.onspeechstart=()=>s.speechStart(),c.onspeechend=()=>s.speechEnd(),c.onresult=u=>{for(let d=u.resultIndex;d<u.results.length;d++){const p=u.results[d],f=p[0];f&&s.result({transcript:f.transcript,isFinal:!!p.isFinal,confidence:p.isFinal&&typeof f.confidence=="number"?f.confidence:null})}},c.onerror=u=>{const d=String((u==null?void 0:u.error)??"");a||d==="aborted"||s.error({code:w(d),message:String((u==null?void 0:u.message)||d||"recognition failed")})},c.onend=()=>{i(),s.end()},t=c;try{c.start()}catch{throw i(),new g("failed")}},async stop(){t==null||t.stop()},async abort(){t&&(a=!0,t.abort())},async supportedLanguages(){return[]},close(){if(!t)return;a=!0;const o=t;i(),o.abort()}}})};export{S as register};
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"plugins": {
|
|
3
|
+
"ar": "plugins/ar/index-CHvaraG4.js",
|
|
4
|
+
"audio-recorder": "plugins/audio-recorder/index-Bt-Caym7.js",
|
|
3
5
|
"browser": "plugins/browser/index-BpFb50ko.js",
|
|
4
6
|
"camera": "plugins/camera/index-5XOM8eHl.js",
|
|
5
7
|
"geolocation": "plugins/geolocation/index-BjTKN9_P.js",
|
|
6
8
|
"map": "plugins/map/index-BTBHQ264.js",
|
|
7
9
|
"qr-scanner": "plugins/qr-scanner/index-CK_cYoJR.js",
|
|
10
|
+
"speech": "plugins/speech/index-DzDPVK_j.js",
|
|
8
11
|
"webrtc": "plugins/webrtc/index-CbCltXvl.js"
|
|
9
12
|
}
|
|
10
13
|
}
|
|
@@ -706,17 +706,18 @@ export interface RuntimePlugin {
|
|
|
706
706
|
}
|
|
707
707
|
|
|
708
708
|
/** The SDK's engine variants — one tag of lecodes-ios-sdk per variant (`<version>` = full,
|
|
709
|
-
* `<version>-3d`, `-2d`, `-core`): core = UI only, 2d = + the 2D engine, 3d = + Filament with Jolt
|
|
710
|
-
*
|
|
709
|
+
* `<version>-3d`, `-2d`, `-core`): core = UI only, 2d = + the 2D engine, 3d = + Filament with Jolt,
|
|
710
|
+
* full = everything. The Swift sources are the same; the engine binary differs. */
|
|
711
711
|
export type SdkVariant = "core" | "2d" | "3d" | "full"
|
|
712
712
|
|
|
713
713
|
/** The tag of lecodes-ios-sdk a shell pins for an SDK version + variant. */
|
|
714
714
|
export const sdkTag = (sdkVersion: string, variant: SdkVariant): string => variant === "full" ? sdkVersion : `${sdkVersion}-${variant}`
|
|
715
715
|
|
|
716
|
-
/** The SDK products a shell links: `LeCodes` always; the 3D variants also
|
|
717
|
-
*
|
|
716
|
+
/** The SDK products a shell links: `LeCodes` always; the 3D variants also the binaural filters
|
|
717
|
+
* (`engine.useHrtf()`), as the viewer does. AR is a plugin (`"ar"` in app.json): its half comes
|
|
718
|
+
* with the plugins, not with the SDK. */
|
|
718
719
|
export const sdkProducts = (variant: SdkVariant): string[] =>
|
|
719
|
-
variant === "full" || variant === "3d" ? ["LeCodes", "
|
|
720
|
+
variant === "full" || variant === "3d" ? ["LeCodes", "LeCodesHRTF"] : ["LeCodes"]
|
|
720
721
|
|
|
721
722
|
/** LeCodesRuntime/Package.swift — the CLI-owned dependency graph: the SDK (one tag per
|
|
722
723
|
* variant, pinned with `.exact(…)`: the variant is the engine binary the tag's manifest names,
|
|
@@ -794,32 +795,30 @@ let package = Package(
|
|
|
794
795
|
/** Sources/LeCodesRuntime/LeCodesRuntime.swift — the SDK re-exported (app code needs only
|
|
795
796
|
* \`import LeCodesRuntime\`) and the shell's controller: LeCodesAppViewController, a
|
|
796
797
|
* LeCodesShellViewController (the SDK's — the boot: language, the OTA check, the newest local
|
|
797
|
-
* bundle) whose \`configure\` registers every plugin of app.json, the
|
|
798
|
-
* (server-backed projects) the baked project identity — all BEFORE the bundle runs, in a
|
|
798
|
+
* bundle) whose \`configure\` registers every plugin of app.json, the binaural filters of a 3D
|
|
799
|
+
* variant and (server-backed projects) the baked project identity — all BEFORE the bundle runs, in a
|
|
799
800
|
* CLI-owned file: a plain \`lecodes app sync\` moves every shell, the user-owned ViewController
|
|
800
801
|
* (a subclass of this one) is never edited. */
|
|
801
802
|
export const runtimeSwiftTemplate = (plugins: RuntimePlugin[], projectUuid: string | undefined, variant: SdkVariant): string => {
|
|
802
803
|
const is3d = variant === "full" || variant === "3d"
|
|
803
|
-
const imports = [...(is3d ? ["import
|
|
804
|
+
const imports = [...(is3d ? ["import LeCodesHRTF\n"] : []), ...plugins.map(p => `import ${p.module}\n`)].join("")
|
|
804
805
|
const identity = !projectUuid ? "" : ` // World identity: this shell IS the project — the engine attributes the boot world so
|
|
805
806
|
// identity-gated services (push registration) see the app's uuid from its first
|
|
806
807
|
// synchronous statement. Baked from .lecodes/manifest.json by \`lecodes app sync\`.
|
|
807
808
|
engine.bootProjectUuid = "${projectUuid}"
|
|
808
809
|
`
|
|
809
|
-
const
|
|
810
|
-
for mode in ["default", "world", "arcore", "detached"] { engine.registerARController(mode) { ARKitController(.world) } }
|
|
811
|
-
engine.registerARController("markers") { ARKitController(.markers) }
|
|
810
|
+
const hrtf = !is3d ? "" : ` // The binaural audio filters of the 3D engine (\`audio.hrtf\`).
|
|
812
811
|
engine.useHrtf()
|
|
813
812
|
`
|
|
814
813
|
const calls = plugins.map(p => ` ${p.register}.register(in: engine)\n`).join("")
|
|
815
|
-
const body = identity +
|
|
814
|
+
const body = identity + hrtf + calls
|
|
816
815
|
return `//
|
|
817
816
|
// LeCodesRuntime.swift
|
|
818
817
|
// GENERATED by lecodes — regenerated by \`lecodes app sync\`; do not edit.
|
|
819
818
|
//
|
|
820
819
|
// Re-exports the SDK (so app code needs only \`import LeCodesRuntime\`) and gives the app its
|
|
821
|
-
// controller: the SDK's shell controller with this project's plugins
|
|
822
|
-
//
|
|
820
|
+
// controller: the SDK's shell controller with this project's plugins and identity registered
|
|
821
|
+
// before the bundle runs. App/ViewController.swift subclasses it.
|
|
823
822
|
//
|
|
824
823
|
|
|
825
824
|
@_exported import LeCodes
|
|
@@ -896,7 +895,7 @@ CLI-owned — regenerated on every sync, hand edits are overwritten:
|
|
|
896
895
|
## SDK variant
|
|
897
896
|
|
|
898
897
|
Sync pins the SDK tag of the smallest engine variant that covers what the compiled bundle actually
|
|
899
|
-
uses: \`core\` (UI only), \`2d\` (+ the 2D engine with physics), \`3d\` (+ the 3D
|
|
898
|
+
uses: \`core\` (UI only), \`2d\` (+ the 2D engine with physics), \`3d\` (+ the 3D engine with
|
|
900
899
|
physics, ~10 MB heavier), or \`full\` (everything) — \`<version>-core\`, \`-2d\`, \`-3d\`, and the
|
|
901
900
|
bare \`<version>\` for full. Adding your first 3D scene or 2D game to the project switches it
|
|
902
901
|
automatically on the next sync; set \`ios.variant\` in \`../app.json\` to pin one instead.
|
|
@@ -1,195 +1,195 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
-
import { basename, dirname, join, resolve } from "node:path"
|
|
3
|
-
import { LECODES_DIR } from "../../project/manifest"
|
|
4
|
-
import { materializeTypesLocal, writeTsconfig } from "../../project/types"
|
|
5
|
-
import { DEFAULT_IGNORE_FILE, ensureGitignore } from "../../project/ignore"
|
|
6
|
-
import { appClaudeMd, CLAUDE_SETTINGS, STARTER_FLOW } from "./templates"
|
|
7
|
-
import { bool, c, defineCommand, log, note, success, warnErr } from "../../cli"
|
|
8
|
-
|
|
9
|
-
/*
|
|
10
|
-
* `lecodes init [dir]` — scaffold a brand-new LOCAL project: no account, no login, no server.
|
|
11
|
-
*
|
|
12
|
-
* Writes a starter entry, a CLAUDE.md (the headless render/test workflow for AI assistants) with
|
|
13
|
-
* a passing starter flow test, the SDK's import-free type surface + a tsconfig (so VS Code gives
|
|
14
|
-
* full IntelliSense offline), and a `.lecodesignore`. The result runs immediately with `lecodes dev`
|
|
15
|
-
* (hot reload on a device) and compiles with `lecodes compile -o app.js` (a self-contained bundle
|
|
16
|
-
* for LeCodes Desktop). It's the offline counterpart to `lecodes create`, which makes a project on
|
|
17
|
-
* the server; a local project needs no manifest — `dev`/`compile` compile straight from the tree.
|
|
18
|
-
*
|
|
19
|
-
* Idempotent: existing files are kept (never clobbered), so it's safe to re-run or run in a folder
|
|
20
|
-
* that already has content. Only the generated tsconfig + `.lecodes/types/` are rewritten each run.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
const STARTER = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
|
|
24
|
-
// Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
|
|
25
|
-
|
|
26
|
-
let count = 0
|
|
27
|
-
|
|
28
|
-
const counter = UIText("Taps: 0").style({ color: "#9fb0d0", fontSize: 22, marginTop: 18 })
|
|
29
|
-
|
|
30
|
-
const button = UIButton(UIText("Tap me").style({ color: "#ffffff", fontSize: 22 }))
|
|
31
|
-
.style({
|
|
32
|
-
backgroundColor: "#4f7cff",
|
|
33
|
-
borderRadius: 14,
|
|
34
|
-
paddingVertical: 16,
|
|
35
|
-
paddingHorizontal: 40,
|
|
36
|
-
marginTop: 24,
|
|
37
|
-
alignItems: "center",
|
|
38
|
-
justifyContent: "center",
|
|
39
|
-
$hovered: { backgroundColor: "#6b91ff" }, // the mouse over it (desktop); $pressed is the press
|
|
40
|
-
})
|
|
41
|
-
.named("tap") // names make elements targetable in tests/*.flow.json (see CLAUDE.md)
|
|
42
|
-
.onClick(() => {
|
|
43
|
-
count++
|
|
44
|
-
counter.text = "Taps: " + count
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
const screen = UIScreen(
|
|
48
|
-
UIColumn(UIText("Hello, LeCodes").style({ color: "#ffffff", fontSize: 32 }), button, counter)
|
|
49
|
-
.style({ backgroundColor: "#2b3550", borderRadius: 24, padding: 40, alignItems: "center" }),
|
|
50
|
-
)
|
|
51
|
-
.style({ backgroundColor: "#1e2230", alignItems: "center", justifyContent: "center", padding: 40 })
|
|
52
|
-
.named("home") // the screen root's name is what tests' \`expect: { screen: … }\` reads
|
|
53
|
-
|
|
54
|
-
screen.open()
|
|
55
|
-
`
|
|
56
|
-
|
|
57
|
-
/** `--desktop`: declare the desktop target up front. Window sizes are logical units (the host
|
|
58
|
-
* scales for HiDPI); `dev --desktop` / `desktop run` / `render --desktop` all read this, and a
|
|
59
|
-
* desktop-only app.json makes headless render/test default to the window size too. */
|
|
60
|
-
const appJsonDesktop = (name: string) => JSON.stringify({
|
|
61
|
-
name,
|
|
62
|
-
desktop: {
|
|
63
|
-
window: { width: 1280, height: 800, minWidth: 640, minHeight: 480 },
|
|
64
|
-
},
|
|
65
|
-
}, null, 2) + "\n"
|
|
66
|
-
|
|
67
|
-
const readmeTemplate = (name: string) => `# ${name}
|
|
68
|
-
|
|
69
|
-
A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
|
|
70
|
-
no imports needed. \`main.ts\` is the entry point.
|
|
71
|
-
|
|
72
|
-
## Run it
|
|
73
|
-
|
|
74
|
-
\`\`\`
|
|
75
|
-
lecodes dev # live on your phone: scan the QR with the LeCodes app, hot reload
|
|
76
|
-
lecodes compile -o app.js # bundle to a single self-contained JS file
|
|
77
|
-
\`\`\`
|
|
78
|
-
|
|
79
|
-
## Test it (no device needed)
|
|
80
|
-
|
|
81
|
-
\`\`\`
|
|
82
|
-
lecodes render --png shot.png # headless screenshot of the running app
|
|
83
|
-
lecodes test # run tests/*.flow.json — the starter ships with a passing one
|
|
84
|
-
\`\`\`
|
|
85
|
-
|
|
86
|
-
Scenario format + conventions: see [CLAUDE.md](./CLAUDE.md) (written for AI assistants, useful
|
|
87
|
-
for humans too).
|
|
88
|
-
|
|
89
|
-
## Put it on the platform
|
|
90
|
-
|
|
91
|
-
This project is local-only. To give it a home on le.codes (web editor, sharing, OTA updates):
|
|
92
|
-
|
|
93
|
-
\`\`\`
|
|
94
|
-
lecodes login # once per machine
|
|
95
|
-
lecodes link # create a project from this folder and connect it
|
|
96
|
-
lecodes push -m "initial" # upload it as the first checkpoint (--publish to go live)
|
|
97
|
-
\`\`\`
|
|
98
|
-
|
|
99
|
-
Already made the project in the web editor? \`lecodes link --project <uuid>\` adopts it — it
|
|
100
|
-
shows what your next push would change there before connecting anything.
|
|
101
|
-
|
|
102
|
-
## Go native
|
|
103
|
-
|
|
104
|
-
\`\`\`
|
|
105
|
-
lecodes app init ios # generate a native Xcode shell in ios/
|
|
106
|
-
lecodes app sync # embed the compiled bundle + plugins into it
|
|
107
|
-
lecodes app open # open it in Xcode, build & run
|
|
108
|
-
\`\`\`
|
|
109
|
-
|
|
110
|
-
## Layout
|
|
111
|
-
|
|
112
|
-
- \`main.ts\` — the app's entry point. Add more .ts files freely and import them from
|
|
113
|
-
\`main.ts\`; a file named \`main.ts\` is always the entry.
|
|
114
|
-
- \`tests/\` — headless flow tests (\`lecodes test\`); failing steps dump the frame to
|
|
115
|
-
\`tests/.artifacts/\`
|
|
116
|
-
- \`CLAUDE.md\` — guidance for AI assistants (the headless feedback loop + testability conventions)
|
|
117
|
-
- \`assets/\` — images/fonts the app references (created on demand)
|
|
118
|
-
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit).
|
|
119
|
-
They come from the SDK bundled in the CLI; \`lecodes update\` refreshes them from the platform.
|
|
120
|
-
- \`.lecodes/\` is gitignored — per-checkout state (those types + \`manifest.json\`, this machine's
|
|
121
|
-
sync cursor once linked). After a fresh git clone: \`lecodes update\`, then
|
|
122
|
-
\`lecodes link --project <uuid>\` to re-attach.
|
|
123
|
-
- \`.lecodesignore\` — files a future \`lecodes push\` would skip
|
|
124
|
-
|
|
125
|
-
Full command list: \`lecodes --help\`.
|
|
126
|
-
`
|
|
127
|
-
|
|
128
|
-
export default defineCommand({
|
|
129
|
-
name: "init",
|
|
130
|
-
summary: "Scaffold a new LOCAL project (no login, no server)",
|
|
131
|
-
usage: "[dir]",
|
|
132
|
-
description: "Writes a starter main.ts, a CLAUDE.md + a passing flow test (the headless render / test loop), the IDE types + tsconfig, and a .lecodesignore. Run it with `lecodes dev` or `lecodes compile`. Idempotent: existing files are kept.",
|
|
133
|
-
flags: { desktop: bool("also write an app.json declaring the desktop target (window size; `dev --desktop`, `desktop run` and headless render / test pick it up)") },
|
|
134
|
-
examples: ["lecodes init myapp", "lecodes init --desktop"],
|
|
135
|
-
run: ({ args, flags }) => init(args[0], flags.desktop),
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
const init = (dir: string | undefined, forDesktop: boolean): void => {
|
|
139
|
-
const dirArg = (dir ?? ".").replace(/[/\\]+$/, "") || "."
|
|
140
|
-
const root = resolve(dirArg)
|
|
141
|
-
const name = basename(root)
|
|
142
|
-
|
|
143
|
-
mkdirSync(root, { recursive: true })
|
|
144
|
-
|
|
145
|
-
let created = 0
|
|
146
|
-
const write = (rel: string, content: string) => {
|
|
147
|
-
const abs = join(root, rel)
|
|
148
|
-
if (existsSync(abs)) { note(`kept ${rel}`); return }
|
|
149
|
-
mkdirSync(dirname(abs), { recursive: true })
|
|
150
|
-
writeFileSync(abs, content)
|
|
151
|
-
log(`${c.green("created")} ${rel}`)
|
|
152
|
-
created++
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Starter entry — a root file named main.ts is always the detected entrypoint.
|
|
156
|
-
write("main.ts", STARTER)
|
|
157
|
-
write("README.md", readmeTemplate(name))
|
|
158
|
-
// --desktop: an app.json declaring the desktop target (window config in logical units).
|
|
159
|
-
if (forDesktop) write("app.json", appJsonDesktop(name))
|
|
160
|
-
// AI-assistant guidance + a passing flow test: the headless loop is discoverable from day one.
|
|
161
|
-
write("CLAUDE.md", appClaudeMd(name))
|
|
162
|
-
write(".claude/settings.json", CLAUDE_SETTINGS)
|
|
163
|
-
write("tests/counter.flow.json", STARTER_FLOW)
|
|
164
|
-
// Keep local render/preview outputs (screenshot.png, shots/) out of any future push.
|
|
165
|
-
write(".lecodesignore", DEFAULT_IGNORE_FILE)
|
|
166
|
-
// .lecodes/ is per-checkout state (generated types + the push cursor) — keep it out of git.
|
|
167
|
-
const gi = ensureGitignore(root)
|
|
168
|
-
if (gi === "created") { log(`${c.green("created")} .gitignore`); created++ }
|
|
169
|
-
else if (gi === "updated") log(`${c.green("updated")} .gitignore ${c.dim("(+ .lecodes/)")}`)
|
|
170
|
-
else note("kept .gitignore")
|
|
171
|
-
|
|
172
|
-
// IDE types: the SDK's import-free global surface + a tsconfig, so VS Code resolves UIScreen,
|
|
173
|
-
// Scene2D, setLoop, … with no server. Regenerated every run to pick up CLI upgrades.
|
|
174
|
-
const wroteTypes = materializeTypesLocal(root)
|
|
175
|
-
writeTsconfig(root)
|
|
176
|
-
log(`${c.green("written")} tsconfig.json${wroteTypes ? ` + ${LECODES_DIR}/types/` : ""} (IDE types)`)
|
|
177
|
-
if (!wroteTypes) {
|
|
178
|
-
warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.")
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
log("")
|
|
182
|
-
success(created
|
|
183
|
-
? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.`
|
|
184
|
-
: `Project ${c.bold(name)} already initialized.`)
|
|
185
|
-
const cd = dirArg === "." ? "" : `cd ${dirArg} && `
|
|
186
|
-
if (forDesktop) {
|
|
187
|
-
log(` ${c.bold(cd + "lecodes dev --desktop")} run it in a desktop window with hot reload`)
|
|
188
|
-
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (desktop-sized)`)
|
|
189
|
-
log(` ${c.bold(cd + "lecodes desktop run")} open the compiled app in the desktop host`)
|
|
190
|
-
} else {
|
|
191
|
-
log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`)
|
|
192
|
-
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (no device)`)
|
|
193
|
-
log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`)
|
|
194
|
-
}
|
|
195
|
-
}
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path"
|
|
3
|
+
import { LECODES_DIR } from "../../project/manifest"
|
|
4
|
+
import { materializeTypesLocal, writeTsconfig } from "../../project/types"
|
|
5
|
+
import { DEFAULT_IGNORE_FILE, ensureGitignore } from "../../project/ignore"
|
|
6
|
+
import { appClaudeMd, CLAUDE_SETTINGS, STARTER_FLOW } from "./templates"
|
|
7
|
+
import { bool, c, defineCommand, log, note, success, warnErr } from "../../cli"
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* `lecodes init [dir]` — scaffold a brand-new LOCAL project: no account, no login, no server.
|
|
11
|
+
*
|
|
12
|
+
* Writes a starter entry, a CLAUDE.md (the headless render/test workflow for AI assistants) with
|
|
13
|
+
* a passing starter flow test, the SDK's import-free type surface + a tsconfig (so VS Code gives
|
|
14
|
+
* full IntelliSense offline), and a `.lecodesignore`. The result runs immediately with `lecodes dev`
|
|
15
|
+
* (hot reload on a device) and compiles with `lecodes compile -o app.js` (a self-contained bundle
|
|
16
|
+
* for LeCodes Desktop). It's the offline counterpart to `lecodes create`, which makes a project on
|
|
17
|
+
* the server; a local project needs no manifest — `dev`/`compile` compile straight from the tree.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent: existing files are kept (never clobbered), so it's safe to re-run or run in a folder
|
|
20
|
+
* that already has content. Only the generated tsconfig + `.lecodes/types/` are rewritten each run.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const STARTER = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
|
|
24
|
+
// Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
|
|
25
|
+
|
|
26
|
+
let count = 0
|
|
27
|
+
|
|
28
|
+
const counter = UIText("Taps: 0").style({ color: "#9fb0d0", fontSize: 22, marginTop: 18 })
|
|
29
|
+
|
|
30
|
+
const button = UIButton(UIText("Tap me").style({ color: "#ffffff", fontSize: 22 }))
|
|
31
|
+
.style({
|
|
32
|
+
backgroundColor: "#4f7cff",
|
|
33
|
+
borderRadius: 14,
|
|
34
|
+
paddingVertical: 16,
|
|
35
|
+
paddingHorizontal: 40,
|
|
36
|
+
marginTop: 24,
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
justifyContent: "center",
|
|
39
|
+
$hovered: { backgroundColor: "#6b91ff" }, // the mouse over it (desktop); $pressed is the press
|
|
40
|
+
})
|
|
41
|
+
.named("tap") // names make elements targetable in tests/*.flow.json (see CLAUDE.md)
|
|
42
|
+
.onClick(() => {
|
|
43
|
+
count++
|
|
44
|
+
counter.text = "Taps: " + count
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const screen = UIScreen(
|
|
48
|
+
UIColumn(UIText("Hello, LeCodes").style({ color: "#ffffff", fontSize: 32 }), button, counter)
|
|
49
|
+
.style({ backgroundColor: "#2b3550", borderRadius: 24, padding: 40, alignItems: "center" }),
|
|
50
|
+
)
|
|
51
|
+
.style({ backgroundColor: "#1e2230", alignItems: "center", justifyContent: "center", padding: 40 })
|
|
52
|
+
.named("home") // the screen root's name is what tests' \`expect: { screen: … }\` reads
|
|
53
|
+
|
|
54
|
+
screen.open()
|
|
55
|
+
`
|
|
56
|
+
|
|
57
|
+
/** `--desktop`: declare the desktop target up front. Window sizes are logical units (the host
|
|
58
|
+
* scales for HiDPI); `dev --desktop` / `desktop run` / `render --desktop` all read this, and a
|
|
59
|
+
* desktop-only app.json makes headless render/test default to the window size too. */
|
|
60
|
+
const appJsonDesktop = (name: string) => JSON.stringify({
|
|
61
|
+
name,
|
|
62
|
+
desktop: {
|
|
63
|
+
window: { width: 1280, height: 800, minWidth: 640, minHeight: 480 },
|
|
64
|
+
},
|
|
65
|
+
}, null, 2) + "\n"
|
|
66
|
+
|
|
67
|
+
const readmeTemplate = (name: string) => `# ${name}
|
|
68
|
+
|
|
69
|
+
A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
|
|
70
|
+
no imports needed. \`main.ts\` is the entry point.
|
|
71
|
+
|
|
72
|
+
## Run it
|
|
73
|
+
|
|
74
|
+
\`\`\`
|
|
75
|
+
lecodes dev # live on your phone: scan the QR with the LeCodes app, hot reload
|
|
76
|
+
lecodes compile -o app.js # bundle to a single self-contained JS file
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
## Test it (no device needed)
|
|
80
|
+
|
|
81
|
+
\`\`\`
|
|
82
|
+
lecodes render --png shot.png # headless screenshot of the running app
|
|
83
|
+
lecodes test # run tests/*.flow.json — the starter ships with a passing one
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
Scenario format + conventions: see [CLAUDE.md](./CLAUDE.md) (written for AI assistants, useful
|
|
87
|
+
for humans too).
|
|
88
|
+
|
|
89
|
+
## Put it on the platform
|
|
90
|
+
|
|
91
|
+
This project is local-only. To give it a home on le.codes (web editor, sharing, OTA updates):
|
|
92
|
+
|
|
93
|
+
\`\`\`
|
|
94
|
+
lecodes login # once per machine
|
|
95
|
+
lecodes link # create a project from this folder and connect it
|
|
96
|
+
lecodes push -m "initial" # upload it as the first checkpoint (--publish to go live)
|
|
97
|
+
\`\`\`
|
|
98
|
+
|
|
99
|
+
Already made the project in the web editor? \`lecodes link --project <uuid>\` adopts it — it
|
|
100
|
+
shows what your next push would change there before connecting anything.
|
|
101
|
+
|
|
102
|
+
## Go native
|
|
103
|
+
|
|
104
|
+
\`\`\`
|
|
105
|
+
lecodes app init ios # generate a native Xcode shell in ios/
|
|
106
|
+
lecodes app sync # embed the compiled bundle + plugins into it
|
|
107
|
+
lecodes app open # open it in Xcode, build & run
|
|
108
|
+
\`\`\`
|
|
109
|
+
|
|
110
|
+
## Layout
|
|
111
|
+
|
|
112
|
+
- \`main.ts\` — the app's entry point. Add more .ts files freely and import them from
|
|
113
|
+
\`main.ts\`; a file named \`main.ts\` is always the entry.
|
|
114
|
+
- \`tests/\` — headless flow tests (\`lecodes test\`); failing steps dump the frame to
|
|
115
|
+
\`tests/.artifacts/\`
|
|
116
|
+
- \`CLAUDE.md\` — guidance for AI assistants (the headless feedback loop + testability conventions)
|
|
117
|
+
- \`assets/\` — images/fonts the app references (created on demand)
|
|
118
|
+
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit).
|
|
119
|
+
They come from the SDK bundled in the CLI; \`lecodes update\` refreshes them from the platform.
|
|
120
|
+
- \`.lecodes/\` is gitignored — per-checkout state (those types + \`manifest.json\`, this machine's
|
|
121
|
+
sync cursor once linked). After a fresh git clone: \`lecodes update\`, then
|
|
122
|
+
\`lecodes link --project <uuid>\` to re-attach.
|
|
123
|
+
- \`.lecodesignore\` — files a future \`lecodes push\` would skip
|
|
124
|
+
|
|
125
|
+
Full command list: \`lecodes --help\`.
|
|
126
|
+
`
|
|
127
|
+
|
|
128
|
+
export default defineCommand({
|
|
129
|
+
name: "init",
|
|
130
|
+
summary: "Scaffold a new LOCAL project (no login, no server)",
|
|
131
|
+
usage: "[dir]",
|
|
132
|
+
description: "Writes a starter main.ts, a CLAUDE.md + a passing flow test (the headless render / test loop), the IDE types + tsconfig, and a .lecodesignore. Run it with `lecodes dev` or `lecodes compile`. Idempotent: existing files are kept.",
|
|
133
|
+
flags: { desktop: bool("also write an app.json declaring the desktop target (window size; `dev --desktop`, `desktop run` and headless render / test pick it up)") },
|
|
134
|
+
examples: ["lecodes init myapp", "lecodes init --desktop"],
|
|
135
|
+
run: ({ args, flags }) => init(args[0], flags.desktop),
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const init = (dir: string | undefined, forDesktop: boolean): void => {
|
|
139
|
+
const dirArg = (dir ?? ".").replace(/[/\\]+$/, "") || "."
|
|
140
|
+
const root = resolve(dirArg)
|
|
141
|
+
const name = basename(root)
|
|
142
|
+
|
|
143
|
+
mkdirSync(root, { recursive: true })
|
|
144
|
+
|
|
145
|
+
let created = 0
|
|
146
|
+
const write = (rel: string, content: string) => {
|
|
147
|
+
const abs = join(root, rel)
|
|
148
|
+
if (existsSync(abs)) { note(`kept ${rel}`); return }
|
|
149
|
+
mkdirSync(dirname(abs), { recursive: true })
|
|
150
|
+
writeFileSync(abs, content)
|
|
151
|
+
log(`${c.green("created")} ${rel}`)
|
|
152
|
+
created++
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Starter entry — a root file named main.ts is always the detected entrypoint.
|
|
156
|
+
write("main.ts", STARTER)
|
|
157
|
+
write("README.md", readmeTemplate(name))
|
|
158
|
+
// --desktop: an app.json declaring the desktop target (window config in logical units).
|
|
159
|
+
if (forDesktop) write("app.json", appJsonDesktop(name))
|
|
160
|
+
// AI-assistant guidance + a passing flow test: the headless loop is discoverable from day one.
|
|
161
|
+
write("CLAUDE.md", appClaudeMd(name))
|
|
162
|
+
write(".claude/settings.json", CLAUDE_SETTINGS)
|
|
163
|
+
write("tests/counter.flow.json", STARTER_FLOW)
|
|
164
|
+
// Keep local render/preview outputs (screenshot.png, shots/) out of any future push.
|
|
165
|
+
write(".lecodesignore", DEFAULT_IGNORE_FILE)
|
|
166
|
+
// .lecodes/ is per-checkout state (generated types + the push cursor) — keep it out of git.
|
|
167
|
+
const gi = ensureGitignore(root)
|
|
168
|
+
if (gi === "created") { log(`${c.green("created")} .gitignore`); created++ }
|
|
169
|
+
else if (gi === "updated") log(`${c.green("updated")} .gitignore ${c.dim("(+ .lecodes/)")}`)
|
|
170
|
+
else note("kept .gitignore")
|
|
171
|
+
|
|
172
|
+
// IDE types: the SDK's import-free global surface + a tsconfig, so VS Code resolves UIScreen,
|
|
173
|
+
// Scene2D, setLoop, … with no server. Regenerated every run to pick up CLI upgrades.
|
|
174
|
+
const wroteTypes = materializeTypesLocal(root)
|
|
175
|
+
writeTsconfig(root)
|
|
176
|
+
log(`${c.green("written")} tsconfig.json${wroteTypes ? ` + ${LECODES_DIR}/types/` : ""} (IDE types)`)
|
|
177
|
+
if (!wroteTypes) {
|
|
178
|
+
warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
log("")
|
|
182
|
+
success(created
|
|
183
|
+
? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.`
|
|
184
|
+
: `Project ${c.bold(name)} already initialized.`)
|
|
185
|
+
const cd = dirArg === "." ? "" : `cd ${dirArg} && `
|
|
186
|
+
if (forDesktop) {
|
|
187
|
+
log(` ${c.bold(cd + "lecodes dev --desktop")} run it in a desktop window with hot reload`)
|
|
188
|
+
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (desktop-sized)`)
|
|
189
|
+
log(` ${c.bold(cd + "lecodes desktop run")} open the compiled app in the desktop host`)
|
|
190
|
+
} else {
|
|
191
|
+
log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`)
|
|
192
|
+
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (no device)`)
|
|
193
|
+
log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`)
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/commands/render.ts
CHANGED
|
@@ -62,6 +62,7 @@ export const viewportFlags = {
|
|
|
62
62
|
device: str("safe-area preset: none | iphone | android (default: orientation-based)", { value: "<preset>" }),
|
|
63
63
|
"safe-area": str("override the safe-area insets (px) the safe-* keywords resolve to", { value: "<t,r,b,l>" }),
|
|
64
64
|
logs: bool("forward the app's console output (to stderr)"),
|
|
65
|
+
"native-view": str("(headless) draw these native views as a placeholder — the label in their box — instead of having none: map=Map,camera (a bare name is its own label); calls to them reject", { value: "<name=label,…>" }),
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
/** `--desktop`: the NATIVE host in place of the headless renderer. */
|
|
@@ -102,6 +103,19 @@ export const resolveSafeAreaChoice = (explicit: string | undefined, device: stri
|
|
|
102
103
|
return undefined
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
/** `--native-view map=Карта,camera`: view name → the label its placeholder shows (a bare name is its own label). */
|
|
107
|
+
export const parseNativeViews = (raw: string | undefined): Record<string, string> | undefined => {
|
|
108
|
+
if (raw === undefined) return undefined
|
|
109
|
+
const views: Record<string, string> = {}
|
|
110
|
+
for (const part of raw.split(",").map((p) => p.trim()).filter(Boolean)) {
|
|
111
|
+
const eq = part.indexOf("=")
|
|
112
|
+
const name = (eq < 0 ? part : part.slice(0, eq)).trim()
|
|
113
|
+
if (!name) throw new CliError(`--native-view expects "name=label,…" (got "${raw}").`)
|
|
114
|
+
views[name] = eq < 0 ? name : part.slice(eq + 1).trim()
|
|
115
|
+
}
|
|
116
|
+
return views
|
|
117
|
+
}
|
|
118
|
+
|
|
105
119
|
/** The optional renderer package — installed into ~/.lecodes/peers on first use (peerInstall.ts). */
|
|
106
120
|
export const requireRenderer = async (): Promise<typeof import("lecodes-headless/headless")> =>
|
|
107
121
|
loadPeer<typeof import("lecodes-headless/headless")>("lecodes-headless", "headless", { for: "lecodes render" })
|
|
@@ -403,6 +417,7 @@ const renderHeadless = async (flags: RenderFlags, file: string | undefined): Pro
|
|
|
403
417
|
timeMs: flags.time,
|
|
404
418
|
afterAnimations: !flags["no-after-animations"],
|
|
405
419
|
...resolveSafeAreaChoice(flags["safe-area"] ?? scenario?.safeArea, flags.device ?? scenario?.device),
|
|
420
|
+
nativeViews: parseNativeViews(flags["native-view"]),
|
|
406
421
|
onConsole,
|
|
407
422
|
}
|
|
408
423
|
|