From 26387fabb433e52e489e6d332a8daa22a0cdb89b Mon Sep 17 00:00:00 2001 From: Vansh7nvc Date: Wed, 10 Jun 2026 22:30:04 +0530 Subject: [PATCH 1/2] fix: add minimum Node.js version check at CLI startup - Add runtime version check at the top of src/cli.ts that exits with a clear error message if Node.js major version is below 18 - Add "engines": { "node": ">=18" } to package.json for install-time warnings via npm/yarn - Add cli-version-check.test.ts with 8 test cases covering the version-check decision logic Closes #63 --- package.json | 3 ++ src/__tests__/cli-version-check.test.ts | 62 +++++++++++++++++++++++++ src/cli.ts | 9 ++++ 3 files changed, 74 insertions(+) create mode 100644 src/__tests__/cli-version-check.test.ts diff --git a/package.json b/package.json index 72c5b2e..8fbf7cf 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,9 @@ "name": "copilot-lens", "version": "1.2.0", "description": "A local dashboard to visualize and analyze your GitHub Copilot CLI sessions", + "engines": { + "node": ">=18" + }, "main": "dist/server.js", "bin": { "copilot-lens": "dist/cli.js" diff --git a/src/__tests__/cli-version-check.test.ts b/src/__tests__/cli-version-check.test.ts new file mode 100644 index 0000000..5064732 --- /dev/null +++ b/src/__tests__/cli-version-check.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from "vitest"; + +/** + * Extracts the version-check logic from cli.ts into a testable pure function. + * The actual cli.ts uses `process.versions.node` and `process.exit` directly, + * so here we test the *decision logic* (should it reject?) in isolation. + */ +function checkNodeVersion(versionString: string): { ok: boolean; major: number } { + const [major] = versionString.split(".").map(Number); + return { ok: major >= 18, major }; +} + +describe("Node.js version check", () => { + it("rejects Node 16", () => { + const result = checkNodeVersion("16.20.2"); + expect(result.ok).toBe(false); + expect(result.major).toBe(16); + }); + + it("rejects Node 14", () => { + const result = checkNodeVersion("14.21.3"); + expect(result.ok).toBe(false); + expect(result.major).toBe(14); + }); + + it("rejects Node 12", () => { + const result = checkNodeVersion("12.22.12"); + expect(result.ok).toBe(false); + expect(result.major).toBe(12); + }); + + it("accepts Node 18", () => { + const result = checkNodeVersion("18.0.0"); + expect(result.ok).toBe(true); + expect(result.major).toBe(18); + }); + + it("accepts Node 18 (LTS point release)", () => { + const result = checkNodeVersion("18.19.1"); + expect(result.ok).toBe(true); + expect(result.major).toBe(18); + }); + + it("accepts Node 20", () => { + const result = checkNodeVersion("20.11.0"); + expect(result.ok).toBe(true); + expect(result.major).toBe(20); + }); + + it("accepts Node 22", () => { + const result = checkNodeVersion("22.4.1"); + expect(result.ok).toBe(true); + expect(result.major).toBe(22); + }); + + it("uses the same parsing logic as cli.ts", () => { + // Verify the actual current Node version passes + const result = checkNodeVersion(process.versions.node); + expect(result.ok).toBe(true); + expect(result.major).toBeGreaterThanOrEqual(18); + }); +}); diff --git a/src/cli.ts b/src/cli.ts index 1ef4334..dd8cd69 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,5 +1,14 @@ #!/usr/bin/env node +// Node.js version gate — must run before any modern syntax/APIs. +const [major] = process.versions.node.split('.').map(Number); +if (major < 18) { + console.error( + `Error: copilot-lens requires Node.js 18 or later. You are running v${process.versions.node}.` + ); + process.exit(1); +} + process.on("uncaughtException", (err) => { console.error("Uncaught error:", err.message); }); From 3e19f3a2487c6cb0cc36bf7c928e502a9c442b4b Mon Sep 17 00:00:00 2001 From: Vansh7nvc Date: Fri, 19 Jun 2026 01:16:51 +0530 Subject: [PATCH 2/2] fix: update minimum Node.js version to 20 and export version check Address reviewer feedback: 1. Bumped Node version threshold in package.json and cli.ts from >=18 to >=20, as vitest 4, vite 7, and better-sqlite3 12 require Node 20+. 2. Extracted pure `meetsNodeRequirement` function from cli.ts and exported it, allowing tests to import and exercise the actual production logic rather than duplicating it. Wrap the CLI side-effects in a guard so they don't run when imported in vitest. --- package.json | 2 +- src/__tests__/cli-version-check.test.ts | 47 ++++---------- src/cli.ts | 85 +++++++++++++------------ 3 files changed, 58 insertions(+), 76 deletions(-) diff --git a/package.json b/package.json index 8fbf7cf..5d3feb7 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.2.0", "description": "A local dashboard to visualize and analyze your GitHub Copilot CLI sessions", "engines": { - "node": ">=18" + "node": ">=20" }, "main": "dist/server.js", "bin": { diff --git a/src/__tests__/cli-version-check.test.ts b/src/__tests__/cli-version-check.test.ts index 5064732..f63966a 100644 --- a/src/__tests__/cli-version-check.test.ts +++ b/src/__tests__/cli-version-check.test.ts @@ -1,62 +1,37 @@ import { describe, it, expect } from "vitest"; - -/** - * Extracts the version-check logic from cli.ts into a testable pure function. - * The actual cli.ts uses `process.versions.node` and `process.exit` directly, - * so here we test the *decision logic* (should it reject?) in isolation. - */ -function checkNodeVersion(versionString: string): { ok: boolean; major: number } { - const [major] = versionString.split(".").map(Number); - return { ok: major >= 18, major }; -} +import { meetsNodeRequirement } from "../cli"; describe("Node.js version check", () => { it("rejects Node 16", () => { - const result = checkNodeVersion("16.20.2"); - expect(result.ok).toBe(false); - expect(result.major).toBe(16); + expect(meetsNodeRequirement("16.20.2")).toBe(false); }); it("rejects Node 14", () => { - const result = checkNodeVersion("14.21.3"); - expect(result.ok).toBe(false); - expect(result.major).toBe(14); + expect(meetsNodeRequirement("14.21.3")).toBe(false); }); it("rejects Node 12", () => { - const result = checkNodeVersion("12.22.12"); - expect(result.ok).toBe(false); - expect(result.major).toBe(12); + expect(meetsNodeRequirement("12.22.12")).toBe(false); }); - it("accepts Node 18", () => { - const result = checkNodeVersion("18.0.0"); - expect(result.ok).toBe(true); - expect(result.major).toBe(18); + it("rejects Node 18", () => { + expect(meetsNodeRequirement("18.0.0")).toBe(false); }); - it("accepts Node 18 (LTS point release)", () => { - const result = checkNodeVersion("18.19.1"); - expect(result.ok).toBe(true); - expect(result.major).toBe(18); + it("rejects Node 18 (LTS point release)", () => { + expect(meetsNodeRequirement("18.19.1")).toBe(false); }); it("accepts Node 20", () => { - const result = checkNodeVersion("20.11.0"); - expect(result.ok).toBe(true); - expect(result.major).toBe(20); + expect(meetsNodeRequirement("20.11.0")).toBe(true); }); it("accepts Node 22", () => { - const result = checkNodeVersion("22.4.1"); - expect(result.ok).toBe(true); - expect(result.major).toBe(22); + expect(meetsNodeRequirement("22.4.1")).toBe(true); }); it("uses the same parsing logic as cli.ts", () => { // Verify the actual current Node version passes - const result = checkNodeVersion(process.versions.node); - expect(result.ok).toBe(true); - expect(result.major).toBeGreaterThanOrEqual(18); + expect(meetsNodeRequirement(process.versions.node)).toBe(true); }); }); diff --git a/src/cli.ts b/src/cli.ts index dd8cd69..24772db 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,28 +1,34 @@ #!/usr/bin/env node +export function meetsNodeRequirement(versionString: string): boolean { + const [major] = versionString.split('.').map(Number); + return major >= 20; +} + // Node.js version gate — must run before any modern syntax/APIs. -const [major] = process.versions.node.split('.').map(Number); -if (major < 18) { +if (!meetsNodeRequirement(process.versions.node)) { console.error( - `Error: copilot-lens requires Node.js 18 or later. You are running v${process.versions.node}.` + `Error: copilot-lens requires Node.js 20 or later. You are running v${process.versions.node}.` ); process.exit(1); } -process.on("uncaughtException", (err) => { - console.error("Uncaught error:", err.message); -}); -process.on("unhandledRejection", (err: any) => { - console.error("Unhandled rejection:", err?.message || err); -}); +// Avoid running the CLI side-effects when imported by Vitest +if (process.env.VITEST !== "true") { + process.on("uncaughtException", (err) => { + console.error("Uncaught error:", err.message); + }); + process.on("unhandledRejection", (err: any) => { + console.error("Unhandled rejection:", err?.message || err); + }); -const args = process.argv.slice(2); + const args = process.argv.slice(2); -if (args[0] === "tokens") { - const { runTokensTUI } = require("./cli-tokens"); - runTokensTUI(args.slice(1)); -} else if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") { - process.stdout.write(` + if (args[0] === "tokens") { + const { runTokensTUI } = require("./cli-tokens"); + runTokensTUI(args.slice(1)); + } else if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") { + process.stdout.write(` Usage: copilot-lens [command] [options] Commands: @@ -36,33 +42,34 @@ if (args[0] === "tokens") { Run "copilot-lens tokens --help" for tokens command options. `); -} else { - const { createApp } = require("./server"); + } else { + const { createApp } = require("./server"); - function getArg(name: string, fallback: string): string { - const idx = args.indexOf(name); - return idx !== -1 && args[idx + 1] ? args[idx + 1] : fallback; - } + function getArg(name: string, fallback: string): string { + const idx = args.indexOf(name); + return idx !== -1 && args[idx + 1] ? args[idx + 1] : fallback; + } - const port = parseInt(getArg("--port", "3000"), 10); - const host = getArg("--host", "localhost"); - const shouldOpen = args.includes("--open"); + const port = parseInt(getArg("--port", "3000"), 10); + const host = getArg("--host", "localhost"); + const shouldOpen = args.includes("--open"); - const app = createApp(); + const app = createApp(); - app.listen(port, host, async () => { - const url = `http://${host}:${port}`; - console.log(`\n 👓 Copilot Lens is running at ${url}\n`); + app.listen(port, host, async () => { + const url = `http://${host}:${port}`; + console.log(`\n 👓 Copilot Lens is running at ${url}\n`); - if (shouldOpen) { - const { exec } = await import("child_process"); - const cmd = - process.platform === "win32" - ? `start "" "${url}"` - : process.platform === "darwin" - ? `open ${url}` - : `xdg-open ${url}`; - exec(cmd); - } - }); + if (shouldOpen) { + const { exec } = await import("child_process"); + const cmd = + process.platform === "win32" + ? `start "" "${url}"` + : process.platform === "darwin" + ? `open ${url}` + : `xdg-open ${url}`; + exec(cmd); + } + }); + } }