Revert "Fix npm global install permission error by using user-level prefix"

This commit is contained in:
LinYushen
2026-01-16 12:36:25 +08:00
committed by GitHub
parent 384be3e39d
commit ddad9ef21d
3 changed files with 10 additions and 144 deletions

View File

@@ -4,12 +4,10 @@
*/
import { spawn } from 'child_process'
import { platform } from 'os'
import * as fs from 'fs'
import type { BrowserWindow } from 'electron'
import { IPC_CHANNELS } from '../../shared/ipc-channels'
import type { InstallStep, InstallResult } from '../../shared/electron-api'
import { commandExists } from './agent-check'
import { getEnhancedPath, getNpmUserPrefix } from './path'
interface InstallOptions {
window: BrowserWindow
@@ -33,8 +31,8 @@ function spawnWithProgress(
useShell: boolean = false
): Promise<{ success: boolean; error?: string }> {
return new Promise((resolve) => {
// Use enhanced PATH that includes user-level npm global directory
const enhancedPath = getEnhancedPath()
// Ensure common paths are in PATH for npm, node, etc.
const enhancedPath = `/usr/local/bin:/opt/homebrew/bin:${process.env.PATH || ''}`
console.log(`[agent-install] Spawning: ${command} ${args.join(' ')} (shell: ${useShell})`)
@@ -111,19 +109,14 @@ async function installClaudeCLI(
}
/**
* Install claude-code-acp via npm to user-level directory
* Install claude-code-acp via npm
*/
async function installClaudeCodeACP(
onProgress: (message: string) => void
): Promise<{ success: boolean; error?: string }> {
const prefix = getNpmUserPrefix()
// Ensure the user-level npm global directory exists
await fs.promises.mkdir(prefix, { recursive: true }).catch(() => {})
return spawnWithProgress(
'npm',
['install', '-g', '--prefix', prefix, '@zed-industries/claude-code-acp'],
['install', '-g', '@zed-industries/claude-code-acp'],
onProgress,
true
)
@@ -224,41 +217,21 @@ async function installClaudeCode(options: InstallOptions): Promise<InstallResult
}
/**
* Install Codex CLI via npm to user-level directory
* Install Codex CLI via npm
*/
async function installCodexCLI(
onProgress: (message: string) => void
): Promise<{ success: boolean; error?: string }> {
const prefix = getNpmUserPrefix()
// Ensure the user-level npm global directory exists
await fs.promises.mkdir(prefix, { recursive: true }).catch(() => {})
return spawnWithProgress(
'npm',
['install', '-g', '--prefix', prefix, '@openai/codex'],
onProgress,
true
)
return spawnWithProgress('npm', ['install', '-g', '@openai/codex'], onProgress, true)
}
/**
* Install codex-acp via npm to user-level directory
* Install codex-acp via npm
*/
async function installCodexACP(
onProgress: (message: string) => void
): Promise<{ success: boolean; error?: string }> {
const prefix = getNpmUserPrefix()
// Ensure the user-level npm global directory exists
await fs.promises.mkdir(prefix, { recursive: true }).catch(() => {})
return spawnWithProgress(
'npm',
['install', '-g', '--prefix', prefix, '@zed-industries/codex-acp'],
onProgress,
true
)
return spawnWithProgress('npm', ['install', '-g', '@zed-industries/codex-acp'], onProgress, true)
}
/**
@@ -387,9 +360,9 @@ export async function installAgent(options: InstallOptions): Promise<InstallResu
/**
* Format installation error messages for user display
*/
export function formatInstallError(error: string): string {
function formatInstallError(error: string): string {
if (error.includes('EACCES') || error.includes('permission denied')) {
return 'Permission denied. Please run: chmod -R u+w ~/.npm-global'
return 'Permission denied. Please check your permissions and try again.'
}
if (error.includes('Could not resolve host') || error.includes('ENOTFOUND')) {
return 'Network error. Please check your internet connection and try again.'

View File

@@ -10,23 +10,11 @@ import { homedir } from 'node:os'
export function getEnhancedPath(): string {
const home = homedir()
const customPaths = [
`${home}/.npm-global/bin`, // User-level npm global directory
`${home}/.opencode/bin`,
`${home}/.claude/local/bin`,
`${home}/.claude/local`,
`${home}/.bun/bin`,
`${home}/.local/bin`,
'/opt/homebrew/bin',
'/usr/local/bin'
]
return `${customPaths.join(':')}:${process.env.PATH || ''}`
}
/**
* Get the user-level npm global prefix directory
* Used for installing npm packages without requiring sudo
*/
export function getNpmUserPrefix(): string {
const home = homedir()
return `${home}/.npm-global`
}

View File

@@ -1,95 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { homedir } from 'node:os'
// Mock child_process
vi.mock('child_process', () => ({
spawn: vi.fn()
}))
// Mock fs
vi.mock('fs', () => ({
promises: {
mkdir: vi.fn().mockResolvedValue(undefined)
}
}))
// Mock os module
vi.mock('os', () => ({
platform: vi.fn().mockReturnValue('darwin')
}))
// Mock path utilities
vi.mock('../../../../src/main/utils/path', () => ({
getEnhancedPath: vi
.fn()
.mockReturnValue(
'/Users/test/.npm-global/bin:/Users/test/.opencode/bin:/usr/local/bin:/usr/bin:/bin'
),
getNpmUserPrefix: vi.fn().mockReturnValue('/Users/test/.npm-global')
}))
// Mock agent-check
vi.mock('../../../../src/main/utils/agent-check', () => ({
commandExists: vi.fn().mockResolvedValue({ exists: false })
}))
import { formatInstallError } from '../../../../src/main/utils/agent-install'
import { getNpmUserPrefix, getEnhancedPath } from '../../../../src/main/utils/path'
describe('agent-install', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getNpmUserPrefix', () => {
it('should return user-level npm global directory path', () => {
const prefix = getNpmUserPrefix()
expect(prefix).toBe('/Users/test/.npm-global')
})
})
describe('getEnhancedPath', () => {
it('should include npm-global/bin in PATH', () => {
const path = getEnhancedPath()
expect(path).toContain('.npm-global/bin')
})
})
describe('formatInstallError', () => {
it('should format EACCES permission errors', () => {
const error = 'npm ERR! code EACCES\nnpm ERR! syscall mkdir'
const result = formatInstallError(error)
expect(result).toBe('Permission denied. Please run: chmod -R u+w ~/.npm-global')
})
it('should format permission denied errors', () => {
const error = 'Error: EACCES: permission denied, mkdir /some/path'
const result = formatInstallError(error)
expect(result).toBe('Permission denied. Please run: chmod -R u+w ~/.npm-global')
})
it('should format network errors with ENOTFOUND', () => {
const error = 'npm ERR! code ENOTFOUND\nnpm ERR! network'
const result = formatInstallError(error)
expect(result).toBe('Network error. Please check your internet connection and try again.')
})
it('should format network errors with host resolution', () => {
const error = 'Could not resolve host: registry.npmjs.org'
const result = formatInstallError(error)
expect(result).toBe('Network error. Please check your internet connection and try again.')
})
it('should format command not found errors', () => {
const error = 'spawn npm ENOENT'
const result = formatInstallError(error)
expect(result).toBe('Command not found. Please ensure required tools are installed.')
})
it('should pass through unknown errors unchanged', () => {
const error = 'Some unknown error message'
const result = formatInstallError(error)
expect(result).toBe('Some unknown error message')
})
})
})