infinicode 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -4
- package/dist/commands/robopark.d.ts +3 -1
- package/dist/commands/robopark.js +45 -61
- package/dist/commands/serve.d.ts +0 -3
- package/dist/commands/serve.js +13 -23
- package/dist/kernel/federation/dashboard-html.d.ts +1 -1
- package/dist/kernel/federation/dashboard-html.js +35 -1
- package/dist/kernel/federation/federation.d.ts +5 -16
- package/dist/kernel/federation/federation.js +1 -7
- package/dist/kernel/federation/transport-http.d.ts +0 -14
- package/dist/kernel/federation/transport-http.js +3 -88
- package/dist/robopark/auto-start.d.ts +16 -0
- package/dist/robopark/auto-start.js +142 -0
- package/dist/robopark/discovery.d.ts +62 -0
- package/dist/robopark/discovery.js +144 -0
- package/dist/robopark/scan.d.ts +10 -0
- package/dist/robopark/scan.js +97 -0
- package/dist/robopark/serve.d.ts +13 -0
- package/dist/robopark/serve.js +142 -0
- package/dist/robopark/setup.d.ts +21 -0
- package/dist/robopark/setup.js +174 -0
- package/dist/robopark-cli.d.ts +0 -1
- package/dist/robopark-cli.js +84 -15
- package/package.json +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoboPark — `robopark scan`.
|
|
3
|
+
*
|
|
4
|
+
* Scans Tailscale + LAN + local config and emits ready-to-run setup commands
|
|
5
|
+
* for every machine in the fleet. No manual IP/token typing.
|
|
6
|
+
*/
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { discoverContext, generateToken } from './discovery.js';
|
|
9
|
+
function hubCommand(ctx, opts) {
|
|
10
|
+
const name = ctx.hub?.name ?? 'livekit-1';
|
|
11
|
+
const site = 'Tel Aviv'; // TODO: discover from role context or prompt
|
|
12
|
+
const token = opts.token ?? ctx.meshToken ?? generateToken();
|
|
13
|
+
const gw = opts.gateway !== false ? ctx.gateway : undefined;
|
|
14
|
+
const parts = [
|
|
15
|
+
'robopark setup --hub',
|
|
16
|
+
`--name ${name}`,
|
|
17
|
+
`--site "${site}"`,
|
|
18
|
+
`--token ${token}`,
|
|
19
|
+
'--start',
|
|
20
|
+
'--auto-start',
|
|
21
|
+
];
|
|
22
|
+
if (ctx.tailscaleAuthKey)
|
|
23
|
+
parts.push(`--tailscale-auth ${ctx.tailscaleAuthKey}`);
|
|
24
|
+
if (gw)
|
|
25
|
+
parts.push(`--gateway ws://${gw.host}:${gw.port}`, `--gateway-token ${gw.token ?? 'SET_TOKEN'}`);
|
|
26
|
+
return parts.join(' ');
|
|
27
|
+
}
|
|
28
|
+
function robotCommand(robot, ctx, opts) {
|
|
29
|
+
const token = opts.token ?? ctx.meshToken ?? generateToken();
|
|
30
|
+
const hubUrl = ctx.hub ? `http://${ctx.hub.ip}:47913` : 'http://HUB_IP:47913';
|
|
31
|
+
const parts = [
|
|
32
|
+
'robopark setup --robot',
|
|
33
|
+
`--name ${robot.name}`,
|
|
34
|
+
`--hub-url ${hubUrl}`,
|
|
35
|
+
`--token ${token}`,
|
|
36
|
+
'--start',
|
|
37
|
+
'--auto-start',
|
|
38
|
+
];
|
|
39
|
+
if (ctx.tailscaleAuthKey)
|
|
40
|
+
parts.push(`--tailscale-auth ${ctx.tailscaleAuthKey}`);
|
|
41
|
+
return parts.join(' ');
|
|
42
|
+
}
|
|
43
|
+
function controlCommand(ctx, opts) {
|
|
44
|
+
const token = opts.token ?? ctx.meshToken ?? generateToken();
|
|
45
|
+
const hubUrl = ctx.hub ? `http://${ctx.hub.ip}:47913` : 'http://HUB_IP:47913';
|
|
46
|
+
const parts = [
|
|
47
|
+
'robopark setup --control',
|
|
48
|
+
`--hub-url ${hubUrl}`,
|
|
49
|
+
`--token ${token}`,
|
|
50
|
+
'--start',
|
|
51
|
+
'--auto-start',
|
|
52
|
+
];
|
|
53
|
+
return parts.join(' ');
|
|
54
|
+
}
|
|
55
|
+
export async function roboparkScan(opts = {}) {
|
|
56
|
+
console.log(chalk.bold('\n robopark scan — auto-discovering your fleet\n'));
|
|
57
|
+
const ctx = await discoverContext();
|
|
58
|
+
if (!ctx.hub) {
|
|
59
|
+
console.log(chalk.yellow(' ⚠ no hub found on Tailscale. Look for a machine named like livekit-* / hub / scheduler.'));
|
|
60
|
+
console.log(chalk.dim(' make sure the hub box is online and tagged, or run `robopark setup --hub` on it first.'));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
console.log(` ${chalk.green('✓')} hub found: ${chalk.cyan(ctx.hub.name)} @ ${chalk.cyan(ctx.hub.ip)}`);
|
|
64
|
+
}
|
|
65
|
+
if (!ctx.gateway) {
|
|
66
|
+
console.log(chalk.dim(' · no InfiniBot gateway discovered (set one with --gateway or INFINIBOT_GATEWAY env)'));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
console.log(` ${chalk.green('✓')} gateway found: ${chalk.cyan(`${ctx.gateway.host}:${ctx.gateway.port}`)}`);
|
|
70
|
+
}
|
|
71
|
+
if (!ctx.robots.length) {
|
|
72
|
+
console.log(chalk.dim(' · no robots discovered yet. They will LAN-join after the hub is running.'));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.log(` ${chalk.green('✓')} ${ctx.robots.length} robot(s) found:`);
|
|
76
|
+
for (const r of ctx.robots)
|
|
77
|
+
console.log(` · ${chalk.cyan(r.name)} @ ${chalk.cyan(r.ip)}`);
|
|
78
|
+
}
|
|
79
|
+
const token = opts.token ?? ctx.meshToken ?? generateToken();
|
|
80
|
+
console.log(`\n ${chalk.green('✓')} using mesh token: ${chalk.cyan(token)}`);
|
|
81
|
+
console.log(chalk.dim(' save it with: echo "${TOKEN}" > ~/.robopark/mesh.token'));
|
|
82
|
+
console.log(chalk.bold('\n Generated setup commands:\n'));
|
|
83
|
+
console.log(chalk.dim(' # on the hub box'));
|
|
84
|
+
console.log(' ' + chalk.cyan(hubCommand(ctx, opts)));
|
|
85
|
+
console.log();
|
|
86
|
+
console.log(chalk.dim(' # on each robot'));
|
|
87
|
+
for (const r of ctx.robots) {
|
|
88
|
+
console.log(' ' + chalk.cyan(robotCommand(r, ctx, opts)));
|
|
89
|
+
}
|
|
90
|
+
if (!ctx.robots.length) {
|
|
91
|
+
console.log(' ' + chalk.cyan(robotCommand({ name: 'robobmw', ip: 'HUB_IP', online: true, source: 'tailscale' }, ctx, opts).replace('robobmw', '<ROBOT_NAME>')));
|
|
92
|
+
}
|
|
93
|
+
console.log();
|
|
94
|
+
console.log(chalk.dim(' # on this dev laptop'));
|
|
95
|
+
console.log(' ' + chalk.cyan(controlCommand(ctx, opts)));
|
|
96
|
+
console.log();
|
|
97
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ServeOptions {
|
|
2
|
+
port?: string;
|
|
3
|
+
host?: string;
|
|
4
|
+
dataDir?: string;
|
|
5
|
+
gateway?: string;
|
|
6
|
+
gatewayToken?: string;
|
|
7
|
+
gatewayPassword?: string;
|
|
8
|
+
open?: boolean;
|
|
9
|
+
foreground?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function roboparkServe(opts: ServeOptions): Promise<void>;
|
|
12
|
+
/** Quick health check: is the scheduler responding? */
|
|
13
|
+
export declare function schedulerHealthy(url: string, timeoutMs?: number): Promise<boolean>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoboPark — `robopark serve`.
|
|
3
|
+
*
|
|
4
|
+
* Starts the RoboPark scheduler (Python FastAPI) and the web UI in one pass.
|
|
5
|
+
* The scheduler lives next to this package in `scheduler/main.py`.
|
|
6
|
+
*
|
|
7
|
+
* robopark serve --port 8080
|
|
8
|
+
* robopark serve --gateway ws://infinibot:18789 --gateway-token <token>
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { spawn, execSync } from 'node:child_process';
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
function pkgDir() {
|
|
16
|
+
try {
|
|
17
|
+
// dist/robopark/serve.js -> project root
|
|
18
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return process.cwd();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function findScheduler() {
|
|
25
|
+
const candidates = [
|
|
26
|
+
join(pkgDir(), 'packages', 'robopark', 'scheduler', 'main.py'),
|
|
27
|
+
join(pkgDir(), 'scheduler', 'main.py'),
|
|
28
|
+
join(process.cwd(), 'packages', 'robopark', 'scheduler', 'main.py'),
|
|
29
|
+
join(process.cwd(), 'scheduler', 'main.py'),
|
|
30
|
+
];
|
|
31
|
+
for (const p of candidates) {
|
|
32
|
+
if (existsSync(p))
|
|
33
|
+
return p;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function findPython() {
|
|
38
|
+
for (const name of ['python3.12', 'python3.11', 'python3.10', 'python3', 'python']) {
|
|
39
|
+
try {
|
|
40
|
+
execSync(`${name} --version`, { stdio: 'ignore' });
|
|
41
|
+
return name;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return 'python3';
|
|
48
|
+
}
|
|
49
|
+
export async function roboparkServe(opts) {
|
|
50
|
+
const schedulerPath = findScheduler();
|
|
51
|
+
if (!schedulerPath) {
|
|
52
|
+
console.log(chalk.red(' ✗ could not find scheduler/main.py'));
|
|
53
|
+
console.log(chalk.dim(' make sure robopark is installed from npm, not a partial checkout.'));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const port = opts.port ? parseInt(opts.port, 10) : 8080;
|
|
57
|
+
const host = opts.host ?? '0.0.0.0';
|
|
58
|
+
const python = findPython();
|
|
59
|
+
const env = {
|
|
60
|
+
...process.env,
|
|
61
|
+
SCHEDULER_HOST: host,
|
|
62
|
+
SCHEDULER_PORT: String(port),
|
|
63
|
+
SCHEDULER_DATA_DIR: opts.dataDir ?? join(pkgDir(), '..', 'data'),
|
|
64
|
+
};
|
|
65
|
+
if (opts.gateway)
|
|
66
|
+
env.INFINIBOT_GATEWAY_URL = opts.gateway;
|
|
67
|
+
if (opts.gatewayToken)
|
|
68
|
+
env.INFINIBOT_GATEWAY_TOKEN = opts.gatewayToken;
|
|
69
|
+
if (opts.gatewayPassword)
|
|
70
|
+
env.INFINIBOT_GATEWAY_PASSWORD = opts.gatewayPassword;
|
|
71
|
+
console.log(chalk.bold('\n robopark serve'));
|
|
72
|
+
console.log(chalk.dim(' ' + '─'.repeat(52)));
|
|
73
|
+
console.log(` scheduler: ${chalk.cyan(schedulerPath)}`);
|
|
74
|
+
console.log(` python: ${chalk.cyan(python)}`);
|
|
75
|
+
console.log(` listen: ${chalk.cyan(`${host}:${port}`)}`);
|
|
76
|
+
if (opts.gateway)
|
|
77
|
+
console.log(` gateway: ${chalk.cyan(opts.gateway)}`);
|
|
78
|
+
console.log();
|
|
79
|
+
const proc = spawn(python, ['main.py'], {
|
|
80
|
+
cwd: dirname(schedulerPath),
|
|
81
|
+
env,
|
|
82
|
+
stdio: 'pipe',
|
|
83
|
+
detached: !opts.foreground,
|
|
84
|
+
});
|
|
85
|
+
let ready = false;
|
|
86
|
+
proc.stdout?.on('data', (d) => {
|
|
87
|
+
const line = d.toString().trim();
|
|
88
|
+
if (line)
|
|
89
|
+
console.log(chalk.dim(' [scheduler] ' + line));
|
|
90
|
+
if (/Uvicorn running|Application startup complete/i.test(line)) {
|
|
91
|
+
ready = true;
|
|
92
|
+
printUrl(port);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
proc.stderr?.on('data', (d) => {
|
|
96
|
+
const line = d.toString().trim();
|
|
97
|
+
if (line)
|
|
98
|
+
console.log(chalk.yellow(' [scheduler] ' + line));
|
|
99
|
+
});
|
|
100
|
+
proc.on('error', (err) => {
|
|
101
|
+
console.log(chalk.red(` ✗ scheduler failed to start: ${err.message}`));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
});
|
|
104
|
+
proc.on('exit', (code) => {
|
|
105
|
+
if (!ready && code !== 0) {
|
|
106
|
+
console.log(chalk.red(` ✗ scheduler exited ${code ?? ''}`));
|
|
107
|
+
process.exit(code ?? 1);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
if (opts.foreground) {
|
|
111
|
+
process.on('SIGINT', () => {
|
|
112
|
+
console.log(chalk.dim('\n stopping scheduler…'));
|
|
113
|
+
try {
|
|
114
|
+
proc.kill('SIGINT');
|
|
115
|
+
}
|
|
116
|
+
catch { /* ignore */ }
|
|
117
|
+
});
|
|
118
|
+
proc.on('exit', () => process.exit(0));
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// Detached mode: let the user walk away. The process keeps running.
|
|
122
|
+
proc.unref();
|
|
123
|
+
printUrl(port);
|
|
124
|
+
console.log(chalk.dim(' running in background. Use Task Manager / systemctl / launchctl to stop.'));
|
|
125
|
+
// give the scheduler a moment to bind before printing URL
|
|
126
|
+
setTimeout(() => process.exit(0), 1200);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function printUrl(port) {
|
|
130
|
+
const url = `http://localhost:${port}`;
|
|
131
|
+
console.log(chalk.bold(` dashboard: ${chalk.cyan(url)}`));
|
|
132
|
+
}
|
|
133
|
+
/** Quick health check: is the scheduler responding? */
|
|
134
|
+
export async function schedulerHealthy(url, timeoutMs = 3000) {
|
|
135
|
+
try {
|
|
136
|
+
const res = await fetch(`${url.replace(/\/$/, '')}/api/settings`, { signal: AbortSignal.timeout(timeoutMs) });
|
|
137
|
+
return res.ok;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type Conf from 'conf';
|
|
2
|
+
import type { InfinicodeConfig } from '../kernel/config-schema.js';
|
|
3
|
+
export interface SetupOptions {
|
|
4
|
+
role?: 'hub' | 'robot' | 'control';
|
|
5
|
+
name?: string;
|
|
6
|
+
site?: string;
|
|
7
|
+
token?: string;
|
|
8
|
+
hub?: string;
|
|
9
|
+
port?: string;
|
|
10
|
+
gateway?: string;
|
|
11
|
+
gatewayToken?: string;
|
|
12
|
+
gatewayPassword?: string;
|
|
13
|
+
tailscaleAuth?: string;
|
|
14
|
+
tag?: string;
|
|
15
|
+
schedulerPort?: string;
|
|
16
|
+
start?: boolean;
|
|
17
|
+
autoStart?: boolean;
|
|
18
|
+
yes?: boolean;
|
|
19
|
+
foreground?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function roboparkSetup(config: Conf<InfinicodeConfig>, opts: SetupOptions): Promise<void>;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoboPark — `robopark setup`.
|
|
3
|
+
*
|
|
4
|
+
* One-liner setup per production machine. Each role turns on the right defaults
|
|
5
|
+
* and hides the mesh vocabulary.
|
|
6
|
+
*
|
|
7
|
+
* robopark setup --hub --name livekit-1 --site "Tel Aviv" --start --auto-start
|
|
8
|
+
* robopark setup --robot --name robobmw --start --auto-start
|
|
9
|
+
* robopark setup --control --start
|
|
10
|
+
*/
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
|
+
import { discoverContext, generateToken } from './discovery.js';
|
|
17
|
+
import { registerAutoStart } from './auto-start.js';
|
|
18
|
+
const DEFAULT_MESH_PORT = 47913;
|
|
19
|
+
const DEFAULT_SCHEDULER_PORT = 8080;
|
|
20
|
+
function saveToken(token) {
|
|
21
|
+
const dir = join(homedir(), '.robopark');
|
|
22
|
+
mkdirSync(dir, { recursive: true });
|
|
23
|
+
writeFileSync(join(dir, 'mesh.token'), token, 'utf8');
|
|
24
|
+
}
|
|
25
|
+
/** Run an external command and detach unless foreground is requested. */
|
|
26
|
+
function runDetached(cmd, args, env) {
|
|
27
|
+
const proc = spawn(cmd, args, {
|
|
28
|
+
stdio: 'ignore',
|
|
29
|
+
detached: true,
|
|
30
|
+
env: { ...process.env, ...env },
|
|
31
|
+
});
|
|
32
|
+
proc.unref();
|
|
33
|
+
}
|
|
34
|
+
export async function roboparkSetup(config, opts) {
|
|
35
|
+
const ctx = await discoverContext();
|
|
36
|
+
const role = opts.role ?? 'control';
|
|
37
|
+
const token = opts.token ?? ctx.meshToken ?? generateToken();
|
|
38
|
+
const port = opts.port ? parseInt(opts.port, 10) : DEFAULT_MESH_PORT;
|
|
39
|
+
const name = opts.name ?? ctx.localName;
|
|
40
|
+
const gatewayHost = opts.gateway ?? (ctx.gateway ? `ws://${ctx.gateway.host}:${ctx.gateway.port ?? 18789}` : undefined);
|
|
41
|
+
const gatewayToken = opts.gatewayToken ?? ctx.gateway?.token;
|
|
42
|
+
saveToken(token);
|
|
43
|
+
console.log(chalk.bold('\n robopark setup'));
|
|
44
|
+
console.log(chalk.dim(' ' + '─'.repeat(52)));
|
|
45
|
+
console.log(` role: ${chalk.yellow(role)}`);
|
|
46
|
+
console.log(` name: ${chalk.cyan(name)}${opts.site ? chalk.dim(' @ ' + opts.site) : ''}`);
|
|
47
|
+
console.log(` mesh: ${chalk.cyan('0.0.0.0:' + port)}`);
|
|
48
|
+
console.log(` token: ${chalk.green(token)}`);
|
|
49
|
+
if (gatewayHost)
|
|
50
|
+
console.log(` gateway:${chalk.cyan(gatewayHost)}`);
|
|
51
|
+
console.log();
|
|
52
|
+
if (role === 'hub') {
|
|
53
|
+
await setupHub(config, opts, { name, token, port, gatewayHost, gatewayToken });
|
|
54
|
+
}
|
|
55
|
+
else if (role === 'robot') {
|
|
56
|
+
await setupRobot(config, opts, ctx, { name, token, port });
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
await setupControl(config, opts, ctx, { name, token, port });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function setupHub(config, opts, internal) {
|
|
63
|
+
const fed = {
|
|
64
|
+
...(config.get('federation') ?? {}),
|
|
65
|
+
enabled: true,
|
|
66
|
+
role: 'hub',
|
|
67
|
+
port: internal.port,
|
|
68
|
+
displayName: internal.name,
|
|
69
|
+
token: internal.token,
|
|
70
|
+
lan: true,
|
|
71
|
+
};
|
|
72
|
+
config.set('federation', fed);
|
|
73
|
+
const infinicodeArgs = [
|
|
74
|
+
'serve', '--hub',
|
|
75
|
+
'--name', internal.name,
|
|
76
|
+
'--port', String(internal.port),
|
|
77
|
+
'--token', internal.token,
|
|
78
|
+
'--lan',
|
|
79
|
+
'--auto-update',
|
|
80
|
+
'--supervised',
|
|
81
|
+
];
|
|
82
|
+
if (internal.gatewayHost)
|
|
83
|
+
infinicodeArgs.push('--gateway', internal.gatewayHost);
|
|
84
|
+
if (internal.gatewayToken)
|
|
85
|
+
infinicodeArgs.push('--gateway-token', internal.gatewayToken);
|
|
86
|
+
const schedulerPort = opts.schedulerPort ? parseInt(opts.schedulerPort, 10) : DEFAULT_SCHEDULER_PORT;
|
|
87
|
+
console.log(chalk.dim(' hub commands:'));
|
|
88
|
+
console.log(' ' + chalk.cyan(`infinicode ${infinicodeArgs.join(' ')}`));
|
|
89
|
+
console.log(' ' + chalk.cyan(`robopark serve --port ${schedulerPort}`));
|
|
90
|
+
console.log();
|
|
91
|
+
if (opts.start) {
|
|
92
|
+
console.log(chalk.dim(' starting infinicode hub…'));
|
|
93
|
+
runDetached('infinicode', infinicodeArgs);
|
|
94
|
+
console.log(chalk.dim(' starting robopark scheduler…'));
|
|
95
|
+
runDetached('robopark', ['serve', '--port', String(schedulerPort)]);
|
|
96
|
+
}
|
|
97
|
+
if (opts.autoStart) {
|
|
98
|
+
const reg = await registerAutoStart({
|
|
99
|
+
role: 'hub',
|
|
100
|
+
name: internal.name,
|
|
101
|
+
command: 'infinicode',
|
|
102
|
+
args: infinicodeArgs,
|
|
103
|
+
});
|
|
104
|
+
console.log(reg.ok ? chalk.green(` ✓ ${reg.message}`) : chalk.yellow(` ⚠ ${reg.message}`));
|
|
105
|
+
const reg2 = await registerAutoStart({
|
|
106
|
+
role: 'hub-scheduler',
|
|
107
|
+
name: internal.name,
|
|
108
|
+
command: 'robopark',
|
|
109
|
+
args: ['serve', '--port', String(schedulerPort), '--foreground'],
|
|
110
|
+
});
|
|
111
|
+
console.log(reg2.ok ? chalk.green(` ✓ ${reg2.message}`) : chalk.yellow(` ⚠ ${reg2.message}`));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function setupRobot(config, opts, ctx, internal) {
|
|
115
|
+
const hubUrl = opts.hub ?? (ctx.hub ? `http://${ctx.hub.ip}:${internal.port}` : undefined);
|
|
116
|
+
if (!hubUrl) {
|
|
117
|
+
console.log(chalk.red(' ✗ no hub discovered. Pass --hub http://HUB_IP:47913'));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const fed = {
|
|
121
|
+
...(config.get('federation') ?? {}),
|
|
122
|
+
enabled: true,
|
|
123
|
+
role: 'satellite',
|
|
124
|
+
port: internal.port,
|
|
125
|
+
displayName: internal.name,
|
|
126
|
+
token: internal.token,
|
|
127
|
+
seeds: [hubUrl],
|
|
128
|
+
lan: true,
|
|
129
|
+
};
|
|
130
|
+
config.set('federation', fed);
|
|
131
|
+
const args = [
|
|
132
|
+
'serve', '--role', 'satellite',
|
|
133
|
+
'--name', internal.name,
|
|
134
|
+
'--port', String(internal.port),
|
|
135
|
+
'--token', internal.token,
|
|
136
|
+
'--seed', hubUrl,
|
|
137
|
+
'--lan',
|
|
138
|
+
'--auto-update',
|
|
139
|
+
'--supervised',
|
|
140
|
+
];
|
|
141
|
+
console.log(chalk.dim(' robot command:'));
|
|
142
|
+
console.log(' ' + chalk.cyan(`infinicode ${args.join(' ')}`));
|
|
143
|
+
console.log();
|
|
144
|
+
if (opts.start) {
|
|
145
|
+
console.log(chalk.dim(' starting robot satellite node…'));
|
|
146
|
+
runDetached('infinicode', args);
|
|
147
|
+
}
|
|
148
|
+
if (opts.autoStart) {
|
|
149
|
+
const reg = await registerAutoStart({
|
|
150
|
+
role: 'robot',
|
|
151
|
+
name: internal.name,
|
|
152
|
+
command: 'infinicode',
|
|
153
|
+
args,
|
|
154
|
+
});
|
|
155
|
+
console.log(reg.ok ? chalk.green(` ✓ ${reg.message}`) : chalk.yellow(` ⚠ ${reg.message}`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function setupControl(config, opts, ctx, internal) {
|
|
159
|
+
const hubUrl = opts.hub ?? (ctx.hub ? `http://${ctx.hub.ip}:${internal.port}` : undefined);
|
|
160
|
+
const args = [
|
|
161
|
+
'mesh', 'install',
|
|
162
|
+
'--token', internal.token,
|
|
163
|
+
'--name', internal.name,
|
|
164
|
+
hubUrl ? '--seed' : '', hubUrl ?? '',
|
|
165
|
+
].filter(Boolean);
|
|
166
|
+
console.log(chalk.dim(' control command:'));
|
|
167
|
+
console.log(' ' + chalk.cyan(`infinicode ${args.join(' ')}`));
|
|
168
|
+
console.log();
|
|
169
|
+
if (opts.start) {
|
|
170
|
+
console.log(chalk.dim(' registering infinicode MCP host…'));
|
|
171
|
+
const proc = spawn('infinicode', args, { stdio: 'inherit' });
|
|
172
|
+
await new Promise(resolve => proc.on('close', () => resolve()));
|
|
173
|
+
}
|
|
174
|
+
}
|
package/dist/robopark-cli.d.ts
CHANGED
package/dist/robopark-cli.js
CHANGED
|
@@ -1,23 +1,92 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
2
|
+
* RoboPark CLI entry point (shipped as `infinicode/robopark` and `robopark`).
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A thin operator layer over the infinicode mesh. Keeps product concerns
|
|
5
|
+
* (scheduler, fleet UI, video sessions, InfiniBot map) separate from the
|
|
6
|
+
* execution kernel.
|
|
7
7
|
*/
|
|
8
8
|
import { Command } from 'commander';
|
|
9
|
+
import chalk from 'chalk';
|
|
9
10
|
import Conf from 'conf';
|
|
10
|
-
import { attachRoboparkSubcommands } from './commands/robopark.js';
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import { attachRoboparkSubcommands as attachLegacy } from './commands/robopark.js';
|
|
12
|
+
import { roboparkScan } from './robopark/scan.js';
|
|
13
|
+
import { roboparkServe } from './robopark/serve.js';
|
|
14
|
+
import { roboparkSetup } from './robopark/setup.js';
|
|
13
15
|
const config = new Conf({
|
|
14
|
-
projectName: 'infinicode',
|
|
15
|
-
defaults: DEFAULT_CONFIG,
|
|
16
|
+
projectName: 'infinicode',
|
|
16
17
|
});
|
|
17
|
-
const program = new Command()
|
|
18
|
+
const program = new Command('robopark')
|
|
19
|
+
.description('RoboPark fleet control CLI — set up, watch, and drive talking robots')
|
|
20
|
+
.version('2.8.0');
|
|
18
21
|
program
|
|
19
|
-
.
|
|
20
|
-
.description('
|
|
21
|
-
.
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
.command('scan')
|
|
23
|
+
.description('Auto-discover the fleet and print ready-to-run setup commands')
|
|
24
|
+
.option('--token <token>', 'mesh auth token to use')
|
|
25
|
+
.option('--output <dir>', 'write install scripts to this directory')
|
|
26
|
+
.option('--no-gateway', 'skip InfiniBot gateway in generated commands')
|
|
27
|
+
.action(async (opts) => {
|
|
28
|
+
await roboparkScan({
|
|
29
|
+
token: opts.token,
|
|
30
|
+
output: opts.output,
|
|
31
|
+
gateway: opts.gateway,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
program
|
|
35
|
+
.command('serve')
|
|
36
|
+
.description('Start the RoboPark scheduler + web UI')
|
|
37
|
+
.option('--port <port>', 'scheduler HTTP port', '8080')
|
|
38
|
+
.option('--host <host>', 'scheduler bind host', '0.0.0.0')
|
|
39
|
+
.option('--data-dir <dir>', 'scheduler data directory')
|
|
40
|
+
.option('--gateway <url>', 'InfiniBot gateway ws:// URL')
|
|
41
|
+
.option('--gateway-token <token>', 'InfiniBot gateway token')
|
|
42
|
+
.option('--gateway-password <pw>', 'InfiniBot gateway password')
|
|
43
|
+
.option('--foreground', 'stay in foreground; do not detach')
|
|
44
|
+
.action(async (opts) => {
|
|
45
|
+
await roboparkServe(opts);
|
|
46
|
+
});
|
|
47
|
+
program
|
|
48
|
+
.command('setup')
|
|
49
|
+
.description('Set up this device in one pass')
|
|
50
|
+
.option('--hub', 'this device is the on-site hub')
|
|
51
|
+
.option('--robot', 'this device is a robot')
|
|
52
|
+
.option('--control', 'this device is a control station')
|
|
53
|
+
.option('--role <role>', 'explicit role: hub | robot | control')
|
|
54
|
+
.option('--name <name>', 'device name (defaults to hostname)')
|
|
55
|
+
.option('--site <site>', 'physical location, e.g. "Tel Aviv"')
|
|
56
|
+
.option('--hub-url <url>', 'hub base URL for robots/control')
|
|
57
|
+
.option('--token <token>', 'shared mesh token')
|
|
58
|
+
.option('--port <port>', 'mesh listen port', '47913')
|
|
59
|
+
.option('--gateway <url>', 'InfiniBot gateway ws:// URL')
|
|
60
|
+
.option('--gateway-token <token>', 'InfiniBot gateway token')
|
|
61
|
+
.option('--scheduler-port <port>', 'scheduler port on hub', '8080')
|
|
62
|
+
.option('--tailscale-auth <token>', 'Tailscale auth key for the hub')
|
|
63
|
+
.option('--tag <tag>', 'Tailscale tag filter')
|
|
64
|
+
.option('--start', 'launch the node now')
|
|
65
|
+
.option('--auto-start', 'register to start on boot')
|
|
66
|
+
.option('--yes', 'non-interactive mode')
|
|
67
|
+
.action(async (opts) => {
|
|
68
|
+
const role = opts.role ?? (opts.hub ? 'hub' : opts.robot ? 'robot' : opts.control ? 'control' : undefined);
|
|
69
|
+
if (!role) {
|
|
70
|
+
console.log(chalk.red(' pass a role: --hub | --robot | --control'));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
await roboparkSetup(config, {
|
|
74
|
+
role,
|
|
75
|
+
name: opts.name,
|
|
76
|
+
site: opts.site,
|
|
77
|
+
hub: opts.hubUrl,
|
|
78
|
+
token: opts.token,
|
|
79
|
+
port: opts.port,
|
|
80
|
+
gateway: opts.gateway,
|
|
81
|
+
gatewayToken: opts.gatewayToken,
|
|
82
|
+
schedulerPort: opts.schedulerPort,
|
|
83
|
+
tailscaleAuth: opts.tailscaleAuth,
|
|
84
|
+
tag: opts.tag,
|
|
85
|
+
start: opts.start,
|
|
86
|
+
autoStart: opts.autoStart,
|
|
87
|
+
yes: opts.yes,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
// Keep legacy fleet/run/apply/dashboard commands available while migrating.
|
|
91
|
+
attachLegacy(program, config, { skipSetup: true });
|
|
92
|
+
await program.parseAsync(process.argv);
|
package/package.json
CHANGED