Skip to content

fix(cli): surface OpenShell errors with context and docs links#345

Closed
hkc5 wants to merge 1 commit intoNVIDIA:mainfrom
hkc5:fix/openshell-error-context
Closed

fix(cli): surface OpenShell errors with context and docs links#345
hkc5 wants to merge 1 commit intoNVIDIA:mainfrom
hkc5:fix/openshell-error-context

Conversation

@hkc5
Copy link
Copy Markdown

@hkc5 hkc5 commented Mar 18, 2026

Closes #60

Problem

When an openshell CLI call fails, NemoClaw currently either:

  • Surfaces a raw Node.js process error with no indication that OpenShell is the source
  • Silently returns a default value (e.g. running: false, "0.0.0") with no user-visible feedback

Users hitting OpenShell errors have no way to tell whether the problem is NemoClaw or OpenShell, and no pointer to where to look for help.

Changes

onboard.ts — add wrapOpenShellError() helper and use it inside execOpenShell():

  • ENOENT → "OpenShell CLI not found. Is OpenShell installed?\n Install guide: <url>"
  • Any other failure → "OpenShell error running 'openshell <subcommand>': <detail>\n Troubleshooting: <url>"

Catch blocks that previously extracted raw stderr from the error object now just read err.message, which already contains the enriched detail.

connect.ts — enrich both ENOENT and generic branches of the spawn error handler with the same prefix and docs link.

logs.ts — same treatment for the log-streaming spawn error handler.

status.ts — instead of silently returning running: false when openshell sandbox status fails, capture the error into a new openshellError: string | null field and display it as "Status: unknown — <message>" so the user knows why the sandbox state could not be determined.

status.test.ts — update the "no openshell, blank state" scenario to assert the new actionable error output instead of the silent "Status: not running".

Test

npm test  # 22/22 passing

Summary by CodeRabbit

  • New Features

    • Enhanced error messages with direct install/troubleshooting links for OpenShell issues.
    • Status output now surfaces OpenShell error context when available.
  • Bug Fixes

    • Clearer distinction between missing OpenShell CLI and other OpenShell failures; consistent messaging across flows.
  • Tests

    • Updated status test to expect actionable OpenShell-missing messaging and docs link.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 18, 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: df62b934-2caa-4557-a9ec-773f468dfea7

📥 Commits

Reviewing files that changed from the base of the PR and between ef53663 and 04fb08e.

