|
|
@@ -1,1207 +0,0 @@
|
|
|
-// electron/modules/installer.ts - 安装逻辑模块
|
|
|
-
|
|
|
-import * as os from 'os'
|
|
|
-import * as path from 'path'
|
|
|
-import * as fs from 'fs'
|
|
|
-import { execa, type ResultPromise } from 'execa'
|
|
|
-import * as sudo from 'sudo-prompt'
|
|
|
-import type { SoftwareType, SoftwareTypeWithAll, InstallOptions, CommandResult, InstalledInfo, AllInstalledInfo, Platform } from './types'
|
|
|
-import {
|
|
|
- NPM_REGISTRY,
|
|
|
- ERROR_MESSAGES,
|
|
|
- STATUS_MESSAGES,
|
|
|
- BREW_PACKAGES,
|
|
|
- APP_NAME,
|
|
|
- FALLBACK_VERSIONS
|
|
|
-} from './constants'
|
|
|
-import { commandExistsWithRefresh, getCommandVersionWithRefresh, downloadFile, getTempDir, cancelCurrentDownload } from './utils'
|
|
|
-import { installVscodeExtension } from './vscode-extension'
|
|
|
-import { needsAptUpdate, markAptUpdated } from './source-updater'
|
|
|
-import { getGitDownloadUrl, getNodejsDownloadUrl, getVSCodeDownloadUrl, getNodejsMirrorConfig } from './version-fetcher'
|
|
|
-import logger from './logger'
|
|
|
-
|
|
|
-// 当前安装进程
|
|
|
-let currentProcess: ResultPromise | null = null
|
|
|
-let installCancelled = false
|
|
|
-
|
|
|
-
|
|
|
-/**
|
|
|
- * 取消当前安装
|
|
|
- */
|
|
|
-export function cancelInstall(): boolean {
|
|
|
- installCancelled = true
|
|
|
- let cancelled = false
|
|
|
-
|
|
|
- // 取消正在进行的下载
|
|
|
- if (cancelCurrentDownload()) {
|
|
|
- logger.installInfo('下载已取消')
|
|
|
- cancelled = true
|
|
|
- }
|
|
|
-
|
|
|
- // 取消正在运行的进程
|
|
|
- if (currentProcess) {
|
|
|
- try {
|
|
|
- currentProcess.kill('SIGTERM')
|
|
|
- logger.installInfo('安装进程已取消')
|
|
|
- cancelled = true
|
|
|
- } catch (error) {
|
|
|
- logger.installError('取消安装进程失败', error)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (cancelled) {
|
|
|
- logger.installInfo('安装已取消')
|
|
|
- }
|
|
|
-
|
|
|
- return cancelled
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 重置取消状态
|
|
|
- */
|
|
|
-function resetCancelState(): void {
|
|
|
- installCancelled = false
|
|
|
- currentProcess = null
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 获取 npm 的完整路径
|
|
|
- * Windows: 安装后 PATH 环境变量不会立即对当前进程生效,需要使用完整路径
|
|
|
- * macOS/Linux: 通常不需要,但为了健壮性也提供完整路径
|
|
|
- */
|
|
|
-function getNpmPath(): string {
|
|
|
- const platform = os.platform() as Platform
|
|
|
- switch (platform) {
|
|
|
- case 'win32': {
|
|
|
- const programFiles = process.env.ProgramFiles || 'C:\\Program Files'
|
|
|
- return path.join(programFiles, 'nodejs', 'npm.cmd')
|
|
|
- }
|
|
|
- case 'darwin': {
|
|
|
- // Homebrew 在 Apple Silicon 上安装到 /opt/homebrew,Intel 上安装到 /usr/local
|
|
|
- const arch = os.arch()
|
|
|
- if (arch === 'arm64') {
|
|
|
- return '/opt/homebrew/bin/npm'
|
|
|
- }
|
|
|
- return '/usr/local/bin/npm'
|
|
|
- }
|
|
|
- case 'linux':
|
|
|
- return '/usr/bin/npm'
|
|
|
- default:
|
|
|
- return 'npm'
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 获取 pnpm 的完整路径
|
|
|
- */
|
|
|
-function getPnpmPath(): string {
|
|
|
- const platform = os.platform() as Platform
|
|
|
- switch (platform) {
|
|
|
- case 'win32': {
|
|
|
- const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming')
|
|
|
- return path.join(appData, 'npm', 'pnpm.cmd')
|
|
|
- }
|
|
|
- case 'darwin': {
|
|
|
- const arch = os.arch()
|
|
|
- if (arch === 'arm64') {
|
|
|
- return '/opt/homebrew/bin/pnpm'
|
|
|
- }
|
|
|
- return '/usr/local/bin/pnpm'
|
|
|
- }
|
|
|
- case 'linux':
|
|
|
- return '/usr/local/bin/pnpm'
|
|
|
- default:
|
|
|
- return 'pnpm'
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 检查是否已取消
|
|
|
- */
|
|
|
-function checkCancelled(): void {
|
|
|
- if (installCancelled) {
|
|
|
- throw new Error(ERROR_MESSAGES.INSTALL_CANCELLED)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 使用 sudo-prompt 执行需要管理员权限的命令
|
|
|
- */
|
|
|
-function sudoExec(command: string): Promise<{ stdout: string; stderr: string }> {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const options = { name: APP_NAME }
|
|
|
- sudo.exec(command, options, (error, stdout, stderr) => {
|
|
|
- // 过滤乱码字符的辅助函数(Windows 命令行输出可能是 GBK 编码)
|
|
|
- const cleanOutput = (str: string | Buffer | undefined): string => {
|
|
|
- if (!str) return ''
|
|
|
- const raw = str.toString()
|
|
|
- // 过滤掉非 ASCII 可打印字符和非中文字符,保留换行符
|
|
|
- return raw.replace(/[^\x20-\x7E\u4e00-\u9fa5\n\r]/g, '').trim()
|
|
|
- }
|
|
|
-
|
|
|
- if (error) {
|
|
|
- // 包含 stderr 和 stdout 信息以便调试
|
|
|
- const exitCode = (error as Error & { code?: number }).code
|
|
|
- const stderrStr = cleanOutput(stderr)
|
|
|
- const stdoutStr = cleanOutput(stdout)
|
|
|
- let errorMessage = error.message
|
|
|
- if (exitCode !== undefined) {
|
|
|
- errorMessage += `\n退出码: ${exitCode}`
|
|
|
- }
|
|
|
- if (stderrStr) {
|
|
|
- errorMessage += `\n详细信息: ${stderrStr}`
|
|
|
- }
|
|
|
- if (stdoutStr) {
|
|
|
- errorMessage += `\n输出: ${stdoutStr}`
|
|
|
- }
|
|
|
- const enhancedError = new Error(errorMessage)
|
|
|
- reject(enhancedError)
|
|
|
- } else {
|
|
|
- resolve({
|
|
|
- stdout: cleanOutput(stdout),
|
|
|
- stderr: cleanOutput(stderr)
|
|
|
- })
|
|
|
- }
|
|
|
- })
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 为包含空格的路径添加引号(Windows)
|
|
|
- */
|
|
|
-function quoteIfNeeded(str: string): string {
|
|
|
- // 如果字符串包含空格且没有被引号包围,则添加引号
|
|
|
- if (str.includes(' ') && !str.startsWith('"') && !str.endsWith('"')) {
|
|
|
- return `"${str}"`
|
|
|
- }
|
|
|
- return str
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 跨平台执行系统命令(带权限处理)
|
|
|
- */
|
|
|
-async function executeCommand(
|
|
|
- command: string,
|
|
|
- args: string[] = [],
|
|
|
- needAdmin = false
|
|
|
-): Promise<{ stdout: string; stderr: string }> {
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- const platform = os.platform() as Platform
|
|
|
-
|
|
|
- // 构建完整命令时,对包含空格的部分添加引号
|
|
|
- const quotedCommand = quoteIfNeeded(command)
|
|
|
- const quotedArgs = args.map(arg => quoteIfNeeded(arg))
|
|
|
- const fullCommand = `${quotedCommand} ${quotedArgs.join(' ')}`
|
|
|
-
|
|
|
- logger.installDebug(`执行命令: ${fullCommand}`)
|
|
|
-
|
|
|
- try {
|
|
|
- if (needAdmin) {
|
|
|
- // 使用 sudo-prompt 执行需要管理员权限的命令
|
|
|
- if (platform === 'win32') {
|
|
|
- // Windows: 直接执行命令,sudo-prompt 会处理 UAC
|
|
|
- return await sudoExec(fullCommand)
|
|
|
- } else {
|
|
|
- // Linux/macOS: 使用 sudo
|
|
|
- return await sudoExec(fullCommand)
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 无需提权,直接执行
|
|
|
- // Windows 上执行 .cmd 文件时,使用 shell: true 避免参数解析问题
|
|
|
- const useShell = platform === 'win32' && command.endsWith('.cmd')
|
|
|
- const proc = execa(command, args, { stdio: 'pipe', shell: useShell })
|
|
|
- currentProcess = proc
|
|
|
- const result = await proc
|
|
|
- currentProcess = null
|
|
|
- return { stdout: result.stdout, stderr: result.stderr }
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- logger.installError(`命令执行失败: ${fullCommand}`, error)
|
|
|
- throw error
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 检测软件是否已安装
|
|
|
- * Windows: 安装后 PATH 环境变量不会立即对当前进程生效,
|
|
|
- * 所以使用 commandExistsWithRefresh 从注册表重新读取最新的 PATH
|
|
|
- */
|
|
|
-export async function checkInstalled(software: SoftwareType): Promise<InstalledInfo> {
|
|
|
- let command: string
|
|
|
- let versionArgs: string[]
|
|
|
-
|
|
|
- switch (software) {
|
|
|
- case 'nodejs':
|
|
|
- command = 'node'
|
|
|
- versionArgs = ['--version']
|
|
|
- break
|
|
|
- case 'pnpm':
|
|
|
- command = 'pnpm'
|
|
|
- versionArgs = ['--version']
|
|
|
- break
|
|
|
- case 'vscode':
|
|
|
- command = 'code'
|
|
|
- versionArgs = ['--version']
|
|
|
- break
|
|
|
- case 'git':
|
|
|
- command = 'git'
|
|
|
- versionArgs = ['--version']
|
|
|
- break
|
|
|
- case 'claudeCode':
|
|
|
- command = 'claude'
|
|
|
- versionArgs = ['--version']
|
|
|
- break
|
|
|
- default:
|
|
|
- return { installed: false, version: null }
|
|
|
- }
|
|
|
-
|
|
|
- // 使用刷新后的 PATH 检测命令是否存在(Windows 上会从注册表重新读取 PATH)
|
|
|
- const exists = await commandExistsWithRefresh(command)
|
|
|
- if (!exists) {
|
|
|
- return { installed: false, version: null }
|
|
|
- }
|
|
|
-
|
|
|
- // 使用刷新后的 PATH 获取版本
|
|
|
- const version = await getCommandVersionWithRefresh(command, versionArgs)
|
|
|
- return { installed: true, version }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 检测所有软件的安装状态
|
|
|
- */
|
|
|
-export async function checkAllInstalled(): Promise<AllInstalledInfo> {
|
|
|
- const results = await Promise.allSettled([
|
|
|
- checkInstalled('nodejs'),
|
|
|
- checkInstalled('pnpm'),
|
|
|
- checkInstalled('vscode'),
|
|
|
- checkInstalled('git'),
|
|
|
- checkInstalled('claudeCode')
|
|
|
- ])
|
|
|
-
|
|
|
- return {
|
|
|
- nodejs: results[0].status === 'fulfilled' ? results[0].value : { installed: false, version: null },
|
|
|
- pnpm: results[1].status === 'fulfilled' ? results[1].value : { installed: false, version: null },
|
|
|
- vscode: results[2].status === 'fulfilled' ? results[2].value : { installed: false, version: null },
|
|
|
- git: results[3].status === 'fulfilled' ? results[3].value : { installed: false, version: null },
|
|
|
- claudeCode: results[4].status === 'fulfilled' ? results[4].value : { installed: false, version: null }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 安装命令生成 ====================
|
|
|
-
|
|
|
-/**
|
|
|
- * 获取 Node.js 安装命令 (macOS/Linux 使用包管理器)
|
|
|
- * Windows 使用下载 msi 安装包方式,不使用此函数
|
|
|
- * @param version 版本号
|
|
|
- */
|
|
|
-function getNodeInstallArgs(version = 'lts'): CommandResult {
|
|
|
- const platform = os.platform() as Platform
|
|
|
- const major = version.split('.')[0]
|
|
|
- const majorNum = parseInt(major)
|
|
|
-
|
|
|
- let brewPkg: string
|
|
|
-
|
|
|
- if (majorNum === 20) {
|
|
|
- brewPkg = BREW_PACKAGES.nodejs['20']
|
|
|
- } else if (majorNum === 18) {
|
|
|
- brewPkg = BREW_PACKAGES.nodejs['18']
|
|
|
- } else {
|
|
|
- brewPkg = BREW_PACKAGES.nodejs.default
|
|
|
- }
|
|
|
-
|
|
|
- switch (platform) {
|
|
|
- case 'darwin':
|
|
|
- return { command: 'brew', args: ['install', brewPkg] }
|
|
|
- case 'linux':
|
|
|
- return { command: 'apt', args: ['install', '-y', 'nodejs', 'npm'] }
|
|
|
- default:
|
|
|
- throw new Error(`${ERROR_MESSAGES.UNSUPPORTED_PLATFORM}: ${platform}`)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 获取 VS Code 安装命令 (macOS/Linux 使用包管理器)
|
|
|
- * Windows 使用下载安装包方式,不使用此函数(除了 insiders 版本)
|
|
|
- * @param version 版本号
|
|
|
- */
|
|
|
-function getVSCodeInstallArgs(version = 'stable'): CommandResult {
|
|
|
- const platform = os.platform() as Platform
|
|
|
-
|
|
|
- if (version === 'insiders') {
|
|
|
- switch (platform) {
|
|
|
- case 'darwin':
|
|
|
- return {
|
|
|
- command: 'brew',
|
|
|
- args: ['install', '--cask', BREW_PACKAGES.vscode.insiders]
|
|
|
- }
|
|
|
- case 'linux':
|
|
|
- return { command: 'snap', args: ['install', 'code', '--channel=insiders'] }
|
|
|
- default:
|
|
|
- throw new Error(`${ERROR_MESSAGES.UNSUPPORTED_PLATFORM}: ${platform}`)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- switch (platform) {
|
|
|
- case 'darwin':
|
|
|
- return {
|
|
|
- command: 'brew',
|
|
|
- args: ['install', '--cask', BREW_PACKAGES.vscode.stable]
|
|
|
- }
|
|
|
- case 'linux':
|
|
|
- return { command: 'snap', args: ['install', 'code', '--classic'] }
|
|
|
- default:
|
|
|
- throw new Error(`${ERROR_MESSAGES.UNSUPPORTED_PLATFORM}: ${platform}`)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 获取 Git 安装命令 (macOS/Linux 使用包管理器)
|
|
|
- * Windows 使用下载安装包方式,不使用此函数
|
|
|
- * @param version 版本号
|
|
|
- */
|
|
|
-function getGitInstallArgs(version = 'stable'): CommandResult {
|
|
|
- const platform = os.platform() as Platform
|
|
|
-
|
|
|
- if (version === 'mingit') {
|
|
|
- switch (platform) {
|
|
|
- case 'darwin':
|
|
|
- return { command: 'brew', args: ['install', BREW_PACKAGES.git.stable] }
|
|
|
- case 'linux':
|
|
|
- return { command: 'apt', args: ['install', '-y', 'git'] }
|
|
|
- default:
|
|
|
- throw new Error(`${ERROR_MESSAGES.UNSUPPORTED_PLATFORM}: ${platform}`)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (version === 'lfs') {
|
|
|
- switch (platform) {
|
|
|
- case 'darwin':
|
|
|
- return { command: 'brew', args: ['install', BREW_PACKAGES.git.lfs] }
|
|
|
- case 'linux':
|
|
|
- return { command: 'apt', args: ['install', '-y', 'git-lfs'] }
|
|
|
- default:
|
|
|
- throw new Error(`${ERROR_MESSAGES.UNSUPPORTED_PLATFORM}: ${platform}`)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- switch (platform) {
|
|
|
- case 'darwin':
|
|
|
- return { command: 'brew', args: ['install', BREW_PACKAGES.git.stable] }
|
|
|
- case 'linux':
|
|
|
- return { command: 'apt', args: ['install', '-y', 'git'] }
|
|
|
- default:
|
|
|
- throw new Error(`${ERROR_MESSAGES.UNSUPPORTED_PLATFORM}: ${platform}`)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 可卸载的软件类型
|
|
|
-type UninstallableSoftwareType = 'nodejs' | 'vscode' | 'git'
|
|
|
-
|
|
|
-/**
|
|
|
- * 获取卸载命令 (macOS/Linux 使用包管理器)
|
|
|
- * Windows 暂不支持通过此工具卸载,请使用系统设置
|
|
|
- */
|
|
|
-function getUninstallArgs(software: UninstallableSoftwareType): CommandResult {
|
|
|
- const platform = os.platform() as Platform
|
|
|
-
|
|
|
- switch (platform) {
|
|
|
- case 'win32':
|
|
|
- // Windows 暂不支持通过此工具卸载,请使用系统设置中的"应用和功能"
|
|
|
- throw new Error('Windows 暂不支持通过此工具卸载软件,请使用系统设置中的"应用和功能"')
|
|
|
- case 'darwin': {
|
|
|
- let brewPkg: string
|
|
|
- switch (software) {
|
|
|
- case 'nodejs':
|
|
|
- brewPkg = BREW_PACKAGES.nodejs.default
|
|
|
- break
|
|
|
- case 'vscode':
|
|
|
- brewPkg = BREW_PACKAGES.vscode.stable
|
|
|
- break
|
|
|
- case 'git':
|
|
|
- brewPkg = BREW_PACKAGES.git.stable
|
|
|
- break
|
|
|
- }
|
|
|
- return { command: 'brew', args: ['uninstall', brewPkg] }
|
|
|
- }
|
|
|
- case 'linux': {
|
|
|
- let aptPkg: string
|
|
|
- switch (software) {
|
|
|
- case 'nodejs':
|
|
|
- aptPkg = 'nodejs'
|
|
|
- break
|
|
|
- case 'vscode':
|
|
|
- return { command: 'snap', args: ['remove', 'code'] }
|
|
|
- case 'git':
|
|
|
- aptPkg = 'git'
|
|
|
- break
|
|
|
- }
|
|
|
- return { command: 'apt', args: ['remove', '-y', aptPkg] }
|
|
|
- }
|
|
|
- default:
|
|
|
- throw new Error(`${ERROR_MESSAGES.UNSUPPORTED_PLATFORM}: ${platform}`)
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// ==================== 安装流程 ====================
|
|
|
-
|
|
|
-export type StatusCallback = (software: SoftwareTypeWithAll, message: string, progress: number, skipLog?: boolean) => void
|
|
|
-export type CompleteCallback = (software: SoftwareTypeWithAll, message: string) => void
|
|
|
-export type ErrorCallback = (software: SoftwareTypeWithAll, message: string) => void
|
|
|
-
|
|
|
-/**
|
|
|
- * Linux 下执行 apt update(带缓存,1天内只更新一次)
|
|
|
- */
|
|
|
-export async function aptUpdate(onStatus?: StatusCallback, software?: SoftwareTypeWithAll): Promise<void> {
|
|
|
- if (os.platform() !== 'linux') return
|
|
|
- if (!needsAptUpdate()) {
|
|
|
- logger.installInfo('apt 源已在缓存期内,跳过更新')
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (onStatus && software) {
|
|
|
- onStatus(software, STATUS_MESSAGES.UPDATING_SOURCE, 10)
|
|
|
- }
|
|
|
- await executeCommand('apt', ['update'], true)
|
|
|
- markAptUpdated()
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 安装 Node.js (Windows 使用 msi 安装包)
|
|
|
- * @param version 版本号
|
|
|
- * @param installPnpm 是否安装 pnpm
|
|
|
- * @param onStatus 状态回调
|
|
|
- * @param customPath 自定义安装路径 (仅 Windows 支持)
|
|
|
- */
|
|
|
-export async function installNodejs(
|
|
|
- version = 'lts',
|
|
|
- installPnpm = true,
|
|
|
- onStatus: StatusCallback,
|
|
|
- customPath?: string
|
|
|
-): Promise<void> {
|
|
|
- resetCancelState()
|
|
|
-
|
|
|
- const platform = os.platform() as Platform
|
|
|
-
|
|
|
- // Windows: 直接下载 msi 安装包
|
|
|
- if (platform === 'win32') {
|
|
|
- // 确定目标版本
|
|
|
- let targetVersion = version
|
|
|
- if (version === 'lts' || !version || !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
|
- // 如果是 lts 或无效版本,使用备用版本
|
|
|
- targetVersion = FALLBACK_VERSIONS.nodejs
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 下载 msi 安装包
|
|
|
- const arch = os.arch() === 'x64' ? 'x64' : os.arch() === 'arm64' ? 'arm64' : 'x86'
|
|
|
- const downloadUrl = getNodejsDownloadUrl(targetVersion).replace('.zip', '.msi').replace(`-win-${arch}`, `-${arch}`)
|
|
|
- const tempDir = getTempDir()
|
|
|
- const installerPath = path.join(tempDir, `node-v${targetVersion}-${arch}.msi`)
|
|
|
-
|
|
|
- onStatus('nodejs', `正在下载 Node.js ${targetVersion}...`, 10)
|
|
|
- logger.installInfo(`开始下载 Node.js: ${downloadUrl}`)
|
|
|
-
|
|
|
- try {
|
|
|
- let lastLoggedPercent = 0
|
|
|
- await downloadFile(downloadUrl, installerPath, (downloaded, total, percent) => {
|
|
|
- const progress = 10 + Math.round(percent * 0.4)
|
|
|
- const downloadedMB = (downloaded / 1024 / 1024).toFixed(1)
|
|
|
- const totalMB = (total / 1024 / 1024).toFixed(1)
|
|
|
- // 每次都更新进度条,但只在每 5% 时记录日志
|
|
|
- const shouldLog = percent - lastLoggedPercent >= 5
|
|
|
- if (shouldLog) {
|
|
|
- lastLoggedPercent = Math.floor(percent / 5) * 5
|
|
|
- }
|
|
|
- onStatus('nodejs', `正在下载 Node.js ${targetVersion}... (${downloadedMB}MB / ${totalMB}MB)`, progress, !shouldLog)
|
|
|
- })
|
|
|
- } catch (error) {
|
|
|
- logger.installError('下载 Node.js 失败', error)
|
|
|
- throw new Error(`下载 Node.js 失败: ${(error as Error).message}`)
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 执行 msi 静默安装
|
|
|
- onStatus('nodejs', `${STATUS_MESSAGES.INSTALLING} Node.js ${targetVersion}...`, 55)
|
|
|
- logger.installInfo(`开始安装 Node.js: ${installerPath}`)
|
|
|
-
|
|
|
- try {
|
|
|
- // msiexec 静默安装参数
|
|
|
- const installArgs = ['/i', installerPath, '/qn', '/norestart']
|
|
|
- if (customPath) {
|
|
|
- installArgs.push(`INSTALLDIR="${customPath}"`)
|
|
|
- }
|
|
|
-
|
|
|
- await sudoExec(`msiexec ${installArgs.join(' ')}`)
|
|
|
-
|
|
|
- // 清理安装包
|
|
|
- try {
|
|
|
- await fs.promises.unlink(installerPath)
|
|
|
- } catch {
|
|
|
- // 忽略清理失败
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- logger.installError('安装 Node.js 失败', error)
|
|
|
- throw error
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 使用完整路径,避免 PATH 未生效的问题
|
|
|
- const npmCmd = getNpmPath()
|
|
|
-
|
|
|
- // 仅当使用国内镜像下载时,才配置 npm 国内镜像
|
|
|
- const { mirror: nodejsMirror } = getNodejsMirrorConfig()
|
|
|
- if (nodejsMirror === 'npmmirror') {
|
|
|
- const npmConfigCmd = `${npmCmd} config set registry ${NPM_REGISTRY}`
|
|
|
- onStatus('nodejs', `${STATUS_MESSAGES.CONFIGURING} npm 镜像: ${npmConfigCmd}`, 70)
|
|
|
- try {
|
|
|
- await executeCommand(npmCmd, ['config', 'set', 'registry', NPM_REGISTRY], false)
|
|
|
- } catch (error) {
|
|
|
- logger.installWarn('配置 npm 镜像失败(npm 可能还未加入 PATH)', error)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 可选安装 pnpm
|
|
|
- if (installPnpm) {
|
|
|
- onStatus('nodejs', `${STATUS_MESSAGES.INSTALLING} pnpm...`, 80)
|
|
|
- await executeCommand(npmCmd, ['install', '-g', 'pnpm'], true)
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 获取刷新后的 PATH,确保能找到刚安装的 pnpm
|
|
|
- const { getRefreshedPath } = await import('./utils')
|
|
|
- const refreshedPath = await getRefreshedPath()
|
|
|
- const execEnv = { ...process.env, PATH: refreshedPath }
|
|
|
-
|
|
|
- // 运行 pnpm setup 配置全局 bin 目录
|
|
|
- onStatus('nodejs', `${STATUS_MESSAGES.CONFIGURING} pnpm 全局目录...`, 88)
|
|
|
- try {
|
|
|
- await execa('pnpm', ['setup'], { env: execEnv, shell: platform === 'win32' })
|
|
|
- logger.installInfo('pnpm setup 完成')
|
|
|
- } catch (error) {
|
|
|
- logger.installWarn('pnpm setup 失败', error)
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 配置 pnpm 镜像
|
|
|
- onStatus('nodejs', `${STATUS_MESSAGES.CONFIGURING} pnpm 镜像...`, 95)
|
|
|
- try {
|
|
|
- await execa('pnpm', ['config', 'set', 'registry', NPM_REGISTRY], { env: execEnv, shell: platform === 'win32' })
|
|
|
- } catch (error) {
|
|
|
- logger.installWarn('配置 pnpm 镜像失败', error)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- onStatus('nodejs', STATUS_MESSAGES.COMPLETE, 100)
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // macOS/Linux: 使用包管理器安装
|
|
|
- onStatus('nodejs', `${STATUS_MESSAGES.INSTALLING} Node.js...`, 20)
|
|
|
- const args = getNodeInstallArgs(version)
|
|
|
- await executeCommand(args.command, args.args, true)
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 使用完整路径,避免 PATH 未生效的问题
|
|
|
- const npmCmd = getNpmPath()
|
|
|
-
|
|
|
- // 仅当使用国内镜像下载时,才配置 npm 国内镜像
|
|
|
- const { mirror: nodejsMirrorMac } = getNodejsMirrorConfig()
|
|
|
- if (nodejsMirrorMac === 'npmmirror') {
|
|
|
- const npmConfigCmd = `${npmCmd} config set registry ${NPM_REGISTRY}`
|
|
|
- onStatus('nodejs', `${STATUS_MESSAGES.CONFIGURING} npm 镜像: ${npmConfigCmd}`, 50)
|
|
|
- try {
|
|
|
- await executeCommand(npmCmd, ['config', 'set', 'registry', NPM_REGISTRY], false)
|
|
|
- } catch (error) {
|
|
|
- logger.installWarn('配置 npm 镜像失败(npm 可能还未加入 PATH)', error)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 可选安装 pnpm
|
|
|
- if (installPnpm) {
|
|
|
- onStatus('nodejs', `${STATUS_MESSAGES.INSTALLING} pnpm...`, 70)
|
|
|
- await executeCommand(npmCmd, ['install', '-g', 'pnpm'], true)
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 获取刷新后的 PATH,确保能找到刚安装的 pnpm
|
|
|
- const { getRefreshedPath } = await import('./utils')
|
|
|
- const refreshedPath = await getRefreshedPath()
|
|
|
- const execEnv = { ...process.env, PATH: refreshedPath }
|
|
|
-
|
|
|
- // 运行 pnpm setup 配置全局 bin 目录
|
|
|
- onStatus('nodejs', `${STATUS_MESSAGES.CONFIGURING} pnpm 全局目录...`, 82)
|
|
|
- try {
|
|
|
- await execa('pnpm', ['setup'], { env: execEnv })
|
|
|
- logger.installInfo('pnpm setup 完成')
|
|
|
- } catch (error) {
|
|
|
- logger.installWarn('pnpm setup 失败', error)
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 配置 pnpm 镜像
|
|
|
- onStatus('nodejs', `${STATUS_MESSAGES.CONFIGURING} pnpm 镜像...`, 90)
|
|
|
- try {
|
|
|
- await execa('pnpm', ['config', 'set', 'registry', NPM_REGISTRY], { env: execEnv })
|
|
|
- } catch (error) {
|
|
|
- logger.installWarn('配置 pnpm 镜像失败', error)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- onStatus('nodejs', STATUS_MESSAGES.COMPLETE, 100)
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 单独安装 pnpm (需要 Node.js 已安装)
|
|
|
- * @param onStatus 状态回调
|
|
|
- */
|
|
|
-export async function installPnpm(onStatus: StatusCallback): Promise<void> {
|
|
|
- resetCancelState()
|
|
|
-
|
|
|
- const platform = os.platform() as Platform
|
|
|
-
|
|
|
- // 检查 Node.js 是否已安装
|
|
|
- const nodeInstalled = await checkInstalled('nodejs')
|
|
|
- if (!nodeInstalled.installed) {
|
|
|
- throw new Error('请先安装 Node.js')
|
|
|
- }
|
|
|
-
|
|
|
- // 刷新系统环境变量,确保能找到刚安装的 Node.js
|
|
|
- const { getRefreshedPath } = await import('./utils')
|
|
|
- await getRefreshedPath()
|
|
|
-
|
|
|
- // 使用完整路径,避免 PATH 未生效的问题
|
|
|
- const npmCmd = getNpmPath()
|
|
|
-
|
|
|
- // 检查 npm 命令是否存在
|
|
|
- if (platform === 'win32' && !fs.existsSync(npmCmd)) {
|
|
|
- throw new Error(`未找到 npm 命令: ${npmCmd},请确保 Node.js 已正确安装`)
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 安装 pnpm,需要管理员权限进行全局安装
|
|
|
- const installCommand = `${npmCmd} install -g pnpm`
|
|
|
- onStatus('pnpm', `正在安装 pnpm...`, 20)
|
|
|
- onStatus('pnpm', `执行命令: ${installCommand}`, 25)
|
|
|
- try {
|
|
|
- await executeCommand(npmCmd, ['install', '-g', 'pnpm'], true)
|
|
|
- } catch (error) {
|
|
|
- // 提取更有意义的错误信息,过滤乱码
|
|
|
- const execaError = error as { message?: string; stderr?: string; stdout?: string; shortMessage?: string; exitCode?: number }
|
|
|
- let errorMessage = `npm install -g pnpm 失败`
|
|
|
- if (execaError.exitCode !== undefined) {
|
|
|
- errorMessage += ` (退出码: ${execaError.exitCode})`
|
|
|
- }
|
|
|
- // 过滤乱码字符,只保留可读字符
|
|
|
- if (execaError.stderr) {
|
|
|
- const stderrClean = execaError.stderr.replace(/[^\x20-\x7E\u4e00-\u9fa5\n\r]/g, '').trim()
|
|
|
- if (stderrClean) {
|
|
|
- errorMessage += `\n${stderrClean}`
|
|
|
- }
|
|
|
- }
|
|
|
- if (execaError.stdout) {
|
|
|
- const stdoutClean = execaError.stdout.replace(/[^\x20-\x7E\u4e00-\u9fa5\n\r]/g, '').trim()
|
|
|
- if (stdoutClean) {
|
|
|
- errorMessage += `\n${stdoutClean}`
|
|
|
- }
|
|
|
- }
|
|
|
- logger.installError('安装 pnpm 失败', error)
|
|
|
- throw new Error(errorMessage)
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 重新刷新 PATH,确保能找到刚安装的 pnpm
|
|
|
- const pnpmRefreshedPath = await getRefreshedPath()
|
|
|
-
|
|
|
- // 使用 pnpm 的完整路径,因为刚安装的 pnpm 可能还不在 PATH 中
|
|
|
- const pnpmCmd = getPnpmPath()
|
|
|
-
|
|
|
- // 构建环境变量,将 pnpm 所在目录添加到 PATH 开头
|
|
|
- const pnpmDir = path.dirname(pnpmCmd)
|
|
|
- const execEnv = { ...process.env, PATH: `${pnpmDir}${platform === 'win32' ? ';' : ':'}${pnpmRefreshedPath}` }
|
|
|
-
|
|
|
- // 运行 pnpm setup 配置全局 bin 目录
|
|
|
- onStatus('pnpm', `${STATUS_MESSAGES.CONFIGURING} pnpm 全局目录...`, 60)
|
|
|
- try {
|
|
|
- await execa(pnpmCmd, ['setup'], { env: execEnv, shell: platform === 'win32' })
|
|
|
- logger.installInfo('pnpm setup 完成')
|
|
|
- } catch (error) {
|
|
|
- logger.installWarn('pnpm setup 失败', error)
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 配置 pnpm 镜像
|
|
|
- onStatus('pnpm', `${STATUS_MESSAGES.CONFIGURING} pnpm 镜像...`, 80)
|
|
|
- try {
|
|
|
- await execa(pnpmCmd, ['config', 'set', 'registry', NPM_REGISTRY], { env: execEnv, shell: platform === 'win32' })
|
|
|
- } catch (error) {
|
|
|
- logger.installWarn('配置 pnpm 镜像失败', error)
|
|
|
- }
|
|
|
-
|
|
|
- onStatus('pnpm', STATUS_MESSAGES.COMPLETE, 100)
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 安装 VS Code (Windows 使用下载安装包)
|
|
|
- * @param version 版本号
|
|
|
- * @param onStatus 状态回调
|
|
|
- * @param customPath 自定义安装路径 (仅 Windows 支持)
|
|
|
- */
|
|
|
-export async function installVscode(version = 'stable', onStatus: StatusCallback, customPath?: string): Promise<void> {
|
|
|
- resetCancelState()
|
|
|
-
|
|
|
- const platform = os.platform() as Platform
|
|
|
-
|
|
|
- // Windows: 直接下载安装包
|
|
|
- if (platform === 'win32' && version !== 'insiders') {
|
|
|
- // 确定目标版本
|
|
|
- let targetVersion = version
|
|
|
- if (version === 'stable' || !version || !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
|
- // 如果是 stable 或无效版本,使用备用版本
|
|
|
- targetVersion = FALLBACK_VERSIONS.vscode
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 下载安装包
|
|
|
- const downloadUrl = getVSCodeDownloadUrl(targetVersion)
|
|
|
- const tempDir = getTempDir()
|
|
|
- const installerPath = path.join(tempDir, `VSCodeUserSetup-${targetVersion}.exe`)
|
|
|
-
|
|
|
- onStatus('vscode', `正在下载 VS Code ${targetVersion}...`, 10)
|
|
|
- logger.installInfo(`开始下载 VS Code: ${downloadUrl}`)
|
|
|
-
|
|
|
- try {
|
|
|
- let lastLoggedPercent = 0
|
|
|
- await downloadFile(downloadUrl, installerPath, (downloaded, total, percent) => {
|
|
|
- const progress = 10 + Math.round(percent * 0.5)
|
|
|
- const downloadedMB = (downloaded / 1024 / 1024).toFixed(1)
|
|
|
- const totalMB = (total / 1024 / 1024).toFixed(1)
|
|
|
- // 每次都更新进度条,但只在每 5% 时记录日志
|
|
|
- const shouldLog = percent - lastLoggedPercent >= 5
|
|
|
- if (shouldLog) {
|
|
|
- lastLoggedPercent = Math.floor(percent / 5) * 5
|
|
|
- }
|
|
|
- onStatus('vscode', `正在下载 VS Code ${targetVersion}... (${downloadedMB}MB / ${totalMB}MB)`, progress, !shouldLog)
|
|
|
- })
|
|
|
- } catch (error) {
|
|
|
- logger.installError('下载 VS Code 失败', error)
|
|
|
- throw new Error(`下载 VS Code 失败: ${(error as Error).message}`)
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 执行静默安装
|
|
|
- onStatus('vscode', `${STATUS_MESSAGES.INSTALLING} VS Code ${targetVersion}...`, 65)
|
|
|
- logger.installInfo(`开始安装 VS Code: ${installerPath}`)
|
|
|
-
|
|
|
- try {
|
|
|
- // VS Code 安装程序支持的静默安装参数
|
|
|
- // /VERYSILENT: 完全静默安装
|
|
|
- // /NORESTART: 不重启
|
|
|
- // /MERGETASKS: 指定安装任务
|
|
|
- const installArgs = ['/VERYSILENT', '/NORESTART', '/MERGETASKS=!runcode,addcontextmenufiles,addcontextmenufolders,associatewithfiles,addtopath']
|
|
|
- if (customPath) {
|
|
|
- installArgs.push(`/DIR=${customPath}`)
|
|
|
- }
|
|
|
-
|
|
|
- await sudoExec(`"${installerPath}" ${installArgs.join(' ')}`)
|
|
|
-
|
|
|
- // 清理安装包
|
|
|
- try {
|
|
|
- await fs.promises.unlink(installerPath)
|
|
|
- } catch {
|
|
|
- // 忽略清理失败
|
|
|
- }
|
|
|
-
|
|
|
- onStatus('vscode', STATUS_MESSAGES.COMPLETE, 100)
|
|
|
- } catch (error) {
|
|
|
- logger.installError('安装 VS Code 失败', error)
|
|
|
- throw error
|
|
|
- }
|
|
|
-
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // macOS/Linux 或 insiders 版本: 使用包管理器安装
|
|
|
- onStatus('vscode', `${STATUS_MESSAGES.INSTALLING} VS Code...`, 30)
|
|
|
- const args = getVSCodeInstallArgs(version)
|
|
|
- await executeCommand(args.command, args.args, true)
|
|
|
- onStatus('vscode', STATUS_MESSAGES.COMPLETE, 100)
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 安装 Git (Windows 直接下载安装)
|
|
|
- * @param version 版本号
|
|
|
- * @param onStatus 状态回调
|
|
|
- * @param customPath 自定义安装路径 (仅 Windows 支持)
|
|
|
- */
|
|
|
-export async function installGit(version = 'stable', onStatus: StatusCallback, customPath?: string): Promise<void> {
|
|
|
- resetCancelState()
|
|
|
-
|
|
|
- const platform = os.platform() as Platform
|
|
|
-
|
|
|
- // Windows: 直接从镜像下载安装
|
|
|
- if (platform === 'win32' && version !== 'mingit' && version !== 'lfs') {
|
|
|
- // 如果是 stable,尝试获取最新版本号
|
|
|
- let targetVersion = version
|
|
|
- if (version === 'stable') {
|
|
|
- onStatus('git', '正在获取最新版本...', 5)
|
|
|
- try {
|
|
|
- const response = await fetch('https://api.github.com/repos/git-for-windows/git/releases/latest', {
|
|
|
- headers: {
|
|
|
- 'Accept': 'application/vnd.github.v3+json',
|
|
|
- 'User-Agent': 'ApqInstaller'
|
|
|
- },
|
|
|
- signal: AbortSignal.timeout(5000) // 5秒超时
|
|
|
- })
|
|
|
- if (response.ok) {
|
|
|
- const release = await response.json() as { tag_name: string }
|
|
|
- const match = release.tag_name.match(/^v?(\d+\.\d+\.\d+)/)
|
|
|
- if (match) {
|
|
|
- targetVersion = match[1]
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- logger.installWarn('获取最新 Git 版本失败,使用备用版本', error)
|
|
|
- // 使用备用版本
|
|
|
- targetVersion = FALLBACK_VERSIONS.git
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 下载安装包
|
|
|
- const downloadUrl = getGitDownloadUrl(targetVersion)
|
|
|
- const tempDir = getTempDir()
|
|
|
- const installerPath = path.join(tempDir, `Git-${targetVersion}-installer.exe`)
|
|
|
-
|
|
|
- onStatus('git', `正在下载 Git ${targetVersion}...`, 10)
|
|
|
- logger.installInfo(`开始下载 Git: ${downloadUrl}`)
|
|
|
-
|
|
|
- try {
|
|
|
- let lastLoggedPercent = 0
|
|
|
- await downloadFile(downloadUrl, installerPath, (downloaded, total, percent) => {
|
|
|
- // 下载进度占 10% - 60%
|
|
|
- const progress = 10 + Math.round(percent * 0.5)
|
|
|
- const downloadedMB = (downloaded / 1024 / 1024).toFixed(1)
|
|
|
- const totalMB = (total / 1024 / 1024).toFixed(1)
|
|
|
- // 每次都更新进度条,但只在每 5% 时记录日志
|
|
|
- const shouldLog = percent - lastLoggedPercent >= 5
|
|
|
- if (shouldLog) {
|
|
|
- lastLoggedPercent = Math.floor(percent / 5) * 5
|
|
|
- }
|
|
|
- onStatus('git', `正在下载 Git ${targetVersion}... (${downloadedMB}MB / ${totalMB}MB)`, progress, !shouldLog)
|
|
|
- })
|
|
|
- } catch (error) {
|
|
|
- logger.installError('下载 Git 失败', error)
|
|
|
- throw new Error(`下载 Git 失败: ${(error as Error).message}`)
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 执行静默安装
|
|
|
- onStatus('git', `${STATUS_MESSAGES.INSTALLING} Git ${targetVersion}...`, 65)
|
|
|
- logger.installInfo(`开始安装 Git: ${installerPath}`)
|
|
|
-
|
|
|
- try {
|
|
|
- // Git 安装程序支持的静默安装参数
|
|
|
- // /VERYSILENT: 完全静默安装
|
|
|
- // /NORESTART: 不重启
|
|
|
- // /NOCANCEL: 不显示取消按钮
|
|
|
- // /SP-: 不显示 "This will install..." 提示
|
|
|
- // /CLOSEAPPLICATIONS: 自动关闭相关应用
|
|
|
- // /DIR: 指定安装目录
|
|
|
- const installArgs = ['/VERYSILENT', '/NORESTART', '/NOCANCEL', '/SP-', '/CLOSEAPPLICATIONS']
|
|
|
- if (customPath) {
|
|
|
- installArgs.push(`/DIR=${customPath}`)
|
|
|
- }
|
|
|
-
|
|
|
- await sudoExec(`"${installerPath}" ${installArgs.join(' ')}`)
|
|
|
-
|
|
|
- // 清理安装包
|
|
|
- try {
|
|
|
- await fs.promises.unlink(installerPath)
|
|
|
- } catch {
|
|
|
- // 忽略清理失败
|
|
|
- }
|
|
|
-
|
|
|
- onStatus('git', STATUS_MESSAGES.COMPLETE, 100)
|
|
|
- } catch (error) {
|
|
|
- logger.installError('安装 Git 失败', error)
|
|
|
- throw error
|
|
|
- }
|
|
|
-
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // Windows 上的特殊版本 (mingit/lfs) 暂不支持
|
|
|
- if (platform === 'win32') {
|
|
|
- throw new Error(`Windows 暂不支持安装 ${version} 版本,请选择具体版本号`)
|
|
|
- }
|
|
|
-
|
|
|
- // macOS/Linux: 使用包管理器安装
|
|
|
- onStatus('git', `${STATUS_MESSAGES.INSTALLING} Git...`, 30)
|
|
|
- const args = getGitInstallArgs(version)
|
|
|
- await executeCommand(args.command, args.args, true)
|
|
|
- onStatus('git', STATUS_MESSAGES.COMPLETE, 100)
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 安装 Claude Code (通过 pnpm 或 npm 全局安装)
|
|
|
- * @param onStatus 状态回调
|
|
|
- */
|
|
|
-export async function installClaudeCode(onStatus: StatusCallback): Promise<void> {
|
|
|
- resetCancelState()
|
|
|
-
|
|
|
- const platform = os.platform() as Platform
|
|
|
-
|
|
|
- onStatus('claudeCode', '正在安装 Claude Code...', 10)
|
|
|
- logger.installInfo('开始安装 Claude Code...')
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 检测是否已安装 pnpm,优先使用 pnpm
|
|
|
- const hasPnpm = await commandExistsWithRefresh('pnpm')
|
|
|
- const pkgManager = hasPnpm ? 'pnpm' : 'npm'
|
|
|
-
|
|
|
- // 获取刷新后的 PATH,确保能找到新安装的命令
|
|
|
- const { getRefreshedPath } = await import('./utils')
|
|
|
- const refreshedPath = await getRefreshedPath()
|
|
|
- const execEnv: Record<string, string> = { ...process.env as Record<string, string>, PATH: refreshedPath }
|
|
|
-
|
|
|
- if (hasPnpm) {
|
|
|
- // 使用 pnpm 安装,需要确保 PNPM_HOME 环境变量已设置
|
|
|
- onStatus('claudeCode', '配置 pnpm 全局目录...', 20)
|
|
|
-
|
|
|
- // 先执行 pnpm setup
|
|
|
- try {
|
|
|
- await execa('pnpm', ['setup'], { env: execEnv, shell: platform === 'win32' })
|
|
|
- logger.installInfo('pnpm setup 完成')
|
|
|
- } catch (error) {
|
|
|
- logger.installWarn('pnpm setup 失败', error)
|
|
|
- }
|
|
|
-
|
|
|
- // 手动设置 PNPM_HOME 环境变量
|
|
|
- if (platform === 'win32') {
|
|
|
- const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
|
|
|
- const pnpmHome = path.join(localAppData, 'pnpm')
|
|
|
- execEnv.PNPM_HOME = pnpmHome
|
|
|
- execEnv.PATH = `${pnpmHome};${execEnv.PATH}`
|
|
|
- logger.installInfo(`设置 PNPM_HOME: ${pnpmHome}`)
|
|
|
- } else {
|
|
|
- const pnpmHome = path.join(os.homedir(), '.local', 'share', 'pnpm')
|
|
|
- execEnv.PNPM_HOME = pnpmHome
|
|
|
- execEnv.PATH = `${pnpmHome}:${execEnv.PATH}`
|
|
|
- logger.installInfo(`设置 PNPM_HOME: ${pnpmHome}`)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- const installArgs = ['install', '-g', '@anthropic-ai/claude-code']
|
|
|
- const fullCommand = `${pkgManager} ${installArgs.join(' ')}`
|
|
|
- onStatus('claudeCode', `执行命令: ${fullCommand}`, 30)
|
|
|
- logger.installInfo(`使用 ${pkgManager} 安装,执行命令: ${fullCommand}`)
|
|
|
-
|
|
|
- try {
|
|
|
- if (hasPnpm) {
|
|
|
- // 使用 pnpm 安装 Claude Code
|
|
|
- await execa('pnpm', installArgs, { env: execEnv, shell: platform === 'win32' })
|
|
|
- } else {
|
|
|
- // 使用 npm 安装
|
|
|
- const npmCmd = getNpmPath()
|
|
|
- await executeCommand(npmCmd, installArgs, true)
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- logger.installError('安装 Claude Code 失败', error)
|
|
|
- throw error
|
|
|
- }
|
|
|
-
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- // 验证安装
|
|
|
- onStatus('claudeCode', '验证安装...', 90)
|
|
|
- const claudeExists = await commandExistsWithRefresh('claude')
|
|
|
-
|
|
|
- if (claudeExists) {
|
|
|
- const { getCommandVersionWithRefresh } = await import('./utils')
|
|
|
- const version = await getCommandVersionWithRefresh('claude', ['--version'])
|
|
|
- logger.installInfo(`Claude Code 安装成功: ${version}`)
|
|
|
- } else {
|
|
|
- // 即使验证失败,安装可能已成功,只是 PATH 还没生效
|
|
|
- logger.installInfo('Claude Code 安装完成,但验证失败(可能需要重启终端)')
|
|
|
- }
|
|
|
-
|
|
|
- onStatus('claudeCode', STATUS_MESSAGES.COMPLETE, 100)
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 安装 Claude Code for VS Code 扩展
|
|
|
- * 复用 ipc-handlers.ts 中的 installVscodeExtension 函数
|
|
|
- * @param _onStatus 状态回调(已由 installVscodeExtension 内部处理)
|
|
|
- */
|
|
|
-export async function installClaudeCodeExt(_onStatus: StatusCallback): Promise<void> {
|
|
|
- resetCancelState()
|
|
|
- checkCancelled()
|
|
|
-
|
|
|
- const extensionId = 'anthropic.claude-code'
|
|
|
- const result = await installVscodeExtension(extensionId)
|
|
|
-
|
|
|
- if (!result.success) {
|
|
|
- throw new Error(result.error || 'VS Code 插件安装失败')
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 卸载软件
|
|
|
- */
|
|
|
-export async function uninstallSoftware(software: SoftwareType): Promise<boolean> {
|
|
|
- // 只有 nodejs, vscode, git 支持卸载
|
|
|
- if (software !== 'nodejs' && software !== 'vscode' && software !== 'git') {
|
|
|
- logger.installWarn(`${software} 不支持通过此方式卸载`)
|
|
|
- return false
|
|
|
- }
|
|
|
- try {
|
|
|
- const args = getUninstallArgs(software)
|
|
|
- await executeCommand(args.command, args.args, true)
|
|
|
- logger.installInfo(`${software} 卸载成功`)
|
|
|
- return true
|
|
|
- } catch (error) {
|
|
|
- logger.installError(`${software} 卸载失败`, error)
|
|
|
- return false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 一键安装所有软件
|
|
|
- * 复用单独安装函数,避免代码重复
|
|
|
- */
|
|
|
-export async function installAll(options: InstallOptions, onStatus: StatusCallback): Promise<string[]> {
|
|
|
- resetCancelState()
|
|
|
-
|
|
|
- onStatus('all', '正在准备安装...', 5)
|
|
|
-
|
|
|
- const {
|
|
|
- installNodejs: doNodejs = true,
|
|
|
- nodejsVersion = 'lts',
|
|
|
- nodejsPath,
|
|
|
- installPnpm: doPnpm = true,
|
|
|
- installVscode: doVscode = true,
|
|
|
- vscodeVersion = 'stable',
|
|
|
- vscodePath,
|
|
|
- installGit: doGit = true,
|
|
|
- gitVersion = 'stable',
|
|
|
- gitPath,
|
|
|
- installClaudeCode: doClaudeCode = false,
|
|
|
- installClaudeCodeExt: doClaudeCodeExt = false
|
|
|
- } = options
|
|
|
-
|
|
|
- // 计算总步骤数
|
|
|
- const steps =
|
|
|
- (doNodejs ? 1 : 0) +
|
|
|
- (doPnpm && !doNodejs ? 1 : 0) + // 如果安装 Node.js,pnpm 会一起安装
|
|
|
- (doGit ? 1 : 0) +
|
|
|
- (doVscode ? 1 : 0) +
|
|
|
- (doClaudeCode ? 1 : 0) +
|
|
|
- (doClaudeCodeExt ? 1 : 0)
|
|
|
- let currentStep = 0
|
|
|
- const getProgress = (): number => Math.round(((currentStep + 0.5) / steps) * 100)
|
|
|
-
|
|
|
- // 创建包装的状态回调,将子安装的状态转发到 'all'
|
|
|
- const createWrappedStatus = (stepName: string): StatusCallback => {
|
|
|
- return (_software, message, _progress, skipLog) => {
|
|
|
- onStatus('all', `[${stepName}] ${message}`, getProgress(), skipLog)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 安装 Node.js(包含 pnpm)
|
|
|
- if (doNodejs) {
|
|
|
- checkCancelled()
|
|
|
- const wrappedStatus = createWrappedStatus('Node.js')
|
|
|
- await installNodejs(nodejsVersion, doPnpm, wrappedStatus, nodejsPath)
|
|
|
- currentStep++
|
|
|
- } else if (doPnpm) {
|
|
|
- // 如果不安装 Node.js 但需要安装 pnpm,单独安装 pnpm
|
|
|
- checkCancelled()
|
|
|
- const wrappedStatus = createWrappedStatus('pnpm')
|
|
|
- await installPnpm(wrappedStatus)
|
|
|
- currentStep++
|
|
|
- }
|
|
|
-
|
|
|
- // 安装 Git (Claude Code 运行时需要 Git)
|
|
|
- if (doGit) {
|
|
|
- checkCancelled()
|
|
|
- const wrappedStatus = createWrappedStatus('Git')
|
|
|
- await installGit(gitVersion, wrappedStatus, gitPath)
|
|
|
- currentStep++
|
|
|
- }
|
|
|
-
|
|
|
- // 安装 Claude Code (需要 Node.js 和 Git,应在 Git 之后安装)
|
|
|
- if (doClaudeCode) {
|
|
|
- checkCancelled()
|
|
|
- const wrappedStatus = createWrappedStatus('Claude Code')
|
|
|
- await installClaudeCode(wrappedStatus)
|
|
|
- currentStep++
|
|
|
- }
|
|
|
-
|
|
|
- // 安装 VS Code (Claude Code Ext 需要 VS Code)
|
|
|
- if (doVscode) {
|
|
|
- checkCancelled()
|
|
|
- const wrappedStatus = createWrappedStatus('VS Code')
|
|
|
- await installVscode(vscodeVersion, wrappedStatus, vscodePath)
|
|
|
- currentStep++
|
|
|
-
|
|
|
- // 如果需要安装扩展,等待 VS Code CLI 准备就绪
|
|
|
- if (doClaudeCodeExt) {
|
|
|
- onStatus('all', '[VS Code] 等待 VS Code CLI 准备就绪...', getProgress(), true)
|
|
|
- await new Promise(resolve => setTimeout(resolve, 3000))
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 安装 Claude Code for VS Code 扩展 (需要 VS Code 和 Claude Code)
|
|
|
- if (doClaudeCodeExt) {
|
|
|
- checkCancelled()
|
|
|
- const wrappedStatus = createWrappedStatus('Claude Code 插件')
|
|
|
- try {
|
|
|
- await installClaudeCodeExt(wrappedStatus)
|
|
|
- } catch (error) {
|
|
|
- // 插件安装失败不阻止整体流程
|
|
|
- logger.installWarn('Claude Code for VS Code 扩展安装失败', error)
|
|
|
- }
|
|
|
- currentStep++
|
|
|
- }
|
|
|
-
|
|
|
- // 生成完成消息
|
|
|
- const installed: string[] = []
|
|
|
- if (doNodejs) installed.push('Node.js')
|
|
|
- if (doPnpm) installed.push('pnpm')
|
|
|
- if (doVscode) installed.push('VS Code')
|
|
|
- if (doGit) installed.push('Git')
|
|
|
- if (doClaudeCode) installed.push('Claude Code')
|
|
|
- if (doClaudeCodeExt) installed.push('Claude Code 插件')
|
|
|
-
|
|
|
- return installed
|
|
|
-}
|
|
|
-
|
|
|
-export {
|
|
|
- executeCommand,
|
|
|
- getNodeInstallArgs,
|
|
|
- getVSCodeInstallArgs,
|
|
|
- getGitInstallArgs,
|
|
|
- getNpmPath,
|
|
|
- getPnpmPath
|
|
|
-}
|