Merge pull request #111 from y574444354/feat/upgrade-fix
fix(update): enhance failure diagnostics and prevent claude symlink deletion
This commit is contained in:
commit
d1f5ee72a5
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user