Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions bin/lib/services.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

// Thin CJS shim — implementation lives in nemoclaw/src/lib/services.ts
module.exports = require("../../nemoclaw/dist/lib/services.js");
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check nemoclaw package.json for "type" field
echo "=== nemoclaw/package.json type field ==="
cat nemoclaw/package.json | jq '.type // "commonjs (default)"'

# Check tsconfig for module output format
echo -e "\n=== nemoclaw/tsconfig.json module setting ==="
cat nemoclaw/tsconfig.json | jq '.compilerOptions.module // "not set"'

# Check if dist/lib/services.js exists and its format
echo -e "\n=== Check compiled output format (if exists) ==="
if [ -f nemoclaw/dist/lib/services.js ]; then
  head -20 nemoclaw/dist/lib/services.js
else
  echo "dist/lib/services.js not found (not built yet)"
fi

Repository: NVIDIA/NemoClaw

Length of output: 261


CJS/ESM interoperability conflict: this shim will fail at runtime.

The shim uses require() to load compiled output, but nemoclaw/package.json declares "type": "module" (ESM package) while nemoclaw/tsconfig.json specifies "module": "Node16" (CommonJS output). When Node.js loads nemoclaw/dist/lib/services.js, it treats it as ESM due to the package type, but the file contains CommonJS syntax, causing an immediate runtime failure.

