Skip to content

feat(cli): add self-update command#644

Open
vasanth53 wants to merge 6 commits intoNVIDIA:mainfrom
vasanth53:feat/self-update-v2
Open

feat(cli): add self-update command#644
vasanth53 wants to merge 6 commits intoNVIDIA:mainfrom
vasanth53:feat/self-update-v2

Conversation

@vasanth53
Copy link

@vasanth53 vasanth53 commented Mar 22, 2026

Summary

  • Add nemoclaw update command to check for CLI updates
  • Add nemoclaw update --yes to automatically update to latest version
  • Add nemoclaw update --force to force update check
  • Detects running from source and suggests git pull instead
  • Uses npm registry and GitHub releases for version check

Usage

nemoclaw update          # Check for updates
nemoclaw update --yes    # Update to latest (non-interactive)
nemoclaw update --force  # Force update even if current

Testing

  • Help text shows new update command
  • Version check works correctly
  • Source detection works (suggests git pull)

Related Issue

Closes #642

Summary by CodeRabbit

  • New Features
    • Added a new update command to check for and install the latest CLI updates.
    • Detects the highest available release from GitHub and the npm registry.
    • Supports --yes to skip prompts and --force to bypass availability checks.
    • Detects when running from source and advises using git instead of auto-update.
    • Verifies downloaded installer (SHA-256), runs it, reports success/failure, and cleans up.

Add completion command supporting bash, zsh, and fish shells.
Completes issue NVIDIA#155.
- Add 'nemoclaw update' command to check for updates
- Add 'nemoclaw update --yes' to update without prompting
- Add 'nemoclaw update --force' to force update check
- Detects running from source and suggests 'git pull' instead
- Uses npm registry and GitHub releases for version check

Closes NVIDIA#642
@coderabbitai
Copy link

coderabbitai bot commented Mar 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fdaf6547-c309-40a1-86b1-21135f13cb14

📥 Commits

Reviewing files that changed from the base of the PR and between dfddeda and 29f4f9c.

📒 Files selected for processing (2)
  • bin/lib/update.js
  • bin/nemoclaw.js
✅ Files skipped from review due to trivial changes (1)
  • bin/lib/update.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • bin/nemoclaw.js

📝 Walkthrough

Walkthrough

Adds a new self-update feature: a standalone updater module that compares the installed CLI version to npm and GitHub releases and, when requested, downloads and runs an install script; integrates this as a new nemoclaw update CLI command with --yes and --force flags.

Changes

Cohort / File(s) Summary
Update Module
bin/lib/update.js
New module implementing semver compare, HTTP(S) fetch with redirects/timeouts, current-version/path detection, GitHub + npm version lookups, checkForUpdate() and runUpdate() that download install.sh, verify SHA-256 prefix, execute via bash, handle errors, and cleanup.
CLI Integration
bin/nemoclaw.js
Added update to global commands, new update(opts) wrapper that lazy-loads ./lib/update and calls runUpdate(opts), CLI dispatch case parsing --force and --yes, and updated help text documenting the command.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI as "nemoclaw CLI"
    participant Update as "bin/lib/update.js"
    participant NPM as "npm Registry"
    participant GitHub as "GitHub Releases"
    participant Shell as "bash / shell"

    User->>CLI: nemoclaw update [--force] [--yes]
    CLI->>Update: runUpdate({force, yes})
    Update->>Update: getCurrentVersion() / getCurrentCliPath()
    Update->>NPM: getLatestNpmVersion()
    NPM-->>Update: npm version
    Update->>GitHub: getLatestVersion()
    GitHub-->>Update: release version
    Update->>Update: select higher latest, compare semver
    alt updateAvailable or force
        alt not yes
            Update->>User: prompt for confirmation
            User-->>Update: confirm/deny
        end
        alt confirmed
            Update->>Update: download install.sh (fetchUrl)
            Update->>Update: compute SHA-256 prefix, write exec script
            Update->>Shell: execute install.sh (bash) with inherited stdio
            Shell-->>Update: exit status
            Update->>Update: re-read local version, cleanup temp
            Update->>User: report success/failure
        end
    else no update
        Update->>User: report already up-to-date
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I sniffed the versions, near and far,
I fetched the tags from npm and the GitHub star,
I downloaded a script, checked its hash,
I hopped and ran it in a bashy dash,
Now the CLI leaps — updated in a flash!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(cli): add self-update command' accurately and concisely describes the main change, which is adding a new CLI command for self-updating the NemoClaw CLI tool.
Linked Issues check ✅ Passed The PR implements all key coding objectives from issue #642: version checking from npm and GitHub, update availability display, non-interactive updates via --yes, --force option, source detection with git pull suggestion, built-in Node.js APIs, SHA256 integrity verification, and version comparison improvements.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the self-update feature specified in issue #642; no out-of-scope modifications detected in the provided summaries.
Docstring Coverage ✅ Passed Docstring coverage is 90.91% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
bin/nemoclaw.js (1)

