UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

486 lines 16.6 kB
/** * Language Server Auto-Discovery * Detects installed language servers on the system */ import { execFileSync, spawn } from 'child_process'; import { existsSync } from 'fs'; import { join } from 'path'; import { TIMEOUT_LSP_SPAWN_VERIFICATION_MS, TIMEOUT_LSP_VERIFICATION_MS, } from '../constants.js'; import { createChildLogger } from '../utils/logging/index.js'; const logger = createChildLogger({ module: 'lsp-discovery' }); /** * Deno project configuration file names */ const DENO_CONFIG_FILES = ['deno.json', 'deno.jsonc']; /** * Known language servers and their configurations */ const KNOWN_SERVERS = [ // TypeScript/JavaScript { name: 'typescript-language-server', command: 'typescript-language-server', args: ['--stdio'], languages: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'], checkCommand: 'typescript-language-server --version', verificationMethod: 'version', installHint: 'npm install -g typescript-language-server typescript', }, // Deno { name: 'deno', command: 'deno', args: ['lsp'], languages: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'], checkCommand: 'deno --version', verificationMethod: 'version', installHint: 'Install Deno from https://deno.com/', }, // Python - Pyright (preferred) { name: 'pyright', command: 'pyright-langserver', args: ['--stdio'], languages: ['py', 'pyi'], checkCommand: 'pyright-langserver --version', verificationMethod: 'lsp', installHint: 'npm install -g pyright', }, // Python - pylsp (alternative) { name: 'pylsp', command: 'pylsp', args: [], languages: ['py', 'pyi'], checkCommand: 'pylsp --version', verificationMethod: 'version', installHint: 'pip install python-lsp-server', }, // Rust { name: 'rust-analyzer', command: 'rust-analyzer', args: [], languages: ['rs'], checkCommand: 'rust-analyzer --version', verificationMethod: 'version', installHint: 'rustup component add rust-analyzer', }, // Go { name: 'gopls', command: 'gopls', args: ['serve'], languages: ['go'], checkCommand: 'gopls version', verificationMethod: 'version', installHint: 'go install golang.org/x/tools/gopls@latest', }, // C/C++ { name: 'clangd', command: 'clangd', args: ['--background-index'], languages: ['c', 'cpp', 'cc', 'cxx', 'h', 'hpp', 'hxx'], checkCommand: 'clangd --version', verificationMethod: 'version', installHint: 'Install via system package manager (apt, brew, etc.)', }, // JSON { name: 'vscode-json-languageserver', command: 'vscode-json-language-server', args: ['--stdio'], languages: ['json', 'jsonc'], checkCommand: 'vscode-json-language-server --version', verificationMethod: 'lsp', installHint: 'npm install -g vscode-langservers-extracted', }, // HTML { name: 'vscode-html-languageserver', command: 'vscode-html-language-server', args: ['--stdio'], languages: ['html', 'htm'], checkCommand: 'vscode-html-language-server --version', verificationMethod: 'lsp', installHint: 'npm install -g vscode-langservers-extracted', }, // CSS { name: 'vscode-css-languageserver', command: 'vscode-css-language-server', args: ['--stdio'], languages: ['css', 'scss', 'less'], checkCommand: 'vscode-css-language-server --version', verificationMethod: 'lsp', installHint: 'npm install -g vscode-langservers-extracted', }, // YAML { name: 'yaml-language-server', command: 'yaml-language-server', args: ['--stdio'], languages: ['yaml', 'yml'], checkCommand: 'yaml-language-server --version', verificationMethod: 'lsp', installHint: 'npm install -g yaml-language-server', }, // Bash/Shell { name: 'bash-language-server', command: 'bash-language-server', args: ['start'], languages: ['sh', 'bash', 'zsh'], checkCommand: 'bash-language-server --version', verificationMethod: 'version', installHint: 'npm install -g bash-language-server', }, // Lua { name: 'lua-language-server', command: 'lua-language-server', args: [], languages: ['lua'], checkCommand: 'lua-language-server --version', verificationMethod: 'version', installHint: 'Install from https://github.com/LuaLS/lua-language-server', }, //markdown { name: 'vscode-markdown-language-server', command: 'vscode-mdx-language-server', args: ['--stdio'], languages: ['md', 'markdown', 'mdx'], checkCommand: 'vscode-mdx-language-server --version', verificationMethod: 'lsp', installHint: 'npm install -g @microsoft/vscode-mdx-language-server or vscode-langservers-extracted', }, { name: 'marksman', command: 'marksman', args: ['server'], languages: ['md', 'markdown'], checkCommand: 'marksman --version', verificationMethod: 'version', installHint: 'npm install -g marksman or download from https://github.com/artempyanykh/marksman/releases', }, //graphql { name: 'graphql-lsp-server', command: 'graphql-lsp', args: ['server -s'], languages: ['graphql', 'gql'], checkCommand: 'graphql-lsp --version', verificationMethod: 'version', installHint: 'npm install -g @graphql-tools/lsp-server', }, { name: 'graphql-language-server-cli', command: 'graphql-lsp', args: ['server', '--stdio'], languages: ['graphql', 'gql'], checkCommand: 'graphql-lsp --version', verificationMethod: 'version', installHint: 'npm install -g graphql-language-service-cli', }, { name: 'docker-language', command: 'docker-langserver', args: ['--stdio'], languages: ['dockerfile'], checkCommand: 'docker-langserver --version', verificationMethod: 'version', installHint: 'npm install -g docker-langserver or https://github.com/rcjsuen/dockerfile-language-server-nodejs', }, { name: 'docker-compose-language', command: 'yaml-language-server', args: ['--stdio'], languages: ['yaml', 'yml', 'docker-compose'], checkCommand: 'yaml-language-server --version', verificationMethod: 'version', installHint: 'npm install -g yaml-language-server', }, ]; /** * Check if a directory is a Deno project by looking for deno.json or deno.jsonc * @param projectRoot The directory to check (defaults to process.cwd()) * @returns true if a Deno configuration file is found, false otherwise */ export function isDenoProject(projectRoot = process.cwd()) { for (const configFile of DENO_CONFIG_FILES) { const configPath = join(projectRoot, configFile); // nosemgrep if (existsSync(configPath)) { return true; } } return false; } /** * Check if a command is available in PATH or locally in node_modules * Returns the path to use, or null if not found */ function findCommand(command) { // First check PATH try { execFileSync('which', [command], { stdio: 'ignore' }); return command; } catch (error) { // Not in PATH - expected for many servers logger.debug({ command, err: error }, 'Command not in PATH'); } // Check local node_modules/.bin // nosemgrep const localBinPath = join(process.cwd(), 'node_modules', '.bin', command); // nosemgrep if (existsSync(localBinPath)) { return localBinPath; } return null; } /** * Check if a command works by running a check command */ function verifyServer(checkCommand) { try { // Parse command and arguments from the check command string const parts = checkCommand.split(/\s+/); const command = parts[0]; const args = parts.slice(1); execFileSync(command, args, { stdio: 'ignore', timeout: TIMEOUT_LSP_VERIFICATION_MS, }); return true; } catch (error) { logger.debug({ checkCommand, err: error }, 'Server verification failed'); return false; } } /** * Verify an LSP server by attempting to start it with its required LSP arguments * and confirming that the process spawns successfully without immediate errors. */ function verifyLSPServerWithCommunication(command, args) { return new Promise(resolve => { // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process // command and args come from KNOWN_SERVERS configuration, not user input const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] }); // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process // Set a timeout to prevent the process from hanging indefinitely const timeout = setTimeout(() => { child.kill(); resolve(false); }, TIMEOUT_LSP_SPAWN_VERIFICATION_MS); // Listen for errors during startup (e.g., command not found) child.on('error', () => { clearTimeout(timeout); child.kill(); resolve(false); }); // If the process spawns successfully, we consider it valid. // We can then kill it immediately. child.on('spawn', () => { clearTimeout(timeout); child.kill(); // Clean up the successfully spawned process resolve(true); }); // Handle cases where the process exits very quickly (either success or failure) child.on('exit', _code => { clearTimeout(timeout); // A clean exit can also indicate success for some servers // However, for LSP servers waiting for input, an immediate exit is often a failure // The 'spawn' event is a more reliable indicator for our purpose }); }); } /** * Get servers ordered by project context * Prioritizes Deno over TypeScript LSP when in a Deno project * @param projectRoot The project root to check for Deno config (defaults to process.cwd()) * @returns Ordered array of server definitions with Deno first if in a Deno project */ export function getOrderedServers(projectRoot) { const root = projectRoot ?? process.cwd(); const servers = [...KNOWN_SERVERS]; if (isDenoProject(root)) { // Move Deno server to the front for Deno projects // Skip if Deno is already first (index 0) or not found (index -1) const denoIndex = servers.findIndex(s => s.name === 'deno'); if (denoIndex > 0) { const [denoServer] = servers.splice(denoIndex, 1); servers.unshift(denoServer); } } return servers; } /** * Discover all installed language servers * @param projectRoot Optional project root for context-aware server selection */ export async function discoverLanguageServers(projectRoot) { const discovered = []; const coveredLanguages = new Set(); const orderedServers = getOrderedServers(projectRoot); for (const server of orderedServers) { // Skip if we already have a server for all of this server's languages const hasNewLanguages = server.languages.some(lang => !coveredLanguages.has(lang)); if (!hasNewLanguages) continue; // Check if command exists (in PATH or local node_modules) const commandPath = findCommand(server.command); if (!commandPath) continue; // Verify server works based on verification method // Use the resolved command path for verification const verificationMethod = server.verificationMethod || 'version'; let verified = true; switch (verificationMethod) { case 'version': // Use the existing check command approach if (server.checkCommand) { const checkCmd = server.checkCommand.replace(server.command, commandPath); verified = verifyServer(checkCmd); } break; case 'lsp': // Use the new LSP verification approach verified = await verifyLSPServerWithCommunication(commandPath, server.args); break; case 'none': // Skip verification, only check if command exists break; } if (!verified) continue; // Add to discovered servers with resolved command path discovered.push({ name: server.name, command: commandPath, args: server.args, languages: server.languages, }); // Mark languages as covered for (const lang of server.languages) { coveredLanguages.add(lang); } } return discovered; } /** * Get language server config for a specific file extension */ export function getServerForLanguage(servers, extension) { const ext = extension.startsWith('.') ? extension.slice(1) : extension; return servers.find(server => server.languages.includes(ext)); } /** * Get the file extension to LSP language ID mapping */ export function getLanguageId(extension) { const ext = extension.startsWith('.') ? extension.slice(1) : extension; // Handle Docker Compose filename patterns if (ext === 'docker-compose.yml' || ext === 'docker-compose.yaml' || ext === 'compose.yml' || ext === 'compose.yaml') { return 'docker-compose'; } const languageMap = { ts: 'typescript', tsx: 'typescriptreact', js: 'javascript', jsx: 'javascriptreact', mjs: 'javascript', cjs: 'javascript', py: 'python', pyi: 'python', rs: 'rust', go: 'go', c: 'c', cpp: 'cpp', cc: 'cpp', cxx: 'cpp', h: 'c', hpp: 'cpp', hxx: 'cpp', json: 'json', jsonc: 'jsonc', html: 'html', htm: 'html', css: 'css', scss: 'scss', less: 'less', yaml: 'yaml', yml: 'yaml', sh: 'shellscript', bash: 'shellscript', zsh: 'shellscript', lua: 'lua', md: 'markdown', markdown: 'markdown', mdx: 'markdown', toml: 'toml', xml: 'xml', sql: 'sql', java: 'java', kt: 'kotlin', swift: 'swift', rb: 'ruby', php: 'php', graphql: 'graphql', gql: 'graphql', dockerfile: 'dockerfile', }; return languageMap[ext] || ext; } /** * Get install hints for missing language servers */ export function getMissingServerHints(extensions) { const hints = []; const checkedServers = new Set(); for (const ext of extensions) { const e = ext.startsWith('.') ? ext.slice(1) : ext; for (const server of KNOWN_SERVERS) { if (checkedServers.has(server.name)) continue; if (!server.languages.includes(e)) continue; checkedServers.add(server.name); if (!findCommand(server.command) && server.installHint) { hints.push(`${server.name}: ${server.installHint}`); } } } return hints; } /** * Try to find language server from node_modules (project-local) */ export function findLocalServer(projectRoot, serverName) { // nosemgrep const localPaths = [ join(projectRoot, 'node_modules', '.bin', serverName), // nosemgrep join(projectRoot, 'node_modules', serverName, 'bin', serverName), // nosemgrep ]; for (const path of localPaths) { if (existsSync(path)) { return path; } } return null; } /** * Get all known language servers with their availability status */ export function getKnownServersStatus() { return KNOWN_SERVERS.map(server => ({ name: server.name, available: findCommand(server.command) !== null, languages: server.languages, installHint: server.installHint, })); } //# sourceMappingURL=server-discovery.js.map