⛔ Files ignored due to path filters (12)
  • nemoclaw/dist/commands/connect.d.ts.map is excluded by !**/dist/**, !**/*.map
  • nemoclaw/dist/commands/connect.js is excluded by !**/dist/**
  • nemoclaw/dist/commands/connect.js.map is excluded by !**/dist/**, !**/*.map
  • nemoclaw/dist/commands/logs.d.ts.map is excluded by !**/dist/**, !**/*.map
  • nemoclaw/dist/commands/logs.js is excluded by !**/dist/**
  • nemoclaw/dist/commands/logs.js.map is excluded by !**/dist/**, !**/*.map
  • nemoclaw/dist/commands/onboard.d.ts.map is excluded by !**/dist/**, !**/*.map
  • nemoclaw/dist/commands/onboard.js is excluded by !**/dist/**
  • nemoclaw/dist/commands/onboard.js.map is excluded by !**/dist/**, !**/*.map
  • nemoclaw/dist/commands/status.d.ts.map is excluded by !**/dist/**, !**/*.map
  • nemoclaw/dist/commands/status.js is excluded by !**/dist/**
  • nemoclaw/dist/commands/status.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (5)
  • nemoclaw/src/commands/connect.ts
  • nemoclaw/src/commands/logs.ts
  • nemoclaw/src/commands/onboard.ts
  • nemoclaw/src/commands/status.test.ts
  • nemoclaw/src/commands/status.ts
✅ Files skipped from review due to trivial changes (1)
  • nemoclaw/src/commands/connect.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • nemoclaw/src/commands/status.test.ts
  • nemoclaw/src/commands/logs.ts
  • nemoclaw/src/commands/onboard.ts
  • nemoclaw/src/commands/status.ts

📝 Walkthrough

Walkthrough

This PR standardizes and improves OpenShell-related error handling across CLI commands: it detects missing OpenShell (ENOENT), wraps and reformats errors with documentation/install links, surfaces OpenShell error messages in sandbox status, and updates tests to expect the new status output.

Changes

Cohort / File(s) Summary
Connect & Logs handlers
nemoclaw/src/commands/connect.ts, nemoclaw/src/commands/logs.ts
Error handlers updated to detect ENOENT (OpenShell CLI not found) and log multi-line, actionable messages with install/troubleshooting URLs; non-ENOENT errors are rephrased to include an OpenShell troubleshooting link.
Onboarding: OpenShell error wrapper
nemoclaw/src/commands/onboard.ts
Adds OPENSHELL_DOCS_URL and wrapOpenShellError(err, args); execFileSync calls are wrapped to throw standardized errors that indicate missing CLI vs other OpenShell failures and include command context and docs links.
Status shape & display
nemoclaw/src/commands/status.ts
Adds `openshellError: string
Test update
nemoclaw/src/commands/status.test.ts
Adjusts the "host — no openshell, blank state" test to expect the new Status: unknown — output and the OpenShell troubleshooting URL instead of the prior not running text.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I hopped through logs and wrapped each scare,

When openshell hides, I point to where —
A link, a hint, a gentle guide,
So missing binaries needn't hide.
Hooray for clearer messages far and wide!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(cli): surface OpenShell errors with context and docs links' directly and concisely summarizes the main change across all modified files—improving error messaging for OpenShell-related failures with contextual information and documentation links.
Linked Issues check ✅ Passed The pull request fully addresses issue #60 by implementing OpenShell-specific error identification and documentation links across connect.ts, logs.ts, onboard.ts, and status.ts, with special handling for ENOENT and troubleshooting URLs as required.
Out of Scope Changes check ✅ Passed All changes are directly related to improving OpenShell error messaging and user guidance; no unrelated modifications detected in the affected files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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

Copy link
Copy Markdown
Contributor

@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: 3

🧹 Nitpick comments (1)
nemoclaw/src/commands/connect.ts (1)

27-35: Consider using a shared OpenShell error formatter across commands.

These strings/URLs are now duplicated in multiple command files; centralizing them (as a shared helper) will prevent drift and keep messaging consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/commands/connect.ts` around lines 27 - 35, The duplicated
OpenShell error messages in connect.ts (the logger.error calls that output
"OpenShell CLI not found..." and the template string using err.message and
troubleshooting URL) should be moved into a single shared formatter helper
(e.g., formatOpenShellError) exported from a common module (like
openshell/errors or openshell/utils); update the connect command to call
formatOpenShellError(err, {notFoundMessage?}) or similar and pass the returned
string to logger.error, and replace other command files' inline strings with the
same helper to centralize the URLs and message formatting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nemoclaw/src/commands/logs.ts`:
- Around line 56-66: cliLogs() currently returns early when isSandboxRunning()
fails, which masks OpenShell-missing ENOENT errors; update cliLogs() so errors
from isSandboxRunning() are either rethrown or handled the same way as the
existing catch that checks err.message.includes("ENOENT"). Concretely, in
cliLogs() wrap the isSandboxRunning() call with try/catch and when the caught
error message contains "ENOENT" call the same logger.error message used in the
diff (the OpenShell CLI not found message and install guide), otherwise
propagate or rethrow the error so the outer error handling (the else branch that
logs `OpenShell error running 'openshell sandbox connect'`) can run. Ensure you
reference isSandboxRunning(), cliLogs(), and the logger.error messages so the
behavior is consistent.

In `@nemoclaw/src/commands/status.test.ts`:
- Around line 126-136: Update the test "shows OpenShell error context and 'Not
configured' in text output" to also simulate the missing OpenShell CLI by
causing the underlying exec/spawn/which call used by cliStatus to throw an
ENOENT error (e.g., stub the method that checks/executes OpenShell) while still
using captureLogger() and defaultConfig, then call cliStatus({ json: false,
logger, pluginConfig: defaultConfig }) and add an assertion that the output
contains the OpenShell install-guide URL (the install-guide branch) in addition
to the existing troubleshooting URL check so the ENOENT/install-guide branch in
cliStatus is exercised.

In `@nemoclaw/src/commands/status.ts`:
- Around line 147-151: The current ENOENT check on the exec() error is
unreliable; update the error detection in the status handler that builds
openshellError (using variables err, msg, and openshellError) to also treat
shell-missing-command cases as "OpenShell CLI not found": check if the error
object has code === 127 or if stderr/message contains "command not found"
(case-insensitive) and, if so, set the openshellError to the install-guide
string; otherwise fall back to the existing generic openshell/troubleshooting
message.

---

Nitpick comments:
In `@nemoclaw/src/commands/connect.ts`:
- Around line 27-35: The duplicated OpenShell error messages in connect.ts (the
logger.error calls that output "OpenShell CLI not found..." and the template
string using err.message and troubleshooting URL) should be moved into a single
shared formatter helper (e.g., formatOpenShellError) exported from a common
module (like openshell/errors or openshell/utils); update the connect command to
call formatOpenShellError(err, {notFoundMessage?}) or similar and pass the
returned string to logger.error, and replace other command files' inline strings
with the same helper to centralize the URLs and message formatting.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 01e22515-d682-4342-98b6-cbe2779cd8f5

📥 Commits

Reviewing files that changed from the base of the PR and between 2b5febe and ef53663.

⛔ Files ignored due to path filters (12)
  • nemoclaw/dist/commands/connect.d.ts.map is excluded by !**/dist/**, !**/*.map
  • nemoclaw/dist/commands/connect.js is excluded by !**/dist/**
  • nemoclaw/dist/commands/connect.js.map is excluded by !**/dist/**, !**/*.map
  • nemoclaw/dist/commands/logs.d.ts.map is excluded by !**/dist/**, !**/*.map
  • nemoclaw/dist/commands/logs.js is excluded by !**/dist/**
  • nemoclaw/dist/commands/logs.js.map is excluded by !**/dist/**, !**/*.map
  • nemoclaw/dist/commands/onboard.d.ts.map is excluded by !**/dist/**, !**/*.map
  • nemoclaw/dist/commands/onboard.js is excluded by !**/dist/**
  • nemoclaw/dist/commands/onboard.js.map is excluded by !**/dist/**, !**/*.map
  • nemoclaw/dist/commands/status.d.ts.map is excluded by !**/dist/**, !**/*.map
  • nemoclaw/dist/commands/status.js is excluded by !**/dist/**
  • nemoclaw/dist/commands/status.js.map is excluded by !**/dist/**, !**/*.map
