Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 92 additions & 7 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,40 @@ const EXPERIMENTAL = process.env.NEMOCLAW_EXPERIMENTAL === "1";

// ── Helpers ──────────────────────────────────────────────────────

/**
* Print a numbered step banner to the console.
*
* @param {number} n Current step number.
* @param {number} total Total number of steps.
* @param {string} msg Description of the step.
*/
function step(n, total, msg) {
console.log("");
console.log(` [${n}/${total}] ${msg}`);
console.log(` ${"─".repeat(50)}`);
}

function isDockerRunning() {
/** Detect which container runtime is available. Returns "docker", "podman", or null. */
function detectContainerRuntime() {
try {
runCapture("docker info", { ignoreError: false });
return true;
return "docker";
} catch {
return false;
// Podman fallback — rootless Podman can substitute for Docker
try {
runCapture("podman info", { ignoreError: false });
return "podman";
} catch {
return null;
}
}
}

/**
* Check whether the openshell CLI is available on PATH.
*
* @returns {boolean} True if `command -v openshell` succeeds.
*/
function isOpenshellInstalled() {
try {
runCapture("command -v openshell");
Expand All @@ -40,6 +59,11 @@ function isOpenshellInstalled() {
}
}

/**
* Attempt to install the openshell CLI via the bundled install script.
*
* @returns {boolean} True if openshell is available after installation.
*/
function installOpenshell() {
console.log(" Installing openshell CLI...");
run(`bash "${path.join(SCRIPTS, "install-openshell.sh")}"`, { ignoreError: true });
Expand All @@ -48,15 +72,25 @@ function installOpenshell() {

// ── Step 1: Preflight ────────────────────────────────────────────

/**
* Step 1: Run preflight checks (container runtime, openshell CLI, cgroup, GPU).
*
* @returns {Promise<object|null>} Detected GPU descriptor, or null if none found.
*/
async function preflight() {
step(1, 7, "Preflight checks");

// Docker
if (!isDockerRunning()) {
console.error(" Docker is not running. Please start Docker and try again.");
// Container runtime
const runtime = detectContainerRuntime();
if (!runtime) {
console.error(" No container runtime found. Please install Docker or Podman and try again.");
process.exit(1);
}
console.log(" ✓ Docker is running");
if (runtime === "podman") {
console.log(" ✓ Podman is running (note: --add-host=host-gateway may not resolve, see issue #116)");
} else {
console.log(" ✓ Docker is running");
}

// OpenShell CLI
if (!isOpenshellInstalled()) {
Expand Down Expand Up @@ -106,6 +140,12 @@ async function preflight() {

// ── Step 2: Gateway ──────────────────────────────────────────────

/**
* Step 2: Start (or restart) the OpenShell gateway and verify health.
*
* @param {object|null} gpu GPU descriptor from preflight (unused but reserved).
* @returns {Promise<void>}
*/
async function startGateway(gpu) {
step(2, 7, "Starting OpenShell gateway");

Expand Down Expand Up @@ -152,6 +192,12 @@ async function startGateway(gpu) {

// ── Step 3: Sandbox ──────────────────────────────────────────────

/**
* Step 3: Prompt for a sandbox name, build the image, and create the sandbox.
*
* @param {object|null} gpu GPU descriptor from preflight.
* @returns {Promise<string>} The validated sandbox name.
*/
async function createSandbox(gpu) {
step(3, 7, "Creating sandbox");

Expand Down Expand Up @@ -232,6 +278,13 @@ async function createSandbox(gpu) {

// ── Step 4: NIM ──────────────────────────────────────────────────

/**
* Step 4: Detect or prompt for an inference backend (NIM, Ollama, vLLM, cloud).
*
* @param {string} sandboxName Name of the active sandbox.
* @param {object|null} gpu GPU descriptor from preflight.
* @returns {Promise<{model: string, provider: string}>} Selected model and provider.
*/
async function setupNim(sandboxName, gpu) {
step(4, 7, "Configuring inference (NIM)");

Expand Down Expand Up @@ -365,6 +418,14 @@ async function setupNim(sandboxName, gpu) {

// ── Step 5: Inference provider ───────────────────────────────────

/**
* Step 5: Register the selected inference provider with openshell.
*
* @param {string} sandboxName Name of the active sandbox.
* @param {string} model Model identifier (e.g. "nvidia/nemotron-3-super-120b-a12b").
* @param {string} provider Provider key ("nvidia-nim", "vllm-local", "ollama-local").
* @returns {Promise<void>}
*/
async function setupInference(sandboxName, model, provider) {
step(5, 7, "Setting up inference provider");

Expand Down Expand Up @@ -414,6 +475,12 @@ async function setupInference(sandboxName, model, provider) {

// ── Step 6: OpenClaw ─────────────────────────────────────────────

/**
* Step 6: Launch the OpenClaw agent gateway inside the sandbox.
*
* @param {string} sandboxName Name of the active sandbox.
* @returns {Promise<void>}
*/
async function setupOpenclaw(sandboxName) {
step(6, 7, "Setting up OpenClaw inside sandbox");

Expand All @@ -427,6 +494,12 @@ async function setupOpenclaw(sandboxName) {

// ── Step 7: Policy presets ───────────────────────────────────────

/**
* Step 7: Suggest and apply policy presets (pypi, npm, messaging integrations).
*
* @param {string} sandboxName Name of the active sandbox.
* @returns {Promise<void>}
*/
async function setupPolicies(sandboxName) {
step(7, 7, "Policy presets");

Expand Down Expand Up @@ -484,6 +557,13 @@ async function setupPolicies(sandboxName) {

// ── Dashboard ────────────────────────────────────────────────────

/**
* Print a summary dashboard showing sandbox, model, and NIM status.
*
* @param {string} sandboxName Name of the active sandbox.
* @param {string} model Active model identifier.
* @param {string} provider Active provider key.
*/
function printDashboard(sandboxName, model, provider) {
const nimStat = nim.nimStatus(sandboxName);
const nimLabel = nimStat.running ? "running" : "not running";
Expand All @@ -508,6 +588,11 @@ function printDashboard(sandboxName, model, provider) {

// ── Main ─────────────────────────────────────────────────────────

/**
* Run the full 7-step interactive onboarding wizard.
*
* @returns {Promise<void>}
*/
async function onboard() {
console.log("");
console.log(" NemoClaw Onboarding");
Expand Down
43 changes: 36 additions & 7 deletions bin/lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,49 @@ const fs = require("fs");
const ROOT = path.resolve(__dirname, "..", "..");
const SCRIPTS = path.join(ROOT, "scripts");

// Auto-detect Colima Docker socket (legacy ~/.colima or XDG ~/.config/colima)
if (!process.env.DOCKER_HOST) {
const home = process.env.HOME || "/tmp";
/**
* Detect a container runtime socket (Colima first, then Docker Desktop, then Podman).
* Returns the socket path or null.
*
* @param {object} [opts] — DI overrides for testing
* @param {string} [opts.home] — HOME directory override
* @param {function} [opts.existsSync] — fs.existsSync override
* @param {number} [opts.uid] — process UID override for rootless Podman
*/
function detectContainerSocket(opts) {
const home = (opts && opts.home) || process.env.HOME || "/tmp";
const exists = (opts && opts.existsSync) || fs.existsSync;
const uid = (opts && opts.uid !== undefined) ? opts.uid : (process.getuid ? process.getuid() : 1000);

const candidates = [
// Colima (preferred — existing behavior)
path.join(home, ".colima/default/docker.sock"),
path.join(home, ".config/colima/default/docker.sock"),
// Docker Desktop (macOS)
path.join(home, ".docker/run/docker.sock"),
// Podman machine
path.join(home, ".local/share/containers/podman/machine/podman.sock"),
`/run/user/${uid}/podman/podman.sock`,
path.join(home, ".local/share/containers/podman/machine/qemu/podman.sock"),
];

for (const sock of candidates) {
if (fs.existsSync(sock)) {
process.env.DOCKER_HOST = `unix://${sock}`;
break;
if (exists(sock)) {
return sock;
}
}
return null;
}

// Auto-detect container socket if DOCKER_HOST not already set
if (!process.env.DOCKER_HOST) {
const sock = detectContainerSocket();
if (sock) {
process.env.DOCKER_HOST = `unix://${sock}`;
}
}

/** Run a shell command with inherited stdio, exiting on failure unless opts.ignoreError is set. */
function run(cmd, opts = {}) {
const result = spawnSync("bash", ["-c", cmd], {
stdio: "inherit",
Expand All @@ -37,6 +65,7 @@ function run(cmd, opts = {}) {
return result;
}

/** Run a shell command and return its trimmed stdout. Returns "" on error when opts.ignoreError is set. */
function runCapture(cmd, opts = {}) {
try {
return execSync(cmd, {
Expand All @@ -52,4 +81,4 @@ function runCapture(cmd, opts = {}) {
}
}

module.exports = { ROOT, SCRIPTS, run, runCapture };
module.exports = { ROOT, SCRIPTS, run, runCapture, detectContainerSocket };
44 changes: 44 additions & 0 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ const GLOBAL_COMMANDS = new Set([

// ── Commands ─────────────────────────────────────────────────────

/** Launch the interactive onboarding wizard. */
async function onboard() {
const { onboard: runOnboard } = require("./lib/onboard");
await runOnboard();
}

/** Run the deprecated legacy setup.sh for backwards compatibility. */
async function setup() {
console.log("");
console.log(" ⚠ `nemoclaw setup` is deprecated. Use `nemoclaw onboard` instead.");
Expand All @@ -42,11 +44,17 @@ async function setup() {
run(`bash "${SCRIPTS}/setup.sh"`);
}

/** Run the DGX Spark setup script (cgroup v2 fix + Docker restart). */
async function setupSpark() {
await ensureApiKey();
run(`sudo -E NVIDIA_API_KEY="${process.env.NVIDIA_API_KEY}" bash "${SCRIPTS}/setup-spark.sh"`);
}

/**
* Deploy NemoClaw to a remote Brev GPU instance, sync files, and connect.
*
* @param {string} instanceName Brev instance name to create or reuse.
*/
async function deploy(instanceName) {
if (!instanceName) {
console.error(" Usage: nemoclaw deploy <instance-name>");
Expand Down Expand Up @@ -132,15 +140,18 @@ async function deploy(instanceName) {
run(`ssh -t -o StrictHostKeyChecking=no -o LogLevel=ERROR ${name} 'cd /home/ubuntu/nemoclaw && set -a && . .env && set +a && openshell sandbox connect nemoclaw'`);
}

/** Start background services (Telegram bridge, tunnel). */
async function start() {
await ensureApiKey();
run(`bash "${SCRIPTS}/start-services.sh"`);
}

/** Stop all running NemoClaw services. */
function stop() {
run(`bash "${SCRIPTS}/start-services.sh" --stop`);
}

/** Display registered sandboxes and current service status. */
function showStatus() {
// Show sandbox registry
const { sandboxes, defaultSandbox } = registry.listSandboxes();
Expand All @@ -159,6 +170,7 @@ function showStatus() {
run(`bash "${SCRIPTS}/start-services.sh" --status`);
}

/** List all registered sandboxes with model, provider, and policy details. */
function listSandboxes() {
const { sandboxes, defaultSandbox } = registry.listSandboxes();
if (sandboxes.length === 0) {
Expand Down Expand Up @@ -186,12 +198,22 @@ function listSandboxes() {

// ── Sandbox-scoped actions ───────────────────────────────────────

/**
* Ensure the port forward is alive, then open an interactive connection.
*
* @param {string} sandboxName Target sandbox name.
*/
function sandboxConnect(sandboxName) {
// Ensure port forward is alive before connecting
run(`openshell forward start --background 18789 "${sandboxName}" 2>/dev/null || true`, { ignoreError: true });
run(`openshell sandbox connect "${sandboxName}"`);
}

/**
* Print detailed status for a sandbox (registry info, openshell state, NIM health).
*
* @param {string} sandboxName Target sandbox name.
*/
function sandboxStatus(sandboxName) {
const sb = registry.getSandbox(sandboxName);
if (sb) {
Expand All @@ -215,11 +237,22 @@ function sandboxStatus(sandboxName) {
console.log("");
}

/**
* Stream sandbox logs, optionally following in real time.
*
* @param {string} sandboxName Target sandbox name.
* @param {boolean} follow If true, pass --follow to tail logs.
*/
function sandboxLogs(sandboxName, follow) {
const followFlag = follow ? " --follow" : "";
run(`openshell sandbox logs "${sandboxName}"${followFlag}`);
}

/**
* Interactively add a policy preset to a sandbox.
*
* @param {string} sandboxName Target sandbox name.
*/
async function sandboxPolicyAdd(sandboxName) {
const allPresets = policies.listPresets();
const applied = policies.getAppliedPresets(sandboxName);
Expand All @@ -242,6 +275,11 @@ async function sandboxPolicyAdd(sandboxName) {
policies.applyPreset(sandboxName, answer);
}

/**
* List all policy presets, marking which are applied to the given sandbox.
*
* @param {string} sandboxName Target sandbox name.
*/
function sandboxPolicyList(sandboxName) {
const allPresets = policies.listPresets();
const applied = policies.getAppliedPresets(sandboxName);
Expand All @@ -255,6 +293,11 @@ function sandboxPolicyList(sandboxName) {
console.log("");
}

/**
* Stop the NIM container, delete the sandbox, and remove it from the registry.
*
* @param {string} sandboxName Target sandbox name.
*/
function sandboxDestroy(sandboxName) {
console.log(` Stopping NIM for '${sandboxName}'...`);
nim.stopNimContainer(sandboxName);
Expand All @@ -268,6 +311,7 @@ function sandboxDestroy(sandboxName) {

// ── Help ─────────────────────────────────────────────────────────

/** Print CLI usage information. */
function help() {
console.log(`
nemoclaw — NemoClaw CLI
Expand Down
Loading