Skip to content

refactor(cli): tighten quick-win type hotspots#2113

Open
cv wants to merge 9 commits intomainfrom
refactor/type-hotspots-quick-wins
Open

refactor(cli): tighten quick-win type hotspots#2113
cv wants to merge 9 commits intomainfrom
refactor/type-hotspots-quick-wins

Conversation

@cv
Copy link
Copy Markdown
Contributor

@cv cv commented Apr 20, 2026

Summary

This is the first of two follow-up typing PRs driven by the new hotspot finder. It focuses on lower-risk helpers and maintainer scripts, removing several @ts-nocheck islands and tightening parsing/type boundaries without changing user-facing behavior.

Changes

  • remove @ts-nocheck and add explicit types in src/lib/sandbox-build-context.ts and src/lib/shields-timer.ts
  • replace untyped JSON/GitHub CLI parsing in scripts/ts-migration-bulk-fix-prs.ts and scripts/check-legacy-migrated-paths.ts with structured helpers and typed result shapes
  • tighten permission/error handling in src/lib/config-io.ts to reduce unsafe casts while preserving the existing API
  • update src/lib/nim.test.ts to use a namespace import so tsconfig.cli.json stays green after the newer dist/lib/nim type surface landed on main
  • reduce the quick-win hotspot scores from 95/74/70/57/47 to 0/8/5/0/29 for sandbox-build-context, shields-timer, ts-migration-bulk-fix-prs, check-legacy-migrated-paths, and config-io

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with doc updates
  • Doc only (prose changes, no code sample modifications)
  • Doc only (includes code sample changes)

Verification

  • npx prek run --all-files passes
  • npm test passes
  • Tests added or updated for new or changed behavior
  • No secrets, API keys, or credentials committed
  • Docs updated for user-facing behavior changes
  • make docs builds without warnings (doc changes only)
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

AI Disclosure

  • AI-assisted — tool: OpenAI Codex (pi coding agent)

Signed-off-by: Carlos Villela cvillela@nvidia.com

Summary by CodeRabbit

  • Refactor
    • Strengthened type safety and input validation across CLI tools and timers; removed broad type suppressions.
  • Public API
    • Introduced clearer typed build-context outputs and updated related function signatures.
  • Bug Fixes
    • More robust config error handling, temp-file/marker cleanup, and CLI/Git-output parsing; improved timer/restore exit behavior.
  • Tests
    • Updated test imports to match module exports.

Signed-off-by: Carlos Villela <cvillela@nvidia.com>
@cv cv self-assigned this Apr 20, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 20, 2026

📝 Walkthrough

Walkthrough

Strengthens TypeScript typing and runtime validation across scripts and libs: removes suppression directives, converts CommonJS to ESM imports, adds explicit types/return annotations and exported interfaces, introduces parsing/type-guard helpers, tightens error handling, and refactors timer/state control flow.

Changes

Cohort / File(s) Summary
CLI & migration scripts
scripts/check-legacy-migrated-paths.ts, scripts/ts-migration-bulk-fix-prs.ts
Removed // @ts-nocheck``; added explicit TypeScript types and return annotations; replaced inline parsing/JSON handling with typed parsers and type guards; improved child_process typings; validated CLI args; renamed/clarified helper functions (e.g., commentOnPr). Review parsing helpers and error paths.
Sandbox build context (typed API)
src/lib/sandbox-build-context.ts
Converted Node built-ins to ESM imports; added/exported StagedBuildContext and BuildContextStats interfaces; annotated exported staging and stats functions with explicit parameter and return types. Verify exported signatures and callers.
Config I/O & error handling
src/lib/config-io.ts
Made HOME resolution nullish-safe (??); added isErrnoException type guard; coerced PID/UID via String(...); introduced cleanupTempFile(); centralized typed error handling in read/write/ensure flows. Check error-branch behavior and temp-file cleanup.
Shields timer & state handling
src/lib/shields-timer.ts
Reworked top-level timeout into typed helpers (parseTimerArgs, readStateFile, updateState, runRestoreTimer, main); moved to ESM imports; removed module-captured globals; unified exit-code handling and ensured marker cleanup in finally. Inspect flow around state persistence, marker semantics, and external run(...) status handling.
Test import fix
src/lib/nim.test.ts
Changed compiled nim import to namespace form (import * as nim) and updated test references to use nim.*. Confirm test import shape matches compiled output.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through types and tidy guards,
Pulled up old requires and set new guards,
I parsed each line and cleaned the trail,
Left markers tidy without fail,
A happy rabbit with a tiny tail. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 PR title 'refactor(cli): tighten quick-win type hotspots' directly and specifically describes the main purpose of the changeset—addressing type safety improvements across multiple files by removing @ts-nocheck directives and adding explicit typing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/type-hotspots-quick-wins

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: 1

