claude-code/commands/install.tsx

300 lines
38 KiB
TypeScript
Raw Permalink Normal View History

import { c as _c } from "react/compiler-runtime";
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(t0) {
const $ = _c(5);
const {
messages
} = t0;
if (messages.length === 0) {
return null;
}
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = <Box><Text color="warning"><StatusIcon status="warning" withSpace={true} />Setup notes:</Text></Box>;
$[0] = t1;
} else {
t1 = $[0];
}
let t2;
if ($[1] !== messages) {
t2 = messages.map(_temp);
$[1] = messages;
$[2] = t2;
} else {
t2 = $[2];
}
let t3;
if ($[3] !== t2) {
t3 = <Box flexDirection="column" gap={0} marginBottom={1}>{t1}{t2}</Box>;
$[3] = t2;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
function _temp(message, index) {
return <Box key={index} marginLeft={2}><Text dimColor={true}> {message}</Text></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_0 => m_0.message)];
// Check if there were any setup errors or notes
if (setupMessages.length > 0) {
setState({
type: 'set-up',
messages: setupMessages.map(m_1 => m_1.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_2 => m_2.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} />);
}
};
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJob21lZGlyIiwiam9pbiIsIlJlYWN0IiwidXNlRWZmZWN0IiwidXNlU3RhdGUiLCJDb21tYW5kUmVzdWx0RGlzcGxheSIsImxvZ0V2ZW50IiwiU3RhdHVzSWNvbiIsIkJveCIsInJlbmRlciIsIlRleHQiLCJsb2dGb3JEZWJ1Z2dpbmciLCJlbnYiLCJlcnJvck1lc3NhZ2UiLCJjaGVja0luc3RhbGwiLCJjbGVhbnVwTnBtSW5zdGFsbGF0aW9ucyIsImNsZWFudXBTaGVsbEFsaWFzZXMiLCJpbnN0YWxsTGF0ZXN0IiwiZ2V0SW5pdGlhbFNldHRpbmdzIiwidXBkYXRlU2V0dGluZ3NGb3JTb3VyY2UiLCJJbnN0YWxsUHJvcHMiLCJvbkRvbmUiLCJyZXN1bHQiLCJvcHRpb25zIiwiZGlzcGxheSIsImZvcmNlIiwidGFyZ2V0IiwiSW5zdGFsbFN0YXRlIiwidHlwZSIsInZlcnNpb24iLCJtZXNzYWdlcyIsInNldHVwTWVzc2FnZXMiLCJtZXNzYWdlIiwid2FybmluZ3MiLCJnZXRJbnN0YWxsYXRpb25QYXRoIiwiaXNXaW5kb3dzIiwicGxhdGZvcm0iLCJob21lRGlyIiwid2luZG93c1BhdGgiLCJyZXBsYWNlIiwiU2V0dXBOb3RlcyIsInQwIiwiJCIsIl9jIiwibGVuZ3RoIiwidDEiLCJTeW1ib2wiLCJmb3IiLCJ0MiIsIm1hcCIsIl90ZW1wIiwidDMiLCJpbmRleCIsIkluc3RhbGwiLCJSZWFjdE5vZGUiLCJzdGF0ZSIsInNldFN0YXRlIiwicnVuIiwiY2hhbm5lbE9yVmVyc2lvbiIsImF1dG9VcGRhdGVzQ2hhbm5lbCIsImxhdGVzdFZlcnNpb24iLCJ3YXNVcGRhdGVkIiwibG9ja0ZhaWxlZCIsIkVycm9yIiwibGV2ZWwiLCJmb3JFYWNoIiwibXNnIiwicmVtb3ZlZCIsImVycm9ycyIsImFsaWFzTWVzc2FnZXMiLCJtIiwiaGFzX3ZlcnNpb24iLCJmb3JjZWQiLCJhbGxXYXJuaW5ncyIsInNldFRpbWVvdXQiLCJjb25zdCIsInVuZGVmaW5lZCIsImVycm9yIiwiaW5zdGFsbCIsIm5hbWUiLCJkZXNjcmlwdGlvbiIsImFyZ3VtZW50SGludCIsImNhbGwiLCJfY29udGV4dCIsImFyZ3MiLCJpbmNsdWRlcyIsIm5vbkZsYWdBcmdzIiwiZmlsdGVyIiwiYXJnIiwic3RhcnRzV2l0aCIsInVubW91bnQiXSwic291cmNlcyI6WyJpbnN0YWxsLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBob21lZGlyIH0gZnJvbSAnbm9kZTpvcydcbmltcG9ydCB7IGpvaW4gfSBmcm9tICdub2RlOnBhdGgnXG5pbXBvcnQgUmVhY3QsIHsgdXNlRWZmZWN0LCB1c2VTdGF0ZSB9IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHR5cGUgeyBDb21tYW5kUmVzdWx0RGlzcGxheSB9IGZyb20gJ3NyYy9jb21tYW5kcy5qcydcbmltcG9ydCB7IGxvZ0V2ZW50IH0gZnJvbSAnc3JjL3NlcnZpY2VzL2FuYWx5dGljcy9pbmRleC5qcydcbmltcG9ydCB7IFN0YXR1c0ljb24gfSBmcm9tICcuLi9jb21wb25lbnRzL2Rlc2lnbi1zeXN0ZW0vU3RhdHVzSWNvbi5qcydcbmltcG9ydCB7IEJveCwgcmVuZGVyLCBUZXh0IH0gZnJvbSAnLi4vaW5rLmpzJ1xuaW1wb3J0IHsgbG9nRm9yRGVidWdnaW5nIH0gZnJvbSAnLi4vdXRpbHMvZGVidWcuanMnXG5pbXBvcnQgeyBlbnYgfSBmcm9tICcuLi91dGlscy9lbnYuanMnXG5pbXBvcnQgeyBlcnJvck1lc3NhZ2UgfSBmcm9tICcuLi91dGlscy9lcnJvcnMuanMnXG5pbXBvcnQge1xuICBjaGVja0luc3RhbGwsXG4gIGNsZWFudXBOcG1JbnN0YWxsYXRpb25zLFxuICBjbGVhbnVwU2hlbGxBbGlhc2VzLFxuICBpbnN0YWxsTGF0ZXN0LFxufSBmcm9tICcuLi91dGlscy9uYXRpdmVJbnN0YWxsZXIvaW5kZXguanMnXG5pbXBvcnQge1xuICBnZXRJbml0aWFsU2V0dGluZ3MsXG4gIHVwZGF0ZVNldHRpbmdzRm9yU291cmNlLFxufSBmcm9tICcuLi91dGlscy9zZXR0aW5ncy9zZXR0aW5ncy5qcydcblxuaW50ZXJmYWNlIEluc3RhbGxQcm9wcyB7XG4gIG9uRG9uZTogKHJlc3VsdDogc3RyaW5nLCBvcHRpb25zPzogeyBkaXNwbGF5PzogQ29tbWFuZFJlc3VsdERpc3BsYXkgfSkgPT4gdm9pZFxuICBmb3JjZT86IGJvb2xlYW5cbiAgdGFyZ2V0Pzogc3RyaW5nIC8vICdsYXRlc3QnLCAnc3RhYmxlJywgb3IgdmVyc2lvbiBsaWtlICcxLjAuMzQnXG59XG5cbnR5cGUgSW5zdGFsbFN0YXRlID1cbiAgfCB7IHR5cGU6ICdjaGVja2luZycgfVxuICB8IHsgdHlwZTogJ2NsZWFuaW5nLW5wbScgfVxuICB8IHsgdHlwZTogJ2luc3RhbGxpbmcnOyB2ZXJzaW9uOiBzdHJpbmcgfVxuICB8IHsgdHlwZTogJ3NldHRpbmctdXAnIH1cbiAgfCB7IHR5cGU6ICdzZXQtdXAnOyBtZXNzYWdlczogc3RyaW5nW10gfVxuICB8IHsgdHlwZTogJ3N1Y2Nlc3MnOyB2ZXJzaW9uOiBzdHJpbmc7IHNldHVwTWVzc2FnZXM/OiBzdHJpbmdbXSB9XG4gIHwgeyB0eXBlOiAnZXJyb3InOyBtZXNzYWdlOiBzdHJpbmc7IHdhcm5pbmdzPzogc3RyaW5nW10gfVxuXG5mdW5jdGlvbiBnZXRJbnN0YWxsYXRpb25QYXRoKCk6IHN0cmluZyB7XG4gIGNvbnN0IGlzV2luZG93cyA9IGVudi5wbGF0Zm9ybSA9PT0gJ3dpbjMyJ1xuICBjb25zdCBob21lRGlyID0gaG9tZWRpcigpXG5cbiAgaWYgKGlzV2luZG93cykge1xuICAgIC8vIENvbnZlcnQgdG8gV2luZG93cy1zdHlsZSBwYXRoXG4gICAgY29uc3Qgd2luZG93c1BhdGggPSBqb2luKGhvbWVEaXIsICcubG9jYWwnLCAnYmluJywgJ2NsYXVkZS5leGUnKVxuICAgIC8vIFJlcGxhY2UgZm9yd2FyZCBzbGFzaGVzIHdpdGggYmFja3NsYXNoZXMgZm9yIFdpbmRvd3MgZGlzcGxheVxuICAgIHJldHVybiB3aW5kb3dzUGF0aC5yZXBsYWNlKC9cXC8vZywgJ1xcXFwnKVxuICB9XG5cbiAgcmV0dXJuICd+Ly5sb2NhbC9iaW4vY2xhdWRlJ1xufVxuXG5mdW5jdGlvbiBTZXR1cE5vdGVzKHsgbWVzc2FnZXMgfTogeyBtZXNzYWdlczogc3RyaW5nW10gfSk6IFJlYWN0LlJlYWN0Tm9kZSB7XG4gIGlmIChtZXNzYWdlcy5sZW5ndGggPT09IDApIHJldHVybiBudWxsXG5cbiAgcmV0dXJuIChcbiAgICA8Qm94IGZsZXhEaXJlY3Rpb249XCJjb2x1bW5cIiBnYXA9ezB9IG1hcmdpbkJvdHRvbT17MX0+XG4gICAgICA8Qm94PlxuICAgICAgICA8VGV4dCBjb2xvcj1cIndhcm5pbmdcIj5cbiAgICAgICAgICA8U3RhdHVzSWNvbiB