infinicode 2.8.8 → 2.8.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.
@@ -697,7 +697,11 @@ button.act:disabled{opacity:0.5;cursor:default;}
697
697
  if (name==='park') Park.activate(); else Park.deactivate();
698
698
  }
699
699
  var initHash = location.hash.replace('#','');
700
- if (initHash==='robopark'||initHash==='park') selectTab(initHash);
700
+ // Defer tab init until Park module is defined (same outer IIFE, later in source).
701
+ function initTab(){
702
+ if (initHash==='robopark'||initHash==='park') selectTab(initHash);
703
+ }
704
+ // initTab() is called at the very bottom of this IIFE, after Park is assigned.
701
705
 
702
706
  function robotsFrom(d){
703
707
  if (Array.isArray(d)) return d;
@@ -822,21 +826,32 @@ button.act:disabled{opacity:0.5;cursor:default;}
822
826
  var drawerId=null, camRAF=null, meterRAF=null, camAbort=null, drawerTimer=null;
823
827
  var previewOn=false, previewTimer=null; // on-demand operator camera preview
824
828
  var lkRoom=null, micAnalyser=null, audioCtx=null;
829
+ var imgs={}; // preloaded robot concept avatars
825
830
  var TW=44, TH=22, PARK_W=15, PARK_D=8;
826
- // The real park (from the aerial): named robot pads at their ground
827
- // positions, plus palms, pond, and the street. A mesh node binds to a pad
828
- // when its display name matches the pad keywords (robobmw VIXEN/BMW).
831
+ // The real park (from the aerial photo): named robot pads at their ground
832
+ // positions along the long Shenzhen Bay promenade plaza, plus palms, pond,
833
+ // street and stage. A mesh node binds to a pad when its display name matches
834
+ // the pad keywords (robobmw → VIXEN/BMW).
829
835
  var SLOTS=[
830
- {label:'VOLT', keys:['volt'], nx:0.30, ny:0.22},
831
- {label:'TESLA', keys:['tesla'], nx:0.44, ny:0.36},
832
- {label:'JAGUAR', keys:['jaguar','jag'], nx:0.60, ny:0.40},
833
- {label:'VIXEN', keys:['vixen','bmw'], nx:0.55, ny:0.66, sub:'BMW'},
834
- {label:'PANDA', keys:['panda'], nx:0.84, ny:0.58}
836
+ {label:'VOLT', keys:['volt'], nx:0.18, ny:0.26, img:'/static/robopark/volt.png'},
837
+ {label:'TESLA', keys:['tesla'], nx:0.38, ny:0.30, img:'/static/robopark/tesla.png'},
838
+ {label:'JAGUAR', keys:['jaguar','jag'], nx:0.58, ny:0.34, img:'/static/robopark/jaguar.png'},
839
+ {label:'VIXEN', keys:['vixen','bmw'], nx:0.54, ny:0.66, sub:'BMW', img:'/static/robopark/bmw.png'},
840
+ {label:'PANDA', keys:['panda'], nx:0.82, ny:0.52, img:'/static/robopark/panda.png'}
841
+ ];
842
+ var HUBSLOT={label:'CONTROL', keys:[], nx:0.06, ny:0.16};
843
+ // Palm positions tuned to the aerial: clusters along the back edge and
844
+ // scattered down the center of the paved strip.
845
+ var PALMS=[
846
+ [0.10,0.38],[0.14,0.55],[0.18,0.72],[0.22,0.30],[0.26,0.48],[0.30,0.66],
847
+ [0.36,0.24],[0.40,0.42],[0.44,0.60],[0.50,0.26],[0.54,0.46],[0.58,0.64],
848
+ [0.66,0.30],[0.70,0.50],[0.74,0.68],[0.80,0.36],[0.84,0.56],[0.90,0.40],[0.94,0.62],
849
+ [0.62,0.80],[0.76,0.84],[0.35,0.82]
835
850
  ];
836
- var HUBSLOT={label:'CONTROL', keys:[], nx:0.09, ny:0.12};
837
- 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]];
838
- var POND={cx:0.33*15, cy:0.99*8, rx:3.1, ry:1.4};
839
- var CARS=[[4,-0.95,'#e0a23c'],[6,-0.8,'#7fb4ff'],[8,-1.0,'#ff7e6e'],[10.5,-0.78,'#d8d8e0'],[12.8,-0.95,'#f2c14e']];
851
+ // Pond / curved water feature at the lower-left of the plaza.
852
+ var POND={cx:0.28*15, cy:0.96*8, rx:3.4, ry:1.6};
853
+ // Street traffic along the top road (yellow taxis + private cars).
854
+ var CARS=[[2.2,-0.92,'#f2c14e'],[4.4,-0.78,'#e0a23c'],[6.6,-0.95,'#7fb4ff'],[8.8,-1.02,'#ff7e6e'],[11.0,-0.84,'#d8d8e0'],[13.2,-0.98,'#f2c14e']];
840
855
 
841
856
  function mono(){ return 'ui-monospace,Menlo,Consolas,monospace'; }
842
857
  function W2(nx,ny){ return [nx*PARK_W, ny*PARK_D]; }
@@ -986,17 +1001,39 @@ button.act:disabled{opacity:0.5;cursor:default;}
986
1001
  X.strokeStyle='rgba(232,210,120,0.5)'; X.setLineDash([9*cam.z,9*cam.z]);
987
1002
  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([]);
988
1003
  CARS.forEach(function(c){ drawCar(c[0],c[1],c[2]); });
989
- // paved plaza
990
- var px0=0.4,py0=-0.15,px1=PARK_W-0.4,py1=PARK_D-0.6;
1004
+ // white event tent / stage on the far left of the plaza (matches aerial)
1005
+ drawTent(0.85, 0.18);
1006
+ // paved plaza — long rectangular promenade matching the aerial footprint
1007
+ var px0=0.2,py0=-0.10,px1=PARK_W-0.2,py1=PARK_D-0.45;
991
1008
  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)');
992
1009
  poly([[px0,py0],[px1,py0],[px1,py1],[px0,py1]], pg, 'rgba(255,255,255,0.07)',1);
1010
+ // plaza grid (tile joints)
993
1011
  X.strokeStyle='rgba(255,255,255,0.05)'; X.lineWidth=1;
994
- 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(); }
995
- 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(); }
996
- // pond
997
- 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)]); }
1012
+ for(var gx=Math.ceil(px0);gx<px1;gx+=1.5){ 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(); }
1013
+ for(var gy=Math.ceil(py0);gy<py1;gy+=1.5){ 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(); }
1014
+ // curved pond / water feature at bottom-left
1015
+ var pts=[]; for(var t=0;t<Math.PI*2;t+=Math.PI/12){ pts.push([POND.cx+POND.rx*Math.cos(t), POND.cy+POND.ry*Math.sin(t)*0.7]); }
998
1016
  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)');
999
1017
  poly(pts, wg, 'rgba(130,205,235,0.32)',1.5);
1018
+ // winding footpath along the pond
1019
+ X.strokeStyle='rgba(210,190,150,0.35)'; X.lineWidth=2.5*cam.z; X.setLineDash([]);
1020
+ var pathPts=[[0.15*PARK_W,0.82*PARK_D],[0.22*PARK_W,0.88*PARK_D],[0.32*PARK_W,0.92*PARK_D],[0.45*PARK_W,0.95*PARK_D]];
1021
+ X.beginPath();
1022
+ for(var pi=0;pi<pathPts.length;pi++){ var pp=iso(pathPts[pi][0],pathPts[pi][1]); if(pi===0)X.moveTo(pp.x,pp.y); else X.lineTo(pp.x,pp.y); }
1023
+ X.stroke();
1024
+ }
1025
+ function drawTent(wx,wy){
1026
+ var p=iso(wx,wy), z=cam.z;
1027
+ // canopy shadow
1028
+ X.fillStyle='rgba(0,0,0,0.28)'; X.beginPath(); X.ellipse(p.x,p.y+4*z,22*z,8*z,0,0,7); X.fill();
1029
+ // white peaked tent
1030
+ X.fillStyle='rgba(235,235,240,0.92)'; X.strokeStyle='rgba(180,180,190,0.6)'; X.lineWidth=1;
1031
+ X.beginPath(); X.moveTo(p.x-20*z,p.y); X.lineTo(p.x,p.y-26*z); X.lineTo(p.x+20*z,p.y); X.closePath(); X.fill(); X.stroke();
1032
+ X.beginPath(); X.moveTo(p.x-20*z,p.y); X.lineTo(p.x-12*z,p.y+16*z); X.lineTo(p.x+12*z,p.y+16*z); X.lineTo(p.x+20*z,p.y); X.closePath(); X.fill(); X.stroke();
1033
+ // support poles
1034
+ X.strokeStyle='rgba(160,160,170,0.7)'; X.lineWidth=1.5*z;
1035
+ X.beginPath(); X.moveTo(p.x-10*z,p.y+16*z); X.lineTo(p.x-10*z,p.y+5*z); X.stroke();
1036
+ X.beginPath(); X.moveTo(p.x+10*z,p.y+16*z); X.lineTo(p.x+10*z,p.y+5*z); X.stroke();
1000
1037
  }
