Merge pull request #111 from y574444354/feat/upgrade-fix

fix(update): enhance failure diagnostics and prevent claude symlink deletion
This commit is contained in:
linkai0924 2026-05-15 16:23:55 +08:00 committed by GitHub
commit d1f5ee72a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 269 additions and 56 deletions

View File

@ -2,7 +2,7 @@ import chalk from 'chalk'
import { logEvent } from 'src/services/analytics/index.js'
import {
getLatestVersion,
type InstallStatus,
type InstallResult,
installGlobalPackage,
} from 'src/utils/autoUpdater.js'
import { regenerateCompletionCache } from 'src/utils/completionCache.js'
@ -356,21 +356,21 @@ export async function update() {
logForDebugging(`update: Update method determined: ${updateMethodName}`)
logForDebugging(`update: useLocalUpdate: ${useLocalUpdate}`)
let status: InstallStatus
let result: InstallResult
if (useLocalUpdate) {
logForDebugging(
'update: Calling installOrUpdateClaudePackage() for local update',
)
status = await installOrUpdateClaudePackage(channel)
result = await installOrUpdateClaudePackage(channel)
} else {
logForDebugging('update: Calling installGlobalPackage() for global update')
status = await installGlobalPackage()
result = await installGlobalPackage()
}
logForDebugging(`update: Installation status: ${status}`)
logForDebugging(`update: Installation status: ${result.status}`)
switch (status) {
switch (result.status) {
case 'success':
writeToStdout(
chalk.green(
@ -381,40 +381,59 @@ export async function update() {
break
case 'no_permissions':
process.stderr.write(
'Error: Insufficient permissions to install update\n',
chalk.red('Error: Insufficient permissions to install update') + '\n',
)
if (result.errorCategory) {
process.stderr.write(
chalk.yellow(`Cause: ${result.errorCategory}`) + '\n',
)
}
if (result.suggestion) {
process.stderr.write('\n')
process.stderr.write(result.suggestion + '\n')
}
if (useLocalUpdate) {
process.stderr.write('Try manually updating with:\n')
process.stderr.write('\n')
process.stderr.write('Or try manually updating with:\n')
process.stderr.write(
` cd ~/.claude/local && npm update ${MACRO.PACKAGE_URL}\n`,
)
} else {
process.stderr.write('Try running with sudo or fix npm permissions\n')
process.stderr.write(
'Or consider using native installation with: claude install\n',
)
}
await gracefulShutdown(1)
break
case 'install_failed':
process.stderr.write('Error: Failed to install update\n')
process.stderr.write(chalk.red('Error: Failed to install update') + '\n')
if (result.errorCategory) {
process.stderr.write(
chalk.yellow(`Cause: ${result.errorCategory}`) + '\n',
)
}
if (result.suggestion) {
process.stderr.write('\n')
process.stderr.write(result.suggestion + '\n')
}
if (result.npmStderr) {
process.stderr.write('\n')
process.stderr.write('Raw error output:\n')
process.stderr.write(result.npmStderr + '\n')
}
if (useLocalUpdate) {
process.stderr.write('Try manually updating with:\n')
process.stderr.write('\n')
process.stderr.write('Or try manually updating with:\n')
process.stderr.write(
` cd ~/.claude/local && npm update ${MACRO.PACKAGE_URL}\n`,
)
} else {
process.stderr.write(
'Or consider using native installation with: claude install\n',
)
}
await gracefulShutdown(1)
break
case 'in_progress':
process.stderr.write(
'Error: Another instance is currently performing an update\n',
chalk.red('Error: Another instance is currently performing an update') +
'\n',
)
process.stderr.write('Please wait and try again later\n')
process.stderr.write('Please wait a moment and try again.\n')
process.stderr.write('If the problem persists, delete the lock file:\n')
process.stderr.write(' rm ~/.claude/.update.lock\n')
await gracefulShutdown(1)
break
}

View File

@ -11,6 +11,7 @@ import {
type AutoUpdaterResult,
getLatestVersion,
getMaxVersion,
type InstallResult,
type InstallStatus,
installGlobalPackage,
shouldSkipVersion,
@ -122,19 +123,19 @@ export function AutoUpdater({
}
// Choose the appropriate update method based on what's actually running
let installStatus: InstallStatus;
let installResult: InstallResult;
let updateMethod: 'local' | 'global';
if (installationType === 'npm-local') {
// Use local update for local installations
logForDebugging('AutoUpdater: Using local update method');
updateMethod = 'local';
installStatus = await installOrUpdateClaudePackage(channel);
installResult = await installOrUpdateClaudePackage(channel);
} else if (installationType === 'npm-global') {
// Use global update for global installations
logForDebugging('AutoUpdater: Using global update method');
updateMethod = 'global';
installStatus = await installGlobalPackage();
installResult = await installGlobalPackage();
} else if (installationType === 'native') {
// This shouldn't happen - native should use NativeAutoUpdater
logForDebugging('AutoUpdater: Unexpected native installation in non-native updater');
@ -147,12 +148,14 @@ export function AutoUpdater({
updateMethod = isMigrated ? 'local' : 'global';
if (isMigrated) {
installStatus = await installOrUpdateClaudePackage(channel);
installResult = await installOrUpdateClaudePackage(channel);
} else {
installStatus = await installGlobalPackage();
installResult = await installGlobalPackage();
}
}
const installStatus: InstallStatus = installResult.status;
onChangeIsUpdating(false);
if (installStatus === 'success') {

View File

@ -38,6 +38,16 @@ export type InstallStatus =
| 'install_failed'
| 'in_progress'
export type InstallResult = {
status: InstallStatus
/** Human-readable error category for targeted troubleshooting */
errorCategory?: string
/** Suggested fix based on the detected error */
suggestion?: string
/** Raw npm stderr for advanced debugging */
npmStderr?: string
}
export type AutoUpdaterResult = {
version: string | null
status: InstallStatus
@ -452,9 +462,137 @@ export async function getVersionHistory(limit: number): Promise<string[]> {
}
}
/**
* Analyze npm/bun install stderr to produce a targeted error category and suggestion.
*/
export function analyzeNpmError(
stderr: string,
stdout: string,
): { category: string; suggestion: string } {
const combined = `${stderr} ${stdout}`.toLowerCase()
if (
combined.includes('eacces') ||
combined.includes('eperm') ||
combined.includes('permission denied') ||
combined.includes('operation not permitted')
) {
return {
category: 'Permission denied',
suggestion:
'The package manager lacks write permission to the global install directory.\n' +
'Options:\n' +
' 1. Fix npm permissions: https://docs.npmjs.com/resolving-eacces-permissions-errors\n' +
' 2. Run with sudo (not recommended long-term)\n' +
' 3. Switch to local installation: csc install --local\n' +
' 4. Switch to native installation: csc install',
}
}
if (
combined.includes('erofs') ||
combined.includes('read-only file system')
) {
return {
category: 'Read-only file system',
suggestion:
'The target directory is read-only.\n' +
'Try installing to a user-writable prefix:\n' +
' npm config set prefix ~/.npm-global\n' +
'Or switch to local/native installation.',
}
}
if (
combined.includes('etimedout') ||
combined.includes('econnrefused') ||
combined.includes('enotfound') ||
combined.includes('network timeout') ||
combined.includes('socket hang up')
) {
return {
category: 'Network error',
suggestion:
'Unable to reach the npm registry.\n' +
'Try:\n' +
' • Check your internet connection\n' +
' • If behind a proxy, configure npm: npm config set proxy http://...\n' +
' • Try again later (registry may be temporarily unavailable)',
}
}
if (
combined.includes('e404') ||
combined.includes('not found') ||
combined.includes('no matching version') ||
combined.includes('package not found')
) {
return {
category: 'Package or version not found',
suggestion:
`The package "${MACRO.PACKAGE_URL}" or the requested version could not be found.\n` +
'Possible causes:\n' +
' • Scoped package requires npm login: npm whoami\n' +
' • The version tag does not exist yet\n' +
' • Corporate registry mirror is missing this package',
}
}
if (
combined.includes('ebusy') ||
combined.includes('eexist') ||
combined.includes('resource busy') ||
combined.includes('file exists')
) {
return {
category: 'File locked or already exists',
suggestion:
'A file is locked or already exists.\n' +
'Try:\n' +
' • Close other CoStrict / Node processes\n' +
' • Delete the lock file and retry: rm ~/.claude/.update.lock',
}
}
if (combined.includes('elifecycle') || combined.includes('postinstall')) {
return {
category: 'Install script failed',
suggestion:
'A post-install script failed.\n' +
'Try:\n' +
' • npm install -g --ignore-scripts (workaround, skips lifecycle hooks)\n' +
' • Check that your Node.js version is supported',
}
}
if (combined.includes('enoent') || combined.includes('no such file')) {
return {
category: 'Missing file or directory',
suggestion:
'A required file or directory is missing.\n' +
'Try reinstalling:\n' +
' npm uninstall -g ' +
MACRO.PACKAGE_URL +
' && npm install -g ' +
MACRO.PACKAGE_URL,
}
}
// Fallback
return {
category: 'Installation failed',
suggestion:
'The package manager returned an unrecognized error.\n' +
'You can try:\n' +
' • Run the install command manually to see full output\n' +
' • Clear npm cache: npm cache clean --force\n' +
' • Switch to native installation: csc install',
}
}
export async function installGlobalPackage(
specificVersion?: string | null,
): Promise<InstallStatus> {
): Promise<InstallResult> {
if (!(await acquireLock())) {
logError(
new AutoUpdaterError('Another process is currently installing an update'),
@ -465,7 +603,7 @@ export async function installGlobalPackage(
currentVersion:
MACRO.VERSION as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
return 'in_progress'
return { status: 'in_progress' }
}
try {
@ -477,23 +615,30 @@ export async function installGlobalPackage(
currentVersion:
MACRO.VERSION as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
console.error(`
Error: Windows NPM detected in WSL
You're running CoStrict in WSL but using the Windows NPM installation from /mnt/c/.
This configuration is not supported for updates.
To fix this issue:
1. Install Node.js within your Linux distribution: e.g. sudo apt install nodejs npm
2. Make sure Linux NPM is in your PATH before the Windows version
3. Try updating again with 'csc update'
`)
return 'install_failed'
return {
status: 'install_failed',
errorCategory: 'Windows NPM in WSL',
suggestion:
'You are running CoStrict in WSL but using the Windows NPM installation from /mnt/c/.\n' +
'To fix:\n' +
' 1. Install Node.js inside WSL: sudo apt install nodejs npm\n' +
' 2. Ensure Linux npm appears first in PATH\n' +
' 3. Retry: csc update',
}
}
const { hasPermissions } = await checkGlobalInstallPermissions()
if (!hasPermissions) {
return 'no_permissions'
return {
status: 'no_permissions',
errorCategory: 'Insufficient permissions',
suggestion:
'No write access to the global npm prefix.\n' +
'Try:\n' +
' 1. Fix npm permissions: https://docs.npmjs.com/resolving-eacces-permissions-errors\n' +
' 2. Run: sudo csc update (temporary workaround)\n' +
' 3. Switch to native installation: csc install',
}
}
// Use specific version if provided, otherwise use latest
@ -510,11 +655,20 @@ To fix this issue:
{ cwd: homedir() },
)
if (installResult.code !== 0) {
const { category, suggestion } = analyzeNpmError(
installResult.stderr,
installResult.stdout,
)
const error = new AutoUpdaterError(
`Failed to install new version of claude: ${installResult.stdout} ${installResult.stderr}`,
`Failed to install new version: ${category}\nstdout: ${installResult.stdout}\nstderr: ${installResult.stderr}`,
)
logError(error)
return 'install_failed'
return {
status: 'install_failed',
errorCategory: category,
suggestion,
npmStderr: installResult.stderr,
}
}
// Set installMethod to 'global' to track npm global installations
@ -523,7 +677,7 @@ To fix this issue:
installMethod: 'global',
}))
return 'success'
return { status: 'success' }
} finally {
// Ensure we always release the lock
await releaseLock()

View File

@ -4,6 +4,7 @@
import { access, chmod, writeFile } from 'fs/promises'
import { join } from 'path'
import { type InstallResult, analyzeNpmError } from './autoUpdater.js'
import { type ReleaseChannel, saveGlobalConfig } from './config.js'
import { getClaudeConfigHomeDir } from './envUtils.js'
import { getErrnoCode } from './errors.js'
@ -97,11 +98,17 @@ export async function ensureLocalPackageEnvironment(): Promise<boolean> {
export async function installOrUpdateClaudePackage(
channel: ReleaseChannel,
specificVersion?: string | null,
): Promise<'in_progress' | 'success' | 'install_failed'> {
): Promise<InstallResult> {
try {
// First ensure the environment is set up
if (!(await ensureLocalPackageEnvironment())) {
return 'install_failed'
return {
status: 'install_failed',
errorCategory: 'Failed to set up local install directory',
suggestion:
'Could not create ~/.claude/local directory or write package.json.\n' +
'Check disk space and permissions.',
}
}
// Use specific version if provided, otherwise use channel tag
@ -117,11 +124,20 @@ export async function installOrUpdateClaudePackage(
)
if (result.code !== 0) {
const { category, suggestion } = analyzeNpmError(
result.stderr,
result.stdout,
)
const error = new Error(
`Failed to install Claude CLI package: ${result.stderr}`,
`Failed to install Claude CLI package: ${category}\nstderr: ${result.stderr}`,
)
logError(error)
return result.code === 190 ? 'in_progress' : 'install_failed'
return {
status: result.code === 190 ? 'in_progress' : 'install_failed',
errorCategory: category,
suggestion,
npmStderr: result.stderr,
}
}
// Set installMethod to 'local' to prevent npm permission warnings
@ -130,10 +146,20 @@ export async function installOrUpdateClaudePackage(
installMethod: 'local',
}))
return 'success'
return { status: 'success' }
} catch (error) {
logError(error)
return 'install_failed'
return {
status: 'install_failed',
errorCategory: 'Unexpected error during local install',
suggestion:
'An unexpected error occurred.\n' +
'Try:\n' +
' • Manually update: cd ~/.claude/local && npm update ' +
MACRO.PACKAGE_URL +
'\n' +
' • Switch to native installation: csc install',
}
}
}

View File

@ -1456,22 +1456,33 @@ async function isNpmSymlink(executablePath: string): Promise<boolean> {
/**
* Remove the claude symlink from the executable directory
* This is used when switching away from native installation
* Will only remove if it's a native binary symlink, not npm-managed JS files
* This is used when switching away from native installation.
* Will only remove if it's a symlink pointing into our managed versions directory,
* ensuring we don't delete user-installed claude binaries from other sources.
*/
export async function removeInstalledSymlink(): Promise<void> {
const dirs = getBaseDirectories()
try {
// Check if this is an npm-managed installation
if (await isNpmSymlink(dirs.executable)) {
const linkStats = await lstat(dirs.executable)
if (!linkStats.isSymbolicLink()) {
logForDebugging(`Skipping removal of ${dirs.executable} - not a symlink`)
return
}
// Resolve symlink target to absolute path
const rawTarget = await readlink(dirs.executable)
const targetPath = resolve(dirname(dirs.executable), rawTarget)
// Only remove if it points into our managed versions directory
// This prevents deleting a user-installed claude from Homebrew, manually, etc.
if (!targetPath.startsWith(dirs.versions)) {
logForDebugging(
`Skipping removal of ${dirs.executable} - appears to be npm-managed`,
`Skipping removal of ${dirs.executable} - target ${targetPath} is not in ${dirs.versions}`,
)
return
}
// It's a native binary symlink, safe to remove
await unlink(dirs.executable)
logForDebugging(`Removed claude symlink at ${dirs.executable}`)
} catch (error) {