Optimized the root .gitignore to exclude virtual environments, node modules, and temp folders to ensure clean and lightweight version tracking. Co-authored-by: Cursor <cursoragent@cursor.com>
341 lines
8.7 KiB
JavaScript
341 lines
8.7 KiB
JavaScript
import { spawn, spawnSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import iconv from 'iconv-lite';
|
|
|
|
const WINDOWS_CODEPAGE_TIMEOUT_MS = 1200;
|
|
let cachedWindowsCodePage = null;
|
|
|
|
function getPlatform(overridePlatform) {
|
|
return overridePlatform || process.platform;
|
|
}
|
|
|
|
function quoteForCmdExec(value) {
|
|
if (!value) return '""';
|
|
if (!/[\s"&^|<>]/.test(value)) return value;
|
|
const escaped = String(value)
|
|
.replace(/(\\*)"/g, '$1$1\\"')
|
|
.replace(/(\\+)$/g, '$1$1');
|
|
return `"${escaped}"`;
|
|
}
|
|
|
|
function buildWindowsCommandLine(command, args) {
|
|
return [command, ...args].map((part) => quoteForCmdExec(String(part))).join(' ');
|
|
}
|
|
|
|
function getEnvValue(env, key) {
|
|
if (!env) return undefined;
|
|
|
|
const direct = env[key];
|
|
if (typeof direct === 'string' && direct.length > 0) {
|
|
return direct;
|
|
}
|
|
|
|
const matchedKey = Object.keys(env).find((candidate) => candidate.toLowerCase() === key.toLowerCase());
|
|
if (!matchedKey) return undefined;
|
|
|
|
const value = env[matchedKey];
|
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
}
|
|
|
|
function getWindowsPathExtList(env) {
|
|
const pathExt = getEnvValue(env, 'PATHEXT') || '.COM;.EXE;.BAT;.CMD';
|
|
return pathExt
|
|
.split(';')
|
|
.map((ext) => ext.trim())
|
|
.filter(Boolean)
|
|
.map((ext) => (ext.startsWith('.') ? ext.toLowerCase() : `.${ext.toLowerCase()}`));
|
|
}
|
|
|
|
function resolveWindowsCommand(command, env) {
|
|
if (!command || typeof command !== 'string') return command;
|
|
|
|
const trimmed = command.trim();
|
|
if (!trimmed) return trimmed;
|
|
|
|
const hasPathSeparator = /[\\/]/.test(trimmed);
|
|
const ext = path.extname(trimmed);
|
|
const pathExts = ext ? [''] : getWindowsPathExtList(env);
|
|
|
|
const candidateDirs = hasPathSeparator
|
|
? ['']
|
|
: (getEnvValue(env, 'PATH') || '')
|
|
.split(';')
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean);
|
|
|
|
const baseCandidates = hasPathSeparator ? [trimmed] : candidateDirs.map((dir) => path.join(dir, trimmed));
|
|
|
|
for (const baseCandidate of baseCandidates) {
|
|
const suffixes = ext ? [''] : pathExts;
|
|
for (const suffix of suffixes) {
|
|
const fullPath = suffix ? `${baseCandidate}${suffix}` : baseCandidate;
|
|
if (fs.existsSync(fullPath)) {
|
|
return fullPath;
|
|
}
|
|
}
|
|
}
|
|
|
|
return trimmed;
|
|
}
|
|
|
|
function shouldUseWindowsCmdWrapper(platform, command) {
|
|
if (platform !== 'win32') return false;
|
|
return /\.(cmd|bat)$/i.test(command) || !/\.(exe|com)$/i.test(command);
|
|
}
|
|
|
|
function getSpawnSpec(command, args, platform = process.platform, env = process.env) {
|
|
const resolvedCommand = platform === 'win32' ? resolveWindowsCommand(command, env) : command;
|
|
|
|
if (!shouldUseWindowsCmdWrapper(platform, resolvedCommand)) {
|
|
return {
|
|
command: resolvedCommand,
|
|
args,
|
|
windowsHide: platform === 'win32',
|
|
};
|
|
}
|
|
|
|
const commandLine = buildWindowsCommandLine(resolvedCommand, args);
|
|
return {
|
|
command: 'cmd.exe',
|
|
args: ['/d', '/s', '/c', commandLine],
|
|
windowsHide: true,
|
|
};
|
|
}
|
|
|
|
function mapCodePageToEncoding(codePage) {
|
|
if (codePage === 65001) return 'utf8';
|
|
if (codePage === 936) return 'gbk';
|
|
if (codePage === 54936) return 'gb18030';
|
|
return 'gb18030';
|
|
}
|
|
|
|
function parseWindowsCodePage(text) {
|
|
if (!text) return null;
|
|
const match = String(text).match(/(\d{3,5})/);
|
|
if (!match) return null;
|
|
const codePage = Number(match[1]);
|
|
return Number.isFinite(codePage) ? codePage : null;
|
|
}
|
|
|
|
function readWindowsCodePageSync() {
|
|
if (cachedWindowsCodePage !== null) {
|
|
return cachedWindowsCodePage;
|
|
}
|
|
|
|
try {
|
|
const result = spawnSync('cmd.exe', ['/d', '/s', '/c', 'chcp'], {
|
|
windowsHide: true,
|
|
encoding: 'utf8',
|
|
timeout: WINDOWS_CODEPAGE_TIMEOUT_MS,
|
|
});
|
|
|
|
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
cachedWindowsCodePage = parseWindowsCodePage(output);
|
|
} catch {
|
|
cachedWindowsCodePage = null;
|
|
}
|
|
|
|
return cachedWindowsCodePage;
|
|
}
|
|
|
|
function toBuffer(value) {
|
|
if (!value) return Buffer.alloc(0);
|
|
if (Buffer.isBuffer(value)) return value;
|
|
if (typeof value === 'string') return Buffer.from(value);
|
|
if (value instanceof Uint8Array) return Buffer.from(value);
|
|
return Buffer.from(String(value));
|
|
}
|
|
|
|
export function decodeOutput(value, options = {}) {
|
|
if (value === null || value === undefined) return '';
|
|
if (typeof value === 'string') return value;
|
|
|
|
const platform = getPlatform(options.platform);
|
|
const buffer = toBuffer(value);
|
|
if (buffer.length === 0) return '';
|
|
|
|
try {
|
|
const strictUtf8 = new TextDecoder('utf-8', { fatal: true }).decode(buffer);
|
|
return strictUtf8;
|
|
} catch {
|
|
// Fall through to platform fallback decoder.
|
|
}
|
|
|
|
if (platform === 'win32') {
|
|
const activeCodePage = readWindowsCodePageSync();
|
|
const preferredEncoding = mapCodePageToEncoding(activeCodePage);
|
|
|
|
try {
|
|
return iconv.decode(buffer, preferredEncoding);
|
|
} catch {
|
|
// Fall through to generic fallback.
|
|
}
|
|
|
|
try {
|
|
return iconv.decode(buffer, 'gb18030');
|
|
} catch {
|
|
// Fall through to latin1 fallback.
|
|
}
|
|
}
|
|
|
|
try {
|
|
return buffer.toString('utf8');
|
|
} catch {
|
|
return buffer.toString('latin1');
|
|
}
|
|
}
|
|
|
|
export function runCommandSync(options) {
|
|
const {
|
|
command,
|
|
args = [],
|
|
cwd,
|
|
env,
|
|
timeoutMs,
|
|
maxBuffer,
|
|
} = options;
|
|
|
|
const platform = process.platform;
|
|
const mergedEnv = env ? { ...process.env, ...env } : process.env;
|
|
const spawnSpec = getSpawnSpec(command, args, platform, mergedEnv);
|
|
const result = spawnSync(spawnSpec.command, spawnSpec.args, {
|
|
cwd,
|
|
env: mergedEnv,
|
|
timeout: timeoutMs,
|
|
maxBuffer,
|
|
windowsHide: spawnSpec.windowsHide,
|
|
encoding: null,
|
|
});
|
|
|
|
const stdoutBuffer = toBuffer(result.stdout);
|
|
const stderrBuffer = toBuffer(result.stderr);
|
|
|
|
return {
|
|
command,
|
|
args,
|
|
spawnCommand: spawnSpec.command,
|
|
spawnArgs: spawnSpec.args,
|
|
status: typeof result.status === 'number' ? result.status : null,
|
|
signal: result.signal || null,
|
|
error: result.error || null,
|
|
stdoutBuffer,
|
|
stderrBuffer,
|
|
stdout: decodeOutput(stdoutBuffer, { platform }),
|
|
stderr: decodeOutput(stderrBuffer, { platform }),
|
|
};
|
|
}
|
|
|
|
export function runCommand(options) {
|
|
const {
|
|
command,
|
|
args = [],
|
|
cwd,
|
|
env,
|
|
timeoutMs,
|
|
detached = false,
|
|
capture = true,
|
|
stdio,
|
|
} = options;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const platform = process.platform;
|
|
const mergedEnv = env ? { ...process.env, ...env } : process.env;
|
|
const spawnSpec = getSpawnSpec(command, args, platform, mergedEnv);
|
|
|
|
const child = spawn(spawnSpec.command, spawnSpec.args, {
|
|
cwd,
|
|
env: mergedEnv,
|
|
detached,
|
|
stdio: stdio || (capture ? ['ignore', 'pipe', 'pipe'] : 'inherit'),
|
|
windowsHide: spawnSpec.windowsHide,
|
|
});
|
|
|
|
const stdoutChunks = [];
|
|
const stderrChunks = [];
|
|
|
|
if (capture && child.stdout) {
|
|
child.stdout.on('data', (chunk) => {
|
|
stdoutChunks.push(toBuffer(chunk));
|
|
});
|
|
}
|
|
|
|
if (capture && child.stderr) {
|
|
child.stderr.on('data', (chunk) => {
|
|
stderrChunks.push(toBuffer(chunk));
|
|
});
|
|
}
|
|
|
|
let timeoutId = null;
|
|
if (typeof timeoutMs === 'number' && timeoutMs > 0) {
|
|
timeoutId = setTimeout(() => {
|
|
child.kill('SIGTERM');
|
|
}, timeoutMs);
|
|
}
|
|
|
|
child.once('error', (error) => {
|
|
if (timeoutId) clearTimeout(timeoutId);
|
|
reject(error);
|
|
});
|
|
|
|
child.once('close', (code, signal) => {
|
|
if (timeoutId) clearTimeout(timeoutId);
|
|
|
|
const stdoutBuffer = Buffer.concat(stdoutChunks);
|
|
const stderrBuffer = Buffer.concat(stderrChunks);
|
|
|
|
resolve({
|
|
command,
|
|
args,
|
|
spawnCommand: spawnSpec.command,
|
|
spawnArgs: spawnSpec.args,
|
|
code: typeof code === 'number' ? code : null,
|
|
signal: signal || null,
|
|
stdoutBuffer,
|
|
stderrBuffer,
|
|
stdout: decodeOutput(stdoutBuffer, { platform }),
|
|
stderr: decodeOutput(stderrBuffer, { platform }),
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
export function commandExists(command) {
|
|
if (!command || typeof command !== 'string') {
|
|
return false;
|
|
}
|
|
|
|
const checker = process.platform === 'win32' ? 'where' : 'which';
|
|
const result = runCommandSync({
|
|
command: checker,
|
|
args: [command],
|
|
timeoutMs: 2000,
|
|
});
|
|
|
|
return result.status === 0;
|
|
}
|
|
|
|
export function getPreferredNpmCommand() {
|
|
if (process.platform !== 'win32') {
|
|
return 'npm';
|
|
}
|
|
|
|
return commandExists('npm.cmd') ? 'npm.cmd' : 'npm';
|
|
}
|
|
|
|
export function getPreferredNpxCommand() {
|
|
if (process.platform !== 'win32') {
|
|
return 'npx';
|
|
}
|
|
|
|
return commandExists('npx.cmd') ? 'npx.cmd' : 'npx';
|
|
}
|
|
|
|
export function getSpawnCommandSpec(command, args = [], platform = process.platform) {
|
|
return getSpawnSpec(command, args, platform);
|
|
}
|
|
|
|
export function __resetWindowsCodePageCacheForTests() {
|
|
cachedWindowsCodePage = null;
|
|
}
|