mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-25 11:20:07 +08:00
- Added IPC handlers for managing Node.js applications, including listing, adding, installing, updating, starting, stopping, and uninstalling apps. - Introduced deployment options for Node.js apps from ZIP files and Git repositories. - Enhanced the process utility to support environment variables during script execution. - Updated preload API to expose Node.js app management functionalities. - Added new UI components and routes for Node.js app management in the renderer. - Included internationalization support for Node.js app features in both English and Chinese.
315 lines
9.4 KiB
JavaScript
315 lines
9.4 KiB
JavaScript
const fs = require('fs')
|
||
const path = require('path')
|
||
const os = require('os')
|
||
const https = require('https')
|
||
const { execSync } = require('child_process')
|
||
|
||
// 配置
|
||
const NODE_VERSION = process.env.NODE_VERSION || '18.18.0' // 默认版本
|
||
const NODE_RELEASE_BASE_URL = 'https://nodejs.org/dist'
|
||
|
||
// 平台映射
|
||
const NODE_PACKAGES = {
|
||
'darwin-arm64': `node-v${NODE_VERSION}-darwin-arm64.tar.gz`,
|
||
'darwin-x64': `node-v${NODE_VERSION}-darwin-x64.tar.gz`,
|
||
'win32-x64': `node-v${NODE_VERSION}-win32-x64.zip`,
|
||
'win32-ia32': `node-v${NODE_VERSION}-win32-x86.zip`,
|
||
'linux-x64': `node-v${NODE_VERSION}-linux-x64.tar.gz`,
|
||
'linux-arm64': `node-v${NODE_VERSION}-linux-arm64.tar.gz`,
|
||
}
|
||
|
||
// 辅助函数 - 递归复制目录
|
||
function copyFolderRecursiveSync(source, target) {
|
||
// 检查目标目录是否存在,不存在则创建
|
||
if (!fs.existsSync(target)) {
|
||
fs.mkdirSync(target, { recursive: true });
|
||
}
|
||
|
||
// 读取源目录中的所有文件和文件夹
|
||
const files = fs.readdirSync(source);
|
||
|
||
// 循环处理每个文件/文件夹
|
||
for (const file of files) {
|
||
const sourcePath = path.join(source, file);
|
||
const targetPath = path.join(target, file);
|
||
|
||
// 检查是文件还是文件夹
|
||
if (fs.statSync(sourcePath).isDirectory()) {
|
||
// 如果是文件夹,递归复制
|
||
copyFolderRecursiveSync(sourcePath, targetPath);
|
||
} else {
|
||
// 如果是文件,直接复制
|
||
fs.copyFileSync(sourcePath, targetPath);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 二进制文件存放目录
|
||
const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||
|
||
// 创建二进制文件存放目录
|
||
async function createBinariesDir() {
|
||
if (!fs.existsSync(binariesDir)) {
|
||
console.log(`Creating binaries directory at ${binariesDir}`)
|
||
fs.mkdirSync(binariesDir, { recursive: true })
|
||
}
|
||
}
|
||
|
||
// 获取当前平台对应的包名
|
||
function getPackageForPlatform() {
|
||
const platform = os.platform()
|
||
const arch = os.arch()
|
||
const key = `${platform}-${arch}`
|
||
|
||
console.log(`Current platform: ${platform}, architecture: ${arch}`)
|
||
|
||
if (!NODE_PACKAGES[key]) {
|
||
throw new Error(`Unsupported platform/architecture: ${key}`)
|
||
}
|
||
|
||
return NODE_PACKAGES[key]
|
||
}
|
||
|
||
// 下载 Node.js
|
||
async function downloadNodeJs() {
|
||
const packageName = getPackageForPlatform()
|
||
const downloadUrl = `${NODE_RELEASE_BASE_URL}/v${NODE_VERSION}/${packageName}`
|
||
const tempFilePath = path.join(os.tmpdir(), packageName)
|
||
|
||
console.log(`Downloading Node.js v${NODE_VERSION} from ${downloadUrl}`)
|
||
console.log(`Temp file path: ${tempFilePath}`)
|
||
|
||
// 如果临时文件已存在,先删除
|
||
if (fs.existsSync(tempFilePath)) {
|
||
fs.unlinkSync(tempFilePath)
|
||
}
|
||
|
||
return new Promise((resolve, reject) => {
|
||
const file = fs.createWriteStream(tempFilePath)
|
||
|
||
https.get(downloadUrl, (response) => {
|
||
if (response.statusCode !== 200) {
|
||
reject(new Error(`Failed to download: ${response.statusCode} ${response.statusMessage}`))
|
||
return
|
||
}
|
||
|
||
console.log(`Download started, status code: ${response.statusCode}`)
|
||
|
||
response.pipe(file)
|
||
|
||
file.on('finish', () => {
|
||
file.close()
|
||
console.log('Download completed')
|
||
resolve(tempFilePath)
|
||
})
|
||
|
||
file.on('error', (err) => {
|
||
fs.unlinkSync(tempFilePath)
|
||
reject(err)
|
||
})
|
||
}).on('error', (err) => {
|
||
if (fs.existsSync(tempFilePath)) {
|
||
fs.unlinkSync(tempFilePath)
|
||
}
|
||
reject(err)
|
||
})
|
||
})
|
||
}
|
||
|
||
// 解压 Node.js 包
|
||
async function extractNodeJs(filePath) {
|
||
const platform = os.platform()
|
||
const extractDir = path.join(os.tmpdir(), `node-v${NODE_VERSION}-extract`)
|
||
|
||
if (fs.existsSync(extractDir)) {
|
||
console.log(`Removing existing extract directory: ${extractDir}`)
|
||
fs.rmSync(extractDir, { recursive: true, force: true })
|
||
}
|
||
|
||
console.log(`Creating extract directory: ${extractDir}`)
|
||
fs.mkdirSync(extractDir, { recursive: true })
|
||
|
||
console.log(`Extracting to ${extractDir}`)
|
||
|
||
if (platform === 'win32') {
|
||
// Windows 使用内置的解压工具
|
||
try {
|
||
const AdmZip = require('adm-zip')
|
||
console.log(`Using adm-zip to extract ${filePath}`)
|
||
const zip = new AdmZip(filePath)
|
||
zip.extractAllTo(extractDir, true)
|
||
console.log(`Extraction completed using adm-zip`)
|
||
} catch (error) {
|
||
console.error(`Error using adm-zip: ${error}`)
|
||
throw error
|
||
}
|
||
} else {
|
||
// Linux/Mac 使用 tar
|
||
try {
|
||
console.log(`Using tar to extract ${filePath} to ${extractDir}`)
|
||
execSync(`tar -xzf "${filePath}" -C "${extractDir}"`, { stdio: 'inherit' })
|
||
console.log(`Extraction completed using tar`)
|
||
} catch (error) {
|
||
console.error(`Error using tar: ${error}`)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
return extractDir
|
||
}
|
||
|
||
// 安装 Node.js
|
||
async function installNodeJs(extractDir) {
|
||
const platform = os.platform()
|
||
console.log(`Finding extracted Node.js directory in ${extractDir}`)
|
||
|
||
const items = fs.readdirSync(extractDir)
|
||
console.log(`Found items in extract directory: ${items.join(', ')}`)
|
||
|
||
// 找到包含"node-v"的目录名
|
||
const folderName = items.find(item => item.startsWith('node-v'))
|
||
|
||
if (!folderName) {
|
||
throw new Error(`Could not find Node.js directory in ${extractDir}`)
|
||
}
|
||
|
||
console.log(`Found Node.js directory: ${folderName}`)
|
||
const nodeBinPath = path.join(extractDir, folderName, 'bin')
|
||
|
||
console.log(`Node.js bin path: ${nodeBinPath}`)
|
||
|
||
// 复制 node 和 npm
|
||
if (platform === 'win32') {
|
||
// Windows
|
||
console.log('Installing Node.js binaries for Windows')
|
||
fs.copyFileSync(
|
||
path.join(extractDir, folderName, 'node.exe'),
|
||
path.join(binariesDir, 'node.exe')
|
||
)
|
||
console.log(`Copied node.exe to ${path.join(binariesDir, 'node.exe')}`)
|
||
|
||
fs.copyFileSync(
|
||
path.join(extractDir, folderName, 'npm.cmd'),
|
||
path.join(binariesDir, 'npm.cmd')
|
||
)
|
||
console.log(`Copied npm.cmd to ${path.join(binariesDir, 'npm.cmd')}`)
|
||
|
||
fs.copyFileSync(
|
||
path.join(extractDir, folderName, 'npx.cmd'),
|
||
path.join(binariesDir, 'npx.cmd')
|
||
)
|
||
console.log(`Copied npx.cmd to ${path.join(binariesDir, 'npx.cmd')}`)
|
||
} else {
|
||
// Linux/Mac
|
||
console.log('Installing Node.js binaries for Linux/Mac')
|
||
fs.copyFileSync(
|
||
path.join(nodeBinPath, 'node'),
|
||
path.join(binariesDir, 'node')
|
||
)
|
||
console.log(`Copied node to ${path.join(binariesDir, 'node')}`)
|
||
|
||
// 创建npm脚本,指向正确路径
|
||
const npmScript = `#!/usr/bin/env node
|
||
require("./node_modules/npm/lib/cli.js")(process)`;
|
||
fs.writeFileSync(path.join(binariesDir, 'npm'), npmScript);
|
||
console.log(`Created npm script at ${path.join(binariesDir, 'npm')}`);
|
||
|
||
// 创建npx脚本,指向正确路径
|
||
const npxScript = `#!/usr/bin/env node
|
||
require("./node_modules/npm/bin/npx-cli.js")`;
|
||
fs.writeFileSync(path.join(binariesDir, 'npx'), npxScript);
|
||
console.log(`Created npx script at ${path.join(binariesDir, 'npx')}`);
|
||
|
||
// 设置执行权限
|
||
execSync(`chmod +x "${path.join(binariesDir, 'node')}"`)
|
||
execSync(`chmod +x "${path.join(binariesDir, 'npm')}"`)
|
||
execSync(`chmod +x "${path.join(binariesDir, 'npx')}"`)
|
||
console.log('Set executable permissions for Node.js binaries')
|
||
}
|
||
|
||
// 复制 npm 相关文件和目录
|
||
const npmDir = path.join(binariesDir, 'node_modules', 'npm')
|
||
fs.mkdirSync(npmDir, { recursive: true })
|
||
console.log(`Created npm directory at ${npmDir}`)
|
||
|
||
// 复制 npm 目录的内容
|
||
const srcNpmDir = path.join(extractDir, folderName, 'lib', 'node_modules', 'npm')
|
||
console.log(`Copying npm files from ${srcNpmDir} to ${npmDir}`)
|
||
|
||
const files = fs.readdirSync(srcNpmDir)
|
||
|
||
for (const file of files) {
|
||
const srcPath = path.join(srcNpmDir, file)
|
||
const destPath = path.join(npmDir, file)
|
||
|
||
if (fs.lstatSync(srcPath).isDirectory()) {
|
||
// 使用自定义函数代替fs.cpSync,确保兼容性
|
||
console.log(`Copying directory: ${file}`)
|
||
copyFolderRecursiveSync(srcPath, destPath)
|
||
} else {
|
||
console.log(`Copying file: ${file}`)
|
||
fs.copyFileSync(srcPath, destPath)
|
||
}
|
||
}
|
||
|
||
console.log('Node.js installation completed successfully')
|
||
}
|
||
|
||
// 清理临时文件
|
||
async function cleanup(filePath, extractDir) {
|
||
try {
|
||
if (fs.existsSync(filePath)) {
|
||
console.log(`Cleaning up temp file: ${filePath}`)
|
||
fs.unlinkSync(filePath)
|
||
}
|
||
|
||
if (fs.existsSync(extractDir)) {
|
||
console.log(`Cleaning up extract directory: ${extractDir}`)
|
||
fs.rmSync(extractDir, { recursive: true, force: true })
|
||
}
|
||
|
||
console.log('Cleaned up temporary files')
|
||
} catch (error) {
|
||
console.error('Error during cleanup:', error)
|
||
}
|
||
}
|
||
|
||
// 主安装函数
|
||
async function install() {
|
||
try {
|
||
console.log(`Starting Node.js v${NODE_VERSION} installation...`)
|
||
|
||
await createBinariesDir()
|
||
console.log('Binary directory created/verified')
|
||
|
||
const filePath = await downloadNodeJs()
|
||
console.log(`Downloaded Node.js to ${filePath}`)
|
||
|
||
const extractDir = await extractNodeJs(filePath)
|
||
console.log(`Extracted Node.js to ${extractDir}`)
|
||
|
||
await installNodeJs(extractDir)
|
||
console.log('Installed Node.js binaries')
|
||
|
||
await cleanup(filePath, extractDir)
|
||
console.log('Cleanup completed')
|
||
|
||
console.log(`Node.js v${NODE_VERSION} has been installed successfully at ${binariesDir}`)
|
||
return true
|
||
} catch (error) {
|
||
console.error('Installation failed:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 执行安装
|
||
install()
|
||
.then(() => {
|
||
console.log('Installation process completed successfully')
|
||
process.exit(0)
|
||
})
|
||
.catch((error) => {
|
||
console.error('Fatal error during installation:', error)
|
||
process.exit(1)
|
||
})
|