Doctor.tsx
1 import React, { useCallback, useEffect, useState } from 'react' 2 import { Box, Text, useInput } from 'ink' 3 import { Select } from '@inkjs/ui' 4 import { getTheme } from '../utils/theme.js' 5 import { ConfigureNpmPrefix } from './ConfigureNpmPrefix.js' 6 import { platform } from 'process' 7 import { 8 checkNpmPermissions, 9 getDefaultNpmPrefix, 10 getPermissionsCommand, 11 } from '../utils/autoUpdater.js' 12 import { saveGlobalConfig, getGlobalConfig } from '../utils/config.js' 13 import { logEvent } from '../services/statsig.js' 14 import { PRODUCT_NAME } from '../constants/product.js' 15 import { PressEnterToContinue } from '../components/PressEnterToContinue.js' 16 17 type Props = { 18 onDone: () => void 19 doctorMode?: boolean 20 } 21 22 type Option = { 23 label: string 24 value: 'auto' | 'manual' | 'ignore' 25 description: string 26 } 27 28 export function Doctor({ onDone, doctorMode = false }: Props): React.ReactNode { 29 const [hasPermissions, setHasPermissions] = useState<boolean | null>(null) 30 const [npmPrefix, setNpmPrefix] = useState<string | null>(null) 31 const [selectedOption, setSelectedOption] = useState<Option['value'] | null>( 32 null, 33 ) 34 const [customPrefix, setCustomPrefix] = useState<string>( 35 getDefaultNpmPrefix(), 36 ) 37 const theme = getTheme() 38 const [showingPermissionsMessage, setShowingPermissionsMessage] = 39 useState(false) 40 41 const options: Option[] = [ 42 { 43 label: `Manually fix permissions on current npm prefix (Recommended)`, 44 value: 'manual', 45 description: 46 platform === 'win32' 47 ? 'Uses icacls to grant write permissions' 48 : 'Uses sudo to change ownership', 49 }, 50 { 51 label: 'Create new npm prefix directory', 52 value: 'auto', 53 description: 54 'Creates a new directory for global npm packages in your home directory', 55 }, 56 { 57 label: 'Skip configuration until next session', 58 value: 'ignore', 59 description: 'Skip this warning (you will be reminded again later)', 60 }, 61 ] 62 63 const checkPermissions = useCallback(async () => { 64 const result = await checkNpmPermissions() 65 logEvent('tengu_auto_updater_permissions_check', { 66 hasPermissions: result.hasPermissions.toString(), 67 npmPrefix: result.npmPrefix ?? 'null', 68 }) 69 setHasPermissions(result.hasPermissions) 70 if (result.npmPrefix) { 71 setNpmPrefix(result.npmPrefix) 72 } 73 if (result.hasPermissions) { 74 const config = getGlobalConfig() 75 saveGlobalConfig({ 76 ...config, 77 autoUpdaterStatus: 'enabled', 78 }) 79 if (!doctorMode) { 80 onDone() 81 } 82 } 83 }, [onDone, doctorMode]) 84 85 useEffect(() => { 86 logEvent('tengu_auto_updater_config_start', {}) 87 checkPermissions() 88 }, [checkPermissions]) 89 90 useInput( 91 (_input, key) => { 92 if ( 93 (showingPermissionsMessage || 94 (doctorMode && hasPermissions === true)) && 95 key.return 96 ) { 97 onDone() 98 } 99 }, 100 { 101 isActive: 102 showingPermissionsMessage || (doctorMode && hasPermissions === true), 103 }, 104 ) 105 106 if (hasPermissions === null) { 107 return ( 108 <Box paddingX={1} paddingTop={1}> 109 <Text color={theme.secondaryText}>Checking npm permissions…</Text> 110 </Box> 111 ) 112 } 113 114 if (hasPermissions === true) { 115 if (doctorMode) { 116 return ( 117 <Box flexDirection="column" gap={1} paddingX={1} paddingTop={1}> 118 <Text color={theme.success}>✓ npm permissions: OK</Text> 119 <Text>Your installation is healthy and ready for auto-updates.</Text> 120 <PressEnterToContinue /> 121 </Box> 122 ) 123 } 124 return ( 125 <Box paddingX={1} paddingTop={1}> 126 <Text color={theme.success}>✓ Auto-updates enabled</Text> 127 </Box> 128 ) 129 } 130 return ( 131 <Box 132 borderColor={theme.permission} 133 borderStyle="round" 134 flexDirection="column" 135 gap={1} 136 paddingX={1} 137 paddingTop={1} 138 > 139 <Text bold color={theme.permission}> 140 Enable automatic updates? 141 </Text> 142 <Text> 143 {PRODUCT_NAME} can't update itself because it doesn't have 144 permissions. Do you want to fix this to get automatic updates? 145 </Text> 146 <Box flexDirection="column"> 147 {!selectedOption && ( 148 <Box marginLeft={2}> 149 <Text>Select an option below to fix the permissions issue:</Text> 150 <Select 151 options={options} 152 onChange={(value: string) => { 153 if ( 154 value !== 'auto' && 155 value !== 'manual' && 156 value !== 'ignore' 157 ) 158 return 159 setSelectedOption(value) 160 161 // Log option selection 162 logEvent('tengu_auto_updater_config_option_selected', { 163 option: value as 'auto' | 'manual' | 'ignore', 164 npmPrefix: npmPrefix ?? 'null', 165 }) 166 167 if (value === 'manual') { 168 const config = getGlobalConfig() 169 saveGlobalConfig({ 170 ...config, 171 autoUpdaterStatus: 'not_configured', 172 }) 173 setShowingPermissionsMessage(true) 174 } else if (value === 'ignore') { 175 const config = getGlobalConfig() 176 saveGlobalConfig({ 177 ...config, 178 autoUpdaterStatus: 'not_configured', 179 }) 180 onDone() 181 } 182 }} 183 /> 184 </Box> 185 )} 186 187 {selectedOption === 'auto' && ( 188 <Box marginLeft={2}> 189 <ConfigureNpmPrefix 190 customPrefix={customPrefix} 191 onCustomPrefixChange={setCustomPrefix} 192 onSuccess={checkPermissions} 193 onCancel={onDone} 194 /> 195 </Box> 196 )} 197 198 {selectedOption === 'manual' && ( 199 <> 200 <Box marginLeft={4} flexDirection="column"> 201 <Text>Run this command in your terminal:</Text> 202 <Box flexDirection="row" gap={1}> 203 <Text color={theme.warning}> 204 {getPermissionsCommand(npmPrefix ?? '')} 205 </Text> 206 </Box> 207 <Box flexDirection="row" gap={1}> 208 <Text color={theme.suggestion}> 209 After running the command, restart {PRODUCT_NAME} 210 </Text> 211 </Box> 212 </Box> 213 <PressEnterToContinue /> 214 </> 215 )} 216 </Box> 217 </Box> 218 ) 219 }