Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
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);
}
94 changes: 94 additions & 0 deletions src/lib/resolve-openshell.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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();
});

it("returns null for null commandVResult with no executable found", () => {
expect(
resolveOpenshell({
commandVResult: null,
checkExecutable: () => false,
home: undefined,
}),
).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;
}
Loading
Loading