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
228 changes: 228 additions & 0 deletions bin/lib/update.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// Self-update functionality for NemoClaw CLI

const { execSync } = require("child_process");
const path = require("path");
const fs = require("fs");
const os = require("os");
const https = require("https");
const http = require("http");

const INSTALL_SCRIPT_URL = "https://raw.githubusercontent.com/NVIDIA/NemoClaw/main/install.sh";

/**
* Compare two semver strings. Returns true if a >= b.
* @param {string} a
* @param {string} b
* @returns {boolean}
*/
function versionGte(a, b) {
const aParts = a.replace(/^v/, "").split(".").map(n => parseInt(n, 10) || 0);
const bParts = b.replace(/^v/, "").split(".").map(n => parseInt(n, 10) || 0);
for (let i = 0; i < 3; i++) {
const ai = aParts[i] || 0;
const bi = bParts[i] || 0;
if (ai > bi) return true;
if (ai < bi) return false;
}
return true;
}

/**
* Fetch content from a URL using Node.js built-in http/https.
* @param {string} url
* @returns {Promise<string>}
*/
function fetchUrl(url) {
return new Promise((resolve, reject) => {
const lib = url.startsWith("https") ? https : http;
const req = lib.get(url, { timeout: 10000 }, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
fetchUrl(res.headers.location).then(resolve).catch(reject);
return;
}
if (res.statusCode !== 200) {
reject(new Error(`HTTP ${res.statusCode}`));
return;
}
let data = "";
res.on("data", chunk => data += chunk);
res.on("end", () => resolve(data));
});
req.on("error", reject);
req.on("timeout", () => {
req.destroy();
reject(new Error("Request timed out"));
});
});
}

/**
* Get the current installed version of NemoClaw.
* @returns {string}
*/
function getCurrentVersion() {
try {
const pkg = require("../../package.json");
return pkg.version || "0.0.0";
} catch {
return "0.0.0";
}
}

/**
* Get the current CLI path to determine if running from source.
* @returns {string|null}
*/
function getCurrentCliPath() {
try {
return execSync("which nemoclaw 2>/dev/null", { encoding: "utf-8" }).trim() || null;
} catch {
return null;
}
}

/**
* Get the latest version from GitHub releases.
* @returns {Promise<string>}
*/
async function getLatestVersion() {
try {
const data = await fetchUrl("https://api.github.com/repos/NVIDIA/NemoClaw/releases/latest");
const release = JSON.parse(data);
return release.tag_name || release.name || "0.0.0";
} catch {
return getCurrentVersion();
}
}

/**
* Get the latest version from npm.
* @returns {Promise<string>}
*/
async function getLatestNpmVersion() {
try {
const data = await fetchUrl("https://registry.npmjs.org/nemoclaw/latest");
const pkg = JSON.parse(data);
return pkg.version || "0.0.0";
} catch {
return getCurrentVersion();
}
}

/**
* Check if an update is available.
* @returns {Promise<{current: string, latest: string, updateAvailable: boolean, runningFromSource: boolean}>}
*/
async function checkForUpdate() {
const cliPath = getCurrentCliPath();
const runningFromSource = !cliPath || cliPath.includes("node_modules/.bin");

let current = getCurrentVersion();
if (runningFromSource && cliPath) {
try {
const output = execSync(`"${cliPath}" --version 2>/dev/null`, { encoding: "utf-8" });
const match = output.match(/(\d+\.\d+\.\d+)/);
if (match) current = match[1];
} catch {}
}

const latestNpm = await getLatestNpmVersion();
const latestGithub = await getLatestVersion();

const latest = versionGte(latestNpm, latestGithub) ? latestNpm : latestGithub;
const updateAvailable = !versionGte(current, latest);

return { current, latest, updateAvailable, runningFromSource };
}

/**
* Run the update. Downloads and executes the install script.
* @param {object} opts
* @param {boolean} opts.force - Force update even if already up to date
* @param {boolean} opts.yes - Skip confirmation prompt
* @returns {Promise<boolean>}
*/
async function runUpdate(opts = {}) {
const { force = false, yes = false } = opts;

console.log("");
console.log(" Checking for updates...");
console.log("");

const { current, latest, updateAvailable, runningFromSource } = await checkForUpdate();

console.log(` Current version: ${current}`);
console.log(` Latest version: ${latest}`);

if (!force && !updateAvailable) {
console.log("");
console.log(" You are running the latest version.");
return true;
}

if (!yes) {
console.log("");
console.log(" A new version is available!");
console.log("");
if (runningFromSource) {
console.log(" Since you're running from source, use 'git pull' to update:");
console.log(" cd /path/to/NemoClaw && git pull");
} else {
console.log(" Run 'nemoclaw update --yes' to update without prompting.");
}
return false;
}

if (runningFromSource) {
console.log("");
console.log(" Since you're running from source, use 'git pull' to update:");
console.log(" cd /path/to/NemoClaw && git pull");
return false;
}

console.log("");
console.log(" Updating NemoClaw...");
console.log("");

try {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-update-"));
const scriptPath = path.join(tmpDir, "install.sh");

console.log(" Downloading installer...");
const scriptContent = await fetchUrl(INSTALL_SCRIPT_URL);
fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });

console.log(" Running installer...");
execSync(`bash "${scriptPath}"`, {
stdio: "inherit",
cwd: tmpDir
});

fs.unlinkSync(scriptPath);
fs.rmdirSync(tmpDir);

const newVersion = getCurrentVersion();
console.log("");
console.log(` Successfully updated to v${newVersion}`);
return true;
} catch (err) {
console.error("");
console.error(` Update failed: ${err.message}`);
console.error("");
console.error(" You can also update manually with:");
console.error(" npm install -g nemoclaw");
return false;
}
}

