diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e37e760..d442e8c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: jobs: quality: - uses: robert7/workflows/.github/workflows/node-ci.yml@v0.3.0 + uses: robert7/workflows/.github/workflows/node-ci.yml@9b0544fccadffeadad23ac555f5861f41a98e61f + with: + node_versions: '["22.13.0", "24"]' + coverage_node_version: '24' secrets: codecov_token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.nvmrc b/.nvmrc index 5bd6811..a45fd52 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.19.0 +24 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b8ac6b..aea37fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ Versioning](https://semver.org/spec/v2.0.0.html). ### Changed +- Require Node.js `22.13.0` or newer, use Node.js 24 LTS for local development, and validate both Node.js 22 and 24 in + CI now that Node.js 20 is end-of-life. - Ignore Python bytecode caches and remove previously tracked cache artifacts. - Link the canonical cross-repo pull request validation and adoption workflow for maintainers and coding agents. - Update `remnote_search` tools reference, CLI command `search` reference, and agent skill documentation to include advanced scoped search with parent ID and document cursor limitations. @@ -30,6 +32,8 @@ Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed +- Make Node.js environment checks reject malformed versions cleanly and report a missing `.nvmrc` without leaking + shell errors. - Reject empty `parentRemId` values for `remnote_search` instead of treating them as unscoped search. ## [0.16.0] - 2026-06-05 diff --git a/docs/guides/development-setup.md b/docs/guides/development-setup.md index b3a8714..e0614ff 100644 --- a/docs/guides/development-setup.md +++ b/docs/guides/development-setup.md @@ -4,7 +4,7 @@ Instructions for contributors who want to modify, test, or develop the RemNote M ## Prerequisites -- **Node.js** >= 20.19.0 for local development and code-quality commands (preferably via nvm) +- **Node.js** >= 22.13.0 for local development and code-quality commands; `.nvmrc` selects Node.js 24 LTS - **git** - Version control - **RemNote app** with RemNote Automation Bridge plugin (for testing) - **Claude Code CLI** or another MCP client (for integration testing) @@ -80,7 +80,8 @@ source ./node-check.sh && npm run dev ``` This script ensures a Node.js version compatible with the local development toolchain is available via nvm, and will -accept newer installed Node versions when they still satisfy the repo floor. +accept newer installed Node versions when they still satisfy the repo floor. CI verifies both the minimum supported +Node.js 22 line and the recommended Node.js 24 LTS line. ### Type Checking diff --git a/docs/guides/installation.md b/docs/guides/installation.md index 6266a71..7d1ae2c 100644 --- a/docs/guides/installation.md +++ b/docs/guides/installation.md @@ -4,7 +4,7 @@ Complete installation instructions for the RemNote MCP Server. ## Prerequisites -- **Node.js** >= 20.19.0 +- **Node.js** >= 22.13.0 (Node.js 24 LTS recommended) - **RemNote app** (desktop or web browser) - **RemNote Automation Bridge plugin** - Install from [GitHub](https://github.com/robert7/remnote-mcp-bridge) - registration in the RemNote marketplace is pending approval @@ -251,11 +251,11 @@ remnote-cli search "MCP Test" --text **Solution:** ```bash node --version -# Ensure >= 20.19.0 +# Ensure >= 22.13.0 # Update Node.js using nvm: -nvm install 18 -nvm use 18 +nvm install 24 +nvm use 24 ``` ### Permission Errors (npm install -g) diff --git a/docs/guides/troubleshooting.md b/docs/guides/troubleshooting.md index a1e1a95..5789871 100644 --- a/docs/guides/troubleshooting.md +++ b/docs/guides/troubleshooting.md @@ -59,11 +59,11 @@ remnote-mcp-server --version ```bash # Check version node --version - # Development tooling requires >= 20.19.0 + # Development tooling requires >= 22.13.0 # Update Node.js - nvm install 20.19.0 - nvm use 20.19.0 + nvm install 24 + nvm use 24 ``` 3. **Missing dependencies** diff --git a/node-check.sh b/node-check.sh index 529748e..b4a5478 100755 --- a/node-check.sh +++ b/node-check.sh @@ -13,37 +13,85 @@ # Usage: # source "$(dirname "$0")/node-check.sh" || exit 1 -# Check if Node.js is already available in PATH -if command -v node &> /dev/null && command -v npm &> /dev/null; then - # Node and npm are already available, exit silently - return 0 2>/dev/null || exit 0 -fi +MIN_NODE_VERSION="22.13.0" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +version_is_supported() { + local current_version="${1//$'\r'/}" + local current_major current_minor current_patch + local minimum_major minimum_minor minimum_patch + + if [[ ! "$current_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + return 1 + fi + + IFS='.' read -r current_major current_minor current_patch <<< "$current_version" + IFS='.' read -r minimum_major minimum_minor minimum_patch <<< "$MIN_NODE_VERSION" + + ((current_major > minimum_major)) || + ((current_major == minimum_major && current_minor > minimum_minor)) || + ((current_major == minimum_major && current_minor == minimum_minor && current_patch >= minimum_patch)) +} + +current_node_version() { + node -p 'process.versions.node' +} + +project_node_version() { + local version_file="$SCRIPT_DIR/.nvmrc" + local version + + if [ ! -r "$version_file" ]; then + return 1 + fi + + IFS= read -r version < "$version_file" + if [ -z "$version" ]; then + return 1 + fi -# Node not in PATH, attempt to load via NVM -if [ -s "$HOME/.nvm/nvm.sh" ]; then - # Source NVM + printf '%s' "$version" +} + +activate_project_node() { + local project_version + + if [ ! -s "$HOME/.nvm/nvm.sh" ]; then + return 1 + fi + + project_version="$(project_node_version)" || return 1 source "$HOME/.nvm/nvm.sh" &> /dev/null + nvm use "$project_version" &> /dev/null +} - # Load default Node version - if ! nvm use default &> /dev/null; then - echo "Error: NVM found but unable to load default Node version." >&2 - echo "Please configure NVM with: nvm install --lts && nvm alias default lts/*" >&2 - return 1 2>/dev/null || exit 1 +commands_available() { + command -v node &> /dev/null && command -v npm &> /dev/null +} + +print_install_guidance() { + local project_version + + if project_version="$(project_node_version)"; then + echo "Install the project version with: nvm install $project_version" >&2 + else + echo "Project Node version file is missing or unreadable: $SCRIPT_DIR/.nvmrc" >&2 fi -else - # NVM not found - echo "Error: Node.js not found and NVM is not installed." >&2 - echo "Please install Node.js via:" >&2 - echo " - NVM: https://github.com/nvm-sh/nvm" >&2 - echo " - Official installer: https://nodejs.org/" >&2 - return 1 2>/dev/null || exit 1 +} + +if commands_available && version_is_supported "$(current_node_version)"; then + return 0 2>/dev/null || exit 0 +fi + +if activate_project_node && commands_available && version_is_supported "$(current_node_version)"; then + return 0 2>/dev/null || exit 0 fi -# Final verification that both node and npm are now available -if ! command -v node &> /dev/null || ! command -v npm &> /dev/null; then - echo "Error: Node.js setup failed. node or npm command not found." >&2 - return 1 2>/dev/null || exit 1 +if commands_available; then + echo "Error: Node.js >= $MIN_NODE_VERSION is required; found $(current_node_version)." >&2 +else + echo "Error: Node.js >= $MIN_NODE_VERSION and npm are required." >&2 fi -# Success - Node.js and npm are available -return 0 2>/dev/null || exit 0 +print_install_guidance +return 1 2>/dev/null || exit 1 diff --git a/package-lock.json b/package-lock.json index 164ccc0..3c7ac4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "vitest": "^4.1.3" }, "engines": { - "node": ">=20.19.0" + "node": ">=22.13.0" } }, "node_modules/@babel/helper-string-parser": { diff --git a/package.json b/package.json index 110f297..01ef6c1 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,6 @@ "vitest": "^4.1.3" }, "engines": { - "node": ">=20.19.0" + "node": ">=22.13.0" } } diff --git a/test/unit/node-check-script.test.ts b/test/unit/node-check-script.test.ts new file mode 100644 index 0000000..8e20552 --- /dev/null +++ b/test/unit/node-check-script.test.ts @@ -0,0 +1,107 @@ +import { + chmodSync, + copyFileSync, + existsSync, + mkdtempSync, + mkdirSync, + rmSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { describe, expect, it } from 'vitest'; + +function runNodeCheck(version: string, includeNvmrc = true, shell = 'bash') { + const tempRoot = mkdtempSync(join(tmpdir(), 'remnote-node-check-')); + const binDir = join(tempRoot, 'bin'); + mkdirSync(binDir); + + copyFileSync(resolve('node-check.sh'), join(tempRoot, 'node-check.sh')); + if (includeNvmrc) { + copyFileSync(resolve('.nvmrc'), join(tempRoot, '.nvmrc')); + } + + writeFileSync( + join(binDir, 'node'), + `#!/usr/bin/env bash +if [[ "$1" == "-p" ]]; then + echo "${version}" + exit 0 +fi +echo "v${version}" +` + ); + writeFileSync(join(binDir, 'npm'), '#!/usr/bin/env bash\nexit 0\n'); + chmodSync(join(binDir, 'node'), 0o755); + chmodSync(join(binDir, 'npm'), 0o755); + + try { + const result = spawnSync(shell, ['-c', 'source ./node-check.sh && echo ready'], { + cwd: tempRoot, + encoding: 'utf-8', + env: { + PATH: `${binDir}:/bin:/usr/bin`, + HOME: tempRoot, + }, + }); + + return { result, tempRoot }; + } finally { + rmSync(tempRoot, { recursive: true, force: true }); + } +} + +describe('node-check.sh', () => { + const zshAvailable = spawnSync('zsh', ['--version']).status === 0; + + it.each(['22.13.0', '22.14.0', '24.0.0', '22.13.0\r'])( + 'accepts supported Node.js version %s', + (version) => { + const { result } = runNodeCheck(version); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('ready'); + } + ); + + it.each(['20.19.0', '22.12.0'])('rejects unsupported Node.js version %s', (version) => { + const { result } = runNodeCheck(version); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain(`Node.js >= 22.13.0 is required; found ${version}.`); + expect(result.stderr).toContain('nvm install 24'); + }); + + it.each(['22.13.0-rc.1', 'not-a-version'])( + 'rejects malformed Node.js version %s without a shell error', + (version) => { + const { result } = runNodeCheck(version); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain(`Node.js >= 22.13.0 is required; found ${version}.`); + expect(result.stderr).not.toContain('arithmetic syntax error'); + } + ); + + it('reports a missing project Node version file without leaking a cat error', () => { + const { result } = runNodeCheck('20.19.0', false); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain('Project Node version file is missing or unreadable:'); + expect(result.stderr).not.toContain('cat:'); + }); + + it('removes its temporary test directory', () => { + const { tempRoot } = runNodeCheck('24.0.0'); + + expect(existsSync(tempRoot)).toBe(false); + }); + + it.skipIf(!zshAvailable)('can be sourced from zsh', () => { + const { result } = runNodeCheck('24.0.0', true, 'zsh'); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('ready'); + }); +}); diff --git a/test/unit/project-config.test.ts b/test/unit/project-config.test.ts index 183aa18..149c567 100644 --- a/test/unit/project-config.test.ts +++ b/test/unit/project-config.test.ts @@ -3,6 +3,23 @@ import { readFileSync } from 'node:fs'; import vitestConfig from '../../vitest.config.js'; describe('project config', () => { + it('keeps the supported Node.js policy aligned across package, lockfile, nvm, and CI', () => { + const packageJson = JSON.parse(readFileSync('package.json', 'utf-8')) as { + engines: { node: string }; + }; + const packageLock = JSON.parse(readFileSync('package-lock.json', 'utf-8')) as { + packages: { '': { engines: { node: string } } }; + }; + const nvmVersion = readFileSync('.nvmrc', 'utf-8').trim(); + const ciWorkflow = readFileSync('.github/workflows/ci.yml', 'utf-8'); + + expect(packageJson.engines.node).toBe('>=22.13.0'); + expect(packageLock.packages[''].engines.node).toBe(packageJson.engines.node); + expect(nvmVersion).toBe('24'); + expect(ciWorkflow).toContain(`node_versions: '["22.13.0", "24"]'`); + expect(ciWorkflow).toContain("coverage_node_version: '24'"); + }); + it('prevents production dist emit when TypeScript reports errors', () => { const tsconfig = JSON.parse(readFileSync('tsconfig.json', 'utf-8')) as { compilerOptions: { noEmitOnError?: boolean };