basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import { createDebugLogger, isGitRepository } from '@qwen-code/qwen-code-core';8import * as fs from 'node:fs';9import * as path from 'node:path';10import * as childProcess from 'node:child_process';11 12export enum PackageManager {13 NPM = 'npm',14 YARN = 'yarn',15 PNPM = 'pnpm',16 PNPX = 'pnpx',17 BUN = 'bun',18 BUNX = 'bunx',19 HOMEBREW = 'homebrew',20 STANDALONE = 'standalone',21 NPX = 'npx',22 UNKNOWN = 'unknown',23}24 25const debugLogger = createDebugLogger('INSTALLATION_INFO');26const STANDALONE_UNIX_INSTALLER =27 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.sh';28const STANDALONE_WINDOWS_INSTALLER =29 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.ps1';30 31export interface InstallationInfo {32 packageManager: PackageManager;33 isGlobal: boolean;34 isStandalone?: boolean;35 standaloneDir?: string;36 updateCommand?: string;37 updateMessage?: string;38}39 40export function getInstallationInfo(41 projectRoot: string,42 isAutoUpdateEnabled: boolean,43): InstallationInfo {44 const cliPath = process.argv[1];45 if (!cliPath) {46 return { packageManager: PackageManager.UNKNOWN, isGlobal: false };47 }48 49 try {50 // Normalize path separators to forward slashes for consistent matching.51 const realPath = fs.realpathSync(cliPath).replace(/\\/g, '/');52 const normalizedProjectRoot = projectRoot?.replace(/\\/g, '/');53 const isGit = isGitRepository(process.cwd());54 55 // Check for local git clone first56 if (57 isGit &&58 normalizedProjectRoot &&59 isSamePathOrInside(realPath, normalizedProjectRoot) &&60 !realPath.includes('/node_modules/')61 ) {62 return {63 packageManager: PackageManager.UNKNOWN, // Not managed by a package manager in this sense64 isGlobal: false,65 updateMessage:66 'Running from a local git clone. Please update with "git pull".',67 };68 }69 70 // Check for npx/pnpx71 if (realPath.includes('/.npm/_npx') || realPath.includes('/npm/_npx')) {72 return {73 packageManager: PackageManager.NPX,74 isGlobal: false,75 updateMessage: 'Running via npx, update not applicable.',76 };77 }78 if (realPath.includes('/.pnpm/_pnpx')) {79 return {80 packageManager: PackageManager.PNPX,81 isGlobal: false,82 updateMessage: 'Running via pnpx, update not applicable.',83 };84 }85 86 const standaloneInfo = getStandaloneInstallInfo(87 realPath,88 isAutoUpdateEnabled,89 );90 if (standaloneInfo) {91 return standaloneInfo;92 }93 94 // Check for Homebrew95 if (process.platform === 'darwin') {96 try {97 // We do not support homebrew for now, keep forward compatibility for future use98 childProcess.execSync('brew list -1 | grep -q "^qwen-code$"', {99 stdio: 'ignore',100 });101 return {102 packageManager: PackageManager.HOMEBREW,103 isGlobal: true,104 updateMessage:105 'Installed via Homebrew. Please update with "brew upgrade".',106 };107 } catch (_error) {108 // continue to the next check109 }110 }111 112 // Check for pnpm113 if (realPath.includes('/.pnpm/global')) {114 const updateCommand = 'pnpm add -g @qwen-code/qwen-code@latest';115 return {116 packageManager: PackageManager.PNPM,117 isGlobal: true,118 updateCommand,119 updateMessage: isAutoUpdateEnabled120 ? 'Installed with pnpm. Attempting to automatically update now...'121 : `Please run ${updateCommand} to update`,122 };123 }124 125 // Check for yarn126 if (realPath.includes('/.yarn/global')) {127 const updateCommand = 'yarn global add @qwen-code/qwen-code@latest';128 return {129 packageManager: PackageManager.YARN,130 isGlobal: true,131 updateCommand,132 updateMessage: isAutoUpdateEnabled133 ? 'Installed with yarn. Attempting to automatically update now...'134 : `Please run ${updateCommand} to update`,135 };136 }137 138 // Check for bun139 if (realPath.includes('/.bun/install/cache')) {140 return {141 packageManager: PackageManager.BUNX,142 isGlobal: false,143 updateMessage: 'Running via bunx, update not applicable.',144 };145 }146 if (realPath.includes('/.bun/bin')) {147 const updateCommand = 'bun add -g @qwen-code/qwen-code@latest';148 return {149 packageManager: PackageManager.BUN,150 isGlobal: true,151 updateCommand,152 updateMessage: isAutoUpdateEnabled153 ? 'Installed with bun. Attempting to automatically update now...'154 : `Please run ${updateCommand} to update`,155 };156 }157 158 // Check for local install159 if (160 normalizedProjectRoot &&161 isSamePathOrInside(realPath, `${normalizedProjectRoot}/node_modules`)162 ) {163 let pm = PackageManager.NPM;164 if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) {165 pm = PackageManager.YARN;166 } else if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) {167 pm = PackageManager.PNPM;168 } else if (fs.existsSync(path.join(projectRoot, 'bun.lockb'))) {169 pm = PackageManager.BUN;170 }171 return {172 packageManager: pm,173 isGlobal: false,174 updateMessage:175 "Locally installed. Please update via your project's package.json.",176 };177 }178 179 // Check if the npm global package directory is writable to determine180 // whether `npm install -g` would require sudo.181 const npmPackageDir = path.dirname(path.dirname(realPath));182 let npmPrefixWritable = false;183 try {184 fs.accessSync(npmPackageDir, fs.constants.W_OK);185 npmPrefixWritable = true;186 } catch {187 // Not writable (e.g., /usr/local/lib/node_modules owned by root)188 }189 190 if (!npmPrefixWritable) {191 // The npm global prefix requires sudo. Do NOT silently migrate to the192 // standalone installer here: that swaps in a bundled Node runtime which193 // can be incompatible with the host (e.g. an older glibc), breaking users194 // who were updating fine via npm. Keep npm installs on npm and ask the195 // user to update with sudo instead. No updateCommand is returned so the196 // auto-updater does not attempt an unattended sudo.197 return {198 packageManager: PackageManager.NPM,199 isGlobal: true,200 updateMessage:201 'Update requires sudo. Please run: sudo npm install -g @qwen-code/qwen-code@latest',202 };203 }204 205 const updateCommand = 'npm install -g @qwen-code/qwen-code@latest';206 return {207 packageManager: PackageManager.NPM,208 isGlobal: true,209 updateCommand,210 updateMessage: isAutoUpdateEnabled211 ? 'Installed with npm. Attempting to automatically update now...'212 : `Please run ${updateCommand} to update`,213 };214 } catch (error) {215 debugLogger.error('Failed to detect installation info:', error);216 return { packageManager: PackageManager.UNKNOWN, isGlobal: false };217 }218}219 220function stripTrailingSlashes(value: string): string {221 return value.replace(/\/+$/, '') || '/';222}223 224function isSamePathOrInside(candidate: string, parent: string): boolean {225 const normalizedCandidate = stripTrailingSlashes(candidate);226 const normalizedParent = stripTrailingSlashes(parent);227 if (normalizedParent === '/') {228 return normalizedCandidate === '/' || normalizedCandidate.startsWith('/');229 }230 return (231 normalizedCandidate === normalizedParent ||232 normalizedCandidate.startsWith(`${normalizedParent}/`)233 );234}235 236function getStandaloneInstallInfo(237 realPath: string,238 isAutoUpdateEnabled: boolean,239): InstallationInfo | null {240 const installDir = standaloneInstallDirForCliPath(realPath);241 if (!installDir || !isStandaloneInstallDir(installDir)) {242 return null;243 }244 245 const updateCommand =246 process.platform === 'win32'247 ? `powershell -ExecutionPolicy Bypass -c "irm ${STANDALONE_WINDOWS_INSTALLER} | iex"`248 : `curl -fsSL ${STANDALONE_UNIX_INSTALLER} | bash`;249 250 return {251 packageManager: PackageManager.STANDALONE,252 isGlobal: true,253 isStandalone: true,254 standaloneDir: installDir,255 updateMessage: isAutoUpdateEnabled256 ? 'Standalone install detected. Attempting to automatically update now...'257 : `Standalone install detected. Please rerun the standalone installer to update: ${updateCommand}`,258 };259}260 261function standaloneInstallDirForCliPath(realPath: string): string | null {262 const normalized = realPath.replace(/\\/g, '/');263 const suffix = '/lib/cli.js';264 if (!normalized.endsWith(suffix)) {265 return null;266 }267 return realPath.slice(0, -suffix.length);268}269 270function isStandaloneInstallDir(installDir: string): boolean {271 try {272 const manifestPath = path.join(installDir, 'manifest.json');273 if (!fs.existsSync(manifestPath)) {274 return false;275 }276 277 const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as {278 name?: unknown;279 target?: unknown;280 };281 // Manifest format is produced by writeManifest in create-standalone-package.js.282 if (283 manifest.name !== '@qwen-code/qwen-code' ||284 typeof manifest.target !== 'string' ||285 !isStandaloneTargetForCurrentPlatform(manifest.target)286 ) {287 return false;288 }289 290 const qwenBin =291 process.platform === 'win32'292 ? path.join(installDir, 'bin', 'qwen.cmd')293 : path.join(installDir, 'bin', 'qwen');294 const nodeBin =295 process.platform === 'win32'296 ? path.join(installDir, 'node', 'node.exe')297 : path.join(installDir, 'node', 'bin', 'node');298 299 return (300 fs.existsSync(qwenBin) &&301 fs.existsSync(nodeBin) &&302 isStandaloneRuntimeFile(qwenBin) &&303 isStandaloneRuntimeFile(nodeBin)304 );305 } catch (err) {306 debugLogger.error('Standalone detection failed:', installDir, err);307 return false;308 }309}310 311function isStandaloneTargetForCurrentPlatform(target: string): boolean {312 switch (process.platform) {313 case 'darwin':314 return /^darwin-(arm64|x64)$/.test(target);315 case 'linux':316 return /^linux-(arm64|x64)$/.test(target);317 case 'win32':318 return /^win-(arm64|x64)$/.test(target);319 default:320 return false;321 }322}323 324function isStandaloneRuntimeFile(filePath: string): boolean {325 const stats = fs.lstatSync(filePath);326 if (!stats.isFile() || stats.isSymbolicLink()) {327 return false;328 }329 return process.platform === 'win32' || (stats.mode & 0o111) !== 0;330}331 