diff --git a/Dockerfile b/Dockerfile index 8ef5906..e2c4886 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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/* diff --git a/Dockerfile.canvas b/Dockerfile.canvas index 47cc40b..bb752bf 100644 --- a/Dockerfile.canvas +++ b/Dockerfile.canvas @@ -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/* diff --git a/README.md b/README.md index 2172865..b21f6a0 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/package.json b/package.json index 202f74e..49f6fae 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.ts b/src/index.ts index 2fa81e2..791ab53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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); diff --git a/src/server.ts b/src/server.ts index 9b6c596..a05baf5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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 { } // 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); diff --git a/src/setup.ts b/src/setup.ts index 3216297..4457eec 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -107,10 +107,10 @@ async function phaseEnvironment(rl: readline.Interface): Promise { // 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 { 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 { } 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'); - existing = JSON.parse(raw); + 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 { + 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,