From 9fb7c90dbd0be26f7ca2583e8594c51fe2ee535c Mon Sep 17 00:00:00 2001 From: shekohex Date: Fri, 13 Mar 2026 09:49:25 +0200 Subject: [PATCH] fix(cli): detect symlinked entrypoints (#652) --- packages/web/bin/cli-entry.js | 38 ++++++++++++++++++++++++++++ packages/web/bin/cli.js | 13 ++-------- packages/web/bin/cli.test.js | 47 +++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 packages/web/bin/cli-entry.js create mode 100644 packages/web/bin/cli.test.js diff --git a/packages/web/bin/cli-entry.js b/packages/web/bin/cli-entry.js new file mode 100644 index 00000000..49badf26 --- /dev/null +++ b/packages/web/bin/cli-entry.js @@ -0,0 +1,38 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath, pathToFileURL } from 'url'; + +function normalizeCliEntryPath(filePath, realpath = fs.realpathSync) { + if (typeof filePath !== 'string' || filePath.trim().length === 0) { + return null; + } + + const resolvedPath = path.resolve(filePath); + try { + return realpath(resolvedPath); + } catch { + return resolvedPath; + } +} + +function isModuleCliExecution(entryPath = process.argv[1], moduleUrl = import.meta.url, realpath = fs.realpathSync) { + if (typeof entryPath !== 'string' || entryPath.trim().length === 0) { + return false; + } + + try { + const normalizedEntryPath = normalizeCliEntryPath(entryPath, realpath); + const normalizedModulePath = normalizeCliEntryPath(fileURLToPath(moduleUrl), realpath); + if (!normalizedEntryPath || !normalizedModulePath) { + return false; + } + return pathToFileURL(normalizedEntryPath).href === pathToFileURL(normalizedModulePath).href; + } catch { + return false; + } +} + +export { + normalizeCliEntryPath, + isModuleCliExecution, +}; diff --git a/packages/web/bin/cli.js b/packages/web/bin/cli.js index 34b0a5a2..1678a4d3 100644 --- a/packages/web/bin/cli.js +++ b/packages/web/bin/cli.js @@ -7,6 +7,7 @@ import path from 'path'; import crypto from 'crypto'; import { spawn, spawnSync } from 'child_process'; import { fileURLToPath, pathToFileURL } from 'url'; +import { isModuleCliExecution } from './cli-entry.js'; import { cloudflareTunnelProviderCapabilities } from '../server/lib/tunnels/providers/cloudflare.js'; import { intro as clackIntro, outro as clackOutro, log as clackLog, @@ -4614,17 +4615,7 @@ async function main() { await commands[command](options); } -const isCliExecution = (() => { - const entry = process.argv[1]; - if (typeof entry !== 'string' || entry.length === 0) { - return false; - } - try { - return pathToFileURL(path.resolve(entry)).href === import.meta.url; - } catch { - return false; - } -})(); +const isCliExecution = isModuleCliExecution(); if (isCliExecution) { let isHandlingSigint = false; diff --git a/packages/web/bin/cli.test.js b/packages/web/bin/cli.test.js new file mode 100644 index 00000000..19ab94ba --- /dev/null +++ b/packages/web/bin/cli.test.js @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'bun:test'; +import path from 'path'; +import { pathToFileURL } from 'url'; + +import { isModuleCliExecution, normalizeCliEntryPath } from './cli-entry.js'; + +describe('cli entry detection', () => { + const modulePath = '/tmp/openchamber/bin/cli.js'; + const moduleUrl = pathToFileURL(modulePath).href; + + it('resolves symlinked entry paths before comparing', () => { + const symlinkPath = '/usr/local/bin/openchamber'; + const realpath = (filePath) => { + if (filePath === path.resolve(symlinkPath)) { + return modulePath; + } + return filePath; + }; + + expect(isModuleCliExecution(symlinkPath, moduleUrl, realpath)).toBe(true); + }); + + it('falls back to resolved paths when realpath fails', () => { + const realpath = () => { + throw new Error('realpath unavailable'); + }; + + expect(isModuleCliExecution(modulePath, moduleUrl, realpath)).toBe(true); + }); + + it('returns false for non-matching entry path', () => { + expect(isModuleCliExecution('/tmp/other-cli.js', moduleUrl)).toBe(false); + }); + + it('returns false for empty entry path', () => { + expect(isModuleCliExecution('', moduleUrl)).toBe(false); + }); + + it('normalizes direct paths when realpath fails', () => { + const unresolvedPath = './packages/web/bin/cli.js'; + const realpath = () => { + throw new Error('no symlink resolution'); + }; + + expect(normalizeCliEntryPath(unresolvedPath, realpath)).toBe(path.resolve(unresolvedPath)); + }); +});