Skip to content
30 changes: 4 additions & 26 deletions bin/lib/chat-filter.js
Original file line number Diff line number Diff line change
@@ -1,29 +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/chat-filter.ts,
// compiled to dist/lib/chat-filter.js.

/**
* Parse and filter Telegram chat IDs from the ALLOWED_CHAT_IDS env var.
*
* @param {string} [raw] - Comma-separated chat IDs (undefined = allow all)
* @returns {string[] | null} Array of allowed chat IDs, or null to allow all
*/
function parseAllowedChatIds(raw) {
if (!raw) return null;
return raw
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}

/**
* Check whether a chat ID is allowed by the parsed allowlist.
*
* @param {string[] | null} allowedChats - Output of parseAllowedChatIds
* @param {string} chatId - The chat ID to check
* @returns {boolean}
*/
function isChatAllowed(allowedChats, chatId) {
return !allowedChats || allowedChats.includes(chatId);
}

module.exports = { parseAllowedChatIds, isChatAllowed };
module.exports = require("../../dist/lib/chat-filter");
59 changes: 4 additions & 55 deletions bin/lib/resolve-openshell.js
Original file line number Diff line number Diff line change
@@ -1,58 +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/resolve-openshell.ts,
// compiled to dist/lib/resolve-openshell.js.

const { execSync } = require("child_process");
const fs = require("fs");

/**
* Resolve the openshell binary path.
*
* Checks `command -v` first (must return an absolute path to prevent alias
* injection), then falls back to common installation directories.
*
* @param {object} [opts] DI overrides for testing
* @param {string|null} [opts.commandVResult] Mock result (undefined = run real command)
* @param {function} [opts.checkExecutable] (path) => boolean
* @param {string} [opts.home] HOME override
* @returns {string|null} Absolute path to openshell, or null if not found
*/
function resolveOpenshell(opts = {}) {
const home = opts.home ?? process.env.HOME;

// Step 1: command -v
if (opts.commandVResult === undefined) {
try {
const found = execSync("command -v openshell", { encoding: "utf-8" }).trim();
if (found.startsWith("/")) return found;
} catch {
/* ignored */
}
} else if (opts.commandVResult && opts.commandVResult.startsWith("/")) {
return opts.commandVResult;
}

// Step 2: fallback candidates
const checkExecutable =
opts.checkExecutable ||
((p) => {
try {
fs.accessSync(p, fs.constants.X_OK);
return true;
} catch {
return false;
}
});

const candidates = [
...(home && home.startsWith("/") ? [`${home}/.local/bin/openshell`] : []),
"/usr/local/bin/openshell",
"/usr/bin/openshell",
];
for (const p of candidates) {
if (checkExecutable(p)) return p;
}

return null;
}

module.exports = { resolveOpenshell };
module.exports = require("../../dist/lib/resolve-openshell");
44 changes: 4 additions & 40 deletions bin/lib/version.js
Original file line number Diff line number Diff line change
@@ -1,43 +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/version.ts,
// compiled to dist/lib/version.js.

/**
* Resolve the NemoClaw version from (in order):
* 1. `git describe --tags --match "v*"` — works in dev / source checkouts
* 2. `.version` file at repo root — stamped at publish time
* 3. `package.json` version — hard-coded fallback
*/

const { execFileSync } = require("child_process");
const path = require("path");
const fs = require("fs");

const ROOT = path.resolve(__dirname, "..", "..");

function getVersion() {
// 1. Try git (available in dev clones and CI)
try {
const raw = execFileSync("git", ["describe", "--tags", "--match", "v*"], {
cwd: ROOT,
encoding: "utf-8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
// raw looks like "v0.3.0" or "v0.3.0-4-gabcdef1"
if (raw) return raw.replace(/^v/, "");
} catch {
// no git, or no matching tags — fall through
}

// 2. Try .version file (stamped by prepublishOnly)
try {
const ver = fs.readFileSync(path.join(ROOT, ".version"), "utf-8").trim();
if (ver) return ver;
} catch {
// not present — fall through
}

// 3. Fallback to package.json
return require(path.join(ROOT, "package.json")).version;
}

module.exports = { getVersion };
module.exports = require("../../dist/lib/version");
43 changes: 43 additions & 0 deletions src/lib/chat-filter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, it, expect } from "vitest";
import { parseAllowedChatIds, isChatAllowed } from "../../dist/lib/chat-filter";

describe("lib/chat-filter", () => {
describe("parseAllowedChatIds", () => {
it("returns null for undefined input", () => {
expect(parseAllowedChatIds(undefined)).toBeNull();
});

it("returns null for empty string", () => {
expect(parseAllowedChatIds("")).toBeNull();
});

it("returns null for whitespace-only string", () => {
expect(parseAllowedChatIds(" , , ")).toBeNull();
});

it("parses single chat ID", () => {
expect(parseAllowedChatIds("12345")).toEqual(["12345"]);
});

it("parses comma-separated chat IDs with whitespace", () => {
expect(parseAllowedChatIds("111, 222 ,333")).toEqual(["111", "222", "333"]);
});
});

describe("isChatAllowed", () => {
it("allows all chats when allowed list is null", () => {
expect(isChatAllowed(null, "999")).toBe(true);
});

it("allows chat in the allowed list", () => {
expect(isChatAllowed(["111", "222"], "111")).toBe(true);
});

it("rejects chat not in the allowed list", () => {
expect(isChatAllowed(["111", "222"], "999")).toBe(false);
});
});
});
24 changes: 24 additions & 0 deletions src/lib/chat-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/**
* Parse a comma-separated list of allowed chat IDs.
* Returns null if the input is empty or undefined (meaning: accept all).
*/
export function parseAllowedChatIds(raw: string | undefined): string[] | null {
if (!raw) return null;
const ids = raw
.split(",")
.map((s) => s.trim())
.filter(Boolean);
return ids.length > 0 ? ids : null;
}

/**
* Check whether a chat ID is allowed by the parsed allowlist.
*
* When `allowedChats` is null every chat is accepted (open mode).
*/
export function isChatAllowed(allowedChats: string[] | null, chatId: string): boolean {
return !allowedChats || allowedChats.includes(chatId);
}
85 changes: 85 additions & 0 deletions src/lib/resolve-openshell.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, it, expect } from "vitest";
import { resolveOpenshell } from "../../dist/lib/resolve-openshell";

