infinicode 2.8.22 → 2.8.25
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/kernel/federation/peer-mesh.js +15 -1
- package/dist/kernel/providers/ollama-provider.d.ts +4 -0
- package/dist/kernel/providers/ollama-provider.js +16 -2
- package/dist/kernel/setup.js +13 -0
- package/dist/robopark/add-robot.d.ts +8 -0
- package/dist/robopark/add-robot.js +113 -0
- package/dist/robopark/agent-ctl.d.ts +20 -0
- package/dist/robopark/agent-ctl.js +237 -0
- package/dist/robopark/doctor.d.ts +6 -0
- package/dist/robopark/doctor.js +175 -0
- package/dist/robopark/llm-set.d.ts +8 -0
- package/dist/robopark/llm-set.js +87 -0
- package/dist/robopark/profile.d.ts +40 -0
- package/dist/robopark/profile.js +69 -0
- package/dist/robopark/server-add.d.ts +13 -0
- package/dist/robopark/server-add.js +114 -0
- package/dist/robopark/setup-livekit.js +89 -22
- package/dist/robopark/setup.js +52 -8
- package/dist/robopark/stop-all.d.ts +6 -0
- package/dist/robopark/stop-all.js +125 -0
- package/dist/robopark-cli.js +119 -0
- package/package.json +1 -1
- package/packages/robopark/scheduler/main.py +107 -10
- package/packages/robopark/scheduler/preview_agent.py +1011 -782
- package/packages/robopark/vision/app_pi_clean.py +81 -1
|
@@ -77,8 +77,22 @@ export class PeerMesh {
|
|
|
77
77
|
peer.lastSeenAt = Date.now();
|
|
78
78
|
if (!peer.connected) {
|
|
79
79
|
peer.connected = true;
|
|
80
|
+
// A peer coming back online may be a fresh process with a changed
|
|
81
|
+
// role (e.g. someone fixed a bad `--hub` flag and relaunched as
|
|
82
|
+
// `--role satellite`) — the nodeId is stable across restarts (it's
|
|
83
|
+
// a persisted identity, not per-process), so without this refetch
|
|
84
|
+
// the stale role from the peer's *first ever* connect() would
|
|
85
|
+
// stick forever, since nothing else in this class ever updates it.
|
|
86
|
+
try {
|
|
87
|
+
const manifest = await this.opts.client.fetchManifest(peer.url);
|
|
88
|
+
peer.role = manifest.role;
|
|
89
|
+
peer.displayName = manifest.displayName;
|
|
90
|
+
peer.version = manifest.version;
|
|
91
|
+
peer.manifest = manifest;
|
|
92
|
+
}
|
|
93
|
+
catch { /* keep last-known manifest fields if the refetch fails */ }
|
|
80
94
|
this.openStream(peer); // revive stream after an outage
|
|
81
|
-
this.opts.logger.info(`[federation] peer ${peer.displayName} back online`);
|
|
95
|
+
this.opts.logger.info(`[federation] peer ${peer.displayName} back online (role: ${peer.role})`);
|
|
82
96
|
this.opts.onPeerChange?.();
|
|
83
97
|
}
|
|
84
98
|
}
|
|
@@ -15,6 +15,8 @@ export interface OllamaProviderOptions {
|
|
|
15
15
|
fallbackURL?: string;
|
|
16
16
|
defaultModel?: string;
|
|
17
17
|
timeoutMs?: number;
|
|
18
|
+
/** Bearer token for Ollama Cloud (ollama.com) — unset for local Ollama, which has no auth. */
|
|
19
|
+
apiKey?: string;
|
|
18
20
|
}
|
|
19
21
|
export declare class OllamaProvider implements ProviderInterface {
|
|
20
22
|
info: ProviderInfo;
|
|
@@ -23,6 +25,7 @@ export declare class OllamaProvider implements ProviderInterface {
|
|
|
23
25
|
private activeURL;
|
|
24
26
|
private defaultModel?;
|
|
25
27
|
private timeoutMs;
|
|
28
|
+
private apiKey?;
|
|
26
29
|
private cachedModels;
|
|
27
30
|
constructor(options: OllamaProviderOptions);
|
|
28
31
|
capabilities(): Promise<ProviderCapabilities>;
|
|
@@ -34,6 +37,7 @@ export declare class OllamaProvider implements ProviderInterface {
|
|
|
34
37
|
latencyMs: number;
|
|
35
38
|
error?: string;
|
|
36
39
|
}>;
|
|
40
|
+
private headers;
|
|
37
41
|
private fetchModels;
|
|
38
42
|
private toProviderModel;
|
|
39
43
|
private guessContextLength;
|
|
@@ -5,6 +5,7 @@ export class OllamaProvider {
|
|
|
5
5
|
activeURL;
|
|
6
6
|
defaultModel;
|
|
7
7
|
timeoutMs;
|
|
8
|
+
apiKey;
|
|
8
9
|
cachedModels = [];
|
|
9
10
|
constructor(options) {
|
|
10
11
|
const id = options.id ?? 'ollama';
|
|
@@ -13,6 +14,7 @@ export class OllamaProvider {
|
|
|
13
14
|
this.activeURL = this.baseURL;
|
|
14
15
|
this.defaultModel = options.defaultModel;
|
|
15
16
|
this.timeoutMs = options.timeoutMs ?? 10_000;
|
|
17
|
+
this.apiKey = options.apiKey;
|
|
16
18
|
this.info = {
|
|
17
19
|
id,
|
|
18
20
|
name: options.name ?? 'Ollama',
|
|
@@ -45,7 +47,7 @@ export class OllamaProvider {
|
|
|
45
47
|
};
|
|
46
48
|
const response = await fetch(endpoint, {
|
|
47
49
|
method: 'POST',
|
|
48
|
-
headers:
|
|
50
|
+
headers: this.headers(),
|
|
49
51
|
body: JSON.stringify(body),
|
|
50
52
|
signal: AbortSignal.timeout(this.timeoutMs * 3),
|
|
51
53
|
});
|
|
@@ -77,7 +79,7 @@ export class OllamaProvider {
|
|
|
77
79
|
};
|
|
78
80
|
const response = await fetch(endpoint, {
|
|
79
81
|
method: 'POST',
|
|
80
|
-
headers:
|
|
82
|
+
headers: this.headers(),
|
|
81
83
|
body: JSON.stringify(body),
|
|
82
84
|
signal: AbortSignal.timeout(this.timeoutMs * 6),
|
|
83
85
|
});
|
|
@@ -133,7 +135,19 @@ export class OllamaProvider {
|
|
|
133
135
|
return { ok: false, latencyMs: Date.now() - start, error: err instanceof Error ? err.message : String(err) };
|
|
134
136
|
}
|
|
135
137
|
}
|
|
138
|
+
headers() {
|
|
139
|
+
const h = { 'Content-Type': 'application/json' };
|
|
140
|
+
if (this.apiKey)
|
|
141
|
+
h['Authorization'] = `Bearer ${this.apiKey}`;
|
|
142
|
+
return h;
|
|
143
|
+
}
|
|
136
144
|
async fetchModels() {
|
|
145
|
+
// Ollama Cloud has no /api/tags (that lists locally-pulled models, which
|
|
146
|
+
// isn't a concept cloud-hosted models have) — a configured defaultModel
|
|
147
|
+
// is used directly by complete()/completeStream() regardless, so treat
|
|
148
|
+
// this as best-effort rather than failing capabilities()/verify().
|
|
149
|
+
if (this.apiKey)
|
|
150
|
+
return this.cachedModels;
|
|
137
151
|
const url = await this.resolveURL();
|
|
138
152
|
const response = await fetch(`${url}/api/tags`, {
|
|
139
153
|
signal: AbortSignal.timeout(this.timeoutMs),
|
package/dist/kernel/setup.js
CHANGED
|
@@ -79,6 +79,19 @@ export function buildKernelFromConfig(config, options) {
|
|
|
79
79
|
};
|
|
80
80
|
const kernel = new Kernel({ loadBuiltinWorkers: true, logger: options?.logger });
|
|
81
81
|
kernel.registerProvider('ollama', new OllamaProvider(ollamaOptions));
|
|
82
|
+
// Ollama Cloud (ollama.com) — separate from the local instance above:
|
|
83
|
+
// different base URL, requires a Bearer token, and its /v1/chat/completions
|
|
84
|
+
// is used directly rather than the local-only /api/tags model listing.
|
|
85
|
+
const ollamaCloudKey = process.env.OLLAMA_CLOUD_API_KEY;
|
|
86
|
+
if (ollamaCloudKey) {
|
|
87
|
+
kernel.registerProvider('ollama-cloud', new OllamaProvider({
|
|
88
|
+
id: 'ollama-cloud',
|
|
89
|
+
name: 'Ollama Cloud',
|
|
90
|
+
baseURL: 'https://ollama.com',
|
|
91
|
+
defaultModel: process.env.OLLAMA_CLOUD_MODEL ?? 'gemma3:27b',
|
|
92
|
+
apiKey: ollamaCloudKey,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
82
95
|
// Register cloud providers
|
|
83
96
|
registerCloudProviders(kernel, config.get('cloudProviders') ?? []);
|
|
84
97
|
// Register local OpenAI-compatible providers (LM Studio, vLLM, SGLang,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type Conf from 'conf';
|
|
2
|
+
import type { InfinicodeConfig } from '../kernel/config-schema.js';
|
|
3
|
+
import { type ExplicitContextOpts } from './profile.js';
|
|
4
|
+
export interface AddRobotOptions extends ExplicitContextOpts {
|
|
5
|
+
name: string;
|
|
6
|
+
start?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function roboparkAddRobot(config: Conf<InfinicodeConfig>, opts: AddRobotOptions): Promise<void>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoboPark — `robopark add-robot`.
|
|
3
|
+
*
|
|
4
|
+
* Collapses what used to be two manual steps — hand-curling
|
|
5
|
+
* `POST /api/devices` to mint an enrollment token, then hand-typing the full
|
|
6
|
+
* `robopark setup --robot --hub-url ... --token ... --scheduler-url ...
|
|
7
|
+
* --enrollment-token ...` one-liner on the actual robot machine — into one
|
|
8
|
+
* command.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* robopark add-robot --name robobmw --site "Tel Aviv"
|
|
12
|
+
* → mints an enrollment token against the resolved scheduler and prints
|
|
13
|
+
* the ready-to-paste `robopark setup --robot ...` command to run ON
|
|
14
|
+
* THE ROBOT.
|
|
15
|
+
*
|
|
16
|
+
* robopark add-robot --name robobmw --start
|
|
17
|
+
* → mints the token AND starts the robot node right here, on this
|
|
18
|
+
* machine (useful when this machine IS the robot).
|
|
19
|
+
*
|
|
20
|
+
* Hub/scheduler/token context is resolved the standard way: explicit flags >
|
|
21
|
+
* saved hub profile (see profile.ts / `robopark hub-use`) > discoverContext()
|
|
22
|
+
* auto-discovery.
|
|
23
|
+
*/
|
|
24
|
+
import chalk from 'chalk';
|
|
25
|
+
import { resolveContext } from './profile.js';
|
|
26
|
+
import { roboparkSetup } from './setup.js';
|
|
27
|
+
async function mintEnrollmentToken(schedulerUrl, name) {
|
|
28
|
+
const url = `${schedulerUrl.replace(/\/$/, '')}/api/devices`;
|
|
29
|
+
const res = await fetch(url, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'content-type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({ name }),
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const text = await res.text().catch(() => '');
|
|
36
|
+
throw new Error(`${res.status}: ${text || res.statusText}`);
|
|
37
|
+
}
|
|
38
|
+
return (await res.json());
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build a Windows-task-friendly command string from an argv array.
|
|
42
|
+
*
|
|
43
|
+
* Mirrors setup.ts's local `commandString()` helper (kept private there) so
|
|
44
|
+
* the one-liner this command prints looks exactly like the ones `robopark
|
|
45
|
+
* setup` itself prints — same quoting convention, so it's paste-safe on both
|
|
46
|
+
* POSIX and Windows shells.
|
|
47
|
+
*/
|
|
48
|
+
function commandString(argv) {
|
|
49
|
+
return argv.map(a => /[^a-zA-Z0-9_./:=,-]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a).join(' ');
|
|
50
|
+
}
|
|
51
|
+
export async function roboparkAddRobot(config, opts) {
|
|
52
|
+
console.log(chalk.bold('\n robopark add-robot'));
|
|
53
|
+
console.log(chalk.dim(' ' + '─'.repeat(52)));
|
|
54
|
+
if (!opts.name || !opts.name.trim()) {
|
|
55
|
+
console.log(chalk.red(' ✗ --name is required, e.g. --name robobmw'));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const ctx = await resolveContext(opts);
|
|
59
|
+
if (!ctx.schedulerUrl) {
|
|
60
|
+
console.log(chalk.red(' ✗ no scheduler URL resolved. Pass --scheduler-url/--hub-url, run `robopark hub-use` first, or run this on/near the hub.'));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
console.log(` scheduler: ${chalk.cyan(ctx.schedulerUrl)}`);
|
|
64
|
+
console.log(` name: ${chalk.cyan(opts.name)}${ctx.site ? chalk.dim(' @ ' + ctx.site) : ''}`);
|
|
65
|
+
console.log();
|
|
66
|
+
let minted;
|
|
67
|
+
try {
|
|
68
|
+
minted = await mintEnrollmentToken(ctx.schedulerUrl, opts.name);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
console.log(chalk.red(` ✗ failed to mint enrollment token: ${e.message}`));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// The API returns this once and does not store it in plaintext — always
|
|
76
|
+
// show it, even when auto-starting, so it's available for reference or a
|
|
77
|
+
// future re-enrollment against the same scheduler.
|
|
78
|
+
console.log(chalk.green(` ✓ minted enrollment token for '${opts.name}' (device ${minted.id})`));
|
|
79
|
+
console.log(` enrollment token: ${chalk.yellow(minted.enrollment_token)}`);
|
|
80
|
+
console.log();
|
|
81
|
+
if (opts.start) {
|
|
82
|
+
console.log(chalk.dim(' --start given — starting robot node on this machine…\n'));
|
|
83
|
+
await roboparkSetup(config, {
|
|
84
|
+
role: 'robot',
|
|
85
|
+
name: opts.name,
|
|
86
|
+
site: ctx.site,
|
|
87
|
+
hub: ctx.hubUrl,
|
|
88
|
+
token: ctx.token,
|
|
89
|
+
schedulerPort: ctx.schedulerUrl ? String(new URL(ctx.schedulerUrl).port || '8080') : undefined,
|
|
90
|
+
enrollmentToken: minted.enrollment_token,
|
|
91
|
+
start: true,
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Default: print the ready-to-paste one-liner for running on the actual
|
|
96
|
+
// (different) robot machine, in the same format `robopark setup` itself
|
|
97
|
+
// prints/expects.
|
|
98
|
+
const argv = ['robopark', 'setup', '--robot', '--name', opts.name];
|
|
99
|
+
if (ctx.site)
|
|
100
|
+
argv.push('--site', ctx.site);
|
|
101
|
+
if (ctx.hubUrl)
|
|
102
|
+
argv.push('--hub-url', ctx.hubUrl);
|
|
103
|
+
if (ctx.token)
|
|
104
|
+
argv.push('--token', ctx.token);
|
|
105
|
+
if (ctx.schedulerUrl) {
|
|
106
|
+
const schedulerPort = new URL(ctx.schedulerUrl).port || '8080';
|
|
107
|
+
argv.push('--scheduler-port', schedulerPort);
|
|
108
|
+
}
|
|
109
|
+
argv.push('--enrollment-token', minted.enrollment_token, '--start', '--auto-start');
|
|
110
|
+
console.log(chalk.dim(' run this ON THE ROBOT:'));
|
|
111
|
+
console.log(' ' + chalk.cyan(commandString(argv)));
|
|
112
|
+
console.log();
|
|
113
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface AgentUpOptions {
|
|
2
|
+
path: string;
|
|
3
|
+
schedulerUrl?: string;
|
|
4
|
+
hubUrl?: string;
|
|
5
|
+
token?: string;
|
|
6
|
+
serverId?: string;
|
|
7
|
+
serverName?: string;
|
|
8
|
+
register?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface AgentDownOptions {
|
|
11
|
+
path: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AgentLogsOptions {
|
|
14
|
+
path: string;
|
|
15
|
+
follow?: boolean;
|
|
16
|
+
service?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function roboparkAgentUp(opts: AgentUpOptions): Promise<void>;
|
|
19
|
+
export declare function roboparkAgentDown(opts: AgentDownOptions): Promise<void>;
|
|
20
|
+
export declare function roboparkAgentLogs(opts: AgentLogsOptions): Promise<void>;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoboPark — `robopark agent up/down/logs`.
|
|
3
|
+
*
|
|
4
|
+
* ROBOVOICE (the production voice stack: LiveKit + STT/TTS + agent) today
|
|
5
|
+
* requires an operator to manually `docker compose up -d` in that checkout,
|
|
6
|
+
* then manually `curl POST /api/servers` on the RoboPark scheduler to
|
|
7
|
+
* register the LiveKit instance it brought up. This file collapses both
|
|
8
|
+
* steps into one command so a hub/GPU machine only needs:
|
|
9
|
+
*
|
|
10
|
+
* robopark setup --role hub
|
|
11
|
+
* robopark agent-up --path C:\path\to\ROBOVOICE
|
|
12
|
+
*
|
|
13
|
+
* to be fully wired end to end.
|
|
14
|
+
*
|
|
15
|
+
* Reuses `roboparkServerAdd` (server-add.ts) for the exact POST /api/servers
|
|
16
|
+
* shape, and `resolveContext` (profile.ts) for hub/scheduler/token
|
|
17
|
+
* resolution with saved hub-profile fallback — nothing about the
|
|
18
|
+
* registration mechanics is reimplemented here.
|
|
19
|
+
*/
|
|
20
|
+
import { spawn, execFile } from 'node:child_process';
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
22
|
+
import { hostname, networkInterfaces } from 'node:os';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import chalk from 'chalk';
|
|
25
|
+
import { roboparkServerAdd } from './server-add.js';
|
|
26
|
+
import { resolveContext } from './profile.js';
|
|
27
|
+
const LIVEKIT_PORT = 7880;
|
|
28
|
+
const LIVEKIT_DEV_API_KEY = 'devkey';
|
|
29
|
+
const LIVEKIT_DEV_API_SECRET = 'secret';
|
|
30
|
+
/** This machine's first non-internal IPv4 LAN address, if any. */
|
|
31
|
+
function getLanIps() {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const addrs of Object.values(networkInterfaces())) {
|
|
34
|
+
if (!addrs)
|
|
35
|
+
continue;
|
|
36
|
+
for (const a of addrs) {
|
|
37
|
+
if (a.family === 'IPv4' && !a.internal)
|
|
38
|
+
out.push(a.address);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** Run `cmd version` quietly; resolve true if it exits 0. */
|
|
44
|
+
function commandWorks(cmd, args) {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
execFile(cmd, args, { timeout: 8000 }, (err) => resolve(!err));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Detect whether to use Docker Compose v2 (`docker compose`, space-separated
|
|
51
|
+
* subcommand) or the legacy v1 standalone `docker-compose` binary. v2 is
|
|
52
|
+
* tried first since it's bundled with modern Docker Desktop/Engine; v1 is
|
|
53
|
+
* the fallback for older hosts that only have the standalone binary.
|
|
54
|
+
*/
|
|
55
|
+
async function resolveComposeCommand() {
|
|
56
|
+
if (await commandWorks('docker', ['compose', 'version'])) {
|
|
57
|
+
return { cmd: 'docker', baseArgs: ['compose'] };
|
|
58
|
+
}
|
|
59
|
+
if (await commandWorks('docker-compose', ['version'])) {
|
|
60
|
+
return { cmd: 'docker-compose', baseArgs: [] };
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/** Spawn a compose subcommand, streaming stdout/stderr to the console. */
|
|
65
|
+
function runCompose(compose, args, cwd) {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const proc = spawn(compose.cmd, [...compose.baseArgs, ...args], {
|
|
68
|
+
cwd,
|
|
69
|
+
stdio: 'inherit',
|
|
70
|
+
});
|
|
71
|
+
proc.on('error', reject);
|
|
72
|
+
proc.on('close', (code) => resolve(code ?? 1));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function sleep(ms) {
|
|
76
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
77
|
+
}
|
|
78
|
+
/** Poll http://localhost:<port> until it responds (any HTTP status counts as "up" — LiveKit's bare HTTP port returns 404 for GET, which is fine). */
|
|
79
|
+
async function waitForLivekitHealthy(port, timeoutMs, intervalMs) {
|
|
80
|
+
const deadline = Date.now() + timeoutMs;
|
|
81
|
+
while (Date.now() < deadline) {
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(`http://localhost:${port}/`, { signal: AbortSignal.timeout(2000) });
|
|
84
|
+
// LiveKit's plain HTTP listener answers (even 4xx) once the process is up.
|
|
85
|
+
if (res.status < 500)
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// not up yet — fall through to retry
|
|
90
|
+
}
|
|
91
|
+
await sleep(intervalMs);
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
/** Validate that `path` looks like a ROBOVOICE checkout (has a docker-compose.yaml). */
|
|
96
|
+
function validateComposePath(path) {
|
|
97
|
+
const candidates = ['docker-compose.yaml', 'docker-compose.yml'];
|
|
98
|
+
for (const name of candidates) {
|
|
99
|
+
const full = join(path, name);
|
|
100
|
+
if (existsSync(full))
|
|
101
|
+
return full;
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`no docker-compose.yaml/.yml found in '${path}'. Pass --path pointing at a ROBOVOICE checkout ` +
|
|
104
|
+
`(the directory containing docker-compose.yaml), not this repo.`);
|
|
105
|
+
}
|
|
106
|
+
export async function roboparkAgentUp(opts) {
|
|
107
|
+
console.log(chalk.bold('\n robopark agent up'));
|
|
108
|
+
console.log(chalk.dim(' ' + '─'.repeat(52)));
|
|
109
|
+
if (!existsSync(opts.path)) {
|
|
110
|
+
console.log(chalk.red(` ✗ path does not exist: ${opts.path}`));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
validateComposePath(opts.path);
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
console.log(chalk.red(` ✗ ${e.message}`));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
const compose = await resolveComposeCommand();
|
|
121
|
+
if (!compose) {
|
|
122
|
+
console.log(chalk.red(' ✗ neither `docker compose` (v2) nor `docker-compose` (v1) is available on PATH.'));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
console.log(chalk.dim(` using: ${compose.cmd} ${compose.baseArgs.join(' ')}`.trimEnd()));
|
|
126
|
+
console.log(` path: ${chalk.cyan(opts.path)}`);
|
|
127
|
+
console.log();
|
|
128
|
+
console.log(chalk.bold(' 1. starting ROBOVOICE docker stack (docker compose up -d)…'));
|
|
129
|
+
const upCode = await runCompose(compose, ['up', '-d'], opts.path);
|
|
130
|
+
if (upCode !== 0) {
|
|
131
|
+
console.log(chalk.red(`\n ✗ docker compose up -d exited with code ${upCode}`));
|
|
132
|
+
process.exit(upCode);
|
|
133
|
+
}
|
|
134
|
+
console.log(chalk.green(' ✓ docker compose up -d completed'));
|
|
135
|
+
console.log();
|
|
136
|
+
console.log(chalk.bold(` 2. waiting for LiveKit to become healthy on :${LIVEKIT_PORT} (up to 60s)…`));
|
|
137
|
+
const healthy = await waitForLivekitHealthy(LIVEKIT_PORT, 60_000, 3_000);
|
|
138
|
+
if (!healthy) {
|
|
139
|
+
console.log(chalk.red(` ✗ LiveKit did not respond on http://localhost:${LIVEKIT_PORT} within 60s.`));
|
|
140
|
+
console.log(chalk.dim(` check container health: ${compose.cmd} ${compose.baseArgs.join(' ')} ps`.trim()));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
console.log(chalk.green(` ✓ LiveKit is up on :${LIVEKIT_PORT}`));
|
|
144
|
+
if (opts.register === false) {
|
|
145
|
+
console.log();
|
|
146
|
+
console.log(chalk.dim(' --no-register passed; skipping scheduler registration.'));
|
|
147
|
+
console.log(chalk.green('\n ✓ robopark agent up complete (docker only)\n'));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
console.log();
|
|
151
|
+
console.log(chalk.bold(' 3. registering LiveKit server with the RoboPark scheduler…'));
|
|
152
|
+
const ctx = await resolveContext({ hubUrl: opts.hubUrl, token: opts.token, schedulerUrl: opts.schedulerUrl });
|
|
153
|
+
if (!ctx.schedulerUrl) {
|
|
154
|
+
console.log(chalk.red(' ✗ no scheduler URL resolved. Pass --scheduler-url/--hub-url, or run `robopark setup --role hub` first.'));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
// Advertise this machine's own reachable address. This is a best-effort
|
|
158
|
+
// guess (first non-internal LAN IPv4); if the scheduler runs on a
|
|
159
|
+
// *different* machine than the one running `agent up` and that machine
|
|
160
|
+
// can't reach this LAN IP (e.g. NAT, VPN split), correct the registration
|
|
161
|
+
// manually afterwards with `robopark server-add --id <id> --url ws://<correct-host>:7880`.
|
|
162
|
+
const lanIps = getLanIps();
|
|
163
|
+
const advertiseHost = lanIps[0] ?? hostname();
|
|
164
|
+
if (lanIps.length === 0) {
|
|
165
|
+
console.log(chalk.yellow(` ⚠ no LAN IPv4 detected; advertising hostname '${advertiseHost}'. Correct with 'robopark server-add' if the scheduler can't resolve it.`));
|
|
166
|
+
}
|
|
167
|
+
const id = opts.serverId ?? `robovoice-${hostname()}`;
|
|
168
|
+
const name = opts.serverName ?? `ROBOVOICE (${hostname()})`;
|
|
169
|
+
const url = `ws://${advertiseHost}:${LIVEKIT_PORT}`;
|
|
170
|
+
await roboparkServerAdd({
|
|
171
|
+
hubUrl: ctx.hubUrl,
|
|
172
|
+
schedulerUrl: ctx.schedulerUrl,
|
|
173
|
+
token: ctx.token,
|
|
174
|
+
id,
|
|
175
|
+
name,
|
|
176
|
+
url,
|
|
177
|
+
apiKey: LIVEKIT_DEV_API_KEY,
|
|
178
|
+
apiSecret: LIVEKIT_DEV_API_SECRET,
|
|
179
|
+
});
|
|
180
|
+
console.log(chalk.green('\n ✓ robopark agent up complete — ROBOVOICE is running and registered with the scheduler\n'));
|
|
181
|
+
}
|
|
182
|
+
export async function roboparkAgentDown(opts) {
|
|
183
|
+
console.log(chalk.bold('\n robopark agent down'));
|
|
184
|
+
console.log(chalk.dim(' ' + '─'.repeat(52)));
|
|
185
|
+
if (!existsSync(opts.path)) {
|
|
186
|
+
console.log(chalk.red(` ✗ path does not exist: ${opts.path}`));
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
validateComposePath(opts.path);
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
console.log(chalk.red(` ✗ ${e.message}`));
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
const compose = await resolveComposeCommand();
|
|
197
|
+
if (!compose) {
|
|
198
|
+
console.log(chalk.red(' ✗ neither `docker compose` (v2) nor `docker-compose` (v1) is available on PATH.'));
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
console.log(` path: ${chalk.cyan(opts.path)}`);
|
|
202
|
+
console.log();
|
|
203
|
+
const code = await runCompose(compose, ['down'], opts.path);
|
|
204
|
+
if (code !== 0) {
|
|
205
|
+
console.log(chalk.red(`\n ✗ docker compose down exited with code ${code}`));
|
|
206
|
+
process.exit(code);
|
|
207
|
+
}
|
|
208
|
+
console.log(chalk.green('\n ✓ ROBOVOICE stack stopped\n'));
|
|
209
|
+
}
|
|
210
|
+
export async function roboparkAgentLogs(opts) {
|
|
211
|
+
if (!existsSync(opts.path)) {
|
|
212
|
+
console.log(chalk.red(` ✗ path does not exist: ${opts.path}`));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
validateComposePath(opts.path);
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
console.log(chalk.red(` ✗ ${e.message}`));
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
const compose = await resolveComposeCommand();
|
|
223
|
+
if (!compose) {
|
|
224
|
+
console.log(chalk.red(' ✗ neither `docker compose` (v2) nor `docker-compose` (v1) is available on PATH.'));
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
const args = ['logs'];
|
|
228
|
+
if (opts.follow)
|
|
229
|
+
args.push('-f');
|
|
230
|
+
if (opts.service)
|
|
231
|
+
args.push(opts.service);
|
|
232
|
+
// Live tail: no buffering, no capture — inherit stdio directly.
|
|
233
|
+
const code = await runCompose(compose, args, opts.path);
|
|
234
|
+
if (code !== 0 && !opts.follow) {
|
|
235
|
+
process.exit(code);
|
|
236
|
+
}
|
|
237
|
+
}
|