2022-11-15 04:26:56 +01:00
package cmd
import (
2022-11-15 05:55:10 +01:00
"bufio"
2022-11-20 02:12:43 +01:00
"context"
"encoding/json"
"errors"
2022-11-15 04:26:56 +01:00
"fmt"
2022-11-20 02:12:43 +01:00
"io"
2022-11-15 04:26:56 +01:00
"os"
2022-11-20 02:05:05 +01:00
"os/exec"
2022-11-20 02:12:43 +01:00
"path"
2023-02-28 03:14:32 +01:00
"runtime"
2022-11-15 05:55:10 +01:00
"strings"
2022-11-20 02:12:43 +01:00
"syscall"
"time"
2022-11-15 04:26:56 +01:00
2022-11-20 02:12:43 +01:00
"github.com/ddworken/hishtory/client/data"
2022-11-15 04:26:56 +01:00
"github.com/ddworken/hishtory/client/hctx"
"github.com/ddworken/hishtory/client/lib"
2022-11-20 02:12:43 +01:00
"github.com/ddworken/hishtory/shared"
2023-10-16 03:30:39 +02:00
"github.com/google/uuid"
2022-11-15 04:26:56 +01:00
"github.com/spf13/cobra"
2023-10-30 05:13:10 +01:00
"gorm.io/gorm"
2022-11-15 04:26:56 +01:00
)
2022-11-15 05:55:10 +01:00
var offlineInit * bool
2024-04-13 18:51:07 +02:00
var forceInit * bool
2022-11-15 05:55:10 +01:00
var offlineInstall * bool
2024-06-14 06:16:17 +02:00
var skipConfigModification * bool
2022-11-15 05:55:10 +01:00
2022-11-15 04:26:56 +01:00
var installCmd = & cobra . Command {
2022-11-15 05:18:22 +01:00
Use : "install" ,
Hidden : true ,
Short : "Copy this binary to ~/.hishtory/ and configure your shell to use it for recording your shell history" ,
2022-11-17 05:28:25 +01:00
Args : cobra . MaximumNArgs ( 1 ) ,
2022-11-15 04:26:56 +01:00
Run : func ( cmd * cobra . Command , args [ ] string ) {
2022-11-17 05:28:25 +01:00
secretKey := ""
if len ( args ) > 0 {
secretKey = args [ 0 ]
}
2024-06-14 06:16:17 +02:00
lib . CheckFatalError ( install ( secretKey , * offlineInstall , * skipConfigModification ) )
2022-11-15 04:26:56 +01:00
if os . Getenv ( "HISHTORY_SKIP_INIT_IMPORT" ) == "" {
db , err := hctx . OpenLocalSqliteDb ( )
lib . CheckFatalError ( err )
2023-10-30 05:13:10 +01:00
count , err := countStoredEntries ( db )
lib . CheckFatalError ( err )
2023-08-28 00:53:01 +02:00
if count < 10 {
2022-11-15 04:26:56 +01:00
fmt . Println ( "Importing existing shell history..." )
ctx := hctx . MakeContext ( )
numImported , err := lib . ImportHistory ( ctx , false , false )
lib . CheckFatalError ( err )
if numImported > 0 {
fmt . Printf ( "Imported %v history entries from your existing shell history\n" , numImported )
}
}
}
2022-11-20 02:05:05 +01:00
lib . CheckFatalError ( warnIfUnsupportedBashVersion ( ) )
2022-11-15 04:26:56 +01:00
} ,
}
2022-11-15 05:55:10 +01:00
var initCmd = & cobra . Command {
Use : "init" ,
Short : "Re-initialize hiSHtory with a specified secret key" ,
GroupID : GROUP_ID_CONFIG ,
Args : cobra . MaximumNArgs ( 1 ) ,
Run : func ( cmd * cobra . Command , args [ ] string ) {
db , err := hctx . OpenLocalSqliteDb ( )
lib . CheckFatalError ( err )
2023-10-30 05:13:10 +01:00
count , err := countStoredEntries ( db )
lib . CheckFatalError ( err )
2024-04-13 18:51:07 +02:00
if count > 0 && ! ( * forceInit ) {
2022-11-15 05:55:10 +01:00
fmt . Printf ( "Your current hishtory profile has saved history entries, are you sure you want to run `init` and reset?\nNote: This won't clear any imported history entries from your existing shell\n[y/N]" )
reader := bufio . NewReader ( os . Stdin )
resp , err := reader . ReadString ( '\n' )
lib . CheckFatalError ( err )
if strings . TrimSpace ( resp ) != "y" {
fmt . Printf ( "Aborting init per user response of %#v\n" , strings . TrimSpace ( resp ) )
return
}
}
secretKey := ""
if len ( args ) > 0 {
secretKey = args [ 0 ]
}
2023-10-16 03:30:39 +02:00
lib . CheckFatalError ( setup ( secretKey , * offlineInit ) )
2022-11-15 05:55:10 +01:00
if os . Getenv ( "HISHTORY_SKIP_INIT_IMPORT" ) == "" {
fmt . Println ( "Importing existing shell history..." )
ctx := hctx . MakeContext ( )
numImported , err := lib . ImportHistory ( ctx , false , false )
lib . CheckFatalError ( err )
if numImported > 0 {
fmt . Printf ( "Imported %v history entries from your existing shell history\n" , numImported )
}
}
} ,
}
2022-11-20 02:12:43 +01:00
var uninstallCmd = & cobra . Command {
Use : "uninstall" ,
Short : "Completely uninstall hiSHtory and remove your shell history" ,
Run : func ( cmd * cobra . Command , args [ ] string ) {
ctx := hctx . MakeContext ( )
fmt . Printf ( "Are you sure you want to uninstall hiSHtory and delete all locally saved history data [y/N]" )
reader := bufio . NewReader ( os . Stdin )
resp , err := reader . ReadString ( '\n' )
lib . CheckFatalError ( err )
if strings . TrimSpace ( resp ) != "y" {
fmt . Printf ( "Aborting uninstall per user response of %#v\n" , strings . TrimSpace ( resp ) )
return
}
fmt . Printf ( "Do you have any feedback on why you're uninstallying hiSHtory? Type any feedback and then hit enter.\nFeedback: " )
feedbackTxt , err := reader . ReadString ( '\n' )
lib . CheckFatalError ( err )
feedback := shared . Feedback {
Date : time . Now ( ) ,
Feedback : feedbackTxt ,
UserId : data . UserId ( hctx . GetConf ( ctx ) . UserSecret ) ,
}
reqBody , err := json . Marshal ( feedback )
lib . CheckFatalError ( err )
2023-10-14 19:52:35 +02:00
_ , _ = lib . ApiPost ( ctx , "/api/v1/feedback" , "application/json" , reqBody )
2022-11-20 02:12:43 +01:00
lib . CheckFatalError ( uninstall ( ctx ) )
2023-12-10 18:15:11 +01:00
_ , err = lib . ApiPost ( ctx , "/api/v1/uninstall?user_id=" + data . UserId ( hctx . GetConf ( ctx ) . UserSecret ) + "&device_id=" + hctx . GetConf ( ctx ) . DeviceId , "application/json" , [ ] byte { } )
if err == nil {
fmt . Println ( "Successfully uninstalled hishtory, please restart your terminal..." )
} else {
fmt . Printf ( "Uninstall completed, but received server error: %v" , err )
}
2022-11-20 02:12:43 +01:00
} ,
}
2023-10-30 05:13:10 +01:00
func countStoredEntries ( db * gorm . DB ) ( int64 , error ) {
return lib . RetryingDbFunctionWithResult ( func ( ) ( int64 , error ) {
var count int64
return count , db . Model ( & data . HistoryEntry { } ) . Count ( & count ) . Error
} )
}
2022-11-20 02:05:05 +01:00
func warnIfUnsupportedBashVersion ( ) error {
_ , err := exec . LookPath ( "bash" )
if err != nil {
// bash is not installed, do nothing
return nil
}
cmd := exec . Command ( "bash" , "--version" )
bashVersion , err := cmd . CombinedOutput ( )
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to check bash version: %w" , err )
2022-11-20 02:05:05 +01:00
}
if strings . Contains ( string ( bashVersion ) , "version 3." ) {
fmt . Printf ( "Warning: Your current bash version does not support overriding control-r. Please upgrade to at least bash 5 to enable the control-r integration.\n" )
}
return nil
}
2024-06-14 06:16:17 +02:00
func install ( secretKey string , offline , skipConfigModification bool ) error {
2022-11-20 02:12:43 +01:00
homedir , err := os . UserHomeDir ( )
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to get user's home directory: %w" , err )
2022-11-20 02:12:43 +01:00
}
err = hctx . MakeHishtoryDir ( )
if err != nil {
return err
}
path , err := installBinary ( homedir )
if err != nil {
return err
}
2024-06-14 06:16:17 +02:00
err = configureBashrc ( homedir , path , skipConfigModification )
2022-11-20 02:12:43 +01:00
if err != nil {
return err
}
2024-06-14 06:16:17 +02:00
err = configureZshrc ( homedir , path , skipConfigModification )
2022-11-20 02:12:43 +01:00
if err != nil {
return err
}
2024-06-14 06:16:17 +02:00
err = configureFish ( homedir , path , skipConfigModification )
2022-11-20 02:12:43 +01:00
if err != nil {
return err
}
err = handleUpgradedFeatures ( )
if err != nil {
return err
}
_ , err = hctx . GetConfig ( )
if err != nil {
// No config, so set up a new installation
2023-10-16 03:30:39 +02:00
return setup ( secretKey , offline )
2022-11-20 02:12:43 +01:00
}
2023-10-31 01:38:40 +01:00
// TODO: Only trigger this if the version is old enough
2023-09-22 22:16:24 +02:00
err = handleDbUpgrades ( hctx . MakeContext ( ) )
if err != nil {
return err
}
2022-11-20 02:12:43 +01:00
return nil
}
2023-09-22 22:16:24 +02:00
// Handles people running `hishtory update` when the DB needs updating.
func handleDbUpgrades ( ctx context . Context ) error {
db := hctx . GetDb ( ctx )
2023-10-30 05:13:10 +01:00
return lib . RetryingDbFunction ( func ( ) error {
return db . Exec ( ` UPDATE history_entries SET entry_id = lower(hex(randomblob(12))) WHERE entry_id IS NULL ` ) . Error
} )
2023-09-22 22:16:24 +02:00
}
2023-09-21 20:35:47 +02:00
// Handles people running `hishtory update` from an old version of hishtory that
2023-10-12 03:18:56 +02:00
// doesn't support certain config options that we now default to true. This ensures
2023-11-12 12:09:56 +01:00
// that we can customize the behavior for upgrades while still respecting the option
// if someone has it explicitly set.
2022-11-20 02:12:43 +01:00
func handleUpgradedFeatures ( ) error {
2023-09-21 20:25:26 +02:00
configContents , err := hctx . GetConfigContents ( )
2022-11-20 02:12:43 +01:00
if err != nil {
// No config, so this is a new install and thus there is nothing to do
return nil
}
config , err := hctx . GetConfig ( )
if err != nil {
return err
}
2023-10-12 03:18:56 +02:00
if ! strings . Contains ( string ( configContents ) , "enable_control_r_search" ) {
// control-r search is not yet configured, so enable it
config . ControlRSearchEnabled = true
}
if ! strings . Contains ( string ( configContents ) , "highlight_matches" ) {
// highlighting is not yet configured, so enable it
config . HighlightMatches = true
}
2023-11-12 14:19:31 +01:00
if ! strings . Contains ( string ( configContents ) , "enable_presaving" ) {
// Presaving is not yet configured, so enable it
2023-11-23 05:52:31 +01:00
config . EnablePresaving = true
2023-11-12 14:19:31 +01:00
}
2023-11-12 12:09:56 +01:00
if ! strings . Contains ( string ( configContents ) , "ai_completion" ) {
// AI completion is not yet configured, disable it for upgrades since this is a new feature
config . AiCompletion = false
}
2023-09-23 21:40:57 +02:00
return hctx . SetConfig ( & config )
2022-11-20 02:12:43 +01:00
}
func installBinary ( homedir string ) ( string , error ) {
clientPath , err := exec . LookPath ( "hishtory" )
if err != nil {
2022-12-17 07:22:57 +01:00
clientPath = path . Join ( homedir , data . GetHishtoryPath ( ) , "hishtory" )
2022-11-20 02:12:43 +01:00
}
if _ , err := os . Stat ( clientPath ) ; err == nil {
err = syscall . Unlink ( clientPath )
if err != nil {
2023-09-05 21:08:55 +02:00
return "" , fmt . Errorf ( "failed to unlink %s for install: %w" , clientPath , err )
2022-11-20 02:12:43 +01:00
}
}
err = copyFile ( os . Args [ 0 ] , clientPath )
if err != nil {
2023-09-05 21:08:55 +02:00
return "" , fmt . Errorf ( "failed to copy hishtory binary to $PATH: %w" , err )
2022-11-20 02:12:43 +01:00
}
err = os . Chmod ( clientPath , 0 o700 )
if err != nil {
2023-09-05 21:08:55 +02:00
return "" , fmt . Errorf ( "failed to set permissions on hishtory binary: %w" , err )
2022-11-20 02:12:43 +01:00
}
return clientPath , nil
}
func getFishConfigPath ( homedir string ) string {
2022-12-17 07:22:57 +01:00
return path . Join ( homedir , data . GetHishtoryPath ( ) , "config.fish" )
2022-11-20 02:12:43 +01:00
}
2024-06-14 06:16:17 +02:00
func configureFish ( homedir , binaryPath string , skipConfigModification bool ) error {
2022-11-20 02:12:43 +01:00
// Check if fish is installed
_ , err := exec . LookPath ( "fish" )
if err != nil {
return nil
}
// Create the file we're going to source. Do this no matter what in case there are updates to it.
configContents := lib . ConfigFishContents
if os . Getenv ( "HISHTORY_TEST" ) != "" {
testConfig , err := tweakConfigForTests ( configContents )
if err != nil {
return err
}
configContents = testConfig
}
2022-11-27 20:59:06 +01:00
err = os . WriteFile ( getFishConfigPath ( homedir ) , [ ] byte ( configContents ) , 0 o644 )
2022-11-20 02:12:43 +01:00
if err != nil {
2023-09-15 08:09:15 +02:00
return fmt . Errorf ( "failed to write config.fish file: %w" , err )
2022-11-20 02:12:43 +01:00
}
// Check if we need to configure the fishrc
fishIsConfigured , err := isFishConfigured ( homedir )
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to check ~/.config/fish/config.fish: %w" , err )
2022-11-20 02:12:43 +01:00
}
if fishIsConfigured {
return nil
}
// Add to fishrc
2024-06-14 06:16:17 +02:00
if _ , err := exec . LookPath ( "fish" ) ; err != nil && skipConfigModification {
// fish is not installed, so avoid prompting the user to configure fish
return nil
}
2022-11-20 02:12:43 +01:00
err = os . MkdirAll ( path . Join ( homedir , ".config/fish" ) , 0 o744 )
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to create fish config directory: %w" , err )
2022-11-20 02:12:43 +01:00
}
2024-06-14 06:16:17 +02:00
return addToShellConfig ( path . Join ( homedir , ".config/fish/config.fish" ) , getFishConfigFragment ( homedir ) , skipConfigModification )
2022-11-20 02:12:43 +01:00
}
func getFishConfigFragment ( homedir string ) string {
2022-12-17 07:22:57 +01:00
return "\n# Hishtory Config:\nexport PATH=\"$PATH:" + path . Join ( homedir , data . GetHishtoryPath ( ) ) + "\"\nsource " + getFishConfigPath ( homedir ) + "\n"
2022-11-20 02:12:43 +01:00
}
func isFishConfigured ( homedir string ) ( bool , error ) {
_ , err := os . Stat ( path . Join ( homedir , ".config/fish/config.fish" ) )
if errors . Is ( err , os . ErrNotExist ) {
return false , nil
}
2022-11-27 20:59:06 +01:00
fishConfig , err := os . ReadFile ( path . Join ( homedir , ".config/fish/config.fish" ) )
2022-11-20 02:12:43 +01:00
if err != nil {
2023-09-05 21:08:55 +02:00
return false , fmt . Errorf ( "failed to read ~/.config/fish/config.fish: %w" , err )
2022-11-20 02:12:43 +01:00
}
2022-12-17 07:22:57 +01:00
return strings . Contains ( string ( fishConfig ) , getFishConfigFragment ( homedir ) ) , nil
2022-11-20 02:12:43 +01:00
}
func getZshConfigPath ( homedir string ) string {
2022-12-17 07:22:57 +01:00
return path . Join ( homedir , data . GetHishtoryPath ( ) , "config.zsh" )
2022-11-20 02:12:43 +01:00
}
2024-06-14 06:16:17 +02:00
func configureZshrc ( homedir , binaryPath string , skipConfigModification bool ) error {
2022-11-20 02:12:43 +01:00
// Create the file we're going to source in our zshrc. Do this no matter what in case there are updates to it.
configContents := lib . ConfigZshContents
if os . Getenv ( "HISHTORY_TEST" ) != "" {
testConfig , err := tweakConfigForTests ( configContents )
if err != nil {
return err
}
configContents = testConfig
}
2022-11-27 20:59:06 +01:00
err := os . WriteFile ( getZshConfigPath ( homedir ) , [ ] byte ( configContents ) , 0 o644 )
2022-11-20 02:12:43 +01:00
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to write config.zsh file: %w" , err )
2022-11-20 02:12:43 +01:00
}
// Check if we need to configure the zshrc
zshIsConfigured , err := isZshConfigured ( homedir )
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to check .zshrc: %w" , err )
2022-11-20 02:12:43 +01:00
}
if zshIsConfigured {
return nil
}
// Add to zshrc
2024-06-14 06:16:17 +02:00
return addToShellConfig ( getZshRcPath ( homedir ) , getZshConfigFragment ( homedir ) , skipConfigModification )
2022-11-20 02:12:43 +01:00
}
func getZshRcPath ( homedir string ) string {
if zdotdir := os . Getenv ( "ZDOTDIR" ) ; zdotdir != "" {
return path . Join ( zdotdir , ".zshrc" )
}
return path . Join ( homedir , ".zshrc" )
}
func getZshConfigFragment ( homedir string ) string {
2022-12-17 07:22:57 +01:00
return "\n# Hishtory Config:\nexport PATH=\"$PATH:" + path . Join ( homedir , data . GetHishtoryPath ( ) ) + "\"\nsource " + getZshConfigPath ( homedir ) + "\n"
2022-11-20 02:12:43 +01:00
}
func isZshConfigured ( homedir string ) ( bool , error ) {
_ , err := os . Stat ( getZshRcPath ( homedir ) )
if errors . Is ( err , os . ErrNotExist ) {
return false , nil
}
2022-11-27 20:59:06 +01:00
bashrc , err := os . ReadFile ( getZshRcPath ( homedir ) )
2022-11-20 02:12:43 +01:00
if err != nil {
2023-09-05 21:08:55 +02:00
return false , fmt . Errorf ( "failed to read zshrc: %w" , err )
2022-11-20 02:12:43 +01:00
}
2022-12-17 07:22:57 +01:00
return strings . Contains ( string ( bashrc ) , getZshConfigFragment ( homedir ) ) , nil
2022-11-20 02:12:43 +01:00
}
func getBashConfigPath ( homedir string ) string {
2022-12-17 07:22:57 +01:00
return path . Join ( homedir , data . GetHishtoryPath ( ) , "config.sh" )
2022-11-20 02:12:43 +01:00
}
2024-06-14 06:16:17 +02:00
func configureBashrc ( homedir , binaryPath string , skipConfigModification bool ) error {
2022-11-20 02:12:43 +01:00
// Create the file we're going to source in our bashrc. Do this no matter what in case there are updates to it.
configContents := lib . ConfigShContents
if os . Getenv ( "HISHTORY_TEST" ) != "" {
testConfig , err := tweakConfigForTests ( configContents )
if err != nil {
return err
}
configContents = testConfig
}
2022-11-27 20:59:06 +01:00
err := os . WriteFile ( getBashConfigPath ( homedir ) , [ ] byte ( configContents ) , 0 o644 )
2022-11-20 02:12:43 +01:00
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to write config.sh file: %w" , err )
2022-11-20 02:12:43 +01:00
}
// Check if we need to configure the bashrc and configure it if so
bashRcIsConfigured , err := isBashRcConfigured ( homedir )
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to check ~/.bashrc: %w" , err )
2022-11-20 02:12:43 +01:00
}
if ! bashRcIsConfigured {
2024-06-14 06:16:17 +02:00
err = addToShellConfig ( path . Join ( homedir , ".bashrc" ) , getBashConfigFragment ( homedir ) , skipConfigModification )
2022-11-20 02:12:43 +01:00
if err != nil {
return err
}
}
// Check if we need to configure the bash_profile and configure it if so
2023-02-28 03:14:32 +01:00
if doesBashProfileNeedConfig ( homedir ) {
bashProfileIsConfigured , err := isBashProfileConfigured ( homedir )
2022-11-20 02:12:43 +01:00
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to check ~/.bash_profile: %w" , err )
2023-02-28 03:14:32 +01:00
}
if ! bashProfileIsConfigured {
2024-06-14 06:16:17 +02:00
err = addToShellConfig ( path . Join ( homedir , ".bash_profile" ) , getBashConfigFragment ( homedir ) , skipConfigModification )
2023-02-28 03:14:32 +01:00
if err != nil {
return err
}
2022-11-20 02:12:43 +01:00
}
}
return nil
}
2024-06-14 06:16:17 +02:00
func addToShellConfig ( shellConfigPath , configFragment string , skipConfigModification bool ) error {
if skipConfigModification {
fmt . Printf ( "Please edit %q to add:\n\n```\n%s\n```\n\n" , convertToRelativePath ( shellConfigPath ) , strings . TrimSpace ( configFragment ) )
return nil
}
2022-11-20 02:12:43 +01:00
f , err := os . OpenFile ( shellConfigPath , os . O_APPEND | os . O_WRONLY | os . O_CREATE , 0 o644 )
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to append to %s: %w" , shellConfigPath , err )
2022-11-20 02:12:43 +01:00
}
defer f . Close ( )
_ , err = f . WriteString ( configFragment )
if err != nil {
2023-09-05 21:08:55 +02:00
return fmt . Errorf ( "failed to append to %s: %w" , shellConfigPath , err )
2022-11-20 02:12:43 +01:00
}
return nil
}
2024-06-14 06:16:17 +02:00
func convertToRelativePath ( path string ) string {
homedir , err := os . UserHomeDir ( )
if err != nil {
return path
}
if strings . HasPrefix ( path , homedir ) {
return strings . Replace ( path , homedir , "~" , 1 )
}
return path
}
2022-11-20 02:12:43 +01:00
func getBashConfigFragment ( homedir string ) string {
2022-12-17 07:22:57 +01:00
return "\n# Hishtory Config:\nexport PATH=\"$PATH:" + path . Join ( homedir , data . GetHishtoryPath ( ) ) + "\"\nsource " + getBashConfigPath ( homedir ) + "\n"
2022-11-20 02:12:43 +01:00
}
func isBashRcConfigured ( homedir string ) ( bool , error ) {
_ , err := os . Stat ( path . Join ( homedir , ".bashrc" ) )
if errors . Is ( err , os . ErrNotExist ) {
return false , nil
}
2022-11-27 20:59:06 +01:00
bashrc , err := os . ReadFile ( path . Join ( homedir , ".bashrc" ) )
2022-11-20 02:12:43 +01:00
if err != nil {
2023-09-05 21:08:55 +02:00
return false , fmt . Errorf ( "failed to read bashrc: %w" , err )
2022-11-20 02:12:43 +01:00
}
2022-12-17 07:22:57 +01:00
return strings . Contains ( string ( bashrc ) , getBashConfigFragment ( homedir ) ) , nil
2022-11-20 02:12:43 +01:00
}
2023-02-28 03:14:32 +01:00
func doesBashProfileNeedConfig ( homedir string ) bool {
if runtime . GOOS == "darwin" {
// Darwin always needs it configured for #14
return true
}
if runtime . GOOS == "linux" {
// Only configure it on linux if .bash_profile already exists
_ , err := os . Stat ( path . Join ( homedir , ".bash_profile" ) )
return ! errors . Is ( err , os . ErrNotExist )
}
// Default to not configuring it
return false
}
2022-11-20 02:12:43 +01:00
func isBashProfileConfigured ( homedir string ) ( bool , error ) {
_ , err := os . Stat ( path . Join ( homedir , ".bash_profile" ) )
if errors . Is ( err , os . ErrNotExist ) {
return false , nil
}
2022-11-27 20:59:06 +01:00
bashrc , err := os . ReadFile ( path . Join ( homedir , ".bash_profile" ) )
2022-11-20 02:12:43 +01:00
if err != nil {
2023-09-05 21:08:55 +02:00
return false , fmt . Errorf ( "failed to read bash_profile: %w" , err )
2022-11-20 02:12:43 +01:00
}
2022-12-17 07:22:57 +01:00
return strings . Contains ( string ( bashrc ) , getBashConfigFragment ( homedir ) ) , nil
2022-11-20 02:12:43 +01:00
}
func tweakConfigForTests ( configContents string ) ( string , error ) {
2023-09-17 23:35:56 +02:00
substitutionCount := 0
removedCount := 0
2022-11-20 02:12:43 +01:00
ret := ""
split := strings . Split ( configContents , "\n" )
for i , line := range split {
if strings . Contains ( line , "# Background Run" ) {
ret += strings . ReplaceAll ( split [ i + 1 ] , "# hishtory" , "hishtory" )
2023-09-17 23:35:56 +02:00
substitutionCount += 1
} else if strings . Contains ( line , "# Foreground Run" ) {
removedCount += 1
2022-11-20 02:12:43 +01:00
continue
} else {
ret += line
}
ret += "\n"
}
2023-09-17 23:35:56 +02:00
if ! ( substitutionCount == 2 && removedCount == 2 ) {
2022-11-20 02:12:43 +01:00
return "" , fmt . Errorf ( "failed to find substitution line in configConents=%#v" , configContents )
}
return ret , nil
}
func copyFile ( src , dst string ) error {
sourceFileStat , err := os . Stat ( src )
if err != nil {
return err
}
if ! sourceFileStat . Mode ( ) . IsRegular ( ) {
return fmt . Errorf ( "%s is not a regular file" , src )
}
source , err := os . Open ( src )
if err != nil {
return err
}
defer source . Close ( )
destination , err := os . Create ( dst )
if err != nil {
return err
}
_ , err = io . Copy ( destination , source )
if err != nil {
return err
}
return destination . Close ( )
}
2023-09-05 21:45:17 +02:00
func uninstall ( ctx context . Context ) error {
2022-11-20 02:12:43 +01:00
homedir := hctx . GetHome ( ctx )
err := stripLines ( path . Join ( homedir , ".bashrc" ) , getBashConfigFragment ( homedir ) )
if err != nil {
return err
}
err = stripLines ( getZshRcPath ( homedir ) , getZshConfigFragment ( homedir ) )
if err != nil {
return err
}
err = stripLines ( path . Join ( homedir , ".config/fish/config.fish" ) , getFishConfigFragment ( homedir ) )
if err != nil {
return err
}
2022-12-17 07:22:57 +01:00
err = os . RemoveAll ( path . Join ( homedir , data . GetHishtoryPath ( ) ) )
2022-11-20 02:12:43 +01:00
if err != nil {
return err
}
return nil
}
func stripLines ( filePath , lines string ) error {
if _ , err := os . Stat ( filePath ) ; errors . Is ( err , os . ErrNotExist ) {
// File does not exist, nothing to do
return nil
}
origContents , err := os . ReadFile ( filePath )
if err != nil {
return err
}
linesToBeRemoved := make ( map [ string ] bool , 0 )
for _ , line := range strings . Split ( lines , "\n" ) {
if strings . TrimSpace ( line ) != "" {
linesToBeRemoved [ line ] = true
}
}
ret := ""
for _ , line := range strings . Split ( string ( origContents ) , "\n" ) {
if ! linesToBeRemoved [ line ] {
ret += line
ret += "\n"
}
}
return os . WriteFile ( filePath , [ ] byte ( ret ) , 0644 )
}
2023-10-16 03:30:39 +02:00
func setup ( userSecret string , isOffline bool ) error {
if userSecret == "" {
userSecret = uuid . Must ( uuid . NewRandom ( ) ) . String ( )
}
fmt . Println ( "Setting secret hishtory key to " + string ( userSecret ) )
2023-12-19 05:58:16 +01:00
// Create and set the config with the defaults that we want for new installs
2023-10-16 03:30:39 +02:00
var config hctx . ClientConfig
config . UserSecret = userSecret
config . IsEnabled = true
config . DeviceId = uuid . Must ( uuid . NewRandom ( ) ) . String ( )
config . ControlRSearchEnabled = true
2023-12-19 05:58:16 +01:00
config . HighlightMatches = true
2023-11-12 12:09:56 +01:00
config . AiCompletion = true
2023-10-16 03:30:39 +02:00
config . IsOffline = isOffline
2023-11-12 14:19:31 +01:00
config . EnablePresaving = true
2023-10-16 03:30:39 +02:00
err := hctx . SetConfig ( & config )
if err != nil {
return fmt . Errorf ( "failed to persist config to disk: %w" , err )
}
// Drop all existing data
db , err := hctx . OpenLocalSqliteDb ( )
if err != nil {
return err
}
2023-11-19 17:52:27 +01:00
err = db . Exec ( "DELETE FROM history_entries" ) . Error
if err != nil {
return fmt . Errorf ( "failed to reset local DB during setup: %w" , err )
}
2023-10-16 03:30:39 +02:00
2023-12-22 03:57:29 +01:00
// Bootstrap from remote data
2023-10-16 03:30:39 +02:00
if config . IsOffline {
return nil
}
2024-04-29 01:33:43 +02:00
return registerAndBootstrapDevice ( hctx . MakeContext ( ) , & config , db , userSecret )
}
func registerAndBootstrapDevice ( ctx context . Context , config * hctx . ClientConfig , db * gorm . DB , userSecret string ) error {
2023-10-16 03:30:39 +02:00
registerPath := "/api/v1/register?user_id=" + data . UserId ( userSecret ) + "&device_id=" + config . DeviceId
2023-11-23 18:53:12 +01:00
if isIntegrationTestDevice ( ) {
2023-10-16 03:30:39 +02:00
registerPath += "&is_integration_test_device=true"
}
2024-04-29 01:33:43 +02:00
_ , err := lib . ApiGet ( ctx , registerPath )
2023-10-16 03:30:39 +02:00
if err != nil {
return fmt . Errorf ( "failed to register device with backend: %w" , err )
}
respBody , err := lib . ApiGet ( ctx , "/api/v1/bootstrap?user_id=" + data . UserId ( userSecret ) + "&device_id=" + config . DeviceId )
if err != nil {
return fmt . Errorf ( "failed to bootstrap device from the backend: %w" , err )
}
var retrievedEntries [ ] * shared . EncHistoryEntry
err = json . Unmarshal ( respBody , & retrievedEntries )
if err != nil {
return fmt . Errorf ( "failed to load JSON response: %w" , err )
}
2023-11-19 17:52:27 +01:00
hctx . GetLogger ( ) . Infof ( "Bootstrapping new device: Found %d entries" , len ( retrievedEntries ) )
2023-10-16 03:30:39 +02:00
for _ , entry := range retrievedEntries {
decEntry , err := data . DecryptHistoryEntry ( userSecret , * entry )
if err != nil {
return fmt . Errorf ( "failed to decrypt history entry from server: %w" , err )
}
lib . AddToDbIfNew ( db , decEntry )
}
return nil
}
2023-11-23 18:53:12 +01:00
func isIntegrationTestDevice ( ) bool {
if os . Getenv ( "HISHTORY_TEST" ) != "" {
return true
}
if os . Getenv ( "GITHUB_ACTION_REPOSITORY" ) == "ddworken/hishtory" {
return true
}
return false
}
2022-11-15 04:26:56 +01:00
func init ( ) {
rootCmd . AddCommand ( installCmd )
2022-11-15 05:55:10 +01:00
rootCmd . AddCommand ( initCmd )
2022-11-20 02:12:43 +01:00
rootCmd . AddCommand ( uninstallCmd )
2022-11-15 05:55:10 +01:00
offlineInit = initCmd . Flags ( ) . Bool ( "offline" , false , "Install hiSHtory in offline mode wiht all syncing capabilities disabled" )
2024-04-13 18:51:07 +02:00
forceInit = initCmd . Flags ( ) . Bool ( "force" , false , "Force re-init without any prompts" )
2024-06-14 06:16:17 +02:00
offlineInstall = installCmd . Flags ( ) . Bool ( "offline" , false , "Install hiSHtory in offline mode with all syncing capabilities disabled" )
skipConfigModification = installCmd . Flags ( ) . Bool ( "skip-config-modification" , false , "Skip modifying shell configs and instead instruct the user on how to modify their configs" )
2022-11-15 04:26:56 +01:00
}