import { homedir } from 'node:os' import { join } from 'node:path' import React, { useEffect, useState } from 'react' import type { CommandResultDisplay } from 'src/commands.js' import { logEvent } from 'src/services/analytics/index.js' import { StatusIcon } from '@anthropic/ink' import { Box, wrappedRender as render, Text } from '@anthropic/ink' import { logForDebugging } from '../utils/debug.js' import { env } from '../utils/env.js' import { errorMessage } from '../utils/errors.js' import { checkInstall, cleanupNpmInstallations, cleanupShellAliases, installLatest, } from '../utils/nativeInstaller/index.js' import { getInitialSettings, updateSettingsForSource, } from '../utils/settings/settings.js' interface InstallProps { onDone: (result: string, options?: { display?: CommandResultDisplay }) => void force?: boolean target?: string // 'latest', 'stable', or version like '1.0.34' } type InstallState = | { type: 'checking' } | { type: 'cleaning-npm' } | { type: 'installing'; version: string } | { type: 'setting-up' } | { type: 'set-up'; messages: string[] } | { type: 'success'; version: string; setupMessages?: string[] } | { type: 'error'; message: string; warnings?: string[] } function getInstallationPath(): string { const isWindows = env.platform === 'win32' const homeDir = homedir() if (isWindows) { // Convert to Windows-style path const windowsPath = join(homeDir, '.local', 'bin', 'claude.exe') // Replace forward slashes with backslashes for Windows display return windowsPath.replace(/\//g, '\\') } return '~/.local/bin/claude' } function SetupNotes({ messages }: { messages: string[] }): React.ReactNode { if (messages.length === 0) return null return ( Setup notes: {messages.map((message, index) => ( • {message} ))} ) } function Install({ onDone, force, target }: InstallProps): React.ReactNode { const [state, setState] = useState({ type: 'checking' }) useEffect(() => { async function run() { try { logForDebugging( `Install: Starting installation process (force=${force}, target=${target})`, ) // Install native build first const channelOrVersion = target || getInitialSettings()?.autoUpdatesChannel || 'latest' setState({ type: 'installing', version: channelOrVersion }) // Pass force flag to trigger reinstall even if up to date logForDebugging( `Install: Calling installLatest(channelOrVersion=${channelOrVersion}, forceReinstall=${force})`, ) const result = await installLatest(channelOrVersion, force) logForDebugging( `Install: installLatest returned version=${result.latestVersion}, wasUpdated=${result.wasUpdated}, lockFailed=${result.lockFailed}`, ) // Check specifically for lock failure if (result.lockFailed) { throw new Error( 'Could not install - another process is currently installing Claude. Please try again in a moment.', ) } // If we couldn't get the version, there might be an issue if (!result.latestVersion) { logForDebugging( 'Install: Failed to retrieve version information during install', { level: 'error' }, ) } if (!result.wasUpdated) { logForDebugging('Install: Already up to date') } // Set up launcher and shell integration setState({ type: 'setting-up' }) const setupMessages = await checkInstall(true) logForDebugging( `Install: Setup launcher completed with ${setupMessages.length} messages`, ) if (setupMessages.length > 0) { setupMessages.forEach(msg => logForDebugging(`Install: Setup message: ${msg.message}`), ) } // Now that native installation succeeded, clean up old npm installations logForDebugging( 'Install: Cleaning up npm installations after successful install', ) const { removed, errors, warnings } = await cleanupNpmInstallations() if (removed > 0) { logForDebugging(`Cleaned up ${removed} npm installation(s)`) } if (errors.length > 0) { logForDebugging(`Cleanup errors: ${errors.join(', ')}`) // Continue despite cleanup errors - native install already succeeded } // Clean up old shell aliases const aliasMessages = await cleanupShellAliases() if (aliasMessages.length > 0) { logForDebugging( `Shell alias cleanup: ${aliasMessages.map(m => m.message).join('; ')}`, ) } // Log success event logEvent('tengu_claude_install_command', { has_version: result.latestVersion ? 1 : 0, forced: force ? 1 : 0, }) // If user explicitly specified a channel, save it to settings if (target === 'latest' || target === 'stable') { updateSettingsForSource('userSettings', { autoUpdatesChannel: target, }) logForDebugging( `Install: Saved autoUpdatesChannel=${target} to user settings`, ) } // Combine all warning/info messages (convert SetupMessage to string) const allWarnings = [...warnings, ...aliasMessages.map(m => m.message)] // Check if there were any setup errors or notes if (setupMessages.length > 0) { setState({ type: 'set-up', messages: setupMessages.map(m => m.message), }) // Still mark as success but show both setup messages and cleanup warnings setTimeout(setState, 2000, { type: 'success' as const, version: result.latestVersion || 'current', setupMessages: [ ...setupMessages.map(m => m.message), ...allWarnings, ], }) } else { // No setup messages, go straight to success (but still show cleanup warnings if any) logForDebugging('Install: Shell PATH already configured') setState({ type: 'success', version: result.latestVersion || 'current', setupMessages: allWarnings.length > 0 ? allWarnings : undefined, }) } } catch (error) { logForDebugging(`Install command failed: ${error}`, { level: 'error', }) setState({ type: 'error', message: errorMessage(error), }) } } void run() }, [force, target]) useEffect(() => { if (state.type === 'success') { // Give success message time to render before exiting setTimeout( onDone, 2000, 'Claude Code installation completed successfully', { display: 'system' as const, }, ) } else if (state.type === 'error') { // Give error message time to render before exiting setTimeout(onDone, 3000, 'Claude Code installation failed', { display: 'system' as const, }) } }, [state, onDone]) return ( {state.type === 'checking' && ( Checking installation status... )} {state.type === 'cleaning-npm' && ( Cleaning up old npm installations... )} {state.type === 'installing' && ( Installing Claude Code native build {state.version}... )} {state.type === 'setting-up' && ( Setting up launcher and shell integration... )} {state.type === 'set-up' && } {state.type === 'success' && ( Claude Code successfully installed! {state.version !== 'current' && ( Version: {state.version} )} Location: {getInstallationPath()} Next: Run claude --help to get started {state.setupMessages && } )} {state.type === 'error' && ( Installation failed {state.message} Try running with --force to override checks )} ) } // This is only used from cli.tsx, not as a slash command export const install = { type: 'local-jsx' as const, name: 'install', description: 'Install Claude Code native build', argumentHint: '[options]', async call( onDone: ( result: string, options?: { display?: CommandResultDisplay }, ) => void, _context: unknown, args: string[], ) { // Parse arguments const force = args.includes('--force') const nonFlagArgs = args.filter(arg => !arg.startsWith('--')) const target = nonFlagArgs[0] // 'latest', 'stable', or version like '1.0.34' const { unmount } = await render( { unmount() onDone(result, options) }} force={force} target={target} />, ) }, }