1001
1038
  function drawCar(wx,wy,col){
1002
1039
  var p=iso(wx,wy), z=cam.z;
@@ -1039,12 +1076,22 @@ button.act:disabled{opacity:0.5;cursor:default;}
1039
1076
  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();
1040
1077
  var col=n.connected?getVar('--gold-bright'):'rgba(150,143,120,0.5)';
1041
1078
  X.globalAlpha=n.connected?1:0.7;
1042
- X.fillStyle='rgba(24,20,38,0.92)'; X.strokeStyle=col; X.lineWidth=1.6;
1043
- X.beginPath(); X.roundRect(bx-11*z,by-24*z,22*z,25*z,5*z); X.fill(); X.stroke();
1044
- 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();
1045
- 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();
1046
- X.strokeStyle=col; X.beginPath(); X.moveTo(bx,by-37*z); X.lineTo(bx,by-44*z); X.stroke();
1047
- X.fillStyle=n.connected?'#FF5B4A':'#655'; X.beginPath(); X.arc(bx,by-45*z,2*z,0,7); X.fill();
1079
+ var img = n.connected && imgs[n.label];
1080
+ if(img && img.complete && img.naturalWidth>0){
1081
+ var iw=26*z, ih=26*z*img.naturalHeight/img.naturalWidth;
1082
+ X.save();
1083
+ X.beginPath(); X.roundRect(bx-13*z,by-38*z,iw,ih+12*z,6*z); X.clip();
1084
+ X.drawImage(img, bx-13*z, by-44*z, iw, ih+18*z);
1085
+ X.restore();
1086
+ X.strokeStyle=col; X.lineWidth=1.6; X.beginPath(); X.roundRect(bx-13*z,by-38*z,iw,ih+12*z,6*z); X.stroke();
1087
+ } else {
1088
+ X.fillStyle='rgba(24,20,38,0.92)'; X.strokeStyle=col; X.lineWidth=1.6;
1089
+ X.beginPath(); X.roundRect(bx-11*z,by-24*z,22*z,25*z,5*z); X.fill(); X.stroke();
1090
+ 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();
1091
+ 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();
1092
+ X.strokeStyle=col; X.beginPath(); X.moveTo(bx,by-37*z); X.lineTo(bx,by-44*z); X.stroke();
1093
+ X.fillStyle=n.connected?'#FF5B4A':'#655'; X.beginPath(); X.arc(bx,by-45*z,2*z,0,7); X.fill();
1094
+ }
1048
1095
  X.globalAlpha=1;
1049
1096
  var sub=n.connected?((n.sub&&n.sub!==n.name?n.sub+' · ':'')+Math.round(n.load)+'%'):'not joined';
1050
1097
  label(bx,p.y+17*z,n.label,col,sub);
@@ -1100,9 +1147,13 @@ button.act:disabled{opacity:0.5;cursor:default;}
1100
1147
  var s=document.createElement('script'); s.src='/vendor/livekit-client.umd.js'+QS; s.async=true;
1101
1148
  s.onerror=function(){ sdkLoading=false; }; document.head.appendChild(s);
1102
1149
  }
