-
Notifications
You must be signed in to change notification settings - Fork 2.2k
refactor(cli): migrate runtime-recovery.js to TypeScript #1268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
448a863
65fa902
eec919d
eb16b9e
01ced3b
d3e2c33
2ac85ce
86f698e
1fa2b13
fd7dea9
354891b
48495af
37bd33d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,84 +1,7 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
| // Thin re-export shim — the implementation lives in src/lib/runtime-recovery.ts, | ||
| // compiled to dist/lib/runtime-recovery.js. | ||
|
|
||
| const onboardSession = require("./onboard-session"); | ||
|
|
||
| function stripAnsi(text) { | ||
| // eslint-disable-next-line no-control-regex | ||
| return String(text || "").replace(/\x1b\[[0-9;]*m/g, ""); | ||
| } | ||
|
|
||
| function parseLiveSandboxNames(listOutput = "") { | ||
| const clean = stripAnsi(listOutput); | ||
| const names = new Set(); | ||
| for (const rawLine of clean.split("\n")) { | ||
| const line = rawLine.trim(); | ||
| if (!line) continue; | ||
| if (/^(NAME|No sandboxes found\.?$)/i.test(line)) continue; | ||
| if (/^Error:/i.test(line)) continue; | ||
| const cols = line.split(/\s+/); | ||
| if (cols[0]) { | ||
| names.add(cols[0]); | ||
| } | ||
| } | ||
| return names; | ||
| } | ||
|
|
||
| function classifySandboxLookup(output = "") { | ||
| const clean = stripAnsi(output).trim(); | ||
| if (!clean) { | ||
| return { state: "missing", reason: "empty" }; | ||
| } | ||
| if (/sandbox not found|status:\s*NotFound/i.test(clean)) { | ||
| return { state: "missing", reason: "not_found" }; | ||
| } | ||
| if ( | ||
| /transport error|client error|Connection reset by peer|Connection refused|No active gateway|Gateway: .*Error/i.test( | ||
| clean, | ||
| ) | ||
| ) { | ||
| return { state: "unavailable", reason: "gateway_unavailable" }; | ||
| } | ||
| return { state: "present", reason: "ok" }; | ||
| } | ||
|
|
||
| function classifyGatewayStatus(output = "") { | ||
| const clean = stripAnsi(output).trim(); | ||
| if (!clean) { | ||
| return { state: "inactive", reason: "empty" }; | ||
| } | ||
| if (/Connected/i.test(clean)) { | ||
| return { state: "connected", reason: "ok" }; | ||
| } | ||
| if ( | ||
| /No active gateway|transport error|client error|Connection reset by peer|Connection refused|Gateway: .*Error/i.test( | ||
| clean, | ||
| ) | ||
| ) { | ||
| return { state: "unavailable", reason: "gateway_unavailable" }; | ||
| } | ||
| return { state: "inactive", reason: "not_connected" }; | ||
| } | ||
|
|
||
| function shouldAttemptGatewayRecovery({ | ||
| sandboxState = "missing", | ||
| gatewayState = "inactive", | ||
| } = {}) { | ||
| return sandboxState === "unavailable" && gatewayState !== "connected"; | ||
| } | ||
|
|
||
| function getRecoveryCommand() { | ||
| const session = onboardSession.loadSession(); | ||
| if (session && session.resumable !== false) { | ||
| return "nemoclaw onboard --resume"; | ||
| } | ||
| return "nemoclaw onboard"; | ||
| } | ||
|
|
||
| module.exports = { | ||
| classifyGatewayStatus, | ||
| classifySandboxLookup, | ||
| getRecoveryCommand, | ||
| parseLiveSandboxNames, | ||
| shouldAttemptGatewayRecovery, | ||
| }; | ||
| module.exports = require("../../dist/lib/runtime-recovery"); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,12 +3,13 @@ | |
|
|
||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| // Import from compiled dist/ for correct coverage attribution. | ||
| import { | ||
| classifyGatewayStatus, | ||
| classifySandboxLookup, | ||
| parseLiveSandboxNames, | ||
| shouldAttemptGatewayRecovery, | ||
| } from "../bin/lib/runtime-recovery"; | ||
| } from "../../dist/lib/runtime-recovery"; | ||
|
Comment on lines
+6
to
+12
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== Check test-related scripts for guaranteed build-before-test =="
rg -n '"(build|build:cli|test|test:cli|pretest|prepare)"\s*:' package.json || true
echo
echo "== Check CI workflows for build step before vitest/test commands =="
fd -HI -t f -i 'yml' .github/workflows -x rg -n -C2 '(vitest|npm run test|pnpm test|yarn test|build:cli|npm run build|pnpm build|yarn build)' {}Repository: NVIDIA/NemoClaw Length of output: 608 🏁 Script executed: cat -n package.jsonRepository: NVIDIA/NemoClaw Length of output: 2418 🏁 Script executed: find .github/workflows -type f -name "*.yml" -o -name "*.yaml" 2>/dev/null | head -20Repository: NVIDIA/NemoClaw Length of output: 481 🏁 Script executed: cat -n src/lib/runtime-recovery.test.ts | head -40Repository: NVIDIA/NemoClaw Length of output: 1694 🏁 Script executed: cat -n .github/workflows/pr.yaml | head -80Repository: NVIDIA/NemoClaw Length of output: 963 🏁 Script executed: cat -n .github/workflows/main.yaml | head -80Repository: NVIDIA/NemoClaw Length of output: 1097 🏁 Script executed: find . -maxdepth 1 -name "vitest.config.*" -o -name "vite.config.*" 2>/dev/null | xargs cat -n 2>/dev/null || echo "No vitest/vite config found"Repository: NVIDIA/NemoClaw Length of output: 1326 🏁 Script executed: ls -la dist/lib/runtime-recovery.* 2>/dev/null || echo "dist/lib/runtime-recovery files not found"Repository: NVIDIA/NemoClaw Length of output: 100 🏁 Script executed: cat -n .github/actions/basic-checks/action.yamlRepository: NVIDIA/NemoClaw Length of output: 1733 🏁 Script executed: find . -name "runtime-recovery.ts" -not -path "*/node_modules/*" 2>/dev/nullRepository: NVIDIA/NemoClaw Length of output: 88 🏁 Script executed: cat -n src/lib/runtime-recovery.ts | head -50Repository: NVIDIA/NemoClaw Length of output: 1996 Either import directly from source TypeScript or add a pretest build hook. The 🤖 Prompt for AI Agents |
||
|
|
||
| describe("runtime recovery helpers", () => { | ||
| it("parses live sandbox names from openshell sandbox list output", () => { | ||
|
|
@@ -29,6 +30,15 @@ describe("runtime recovery helpers", () => { | |
| expect(Array.from(parseLiveSandboxNames("No sandboxes found."))).toEqual([]); | ||
| }); | ||
|
|
||
| it("skips error lines", () => { | ||
| expect(Array.from(parseLiveSandboxNames("Error: something went wrong"))).toEqual([]); | ||
| }); | ||
|
|
||
| it("handles empty input", () => { | ||
| expect(Array.from(parseLiveSandboxNames(""))).toEqual([]); | ||
| expect(Array.from(parseLiveSandboxNames())).toEqual([]); | ||
| }); | ||
|
|
||
| it("classifies missing sandbox lookups", () => { | ||
| expect( | ||
| classifySandboxLookup('Error: × status: NotFound, message: "sandbox not found"').state, | ||
|
|
@@ -52,14 +62,7 @@ describe("runtime recovery helpers", () => { | |
| it("classifies successful sandbox lookups as present", () => { | ||
| expect( | ||
| classifySandboxLookup( | ||
| [ | ||
| "Sandbox:", | ||
| "", | ||
| " Id: abc", | ||
| " Name: my-assistant", | ||
| " Namespace: openshell", | ||
| " Phase: Ready", | ||
| ].join("\n"), | ||
| ["Sandbox:", "", " Id: abc", " Name: my-assistant", " Phase: Ready"].join("\n"), | ||
| ).state, | ||
| ).toBe("present"); | ||
| }); | ||
|
|
@@ -68,6 +71,9 @@ describe("runtime recovery helpers", () => { | |
| expect(classifyGatewayStatus("Gateway: nemoclaw\nStatus: Connected").state).toBe("connected"); | ||
| expect(classifyGatewayStatus("Error: × No active gateway").state).toBe("unavailable"); | ||
| expect(classifyGatewayStatus("").state).toBe("inactive"); | ||
| expect(classifyGatewayStatus("Gateway: nemoclaw\nStatus: Disconnected").state).toBe( | ||
| "inactive", | ||
| ); | ||
| }); | ||
|
|
||
| it("only attempts gateway recovery when sandbox access is unavailable and gateway is down", () => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| /** | ||
| * Runtime recovery helpers — classify sandbox/gateway state from CLI | ||
| * output and determine recovery strategy. | ||
| */ | ||
|
|
||
| // onboard-session is CJS — keep as require | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports | ||
| const onboardSession = require("../../bin/lib/onboard-session"); | ||
|
|
||
| // eslint-disable-next-line no-control-regex | ||
| const ANSI_RE = /\x1b\[[0-9;]*m/g; | ||
|
|
||
| function stripAnsi(text: unknown): string { | ||
| return String(text || "").replace(ANSI_RE, ""); | ||
| } | ||
|
|
||
| export interface StateClassification { | ||
| state: string; | ||
| reason: string; | ||
| } | ||
|
|
||
| export function parseLiveSandboxNames(listOutput = ""): Set<string> { | ||
| const clean = stripAnsi(listOutput); | ||
| const names = new Set<string>(); | ||
| for (const rawLine of clean.split("\n")) { | ||
| const line = rawLine.trim(); | ||
| if (!line) continue; | ||
| if (/^(NAME|No sandboxes found\.?$)/i.test(line)) continue; | ||
| if (/^Error:/i.test(line)) continue; | ||
| const cols = line.split(/\s+/); | ||
| if (cols[0]) { | ||
| names.add(cols[0]); | ||
| } | ||
| } | ||
| return names; | ||
| } | ||
|
|
||
| export function classifySandboxLookup(output = ""): StateClassification { | ||
| const clean = stripAnsi(output).trim(); | ||
| if (!clean) { | ||
| return { state: "missing", reason: "empty" }; | ||
| } | ||
| if (/sandbox not found|status:\s*NotFound/i.test(clean)) { | ||
| return { state: "missing", reason: "not_found" }; | ||
| } | ||
| if ( | ||
| /transport error|client error|Connection reset by peer|Connection refused|No active gateway|Gateway: .*Error/i.test( | ||
| clean, | ||
| ) | ||
| ) { | ||
| return { state: "unavailable", reason: "gateway_unavailable" }; | ||
| } | ||
| return { state: "present", reason: "ok" }; | ||
| } | ||
|
|
||
| export function classifyGatewayStatus(output = ""): StateClassification { | ||
| const clean = stripAnsi(output).trim(); | ||
| if (!clean) { | ||
| return { state: "inactive", reason: "empty" }; | ||
| } | ||
| if (/\bConnected\b/i.test(clean) && !/\bDisconnected\b/i.test(clean)) { | ||
| return { state: "connected", reason: "ok" }; | ||
| } | ||
| if ( | ||
| /No active gateway|transport error|client error|Connection reset by peer|Connection refused|Gateway: .*Error/i.test( | ||
| clean, | ||
| ) | ||
| ) { | ||
| return { state: "unavailable", reason: "gateway_unavailable" }; | ||
| } | ||
| return { state: "inactive", reason: "not_connected" }; | ||
| } | ||
|
|
||
| export function shouldAttemptGatewayRecovery({ | ||
| sandboxState = "missing", | ||
| gatewayState = "inactive", | ||
| } = {}): boolean { | ||
| return sandboxState === "unavailable" && gatewayState !== "connected"; | ||
| } | ||
|
|
||
| export function getRecoveryCommand(): string { | ||
| const session = onboardSession.loadSession(); | ||
| if (session && session.resumable !== false) { | ||
| return "nemoclaw onboard --resume"; | ||
| } | ||
| return "nemoclaw onboard"; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: NVIDIA/NemoClaw
Length of output: 1447
🏁 Script executed:
Repository: NVIDIA/NemoClaw
Length of output: 2384
🏁 Script executed:
Repository: NVIDIA/NemoClaw
Length of output: 7681
🏁 Script executed:
Repository: NVIDIA/NemoClaw
Length of output: 1245
Multiple critical dependencies on missing
dist/directory — CLI and test flows broken in fresh checkout.The shim in line 7 (
bin/lib/runtime-recovery.js) and related shims (bin/lib/preflight.js,bin/lib/onboard.js) require compiled modules fromdist/lib/that do not exist without an explicit build step. Additionally:dist/lib/is excluded from package.json"files"field (only"nemoclaw/dist/"is included), sonpm installwill not provide these filespreparehook does not invokebuild:cli, leaving no automatic build steptestand CLI execution will fail withMODULE_NOT_FOUNDon fresh checkout or afternpm installAffected imports:
bin/lib/runtime-recovery.js:7→dist/lib/runtime-recoverybin/lib/preflight.js:7→dist/lib/preflightbin/lib/onboard.js:59-63→dist/lib/gateway-state,validation,url-utils,build-context,dashboardEither add
dist/to the"files"field andbuild:clitoprepare, or convert these modules back to CommonJS sources inbin/lib/(preferred per guidelines: "bin/lib/**/*.jsmodules are expected to remain CommonJS").🤖 Prompt for AI Agents