From d1f36bfd846b687edac07fdb6964b00d185d7dc5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Feb 2026 15:00:53 +0000 Subject: [PATCH] refactor(cli): share windows argv normalization --- src/cli/run-main.ts | 60 ++----------------------------- src/cli/windows-argv.ts | 78 +++++++++++++++++++++++++++++++++++++++++ src/entry.ts | 69 +----------------------------------- 3 files changed, 81 insertions(+), 126 deletions(-) create mode 100644 src/cli/windows-argv.ts diff --git a/src/cli/run-main.ts b/src/cli/run-main.ts index e9eb5d8243..0d0eee7825 100644 --- a/src/cli/run-main.ts +++ b/src/cli/run-main.ts @@ -1,5 +1,3 @@ -import fs from "node:fs"; -import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; import { loadDotEnv } from "../infra/dotenv.js"; @@ -12,6 +10,7 @@ import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections. import { enableConsoleCapture } from "../logging.js"; import { getCommandPath, getPrimaryCommand, hasHelpOrVersion } from "./argv.js"; import { tryRouteCli } from "./route.js"; +import { normalizeWindowsArgv } from "./windows-argv.js"; export function rewriteUpdateFlagArgv(argv: string[]): string[] { const index = argv.indexOf("--update"); @@ -63,7 +62,7 @@ export function shouldEnsureCliPath(argv: string[]): boolean { } export async function runCli(argv: string[] = process.argv) { - const normalizedArgv = stripWindowsNodeExec(argv); + const normalizedArgv = normalizeWindowsArgv(argv); loadDotEnv({ quiet: true }); normalizeEnv(); if (shouldEnsureCliPath(normalizedArgv)) { @@ -124,61 +123,6 @@ export async function runCli(argv: string[] = process.argv) { await program.parseAsync(parseArgv); } -function stripWindowsNodeExec(argv: string[]): string[] { - if (process.platform !== "win32") { - return argv; - } - const stripControlChars = (value: string): string => { - let out = ""; - for (let i = 0; i < value.length; i += 1) { - const code = value.charCodeAt(i); - if (code >= 32 && code !== 127) { - out += value[i]; - } - } - return out; - }; - const normalizeArg = (value: string): string => - stripControlChars(value) - .replace(/^['"]+|['"]+$/g, "") - .trim(); - const normalizeCandidate = (value: string): string => - normalizeArg(value).replace(/^\\\\\\?\\/, ""); - const execPath = normalizeCandidate(process.execPath); - const execPathLower = execPath.toLowerCase(); - const execBase = path.basename(execPath).toLowerCase(); - const isExecPath = (value: string | undefined): boolean => { - if (!value) { - return false; - } - const normalized = normalizeCandidate(value); - if (!normalized) { - return false; - } - const lower = normalized.toLowerCase(); - return ( - lower === execPathLower || - path.basename(lower) === execBase || - lower.endsWith("\\node.exe") || - lower.endsWith("/node.exe") || - lower.includes("node.exe") || - (path.basename(lower) === "node.exe" && fs.existsSync(normalized)) - ); - }; - const filtered = argv.filter((arg, index) => index === 0 || !isExecPath(arg)); - if (filtered.length < 3) { - return filtered; - } - const cleaned = [...filtered]; - if (isExecPath(cleaned[1])) { - cleaned.splice(1, 1); - } - if (isExecPath(cleaned[2])) { - cleaned.splice(2, 1); - } - return cleaned; -} - export function isCliMainModule(): boolean { return isMainModule({ currentFile: fileURLToPath(import.meta.url) }); } diff --git a/src/cli/windows-argv.ts b/src/cli/windows-argv.ts new file mode 100644 index 0000000000..14c1645b66 --- /dev/null +++ b/src/cli/windows-argv.ts @@ -0,0 +1,78 @@ +import fs from "node:fs"; +import path from "node:path"; + +export function normalizeWindowsArgv(argv: string[]): string[] { + if (process.platform !== "win32") { + return argv; + } + if (argv.length < 2) { + return argv; + } + + const stripControlChars = (value: string): string => { + let out = ""; + for (let i = 0; i < value.length; i += 1) { + const code = value.charCodeAt(i); + if (code >= 32 && code !== 127) { + out += value[i]; + } + } + return out; + }; + + const normalizeArg = (value: string): string => + stripControlChars(value) + .replace(/^['"]+|['"]+$/g, "") + .trim(); + const normalizeCandidate = (value: string): string => + normalizeArg(value).replace(/^\\\\\\?\\/, ""); + + const execPath = normalizeCandidate(process.execPath); + const execPathLower = execPath.toLowerCase(); + const execBase = path.basename(execPath).toLowerCase(); + const isExecPath = (value: string | undefined): boolean => { + if (!value) { + return false; + } + const normalized = normalizeCandidate(value); + if (!normalized) { + return false; + } + const lower = normalized.toLowerCase(); + return ( + lower === execPathLower || + path.basename(lower) === execBase || + lower.endsWith("\\node.exe") || + lower.endsWith("/node.exe") || + lower.includes("node.exe") || + (path.basename(lower) === "node.exe" && fs.existsSync(normalized)) + ); + }; + + const next = [...argv]; + for (let i = 1; i <= 3 && i < next.length; ) { + if (isExecPath(next[i])) { + next.splice(i, 1); + continue; + } + i += 1; + } + const filtered = next.filter((arg, index) => index === 0 || !isExecPath(arg)); + if (filtered.length < 3) { + return filtered; + } + const cleaned = [...filtered]; + for (let i = 2; i < cleaned.length; ) { + const arg = cleaned[i]; + if (!arg || arg.startsWith("-")) { + i += 1; + continue; + } + if (isExecPath(arg)) { + cleaned.splice(i, 1); + continue; + } + break; + } + return cleaned; +} diff --git a/src/entry.ts b/src/entry.ts index 46fc96d1e5..e066432893 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -1,9 +1,9 @@ #!/usr/bin/env node import { spawn } from "node:child_process"; -import path from "node:path"; import process from "node:process"; import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js"; import { shouldSkipRespawnForArgv } from "./cli/respawn-policy.js"; +import { normalizeWindowsArgv } from "./cli/windows-argv.js"; import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js"; import { installProcessWarningFilter } from "./infra/warning-filter.js"; import { attachChildProcessBridge } from "./process/child-process-bridge.js"; @@ -80,73 +80,6 @@ function ensureExperimentalWarningSuppressed(): boolean { return true; } -function normalizeWindowsArgv(argv: string[]): string[] { - if (process.platform !== "win32") { - return argv; - } - if (argv.length < 2) { - return argv; - } - const stripControlChars = (value: string): string => { - let out = ""; - for (let i = 0; i < value.length; i += 1) { - const code = value.charCodeAt(i); - if (code >= 32 && code !== 127) { - out += value[i]; - } - } - return out; - }; - const normalizeArg = (value: string): string => - stripControlChars(value) - .replace(/^['"]+|['"]+$/g, "") - .trim(); - const normalizeCandidate = (value: string): string => - normalizeArg(value).replace(/^\\\\\\?\\/, ""); - const execPath = normalizeCandidate(process.execPath); - const execPathLower = execPath.toLowerCase(); - const execBase = path.basename(execPath).toLowerCase(); - const isExecPath = (value: string | undefined): boolean => { - if (!value) { - return false; - } - const lower = normalizeCandidate(value).toLowerCase(); - return ( - lower === execPathLower || - path.basename(lower) === execBase || - lower.endsWith("\\node.exe") || - lower.endsWith("/node.exe") || - lower.includes("node.exe") - ); - }; - const next = [...argv]; - for (let i = 1; i <= 3 && i < next.length; ) { - if (isExecPath(next[i])) { - next.splice(i, 1); - continue; - } - i += 1; - } - const filtered = next.filter((arg, index) => index === 0 || !isExecPath(arg)); - if (filtered.length < 3) { - return filtered; - } - const cleaned = [...filtered]; - for (let i = 2; i < cleaned.length; ) { - const arg = cleaned[i]; - if (!arg || arg.startsWith("-")) { - i += 1; - continue; - } - if (isExecPath(arg)) { - cleaned.splice(i, 1); - continue; - } - break; - } - return cleaned; -} - process.argv = normalizeWindowsArgv(process.argv); if (!ensureExperimentalWarningSuppressed()) {