1150
+ function loadImages(){
1151
+ SLOTS.forEach(function(s){ if(!s.img) return; var i=new Image(); i.src=s.img+QS; i.onload=function(){ imgs[s.label]=i; }; i.onerror=function(){ imgs[s.label]=null; }; });
1152
+ }
1103
1153
  function activate(){
1104
1154
  ensure();
1105
1155
  loadSDK();
1156
+ loadImages();
1106
1157
  if(lastStatus) onStatus(lastStatus);
1107
1158
  resize();
1108
1159
  connectStream();
@@ -1319,6 +1370,7 @@ button.act:disabled{opacity:0.5;cursor:default;}
1319
1370
  poll();
1320
1371
  setInterval(poll, 2000);
1321
1372
  setInterval(function(){ if (activeTab==='robopark') pollRobopark(); }, 3000);
1373
+ initTab();
1322
1374
  })();
1323
1375
  </script>
1324
1376
  </body>
@@ -38,6 +38,28 @@ function loadLiveKitUmd() {
38
38
  }
39
39
  return _lkUmd;
40
40
  }
41
+ /** In-memory cache of small static assets embedded next to the dashboard so the
42
+ * Park map can show per-robot concept avatars without external URLs. */
43
+ const _staticCache = {};
44
+ function loadStaticAsset(relPath) {
45
+ if (_staticCache[relPath])
46
+ return _staticCache[relPath];
47
+ try {
48
+ const require = createRequire(import.meta.url);
49
+ // Resolve from dist/kernel/federation/transport-http.js → project root.
50
+ const here = require.resolve('./transport-http.js');
51
+ const base = here.replace(/dist[\\/]kernel[\\/]federation[\\/]transport-http\.js$/, 'static');
52
+ const file = base + '/' + relPath;
53
+ const data = readFileSync(file);
54
+ const ext = file.split('.').pop()?.toLowerCase();
55
+ const type = ext === 'png' ? 'image/png' : ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'application/octet-stream';
56
+ _staticCache[relPath] = { type, data };
57
+ return _staticCache[relPath];
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
41
63
  /** Constant-time string compare — avoids leaking the token via response timing. */
42
64
  function safeEqual(a, b) {
43
65
  const ab = Buffer.from(a);
@@ -152,6 +174,17 @@ export class MeshServer {
152
174
  res.end(this.opts.dashboardHtml);
153
175
  return;
154
176
  }
177
+ // Static assets bundled with the dashboard (robot concept avatars, etc.).
178
+ if (req.method === 'GET' && path.startsWith('/static/')) {
179
+ const asset = loadStaticAsset(path.slice(8));
180
+ if (!asset) {
181
+ res.writeHead(404).end('asset not found');
182
+ return;
183
+ }
184
+ res.writeHead(200, { 'content-type': asset.type, 'cache-control': 'public, max-age=86400' });
185
+ res.end(asset.data);
186
+ return;
187
+ }
155
188
  // LiveKit browser SDK, served from the installed dependency so the Park
156
189
  // tab can subscribe to a robot's live camera track. Loaded by the dashboard
157
190
  // with the ?token= appended (so it passes the auth gate above). Optional:
@@ -1,5 +1,5 @@
1
1
  export interface ServiceArgs {
2
- role: 'hub' | 'robot' | 'control' | 'hub-scheduler';
2
+ role: 'hub' | 'robot' | 'control' | 'hub-scheduler' | 'robot-preview-agent';
3
3
  name: string;
4
4
  command: string;
5
5
  args: string[];
@@ -0,0 +1,7 @@
1
+ export interface EnrollOptions {
2
+ schedulerUrl: string;
3
+ robotId?: string;
4
+ enrollmentToken?: string;
5
+ saveConfig?: boolean;
6
+ }
7
+ export declare function roboparkEnroll(opts: EnrollOptions): Promise<void>;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * RoboPark — `robopark enroll`.
3
+ *
4
+ * Enrolls this machine as a scheduler device. Either uses an explicit
5
+ * enrollment token, or reads it from ~/.robopark/enrollment_token.
6
+ */
7
+ import { spawn } from 'node:child_process';
8
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
9
+ import { homedir, hostname } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import chalk from 'chalk';
12
+ const ROBOPARK_DIR = join(homedir(), '.robopark');
13
+ const ENROLLMENT_FILE = join(ROBOPARK_DIR, 'enrollment_token');
14
+ function ensureDir() {
15
+ mkdirSync(ROBOPARK_DIR, { recursive: true });
16
+ }
17
+ function readEnrollmentToken() {
18
+ if (existsSync(ENROLLMENT_FILE)) {
19
+ return readFileSync(ENROLLMENT_FILE, 'utf8').trim();
20
+ }
21
+ return undefined;
22
+ }
23
+ function saveEnrollmentToken(token) {
24
+ ensureDir();
25
+ writeFileSync(ENROLLMENT_FILE, token, { mode: 0o600 });
26
+ }
27
+ async function findSchedulerPath() {
28
+ const candidates = [
29
+ join(process.cwd(), 'packages', 'robopark', 'scheduler', 'preview_agent.py'),
30
+ join(process.cwd(), 'scheduler', 'preview_agent.py'),
31
+ ];
32
+ for (const p of candidates) {
33
+ if (existsSync(p))
34
+ return p;
35
+ }
36
+ try {
37
+ const { createRequire } = await import('node:module');
38
+ const require = createRequire(import.meta.url);
39
+ const pkgMain = require.resolve('infinicode/package.json');
40
+ const p = join(pkgMain, '..', 'packages', 'robopark', 'scheduler', 'preview_agent.py');
41
+ if (existsSync(p))
42
+ return p;
43
+ }
44
+ catch { /* ignore */ }
45
+ return null;
46
+ }
47
+ function findPython() {
48
+ const names = process.platform === 'win32'
49
+ ? ['python', 'python3', 'py']
50
+ : ['python3', 'python'];
51
+ for (const name of names) {
52
+ try {
53
+ const { execSync } = require('node:child_process');
54
+ execSync(`${name} --version`, { stdio: 'ignore' });
55
+ return name;
56
+ }
57
+ catch {
58
+ continue;
59
+ }
60
+ }
61
+ return process.platform === 'win32' ? 'python' : 'python3';
62
+ }
63
+ export async function roboparkEnroll(opts) {
64
+ const token = opts.enrollmentToken ?? readEnrollmentToken();
65
+ if (!token) {
66
+ console.log(chalk.red(' ✗ pass --enrollment-token <token> or write it to ~/.robopark/enrollment_token'));
67
+ process.exit(1);
68
+ }
69
+ saveEnrollmentToken(token);
70
+ const robotId = opts.robotId ?? hostname().split('.')[0];
71
+ const schedulerUrl = opts.schedulerUrl;
72
+ const script = await findSchedulerPath();
73
+ if (!script) {
74
+ console.log(chalk.red(' ✗ could not find preview_agent.py'));
75
+ process.exit(1);
76
+ }
77
+ const python = findPython();
78
+ const args = [
79
+ script,
80
+ '--scheduler-url', schedulerUrl,
81
+ '--robot-id', robotId,
82
+ '--enrollment-token', token,
83
+ ];
84
+ if (opts.saveConfig)
85
+ args.push('--save-config');
86
+ console.log(chalk.bold('\n robopark enroll'));
87
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
88
+ console.log(` robot: ${chalk.cyan(robotId)}`);
89
+ console.log(` scheduler: ${chalk.cyan(schedulerUrl)}`);
90
+ console.log();
91
+ const proc = spawn(python, args, { stdio: 'inherit' });
92
+ await new Promise((resolve, reject) => {
93
+ proc.on('error', reject);
94
+ proc.on('close', (code) => {
95
+ if (code === 0)
96
+ resolve();
97
+ else
98
+ reject(new Error(`enrollment exited ${code ?? ''}`));
99
+ });
100
+ });
101
+ }
@@ -0,0 +1,14 @@
1
+ export interface PreviewAgentOptions {
2
+ schedulerUrl: string;
3
+ robotId?: string;
4
+ deviceToken?: string;
5
+ enrollmentToken?: string;
6
+ videoDevice: string;
7
+ audioDevice: string;
8
+ width: string;
9
+ height: string;
10
+ fps: string;
11
+ saveConfig?: boolean;
12
+ foreground?: boolean;
13
+ }
14
+ export declare function roboparkPreviewAgent(opts: PreviewAgentOptions): Promise<void>;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * RoboPark — `robopark preview-agent` launcher.
3
+ *
4
+ * Starts preview_agent.py with the right Python interpreter, auto-discovers
5
+ * the scheduler URL from mesh/hub, and detaches unless --foreground.
6
+ */
7
+ import { spawn } from 'node:child_process';
8
+ import { existsSync } from 'node:fs';
9
+ import { hostname } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import chalk from 'chalk';
12
+ import { discoverContext } from './discovery.js';
13
+ const DEFAULT_SCHEDULER_PORT = 8080;
14
+ async function findSchedulerPath() {
15
+ const candidates = [
16
+ join(process.cwd(), 'packages', 'robopark', 'scheduler', 'preview_agent.py'),
17
+ join(process.cwd(), 'scheduler', 'preview_agent.py'),
18
+ ];
19
+ for (const p of candidates) {
20
+ if (existsSync(p))
21
+ return p;
22
+ }
23
+ try {
24
+ const { createRequire } = await import('node:module');
25
+ const require = createRequire(import.meta.url);
26
+ const pkgMain = require.resolve('infinicode/package.json');
27
+ const p = join(pkgMain, '..', 'packages', 'robopark', 'scheduler', 'preview_agent.py');
28
+ if (existsSync(p))
29
+ return p;
30
+ }
31
+ catch { /* ignore */ }
32
+ return null;
33
+ }
34
+ function findPython() {
35
+ const names = process.platform === 'win32'
36
+ ? ['python', 'python3', 'py']
37
+ : ['python3', 'python'];
38
+ for (const name of names) {
39
+ try {
40
+ const { execSync } = require('node:child_process');
41
+ execSync(`${name} --version`, { stdio: 'ignore' });
42
+ return name;
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ }
48
+ return process.platform === 'win32' ? 'python' : 'python3';
49
+ }
50
+ export async function roboparkPreviewAgent(opts) {
51
+ const ctx = await discoverContext();
52
+ const port = DEFAULT_SCHEDULER_PORT;
53
+ const schedulerUrl = opts.schedulerUrl !== 'http://localhost:8080'
54
+ ? opts.schedulerUrl
55
+ : ctx.hub
56
+ ? `http://${ctx.hub.ip}:${port}`
57
+ : 'http://localhost:8080';
58
+ const robotId = opts.robotId ?? hostname().split('.')[0];
59
+ const script = await findSchedulerPath();
60
+ if (!script) {
61
+ console.log(chalk.red(' ✗ could not find preview_agent.py'));
62
+ process.exit(1);
63
+ }
64
+ const python = findPython();
65
+ const args = [
66
+ script,
67
+ '--scheduler-url', schedulerUrl,
68
+ '--robot-id', robotId,
69
+ '--video-device', opts.videoDevice,
70
+ '--audio-device', opts.audioDevice,
71
+ '--width', opts.width,
72
+ '--height', opts.height,
73
+ '--fps', opts.fps,
74
+ ];
75
+ if (opts.deviceToken)
76
+ args.push('--device-token', opts.deviceToken);
77
+ if (opts.enrollmentToken)
78
+ args.push('--enrollment-token', opts.enrollmentToken);
79
+ if (opts.saveConfig)
80
+ args.push('--save-config');
81
+ console.log(chalk.bold('\n robopark preview-agent'));
82
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
83
+ console.log(` robot: ${chalk.cyan(robotId)}`);
84
+ console.log(` scheduler: ${chalk.cyan(schedulerUrl)}`);
85
+ console.log(` video: ${chalk.cyan(opts.videoDevice)}`);
86
+ console.log(` audio: ${chalk.cyan(opts.audioDevice)}`);
87
+ console.log();
88
+ if (opts.foreground) {
89
+ const proc = spawn(python, args, { stdio: 'inherit' });
90
+ await new Promise((resolve) => {
91
+ proc.on('close', () => resolve());
92
+ });
93
+ return;
94
+ }
95
+ const proc = spawn(python, args, {
96
+ stdio: 'ignore',
97
+ detached: true,
98
+ env: { ...process.env, ROBOPARK_PREVIEW_AGENT: '1' },
99
+ });
100
+ proc.unref();
101
+ console.log(chalk.green(' ✓ preview agent started in background'));
102
+ }
@@ -0,0 +1,7 @@
1
+ export interface ProbeOptions {
2
+ token?: string;
3
+ timeout?: string;
4
+ output?: string;
5
+ local?: boolean;
6
+ }
7
+ export declare function roboparkProbe(opts: ProbeOptions): Promise<void>;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * RoboPark — `robopark probe`.
3
+ *
4
+ * Spawn opencode agents (locally or over mesh) to build an accurate
5
+ * infrastructure map. Discovers peers via Tailscale, then runs a structured
6
+ * prompt through the locally installed `opencode` CLI for each node.
7
+ *
8
+ * The agent is asked only to report facts — it does not mutate anything.
9
+ */
10
+ import { spawn } from 'node:child_process';
11
+ import { hostname, networkInterfaces } from 'node:os';
12
+ import chalk from 'chalk';
13
+ import { discoverContext } from './discovery.js';
14
+ function getLanIps() {
15
+ const out = [];
16
+ for (const [name, addrs] of Object.entries(networkInterfaces())) {
17
+ if (!addrs)
18
+ continue;
19
+ for (const a of addrs) {
20
+ if (a.family === 'IPv4' && !a.internal)
21
+ out.push(a.address);
22
+ }
23
+ }
24
+ return out;
25
+ }
26
+ function findOpencode() {
27
+ const names = process.platform === 'win32' ? ['opencode.exe', 'opencode'] : ['opencode'];
28
+ for (const name of names) {
29
+ try {
30
+ const { execSync } = require('node:child_process');
31
+ execSync(`${name} --version`, { stdio: 'ignore' });
32
+ return name;
33
+ }
34
+ catch {
35
+ continue;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ function runOpencodePrompt(prompt, timeoutMs) {
41
+ const bin = findOpencode();
42
+ if (!bin)
43
+ return Promise.resolve('opencode CLI not found; install opencode to run probes');
44
+ return new Promise((resolve) => {
45
+ const proc = spawn(bin, ['--no-project', '--non-interactive'], {
46
+ stdio: ['pipe', 'pipe', 'pipe'],
47
+ env: { ...process.env, OPENCODE_QUIET: '1' },
48
+ });
49
+ let stdout = '';
50
+ let stderr = '';
51
+ let killed = false;
52
+ const timer = setTimeout(() => {
53
+ killed = true;
54
+ proc.kill('SIGTERM');
55
+ }, timeoutMs);
56
+ proc.stdin?.write(prompt);
57
+ proc.stdin?.end();
58
+ proc.stdout?.on('data', (d) => { stdout += d.toString(); });
59
+ proc.stderr?.on('data', (d) => { stderr += d.toString(); });
60
+ proc.on('close', () => {
61
+ clearTimeout(timer);
62
+ if (killed)
63
+ resolve(stdout + '\n[probe timed out]');
64
+ else
65
+ resolve(stdout || stderr || '(no output)');
66
+ });
67
+ proc.on('error', (err) => {
68
+ clearTimeout(timer);
69
+ resolve(`opencode error: ${err.message}`);
70
+ });
71
+ });
72
+ }
73
+ function probePrompt(target, token) {
74
+ const tokenHint = token ? `mesh token is "${token}"` : 'no mesh token provided';
75
+ return `You are an infrastructure probe for the RoboPark fleet.
76
+ Target node: ${target.name} (${target.ip}) via ${target.source}.
77
+ ${tokenHint}.
78
+
79
+ Do NOT modify anything. Only report facts. Keep your answer under 600 words.
80
+
81
+ Please report:
82
+ 1. Hostname, OS, architecture.
83
+ 2. LAN IPs and Tailscale IP if available.
84
+ 3. Whether these processes are running: infinicode node, robopark scheduler, robopark preview_agent.
85
+ 4. If this node is the hub, whether the scheduler at http://localhost:8080/api/settings responds and what production_mode is.
86
+ 5. If this node is a robot/satellite, whether it has enrolled to the scheduler and whether ~/.robopark/device_token exists.
87
+ 6. Any recent errors in journalctl/systemctl for infinicode or robopark services (last 10 lines).
88
+ 7. Whether a camera (/dev/video0) or microphone is detected.
89
+
90
+ Answer as concise structured Markdown bullet list.`;
91
+ }
92
+ async function probeTarget(target, opts) {
93
+ const timeoutMs = opts.timeout ? parseInt(opts.timeout, 10) * 1000 : 60000;
94
+ try {
95
+ const summary = await runOpencodePrompt(probePrompt(target, opts.token), timeoutMs);
96
+ return { target, ok: true, summary, timestamp: new Date().toISOString() };
97
+ }
98
+ catch (e) {
99
+ return { target, ok: false, error: e.message, summary: '', timestamp: new Date().toISOString() };
100
+ }
101
+ }
102
+ export async function roboparkProbe(opts) {
103
+ const ctx = await discoverContext();
104
+ const self = { name: hostname().split('.')[0], ip: getLanIps()[0] || '127.0.0.1', source: 'self' };
105
+ const rawTargets = opts.local ? [self] : [self, ...(ctx.robots || []), ...(ctx.hub ? [ctx.hub] : [])];
106
+ const targets = rawTargets.map((t) => ({
107
+ name: t.name,
108
+ ip: t.ip,
109
+ source: t === self ? 'self' : 'tailscale',
110
+ }));
111
+ console.log(chalk.bold('\n robopark probe'));
112
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
113
+ console.log(` targets: ${chalk.cyan(targets.length)}`);
114
+ console.log();
115
+ const results = [];
116
+ for (const t of targets) {
117
+ process.stdout.write(chalk.dim(` probing ${t.name}… `));
118
+ const r = await probeTarget(t, opts);
119
+ results.push(r);
120
+ console.log(r.ok ? chalk.green('ok') : chalk.red('failed'));
121
+ }
122
+ console.log();
123
+ console.log(chalk.bold(' Probe results'));
124
+ console.log(chalk.dim(' ' + '─'.repeat(52)));
125
+ for (const r of results) {
126
+ console.log(`\n ${chalk.cyan(r.target.name)} (${r.target.ip})`);
127
+ if (!r.ok) {
128
+ console.log(chalk.red(` error: ${r.error}`));
129
+ continue;
130
+ }
131
+ console.log(r.summary.split('\n').map(l => ' ' + l).join('\n'));
132
+ }
133
+ if (opts.output) {
134
+ const { writeFileSync } = await import('node:fs');
135
+ writeFileSync(opts.output, JSON.stringify(results, null, 2), 'utf8');
136
+ console.log(`\n ${chalk.green('✓')} wrote ${opts.output}`);
137
+ }
138
+ }
@@ -108,6 +108,19 @@ export async function roboparkServe(opts) {
108
108
  const port = opts.port ? parseInt(opts.port, 10) : 8080;
109
109
  const host = opts.host ?? '0.0.0.0';
110
110
  const python = findPython();
111
+ const schedulerDir = dirname(schedulerPath);
112
+ const requirements = join(schedulerDir, 'requirements.txt');
113
+ // Ensure scheduler Python dependencies are installed before starting.
114
+ if (existsSync(requirements)) {
115
+ console.log(chalk.dim(' ensuring scheduler Python deps…'));
116
+ try {
117
+ execSync(`${python} -m pip install -r "${requirements}" --user --quiet`, { stdio: 'pipe' });
118
+ }
119
+ catch (err) {
120
+ console.log(chalk.yellow(` ⚠ pip install failed: ${err instanceof Error ? err.message : String(err)}`));
121
+ console.log(chalk.dim(' continuing; scheduler may fail if deps are missing.'));
122
+ }
123
+ }
111
124
  const env = {
112
125
  ...process.env,
113
126
  SCHEDULER_HOST: host,
@@ -0,0 +1,8 @@
1
+ export interface SetupLivekitOptions {
2
+ hubUrl?: string;
3
+ token?: string;
4
+ schedulerUrl?: string;
5
+ timeout?: string;
6
+ dryRun?: boolean;
7
+ }
8
+ export declare function roboparkSetupLivekit(opts: SetupLivekitOptions): Promise<void>;