Fix one of these:

  • Change nemoclaw/tsconfig.json to output ESM (e.g., "module": "ES2020")
  • Use .cjs file extension for CommonJS output in ESM packages
  • Switch the shim to use dynamic import() instead of require()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/services.js` around lines 4 - 5, The runtime fails because
bin/lib/services.js uses require() to load an ESM package; replace the CJS
require shim with an async dynamic import so the ESM-built output can be loaded
at runtime: change the module.exports =
require("../../nemoclaw/dist/lib/services.js") pattern to perform an
import("../../nemoclaw/dist/lib/services.js") and then re-export the imported
namespace (or its default) via module.exports (or export default) so callers
continue to work; target the symbol in the diff (the module.exports =
require(...) line) when making the change.

8 changes: 4 additions & 4 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const registry = require("./lib/registry");
const nim = require("./lib/nim");
const policies = require("./lib/policies");
const { parseGatewayInference } = require("./lib/inference-config");
const services = require("./lib/services");

// ── Global commands ──────────────────────────────────────────────

Expand Down Expand Up @@ -208,12 +209,11 @@ async function start() {
await ensureApiKey();
const { defaultSandbox } = registry.listSandboxes();
const safeName = defaultSandbox && /^[a-zA-Z0-9._-]+$/.test(defaultSandbox) ? defaultSandbox : null;
const sandboxEnv = safeName ? `SANDBOX_NAME=${shellQuote(safeName)}` : "";
run(`${sandboxEnv} bash "${SCRIPTS}/start-services.sh"`);
await services.startAll({ sandboxName: safeName || undefined, repoDir: ROOT });
}

function stop() {
run(`bash "${SCRIPTS}/start-services.sh" --stop`);
services.stopAll();
}

function debug(args) {
Expand Down Expand Up @@ -268,7 +268,7 @@ function showStatus() {
}

// Show service status
run(`bash "${SCRIPTS}/start-services.sh" --status`);
services.showStatus();
}

function listSandboxes() {
Expand Down
147 changes: 147 additions & 0 deletions nemoclaw/src/lib/services.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
import { showStatus, stopAll, getServiceStatuses } from "../../dist/lib/services.js";

// ---------------------------------------------------------------------------
// Mock node:fs — in-memory filesystem for PID files & logs
// ---------------------------------------------------------------------------

const store = new Map<string, string>();
const dirs = new Set<string>();

vi.mock("node:fs", async (importOriginal) => {
const original = await importOriginal();
return {
...original,
existsSync: (p: string) => store.has(p) || dirs.has(p),
mkdirSync: (_p: string) => {
dirs.add(_p);
},
readFileSync: (p: string) => {
const content = store.get(p);
if (content === undefined) throw new Error(`ENOENT: ${p}`);
return content;
},
writeFileSync: (p: string, data: string) => {
store.set(p, data);
},
unlinkSync: (p: string) => {
store.delete(p);
},
};
});

// ---------------------------------------------------------------------------
// Mock process.kill for PID checks (default: process not found)
// ---------------------------------------------------------------------------

const originalKill = process.kill.bind(process);
let killMock: (pid: number, signal?: string | number) => boolean;

beforeEach(() => {
store.clear();
dirs.clear();
// Default: all processes are dead
killMock = (_pid: number, _signal?: string | number) => {
const err = new Error("ESRCH") as NodeJS.ErrnoException;
err.code = "ESRCH";
throw err;
};
process.kill = killMock as typeof process.kill;
});

afterEach(() => {
process.kill = originalKill;
});

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("lib/services", () => {
describe("getServiceStatuses", () => {
it("reports all services as stopped when no PID files exist", () => {
const statuses = getServiceStatuses({ pidDir: "/tmp/test-svc" });
expect(statuses).toEqual([
{ name: "telegram-bridge", running: false, pid: null },
{ name: "cloudflared", running: false, pid: null },
]);
});

it("reports a service as running when PID file exists and process is alive", () => {
store.set("/tmp/test-svc/telegram-bridge.pid", "12345");
// Make process.kill(12345, 0) succeed
killMock = (pid: number, signal?: string | number) => {
if (pid === 12345 && (signal === 0 || signal === undefined)) return true;
const err = new Error("ESRCH") as NodeJS.ErrnoException;
err.code = "ESRCH";
throw err;
};
process.kill = killMock as typeof process.kill;

const statuses = getServiceStatuses({ pidDir: "/tmp/test-svc" });
expect(statuses[0]).toEqual({ name: "telegram-bridge", running: true, pid: 12345 });
expect(statuses[1]).toEqual({ name: "cloudflared", running: false, pid: null });
});

it("reports a service as not running when PID file exists but process is dead", () => {
store.set("/tmp/test-svc/cloudflared.pid", "99999");
const statuses = getServiceStatuses({ pidDir: "/tmp/test-svc" });
expect(statuses[1]).toEqual({ name: "cloudflared", running: false, pid: 99999 });
});

it("handles malformed PID file", () => {
store.set("/tmp/test-svc/telegram-bridge.pid", "not-a-number");
const statuses = getServiceStatuses({ pidDir: "/tmp/test-svc" });
expect(statuses[0]).toEqual({ name: "telegram-bridge", running: false, pid: null });
});
});

describe("showStatus", () => {
it("prints status without crashing", () => {
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
showStatus({ pidDir: "/tmp/test-svc" });
expect(logSpy).toHaveBeenCalled();
logSpy.mockRestore();
});

it("prints cloudflared URL from log file", () => {
store.set(
"/tmp/test-svc/cloudflared.log",
"some output\nhttps://abc-123.trycloudflare.com\nmore output",
);
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
showStatus({ pidDir: "/tmp/test-svc" });
const output = logSpy.mock.calls.map((c) => String(c[0])).join("\n");
expect(output).toContain("https://abc-123.trycloudflare.com");
logSpy.mockRestore();
});
});

describe("stopAll", () => {
it("removes PID files and reports stopped", () => {
store.set("/tmp/test-svc/telegram-bridge.pid", "111");
store.set("/tmp/test-svc/cloudflared.pid", "222");

const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
stopAll({ pidDir: "/tmp/test-svc" });

expect(store.has("/tmp/test-svc/telegram-bridge.pid")).toBe(false);
expect(store.has("/tmp/test-svc/cloudflared.pid")).toBe(false);

const output = logSpy.mock.calls.map((c) => String(c[0])).join("\n");
expect(output).toContain("All services stopped");
logSpy.mockRestore();
});

it("handles already-stopped services gracefully", () => {
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
expect(() => {
stopAll({ pidDir: "/tmp/test-svc" });
}).not.toThrow();
logSpy.mockRestore();
});
});
});
Loading