module.exports = {
getCurrentVersion,
getCurrentCliPath,
getLatestVersion,
getLatestNpmVersion,
checkForUpdate,
runUpdate,
};
116 changes: 116 additions & 0 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,17 @@ const policies = require("./lib/policies");
const GLOBAL_COMMANDS = new Set([
"onboard", "list", "deploy", "setup", "setup-spark",
"start", "stop", "status", "debug", "uninstall",
"completion",
"update",
"help", "--help", "-h", "--version", "-v",
]);

const SANDBOX_ACTIONS = [
"connect", "status", "logs", "policy-add", "policy-list", "destroy"
];

const SHELL_TYPES = ["bash", "zsh", "fish"];

const REMOTE_UNINSTALL_URL = "https://raw.githubusercontent.com/NVIDIA/NemoClaw/refs/heads/main/uninstall.sh";

function resolveUninstallScript() {
Expand Down Expand Up @@ -389,6 +397,104 @@ async function sandboxDestroy(sandboxName, args = []) {
console.log(` ${G}✓${R} Sandbox '${sandboxName}' destroyed`);
}

// ── Shell Completion ─────────────────────────────────────────────

async function update(opts) {
const { runUpdate } = require("./lib/update");
await runUpdate(opts);
}

function printCompletion(shell) {
if (!shell || !SHELL_TYPES.includes(shell)) {
console.log(" Usage: nemoclaw completion <shell>");
console.log("");
console.log(" Generate shell completion scripts.");
console.log("");
console.log(" Shells supported: bash, zsh, fish");
console.log("");
console.log(" Example:");
console.log(" # Bash:");
console.log(" nemoclaw completion bash >> ~/.bashrc");
console.log("");
console.log(" # Zsh:");
console.log(" nemoclaw completion zsh >> ~/.zshrc");
console.log("");
console.log(" # Fish:");
console.log(" nemoclaw completion fish > ~/.config/fish/completions/nemoclaw.fish");
return;
}

const globalCmds = Array.from(GLOBAL_COMMANDS).filter(c => !c.startsWith("-"));
let sandboxNames = [];
try {
sandboxNames = registry.listSandboxes().sandboxes.map(s => s.name);
} catch {
sandboxNames = [];
}

const sandboxNamesStr = sandboxNames.length > 0 ? sandboxNames.join(" ") : "";
const sandboxNamesZsh = sandboxNames.length > 0 ? sandboxNames.map(s => `"${s}"`).join(" ") : "";
const sandboxNamesFish = sandboxNames.length > 0 ? sandboxNames.map(s => `'${s}'`).join(" ") : "";

if (shell === "bash") {
console.log(`_nemoclaw_completions() {
local cur prev opts
COMPREPLY=()
cur="\${COMP_WORDS[COMP_CWORD]}"
prev="\${COMP_WORDS[COMP_CWORD-1]}"

# Global commands
opts="${globalCmds.join(" ")}"

# Sandbox names (if previous word is a known sandbox)
if [[ " ${sandboxNamesStr} " =~ " $prev " ]]; then
opts="${SANDBOX_ACTIONS.join(" ")}"
fi

# Also add sandbox names as possible first argument
opts="$opts ${sandboxNamesStr}"

COMPREPLY=(\$(compgen -W "\$opts" -- \$cur))
return 0
}

complete -F _nemoclaw_completions nemoclaw`);
} else if (shell === "zsh") {
console.log(`# nemoclaw zsh completion

local -a global_cmds
global_cmds=(${globalCmds.map(c => `"${c}"`).join(" ")})

local -a sandbox_actions
sandbox_actions=(${SANDBOX_ACTIONS.map(a => `"${a}"`).join(" ")})

local -a sandbox_names
sandbox_names=(${sandboxNamesZsh})

_nemoclaw() {
local -a cmd
cmd=(\${words[1,CURRENT-1]})

# Check if first word is a sandbox name
if [[ " \${sandbox_names[@]} " =~ " \${cmd[1]} " ]]; then
_describe 'sandbox actions' sandbox_actions
else
_describe 'commands' global_cmds
_describe 'sandboxes' sandbox_names
fi
}

compdef _nemoclaw nemoclaw`);
} else if (shell === "fish") {
console.log(`# nemoclaw fish completion

complete -c nemoclaw -f -a "${globalCmds.join(" ")} ${sandboxNamesStr}" -n "test (count (commandline -opc)) -eq 1"

complete -c nemoclaw -f -a "${SANDBOX_ACTIONS.join(" ")}" -n "test (count (commandline -opc)) -ge 2; and contains (commandline -opc | head -1) ${sandboxNamesFish}"
`);
}
}

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

function help() {
Expand Down Expand Up @@ -420,6 +526,14 @@ function help() {
nemoclaw stop Stop all services
nemoclaw status Show sandbox list and service status

<<<<<<< HEAD
Shell Completion:
nemoclaw completion <shell> Generate shell completion script

Update:
nemoclaw update Check for updates
nemoclaw update --yes Update to latest version

Troubleshooting:
nemoclaw debug [--quick] Collect diagnostics for bug reports
nemoclaw debug --output FILE Save diagnostics tarball for GitHub issues
Expand Down Expand Up @@ -462,6 +576,8 @@ const [cmd, ...args] = process.argv.slice(2);
case "debug": debug(args); break;
case "uninstall": uninstall(args); break;
case "list": listSandboxes(); break;
case "completion": printCompletion(args[0]); break;
case "update": await update({ force: args.includes("--force"), yes: args.includes("--yes") }); break;
case "--version":
case "-v": {
const pkg = require(path.join(__dirname, "..", "package.json"));
Expand Down
Loading