🧹 Nitpick comments (4)
scripts/ts-migration-bulk-fix-prs.ts (2)

282-423: Consider extracting PR processing logic to reduce complexity.

The main function handles many responsibilities: argument parsing, PR iteration, checkout, migration, merge, validation, commit, push, and commenting. While the logic is clear, the function has high cyclomatic complexity (estimated ~18-20 branches) approaching the guideline limit of 20.

Consider extracting the per-PR processing loop body (lines 294-412) into a dedicated processPr(prNumber, options, summary) function for improved readability and testability.

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

In `@scripts/ts-migration-bulk-fix-prs.ts` around lines 282 - 423, Extract the
per-PR logic inside main (the loop handling each prNumber) into a new function
processPr(prNumber, options, summary) so main only parses args, gets targets via
listTargetPrs(), iterates and calls processPr, and finalizes cleanup; move all
checks and actions that reference getPrMetadata, touchesLegacyPaths,
maintainerCanModify handling, gh checkout/run/npm
assist/merge/validateBranch/git add/commit/push and commentOnPr into processPr,
return status updates (fixed/skipped/manual) or mutate summary there, and ensure
main still performs the final checkout to startingBranch in the finally block
and forwards options.base and options.dryRun where needed.

82-96: Consider validating required fields in parsePrMetadata.

The number field defaults to 0 when missing or invalid (line 88). Since PR numbers are always positive, a 0 value indicates malformed data. Consider throwing an error similar to how parsePrList handles malformed data, rather than silently returning an invalid metadata object that could cause confusing behavior downstream.

