Repository navigation
refactor(cli): migrate to oclif + TypeScript #909
Description
Summary
Migrate the NemoClaw CLI from the current hand-rolled switch/case dispatcher (bin/nemoclaw.js, 515 lines) to oclif (Open CLI Framework) with TypeScript. This is a foundational cleanup that addresses systemic CLI issues — inconsistent help, missing validations, no completions, docs drift, and poor error handling — in one structural change rather than piecemeal fixes.
This wouldn't take precedence over security work, but it should be high priority. — @cv
Motivation
The current CLI has accumulated enough problems that fixing them individually is less efficient than fixing the architecture:
- No shell completions — users type everything manually
- No per-command
--help— most commands have no usage info - Docs drift from implementation — docs say
Xbut the CLI actually uses--Y X Z(per @rubenhagege) - No flag/parameter validation — arbitrary input reaches shell commands unvalidated (MEDIUM: Unvalidated Instance Name in
deploy()Shell Commands #575) - Secrets leak in error output (CRITICAL: NVIDIA API Key Exposed in Process Arguments and Terminal Output #579, security: redact secret patterns from CLI log and error output #664)
- Monolithic dispatcher — 515-line switch/case with inline implementations
- No type safety — plain JS with no compile-time checks
oclif solves most of these structurally: auto-generated help, built-in flag validation, shell completions via plugin, one-file-per-command architecture, and TypeScript support.
Note: oclif's @oclif/plugin-autocomplete supports bash, zsh, and PowerShell — not fish. Fish completion will need a lightweight custom generator or a community plugin on top of oclif.
Issues addressed (24)
Completions
- feat: add shell completion for nemoclaw CLI #155 — Shell completion for nemoclaw CLI → oclif's
@oclif/plugin-autocomplete(bash/zsh/PowerShell; fish requires additional work)
Help / docs consistency
- feat(cli): add per-command --help for all top-level and sandbox-scoped commands #757 — Per-command
--helpfor all commands → oclif auto-generates help from command metadata - feat(cli): clarify global vs sandbox-scoped status in help and UX #754 — Clarify global vs sandbox-scoped status → oclif's topic/subtopic structure makes hierarchy explicit
- feat(cli): standardize command examples and next-step remediation hints across the CLI #755 — Standardize command examples and hints → oclif's
examplesproperty on each command class - docs(cli): update command reference to match implemented slash commands and host commands #758 — Update command reference to match implementation →
oclif readmeauto-generates docs from code - ci: add CLI/docs drift test for documented commands and slash subcommands #756 — CLI/docs drift test → auto-generated docs eliminate drift
- Instructions are incorrect/misleading/unclear #406 — Instructions are incorrect/misleading/unclear → structured help clarifies usage
- NVIDIA NemoClaw Developer Guide OpenShell Install Command Out of Date #835 — Developer guide install command out of date → auto-generated docs prevent this
- unrecognized subcommand 'logs' #253 — Unrecognized subcommand 'logs' → oclif enforces documented = implemented
Flag / parameter validation
- MEDIUM: Unvalidated Instance Name in
deploy()Shell Commands #575 — Unvalidated instance name in deploy (shell injection risk) → oclif'sargswithparsefunctions - ci: validate nemoclaw config and schema files against a JSON Schema #553 — Validate config against JSON schema → typed flags and config handling
- [Bug] nemoclaw connect hangs indefinitely with no timeout or status indicator when sandbox is starting up or has crashed #430 — Connect hangs indefinitely with no timeout or status indicator when sandbox is starting up or has crashed → adding
--timeoutflags is trivial in oclif - Default ports conflict with common services (8080, 8000) #684 — Default ports conflict with common services (8080, 8000) → adding
--portflags with validation - nemoclaw 0.1.0 fails because required openshell CLI is missing and undeclared #410 — Missing openshell CLI undeclared → oclif
inithook for preflight checks - [Brev][Ubuntu 22.04][Github Issue #] nemoclaw status does not reflect runtime inference changes from openshell inference set #806 — nemoclaw status does not reflect runtime inference changes → command lifecycle hooks ensure fresh data
CLI structure / architecture
- refactor: split onboarding provider logic out of bin/lib/onboard.js #767 — Split onboarding provider logic → one-file-per-command architecture
- feat(cli): add self-update command for automatic CLI updates #642 — Self-update command →
@oclif/plugin-update - feat(cli): add --json output for list and status commands #753 —
--jsonoutput for list/status → oclif'senableJsonFlagandthis.logJson() - feat: add --verbose / --debug flag for CLI observability #666 —
--verbose/--debugglobal flag → oclif base flags inherited by all commands - feat: add basic usage metrics and observability to CLI #30 — Usage metrics/observability →
prerun/postrunhooks - Add openclaw sessions reset <session-id> subcommand #834 — Add
sessions resetsubcommand → topic/command structure - Multiple Deployment Blockers on Ubuntu 24.04: Nested Proxy Isolation, Image Pull Timeouts, and CLI Parsing Errors #802 — Multiple deployment blockers on Ubuntu 24.04 including CLI parsing errors → eliminates hand-rolled parsing bugs
Error handling
- security: redact secret patterns from CLI log and error output #664 — Redact secrets from error output → centralized
catch()on Command base class - CRITICAL: NVIDIA API Key Exposed in Process Arguments and Terminal Output #579 — API key exposed in process args → flag handling + error hooks for redaction
PRs that would be superseded or simplified (17)
| PR | Title | How migration addresses it |
|---|---|---|
| #895 | Shell completion (bash/zsh/fish) | Replaced by oclif autocomplete plugin (bash/zsh/PowerShell; fish needs custom work) |
| #160 | Shell completion (earlier attempt) | Same |
| #472 | Sandbox export + shell completion | Completion half solved by oclif |
| #901 | Sync command reference with CLI | Automated by oclif readme |
| #848 | Fix policy CLI examples in docs | Auto-generated docs stay in sync |
| #182 | Add --json to list command |
oclif's enableJsonFlag + this.logJson() |
| #350 | Fix logs subcommand routing | oclif command router eliminates this class of bug |
| #604 | Validate deploy instance names | oclif args with parse |
| #477 | Fix --type flag for brev create |
TypeScript catches flag name mismatches at compile time |
| #631 | Fix defaultValue in promptOrDefault interactive mode (Fixes #360) |
TypeScript strict null checks flag this |
| #853 | Improve model entry validation | TypeScript interfaces enforce structure |
| #644 | Self-update command | Replaced by @oclif/plugin-update |
| #826 | CLI metrics collection | oclif hooks provide cleaner pattern |
| #672 | Redact secrets from output | oclif base class catch() for global redaction |
| #794 | Redact secrets (alternate approach) | Same |
| #638 | Error handling for JSON.parse | TypeScript + oclif error classes |
| #398 | Error handling for gateway startup | oclif catch()/finally() lifecycle |
Migration plan
Phase 1 — Scaffold + simple commands (1-2 days)
- Initialize oclif project structure with TypeScript
- Migrate trivial commands:
list,status,start,stop,setup,setup-spark,debug,uninstall - Verify oclif entry point works
Phase 2 — Sandbox commands + deploy (1-2 days)
- Migrate sandbox-scoped commands under
sandbox/topic - Add
command_not_foundhook sonemoclaw <sandbox-name> <action>still works (zero UX breakage) - Migrate
deploy - Replace
process.exit()calls in lib modules with thrown errors (~15-20 call sites)
Phase 3 — Onboard wizard (2-3 days)
- Migrate the ~900-line onboard wizard (most complex command)
- Replace custom
readline/promptSecretwith oclif-compatible prompts - Verify
--non-interactivemode still works
Phase 4 — Cleanup, tests, hardening (1-2 days)
- Delete old dispatcher (
bin/nemoclaw.js) - Rewrite CLI integration tests for oclif
- Verify auto-generated help, completions, and
--jsonoutput - Update README and contributing guide
Estimated total: 5-9 days of focused work, ~30-35 files touched.
Proposed structure
src/
commands/
onboard.ts
deploy.ts
start.ts / stop.ts / status.ts / list.ts
debug.ts / uninstall.ts
setup.ts / setup-spark.ts
sandbox/
connect.ts # nemoclaw sandbox connect <name>
status.ts # nemoclaw sandbox status <name>
logs.ts # nemoclaw sandbox logs <name>
destroy.ts # nemoclaw sandbox destroy <name>
policy/
add.ts # nemoclaw sandbox policy add <name>
list.ts # nemoclaw sandbox policy list <name>
lib/ # existing modules, converted to TS
runner.ts
credentials.ts
registry.ts
nim.ts
policies.ts
onboard-wizard.ts
platform.ts
preflight.ts
inference-config.ts
local-inference.ts
resolve-openshell.ts
hooks/
command-not-found.ts # backward compat for "nemoclaw <sandbox> <action>"
UX compatibility
The nemoclaw <sandbox-name> <action> syntax is preserved via a command_not_found hook — no breaking changes to existing workflows or scripts. The new nemoclaw sandbox <action> <name> syntax is also available.
Risks
- Onboard wizard complexity — At ~900 lines with deeply nested conditional flows, interactive prompts, process spawning, and polling loops, this is where bugs are most likely during migration. Plan: keep the wizard logic in its own module and have the oclif command class be a thin wrapper.
- Interactive prompt fidelity — The custom
promptSecretincredentials.jsdoes raw-mode terminal input with careful escape sequence handling. oclif's prompts orinquirermay behave differently. Needs careful testing on macOS and Linux. - Atomic cutover —
package.jsonbinfield can only point to one dispatcher. There's no way to run both the old and new entry points simultaneously for the same binary name. The switch happens all at once. stdio: 'inherit'with oclif — Several commands (debug,uninstall,start,stop,setup-spark) delegate to bash scripts viaspawnSync(..., { stdio: 'inherit' }). oclif's output handling could interfere. Test early in Phase 1.- Fish completion gap — oclif's autocomplete plugin does not support fish. Since feat: add shell completion for nemoclaw CLI #155 and feat(cli): add shell completion for bash, zsh, and fish #895 both explicitly request fish, this needs a custom fish completion generator or community plugin as a follow-up.
Build & tooling changes
- TypeScript compilation — Adding a
tscbuild step. The project already usestscfor type-checking viajsconfig.json, but now it becomes a required build step for the CLI to run. package.jsonbinfield — Must change from./bin/nemoclaw.jsto oclif's entry point. This is the atomic cutover point.- Test runner — The project uses Vitest with ESM imports. oclif scaffolds mocha/jest by default, but we will keep Vitest to avoid rewriting all existing lib module tests. Only CLI integration tests (
cli.test.js) need rewriting for oclif's test patterns. - ESM vs CommonJS — The project is currently CommonJS, but tests use ESM via Vitest. Newer oclif versions support ESM. Decision on module system should be made in Phase 1 scaffolding.
PR disposition
- feat(cli): add shell completion for bash, zsh, and fish #895 (shell completion, this branch) — Will be closed in favor of this migration. Completion comes from oclif's autocomplete plugin instead.
- feat: add shell completion for nemoclaw CLI #160 (earlier completion attempt) — Same, close in favor of this.
- Other listed PRs can be closed individually as each phase lands, or kept open if the non-completion parts of the PR (e.g., backup feature in feat: add sandbox export/import backup commands #472) are still needed.
Definition of done
This issue is complete when:
- All commands are migrated to oclif command classes (Phases 1-3)
- All lib modules are converted to TypeScript
- Old dispatcher (
bin/nemoclaw.js) is deleted -
nemoclaw <sandbox-name> <action>backward compat works via hook - Auto-generated
--helpworks for all commands - Shell completions work for bash and zsh via
@oclif/plugin-autocomplete - All existing tests pass (Vitest)
- CI pipeline updated for TypeScript build step
Fish completion and further hardening (per-command examples, stricter validations, etc.) are follow-up PRs.
Out of scope
- Security fixes — those continue on their own track with higher priority
- New features — this is purely structural migration + TypeScript conversion
- The
onboardwizard's business logic — it moves to an oclif command class but the flow stays the same
Impact
24 issues + 17 PRs = 41 items fully or partially addressed by this migration.
/cc @cv @rubenhagege
Activity
wscurran commentedon Apr 13, 2026
Superseded by #924, which expands this into a full 6-PR migration plan covering shell consolidation, TypeScript, and oclif together. Closing in favor of that tracking issue.
ericksoa commentedon Apr 30, 2026
Follow-up on the PRs listed here as superseded or simplified: #472 has now been closed as superseded.
For sandbox state backup and restore, users should use the supported commands that are now available on
main:nemoclaw <sandbox-name> snapshot create [--name <label>]nemoclaw <sandbox-name> snapshot listnemoclaw <sandbox-name> snapshot restore [version|name|timestamp]nemoclaw <sandbox-name> snapshot restore [version|name|timestamp] --to <new-sandbox-name>nemoclaw backup-allThose commands store and restore sandbox state through
~/.nemoclaw/rebuild-backups/and are the maintained path for the workflow that #472 was aiming to cover. Thanks again to @vasanth53 for contributing the original backup/export/import work.