diff --git a/bin/lib/resolve-openshell.js b/bin/lib/resolve-openshell.js index 345e218e4..33c216e7b 100644 --- a/bin/lib/resolve-openshell.js +++ b/bin/lib/resolve-openshell.js @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -const { execSync } = require("child_process"); +const { execSync, execFileSync } = require("child_process"); const fs = require("fs"); /** @@ -16,22 +16,46 @@ const fs = require("fs"); * @param {string} [opts.home] HOME override * @returns {string|null} Absolute path to openshell, or null if not found */ +/** + * Verify if the binary is the OpenShell Rust CLI (and not a shadowing NPM package). + * @param {string} path Absolute path to binary + * @returns {boolean} + */ +function isRustCli(path) { + try { + const output = execFileSync(path, ["-V"], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + timeout: 2000, + }).trim(); + // Rust version string: "openshell 0.1.0" + return /^openshell\s+[0-9]+\.[0-9]+\.[0-9]+/i.test(output); + } catch { + return false; + } +} + function resolveOpenshell(opts = {}) { const home = opts.home ?? process.env.HOME; - // Step 1: command -v + // Step 1: command -v (check if it is the Rust CLI) if (opts.commandVResult === undefined) { try { const found = execSync("command -v openshell", { encoding: "utf-8" }).trim(); - if (found.startsWith("/")) return found; + if (found.startsWith("/") && isRustCli(found)) return found; } catch { /* ignored */ } - } else if (opts.commandVResult && opts.commandVResult.startsWith("/")) { + } else if (opts.commandVResult && opts.commandVResult.startsWith("/") && isRustCli(opts.commandVResult)) { return opts.commandVResult; } - // Step 2: fallback candidates + // Step 2: fallback candidates (verify they are the Rust CLI) const checkExecutable = opts.checkExecutable || ((p) => { - try { fs.accessSync(p, fs.constants.X_OK); return true; } catch { return false; } + try { + fs.accessSync(p, fs.constants.X_OK); + return isRustCli(p); + } catch { + return false; + } }); const candidates = [ diff --git a/scripts/install-openshell.sh b/scripts/install-openshell.sh index 2cd6934cc..b7a472fbe 100755 --- a/scripts/install-openshell.sh +++ b/scripts/install-openshell.sh @@ -50,13 +50,27 @@ version_gte() { return 0 } +# Use -V (capital V) as it is specific to the Rust CLI version strings if command -v openshell >/dev/null 2>&1; then - INSTALLED_VERSION="$(openshell --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo '0.0.0')" - if version_gte "$INSTALLED_VERSION" "$MIN_VERSION"; then - info "openshell already installed: $INSTALLED_VERSION (>= $MIN_VERSION)" - exit 0 + # Verify if it's the Rust CLI, not a shadowing NPM package + # Use timeout to prevent hanging on faulty existing binaries + TIMEOUT_PREFIX="" + if command -v timeout >/dev/null 2>&1; then + TIMEOUT_PREFIX="timeout -s KILL 2s" + elif command -v gtimeout >/dev/null 2>&1; then + TIMEOUT_PREFIX="gtimeout -s KILL 2s" + fi + VERSION_OUT="$($TIMEOUT_PREFIX openshell -V 2>&1 || echo "")" + if echo "$VERSION_OUT" | grep -qi "^openshell [0-9]"; then + INSTALLED_VERSION="$(echo "$VERSION_OUT" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo '0.0.0')" + if version_gte "$INSTALLED_VERSION" "$MIN_VERSION"; then + info "openshell already installed: $INSTALLED_VERSION (>= $MIN_VERSION)" + exit 0 + fi + warn "openshell $INSTALLED_VERSION is below minimum $MIN_VERSION — upgrading..." + else + warn "The 'openshell' command on PATH is not the OpenShell CLI binary (possibly shadowed by NPM). Reinstalling CLI..." fi - warn "openshell $INSTALLED_VERSION is below minimum $MIN_VERSION — upgrading..." fi info "Installing openshell CLI..."