mirror of
https://github.com/ddworken/hishtory.git
synced 2025-02-17 19:11:02 +01:00
* Swap to using iterators for uploading to avoid storing all chunks in memory * Chunk uploads for reuploading * Revert "Swap to using iterators for uploading to avoid storing all chunks in memory" This reverts commit632ecc5c81
. * Make hishtory install work even if there is zero shell history on the device * Skip DD integration for m1 mac since it seems to fail for mysterious beta-related reasons * Log OpenAI error to debug log for #167 * Release v0.269 * Add explicit handling for 429 error code from OpenAI * Release v0.270 * Fix handling of new lines in commands for #163 (#170) * Fix handling of new lines in commands for #163 * Move code for table from lib.go to query.go * Update goldens * Release v0.271 * Properly silence which output to fix #166 * Release v0.272 * Add || true to fully fix #166 * Release v0.273 * Improve install.py script to attempt to detect when /tmp/ is noexec (#172) * Improve install.py script to attempt to detect when /tmp/ is noexec * Add test to install from python script at HEAD * Remove incorrect duplicated line * Delete the tmp hishtory-client download since it may be dropped in CWD rather than /tmp/ * Add basic smoke test to provide test coverage for other distros (#174) * Fix quotes on container names * More tweaks for smoke testing * Skip setting the hostname for smoke tests since we don't need it * Dependencies for smoke testing * Add cgo deps * Install killall command * Add two more distros for smoke testing * Add smoke tests for arch * Update distro-smoke-test.yml * Remove sudo since the arch container runs as root * Drop sudo for OpenSUSE * Update install commands for OpenSUSE and Arch * More tweaks to install commands * Update arch install command * Remove OpenSUSE since their package repos are currently returning 500 errors * Add another dep for arch * Move up os.remove so that the file is removed even if it fails to execute * Move function to start of python file to make it more idiomatic * Update go action to enable caching of dependencies * Run integration tests in parallel to speed up testing (#175) * Remove a few direct DB insertions to prepare for parallel tests * Revert "Remove a few direct DB insertions to prepare for parallel tests" This reverts commitf8a3552ad8
. * Add rudimentary experiment of splitting tests into two chunks to make them faster * Add missing tag * Remove code that enforces that all goldens are used, since it is incompatible with how tests are currently split into chunks * Lay out the framework for checking goldens being used across all test runs * Fix missing brace * Revert "Remove code that enforces that all goldens are used, since it is incompatible with how tests are currently split into chunks" This reverts commit06cc3eedbc
. * Add initial work towards checking that all goldens are used * Delete incorrect and unreferenced matrix * Upgrade actions/upload-artifact to see if that makes the download in the next job work * Alternatively, try downloading the artifact by name * Update golden checker to read all the golden artifacts * Swap to using glob to enumerate all golden files, rather than hardcoding them * Remove debugging commands * Remove goldens that are actually used * Remove another golden that is actually used * Add more comprehensive support for test sharding * Fix references to test shards and increase shard count * Shard the fuzz test * Add debug prints * Mark additional tests for sharding * Fix logic error that broke test sharding * Remove debug print * Fix incorrect logic with skipping the fuzz test * Move sharding functions to testutils and add some comments * Upgrade all setup-go actions to enable caching of deps * Remove goldens that don't exist * Remove new line * Reduce delay * Correct stage name * Remove incorrect skip code from the first version of sharding * Remove unused import * Reduce number of test shards to match GitHub's limit of 5 concurrent macos jobs * Use cask for installing homebrew to speed up github actions * More cleanup for unused goldens * Swap away from brew cask since it appears to be slower * Add sync server to status -v #176 so that self-hosted users can easily confirm they're using the self-hosted server (#178) * Release v0.274 * Make bash support lenient with empty history lines, which seems to happen for the first command or two of new installs * Remove unnecessary sub-shell, since we just need a truthy value here * Release v0.275 * Add web UI for querying history from the browser (#180) As requested in #176 and #147 * Add initail version of a web UI for querying history from the browser * Rename webui command * Add basic test for the web UI * Add README for the web UI * Add basic auth for the web server * Add status code when panic-ing * Release v0.276 * Add ability to disable auth and force specific creds for the web UI * Add cleaning for integration test devices to remove DB entries * Wire through the shell name into AI suggestions so that we can get more precise AI suggestions for the current shell * Add support for control-A and control-E shortcuts similar to GNU readline * Allow register new device when exceed user limit when user already exist (#181) * Add basic readline-like support for using control-left and control-right to scroll horizontally by one word at a time * Release v0.277 * Improve word boundary algorithm to ignore previous spaces so that control+arrow-keys will skip over repeated spaces * Update colored golden * Update test golden * Update golden * Disable colored output tests * Add updated goldens * Delete temporarily unused goldens * Delete an unused file * Bump github.com/jackc/pgx/v4 from 4.14.1 to 4.18.2 (#189) Bumps [github.com/jackc/pgx/v4](https://github.com/jackc/pgx) from 4.14.1 to 4.18.2. - [Changelog](https://github.com/jackc/pgx/blob/v4.18.2/CHANGELOG.md) - [Commits](https://github.com/jackc/pgx/compare/v4.14.1...v4.18.2) --- updated-dependencies: - dependency-name: github.com/jackc/pgx/v4 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump google.golang.org/protobuf from 1.28.1 to 1.33.0 (#191) Bumps google.golang.org/protobuf from 1.28.1 to 1.33.0. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Upgrade SLSA releaser due to github.com/slsa-framework/slsa-github-generator/issues/3350 * Release v0.278 * Update slsa-verifier to attempt to fix SLSA breakage * Release v0.279 * Release v0.280 * Add better error message for SLSA failures * Disable validation so we can push out a working binary even though SLSA is broken * Release v0.281 * Fully disable validation to allow an emergency release due to SLSA breakage * Release v0.282 * Update cosign too to fix slsa breakage from https://blog.sigstore.dev/tuf-root-update/ * Release v0.283 * Release v0.284 * Fix go.mod version after cosign upgrade * Update go.sum after cosign update * Release v0.285 * Re-enable SLSA verification now that we've updated the SLSA version throughout the repo * Release v0.286 * Disable validation with local build since it seems to fail for some reason * Add SLSA validation with current binary built by SLSA * Set up tmate session to debug slsa releaser * Add SLSA failure warning for versions broken by SLSA * Remove tmate session for debugging * Release v0.287 * Bump gopkg.in/go-jose/go-jose.v2 from 2.6.1 to 2.6.3 (#197) Bumps gopkg.in/go-jose/go-jose.v2 from 2.6.1 to 2.6.3. --- updated-dependencies: - dependency-name: gopkg.in/go-jose/go-jose.v2 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add support for horizontal scrolling of all columns for #188 (#195) * Bump github.com/docker/docker (#193) Bumps [github.com/docker/docker](https://github.com/docker/docker) from 24.0.7+incompatible to 24.0.9+incompatible. - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v24.0.7...v24.0.9) --- updated-dependencies: - dependency-name: github.com/docker/docker dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump github.com/sigstore/rekor from 1.0.0 to 1.2.0 (#91) Bumps [github.com/sigstore/rekor](https://github.com/sigstore/rekor) from 1.0.0 to 1.2.0. - [Release notes](https://github.com/sigstore/rekor/releases) - [Changelog](https://github.com/sigstore/rekor/blob/main/CHANGELOG.md) - [Commits](https://github.com/sigstore/rekor/compare/v1.0.0...v1.2.0) --- updated-dependencies: - dependency-name: github.com/sigstore/rekor dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add ability to configure custom OpenAI API endpoint for #186 (#194) * Add ability to configure custom OpenAI API endpoint for #186 * Ensure the AiCompletionEndpoint field is always initialized * Release v0.288 * Enable colored golden tests for linux (#184) * Enable golden tests for linux and ensure all goldens get saved as outputs * Swap in OS specific goldens * Update colored goldens to take into account OS version, since different macos versions have different behavior here * Update goldens * Re-enable golden tests * Add missing golden * Empty commit * Remove linux kernel version from OS name * Remove minor version numbers from os versions for golden files for tests * Continue-on-error for the DD setup since it will also fail if colima fails * Add test for horizontal scrolling other columns for #188 * Add support for forcing init without prompting via --force flag for #198 * Clean up: Remove duplicated code by calling existing utility function * Add mouse scrolling support for #200 * Revert "Add mouse scrolling support for #200" since it breaks the ability to highlight text This reverts commit7d9bb6654d
. * Release v0.289 * Add benchmarking for searching for #202 * Add index of start time so that queries with a LIMIT clause can avoid a full table scan (for #202) * Release v0.290 * Add --port flag for the web UI for #203 * Add additional test for smoke tests to cover syncing * Move extra delay to a separate job to avoid wasting GH action quota by sleeping in duplicated jobs * Release v0.291 * Revert "Add additional test for smoke tests to cover syncing" This reverts commit514d95ba4e
. * Fix double-syncing error where devices receive entries from themselves #202 (#204) * Fix double-syncing error where devices receive entries from themselves * Fix incorrect error message * Add TODO * Update TestESubmitThenQuery after making query more efficient * Update TestDeletionRequests and remove unnecessary asserts * Swap server_test.go to using require * Fix incorrect require due to typo * Slow down gif per feedback in #199 * Update bubbletea to include 2b46020ca0725219da1a7d7969fa85c486181258 since it seems to help fix #185 * Fix test broken by7ae9f15b
by making sure input is sent and processed as separate events * Fix test broken by7ae9f15b
by making sure input is sent and processed as separate events * Fix test broken by7ae9f15b
by making sure input is sent and processed as separate events and updating the golden to reflect this * Release v0.292 * Bump golang.org/x/net from 0.22.0 to 0.23.0 (#206) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.22.0 to 0.23.0. - [Commits](https://github.com/golang/net/compare/v0.22.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Remove darwin-21 goldens since they're no longer used now that GH upgraded their macos image (#210) * Update backend to avoid persisting entries to be read by devices that have been uninstalled * Add support for custom key bindings for #190 (#209) * Add support for custom key bindings for #190 * Add tests for configuring custom key bindings * Simplify key bindings test * Add docs on custom key bindings + error message for unhandled actions * Fix condition added ind6a60214a2
to also apply to rows with the go 'empty' value and not just null * Add support for enabling/disabling syncing post-install * Release v0.293 * fix: close file (#213) * Release v0.294 * Move docs on custom key bindings to a more logical location * Fix duplicate pre-saving issue reported in #215 * Revert "Fix duplicate pre-saving issue reported in #215" This reverts commit336b331687
. * Fix duplicate pre-saving issue reported in #215 (#217) * Release v0.295 * Add full fix for #215 along with a test to reproduce the issue (#218) * Release v0.296 * Add ability to skip config modifications for #212 (#216) * Add ability to skip config modifications * Update golden names to fork on OS * Remove incorrect newline in golden * Add README documentation for default-filter * Update title for section * Release v0.297 * Add basic fix for #225 by escaping tab characters before rendering This is a tricky bug to fix because the width of a tab character varies depending on context. This means that when we're trying to build a table and calculating the width of columns for budgeting, we can't actually know the width of a tab without knowing exactly what characters come before it. This is in theory doable, but it leads to some really complex code that I'd rather not adopt. * Release v0.298 * Bump github.com/hashicorp/go-retryablehttp from 0.7.2 to 0.7.7 (#223) Bumps [github.com/hashicorp/go-retryablehttp](https://github.com/hashicorp/go-retryablehttp) from 0.7.2 to 0.7.7. - [Changelog](https://github.com/hashicorp/go-retryablehttp/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/go-retryablehttp/compare/v0.7.2...v0.7.7) --- updated-dependencies: - dependency-name: github.com/hashicorp/go-retryablehttp dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add additional fallback method for retrieving the CWD to further improve the situation for #226 * Explicitly install openssl to see if it fixes smoke test errors on arch * Add integration test for #226 * Release v0.299 * Update macos version for signer since GH dropped support for macos 11 * Release v0.300 * Swap to macos-latest to see if GH has more quota for that tag * Release v0.301 * Release v0.302 * Upgrade to setup-go@v4 for automatic caching support * Revert "Remove OpenSUSE since their package repos are currently returning 500 errors" This reverts commit62700605d7
. * Install git and tar for opensuse smoke tests * Link /bin/sh for opensuse smoke tests * Remove opensuse smoke tests * use http.DefaultClient (#232) * Add new short name for "ExitCode" - "$?" (#228) * Add more short column name alternatives similar to #228 * add forceComapctMode config entry (#237) * Add docs in readme to call out shorter column names as added in #228 * Change compact-mode setting that was added in #237 to respect the convention of taking in an argument * Add config-get compact-mode command (as needed by #237) * Move checking of forced compact mode into helper functions to ensure it is checked everywhere (follow up to #237) * Add test for forced compact mode (from #237) * ai: add some new env variables to control OpenAI requests (#231) Co-authored-by: David Dworken <david@daviddworken.com> * Update incorrect docs on ClientConfig struct * Add ability for the client to configure the model via an environment variable * Bump github.com/docker/docker (#236) Bumps [github.com/docker/docker](https://github.com/docker/docker) from 24.0.9+incompatible to 25.0.6+incompatible. - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v24.0.9...v25.0.6) --- updated-dependencies: - dependency-name: github.com/docker/docker dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * run "make fmt" (#233) * Add make fmt to pre-commit * Fix import --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Nguyễn Hoàng Đức <lazyc97@tutanota.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: guangwu <guoguangwu@magic-shield.com> Co-authored-by: Pavel Griaznov <grbitt@gmail.com>
676 lines
21 KiB
Go
676 lines
21 KiB
Go
package cmd
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/ddworken/hishtory/client/data"
|
|
"github.com/ddworken/hishtory/client/hctx"
|
|
"github.com/ddworken/hishtory/client/lib"
|
|
"github.com/ddworken/hishtory/shared"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/spf13/cobra"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var (
|
|
offlineInit *bool
|
|
forceInit *bool
|
|
offlineInstall *bool
|
|
skipConfigModification *bool
|
|
)
|
|
|
|
var installCmd = &cobra.Command{
|
|
Use: "install",
|
|
Hidden: true,
|
|
Short: "Copy this binary to ~/.hishtory/ and configure your shell to use it for recording your shell history",
|
|
Args: cobra.MaximumNArgs(1),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
secretKey := ""
|
|
if len(args) > 0 {
|
|
secretKey = args[0]
|
|
}
|
|
lib.CheckFatalError(install(secretKey, *offlineInstall, *skipConfigModification))
|
|
if os.Getenv("HISHTORY_SKIP_INIT_IMPORT") == "" {
|
|
db, err := hctx.OpenLocalSqliteDb()
|
|
lib.CheckFatalError(err)
|
|
count, err := lib.CountStoredEntries(db)
|
|
lib.CheckFatalError(err)
|
|
if count < 10 {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
lib.CheckFatalError(warnIfUnsupportedBashVersion())
|
|
},
|
|
}
|
|
|
|
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)
|
|
count, err := lib.CountStoredEntries(db)
|
|
lib.CheckFatalError(err)
|
|
if count > 0 && !(*forceInit) {
|
|
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]
|
|
}
|
|
lib.CheckFatalError(setup(secretKey, *offlineInit))
|
|
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)
|
|
}
|
|
}
|
|
},
|
|
}
|
|
|
|
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)
|
|
_, _ = lib.ApiPost(ctx, "/api/v1/feedback", "application/json", reqBody)
|
|
lib.CheckFatalError(uninstall(ctx))
|
|
_, 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)
|
|
}
|
|
},
|
|
}
|
|
|
|
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 {
|
|
return fmt.Errorf("failed to check bash version: %w", err)
|
|
}
|
|
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
|
|
}
|
|
|
|
func install(secretKey string, offline, skipConfigModification bool) error {
|
|
homedir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user's home directory: %w", err)
|
|
}
|
|
err = hctx.MakeHishtoryDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path, err := installBinary(homedir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = configureBashrc(homedir, path, skipConfigModification)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = configureZshrc(homedir, path, skipConfigModification)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = configureFish(homedir, path, skipConfigModification)
|
|
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
|
|
return setup(secretKey, offline)
|
|
}
|
|
// TODO: Only trigger this if the version is old enough
|
|
err = handleDbUpgrades(hctx.MakeContext())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Handles people running `hishtory update` when the DB needs updating.
|
|
func handleDbUpgrades(ctx context.Context) error {
|
|
db := hctx.GetDb(ctx)
|
|
return lib.RetryingDbFunction(func() error {
|
|
return db.Exec(`UPDATE history_entries SET entry_id = lower(hex(randomblob(12))) WHERE entry_id IS NULL`).Error
|
|
})
|
|
}
|
|
|
|
// Handles people running `hishtory update` from an old version of hishtory that
|
|
// doesn't support certain config options that we now default to true. This ensures
|
|
// that we can customize the behavior for upgrades while still respecting the option
|
|
// if someone has it explicitly set.
|
|
func handleUpgradedFeatures() error {
|
|
configContents, err := hctx.GetConfigContents()
|
|
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
|
|
}
|
|
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
|
|
}
|
|
if !strings.Contains(string(configContents), "enable_presaving") {
|
|
// Presaving is not yet configured, so enable it
|
|
config.EnablePresaving = true
|
|
}
|
|
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
|
|
}
|
|
return hctx.SetConfig(&config)
|
|
}
|
|
|
|
func installBinary(homedir string) (string, error) {
|
|
clientPath, err := exec.LookPath("hishtory")
|
|
if err != nil {
|
|
clientPath = path.Join(homedir, data.GetHishtoryPath(), "hishtory")
|
|
}
|
|
if _, err := os.Stat(clientPath); err == nil {
|
|
err = syscall.Unlink(clientPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to unlink %s for install: %w", clientPath, err)
|
|
}
|
|
}
|
|
err = copyFile(os.Args[0], clientPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to copy hishtory binary to $PATH: %w", err)
|
|
}
|
|
err = os.Chmod(clientPath, 0o700)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to set permissions on hishtory binary: %w", err)
|
|
}
|
|
return clientPath, nil
|
|
}
|
|
|
|
func getFishConfigPath(homedir string) string {
|
|
return path.Join(homedir, data.GetHishtoryPath(), "config.fish")
|
|
}
|
|
|
|
func configureFish(homedir, binaryPath string, skipConfigModification bool) error {
|
|
// 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
|
|
}
|
|
err = os.WriteFile(getFishConfigPath(homedir), []byte(configContents), 0o644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write config.fish file: %w", err)
|
|
}
|
|
// Check if we need to configure the fishrc
|
|
fishIsConfigured, err := isFishConfigured(homedir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check ~/.config/fish/config.fish: %w", err)
|
|
}
|
|
if fishIsConfigured {
|
|
return nil
|
|
}
|
|
// Add to fishrc
|
|
if _, err := exec.LookPath("fish"); err != nil && skipConfigModification {
|
|
// fish is not installed, so avoid prompting the user to configure fish
|
|
return nil
|
|
}
|
|
err = os.MkdirAll(path.Join(homedir, ".config/fish"), 0o744)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create fish config directory: %w", err)
|
|
}
|
|
return addToShellConfig(path.Join(homedir, ".config/fish/config.fish"), getFishConfigFragment(homedir), skipConfigModification)
|
|
}
|
|
|
|
func getFishConfigFragment(homedir string) string {
|
|
return "\n# Hishtory Config:\nexport PATH=\"$PATH:" + path.Join(homedir, data.GetHishtoryPath()) + "\"\nsource " + getFishConfigPath(homedir) + "\n"
|
|
}
|
|
|
|
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
|
|
}
|
|
fishConfig, err := os.ReadFile(path.Join(homedir, ".config/fish/config.fish"))
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to read ~/.config/fish/config.fish: %w", err)
|
|
}
|
|
return strings.Contains(string(fishConfig), getFishConfigFragment(homedir)), nil
|
|
}
|
|
|
|
func getZshConfigPath(homedir string) string {
|
|
return path.Join(homedir, data.GetHishtoryPath(), "config.zsh")
|
|
}
|
|
|
|
func configureZshrc(homedir, binaryPath string, skipConfigModification bool) error {
|
|
// 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
|
|
}
|
|
err := os.WriteFile(getZshConfigPath(homedir), []byte(configContents), 0o644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write config.zsh file: %w", err)
|
|
}
|
|
// Check if we need to configure the zshrc
|
|
zshIsConfigured, err := isZshConfigured(homedir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check .zshrc: %w", err)
|
|
}
|
|
if zshIsConfigured {
|
|
return nil
|
|
}
|
|
// Add to zshrc
|
|
return addToShellConfig(getZshRcPath(homedir), getZshConfigFragment(homedir), skipConfigModification)
|
|
}
|
|
|
|
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 {
|
|
return "\n# Hishtory Config:\nexport PATH=\"$PATH:" + path.Join(homedir, data.GetHishtoryPath()) + "\"\nsource " + getZshConfigPath(homedir) + "\n"
|
|
}
|
|
|
|
func isZshConfigured(homedir string) (bool, error) {
|
|
_, err := os.Stat(getZshRcPath(homedir))
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return false, nil
|
|
}
|
|
bashrc, err := os.ReadFile(getZshRcPath(homedir))
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to read zshrc: %w", err)
|
|
}
|
|
return strings.Contains(string(bashrc), getZshConfigFragment(homedir)), nil
|
|
}
|
|
|
|
func getBashConfigPath(homedir string) string {
|
|
return path.Join(homedir, data.GetHishtoryPath(), "config.sh")
|
|
}
|
|
|
|
func configureBashrc(homedir, binaryPath string, skipConfigModification bool) error {
|
|
// 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
|
|
}
|
|
err := os.WriteFile(getBashConfigPath(homedir), []byte(configContents), 0o644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write config.sh file: %w", err)
|
|
}
|
|
// Check if we need to configure the bashrc and configure it if so
|
|
bashRcIsConfigured, err := isBashRcConfigured(homedir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check ~/.bashrc: %w", err)
|
|
}
|
|
if !bashRcIsConfigured {
|
|
err = addToShellConfig(path.Join(homedir, ".bashrc"), getBashConfigFragment(homedir), skipConfigModification)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Check if we need to configure the bash_profile and configure it if so
|
|
if doesBashProfileNeedConfig(homedir) {
|
|
bashProfileIsConfigured, err := isBashProfileConfigured(homedir)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check ~/.bash_profile: %w", err)
|
|
}
|
|
if !bashProfileIsConfigured {
|
|
err = addToShellConfig(path.Join(homedir, ".bash_profile"), getBashConfigFragment(homedir), skipConfigModification)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
f, err := os.OpenFile(shellConfigPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to append to %s: %w", shellConfigPath, err)
|
|
}
|
|
defer f.Close()
|
|
_, err = f.WriteString(configFragment)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to append to %s: %w", shellConfigPath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func getBashConfigFragment(homedir string) string {
|
|
return "\n# Hishtory Config:\nexport PATH=\"$PATH:" + path.Join(homedir, data.GetHishtoryPath()) + "\"\nsource " + getBashConfigPath(homedir) + "\n"
|
|
}
|
|
|
|
func isBashRcConfigured(homedir string) (bool, error) {
|
|
_, err := os.Stat(path.Join(homedir, ".bashrc"))
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return false, nil
|
|
}
|
|
bashrc, err := os.ReadFile(path.Join(homedir, ".bashrc"))
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to read bashrc: %w", err)
|
|
}
|
|
return strings.Contains(string(bashrc), getBashConfigFragment(homedir)), nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func isBashProfileConfigured(homedir string) (bool, error) {
|
|
_, err := os.Stat(path.Join(homedir, ".bash_profile"))
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return false, nil
|
|
}
|
|
bashrc, err := os.ReadFile(path.Join(homedir, ".bash_profile"))
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to read bash_profile: %w", err)
|
|
}
|
|
return strings.Contains(string(bashrc), getBashConfigFragment(homedir)), nil
|
|
}
|
|
|
|
func tweakConfigForTests(configContents string) (string, error) {
|
|
substitutionCount := 0
|
|
removedCount := 0
|
|
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")
|
|
substitutionCount += 1
|
|
} else if strings.Contains(line, "# Foreground Run") {
|
|
removedCount += 1
|
|
continue
|
|
} else {
|
|
ret += line
|
|
}
|
|
ret += "\n"
|
|
}
|
|
if !(substitutionCount == 2 && removedCount == 2) {
|
|
return "", fmt.Errorf("failed to find substitution line in configContents=%#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()
|
|
}
|
|
|
|
func uninstall(ctx context.Context) error {
|
|
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
|
|
}
|
|
err = os.RemoveAll(path.Join(homedir, data.GetHishtoryPath()))
|
|
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), 0o644)
|
|
}
|
|
|
|
func setup(userSecret string, isOffline bool) error {
|
|
if userSecret == "" {
|
|
userSecret = uuid.Must(uuid.NewRandom()).String()
|
|
}
|
|
fmt.Println("Setting secret hishtory key to " + string(userSecret))
|
|
|
|
// Create and set the config with the defaults that we want for new installs
|
|
var config hctx.ClientConfig
|
|
config.UserSecret = userSecret
|
|
config.IsEnabled = true
|
|
config.DeviceId = uuid.Must(uuid.NewRandom()).String()
|
|
config.ControlRSearchEnabled = true
|
|
config.HighlightMatches = true
|
|
config.AiCompletion = true
|
|
config.IsOffline = isOffline
|
|
if isOffline {
|
|
// By default, offline mode disables AI completion. Users can still enable it if they want it. See #220.
|
|
config.AiCompletion = false
|
|
}
|
|
config.EnablePresaving = true
|
|
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
|
|
}
|
|
err = db.Exec("DELETE FROM history_entries").Error
|
|
if err != nil {
|
|
return fmt.Errorf("failed to reset local DB during setup: %w", err)
|
|
}
|
|
|
|
// Bootstrap from remote data
|
|
if config.IsOffline {
|
|
return nil
|
|
}
|
|
return registerAndBootstrapDevice(hctx.MakeContext(), &config, db, userSecret)
|
|
}
|
|
|
|
func registerAndBootstrapDevice(ctx context.Context, config *hctx.ClientConfig, db *gorm.DB, userSecret string) error {
|
|
registerPath := "/api/v1/register?user_id=" + data.UserId(userSecret) + "&device_id=" + config.DeviceId
|
|
if isIntegrationTestDevice() {
|
|
registerPath += "&is_integration_test_device=true"
|
|
}
|
|
_, err := lib.ApiGet(ctx, registerPath)
|
|
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)
|
|
}
|
|
hctx.GetLogger().Infof("Bootstrapping new device: Found %d entries", len(retrievedEntries))
|
|
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
|
|
}
|
|
|
|
func isIntegrationTestDevice() bool {
|
|
if os.Getenv("HISHTORY_TEST") != "" {
|
|
return true
|
|
}
|
|
if os.Getenv("GITHUB_ACTION_REPOSITORY") == "ddworken/hishtory" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.AddCommand(installCmd)
|
|
rootCmd.AddCommand(initCmd)
|
|
rootCmd.AddCommand(uninstallCmd)
|
|
|
|
offlineInit = initCmd.Flags().Bool("offline", false, "Install hiSHtory in offline mode wiht all syncing capabilities disabled")
|
|
forceInit = initCmd.Flags().Bool("force", false, "Force re-init without any prompts")
|
|
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")
|
|
}
|