-
Notifications
You must be signed in to change notification settings - Fork 2.3k
refactor(cli): port start-services.sh to TypeScript #1307
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
Merged
+561
−5
Merged
Changes from 1 commit
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
013709f
refactor(cli): port start-services.sh to TypeScript
0bed503
refactor(cli): port start-services.sh to TypeScript
f95d0d0
refactor(cli): port start-services.sh to TypeScript
1e94ad3
refactor(cli): port start-services.sh to TypeScript
78faae7
fix: move services to src/lib/, replace execa with child_process, add…
e5cadb2
Merge branch 'refactor/port-start-services-to-ts-1297' of github.com:…
5a84739
fix: validate sandbox name, normalize dead PIDs, handle spawn errors
6cd30f8
fix: wire bin/nemoclaw.js to TS services module, add WSL2 workaround
4b625a2
Merge branch 'main' into refactor/port-start-services-to-ts-1297
cv 23b3df8
Merge branch 'main' into refactor/port-start-services-to-ts-1297
prekshivyas 04baa47
fix: pass sandboxName consistently to stop() and showStatus()
cv 20e804d
Merge branch 'main' into refactor/port-start-services-to-ts-1297
prekshivyas File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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: 261
CJS/ESM interoperability conflict: this shim will fail at runtime.
The shim uses
require()to load compiled output, butnemoclaw/package.jsondeclares"type": "module"(ESM package) whilenemoclaw/tsconfig.jsonspecifies"module": "Node16"(CommonJS output). When Node.js loadsnemoclaw/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:
nemoclaw/tsconfig.jsonto output ESM (e.g.,"module": "ES2020").cjsfile extension for CommonJS output in ESM packagesimport()instead ofrequire()🤖 Prompt for AI Agents