Files
claude-code/src/commands/install.tsx
claude-code-best 5b1a52b8e0 更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)
* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files)

纯格式化:移除分号、React Compiler import、import 多行展开。
修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-2): 格式化 commands (79 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-5): 格式化 components其余 + hooks + tools (232 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md

- README.md: 大幅重写,更详细版本历史和配置示例
- Run.ps1: 新增 Windows 启动脚本
- TODO.md: 新增包完成清单
- V6.md: 删除(架构重构规划已不适用)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复以前的问题

* fix: 修复 login 面板的问题

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 23:24:27 +08:00

328 lines
10 KiB
TypeScript

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 '../components/design-system/StatusIcon.js'
import { Box, render, Text } from '../ink.js'
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 (
<Box flexDirection="column" gap={0} marginBottom={1}>
<Box>
<Text color="warning">
<StatusIcon status="warning" withSpace />
Setup notes:
</Text>
</Box>
{messages.map((message, index) => (
<Box key={index} marginLeft={2}>
<Text dimColor> {message}</Text>
</Box>
))}
</Box>
)
}
function Install({ onDone, force, target }: InstallProps): React.ReactNode {
const [state, setState] = useState<InstallState>({ 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 (
<Box flexDirection="column" marginTop={1}>
{state.type === 'checking' && (
<Text color="claude">Checking installation status...</Text>
)}
{state.type === 'cleaning-npm' && (
<Text color="warning">Cleaning up old npm installations...</Text>
)}
{state.type === 'installing' && (
<Text color="claude">
Installing Claude Code native build {state.version}...
</Text>
)}
{state.type === 'setting-up' && (
<Text color="claude">Setting up launcher and shell integration...</Text>
)}
{state.type === 'set-up' && <SetupNotes messages={state.messages} />}
{state.type === 'success' && (
<Box flexDirection="column" gap={1}>
<Box>
<StatusIcon status="success" withSpace />
<Text color="success" bold>
Claude Code successfully installed!
</Text>
</Box>
<Box marginLeft={2} flexDirection="column" gap={1}>
{state.version !== 'current' && (
<Box>
<Text dimColor>Version: </Text>
<Text color="claude">{state.version}</Text>
</Box>
)}
<Box>
<Text dimColor>Location: </Text>
<Text color="text">{getInstallationPath()}</Text>
</Box>
</Box>
<Box marginLeft={2} flexDirection="column" gap={1}>
<Box marginTop={1}>
<Text dimColor>Next: Run </Text>
<Text color="claude" bold>
claude --help
</Text>
<Text dimColor> to get started</Text>
</Box>
</Box>
{state.setupMessages && <SetupNotes messages={state.setupMessages} />}
</Box>
)}
{state.type === 'error' && (
<Box flexDirection="column" gap={1}>
<Box>
<StatusIcon status="error" withSpace />
<Text color="error">Installation failed</Text>
</Box>
<Text color="error">{state.message}</Text>
<Box marginTop={1}>
<Text dimColor>Try running with --force to override checks</Text>
</Box>
</Box>
)}
</Box>
)
}
// 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(
<Install
onDone={(result, options) => {
unmount()
onDone(result, options)
}}
force={force}
target={target}
/>,
)
},
}