438-441: Inconsistent help text formatting.

Other section headers use the green color format ${G}Section:${R} (e.g., "Getting Started:", "Sandbox Management:"), but "Update:" is plain text.

✨ Proposed fix for consistent styling
-  Update:
-    nemoclaw update                    Check for updates
-    nemoclaw update --yes             Update to latest version
+  ${G}Update:${R}
+    nemoclaw update                  Check for updates
+    nemoclaw update --yes            Update to latest version
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/nemoclaw.js` around lines 438 - 441, Replace the plain "Update:" help
header with the colored format used elsewhere by changing the displayed header
string from "Update:" to `${G}Update:${R}` in the help output generator (the
block that prints the commands and headers where "Update:" currently appears);
ensure the rest of the two lines under that header remain unchanged and preserve
spacing/indentation to match other sections like "Getting Started:" and "Sandbox
Management:".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/lib/update.js`:
- Around line 207-209: getCurrentVersion() can return a cached package.json
value after the installer runs; update the code so getCurrentVersion() returns
the fresh version (either by clearing Node's require cache for the package.json
module before calling require, e.g. delete
require.cache[require.resolve("../../package.json")], or by changing
getCurrentVersion() to read and JSON.parse fs.readFileSync of the package.json
path), then call that refreshed getCurrentVersion() for the success message;
ensure the change targets the getCurrentVersion() implementation (or the call
site right before console.log) so the logged v${newVersion} reflects the newly
written package.json.
- Around line 190-205: The downloaded installer is executed without integrity
verification and the cleanup uses fs.rmdirSync(tmpDir) which will fail if the
installer created files; update the flow around fetchUrl/INSTALL_SCRIPT_URL and
execSync to first obtain and verify the script's SHA256 (either by comparing
against an embedded expected hash or by fetching a .sha256 alongside the script)
and only proceed to write and execute the script if the checksum matches, and
change the cleanup to remove the temp dir recursively/forcibly (replace
fs.rmdirSync(tmpDir) with a recursive removal like fs.rmSync(tmpDir, {
recursive: true, force: true }) or equivalent) while ensuring errors on
verification abort before execSync is called; reference the tmpDir, scriptPath,
fetchUrl, INSTALL_SCRIPT_URL and execSync usages when making the changes.
- Around line 21-31: The versionGte function incorrectly treats prerelease tags
as numeric parts; update versionGte to either use a proper semver comparator
(e.g., import and call semver.gte with the original strings) or normalize inputs
by stripping prerelease/build suffixes before parsing (split each input on '-'
and '+' and take the first part, then split on '.' and parse ints) so that
prerelease labels like "1.0.0-beta.1" are not parsed as "0-beta"; ensure you
keep the function name versionGte and its return semantics when applying the
change.
- Around line 38-59: The fetchUrl function currently omits a User-Agent header
and follows redirects recursively without a depth limit; update fetchUrl to
include a sensible User-Agent header in the lib.get/request options (e.g.,
"User-Agent": "<your-app-name>") and add a redirect depth guard by accepting or
tracking a maxRedirects counter (e.g., maxRedirects param or internal attempts
variable) and reject if exceeded (suggest 5); replace the unbounded recursive
call fetchUrl(res.headers.location).then(...) with a call that passes/updates
the remaining redirect count and rejects with a clear error when the limit is
reached, keeping existing timeout and error handling intact.
- Around line 119-121: The source-detection in checkForUpdate is wrong: change
the runningFromSource calculation in checkForUpdate (where getCurrentCliPath()
is used) to only treat the CLI as "from source" when cliPath is falsy (i.e.,
runningFromSource = !cliPath) and remove the
cliPath.includes("node_modules/.bin") condition; also ensure any downstream
messages that instruct "use 'git pull' to update" are only shown when
runningFromSource is true so local project installs in node_modules are not
misclassified as source installs.