📒 Files selected for processing (5)
  • nemoclaw/src/commands/connect.ts
  • nemoclaw/src/commands/logs.ts
  • nemoclaw/src/commands/onboard.ts
  • nemoclaw/src/commands/status.test.ts
  • nemoclaw/src/commands/status.ts

Comment on lines +56 to +66
if (err.message.includes("ENOENT")) {
logger.error(
"OpenShell CLI not found. Is OpenShell installed?\n" +
" Install guide: https://docs.nvidia.com/nemoclaw/openshell/install",
);
} else {
logger.error(
`OpenShell error running 'openshell sandbox connect': ${err.message}\n` +
" Troubleshooting: https://docs.nvidia.com/nemoclaw/openshell/troubleshooting",
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

OpenShell-missing errors are still masked before this handler runs.

cliLogs() returns early when isSandboxRunning() fails, so users can still get “not running” instead of the new OpenShell guidance in missing-CLI cases. Please surface OpenShell errors from the preflight path too.

Suggested direction
-const sandboxRunning = await isSandboxRunning(sandboxName);
-if (!sandboxRunning) {
+const sandbox = await isSandboxRunning(sandboxName);
+if (sandbox.error) {
+  logger.error(sandbox.error);
+  return;
+}
+if (!sandbox.running) {
   logger.info(`Sandbox '${sandboxName}' is not running. No live logs available.`);
   return;
 }
-async function isSandboxRunning(sandboxName: string): Promise<boolean> {
+async function isSandboxRunning(
+  sandboxName: string,
+): Promise<{ running: boolean; error: string | null }> {
   try {
     const { stdout } = await execAsync(`openshell sandbox get ${sandboxName} --json`, {
       timeout: 5000,
     });
     const parsed = JSON.parse(stdout) as { state?: string };
-    return parsed.state === "running";
-  } catch {
-    return false;
+    return { running: parsed.state === "running", error: null };
+  } catch (err) {
+    const msg = err instanceof Error ? err.message : String(err);
+    return {
+      running: false,
+      error: `OpenShell preflight error: ${msg}\n  Troubleshooting: https://docs.nvidia.com/nemoclaw/openshell/troubleshooting`,
+    };
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/commands/logs.ts` around lines 56 - 66, cliLogs() currently
returns early when isSandboxRunning() fails, which masks OpenShell-missing
ENOENT errors; update cliLogs() so errors from isSandboxRunning() are either
rethrown or handled the same way as the existing catch that checks
err.message.includes("ENOENT"). Concretely, in cliLogs() wrap the
isSandboxRunning() call with try/catch and when the caught error message
contains "ENOENT" call the same logger.error message used in the diff (the
OpenShell CLI not found message and install guide), otherwise propagate or
rethrow the error so the outer error handling (the else branch that logs
`OpenShell error running 'openshell sandbox connect'`) can run. Ensure you
reference isSandboxRunning(), cliLogs(), and the logger.error messages so the
behavior is consistent.

Comment on lines +126 to 136
it("shows OpenShell error context and 'Not configured' in text output", async () => {
const { lines, logger } = captureLogger();

await cliStatus({ json: false, logger, pluginConfig: defaultConfig });

const output = lines.join("\n");
expect(output).toContain("Status: not running");
// When openshell is not installed, status shows an actionable error with a docs link
// rather than a silent "not running" — see https://github.com/NVIDIA/NemoClaw/issues/60
expect(output).toContain("Status: unknown —");
expect(output).toContain("https://docs.nvidia.com/nemoclaw/openshell/troubleshooting");
expect(output).toContain("Not configured");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

“No openshell” scenario should explicitly cover the install-guide branch.

This assertion currently checks the troubleshooting URL path. Please add/adjust a case that simulates missing CLI (ENOENT) and asserts the install guide URL so the intended branch is protected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/commands/status.test.ts` around lines 126 - 136, Update the test
"shows OpenShell error context and 'Not configured' in text output" to also
simulate the missing OpenShell CLI by causing the underlying exec/spawn/which
call used by cliStatus to throw an ENOENT error (e.g., stub the method that
checks/executes OpenShell) while still using captureLogger() and defaultConfig,
then call cliStatus({ json: false, logger, pluginConfig: defaultConfig }) and
add an assertion that the output contains the OpenShell install-guide URL (the
install-guide branch) in addition to the existing troubleshooting URL check so
the ENOENT/install-guide branch in cliStatus is exercised.

Comment on lines +147 to +151
const msg = err instanceof Error ? err.message : String(err);
const openshellError = msg.includes("ENOENT")
? "OpenShell CLI not found — see https://docs.nvidia.com/nemoclaw/openshell/install"
: `OpenShell error: ${msg} — see https://docs.nvidia.com/nemoclaw/openshell/troubleshooting`;
return { name: sandboxName, running: false, uptime: null, insideSandbox: false, openshellError };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Node.js child_process.exec (shell command string), what error fields are typically set when the command is missing on Linux/macOS? Specifically, is ENOENT guaranteed in error.message, or is it usually an exit code/stderr "command not found" path?

💡 Result:

On Linux/macOS, child_process.exec(commandString, cb) runs your string inside a shell (typically /bin/sh -c ...), so a missing command is usually not a spawn failure in Node—it’s a shell-level error. Node’s docs note that exec() “spawns a shell” and that, on error, error.code is the exit code of the process. [1]

What you typically see with exec() when the command is missing

  • error.code: commonly 127 (the conventional/POSIX shell exit status for “command not found”). [3]
  • stderr: usually contains something like sh: <cmd>: command not found (exact wording varies by shell).
  • error.message: typically starts with Command failed: <your command> and often includes the shell’s stderr text, but this is not guaranteed to include ENOENT (because the shell did run; the inner command didn’t). [1]

When you do get ENOENT

You get ENOENT when the process can’t be spawned at all (e.g., using spawn()/execFile() without a shell and the executable can’t be found, or if the configured shell path itself doesn’t exist). Node explicitly documents for spawn() that ENOENT is emitted when the command does not exist. [2]

Bottom line: For exec() with a missing command on Linux/macOS, expect exit code 127 + “command not found” on stderr, not an ENOENT in error.message. ENOENT is typical for non-shell spawning (spawn/execFile) or a missing shell, not for a missing command inside the shell. [1][2][3]

Sources: [1] Node.js child_process.exec() docs (latest) (nodejs.org), [2] Node.js child_process.spawn() docs on ENOENT (r2.nodejs.org), [3] POSIX/shell convention: exit status 127 for “command not found” (unix.stackexchange.com)

Citations:


ENOENT detection is unreliable—exec() surfaces missing commands as shell exit code 127 + stderr.

When child_process.exec() runs with a missing command on Linux/macOS, the shell (not Node) encounters the missing executable and returns exit code 127 with stderr containing "command not found". The error object does not contain ENOENT in the message. The current check msg.includes("ENOENT") will miss this case, showing a generic error instead of the install-guide message.

Suggested fix
   } catch (err) {
     const msg = err instanceof Error ? err.message : String(err);
-    const openshellError = msg.includes("ENOENT")
+    const stderr =
+      err && typeof err === "object" && "stderr" in err
+        ? String((err as { stderr?: unknown }).stderr ?? "")
+        : "";
+    const code =
+      err && typeof err === "object" && "code" in err
+        ? (err as { code?: unknown }).code
+        : undefined;
+    const combined = `${msg}\n${stderr}`.toLowerCase();
+    const missingOpenShell =
+      code === "ENOENT" ||
+      code === 127 ||
+      combined.includes("command not found") ||
+      combined.includes("not found");
+
+    const openshellError = missingOpenShell
       ? "OpenShell CLI not found — see https://docs.nvidia.com/nemoclaw/openshell/install"
       : `OpenShell error: ${msg} — see https://docs.nvidia.com/nemoclaw/openshell/troubleshooting`;
     return { name: sandboxName, running: false, uptime: null, insideSandbox: false, openshellError };
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/commands/status.ts` around lines 147 - 151, The current ENOENT
check on the exec() error is unreliable; update the error detection in the
status handler that builds openshellError (using variables err, msg, and
openshellError) to also treat shell-missing-command cases as "OpenShell CLI not
found": check if the error object has code === 127 or if stderr/message contains
"command not found" (case-insensitive) and, if so, set the openshellError to the
install-guide string; otherwise fall back to the existing generic
openshell/troubleshooting message.

@wscurran wscurran added documentation Improvements or additions to documentation NemoClaw CLI Use this label to identify issues with the NemoClaw command-line interface (CLI). labels Mar 19, 2026
Closes NVIDIA#60

When an `openshell` CLI call fails, errors now identify OpenShell as
the source and include a docs link rather than surfacing raw process
errors or silently returning a default value.

Changes:
- onboard.ts: add `wrapOpenShellError()` helper that wraps ENOENT as
  "OpenShell CLI not found" and all other failures as
  "OpenShell error running 'openshell <subcommand>': <detail>" with
  a link to the troubleshooting guide. `execOpenShell()` now calls
  this helper so every call site gets consistent messages.
- connect.ts: enrich both ENOENT and generic error branches with
  the same "OpenShell error" prefix and docs link.
- logs.ts: add matching ENOENT / generic error handling to the log
  streaming spawn error handler.
- status.ts: instead of silently returning `running: false` when
  `openshell sandbox status` fails, capture the error into a new
  `openshellError` field and display it in the status output as
  "Status:  unknown — <message>" so the user knows why the sandbox
  state could not be determined.
- status.test.ts: update the "no openshell" scenario to assert the
  new actionable error message rather than the silent "not running".
@hkc5 hkc5 force-pushed the fix/openshell-error-context branch from ef53663 to 04fb08e Compare March 20, 2026 08:45
@miyoungc
Copy link
Copy Markdown
Contributor

There was a big change merged in #473.
And another one in the queue #492.
These are/are going to be no longer existing.
Thank you for your contribution effort. Closing the issue and this PR.

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

Labels

documentation Improvements or additions to documentation NemoClaw CLI Use this label to identify issues with the NemoClaw command-line interface (CLI).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI errors that come from OpenShell mention that dependency explicitly and point to the relevant docs

3 participants