Skip to main content
Glama
cli.js7.08 kB
#!/usr/bin/env node const https = require("https"); const fs = require("fs"); const path = require("path"); const { execSync, spawn } = require("child_process"); const PACKAGE_VERSION = require("./package.json").version; const REPO = "aybelatchane/mcp-server-terminal"; const BINARY_NAME = "terminal-mcp"; // Cache directory for the binary (in user's home to persist across npx runs) const CACHE_DIR = path.join( process.env.HOME || process.env.USERPROFILE || __dirname, ".cache", "mcp-server-terminal" ); const BINARY_PATH = path.join(CACHE_DIR, BINARY_NAME); const VERSION_FILE = path.join(CACHE_DIR, "version.txt"); // Map Node.js platform/arch to Rust target triples const PLATFORM_MAPPING = { darwin: { x64: "x86_64-apple-darwin", arm64: "aarch64-apple-darwin", }, linux: { x64: "x86_64-unknown-linux-gnu", arm64: "aarch64-unknown-linux-gnu", }, }; function getTarget() { const platform = process.platform; const arch = process.arch; if (!PLATFORM_MAPPING[platform]) { console.error(`Unsupported platform: ${platform}`); console.error("Supported platforms: macOS (darwin), Linux"); process.exit(1); } const target = PLATFORM_MAPPING[platform][arch]; if (!target) { console.error(`Unsupported architecture: ${arch} on ${platform}`); console.error("Supported architectures: x64, arm64"); process.exit(1); } return target; } function getDownloadUrl(version, target) { return `https://github.com/${REPO}/releases/download/v${version}/${BINARY_NAME}-${target}.tar.gz`; } // Fetch latest version from GitHub releases function fetchLatestVersion() { return new Promise((resolve, reject) => { const options = { hostname: "api.github.com", path: `/repos/${REPO}/releases/latest`, headers: { "User-Agent": "mcp-server-terminal-cli", Accept: "application/vnd.github.v3+json", }, }; https .get(options, (response) => { if (response.statusCode === 404) { // No releases yet, use package version resolve(PACKAGE_VERSION); return; } if (response.statusCode !== 200) { reject(new Error(`GitHub API error: ${response.statusCode}`)); return; } let data = ""; response.on("data", (chunk) => (data += chunk)); response.on("end", () => { try { const release = JSON.parse(data); // tag_name is like "v1.0.1", strip the "v" const version = release.tag_name.replace(/^v/, ""); resolve(version); } catch (e) { reject(e); } }); }) .on("error", reject); }); } // Get cached version function getCachedVersion() { try { if (fs.existsSync(VERSION_FILE)) { return fs.readFileSync(VERSION_FILE, "utf8").trim(); } } catch { // Ignore errors } return null; } // Save version to cache function saveCachedVersion(version) { try { fs.writeFileSync(VERSION_FILE, version); } catch { // Ignore errors } } function downloadFile(url) { return new Promise((resolve, reject) => { const request = (url) => { https .get(url, (response) => { if (response.statusCode === 302 || response.statusCode === 301) { request(response.headers.location); return; } if (response.statusCode !== 200) { reject( new Error(`Failed to download: HTTP ${response.statusCode}`) ); return; } const chunks = []; response.on("data", (chunk) => chunks.push(chunk)); response.on("end", () => resolve(Buffer.concat(chunks))); response.on("error", reject); }) .on("error", reject); }; request(url); }); } function extractTarGz(buffer, destDir) { const tarballPath = path.join(destDir, "temp.tar.gz"); fs.writeFileSync(tarballPath, buffer); try { execSync(`tar -xzf "${tarballPath}" -C "${destDir}"`, { stdio: "pipe" }); } finally { fs.unlinkSync(tarballPath); } } // Compare versions (simple semver comparison) function isNewerVersion(latest, current) { const latestParts = latest.split(".").map(Number); const currentParts = current.split(".").map(Number); for (let i = 0; i < 3; i++) { const l = latestParts[i] || 0; const c = currentParts[i] || 0; if (l > c) return true; if (l < c) return false; } return false; } async function ensureBinary() { // Create cache directory if needed if (!fs.existsSync(CACHE_DIR)) { fs.mkdirSync(CACHE_DIR, { recursive: true }); } const cachedVersion = getCachedVersion(); let targetVersion = PACKAGE_VERSION; let needsDownload = !fs.existsSync(BINARY_PATH); // Check for updates (unless MCP_NO_UPDATE is set) if (!process.env.MCP_NO_UPDATE) { try { const latestVersion = await fetchLatestVersion(); if (cachedVersion && isNewerVersion(latestVersion, cachedVersion)) { console.error( `Update available: v${cachedVersion} -> v${latestVersion}` ); targetVersion = latestVersion; needsDownload = true; } else if (!cachedVersion) { // No cached version, use latest targetVersion = latestVersion; } } catch (error) { // Failed to check for updates, continue with cached or package version if (process.env.DEBUG) { console.error(`Update check failed: ${error.message}`); } } } // If binary exists and version matches, verify it's executable if (!needsDownload && fs.existsSync(BINARY_PATH)) { try { fs.accessSync(BINARY_PATH, fs.constants.X_OK); return BINARY_PATH; } catch { needsDownload = true; } } if (!needsDownload) { return BINARY_PATH; } const target = getTarget(); const url = getDownloadUrl(targetVersion, target); console.error( `Downloading mcp-server-terminal v${targetVersion} for ${target}...` ); try { const buffer = await downloadFile(url); extractTarGz(buffer, CACHE_DIR); fs.chmodSync(BINARY_PATH, 0o755); saveCachedVersion(targetVersion); console.error(`Successfully installed mcp-server-terminal v${targetVersion}`); return BINARY_PATH; } catch (error) { console.error(`Failed to download binary: ${error.message}`); console.error(`\nPlease download manually from:`); console.error(`https://github.com/${REPO}/releases/tag/v${targetVersion}`); process.exit(1); } } async function main() { const binaryPath = await ensureBinary(); // Pass all arguments to the binary const args = process.argv.slice(2); const child = spawn(binaryPath, args, { stdio: "inherit", env: process.env, }); child.on("error", (error) => { console.error(`Failed to start: ${error.message}`); process.exit(1); }); child.on("exit", (code, signal) => { if (signal) { process.kill(process.pid, signal); } else { process.exit(code ?? 0); } }); } main();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aybelatchane/mcp-server-terminal'

If you have feedback or need assistance with the MCP directory API, please join our Discord server