describe("lib/resolve-openshell", () => {
it("returns command -v result when absolute path", () => {
expect(resolveOpenshell({ commandVResult: "/usr/bin/openshell" })).toBe("/usr/bin/openshell");
});

it("rejects non-absolute command -v result (alias)", () => {
expect(
resolveOpenshell({ commandVResult: "openshell", checkExecutable: () => false }),
).toBeNull();
});

it("rejects alias definition from command -v", () => {
expect(
resolveOpenshell({
commandVResult: "alias openshell='echo pwned'",
checkExecutable: () => false,
}),
).toBeNull();
});

it("falls back to ~/.local/bin when command -v fails", () => {
expect(
resolveOpenshell({
commandVResult: null,
checkExecutable: (p) => p === "/fakehome/.local/bin/openshell",
home: "/fakehome",
}),
).toBe("/fakehome/.local/bin/openshell");
});

it("falls back to /usr/local/bin", () => {
expect(
resolveOpenshell({
commandVResult: null,
checkExecutable: (p) => p === "/usr/local/bin/openshell",
}),
).toBe("/usr/local/bin/openshell");
});

it("falls back to /usr/bin", () => {
expect(
resolveOpenshell({
commandVResult: null,
checkExecutable: (p) => p === "/usr/bin/openshell",
}),
).toBe("/usr/bin/openshell");
});

it("prefers ~/.local/bin over /usr/local/bin", () => {
expect(
resolveOpenshell({
commandVResult: null,
checkExecutable: (p) =>
p === "/fakehome/.local/bin/openshell" || p === "/usr/local/bin/openshell",
home: "/fakehome",
}),
).toBe("/fakehome/.local/bin/openshell");
});

it("returns null when openshell not found anywhere", () => {
expect(
resolveOpenshell({
commandVResult: null,
checkExecutable: () => false,
}),
).toBeNull();
});

it("skips home candidate when home is not absolute", () => {
expect(
resolveOpenshell({
commandVResult: null,
checkExecutable: () => false,
home: "relative/path",
}),
).toBeNull();
});

});
59 changes: 59 additions & 0 deletions src/lib/resolve-openshell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { execSync } from "node:child_process";
import { accessSync, constants } from "node:fs";

export interface ResolveOpenshellOptions {
/** Mock result for `command -v` (undefined = run real command). */
commandVResult?: string | null;
/** Override executable check (default: fs.accessSync X_OK). */
checkExecutable?: (path: string) => boolean;
/** HOME directory override. */
home?: string;
}

/**
* Resolve the openshell binary path.
*
* Checks `command -v` first (must return an absolute path to prevent alias
* injection), then falls back to common installation directories.
*/
export function resolveOpenshell(opts: ResolveOpenshellOptions = {}): string | null {
const home = opts.home ?? process.env.HOME;

// Step 1: command -v
if (opts.commandVResult === undefined) {
try {
const found = execSync("command -v openshell", { encoding: "utf-8" }).trim();
if (found.startsWith("/")) return found;
} catch {
/* ignored */
}
} else if (opts.commandVResult?.startsWith("/")) {
return opts.commandVResult;
}

// Step 2: fallback candidates
const checkExecutable =
opts.checkExecutable ??
((p: string): boolean => {
try {
accessSync(p, constants.X_OK);
return true;
} catch {
return false;
}
});

const candidates = [
...(home?.startsWith("/") ? [`${home}/.local/bin/openshell`] : []),
"/usr/local/bin/openshell",
"/usr/bin/openshell",
];
for (const p of candidates) {
if (checkExecutable(p)) return p;
}

return null;
}
36 changes: 36 additions & 0 deletions src/lib/version.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { getVersion } from "../../dist/lib/version";

describe("lib/version", () => {
let testDir: string;

beforeAll(() => {
testDir = mkdtempSync(join(tmpdir(), "version-test-"));
writeFileSync(join(testDir, "package.json"), JSON.stringify({ version: "1.2.3" }));
});

afterAll(() => {
rmSync(testDir, { recursive: true, force: true });
});

it("falls back to package.json version when no git and no .version", () => {
expect(getVersion({ rootDir: testDir })).toBe("1.2.3");
});

it("prefers .version file over package.json", () => {
writeFileSync(join(testDir, ".version"), "0.5.0-rc1\n");
const result = getVersion({ rootDir: testDir });
expect(result).toBe("0.5.0-rc1");
rmSync(join(testDir, ".version"));
});

it("returns a string", () => {
expect(typeof getVersion({ rootDir: testDir })).toBe("string");
});
});
Loading
Loading