infinicode 2.6.0 → 2.6.2
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/cli.js +1 -0
- package/dist/commands/serve.d.ts +1 -0
- package/dist/commands/serve.js +5 -0
- package/dist/kernel/command-system.js +71 -0
- package/dist/kernel/config-schema.d.ts +2 -0
- package/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +231 -96
- package/dist/kernel/federation/federation.js +12 -0
- package/dist/kernel/federation/self-update.d.ts +26 -0
- package/dist/kernel/federation/self-update.js +68 -0
- package/dist/kernel/federation/transport-http.js +35 -0
- package/package.json +8 -7
|
@@ -358,6 +358,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
358
358
|
<input id="cmdInput" placeholder="/status /nodes /health (Enter to run)" autocomplete="off" spellcheck="false">
|
|
359
359
|
<button class="act" id="runBtn">Run</button>
|
|
360
360
|
<button class="act ghost" id="applyBtn">Apply config to fleet</button>
|
|
361
|
+
<button class="act ghost" id="updateBtn" title="npm-update every node to the latest release and restart (supervised nodes come back on their own)">Update fleet</button>
|
|
361
362
|
</footer>
|
|
362
363
|
<div id="out"><button class="x" id="outClose">close</button><span id="outText"></span></div>
|
|
363
364
|
|
|
@@ -554,6 +555,16 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
554
555
|
.catch(function(e){ showOut('apply failed: '+e.message); })
|
|
555
556
|
.then(function(){ $('applyBtn').disabled=false; });
|
|
556
557
|
});
|
|
558
|
+
$('updateBtn').addEventListener('click', function(){
|
|
559
|
+
if(!confirm('Update every node to the latest npm release and restart them?\\n\\nSupervised nodes relaunch automatically; the hub itself will blip while it restarts.')) return;
|
|
560
|
+
$('updateBtn').disabled = true;
|
|
561
|
+
showOut('Updating fleet — installing latest on each node…');
|
|
562
|
+
fetch('/fed/command'+QS, { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ input:'/update all' }) })
|
|
563
|
+
.then(function(r){ return r.json().catch(function(){ return { ok:false, text:'HTTP '+r.status }; }); })
|
|
564
|
+
.then(function(res){ showOut('Update fleet:\\n' + ((res&&(res.text!=null?res.text:JSON.stringify(res,null,2)))||'done')); poll(); })
|
|
565
|
+
.catch(function(e){ showOut('update failed: '+e.message); })
|
|
566
|
+
.then(function(){ $('updateBtn').disabled=false; });
|
|
567
|
+
});
|
|
557
568
|
|
|
558
569
|
// ── tabs: Fleet + Park + RoboPark sessions/telemetry ──
|
|
559
570
|
var tabs = document.querySelectorAll('.tab');
|
|
@@ -694,13 +705,28 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
694
705
|
// Robot cam targets a LiveKit track via /robopark/api/robots/:id/stream.
|
|
695
706
|
// ══════════════════════════════════════════════════════════════════
|
|
696
707
|
var Park = (function(){
|
|
697
|
-
var C, X, DPR=1, W=0, H=0, cam={x:0,y:0,z:1};
|
|
708
|
+
var C, X, DPR=1, W=0, H=0, cam={x:0,y:0,z:1}, fitted=false;
|
|
698
709
|
var nodes=[], byId={}, orbs=[], pulses=[], t0=0, raf=null, running=false, es=null, animSeq=-1;
|
|
699
|
-
var TW=124, TH=62;
|
|
700
710
|
var drawerId=null, camRAF=null, meterRAF=null, camAbort=null, drawerTimer=null;
|
|
701
|
-
var
|
|
711
|
+
var lkRoom=null, micAnalyser=null, audioCtx=null;
|
|
712
|
+
var TW=44, TH=22, PARK_W=15, PARK_D=8;
|
|
713
|
+
// The real park (from the aerial): named robot pads at their ground
|
|
714
|
+
// positions, plus palms, pond, and the street. A mesh node binds to a pad
|
|
715
|
+
// when its display name matches the pad keywords (robobmw → VIXEN/BMW).
|
|
716
|
+
var SLOTS=[
|
|
717
|
+
{label:'VOLT', keys:['volt'], nx:0.30, ny:0.22},
|
|
718
|
+
{label:'TESLA', keys:['tesla'], nx:0.44, ny:0.36},
|
|
719
|
+
{label:'JAGUAR', keys:['jaguar','jag'], nx:0.60, ny:0.40},
|
|
720
|
+
{label:'VIXEN', keys:['vixen','bmw'], nx:0.55, ny:0.66, sub:'BMW'},
|
|
721
|
+
{label:'PANDA', keys:['panda'], nx:0.84, ny:0.58}
|
|
722
|
+
];
|
|
723
|
+
var HUBSLOT={label:'CONTROL', keys:[], nx:0.09, ny:0.12};
|
|
724
|
+
var PALMS=[[0.11,0.55],[0.15,0.74],[0.19,0.42],[0.23,0.64],[0.17,0.31],[0.25,0.82],[0.37,0.24],[0.41,0.56],[0.49,0.74],[0.52,0.29],[0.59,0.62],[0.66,0.26],[0.71,0.69],[0.76,0.42],[0.82,0.74],[0.89,0.42],[0.92,0.68],[0.67,0.83],[0.34,0.71]];
|
|
725
|
+
var POND={cx:0.33*15, cy:0.99*8, rx:3.1, ry:1.4};
|
|
726
|
+
var CARS=[[4,-0.95,'#e0a23c'],[6,-0.8,'#7fb4ff'],[8,-1.0,'#ff7e6e'],[10.5,-0.78,'#d8d8e0'],[12.8,-0.95,'#f2c14e']];
|
|
702
727
|
|
|
703
728
|
function mono(){ return 'ui-monospace,Menlo,Consolas,monospace'; }
|
|
729
|
+
function W2(nx,ny){ return [nx*PARK_W, ny*PARK_D]; }
|
|
704
730
|
function kindOf(n){
|
|
705
731
|
var r=(n.role||'').toLowerCase();
|
|
706
732
|
if(r==='hub'||r==='scheduler') return 'hub';
|
|
@@ -712,19 +738,41 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
712
738
|
function loadColor(l){ return l>=75?getVar('--bad'):l>=45?getVar('--warn'):getVar('--ok'); }
|
|
713
739
|
function getVar(v){ try{ return getComputedStyle(document.documentElement).getPropertyValue(v).trim()||'#ccc'; }catch(e){ return '#ccc'; } }
|
|
714
740
|
|
|
741
|
+
// Bind mesh nodes onto the fixed park pads; unmatched pads render as
|
|
742
|
+
// offline placeholders so the whole park is always visible.
|
|
715
743
|
function layout(src){
|
|
716
|
-
var list=(src||[]).slice()
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
744
|
+
var list=(src||[]).slice(), used={}, out=[];
|
|
745
|
+
function prevHit(id){ for(var p=0;p<nodes.length;p++){ if(nodes[p].id===id) return nodes[p]._hit; } return null; }
|
|
746
|
+
function bind(slot, wx, wy, kind){
|
|
747
|
+
var match=null;
|
|
748
|
+
for(var i=0;i<list.length;i++){ var n=list[i]; if(used[n.nodeId]) continue;
|
|
749
|
+
var nm=(n.displayName||n.nodeId||'').toLowerCase(), isHub=kindOf(n)==='hub';
|
|
750
|
+
if(kind==='hub'){ if(isHub){ match=n; break; } }
|
|
751
|
+
else if(!isHub){ for(var k=0;k<slot.keys.length;k++){ if(slot.keys[k] && nm.indexOf(slot.keys[k])>-1){ match=n; break; } } if(match) break; }
|
|
752
|
+
}
|
|
753
|
+
var id = match?match.nodeId:('pad:'+slot.label);
|
|
754
|
+
if(match) used[match.nodeId]=1;
|
|
755
|
+
return {
|
|
756
|
+
id:id, kind:kind, label:slot.label,
|
|
757
|
+
name: match?(match.displayName||match.nodeId):slot.label.toLowerCase(),
|
|
758
|
+
sub: match?(slot.sub||''):'not joined',
|
|
759
|
+
connected: match?!!match.connected:false,
|
|
760
|
+
load: (match&&match.load!=null)?match.load:0,
|
|
761
|
+
platform: match?(match.platform||''):'', version: match?(match.version||'?'):'—',
|
|
762
|
+
site: match?(match.site||''):'', caps: match?(match.capabilities||[]):[],
|
|
763
|
+
telemKeys: [slot.label.toLowerCase()].concat(slot.keys||[]).concat(match?[(match.displayName||'').toLowerCase()]:[]),
|
|
764
|
+
wx:wx, wy:wy, _hit:prevHit(id)
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
var h=W2(HUBSLOT.nx,HUBSLOT.ny); out.push(bind(HUBSLOT,h[0],h[1],'hub'));
|
|
768
|
+
SLOTS.forEach(function(s){ var w=W2(s.nx,s.ny); out.push(bind(s,w[0],w[1],'robot')); });
|
|
769
|
+
// Any extra live node that matched no pad → line it along the near edge.
|
|
770
|
+
var ex=0;
|
|
771
|
+
list.forEach(function(n){ if(used[n.nodeId]) return; used[n.nodeId]=1;
|
|
772
|
+
var w=W2(0.20+ex*0.13, 0.90); ex++;
|
|
773
|
+
out.push({ id:n.nodeId, kind:kindOf(n), label:(n.displayName||n.nodeId), name:(n.displayName||n.nodeId),
|
|
774
|
+
sub:n.role||'', connected:!!n.connected, load:(n.load!=null?n.load:0), platform:n.platform||'', version:n.version||'?',
|
|
775
|
+
site:n.site||'', caps:n.capabilities||[], telemKeys:[(n.displayName||'').toLowerCase()], wx:w[0], wy:w[1], _hit:prevHit(n.nodeId) });
|
|
728
776
|
});
|
|
729
777
|
return out;
|
|
730
778
|
}
|
|
@@ -739,8 +787,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
739
787
|
|
|
740
788
|
function hub(){ for(var i=0;i<nodes.length;i++) if(nodes[i].kind==='hub') return nodes[i]; return nodes[0]; }
|
|
741
789
|
function findByFrame(f){
|
|
742
|
-
var key=f.n; if(key
|
|
743
|
-
for(var i=0;i<nodes.length;i++){
|
|
790
|
+
var key=(f.n==null?'':String(f.n)).toLowerCase(); if(!key) return null;
|
|
791
|
+
for(var i=0;i<nodes.length;i++){ var n=nodes[i];
|
|
792
|
+
if(n.id===f.n || (n.name||'').toLowerCase()===key) return n;
|
|
793
|
+
for(var k=0;k<n.telemKeys.length;k++){ if(n.telemKeys[k] && key.indexOf(n.telemKeys[k])>-1) return n; }
|
|
794
|
+
}
|
|
744
795
|
return null;
|
|
745
796
|
}
|
|
746
797
|
function ingest(f, fromPoll){
|
|
@@ -786,30 +837,67 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
786
837
|
C.addEventListener('click',function(e){
|
|
787
838
|
var r=C.getBoundingClientRect(), mx=e.clientX-r.left, my=e.clientY-r.top, hit=null;
|
|
788
839
|
for(var i=0;i<nodes.length;i++){ var hb=nodes[i]._hit; if(hb&&mx>=hb.x&&mx<=hb.x+hb.w&&my>=hb.y&&my<=hb.y+hb.h) hit=nodes[i]; }
|
|
789
|
-
if(hit){ openDrawer(hit.id); selectNode(hit.id, hit.name); }
|
|
840
|
+
if(hit){ openDrawer(hit.id); if(String(hit.id).indexOf('pad:')!==0) selectNode(hit.id, hit.name); }
|
|
790
841
|
});
|
|
791
842
|
window.addEventListener('resize', function(){ if(running) resize(); });
|
|
792
843
|
}
|
|
793
844
|
function resize(){
|
|
794
845
|
var st=$('pk-stage'); if(!st) return;
|
|
795
846
|
W=st.clientWidth; H=st.clientHeight; C.width=W*DPR; C.height=H*DPR; X.setTransform(DPR,0,0,DPR,0,0);
|
|
847
|
+
if(!fitted){ fit(); fitted=true; }
|
|
796
848
|
}
|
|
797
|
-
function
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
849
|
+
function fit(){
|
|
850
|
+
var span=PARK_W+PARK_D;
|
|
851
|
+
var zx=(W-330)/(span*TW*0.5+120), zy=H/(span*TH+220);
|
|
852
|
+
cam.z=Math.max(0.5,Math.min(1.5, Math.min(zx,zy))); cam.x=0; cam.y=0;
|
|
853
|
+
}
|
|
854
|
+
// World (park) coords → screen. Origin biased left so the right-docked feed
|
|
855
|
+
// doesn't cover the plaza; the park is centered on (PARK_W/2, PARK_D/2).
|
|
856
|
+
function iso(wx,wy){
|
|
857
|
+
var dx=(wx-PARK_W/2)-(wy-PARK_D/2), dy=(wx-PARK_W/2)+(wy-PARK_D/2);
|
|
858
|
+
return { x:(W-300)/2 + cam.x + dx*TW*cam.z, y:H*0.42 + cam.y + dy*TH*cam.z };
|
|
859
|
+
}
|
|
860
|
+
function poly(pts, fill, stroke, lw){
|
|
861
|
+
X.beginPath();
|
|
862
|
+
for(var i=0;i<pts.length;i++){ var p=iso(pts[i][0],pts[i][1]); if(i===0)X.moveTo(p.x,p.y); else X.lineTo(p.x,p.y); }
|
|
863
|
+
X.closePath();
|
|
864
|
+
if(fill){ X.fillStyle=fill; X.fill(); }
|
|
865
|
+
if(stroke){ X.strokeStyle=stroke; X.lineWidth=lw||1; X.stroke(); }
|
|
866
|
+
}
|
|
867
|
+
function drawGround(){
|
|
868
|
+
// grass base
|
|
869
|
+
var g=X.createLinearGradient(0,0,0,H); g.addColorStop(0,'rgba(38,68,44,0.55)'); g.addColorStop(1,'rgba(22,44,28,0.5)');
|
|
870
|
+
poly([[-2,-2.2],[PARK_W+2,-2.2],[PARK_W+2,PARK_D+2],[-2,PARK_D+2]], g, 'rgba(120,180,120,0.10)',1);
|
|
871
|
+
// street band along the far edge + dashed centerline + cars
|
|
872
|
+
poly([[0,-1.5],[PARK_W,-1.5],[PARK_W,-0.35],[0,-0.35]], 'rgba(26,26,32,0.88)', 'rgba(255,255,255,0.08)',1);
|
|
873
|
+
X.strokeStyle='rgba(232,210,120,0.5)'; X.setLineDash([9*cam.z,9*cam.z]);
|
|
874
|
+
var r1=iso(0.2,-0.92), r2=iso(PARK_W-0.2,-0.92); X.beginPath(); X.moveTo(r1.x,r1.y); X.lineTo(r2.x,r2.y); X.stroke(); X.setLineDash([]);
|
|
875
|
+
CARS.forEach(function(c){ drawCar(c[0],c[1],c[2]); });
|
|
876
|
+
// paved plaza
|
|
877
|
+
var px0=0.4,py0=-0.15,px1=PARK_W-0.4,py1=PARK_D-0.6;
|
|
878
|
+
var pg=X.createLinearGradient(0,0,0,H); pg.addColorStop(0,'rgba(74,70,92,0.66)'); pg.addColorStop(1,'rgba(46,42,60,0.62)');
|
|
879
|
+
poly([[px0,py0],[px1,py0],[px1,py1],[px0,py1]], pg, 'rgba(255,255,255,0.07)',1);
|
|
880
|
+
X.strokeStyle='rgba(255,255,255,0.05)'; X.lineWidth=1;
|
|
881
|
+
for(var gx=Math.ceil(px0);gx<px1;gx++){ var a=iso(gx,py0), b=iso(gx,py1); X.beginPath(); X.moveTo(a.x,a.y); X.lineTo(b.x,b.y); X.stroke(); }
|
|
882
|
+
for(var gy=Math.ceil(py0);gy<py1;gy++){ var c=iso(px0,gy), d=iso(px1,gy); X.beginPath(); X.moveTo(c.x,c.y); X.lineTo(d.x,d.y); X.stroke(); }
|
|
883
|
+
// pond
|
|
884
|
+
var pts=[]; for(var t=0;t<Math.PI*2;t+=Math.PI/9){ pts.push([POND.cx+POND.rx*Math.cos(t), POND.cy+POND.ry*Math.sin(t)]); }
|
|
885
|
+
var wg=X.createLinearGradient(0,0,0,H); wg.addColorStop(0,'rgba(44,116,156,0.72)'); wg.addColorStop(1,'rgba(26,74,116,0.72)');
|
|
886
|
+
poly(pts, wg, 'rgba(130,205,235,0.32)',1.5);
|
|
887
|
+
}
|
|
888
|
+
function drawCar(wx,wy,col){
|
|
889
|
+
var p=iso(wx,wy), z=cam.z;
|
|
890
|
+
X.fillStyle='rgba(0,0,0,0.3)'; X.beginPath(); X.ellipse(p.x,p.y+3*z,11*z,5*z,0,0,7); X.fill();
|
|
891
|
+
X.fillStyle=col; X.beginPath(); X.roundRect(p.x-11*z,p.y-7*z,22*z,12*z,3*z); X.fill();
|
|
892
|
+
X.fillStyle='rgba(255,255,255,0.25)'; X.beginPath(); X.roundRect(p.x-6*z,p.y-6*z,12*z,5*z,2*z); X.fill();
|
|
893
|
+
}
|
|
894
|
+
function drawPalm(wx,wy){
|
|
895
|
+
var p=iso(wx,wy), z=cam.z;
|
|
896
|
+
X.fillStyle='rgba(0,0,0,0.26)'; X.beginPath(); X.ellipse(p.x,p.y,10*z,4.5*z,0,0,7); X.fill();
|
|
897
|
+
X.strokeStyle='#6b4a24'; X.lineWidth=3*z; X.beginPath(); X.moveTo(p.x,p.y); X.quadraticCurveTo(p.x+3*z,p.y-14*z,p.x+1*z,p.y-24*z); X.stroke();
|
|
898
|
+
var tx=p.x+1*z, ty=p.y-24*z; X.strokeStyle='#3f8f4a'; X.lineWidth=2.4*z;
|
|
899
|
+
for(var a=0;a<6;a++){ var ang=(-Math.PI/2)+(a-2.5)*0.52; X.beginPath(); X.moveTo(tx,ty); X.quadraticCurveTo(tx+Math.cos(ang)*10*z, ty+Math.sin(ang)*8*z-4*z, tx+Math.cos(ang)*20*z, ty+Math.sin(ang)*11*z+3*z); X.stroke(); }
|
|
900
|
+
X.fillStyle='#caa23a'; X.beginPath(); X.arc(tx,ty+1*z,2*z,0,7); X.fill();
|
|
813
901
|
}
|
|
814
902
|
function ring(cx,cy,r,frac,color){
|
|
815
903
|
X.beginPath(); X.arc(cx,cy,r,0,Math.PI*2); X.strokeStyle='rgba(255,255,255,0.12)'; X.lineWidth=3*cam.z; X.stroke();
|
|
@@ -820,62 +908,68 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
820
908
|
X.font='700 '+(11*z)+'px '+mono(); X.fillStyle=col; X.fillText(txt,cx,cy);
|
|
821
909
|
X.font=(8.5*z)+'px '+mono(); X.fillStyle='rgba(200,185,150,0.9)'; X.fillText(sub,cx,cy+11*z);
|
|
822
910
|
}
|
|
911
|
+
function padMark(bx,by,z){ X.strokeStyle='rgba(255,255,255,0.10)'; X.lineWidth=1; X.beginPath(); X.ellipse(bx,by,15*z,7*z,0,0,7); X.stroke(); }
|
|
823
912
|
function drawHub(n){
|
|
824
|
-
var p=
|
|
825
|
-
X.fillStyle='rgba(
|
|
913
|
+
var p=iso(n.wx,n.wy), z=cam.z, bob=Math.sin((performance.now()-t0)/700)*2*z, bx=p.x, by=p.y-6*z-bob;
|
|
914
|
+
X.fillStyle='rgba(0,0,0,0.3)'; X.beginPath(); X.ellipse(bx,p.y+2*z,16*z,7*z,0,0,7); X.fill();
|
|
915
|
+
X.fillStyle='rgba(40,34,58,0.92)'; X.strokeStyle=getVar('--gold-bright'); X.lineWidth=1.2;
|
|
826
916
|
X.beginPath(); X.rect(bx-16*z,by-40*z,32*z,44*z); X.fill(); X.stroke();
|
|
827
917
|
for(var i=0;i<4;i++){ X.fillStyle=i%2?getVar('--ok'):getVar('--gold-bright'); X.globalAlpha=0.4+0.6*Math.abs(Math.sin((performance.now()-t0)/300+i)); X.fillRect(bx-11*z,by-34*z+i*9*z,22*z,4*z); X.globalAlpha=1; }
|
|
828
918
|
X.strokeStyle=getVar('--gold'); X.beginPath(); X.moveTo(bx,by-40*z); X.lineTo(bx,by-52*z); X.stroke();
|
|
829
919
|
X.fillStyle=getVar('--gold-bright'); X.beginPath(); X.arc(bx,by-53*z,3*z,0,7); X.fill();
|
|
830
|
-
label(bx,p.y+
|
|
920
|
+
label(bx,p.y+16*z,n.name,getVar('--gold-bright'), (n.connected?'HUB · '+Math.round(n.load)+'%':'offline'));
|
|
831
921
|
n._hit={x:bx-20*z,y:by-54*z,w:40*z,h:80*z};
|
|
832
922
|
}
|
|
833
923
|
function drawRobot(n){
|
|
834
|
-
var p=
|
|
835
|
-
ring(bx,p.y+
|
|
836
|
-
X.fillStyle='rgba(0,0,0,0.
|
|
837
|
-
var col=n.connected?getVar('--gold-bright'):'rgba(150,
|
|
838
|
-
X.
|
|
839
|
-
X.
|
|
840
|
-
X.beginPath(); X.roundRect(bx-
|
|
841
|
-
X.
|
|
842
|
-
X.
|
|
843
|
-
X.
|
|
844
|
-
|
|
845
|
-
|
|
924
|
+
var p=iso(n.wx,n.wy), z=cam.z, bob=n.connected?Math.sin((performance.now()-t0)/500+n.wx)*2.3*z:0, bx=p.x, by=p.y-4*z-bob;
|
|
925
|
+
if(n.connected) ring(bx,p.y+3*z,18*z,n.load/100,loadColor(n.load)); else padMark(bx,p.y+3*z,z);
|
|
926
|
+
X.fillStyle='rgba(0,0,0,0.3)'; X.beginPath(); X.ellipse(bx,p.y+3*z,13*z,6*z,0,0,7); X.fill();
|
|
927
|
+
var col=n.connected?getVar('--gold-bright'):'rgba(150,143,120,0.5)';
|
|
928
|
+
X.globalAlpha=n.connected?1:0.7;
|
|
929
|
+
X.fillStyle='rgba(24,20,38,0.92)'; X.strokeStyle=col; X.lineWidth=1.6;
|
|
930
|
+
X.beginPath(); X.roundRect(bx-11*z,by-24*z,22*z,25*z,5*z); X.fill(); X.stroke();
|
|
931
|
+
X.beginPath(); X.roundRect(bx-8*z,by-37*z,16*z,14*z,4*z); X.fillStyle='rgba(30,24,44,0.95)'; X.fill(); X.stroke();
|
|
932
|
+
X.fillStyle=n.connected?getVar('--ok'):'#655'; X.beginPath(); X.arc(bx-3.5*z,by-30*z,2*z,0,7); X.arc(bx+3.5*z,by-30*z,2*z,0,7); X.fill();
|
|
933
|
+
X.strokeStyle=col; X.beginPath(); X.moveTo(bx,by-37*z); X.lineTo(bx,by-44*z); X.stroke();
|
|
934
|
+
X.fillStyle=n.connected?'#FF5B4A':'#655'; X.beginPath(); X.arc(bx,by-45*z,2*z,0,7); X.fill();
|
|
935
|
+
X.globalAlpha=1;
|
|
936
|
+
var sub=n.connected?((n.sub&&n.sub!==n.name?n.sub+' · ':'')+Math.round(n.load)+'%'):'not joined';
|
|
937
|
+
label(bx,p.y+17*z,n.label,col,sub);
|
|
938
|
+
n._hit={x:bx-15*z,y:by-47*z,w:30*z,h:60*z};
|
|
846
939
|
}
|
|
847
940
|
function drawPeer(n){
|
|
848
|
-
var p=
|
|
849
|
-
|
|
941
|
+
var p=iso(n.wx,n.wy), z=cam.z, bx=p.x, by=p.y-3*z;
|
|
942
|
+
X.fillStyle='rgba(0,0,0,0.3)'; X.beginPath(); X.ellipse(bx,p.y+2*z,13*z,6*z,0,0,7); X.fill();
|
|
943
|
+
var col=n.connected?getVar('--info'):'rgba(150,143,120,0.5)';
|
|
850
944
|
X.fillStyle='rgba(24,22,40,0.9)'; X.strokeStyle=col; X.lineWidth=1.4;
|
|
851
945
|
X.beginPath(); X.roundRect(bx-13*z,by-22*z,26*z,22*z,4*z); X.fill(); X.stroke();
|
|
852
946
|
X.fillStyle=col; X.globalAlpha=0.5+0.5*Math.abs(Math.sin((performance.now()-t0)/500)); X.fillRect(bx-8*z,by-14*z,16*z,3*z); X.globalAlpha=1;
|
|
853
|
-
label(bx,p.y+
|
|
947
|
+
label(bx,p.y+15*z,n.label,col,(n.connected?n.sub:'offline'));
|
|
854
948
|
n._hit={x:bx-15*z,y:by-24*z,w:30*z,h:42*z};
|
|
855
949
|
}
|
|
856
950
|
|
|
857
951
|
function frame(){
|
|
858
952
|
if(!running) return;
|
|
859
953
|
X.clearRect(0,0,W,H);
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
for(var
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
954
|
+
drawGround();
|
|
955
|
+
// depth-sort palms + nodes so nearer objects overlap farther ones
|
|
956
|
+
var objs=[];
|
|
957
|
+
for(var pi=0;pi<PALMS.length;pi++){ var w=W2(PALMS[pi][0],PALMS[pi][1]); objs.push({wx:w[0],wy:w[1],palm:true}); }
|
|
958
|
+
for(var ni=0;ni<nodes.length;ni++){ objs.push({wx:nodes[ni].wx,wy:nodes[ni].wy,n:nodes[ni]}); }
|
|
959
|
+
objs.sort(function(a,b){ return (a.wx+a.wy)-(b.wx+b.wy); });
|
|
960
|
+
objs.forEach(function(o){ if(o.palm) drawPalm(o.wx,o.wy); else { var n=o.n; if(n.kind==='hub')drawHub(n); else if(n.kind==='robot')drawRobot(n); else drawPeer(n); } });
|
|
961
|
+
for(var i=orbs.length-1;i>=0;i--){ var o2=orbs[i]; o2.t+=0.024; if(o2.t>=1){orbs.splice(i,1);continue;}
|
|
962
|
+
var a=iso(o2.a.wx,o2.a.wy), b=iso(o2.b.wx,o2.b.wy), e=o2.t<.5?2*o2.t*o2.t:-1+(4-2*o2.t)*o2.t;
|
|
963
|
+
var x=a.x+(b.x-a.x)*e, y=a.y+(b.y-a.y)*e-Math.sin(o2.t*Math.PI)*40*cam.z;
|
|
964
|
+
X.beginPath(); X.arc(x,y,4*cam.z,0,7); X.fillStyle=o2.color; X.shadowColor=o2.color; X.shadowBlur=12; X.fill(); X.shadowBlur=0;
|
|
867
965
|
}
|
|
868
966
|
for(var j=pulses.length-1;j>=0;j--){ var pu=pulses[j]; pu.t+=0.02; if(pu.t>=1){pulses.splice(j,1);continue;}
|
|
869
|
-
var pp=iso(pu.n.
|
|
967
|
+
var pp=iso(pu.n.wx,pu.n.wy), yy=pp.y-56*cam.z-pu.t*24*cam.z;
|
|
870
968
|
X.globalAlpha=1-pu.t; X.font='700 '+(15*cam.z)+'px '+mono(); X.textAlign='center'; X.fillStyle=getVar('--gold-bright'); X.fillText(pu.g,pp.x,yy);
|
|
871
|
-
X.beginPath(); X.arc(pp.x,pp.y-
|
|
969
|
+
X.beginPath(); X.arc(pp.x,pp.y-18*cam.z,(10+pu.t*22)*cam.z,0,7); X.strokeStyle='rgba(255,214,112,'+(1-pu.t)+')'; X.lineWidth=2; X.stroke(); X.globalAlpha=1;
|
|
872
970
|
}
|
|
873
971
|
raf=requestAnimationFrame(frame);
|
|
874
972
|
}
|
|
875
|
-
function faint(gx,gy){ var p=iso(gx,gy),z=cam.z,hw=TW/2*z,hh=TH/2*z;
|
|
876
|
-
X.beginPath(); X.moveTo(p.x,p.y-hh); X.lineTo(p.x+hw,p.y); X.lineTo(p.x,p.y+hh); X.lineTo(p.x-hw,p.y); X.closePath();
|
|
877
|
-
X.strokeStyle='rgba(255,255,255,0.045)'; X.lineWidth=1; X.stroke();
|
|
878
|
-
}
|
|
879
973
|
|
|
880
974
|
function connectStream(){
|
|
881
975
|
if(es) return;
|
|
@@ -887,8 +981,15 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
887
981
|
}catch(e){ es=null; }
|
|
888
982
|
}
|
|
889
983
|
|
|
984
|
+
var sdkLoading=false;
|
|
985
|
+
function loadSDK(){
|
|
986
|
+
if(window.LivekitClient || sdkLoading) return; sdkLoading=true;
|
|
987
|
+
var s=document.createElement('script'); s.src='/vendor/livekit-client.umd.js'+QS; s.async=true;
|
|
988
|
+
s.onerror=function(){ sdkLoading=false; }; document.head.appendChild(s);
|
|
989
|
+
}
|
|
890
990
|
function activate(){
|
|
891
991
|
ensure();
|
|
992
|
+
loadSDK();
|
|
892
993
|
if(lastStatus) onStatus(lastStatus);
|
|
893
994
|
resize();
|
|
894
995
|
connectStream();
|
|
@@ -902,9 +1003,9 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
902
1003
|
var n=byId[id]; if(!n) return; drawerId=id;
|
|
903
1004
|
$('pk-drawer').classList.add('open');
|
|
904
1005
|
$('pk-av').textContent = n.kind==='hub'?'🖥':(n.kind==='robot'?'🤖':'🧩');
|
|
905
|
-
$('pk-name').textContent=n.name;
|
|
906
|
-
$('pk-sub').textContent=n.
|
|
907
|
-
var st=$('pk-state'); st.textContent=n.connected?'live':'
|
|
1006
|
+
$('pk-name').textContent=n.label||n.name;
|
|
1007
|
+
$('pk-sub').textContent = n.connected ? (n.name+' · '+(n.platform||'?')+' · v'+n.version+(n.site?(' · '+n.site):'')) : 'pad reserved · not joined';
|
|
1008
|
+
var st=$('pk-state'); st.textContent=n.connected?'live':'offline'; st.className='pk-pill '+(n.connected?'live':'idle');
|
|
908
1009
|
buildDrawer(n);
|
|
909
1010
|
pollRoboparkQuiet(); // fill session telemetry right away instead of waiting for the tick
|
|
910
1011
|
if(drawerTimer) clearInterval(drawerTimer);
|
|
@@ -916,6 +1017,8 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
916
1017
|
if(meterRAF)cancelAnimationFrame(meterRAF); meterRAF=null;
|
|
917
1018
|
if(camAbort){ try{camAbort.abort();}catch(e){} camAbort=null; }
|
|
918
1019
|
if(drawerTimer){ clearInterval(drawerTimer); drawerTimer=null; }
|
|
1020
|
+
if(lkRoom){ try{ lkRoom.disconnect(); }catch(e){} lkRoom=null; }
|
|
1021
|
+
micAnalyser=null;
|
|
919
1022
|
}
|
|
920
1023
|
function pollRoboparkQuiet(){
|
|
921
1024
|
fetch('/robopark/api/telemetry'+QS,{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){
|
|
@@ -926,6 +1029,12 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
926
1029
|
}
|
|
927
1030
|
function buildDrawer(n){
|
|
928
1031
|
var b=$('pk-body');
|
|
1032
|
+
if(n.kind==='robot' && !n.connected){
|
|
1033
|
+
b.innerHTML =
|
|
1034
|
+
sect('🅿 park pad · '+esc(n.label), '<div class="pk-note" style="opacity:1;color:var(--muted)">No robot has joined this pad yet. When a node named <b>'+esc((n.telemKeys[1]||n.label).toString())+'</b> connects to the mesh, its live camera, mic, speaker, and telemetry appear here automatically.</div>')
|
|
1035
|
+
+ sect('📍 location', '<div class="pk-grid2"><div class="pk-kv"><div class="v">'+esc(n.label)+'</div><div class="l">pad</div></div><div class="pk-kv"><div class="v" style="color:var(--muted)">idle</div><div class="l">status</div></div></div>');
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
929
1038
|
if(n.kind==='robot'){
|
|
930
1039
|
b.innerHTML =
|
|
931
1040
|
sect('👁 camera feed',
|
|
@@ -955,6 +1064,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
955
1064
|
var lv=$('pk-tv-load'); if(lv) lv.textContent=Math.round(n.load)+'%';
|
|
956
1065
|
var hc=$('pk-hw-conn'); if(hc) hc.style.width=n.connected?'100%':'12%';
|
|
957
1066
|
var row=roboparkByName[n.name];
|
|
1067
|
+
if(!row){ var keys=n.telemKeys||[]; for(var rk in roboparkByName){ var lk=rk.toLowerCase(); for(var ki=0;ki<keys.length;ki++){ if(keys[ki] && (lk===keys[ki]||lk.indexOf(keys[ki])>-1)){ row=roboparkByName[rk]; break; } } if(row) break; } }
|
|
958
1068
|
if(row){
|
|
959
1069
|
set('pk-tv-trig', row.trigger_count!=null?row.trigger_count:0);
|
|
960
1070
|
set('pk-tv-sess', row.sessions_total!=null?row.sessions_total:0);
|
|
@@ -969,30 +1079,54 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
969
1079
|
|
|
970
1080
|
// ── cam: target a LiveKit track; fall back to a styled preview ──
|
|
971
1081
|
function startCam(n){
|
|
972
|
-
var note=$('pk-camnote')
|
|
1082
|
+
var note=$('pk-camnote');
|
|
973
1083
|
camAbort = ('AbortController' in window)?new AbortController():null;
|
|
974
|
-
|
|
1084
|
+
idleCam('connecting');
|
|
1085
|
+
// scheduler mints a subscribe-only viewer token for the robot's current
|
|
1086
|
+
// LiveKit room; the Pi already publishes 'cam' (video) + 'mic' (audio).
|
|
975
1087
|
fetch('/robopark/api/robots/'+encodeURIComponent(n.name)+'/stream'+QS, {cache:'no-store', signal:camAbort?camAbort.signal:undefined})
|
|
976
|
-
.then(function(r){
|
|
1088
|
+
.then(function(r){ return r.json().catch(function(){ throw new Error('bad response'); }); })
|
|
977
1089
|
.then(function(info){
|
|
978
|
-
if(info && info.
|
|
979
|
-
|
|
980
|
-
|
|
1090
|
+
if(info && info.active===false){ idleCam(info.reason||'idle'); return; }
|
|
1091
|
+
if(info && info.url && info.token){
|
|
1092
|
+
if(window.LivekitClient){ attachLiveKit(info, n); }
|
|
1093
|
+
else { idleCam('no-sdk'); }
|
|
1094
|
+
} else { idleCam('idle'); }
|
|
981
1095
|
})
|
|
982
|
-
.catch(function(){
|
|
1096
|
+
.catch(function(){ idleCam('unreachable'); });
|
|
1097
|
+
}
|
|
1098
|
+
function idleCam(reason){
|
|
1099
|
+
var cc=$('pk-camcv'), msg={connecting:'connecting to live session…', idle:'robot idle · no live session', production_mode_off:'production mode off', 'no-sdk':'live session up · loading video…', unreachable:'scheduler unreachable'}[reason]||'no live feed';
|
|
1100
|
+
if(cc&&cc.getContext){ var cx=cc.getContext('2d'); cx.fillStyle='#0b0d12'; cx.fillRect(0,0,cc.width,cc.height); cx.strokeStyle='rgba(255,255,255,0.05)'; for(var y=0;y<cc.height;y+=4){cx.beginPath();cx.moveTo(0,y);cx.lineTo(cc.width,y);cx.stroke();} cx.fillStyle='#5a6a80'; cx.font='11px monospace'; cx.textAlign='center'; cx.fillText(msg, cc.width/2, cc.height/2); }
|
|
1101
|
+
var res=$('pk-camres'); if(res) res.textContent = reason==='idle'?'idle':'—';
|
|
1102
|
+
var note=$('pk-camnote'); if(note) note.textContent = reason==='idle'?'the feed goes live automatically when the robot opens a session':msg;
|
|
983
1103
|
}
|
|
984
1104
|
function attachLiveKit(info, n){
|
|
985
|
-
var
|
|
1105
|
+
var note=$('pk-camnote'), res=$('pk-camres');
|
|
1106
|
+
try{
|
|
1107
|
+
var LK=window.LivekitClient; lkRoom=new LK.Room();
|
|
1108
|
+
lkRoom.on(LK.RoomEvent.TrackSubscribed, function(track){
|
|
1109
|
+
if(track.kind==='video'){
|
|
1110
|
+
var el=track.attach(); el.setAttribute('playsinline',''); el.muted=true; el.autoplay=true;
|
|
1111
|
+
var cv=$('pk-camcv'); if(cv&&cv.parentNode) cv.parentNode.replaceChild(el, cv);
|
|
1112
|
+
if(res) res.textContent='LIVE'; if(note) note.textContent='live camera via LiveKit · '+esc(info.room);
|
|
1113
|
+
} else if(track.kind==='audio'){ hookMicAnalyser(track); track.attach(); }
|
|
1114
|
+
});
|
|
1115
|
+
lkRoom.on(LK.RoomEvent.Disconnected, function(){ micAnalyser=null; if(note) note.textContent='session ended'; if(res) res.textContent='—'; });
|
|
1116
|
+
lkRoom.connect(info.url, info.token).then(function(){ if(note) note.textContent='connected · waiting for camera track…'; })
|
|
1117
|
+
.catch(function(){ idleCam('unreachable'); });
|
|
1118
|
+
}catch(e){ idleCam('no-sdk'); }
|
|
1119
|
+
}
|
|
1120
|
+
// Real mic level: tap the robot's subscribed 'mic' audio track with a Web
|
|
1121
|
+
// Audio analyser and read RMS in startMeters (no invented levels).
|
|
1122
|
+
function hookMicAnalyser(track){
|
|
986
1123
|
try{
|
|
987
|
-
var
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
});
|
|
994
|
-
}).catch(function(){ if(note) note.textContent='LiveKit connect failed — preview'; previewCam(n); });
|
|
995
|
-
}catch(e){ if(note) note.textContent='LiveKit error — preview'; previewCam(n); }
|
|
1124
|
+
var AC=window.AudioContext||window.webkitAudioContext; if(!AC) return;
|
|
1125
|
+
audioCtx=audioCtx||new AC();
|
|
1126
|
+
var ms=track.mediaStreamTrack; if(!ms) return;
|
|
1127
|
+
var src=audioCtx.createMediaStreamSource(new MediaStream([ms]));
|
|
1128
|
+
var an=audioCtx.createAnalyser(); an.fftSize=256; src.connect(an); micAnalyser=an;
|
|
1129
|
+
}catch(e){}
|
|
996
1130
|
}
|
|
997
1131
|
function previewCam(n){
|
|
998
1132
|
var cc=$('pk-camcv'); if(!cc||!cc.getContext) return; var cx=cc.getContext('2d'), w=cc.width, h=cc.height, s0=performance.now();
|
|
@@ -1015,18 +1149,19 @@ button.act:disabled{opacity:0.5;cursor:default;}
|
|
|
1015
1149
|
function bars(el,n){ el.innerHTML=''; for(var i=0;i<n;i++){ var b=document.createElement('i'); el.appendChild(b); } return el.querySelectorAll('i'); }
|
|
1016
1150
|
function startMeters(n){
|
|
1017
1151
|
var mb=$('pk-micbars'), sb=$('pk-spkbars'); if(!mb) return;
|
|
1018
|
-
var mi=bars(mb,18), si=bars(sb,18), sw=$('pk-spkwave'), swx=sw.getContext('2d'),
|
|
1019
|
-
// Preview envelopes until the robot audio plugin streams real levels.
|
|
1152
|
+
var mi=bars(mb,18), si=bars(sb,18), sw=$('pk-spkwave'), swx=sw?sw.getContext('2d'):null, buf=new Uint8Array(128);
|
|
1020
1153
|
(function loop(){ if(!$('pk-micbars')||drawerId==null) return;
|
|
1021
|
-
|
|
1022
|
-
var
|
|
1023
|
-
for(var
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
for(var
|
|
1154
|
+
// MIC — real RMS from the subscribed LiveKit mic track (else no signal).
|
|
1155
|
+
var lvl=0;
|
|
1156
|
+
if(micAnalyser){ micAnalyser.getByteTimeDomainData(buf); var sum=0; for(var q=0;q<buf.length;q++){ var v=(buf[q]-128)/128; sum+=v*v; } lvl=Math.min(1, Math.sqrt(sum/buf.length)*3.4); }
|
|
1157
|
+
for(var i=0;i<mi.length;i++){ var on=i<lvl*mi.length; mi[i].style.height=(3+(on?lvl*13:0))+'px'; mi[i].style.background=on?(i<mi.length*0.7?'var(--ok)':i<mi.length*0.9?'var(--warn)':'var(--bad)'):'var(--glass-border)'; }
|
|
1158
|
+
set('pk-micval', micAnalyser?Math.round(lvl*100)+'%':'—');
|
|
1159
|
+
set('pk-micstate', micAnalyser?(lvl>0.12?'live · hearing audio':'live · quiet'):'no live mic track');
|
|
1160
|
+
// SPEAKER — the Pi plays TTS out its own speaker; it is not published as a
|
|
1161
|
+
// track, so report status honestly instead of inventing a level.
|
|
1162
|
+
for(var j=0;j<si.length;j++){ si[j].style.height='3px'; si[j].style.background='var(--glass-border)'; }
|
|
1163
|
+
set('pk-spkval','—'); set('pk-spkstate','TTS plays on the robot speaker (not streamed)');
|
|
1164
|
+
if(swx){ swx.clearRect(0,0,sw.width,sw.height); swx.strokeStyle='rgba(255,255,255,0.12)'; swx.lineWidth=1.4; swx.beginPath(); for(var x=0;x<sw.width;x++){ var yy=sw.height/2+Math.sin(x*0.1)*1.1; if(x===0)swx.moveTo(x,yy); else swx.lineTo(x,yy); } swx.stroke(); }
|
|
1030
1165
|
meterRAF=requestAnimationFrame(loop);
|
|
1031
1166
|
})();
|
|
1032
1167
|
}
|
|
@@ -4,6 +4,7 @@ import { PeerMesh } from './peer-mesh.js';
|
|
|
4
4
|
import { EventBridge } from './event-bridge.js';
|
|
5
5
|
import { ConfigSync } from './config-sync.js';
|
|
6
6
|
import { AutoUpdate } from './auto-update.js';
|
|
7
|
+
import { performSelfUpdate } from './self-update.js';
|
|
7
8
|
import { ComputeRouter } from './compute-router.js';
|
|
8
9
|
import { TelemetryCollector, loadFromSnapshot, selfNodeStatus, nodeStatusFromPeer } from './telemetry.js';
|
|
9
10
|
import { buildManifest } from './node-identity.js';
|
|
@@ -58,6 +59,17 @@ export class Federation {
|
|
|
58
59
|
version: deps.options.version,
|
|
59
60
|
logger: deps.logger,
|
|
60
61
|
enabled: deps.options.autoUpdate,
|
|
62
|
+
// Apply the announced update (npm install + supervised restart). Only ever
|
|
63
|
+
// reached for announces from a hub/relay with auto-update enabled — see
|
|
64
|
+
// AutoUpdate.onAnnounce. Supervision is read from INFINICODE_SUPERVISED.
|
|
65
|
+
applyUpdate: async (announce) => {
|
|
66
|
+
await performSelfUpdate({
|
|
67
|
+
version: announce.version,
|
|
68
|
+
channel: announce.channel === 'git' ? 'git' : 'npm',
|
|
69
|
+
currentVersion: deps.options.version,
|
|
70
|
+
logger: deps.logger,
|
|
71
|
+
});
|
|
72
|
+
},
|
|
61
73
|
});
|
|
62
74
|
}
|
|
63
75
|
get identity() {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Logger } from '../types.js';
|
|
2
|
+
export interface SelfUpdateOptions {
|
|
3
|
+
/** Target version — 'latest' or a semver like '2.6.1'. Default 'latest'. */
|
|
4
|
+
version?: string;
|
|
5
|
+
/** npm package to install. Default 'infinicode' (override via INFINICODE_UPDATE_PKG). */
|
|
6
|
+
pkg?: string;
|
|
7
|
+
channel?: 'npm' | 'git' | 'manual';
|
|
8
|
+
/** The version currently running, for the from→to report. */
|
|
9
|
+
currentVersion?: string;
|
|
10
|
+
/** Node runs under a supervisor → safe to exit-for-restart. Falls back to
|
|
11
|
+
* INFINICODE_SUPERVISED=1. */
|
|
12
|
+
supervised?: boolean;
|
|
13
|
+
/** Schedule a restart after a successful install. Defaults to `supervised`. */
|
|
14
|
+
restart?: boolean;
|
|
15
|
+
/** Delay before exiting so the command reply flushes back over the mesh. */
|
|
16
|
+
restartDelayMs?: number;
|
|
17
|
+
logger?: Logger;
|
|
18
|
+
}
|
|
19
|
+
export interface SelfUpdateResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
from?: string;
|
|
22
|
+
to: string;
|
|
23
|
+
restarting: boolean;
|
|
24
|
+
message: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function performSelfUpdate(opts?: SelfUpdateOptions): Promise<SelfUpdateResult>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenKernel — Self Update
|
|
3
|
+
*
|
|
4
|
+
* The apply side of mesh updates: install a newer npm release of this package
|
|
5
|
+
* globally, then (if the node runs under a process supervisor) exit so the
|
|
6
|
+
* supervisor relaunches it on the new version. Isolated here so the shell-out
|
|
7
|
+
* lives in exactly one place — the `/update` command and the AutoUpdate gossip
|
|
8
|
+
* path both call this, nothing else shells out for updates.
|
|
9
|
+
*
|
|
10
|
+
* Restart policy is deliberately conservative: a node only exits-for-restart
|
|
11
|
+
* when it is explicitly marked supervised (`serve --supervised`, which sets
|
|
12
|
+
* INFINICODE_SUPERVISED=1). An unsupervised node still installs the new version
|
|
13
|
+
* on disk but stays up on the old one, and says so — killing a hand-started
|
|
14
|
+
* process would just take the node offline with nothing to bring it back.
|
|
15
|
+
*/
|
|
16
|
+
import { execa } from 'execa';
|
|
17
|
+
const SAFE_VERSION = /^[A-Za-z0-9._-]+$/; // 'latest', '2.6.1', 'next' — never a shell metachar
|
|
18
|
+
const SAFE_PKG = /^[@A-Za-z0-9._/-]+$/;
|
|
19
|
+
export async function performSelfUpdate(opts = {}) {
|
|
20
|
+
const version = (opts.version ?? 'latest').trim();
|
|
21
|
+
const pkg = (opts.pkg ?? process.env.INFINICODE_UPDATE_PKG ?? 'infinicode').trim();
|
|
22
|
+
const channel = opts.channel ?? 'npm';
|
|
23
|
+
const supervised = opts.supervised ?? process.env.INFINICODE_SUPERVISED === '1';
|
|
24
|
+
const restart = opts.restart ?? supervised;
|
|
25
|
+
const info = (m) => (opts.logger ? opts.logger.info(m) : console.log(' ' + m));
|
|
26
|
+
const warn = (m) => (opts.logger ? opts.logger.warn(m) : console.log(' ' + m));
|
|
27
|
+
if (channel !== 'npm') {
|
|
28
|
+
return { ok: false, to: version, restarting: false, message: `channel "${channel}" is not supported for self-update yet (npm only).` };
|
|
29
|
+
}
|
|
30
|
+
if (!SAFE_VERSION.test(version) || !SAFE_PKG.test(pkg)) {
|
|
31
|
+
return { ok: false, to: version, restarting: false, message: `refused: unsafe update spec "${pkg}@${version}".` };
|
|
32
|
+
}
|
|
33
|
+
const spec = `${pkg}@${version}`;
|
|
34
|
+
info(`[update] installing ${spec} (global)…`);
|
|
35
|
+
try {
|
|
36
|
+
// Use npm.cmd on Windows so we never need a shell (no injection surface).
|
|
37
|
+
const bin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
38
|
+
const res = await execa(bin, ['install', '-g', spec], { all: true, timeout: 300_000 });
|
|
39
|
+
const tail = String(res.all ?? res.stdout ?? '').split('\n').filter(Boolean).slice(-2).join(' ').trim();
|
|
40
|
+
info(`[update] installed ${spec}${tail ? ' — ' + tail : ''}`);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const detail = err;
|
|
44
|
+
const message = (detail.all || detail.shortMessage || detail.message || String(err)).replace(/\s+/g, ' ').slice(0, 300);
|
|
45
|
+
warn(`[update] npm install failed: ${message}`);
|
|
46
|
+
return { ok: false, from: opts.currentVersion, to: version, restarting: false, message: `npm install -g ${spec} failed: ${message}` };
|
|
47
|
+
}
|
|
48
|
+
let restarting = false;
|
|
49
|
+
if (restart && supervised) {
|
|
50
|
+
restarting = true;
|
|
51
|
+
const delay = opts.restartDelayMs ?? 2000;
|
|
52
|
+
info(`[update] restarting in ${delay}ms for the supervisor to relaunch on ${version}…`);
|
|
53
|
+
const timer = setTimeout(() => process.exit(0), delay);
|
|
54
|
+
timer.unref?.();
|
|
55
|
+
}
|
|
56
|
+
else if (restart && !supervised) {
|
|
57
|
+
warn('[update] installed but NOT restarting — node is not marked supervised (start serve with --supervised). Restart it to activate.');
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
from: opts.currentVersion,
|
|
62
|
+
to: version,
|
|
63
|
+
restarting,
|
|
64
|
+
message: restarting
|
|
65
|
+
? `updated to ${spec} — restarting now; the supervisor will relaunch on the new version.`
|
|
66
|
+
: `updated to ${spec} on disk — restart the node to activate${supervised ? '' : ' (not supervised, so no auto-restart)'}.`,
|
|
67
|
+
};
|
|
68
|
+
}
|