From 7221476df6977d1b5792c24843e6d7e88b084b3a Mon Sep 17 00:00:00 2001
From: Sergio
-
+
@@ -205,7 +205,7 @@ Requirements: - macOS 13 or newer; - Git; -- Node.js 22.12 or newer; +- Node.js 22.13 or newer; - Xcode Command Line Tools and Swift only when building the Claude native companion; - the official Codex Desktop and/or Claude Desktop application. diff --git a/docs/install-claude-desktop.md b/docs/install-claude-desktop.md index 31b44b1..a36bd86 100644 --- a/docs/install-claude-desktop.md +++ b/docs/install-claude-desktop.md @@ -12,7 +12,7 @@ The signed and Apple-notarized DMG is the normal installation path. Source build - The official signed Claude Desktop application at `/Applications/Claude.app`. - Permission to enable **Token Widget** in System Settings > Privacy & Security > Accessibility. -The DMG contains its own compatible Node.js runtime. Building from source additionally requires Git, Node.js 22.12 or newer, and Xcode Command Line Tools with Swift. +The DMG contains its own compatible Node.js runtime. Building from source additionally requires Git, Node.js 22.13 or newer with `node:sqlite`, and Xcode Command Line Tools with Swift. ## Install the release DMG diff --git a/integrations/claude-desktop/scripts/doctor.sh b/integrations/claude-desktop/scripts/doctor.sh index 25c63ce..171e308 100755 --- a/integrations/claude-desktop/scripts/doctor.sh +++ b/integrations/claude-desktop/scripts/doctor.sh @@ -82,7 +82,7 @@ else printf 'Swift compiler: %s\n' "$SWIFT_COMPILER" printf 'Code-signing tools: %s\n' "$CODE_SIGNING_TOOLS" printf 'Git: %s\n' "$GIT_AVAILABLE" - printf 'Node.js 22.12 or newer: %s' "$NODE_COMPATIBLE" + printf 'Node.js 22.13 or newer with node:sqlite: %s' "$NODE_COMPATIBLE" [ -n "$RESOLVED_NODE" ] && printf ' (%s)' "$RESOLVED_NODE" printf '\nReady for source install: %s\n' "$READY" fi diff --git a/integrations/claude-desktop/scripts/install.sh b/integrations/claude-desktop/scripts/install.sh index 0a78116..876d341 100755 --- a/integrations/claude-desktop/scripts/install.sh +++ b/integrations/claude-desktop/scripts/install.sh @@ -33,7 +33,7 @@ Install the native Token Meter companion for Claude Code inside Claude Desktop. This installer does not quit, relaunch, modify, patch, or re-sign Claude.app. Options: - --node PATH Absolute Node.js 22.12+ executable path + --node PATH Absolute Node.js 22.13+ executable path --claude-app PATH Claude.app path (default: /Applications/Claude.app) --no-load Install files and LaunchAgent without loading it --no-prompt Do not request macOS Accessibility permission diff --git a/integrations/claude-desktop/scripts/runtime-selection.sh b/integrations/claude-desktop/scripts/runtime-selection.sh index 810de8e..d40842e 100755 --- a/integrations/claude-desktop/scripts/runtime-selection.sh +++ b/integrations/claude-desktop/scripts/runtime-selection.sh @@ -1,14 +1,31 @@ #!/bin/bash +token_meter_node_version_is_compatible() { + if [ "$#" -ne 1 ]; then return 2; fi + local version="${1#v}" + local major="${version%%.*}" + local remainder="${version#*.}" + [ "$remainder" != "$version" ] || return 1 + local minor="${remainder%%.*}" + case "$major" in ''|*[!0-9]*) return 1 ;; esac + case "$minor" in ''|*[!0-9]*) return 1 ;; esac + [ "$major" -gt 22 ] || { [ "$major" -eq 22 ] && [ "$minor" -ge 13 ]; } +} + token_meter_node_is_compatible() { if [ "$#" -ne 1 ]; then return 2; fi local candidate="$1" [ -n "$candidate" ] || return 1 [ "${candidate#/}" != "$candidate" ] || return 1 [ -x "$candidate" ] || return 1 + local version + version="$("$candidate" --version 2>/dev/null)" || return 1 + token_meter_node_version_is_compatible "$version" || return 1 "$candidate" -e ' - const [major, minor] = process.versions.node.split(".").map(Number); - process.exit(major > 22 || (major === 22 && minor >= 12) ? 0 : 1); + import("node:sqlite").then( + ({ DatabaseSync }) => process.exit(typeof DatabaseSync === "function" ? 0 : 1), + () => process.exit(1), + ); ' >/dev/null 2>&1 } @@ -53,14 +70,14 @@ token_meter_resolve_node() { printf '%s\n' "$explicit" return 0 fi - printf 'Node.js 22.12 or newer is required: %s\n' "$explicit" >&2 + printf 'Node.js 22.13 or newer with node:sqlite is required: %s\n' "$explicit" >&2 return 1 fi local selected selected="$(token_meter_find_compatible_node || true)" if [ -z "$selected" ]; then - printf 'Node.js 22.12 or newer was not found. Run the Claude source-install doctor for details.\n' >&2 + printf 'Node.js 22.13 or newer with node:sqlite was not found. Run the Claude source-install doctor for details.\n' >&2 return 1 fi printf '%s\n' "$selected" diff --git a/integrations/claude-desktop/src/overlay-bridge.mjs b/integrations/claude-desktop/src/overlay-bridge.mjs index db26114..90f3653 100644 --- a/integrations/claude-desktop/src/overlay-bridge.mjs +++ b/integrations/claude-desktop/src/overlay-bridge.mjs @@ -5,12 +5,13 @@ import readline from "node:readline"; import { once } from "node:events"; import { fileURLToPath } from "node:url"; import { ClaudeSnapshotRuntime } from "./snapshot-runtime.mjs"; -import { CodexSnapshotRuntime } from "../../codex-desktop/src/snapshot-runtime.mjs"; // The Codex host path reads state_5.sqlite through node:sqlite, whose // experimental warning would otherwise repeat into the LaunchAgent log on -// every process start. Replace Node's default warning printer with one that -// drops only that warning and prints everything else unchanged. +// every process start. Install the filter before dynamically loading that +// runtime; static dependencies are evaluated before this module body. Replace +// Node's default warning printer with one that drops only that warning and +// prints everything else unchanged. process.removeAllListeners("warning"); process.on("warning", (warning) => { if (warning.name === "ExperimentalWarning" && /SQLite/i.test(warning.message)) { @@ -18,6 +19,9 @@ process.on("warning", (warning) => { } process.stderr.write(`${warning.name}: ${warning.message}\n`); }); +const { CodexSnapshotRuntime } = await import( + "../../codex-desktop/src/snapshot-runtime.mjs", +); import { runCommunitySyncWorker } from "../../../src/core/community-sync.mjs"; import { loadOrCreateIdentity, diff --git a/integrations/codex-desktop/src/cdp-client.mjs b/integrations/codex-desktop/src/cdp-client.mjs index f627d0a..600f8f0 100644 --- a/integrations/codex-desktop/src/cdp-client.mjs +++ b/integrations/codex-desktop/src/cdp-client.mjs @@ -34,7 +34,7 @@ export class CdpClient { throw new Error("Refusing a non-loopback CDP WebSocket URL"); } if (typeof WebSocket !== "function") { - throw new Error("Token Meter requires Node.js 22.12 or newer for WebSocket support"); + throw new Error("Token Meter requires Node.js 22.13 or newer for WebSocket support"); } const socket = new WebSocket(webSocketUrl); diff --git a/package-lock.json b/package-lock.json index 707b33f..f8e7777 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "pg-mem": "^3.0.5" }, "engines": { - "node": ">=22.12.0" + "node": ">=22.13.0" } }, "node_modules/call-bind": { diff --git a/package.json b/package.json index 22a9717..8858923 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "darwin" ], "engines": { - "node": ">=22.12.0" + "node": ">=22.13.0" }, "dependencies": { "pg": "^8.16.3" diff --git a/scripts/build-v2-app.sh b/scripts/build-v2-app.sh index ca2afe4..da54fa9 100755 --- a/scripts/build-v2-app.sh +++ b/scripts/build-v2-app.sh @@ -9,7 +9,7 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" VERSION="$(node -p "require('$ROOT/package.json').version")" DIST="$ROOT/dist" -NODE_VERSION="v22.22.0" +NODE_VERSION="v$(/bin/cat "$ROOT/.node-version")" NODE_TAR="$DIST/node-$NODE_VERSION-darwin-arm64.tar.gz" IDENTITY="${TOKEN_METER_CODESIGN_IDENTITY:?set TOKEN_METER_CODESIGN_IDENTITY}" diff --git a/scripts/package-macos.sh b/scripts/package-macos.sh index 933c1e1..3777c8c 100755 --- a/scripts/package-macos.sh +++ b/scripts/package-macos.sh @@ -38,10 +38,12 @@ xcode-select -p >/dev/null 2>&1 \ || fail "Xcode Command Line Tools are required." "Run: xcode-select --install then re-run this installer." if ! command -v node >/dev/null 2>&1; then - fail "Node.js 22.12+ is required." "Install with: brew install node (or https://nodejs.org) then re-run." + fail "Node.js 22.13+ is required." "Install with: brew install node (or https://nodejs.org) then re-run." fi -node -e 'const [maj, min] = process.versions.node.split(".").map(Number); process.exit(maj > 22 || (maj === 22 && min >= 12) ? 0 : 1)' \ - || fail "Node.js 22.12+ is required (found $(node --version))." "Upgrade with: brew upgrade node or via nvm." +node -e 'const [maj, min] = process.versions.node.split(".").map(Number); process.exit(maj > 22 || (maj === 22 && min >= 13) ? 0 : 1)' \ + || fail "Node.js 22.13+ is required (found $(node --version))." "Upgrade with: brew upgrade node or via nvm." +node -e 'import("node:sqlite")' >/dev/null 2>&1 \ + || fail "This Node.js build does not provide node:sqlite." "Install an official Node.js 22.13+ release, then re-run." echo "Prerequisites OK (Node $(node --version)). Building — this takes about a minute..." echo diff --git a/test/claude-runtime-selection.test.mjs b/test/claude-runtime-selection.test.mjs index ddcdd39..5e4f665 100644 --- a/test/claude-runtime-selection.test.mjs +++ b/test/claude-runtime-selection.test.mjs @@ -10,17 +10,54 @@ import test from "node:test"; const execFileAsync = promisify(execFile); const helper = "integrations/claude-desktop/scripts/runtime-selection.sh"; -async function fakeNode(file, compatible) { - await writeFile(file, `#!/bin/bash\nexit ${compatible ? 0 : 1}\n`); +async function fakeNode(file, version, sqliteAvailable = true) { + await writeFile( + file, + `#!/bin/bash +if [ "$1" = "--version" ]; then + printf '%s\\n' '${version}' + exit 0 +fi +if [ "$1" = "-e" ]; then + exit ${sqliteAvailable ? 0 : 1} +fi +exit 1 +`, + ); await chmod(file, 0o755); } +async function versionIsCompatible(version) { + try { + await execFileAsync("/bin/bash", [ + "-c", + 'source "$1"; token_meter_node_version_is_compatible "$2"', + "bash", + helper, + version, + ]); + return true; + } catch { + return false; + } +} + +test("Claude runtime policy starts at unflagged node:sqlite support", async () => { + assert.equal(await versionIsCompatible("v22.12.9"), false); + assert.equal(await versionIsCompatible("v22.13.0"), true); + assert.equal(await versionIsCompatible("v23.0.0"), true); + assert.equal(await versionIsCompatible("not-a-version"), false); +}); + test("Claude runtime selection skips an older Node and chooses a compatible candidate", async (context) => { const directory = await mkdtemp(path.join(os.tmpdir(), "token-meter-node-select-")); context.after(() => rm(directory, { recursive: true, force: true })); const oldNode = path.join(directory, "node-old"); const currentNode = path.join(directory, "node-current"); - await Promise.all([fakeNode(oldNode, false), fakeNode(currentNode, true)]); + await Promise.all([ + fakeNode(oldNode, "v22.12.9"), + fakeNode(currentNode, "v22.13.0"), + ]); const { stdout } = await execFileAsync( "/bin/bash", @@ -41,7 +78,10 @@ test("Claude runtime selection fails an explicitly selected incompatible Node", context.after(() => rm(directory, { recursive: true, force: true })); const oldNode = path.join(directory, "node-old"); const currentNode = path.join(directory, "node-current"); - await Promise.all([fakeNode(oldNode, false), fakeNode(currentNode, true)]); + await Promise.all([ + fakeNode(oldNode, "v22.12.9"), + fakeNode(currentNode, "v22.13.0"), + ]); await assert.rejects( execFileAsync( @@ -55,7 +95,25 @@ test("Claude runtime selection fails an explicitly selected incompatible Node", currentNode, ], ), - /Node\.js 22\.12 or newer is required/i, + /Node\.js 22\.13 or newer with node:sqlite is required/i, + ); +}); + +test("Claude runtime selection rejects a Node build without node:sqlite", async (context) => { + const directory = await mkdtemp(path.join(os.tmpdir(), "token-meter-node-sqlite-")); + context.after(() => rm(directory, { recursive: true, force: true })); + const nodeWithoutSqlite = path.join(directory, "node-without-sqlite"); + await fakeNode(nodeWithoutSqlite, "v22.13.0", false); + + await assert.rejects( + execFileAsync("/bin/bash", [ + "-c", + 'source "$1"; token_meter_resolve_node "$2"', + "bash", + helper, + nodeWithoutSqlite, + ]), + /node:sqlite is required/i, ); }); diff --git a/web/install.sh b/web/install.sh index bf76ee7..51586bb 100644 --- a/web/install.sh +++ b/web/install.sh @@ -15,11 +15,13 @@ sw_vers -productVersion | awk -F. '{ exit ($1 < 13) }' \ xcode-select -p >/dev/null 2>&1 \ || fail "Xcode Command Line Tools are required." "Run: xcode-select --install then re-run this installer." command -v node >/dev/null 2>&1 \ - || fail "Node.js 22.12+ is required." "Install with: brew install node (or https://nodejs.org) then re-run." + || fail "Node.js 22.13+ is required." "Install with: brew install node (or https://nodejs.org) then re-run." command -v git >/dev/null 2>&1 \ || fail "Git is required." "Install the Xcode Command Line Tools, then re-run this installer." -node -e 'const [maj, min] = process.versions.node.split(".").map(Number); process.exit(maj > 22 || (maj === 22 && min >= 12) ? 0 : 1)' \ - || fail "Node.js 22.12+ is required (found $(node --version))." "Upgrade with: brew upgrade node or via nvm." +node -e 'const [maj, min] = process.versions.node.split(".").map(Number); process.exit(maj > 22 || (maj === 22 && min >= 13) ? 0 : 1)' \ + || fail "Node.js 22.13+ is required (found $(node --version))." "Upgrade with: brew upgrade node or via nvm." +node -e 'import("node:sqlite")' >/dev/null 2>&1 \ + || fail "This Node.js build does not provide node:sqlite." "Install an official Node.js 22.13+ release, then re-run." TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT