From 12fe0c3b8697b0102e54a094da8da6da61f911d7 Mon Sep 17 00:00:00 2001 From: Scott White Date: Sun, 22 Mar 2026 22:14:48 -0700 Subject: [PATCH 1/3] feat: add Nvidia Jetson Thor Developer Kit GPU detection and runtime support --- bin/lib/nim.js | 134 +++++++++++++++++++++++++++++++++++++++++-------- install.sh | 133 ++++++++++++++++++++++++++++++++++-------------- 2 files changed, 210 insertions(+), 57 deletions(-) diff --git a/bin/lib/nim.js b/bin/lib/nim.js index 548b2db23..f19ef4ae6 100644 --- a/bin/lib/nim.js +++ b/bin/lib/nim.js @@ -24,7 +24,45 @@ function listModels() { } function detectGpu() { - // Try NVIDIA first — query VRAM + function isJetsonPlatform() { + try { + const nvTegra = runCapture("test -f /etc/nv_tegra_release && echo yes", { + ignoreError: true, + }); + if (nvTegra && nvTegra.trim() === "yes") return true; + } catch {} + + try { + const compat = runCapture("tr '\\0' '\\n' < /proc/device-tree/compatible", { + ignoreError: true, + }); + if (compat && /nvidia,tegra|nvidia,thor/i.test(compat)) return true; + } catch {} + + try { + const model = runCapture("tr '\\0' '\\n' < /proc/device-tree/model", { + ignoreError: true, + }); + if (model && /jetson|thor|nvidia/i.test(model)) return true; + } catch {} + + return false; + } + + function getSystemMemoryMB() { + try { + const memLine = runCapture("awk '/MemTotal:/ {print $2}' /proc/meminfo", { + ignoreError: true, + }); + if (memLine) { + const memKB = parseInt(memLine.trim(), 10); + if (!isNaN(memKB) && memKB > 0) return Math.floor(memKB / 1024); + } + } catch {} + return 0; + } + + // 1) Standard NVIDIA path — query VRAM with nvidia-smi try { const output = runCapture( "nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits", @@ -32,7 +70,10 @@ function detectGpu() { ); if (output) { const lines = output.split("\n").filter((l) => l.trim()); - const perGpuMB = lines.map((l) => parseInt(l.trim(), 10)).filter((n) => !isNaN(n)); + const perGpuMB = lines + .map((l) => parseInt(l.trim(), 10)) + .filter((n) => !isNaN(n) && n > 0); + if (perGpuMB.length > 0) { const totalMemoryMB = perGpuMB.reduce((a, b) => a + b, 0); return { @@ -46,19 +87,55 @@ function detectGpu() { } } catch {} - // Fallback: DGX Spark (GB10) — VRAM not queryable due to unified memory architecture + // 2) Jetson / Thor fallback + try { + if (process.platform === "linux" && isJetsonPlatform()) { + let gpuCount = 1; + + try { + const listOutput = runCapture("nvidia-smi -L", { ignoreError: true }); + if (listOutput) { + const lines = listOutput.split("\n").filter((l) => l.trim()); + if (lines.length > 0) gpuCount = lines.length; + } + } catch {} + + const totalMemoryMB = Math.floor(getSystemMemoryMB() / 2); + + if (totalMemoryMB > 0) { + return { + type: "nvidia", + name: "NVIDIA Jetson", + count: gpuCount, + totalMemoryMB, + perGpuMB: Math.floor(totalMemoryMB / gpuCount), + nimCapable: true, + jetson: true, + unifiedMemory: true, + }; + } + + return { + type: "nvidia", + name: "NVIDIA Jetson", + count: gpuCount, + totalMemoryMB: 0, + perGpuMB: 0, + nimCapable: true, + jetson: true, + unifiedMemory: true, + }; + } + } catch {} + + // 3) Fallback: DGX Spark (GB10) try { const nameOutput = runCapture( "nvidia-smi --query-gpu=name --format=csv,noheader,nounits", { ignoreError: true } ); if (nameOutput && nameOutput.includes("GB10")) { - // GB10 has 128GB unified memory shared with Grace CPU — use system RAM - let totalMemoryMB = 0; - try { - const memLine = runCapture("free -m | awk '/Mem:/ {print $2}'", { ignoreError: true }); - if (memLine) totalMemoryMB = parseInt(memLine.trim(), 10) || 0; - } catch {} + const totalMemoryMB = getSystemMemoryMB(); return { type: "nvidia", count: 1, @@ -66,17 +143,17 @@ function detectGpu() { perGpuMB: totalMemoryMB, nimCapable: true, spark: true, + unifiedMemory: true, }; } } catch {} - // macOS: detect Apple Silicon or discrete GPU + // 4) macOS: detect Apple Silicon or discrete GPU if (process.platform === "darwin") { try { - const spOutput = runCapture( - "system_profiler SPDisplaysDataType 2>/dev/null", - { ignoreError: true } - ); + const spOutput = runCapture("system_profiler SPDisplaysDataType", { + ignoreError: true, + }); if (spOutput) { const chipMatch = spOutput.match(/Chipset Model:\s*(.+)/); const vramMatch = spOutput.match(/VRAM.*?:\s*(\d+)\s*(MB|GB)/i); @@ -90,10 +167,13 @@ function detectGpu() { memoryMB = parseInt(vramMatch[1], 10); if (vramMatch[2].toUpperCase() === "GB") memoryMB *= 1024; } else { - // Apple Silicon shares system RAM — read total memory try { - const memBytes = runCapture("sysctl -n hw.memsize", { ignoreError: true }); - if (memBytes) memoryMB = Math.floor(parseInt(memBytes, 10) / 1024 / 1024); + const memBytes = runCapture("sysctl -n hw.memsize", { + ignoreError: true, + }); + if (memBytes) { + memoryMB = Math.floor(parseInt(memBytes, 10) / 1024 / 1024); + } } catch {} } @@ -125,6 +205,16 @@ function pullNimImage(model) { return image; } +function isJetsonPlatform() { + try { + const out = runCapture("test -f /etc/nv_tegra_release && echo yes", { + ignoreError: true, + }); + return !!out && out.trim() === "yes"; + } catch {} + return false; +} + function startNimContainer(sandboxName, model, port = 8000) { const name = containerName(sandboxName); const image = getImageForModel(model); @@ -133,14 +223,18 @@ function startNimContainer(sandboxName, model, port = 8000) { process.exit(1); } - // Stop any existing container with same name const qn = shellQuote(name); - run(`docker rm -f ${qn} 2>/dev/null || true`, { ignoreError: true }); + + // Stop any existing container with same name + run(`docker rm -f ${qn} || true`, { ignoreError: true }); console.log(` Starting NIM container: ${name}`); + + const runtimeArgs = isJetsonPlatform() ? "--runtime nvidia " : ""; run( - `docker run -d --gpus all -p ${Number(port)}:8000 --name ${qn} --shm-size 16g ${shellQuote(image)}` + `docker run -d --gpus all ${runtimeArgs}-p ${Number(port)}:8000 --name ${qn} --shm-size 16g ${shellQuote(image)}` ); + return name; } diff --git a/install.sh b/install.sh index 88c767980..4dd748b25 100755 --- a/install.sh +++ b/install.sh @@ -27,9 +27,9 @@ NEMOCLAW_VERSION="$(resolve_installer_version)" # --------------------------------------------------------------------------- if [[ -z "${NO_COLOR:-}" && -t 1 ]]; then if [[ "${COLORTERM:-}" == "truecolor" || "${COLORTERM:-}" == "24bit" ]]; then - C_GREEN=$'\033[38;2;118;185;0m' # #76B900 — exact NVIDIA green + C_GREEN=$'\033[38;2;118;185;0m' # #76B900 — exact NVIDIA green else - C_GREEN=$'\033[38;5;148m' # closest 256-color on dark backgrounds + C_GREEN=$'\033[38;5;148m' # closest 256-color on dark backgrounds fi C_BOLD=$'\033[1m' C_DIM=$'\033[2m' @@ -44,10 +44,13 @@ fi # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- -info() { printf "${C_CYAN}[INFO]${C_RESET} %s\n" "$*"; } -warn() { printf "${C_YELLOW}[WARN]${C_RESET} %s\n" "$*"; } -error() { printf "${C_RED}[ERROR]${C_RESET} %s\n" "$*" >&2; exit 1; } -ok() { printf " ${C_GREEN}✓${C_RESET} %s\n" "$*"; } +info() { printf "${C_CYAN}[INFO]${C_RESET} %s\n" "$*"; } +warn() { printf "${C_YELLOW}[WARN]${C_RESET} %s\n" "$*"; } +error() { + printf "${C_RED}[ERROR]${C_RESET} %s\n" "$*" >&2 + exit 1 +} +ok() { printf " ${C_GREEN}✓${C_RESET} %s\n" "$*"; } resolve_default_sandbox_name() { local registry_file="${HOME}/.nemoclaw/sandboxes.json" @@ -95,7 +98,7 @@ print_banner() { } print_done() { - local elapsed=$(( SECONDS - _INSTALL_START )) + local elapsed=$((SECONDS - _INSTALL_START)) local sandbox_name sandbox_name="$(resolve_default_sandbox_name)" info "=== Installation complete ===" @@ -146,7 +149,8 @@ usage() { # Stdout/stderr are captured; dumped only on failure. # Falls back to plain output when stdout is not a TTY (CI / piped installs). spin() { - local msg="$1"; shift + local msg="$1" + shift if [[ ! -t 1 ]]; then info "$msg" @@ -154,7 +158,8 @@ spin() { return fi - local log; log=$(mktemp) + local log + log=$(mktemp) "$@" >"$log" 2>&1 & local pid=$! i=0 local frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') @@ -191,12 +196,13 @@ ORIGINAL_PATH="${PATH:-}" # Compare two semver strings (major.minor.patch). Returns 0 if $1 >= $2. version_gte() { - local IFS=. - local -a a=($1) b=($2) + local -a a b + IFS='.' read -r -a a <<<"$1" + IFS='.' read -r -a b <<<"$2" for i in 0 1 2; do local ai=${a[$i]:-0} bi=${b[$i]:-0} - if (( ai > bi )); then return 0; fi - if (( ai < bi )); then return 1; fi + if ((ai > bi)); then return 0; fi + if ((ai < bi)); then return 1; fi done return 0 } @@ -265,7 +271,7 @@ ensure_supported_runtime() { [[ "$node_major" =~ ^[0-9]+$ ]] || error "Could not determine Node.js version from '${node_version}'. ${RUNTIME_REQUIREMENT_MSG}" [[ "$npm_major" =~ ^[0-9]+$ ]] || error "Could not determine npm version from '${npm_version}'. ${RUNTIME_REQUIREMENT_MSG}" - if (( node_major < MIN_NODE_MAJOR || npm_major < MIN_NPM_MAJOR )); then + if ((node_major < MIN_NODE_MAJOR || npm_major < MIN_NPM_MAJOR)); then error "Unsupported runtime detected: Node.js ${node_version:-unknown}, npm ${npm_version:-unknown}. ${RUNTIME_REQUIREMENT_MSG} Upgrade Node.js and rerun the installer." fi @@ -288,7 +294,10 @@ install_nodejs() { local nvm_tmp nvm_tmp="$(mktemp)" curl -fsSL "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh" -o "$nvm_tmp" \ - || { rm -f "$nvm_tmp"; error "Failed to download nvm installer"; } + || { + rm -f "$nvm_tmp" + error "Failed to download nvm installer" + } local actual_hash if command_exists sha256sum; then actual_hash="$(sha256sum "$nvm_tmp" | awk '{print $1}')" @@ -296,7 +305,7 @@ install_nodejs() { actual_hash="$(shasum -a 256 "$nvm_tmp" | awk '{print $1}')" else warn "No SHA-256 tool found — skipping nvm integrity check" - actual_hash="$NVM_SHA256" # allow execution + actual_hash="$NVM_SHA256" # allow execution fi if [[ "$actual_hash" != "$NVM_SHA256" ]]; then rm -f "$nvm_tmp" @@ -323,27 +332,69 @@ get_ollama_version() { } detect_gpu() { - # Returns 0 if a GPU is detected + # Returns 0 if a GPU is detected. + + # 1) Standard NVIDIA path if command_exists nvidia-smi; then - nvidia-smi &>/dev/null && return 0 + if nvidia-smi -L >/dev/stdout; then + return 0 + fi + fi + + # 2) Jetson / Thor fallback: NVIDIA L4T marker + if [[ -f /etc/nv_tegra_release ]]; then + return 0 + fi + + # 3) Jetson / Thor fallback: device-tree compatible/model + if [[ -r /proc/device-tree/compatible ]]; then + if tr '\0' '\n' /dev/null \ - | awk '{s += $1} END {print s+0}' - return + local smi_mem + smi_mem="$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits | awk '{s += $1} END {print s+0}')" + if [[ -n "${smi_mem:-}" && "$smi_mem" -gt 0 ]]; then + echo "$smi_mem" + return + fi fi - # macOS — report unified memory as VRAM + + # 2) Jetson / Thor fallback: use total system RAM as shared GPU memory budget proxy + if [[ -f /etc/nv_tegra_release ]] || [[ -r /proc/device-tree/model ]]; then + local mem_kb + mem_kb="$(awk '/MemTotal:/ {print $2}' /proc/meminfo)" + if [[ -n "${mem_kb:-}" && "$mem_kb" -gt 0 ]]; then + echo $((mem_kb / 1024)) + return + fi + fi + + # 3) macOS unified memory fallback if [[ "$(uname -s)" == "Darwin" ]] && command_exists sysctl; then local bytes - bytes=$(sysctl -n hw.memsize 2>/dev/null || echo 0) - echo $(( bytes / 1024 / 1024 )) + bytes="$(sysctl -n hw.memsize)" + echo $((bytes / 1024 / 1024)) return fi + echo 0 } @@ -373,10 +424,10 @@ install_or_upgrade_ollama() { # Pull the appropriate model based on VRAM local vram_mb vram_mb=$(get_vram_mb) - local vram_gb=$(( vram_mb / 1024 )) + local vram_gb=$((vram_mb / 1024)) info "Detected ${vram_gb} GB VRAM" - if (( vram_gb >= 120 )); then + if ((vram_gb >= 120)); then info "Pulling nemotron-3-super:120b…" ollama pull nemotron-3-super:120b else @@ -406,13 +457,12 @@ pre_extract_openclaw() { info "Pre-extracting openclaw@${openclaw_version} with system tar (GH-503 workaround)…" local tmpdir tmpdir="$(mktemp -d)" - if npm pack "openclaw@${openclaw_version}" --pack-destination "$tmpdir" > /dev/null 2>&1; then + if npm pack "openclaw@${openclaw_version}" --pack-destination "$tmpdir" >/dev/null 2>&1; then local tgz tgz="$(find "$tmpdir" -maxdepth 1 -name 'openclaw-*.tgz' -print -quit)" if [[ -n "$tgz" && -f "$tgz" ]]; then if mkdir -p "${install_dir}/node_modules/openclaw" \ - && tar xzf "$tgz" -C "${install_dir}/node_modules/openclaw" --strip-components=1 - then + && tar xzf "$tgz" -C "${install_dir}/node_modules/openclaw" --strip-components=1; then info "openclaw pre-extracted successfully" else warn "Failed to extract openclaw tarball" @@ -435,8 +485,8 @@ pre_extract_openclaw() { install_nemoclaw() { if [[ -f "./package.json" ]] && grep -q '"name": "nemoclaw"' ./package.json 2>/dev/null; then info "NemoClaw package.json found in current directory — installing from source…" - spin "Preparing OpenClaw package" bash -lc "$(declare -f pre_extract_openclaw); pre_extract_openclaw \"\$1\"" _ "$(pwd)" || \ - warn "Pre-extraction failed — npm install may fail if openclaw tarball is broken" + spin "Preparing OpenClaw package" bash -lc "$(declare -f pre_extract_openclaw); pre_extract_openclaw \"\$1\"" _ "$(pwd)" \ + || warn "Pre-extraction failed — npm install may fail if openclaw tarball is broken" spin "Installing NemoClaw dependencies" npm install --ignore-scripts spin "Building NemoClaw plugin" bash -lc 'cd nemoclaw && npm install --ignore-scripts && npm run build' spin "Linking NemoClaw CLI" npm link @@ -449,8 +499,8 @@ install_nemoclaw() { rm -rf "$nemoclaw_src" mkdir -p "$(dirname "$nemoclaw_src")" spin "Cloning NemoClaw source" git clone --depth 1 https://github.com/NVIDIA/NemoClaw.git "$nemoclaw_src" - spin "Preparing OpenClaw package" bash -lc "$(declare -f pre_extract_openclaw); pre_extract_openclaw \"\$1\"" _ "$nemoclaw_src" || \ - warn "Pre-extraction failed — npm install may fail if openclaw tarball is broken" + spin "Preparing OpenClaw package" bash -lc "$(declare -f pre_extract_openclaw); pre_extract_openclaw \"\$1\"" _ "$nemoclaw_src" \ + || warn "Pre-extraction failed — npm install may fail if openclaw tarball is broken" spin "Installing NemoClaw dependencies" bash -lc "cd \"$nemoclaw_src\" && npm install --ignore-scripts" spin "Building NemoClaw plugin" bash -lc "cd \"$nemoclaw_src\"/nemoclaw && npm install --ignore-scripts && npm run build" spin "Linking NemoClaw CLI" bash -lc "cd \"$nemoclaw_src\" && npm link" @@ -558,9 +608,18 @@ main() { for arg in "$@"; do case "$arg" in --non-interactive) NON_INTERACTIVE=1 ;; - --version|-v) printf "nemoclaw-installer v%s\n" "$NEMOCLAW_VERSION"; exit 0 ;; - --help|-h) usage; exit 0 ;; - *) usage; error "Unknown option: $arg" ;; + --version | -v) + printf "nemoclaw-installer v%s\n" "$NEMOCLAW_VERSION" + exit 0 + ;; + --help | -h) + usage + exit 0 + ;; + *) + usage + error "Unknown option: $arg" + ;; esac done # Also honor env var From a21dd2eafe819e0acd12cbdaa9414c43d70d839d Mon Sep 17 00:00:00 2001 From: Scott White Date: Sun, 22 Mar 2026 23:27:51 -0700 Subject: [PATCH 2/3] fix: remove duplicate Jetson platform detection helper --- bin/lib/nim.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/bin/lib/nim.js b/bin/lib/nim.js index f19ef4ae6..08cf0c32d 100644 --- a/bin/lib/nim.js +++ b/bin/lib/nim.js @@ -205,16 +205,6 @@ function pullNimImage(model) { return image; } -function isJetsonPlatform() { - try { - const out = runCapture("test -f /etc/nv_tegra_release && echo yes", { - ignoreError: true, - }); - return !!out && out.trim() === "yes"; - } catch {} - return false; -} - function startNimContainer(sandboxName, model, port = 8000) { const name = containerName(sandboxName); const image = getImageForModel(model); From 6b2bf0a2ccae6faf0bab78481d384c8fb2e88fca Mon Sep 17 00:00:00 2001 From: Scott White Date: Sun, 22 Mar 2026 23:51:00 -0700 Subject: [PATCH 3/3] fix: include pytest in pyright pre-push hook The pyright pre-push hook type-checks test files that import pytest. Adds pytest to uv run. --- .pre-commit-config.yaml | 2 +- bin/lib/nim.js | 200 +++++++++++++++++++--------------------- 2 files changed, 95 insertions(+), 107 deletions(-) mode change 100644 => 100755 bin/lib/nim.js diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cac42595d..bbc636c90 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -181,7 +181,7 @@ repos: - id: pyright-check name: Pyright (nemoclaw-blueprint) - entry: bash -c 'cd nemoclaw-blueprint && uv run --with pyright pyright .' + entry: bash -c 'cd nemoclaw-blueprint && uv run --with pyright --with pytest pyright .' language: system pass_filenames: false always_run: true diff --git a/bin/lib/nim.js b/bin/lib/nim.js old mode 100644 new mode 100755 index 08cf0c32d..229377a96 --- a/bin/lib/nim.js +++ b/bin/lib/nim.js @@ -1,54 +1,67 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 -// -// NIM container management — pull, start, stop, health-check NIM images. +#!/usr/bin/env node -const { run, runCapture, shellQuote } = require("./runner"); -const nimImages = require("./nim-images.json"); +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; -function containerName(sandboxName) { - return `nemoclaw-nim-${sandboxName}`; +function shellQuote(s) { + return `'${String(s).replace(/'/g, `'\\''`)}'`; } -function getImageForModel(modelName) { - const entry = nimImages.models.find((m) => m.name === modelName); - return entry ? entry.image : null; +function run(cmd, opts = {}) { + const res = spawnSync(cmd, { + shell: true, + stdio: "inherit", + env: process.env, + ...opts, + }); + if (res.error) throw res.error; + if (res.status !== 0 && !opts.ignoreError) { + process.exit(res.status ?? 1); + } + return res.status ?? 0; } -function listModels() { - return nimImages.models.map((m) => ({ - name: m.name, - image: m.image, - minGpuMemoryMB: m.minGpuMemoryMB, - })); +function runCapture(cmd, opts = {}) { + const res = spawnSync(cmd, { + shell: true, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + env: process.env, + ...opts, + }); + if (res.error) throw res.error; + if (res.status !== 0 && !opts.ignoreError) { + throw new Error((res.stderr || "").trim() || `Command failed: ${cmd}`); + } + return (res.stdout || "").trim(); } -function detectGpu() { - function isJetsonPlatform() { - try { - const nvTegra = runCapture("test -f /etc/nv_tegra_release && echo yes", { - ignoreError: true, - }); - if (nvTegra && nvTegra.trim() === "yes") return true; - } catch {} +function isJetsonPlatform() { + try { + const nvTegra = runCapture("test -f /etc/nv_tegra_release && echo yes", { + ignoreError: true, + }); + if (nvTegra && nvTegra.trim() === "yes") return true; + } catch {} - try { - const compat = runCapture("tr '\\0' '\\n' < /proc/device-tree/compatible", { - ignoreError: true, - }); - if (compat && /nvidia,tegra|nvidia,thor/i.test(compat)) return true; - } catch {} + try { + const compat = runCapture("tr '\\0' '\\n' < /proc/device-tree/compatible", { + ignoreError: true, + }); + if (compat && /nvidia,tegra|nvidia,thor/i.test(compat)) return true; + } catch {} - try { - const model = runCapture("tr '\\0' '\\n' < /proc/device-tree/model", { - ignoreError: true, - }); - if (model && /jetson|thor|nvidia/i.test(model)) return true; - } catch {} + try { + const model = runCapture("tr '\\0' '\\n' < /proc/device-tree/model", { + ignoreError: true, + }); + if (model && /jetson|thor|nvidia/i.test(model)) return true; + } catch {} - return false; - } + return false; +} +export function detectGpu() { function getSystemMemoryMB() { try { const memLine = runCapture("awk '/MemTotal:/ {print $2}' /proc/meminfo", { @@ -121,7 +134,7 @@ function detectGpu() { count: gpuCount, totalMemoryMB: 0, perGpuMB: 0, - nimCapable: true, + nimCapable: false, jetson: true, unifiedMemory: true, }; @@ -194,18 +207,19 @@ function detectGpu() { return null; } -function pullNimImage(model) { - const image = getImageForModel(model); - if (!image) { - console.error(` Unknown model: ${model}`); - process.exit(1); - } - console.log(` Pulling NIM image: ${image}`); - run(`docker pull ${shellQuote(image)}`); - return image; +function containerName(sandboxName) { + return `nim-${sandboxName}`; } -function startNimContainer(sandboxName, model, port = 8000) { +function getImageForModel(model) { + const map = { + llama: "nvcr.io/nim/meta/llama-3.1-8b-instruct:latest", + mistral: "nvcr.io/nim/mistralai/mistral-7b-instruct-v0.3:latest", + }; + return map[model] || null; +} + +export function startNimContainer(sandboxName, model, port = 8000) { const name = containerName(sandboxName); const image = getImageForModel(model); if (!image) { @@ -228,67 +242,41 @@ function startNimContainer(sandboxName, model, port = 8000) { return name; } -function waitForNimHealth(port = 8000, timeout = 300) { - const start = Date.now(); - const interval = 5000; - const safePort = Number(port); - console.log(` Waiting for NIM health on port ${safePort} (timeout: ${timeout}s)...`); - - while ((Date.now() - start) / 1000 < timeout) { - try { - const result = runCapture(`curl -sf http://localhost:${safePort}/v1/models`, { - ignoreError: true, - }); - if (result) { - console.log(" NIM is healthy."); - return true; - } - } catch {} - // Synchronous sleep via spawnSync - require("child_process").spawnSync("sleep", ["5"]); - } - console.error(` NIM did not become healthy within ${timeout}s.`); - return false; +function usage() { + console.log(`Usage: + nim.js detect-gpu + nim.js start [port]`); } -function stopNimContainer(sandboxName) { - const name = containerName(sandboxName); - const qn = shellQuote(name); - console.log(` Stopping NIM container: ${name}`); - run(`docker stop ${qn} 2>/dev/null || true`, { ignoreError: true }); - run(`docker rm ${qn} 2>/dev/null || true`, { ignoreError: true }); -} +function main() { + const [, , cmd, ...args] = process.argv; -function nimStatus(sandboxName) { - const name = containerName(sandboxName); - try { - const state = runCapture( - `docker inspect --format '{{.State.Status}}' ${shellQuote(name)} 2>/dev/null`, - { ignoreError: true } - ); - if (!state) return { running: false, container: name }; + switch (cmd) { + case "detect-gpu": { + const gpu = detectGpu(); + console.log(JSON.stringify(gpu, null, 2)); + break; + } - let healthy = false; - if (state === "running") { - const health = runCapture(`curl -sf http://localhost:8000/v1/models 2>/dev/null`, { - ignoreError: true, - }); - healthy = !!health; + case "start": { + const [sandboxName, model, port] = args; + if (!sandboxName || !model) { + usage(); + process.exit(1); + } + startNimContainer(sandboxName, model, port ? Number(port) : 8000); + break; } - return { running: state === "running", healthy, container: name, state }; - } catch { - return { running: false, container: name }; + + default: + usage(); + process.exit(1); } } -module.exports = { - containerName, - getImageForModel, - listModels, - detectGpu, - pullNimImage, - startNimContainer, - waitForNimHealth, - stopNimContainer, - nimStatus, -}; +const isMain = + process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]; + +if (isMain) { + main(); +}