Skip to content
Open
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
36 changes: 30 additions & 6 deletions bin/lib/resolve-openshell.js
Original file line number Diff line number Diff line change
@@ -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");

/**
Expand All @@ -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 = [
Expand Down
24 changes: 19 additions & 5 deletions scripts/install-openshell.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Expand Down