💡 Proposed improvement
 function parsePrMetadata(value: unknown): PullRequestMetadata {
   if (!isRecord(value)) {
     throw new Error("GitHub CLI returned malformed PR metadata.");
   }

+  const number = readNumber(value, "number");
+  if (number === null || number <= 0) {
+    throw new Error("GitHub CLI returned PR metadata with invalid number.");
+  }
+
   return {
-    number: readNumber(value, "number") ?? 0,
+    number,
     title: readString(value, "title"),
     headRefName: readString(value, "headRefName"),
     baseRefName: readString(value, "baseRefName"),
     url: readString(value, "url"),
     maintainerCanModify: readBoolean(value, "maintainerCanModify"),
     files: parsePrFiles(value.files),
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/ts-migration-bulk-fix-prs.ts` around lines 82 - 96, The
parsePrMetadata function currently defaults number to 0 which can hide malformed
input; update parsePrMetadata to validate required fields (at minimum ensure
readNumber(value, "number") returns a positive integer) and throw an Error if
the number is missing or <= 0 (mirroring parsePrList behavior) instead of using
0; adjust error text to indicate malformed PR metadata and keep the existing
parsing of title, headRefName, baseRefName, url, maintainerCanModify, and files
(parsePrFiles) intact so callers receive a guaranteed-valid PullRequestMetadata
object.
scripts/check-legacy-migrated-paths.ts (1)

83-95: Verify the fallback logic for rename/copy entries.

For rename (R) and copy (C) status entries, git outputs the format R100\toldPath\tnewPath. If secondPath is empty for a rename/copy entry, this would indicate malformed git output. The fallback secondPath || firstPath would incorrectly set newPath to the old path in that scenario.

Consider returning null for R/C entries with missing secondPath rather than silently using the old path as the new path:

💡 Proposed stricter handling
   const isRenameOrCopy = /^[RC]/.test(status);
+  if (isRenameOrCopy && !secondPath) {
+    return null; // Malformed R/C entry
+  }
   return {
     status,
     oldPath: isRenameOrCopy ? firstPath : null,
-    newPath: isRenameOrCopy ? secondPath || firstPath : firstPath,
+    newPath: isRenameOrCopy ? secondPath : firstPath,
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/check-legacy-migrated-paths.ts` around lines 83 - 95,
parseChangedFileLine currently treats rename/copy entries (status matching
/^[RC]/) as valid even when the secondPath is missing by falling back to
firstPath; change this to treat such malformed R/C lines as invalid and return
null instead. Specifically, in parseChangedFileLine check if isRenameOrCopy and
secondPath is empty — if so return null instead of using secondPath ||
firstPath; otherwise keep the existing behavior (oldPath = firstPath, newPath =
secondPath). This ensures we don't silently treat oldPath as newPath for R/C
entries and makes the function fail fast on malformed git output.
src/lib/config-io.ts (1)

124-126: Consider adding runtime validation for type safety.

While this change makes the unsafe cast more explicit (which is good), parsed as T still lacks runtime validation. If the config file contains JSON that doesn't match type T, the caller will receive incorrectly typed data, potentially causing runtime errors elsewhere.

🛡️ Consider adding a validator parameter for runtime safety
-export function readConfigFile<T>(filePath: string, fallback: T): T {
+export function readConfigFile<T>(
+  filePath: string,
+  fallback: T,
+  validator?: (data: unknown) => data is T
+): T {
   try {
     const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf-8"));
+    if (validator && !validator(parsed)) {
+      return fallback;
+    }
     return parsed as T;
   } catch (error: unknown) {

Alternatively, use a schema validation library like zod for robust type checking:

import { z } from 'zod';

export function readConfigFile<T>(
  filePath: string,
  fallback: T,
  schema: z.ZodSchema<T>
): T {
  try {
    const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf-8"));
    return schema.parse(parsed);
  } catch (error: unknown) {
    // ...
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/config-io.ts` around lines 124 - 126, The readConfigFile function
currently returns parsed as T without runtime checks; update its signature
(readConfigFile) to accept a validator (either a predicate function or a
zod/schema object) and use that validator to validate/parse the unknown JSON
before returning, e.g., call schema.parse(parsed) or validator(parsed) and throw
or fallback when validation fails; ensure the catch block and return path use
the validated value (not the raw parsed) and preserve existing fallback behavior
so callers receive a runtime-checked T.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/config-io.ts`:
- Line 13: The current assignment to the local variable `home` uses the nullish
coalescing operator and will not fall back when `process.env.HOME` is an empty
string; change the expression that initializes `home` (currently `const home =
process.env.HOME ?? os.homedir();`) to use a logical OR so an empty string falls
back to `os.homedir()`, matching the handling used in `src/lib/credentials.ts`.

---

Nitpick comments:
In `@scripts/check-legacy-migrated-paths.ts`:
- Around line 83-95: parseChangedFileLine currently treats rename/copy entries
(status matching /^[RC]/) as valid even when the secondPath is missing by
falling back to firstPath; change this to treat such malformed R/C lines as
invalid and return null instead. Specifically, in parseChangedFileLine check if
isRenameOrCopy and secondPath is empty — if so return null instead of using
secondPath || firstPath; otherwise keep the existing behavior (oldPath =
firstPath, newPath = secondPath). This ensures we don't silently treat oldPath
as newPath for R/C entries and makes the function fail fast on malformed git
output.

In `@scripts/ts-migration-bulk-fix-prs.ts`:
- Around line 282-423: Extract the per-PR logic inside main (the loop handling
each prNumber) into a new function processPr(prNumber, options, summary) so main
only parses args, gets targets via listTargetPrs(), iterates and calls
processPr, and finalizes cleanup; move all checks and actions that reference
getPrMetadata, touchesLegacyPaths, maintainerCanModify handling, gh
checkout/run/npm assist/merge/validateBranch/git add/commit/push and commentOnPr
into processPr, return status updates (fixed/skipped/manual) or mutate summary
there, and ensure main still performs the final checkout to startingBranch in
the finally block and forwards options.base and options.dryRun where needed.
- Around line 82-96: The parsePrMetadata function currently defaults number to 0
which can hide malformed input; update parsePrMetadata to validate required
fields (at minimum ensure readNumber(value, "number") returns a positive
integer) and throw an Error if the number is missing or <= 0 (mirroring
parsePrList behavior) instead of using 0; adjust error text to indicate
malformed PR metadata and keep the existing parsing of title, headRefName,
baseRefName, url, maintainerCanModify, and files (parsePrFiles) intact so
callers receive a guaranteed-valid PullRequestMetadata object.

In `@src/lib/config-io.ts`:
- Around line 124-126: The readConfigFile function currently returns parsed as T
without runtime checks; update its signature (readConfigFile) to accept a
validator (either a predicate function or a zod/schema object) and use that
validator to validate/parse the unknown JSON before returning, e.g., call
schema.parse(parsed) or validator(parsed) and throw or fallback when validation
fails; ensure the catch block and return path use the validated value (not the
raw parsed) and preserve existing fallback behavior so callers receive a
runtime-checked T.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f757ef4-9be3-4d6d-8a8e-b50d83c9c935

📥 Commits

Reviewing files that changed from the base of the PR and between a7dca9c and f19f948.

📒 Files selected for processing (6)
  • scripts/check-legacy-migrated-paths.ts
  • scripts/ts-migration-bulk-fix-prs.ts
  • src/lib/config-io.ts
  • src/lib/nim.test.ts
  • src/lib/sandbox-build-context.ts
  • src/lib/shields-timer.ts

Comment thread src/lib/config-io.ts

function buildRemediation(): string {
const home = process.env.HOME || os.homedir();
const home = process.env.HOME ?? os.homedir();
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check for HOME environment variable handling patterns

# Search for other HOME handling patterns
rg -n --type=ts -C3 'process\.env\.HOME' -g '!node_modules'

# Check if empty string HOME is explicitly handled anywhere
rg -n --type=ts 'HOME.*==.*""' -g '!node_modules'

Repository: NVIDIA/NemoClaw

Length of output: 34338


Use || instead of ?? to handle empty process.env.HOME.

The nullish coalescing operator ?? only handles null or undefined, not empty strings. If process.env.HOME is set to an empty string, the code will use the empty string rather than falling back to os.homedir(). This contradicts the PR summary's intent to "handle process.env.HOME being an empty string."

For consistency with similar handling elsewhere in the codebase (e.g., src/lib/credentials.ts), use the logical OR operator || instead:

const home = process.env.HOME || os.homedir();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/config-io.ts` at line 13, The current assignment to the local
variable `home` uses the nullish coalescing operator and will not fall back when
`process.env.HOME` is an empty string; change the expression that initializes
`home` (currently `const home = process.env.HOME ?? os.homedir();`) to use a
logical OR so an empty string falls back to `os.homedir()`, matching the
handling used in `src/lib/credentials.ts`.

@jyaunches jyaunches self-requested a review April 20, 2026 20:14
@cv cv enabled auto-merge (squash) April 20, 2026 20:26
@wscurran wscurran added refactor This is a refactor of the code and/or architecture. NemoClaw CLI Use this label to identify issues with the NemoClaw command-line interface (CLI). labels Apr 20, 2026
ericksoa added a commit that referenced this pull request Apr 21, 2026
…erations (#2117)

## Summary

Adds a shared `sandbox-session-state.ts` module that detects active SSH
connections to OpenShell sandboxes before destructive operations
proceed. This addresses the core issue in #992: no destructive operation
in the CLI checks for active SSH sessions before proceeding, leading to
abrupt Broken pipe errors with no graceful warning.

## Architectural Context

This is part of the broader onboard.ts decomposition effort and JS→TS
migration arc (see #2113 for the type-safety direction). The fix creates
reusable infrastructure that improves **destroy, rebuild, status, list,
and connect**.

The module follows the `gateway-state.ts` pattern: pure typed
classifiers that parse CLI output are separated from the I/O layer with
dependency injection for testability.

## Changes

- **New module: `src/lib/sandbox-session-state.ts`** — ESM TypeScript,
no `@ts-nocheck`, explicit types, dependency-injected I/O
  - `parseForwardList()` — parses `openshell forward list` output
  - `parseSshProcesses()` — parses `pgrep -a ssh` output
  - `classifySessionState()` — combines both sources
  - `getActiveSandboxSessions()` — high-level entry point
  - `createSystemDeps()` — factory for real system deps
- **`src/nemoclaw.ts`** — wires session detection into 6 consumers:
- `sandboxDestroy()`: ⚠️ "Active SSH session detected. Destroy anyway?"
  - `sandboxRebuild()`: ⚠️ Same warning pattern
  - `cleanupGatewayAfterLastSandbox()`: Note about active port forwards
- `sandboxConnect()`: "Note: existing session detected (another
terminal)"
- `sandboxStatus()`: Shows "Connected: yes (N sessions)" / "Connected:
no"
  - `nemoclaw list`: ● indicator next to connected sandboxes
- **`src/lib/inventory-commands.ts`** — adds optional
`getActiveSessionCount` to `ListSandboxesCommandDeps`

## Type of Change
- [x] Code change (feature, bug fix, or refactor)

## Test plan
- [x] 27 unit tests in `src/lib/sandbox-session-state.test.ts` covering
all pure classifiers and integration function
- [x] `npx vitest run --project cli` passes (existing tests unaffected)
- [x] `npx tsc -p tsconfig.src.json --noEmit` clean
- [x] All guards wrapped in try/catch — if detection fails, commands
behave identically to before (zero regression risk)

## Verification
- [x] `npm test` passes (pre-existing failure in
install-preflight.test.ts unrelated)
- [x] No secrets, API keys, or credentials committed
- [ ] Tests added or updated for new or changed behavior ✅ (27 new
tests)

## AI Disclosure
- [x] AI-assisted — tool: pi coding agent

Closes #992

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Sandbox list shows a connection indicator when active SSH sessions are
detected.
  * Status command reports active session counts per sandbox.
  * Connect alerts if a sandbox already has active sessions.
* Destroy and rebuild prompt warn about active sessions and require
confirmation when present.

* **Tests**
* Added comprehensive tests for session detection, parsing, and
classification.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Test User <test@example.com>
Co-authored-by: Aaron Erickson 🦞 <aerickson@nvidia.com>
@cv cv requested a review from brandonpelfrey April 21, 2026 23:39
…s-quick-wins

# Conflicts:
#	src/lib/shields-timer.ts
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.

🧹 Nitpick comments (1)
src/lib/shields-timer.ts (1)

88-96: Consider ensuring STATE_DIR exists before writing.

Unlike the pattern in shields.ts (lines 92-93), this version of updateState doesn't call fs.mkdirSync(STATE_DIR, { recursive: true }) before writing. While the directory should exist (created when shields were lowered), adding the defensive creation would improve robustness if the directory is removed between timer spawn and restore.

♻️ Optional: defensive directory creation
 function updateState(stateFile: string, patch: ShieldsStatePatch): void {
   try {
     const current = readStateFile(stateFile);
     const updated = { ...current, ...patch, updatedAt: new Date().toISOString() };
+    fs.mkdirSync(path.dirname(stateFile), { recursive: true, mode: 0o700 });
     fs.writeFileSync(stateFile, JSON.stringify(updated, null, 2), { mode: 0o600 });
   } catch {
     // Best effort
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/shields-timer.ts` around lines 88 - 96, The updateState function
should defensively ensure the state directory exists before writing: inside
updateState (function name) call fs.mkdirSync(STATE_DIR, { recursive: true })
prior to fs.writeFileSync so the write won't fail if the directory was removed;
keep the existing try/catch behavior and make sure you reference the same
STATE_DIR symbol used in shields.ts to create the directory before serializing
the updated object (ShieldsStatePatch and readStateFile remain unchanged).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/lib/shields-timer.ts`:
- Around line 88-96: The updateState function should defensively ensure the
state directory exists before writing: inside updateState (function name) call
fs.mkdirSync(STATE_DIR, { recursive: true }) prior to fs.writeFileSync so the
write won't fail if the directory was removed; keep the existing try/catch
behavior and make sure you reference the same STATE_DIR symbol used in
shields.ts to create the directory before serializing the updated object
(ShieldsStatePatch and readStateFile remain unchanged).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ac237e4-8784-4a0b-a71d-0a4b3953a1e6

📥 Commits

Reviewing files that changed from the base of the PR and between a11bf23 and 797a48e.

📒 Files selected for processing (2)
  • src/lib/sandbox-build-context.ts
  • src/lib/shields-timer.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/sandbox-build-context.ts

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

Labels

NemoClaw CLI Use this label to identify issues with the NemoClaw command-line interface (CLI). refactor This is a refactor of the code and/or architecture.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants