🐛 fix(cli): robust npx entry point, Node 20 requirement, setup hardening (#8)

- Use fs.realpathSync for entry point detection to fix npx symlink failures
- Add --help and --version CLI flags with explicit process.exit(0)
- Add TTY detection in setup wizard to prevent non-interactive hangs
- Wrap JSON.parse in mergeJsonConfig with try/catch for malformed configs
- Update engines.node to >=20.0.0 to match better-sqlite3 requirements
- Update Dockerfiles from node:18-slim to node:20-slim
- Remove error-swallowing || true from postinstall script
- Replace deprecated windows-build-tools with VS Build Tools link
This commit is contained in:
Sanjib Devnath
2026-03-13 18:21:19 +05:30
committed by GitHub
parent 6a69ac6761
commit 97d816df4a
7 changed files with 65 additions and 20 deletions
+2 -2
View File
@@ -2,7 +2,7 @@
# Builds the MCP server with SQLite persistence
# Stage 1: Build backend (TypeScript compilation + native modules)
FROM node:18-slim AS builder
FROM node:20-slim AS builder
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
@@ -16,7 +16,7 @@ COPY tsconfig.json ./
RUN npm run build:server
# Stage 2: Production MCP Server
FROM node:18-slim AS production
FROM node:20-slim AS production
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
+3 -3
View File
@@ -2,7 +2,7 @@
# Provides the web interface, REST API, and SQLite persistence
# Stage 1: Build frontend
FROM node:18-slim AS frontend-builder
FROM node:20-slim AS frontend-builder
WORKDIR /app
@@ -14,7 +14,7 @@ COPY vite.config.js ./
RUN npm run build:frontend
# Stage 2: Build backend (TypeScript compilation + native modules)
FROM node:18-slim AS backend-builder
FROM node:20-slim AS backend-builder
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
@@ -28,7 +28,7 @@ COPY tsconfig.json ./
RUN npm run build:server
# Stage 3: Production Canvas Server
FROM node:18-slim AS production
FROM node:20-slim AS production
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
+2 -4
View File
@@ -58,7 +58,7 @@ Click the workspace badge to switch between isolated canvases — each workspace
| Requirement | Why | Check |
|---|---|---|
| **Node.js >= 18** (LTS 20 or 22 recommended) | Runtime | `node --version` |
| **Node.js >= 20** (LTS 20 or 22 recommended) | Runtime | `node --version` |
| **C++ build tools** | `better-sqlite3` compiles native bindings | See below |
| **npm** (bundled with Node.js) | Package manager | `npm --version` |
@@ -75,9 +75,7 @@ sudo apt install build-essential python3
```
**Windows:**
```bash
npm install --global windows-build-tools
```
Install "Desktop development with C++" from [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/).
> `better-sqlite3` ships prebuilt binaries for most Node LTS versions. The build tools are only needed when a prebuilt binary isn't available for your platform/Node combination.
+2 -2
View File
@@ -18,7 +18,7 @@
"dev:server": "npx tsc --watch",
"production": "npm run build && npm run canvas",
"prepublishOnly": "npm run build",
"postinstall": "prebuild-install --runtime napi || node-gyp rebuild --directory node_modules/better-sqlite3 2>/dev/null || true",
"postinstall": "prebuild-install --runtime napi || node-gyp rebuild --directory node_modules/better-sqlite3",
"setup": "node dist/index.js setup",
"type-check": "npx tsc --noEmit",
"test": "vitest run",
@@ -107,7 +107,7 @@
]
},
"engines": {
"node": ">=18.0.0"
"node": ">=20.0.0"
},
"publishConfig": {
"access": "public",
+29 -3
View File
@@ -2708,13 +2708,39 @@ if (process.env.DEBUG === 'true') {
logger.debug('Debug mode enabled');
}
// Start the server if this file is run directly
if (fileURLToPath(import.meta.url) === process.argv[1]) {
if (process.argv[2] === 'setup') {
function isMainModule(): boolean {
try {
const ourPath = fs.realpathSync(fileURLToPath(import.meta.url));
const argPath = process.argv[1];
if (!argPath) return false;
return ourPath === fs.realpathSync(path.resolve(argPath));
} catch {
return false;
}
}
if (isMainModule()) {
const arg = process.argv[2];
if (arg === 'setup') {
import('./setup.js').then(m => m.runSetup()).catch(error => {
process.stderr.write(`Setup failed: ${(error as Error).message}\n`);
process.exit(1);
});
} else if (arg === '--help' || arg === '-h' || arg === '--version' || arg === '-v') {
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
if (arg === '--help' || arg === '-h') {
process.stdout.write(`${pkg.name} v${pkg.version}\n\nUsage:\n mcp-excalidraw-local Start MCP server (stdio transport)\n mcp-excalidraw-local setup Interactive setup wizard\n mcp-excalidraw-local --help Show this help\n mcp-excalidraw-local --version Show version\n`);
} else {
process.stdout.write(`${pkg.version}\n`);
}
} catch {
process.stderr.write('Could not read package.json\n');
process.exit(1);
}
process.exit(0);
} else {
runServer().catch(error => {
logger.error('Failed to start server:', error);
+13 -1
View File
@@ -2,6 +2,7 @@ import express, { type Application, Request, Response, NextFunction } from 'expr
import cors from 'cors';
import { WebSocketServer } from 'ws';
import { createServer } from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
@@ -1272,7 +1273,18 @@ export function stopCanvasServer(): Promise<void> {
}
// Direct execution: `node dist/server.js` still works standalone
if (fileURLToPath(import.meta.url) === process.argv[1]) {
function isServerMainModule(): boolean {
try {
const ourPath = fs.realpathSync(fileURLToPath(import.meta.url));
const argPath = process.argv[1];
if (!argPath) return false;
return ourPath === fs.realpathSync(path.resolve(argPath));
} catch {
return false;
}
}
if (isServerMainModule()) {
startCanvasServer().catch((err) => {
logger.error('Failed to start canvas server:', err);
process.exit(1);
+13 -4
View File
@@ -107,10 +107,10 @@ async function phaseEnvironment(rl: readline.Interface): Promise<boolean> {
// Node.js version
const nodeVersion = process.version;
const major = parseInt(nodeVersion.slice(1).split('.')[0] ?? '0', 10);
if (major >= 18) {
if (major >= 20) {
ok(`Node.js ${nodeVersion} ${'.' .repeat(Math.max(0, 24 - nodeVersion.length))} OK`);
} else {
fail(`Node.js ${nodeVersion} — requires >= 18.0.0`);
fail(`Node.js ${nodeVersion} — requires >= 20.0.0`);
allOk = false;
}
@@ -133,7 +133,6 @@ async function phaseEnvironment(rl: readline.Interface): Promise<boolean> {
ok('Rebuild successful');
} catch {
fail('Rebuild failed. Try manually:');
info(` cd ${path.resolve(__dirname, '..')}`);
info(' npm rebuild better-sqlite3');
info('');
info('Prerequisites:');
@@ -142,7 +141,8 @@ async function phaseEnvironment(rl: readline.Interface): Promise<boolean> {
} else if (process.platform === 'linux') {
info(' sudo apt install build-essential python3');
} else {
info(' npm install --global windows-build-tools');
info(' Install "Desktop development with C++" from Visual Studio Build Tools');
info(' https://visualstudio.microsoft.com/visual-cpp-build-tools/');
}
allOk = false;
}
@@ -306,7 +306,11 @@ function mergeJsonConfig(configPath: string): void {
let existing: any = {};
if (fs.existsSync(configPath)) {
const raw = fs.readFileSync(configPath, 'utf-8');
try {
existing = JSON.parse(raw);
} catch {
throw new Error(`Failed to parse ${configPath} — fix the JSON syntax and try again.`);
}
}
if (!existing.mcpServers) {
@@ -345,6 +349,11 @@ function printManualConfig(): void {
// ── Main ─────────────────────────────────────────────────────
export async function runSetup(): Promise<void> {
if (!process.stdin.isTTY) {
process.stderr.write('Error: Setup requires an interactive terminal. Run this command directly in your terminal (not piped or in CI).\n');
process.exit(1);
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,