Files
ONE-OS/axhub-make/scripts/utils/command-runtime.mjs
王冕 a27e3b8e43 feat: sync full workspace including web modules, docs, and configurations to Gitea
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>
2026-06-09 18:12:25 +08:00

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;
}