In `@bin/nemoclaw.js`:
- Line 484: The update command registers the --force flag in the invocation
(case "update": await update({ force: args.includes("--force"), yes:
args.includes("--yes") });) but the CLI help only documents --yes; update the
CLI help/usage text to include a short description for --force (e.g., "Bypass
update availability checks and force an update") alongside --yes so users see
both flags; locate where the update command help/usage string is defined and add
the --force entry and example usage referencing the update command and flags.
- Around line 44-48: Remove the unused constants SANDBOX_ACTIONS and SHELL_TYPES
from the file: locate the constant declarations for SANDBOX_ACTIONS and
SHELL_TYPES in bin/nemoclaw.js and delete them; if they are intended as
placeholders for future functionality, instead keep them but add a clear
explanatory comment above each declaring their intended purpose and why they
remain unused so linters/reviewers understand they’re intentional.

---

Nitpick comments:
In `@bin/nemoclaw.js`:
- Around line 438-441: Replace the plain "Update:" help header with the colored
format used elsewhere by changing the displayed header string from "Update:" to
`${G}Update:${R}` in the help output generator (the block that prints the
commands and headers where "Update:" currently appears); ensure the rest of the
two lines under that header remain unchanged and preserve spacing/indentation to
match other sections like "Getting Started:" and "Sandbox Management:".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b74decfe-9077-4bca-9eb9-648a1e474858

📥 Commits

Reviewing files that changed from the base of the PR and between 04012f7 and e549689.

📒 Files selected for processing (2)
  • bin/lib/update.js
  • bin/nemoclaw.js

Comment on lines +190 to +205
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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing install script integrity verification.

The PR objectives state: "verify install script integrity before execution." Currently, the script is downloaded and executed without any checksum or signature verification. While HTTPS provides transport security, it doesn't protect against a compromised source.

Additionally, fs.rmdirSync(tmpDir) will fail if the installer creates additional files in the temp directory.

🔧 Proposed fix for cleanup (integrity check requires infrastructure)
-    fs.unlinkSync(scriptPath);
-    fs.rmdirSync(tmpDir);
+    // Clean up temp directory recursively
+    fs.rmSync(tmpDir, { recursive: true, force: true });

For integrity verification, consider:

  1. Embedding an expected SHA256 hash and verifying before execution
  2. Fetching a .sha256 file alongside the script and comparing
  3. Using GPG signature verification if available
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/update.js` around lines 190 - 205, The downloaded installer is
executed without integrity verification and the cleanup uses
fs.rmdirSync(tmpDir) which will fail if the installer created files; update the
flow around fetchUrl/INSTALL_SCRIPT_URL and execSync to first obtain and verify
the script's SHA256 (either by comparing against an embedded expected hash or by
fetching a .sha256 alongside the script) and only proceed to write and execute
the script if the checksum matches, and change the cleanup to remove the temp
dir recursively/forcibly (replace fs.rmdirSync(tmpDir) with a recursive removal
like fs.rmSync(tmpDir, { recursive: true, force: true }) or equivalent) while
ensuring errors on verification abort before execSync is called; reference the
tmpDir, scriptPath, fetchUrl, INSTALL_SCRIPT_URL and execSync usages when making
the changes.

- versionGte: strip prerelease/build suffixes before parsing
- fetchUrl: add User-Agent header and 5-redirect limit
- checkForUpdate: fix source detection (only when cliPath is falsy)
- runUpdate: add SHA256 hash display, use fs.rmSync with recursive/force
- getCurrentVersion: add clearCache param to read fresh version
- bin/nemoclaw.js: remove unused SANDBOX_ACTIONS and SHELL_TYPES
- help text: fix Update section formatting, add --force flag docs
@vasanth53
Copy link
Author

vasanth53 commented Mar 22, 2026

Thanks for the review! All comments addressed:

  • versionGte: now strips prerelease/build suffixes before parsing
  • fetchUrl: added User-Agent header + 5-redirect limit
  • checkForUpdate: fixed source detection to only check !cliPath
  • runUpdate: added SHA256 hash display, uses fs.rmSync with recursive/force
  • getCurrentVersion: added clearCache param for fresh version read
  • Removed unused SANDBOX_ACTIONS and SHELL_TYPES constants
  • Fixed Update help section formatting + added --force flag docs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
bin/lib/update.js (1)

209-217: ⚠️ Potential issue | 🟠 Major

Installer integrity is still not verified before execution.

Line 210 computes SHA-256, but the value is only logged before execSync runs the script. There is no trusted expected hash/signature comparison, so this does not provide integrity protection.

Proposed verification gate (example)
+    const EXPECTED_INSTALL_SHA256 = process.env.NEMOCLAW_INSTALL_SHA256; // or fetch trusted checksum/signature
     const hash = crypto.createHash("sha256").update(scriptContent).digest("hex");
-    console.log(`  Script SHA256: ${hash.substring(0, 16)}...`);
+    if (!EXPECTED_INSTALL_SHA256 || hash !== EXPECTED_INSTALL_SHA256) {
+      throw new Error("Installer integrity verification failed");
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/update.js` around lines 209 - 217, The code computes a SHA-256 for
scriptContent (variable hash) and immediately executes the scriptPath via
execSync without verifying it; add a verification gate that compares the
computed hash against a trusted expected value (provided either as a constant,
an environment variable, or a downloaded/packaged signature) and aborts with an
error/log if they differ before calling execSync; update the logic around hash,
scriptPath, scriptContent, and the execSync call to perform this comparison and
only run the installer when the hashes match.
🧹 Nitpick comments (1)
bin/lib/update.js (1)

136-141: Unreachable condition in source/version detection.

runningFromSource is !cliPath (Line 133), so runningFromSource && cliPath can never be true. This branch is dead and should be removed or corrected.

Proposed cleanup
-  if (runningFromSource && cliPath) {
+  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 {}
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/update.js` around lines 136 - 141, The branch checking "if
(runningFromSource && cliPath)" is unreachable because runningFromSource is set
to "!cliPath"; replace the condition so the CLI version probe runs when a
cliPath exists (e.g., use "if (cliPath)" or "if (!runningFromSource &&
cliPath)") or remove the dead block entirely; update the block around
execSync/version matching (referencing runningFromSource and cliPath)
accordingly to ensure the external CLI is queried only when cliPath is present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/lib/update.js`:
- Around line 178-188: When no newer version is found but the user passed a
force flag, the current block still prints "A new version is available!" and
suggests using '--yes'; change the logic around the variables yes,
runningFromSource and the force flag (e.g., force) so that if force is true you
do not claim a new version exists but instead print a message like "No new
version available; --force will reinstall the current version" and, when not
runningFromSource, suggest using 'nemoclaw update --force' (and when
runningFromSource, keep the git pull hint). Update the messages and suggested
command accordingly in the code paths that reference yes and runningFromSource
so forced behavior is correctly described.

In `@bin/nemoclaw.js`:
- Around line 39-40: GLOBAL_COMMANDS includes the string "completion" but the
global dispatch switch that handles commands lacks a corresponding case so it
falls through to help(); either remove "completion" from GLOBAL_COMMANDS if it
shouldn't be a global command, or add a proper case "completion" branch in the
global dispatch (the same switch that currently calls help()) that invokes the
intended completion handler (e.g., call the existing completion implementation
or a new completionHandler function). Update the switch to include case
"completion": ... and ensure the handler is imported/defined and
returns/executes consistently with other global commands.

---

Duplicate comments:
In `@bin/lib/update.js`:
- Around line 209-217: The code computes a SHA-256 for scriptContent (variable
hash) and immediately executes the scriptPath via execSync without verifying it;
add a verification gate that compares the computed hash against a trusted
expected value (provided either as a constant, an environment variable, or a
downloaded/packaged signature) and aborts with an error/log if they differ
before calling execSync; update the logic around hash, scriptPath,
scriptContent, and the execSync call to perform this comparison and only run the
installer when the hashes match.

---

Nitpick comments:
In `@bin/lib/update.js`:
- Around line 136-141: The branch checking "if (runningFromSource && cliPath)"
is unreachable because runningFromSource is set to "!cliPath"; replace the
condition so the CLI version probe runs when a cliPath exists (e.g., use "if
(cliPath)" or "if (!runningFromSource && cliPath)") or remove the dead block
entirely; update the block around execSync/version matching (referencing
runningFromSource and cliPath) accordingly to ensure the external CLI is queried
only when cliPath is present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 517b0cbb-7c4f-4433-bf0f-a76ec9f897c7

📥 Commits

Reviewing files that changed from the base of the PR and between e549689 and dfddeda.

📒 Files selected for processing (2)
  • bin/lib/update.js
  • bin/nemoclaw.js

- Fix --force message when no new version (show 'reinstalling current version')
- Remove unused 'completion' from GLOBAL_COMMANDS (no handler exists)
@vasanth53
Copy link
Author

Addressed remaining comments:

  • Fixed --force message when no new version available (now shows 'Reinstalling current version')
  • Removed 'completion' from GLOBAL_COMMANDS (no handler exists)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cli): add self-update command for automatic CLI updates

1 participant