From e10f27d3960af4ba571ec007bc5a54fc3951fee3 Mon Sep 17 00:00:00 2001 From: Ivan Frolov <59515280+frolvanya@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:58:20 -0500 Subject: [PATCH] feat: Get the final CLI command into the shell history with a small helper setup (#415) This PR introduces shell-specific functions for Bash, Zsh, and Fish shells to improve NEAR CLI's integration with shell history. The provided functions ensure that commands executed via the near CLI are automatically added to the shell's history, making them accessible through arrow keys and standard history mechanisms --- .github/workflows/release.yml | 48 +++++++------- Cargo.toml | 16 ++--- README.md | 2 +- docs/SHELL_HISTORY_INTEGRATION.md | 106 ++++++++++++++++++++++++++++++ src/common.rs | 21 ++++++ src/main.rs | 24 ++++--- 6 files changed, 173 insertions(+), 44 deletions(-) create mode 100644 docs/SHELL_HISTORY_INTEGRATION.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8093c74d..df34114ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -# This file was autogenerated by cargo-dist: https://opensource.axo.dev/cargo-dist/ +# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/ # # Copyright 2022-2024, axodotdev # SPDX-License-Identifier: MIT or Apache-2.0 @@ -6,7 +6,7 @@ # CI that: # # * checks for a Git Tag that looks like a release -# * builds artifacts with cargo-dist (archives, installers, hashes) +# * builds artifacts with dist (archives, installers, hashes) # * uploads those artifacts to temporary workflow zip # * on success, uploads the artifacts to a GitHub Release # @@ -24,10 +24,10 @@ permissions: # must be a Cargo-style SemVer Version (must have at least major.minor.patch). # # If PACKAGE_NAME is specified, then the announcement will be for that -# package (erroring out if it doesn't have the given version or isn't cargo-dist-able). +# package (erroring out if it doesn't have the given version or isn't dist-able). # # If PACKAGE_NAME isn't specified, then the announcement will be for all -# (cargo-dist-able) packages in the workspace with that version (this mode is +# (dist-able) packages in the workspace with that version (this mode is # intended for workspaces with only one dist-able package, or with all dist-able # packages versioned/released in lockstep). # @@ -45,7 +45,7 @@ on: - '**[0-9]+.[0-9]+.[0-9]+*' jobs: - # Run 'cargo dist plan' (or host) to determine what tasks we need to do + # Run 'dist plan' (or host) to determine what tasks we need to do plan: runs-on: "ubuntu-20.04" outputs: @@ -59,16 +59,16 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive - - name: Install cargo-dist + - name: Install dist # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.21.1/cargo-dist-installer.sh | sh" - - name: Cache cargo-dist + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.25.1/cargo-dist-installer.sh | sh" + - name: Cache dist uses: actions/upload-artifact@v4 with: name: cargo-dist-cache - path: ~/.cargo/bin/cargo-dist + path: ~/.cargo/bin/dist # sure would be cool if github gave us proper conditionals... # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible # functionality based on whether this is a pull_request, and whether it's from a fork. @@ -76,8 +76,8 @@ jobs: # but also really annoying to build CI around when it needs secrets to work right.) - id: plan run: | - cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json - echo "cargo dist ran successfully" + dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "dist ran successfully" cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" @@ -95,12 +95,12 @@ jobs: if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} strategy: fail-fast: false - # Target platforms/runners are computed by cargo-dist in create-release. + # Target platforms/runners are computed by dist in create-release. # Each member of the matrix has the following arguments: # # - runner: the github runner - # - dist-args: cli flags to pass to cargo dist - # - install-dist: expression to run to install cargo-dist on the runner + # - dist-args: cli flags to pass to dist + # - install-dist: expression to run to install dist on the runner # # Typically there will be: # - 1 "global" task that builds universal installers @@ -121,7 +121,7 @@ jobs: with: key: ${{ join(matrix.targets, '-') }} cache-provider: ${{ matrix.cache_provider }} - - name: Install cargo-dist + - name: Install dist run: ${{ matrix.install_dist }} # Get the dist-manifest - name: Fetch local artifacts @@ -136,8 +136,8 @@ jobs: - name: Build artifacts run: | # Actually do builds and make zips and whatnot - cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json - echo "cargo dist ran successfully" + dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "dist ran successfully" - id: cargo-dist name: Post-build # We force bash here just because github makes it really hard to get values up @@ -172,12 +172,12 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive - - name: Install cached cargo-dist + - name: Install cached dist uses: actions/download-artifact@v4 with: name: cargo-dist-cache path: ~/.cargo/bin/ - - run: chmod +x ~/.cargo/bin/cargo-dist + - run: chmod +x ~/.cargo/bin/dist # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts uses: actions/download-artifact@v4 @@ -188,8 +188,8 @@ jobs: - id: cargo-dist shell: bash run: | - cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json - echo "cargo dist ran successfully" + dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "dist ran successfully" # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" @@ -221,12 +221,12 @@ jobs: - uses: actions/checkout@v4 with: submodules: recursive - - name: Install cached cargo-dist + - name: Install cached dist uses: actions/download-artifact@v4 with: name: cargo-dist-cache path: ~/.cargo/bin/ - - run: chmod +x ~/.cargo/bin/cargo-dist + - run: chmod +x ~/.cargo/bin/dist # Fetch artifacts from scratch-storage - name: Fetch artifacts uses: actions/download-artifact@v4 @@ -237,7 +237,7 @@ jobs: - id: host shell: bash run: | - cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json echo "artifacts uploaded and released successfully" cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" diff --git a/Cargo.toml b/Cargo.toml index db163dac0..71b0fd283 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,24 +118,18 @@ self-update = ["self_update", "semver"] inherits = "release" lto = "thin" -# Config for 'cargo dist' +# Config for 'dist' [workspace.metadata.dist] -# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.21.1" +# The preferred dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.25.1" # CI backends to support ci = "github" # The installers to generate for each app installers = ["shell", "powershell", "npm", "msi"] -# Enable additional publishing steps +# Publish jobs to run in CI publish-jobs = ["npm"] # Target platforms to build apps for (Rust target-triple syntax) -targets = [ - "aarch64-apple-darwin", - "aarch64-unknown-linux-gnu", - "x86_64-apple-darwin", - "x86_64-unknown-linux-gnu", - "x86_64-pc-windows-msvc", -] +targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] # The archive format to use for windows builds (defaults .zip) windows-archive = ".tar.gz" # The archive format to use for non-windows builds (defaults .tar.xz) diff --git a/README.md b/README.md index 5c477588e..32f5ab7e8 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ $ near [↑↓ to move, enter to select, type to filter] ``` -The CLI interactively guides you through some pretty complex topics, helping you make informed decisions along the way. +The CLI interactively guides you through some pretty complex topics, helping you make informed decisions along the way. Also, you can enable [shell history integration](docs/SHELL_HISTORY_INTEGRATION.md) ## [Read more in English](docs/README.en.md) - [Usage](docs/README.en.md#usage) diff --git a/docs/SHELL_HISTORY_INTEGRATION.md b/docs/SHELL_HISTORY_INTEGRATION.md new file mode 100644 index 000000000..546696bce --- /dev/null +++ b/docs/SHELL_HISTORY_INTEGRATION.md @@ -0,0 +1,106 @@ +# Shell Configuration for Command History + +To enhance your experience with the NEAR CLI, you can configure your shell to integrate better with the near command. By adding the following functions to your shell configuration file, you ensure that commands executed via near are properly stored in your shell history and easily accessible via the arrow keys. + +## Bash + +Add the following function to your `~/.bashrc` file: + +```bash +function near() { + command near "$@" + + tmp_dir="${TMPDIR:-/tmp}" + tmp_file="$tmp_dir/near-cli-rs-final-command.log" + + if [[ -f "$tmp_file" ]]; then + final_command=$(<"$tmp_file") + + if [[ -n "$final_command" ]]; then + history -s -- "$final_command" + fi + + rm "$tmp_file" + fi +} +``` + +## Zsh + +Add the following function to your `~/.zshrc` file: + +```zsh +function near() { + command near "$@" + + tmp_dir="${TMPDIR:-/tmp}" + tmp_file="$tmp_dir/near-cli-rs-final-command.log" + + if [[ -f "$tmp_file" ]]; then + final_command=$(<"$tmp_file") + + if [[ -n "$final_command" ]]; then + print -s -- "$final_command" + fi + + rm "$tmp_file" + fi +} +``` + +## Fish + +Add the following function to your `~/.config/fish/config.fish` file: + +```fish +function near + command near $argv + + set tmp_dir (set -q TMPDIR; and echo $TMPDIR; or echo /tmp) + set tmp_file "$tmp_dir/near-cli-rs-final-command.log" + + if test -f "$tmp_file" + set -l final_command (cat "$tmp_file") + + if test -n "$final_command" + set -l history_file (dirname (status --current-filename))/../fish_history + + if set -q XDG_DATA_HOME + set history_file "$XDG_DATA_HOME/fish/fish_history" + else if test -d "$HOME/.local/share/fish" + set history_file "$HOME/.local/share/fish/fish_history" + else + set history_file "$HOME/.fish_history" + end + + echo "- cmd: $final_command" >> $history_file + echo " when: "(date +%s) >> $history_file + + history --merge + end + + rm "$tmp_file" + end +end +``` + +> [!NOTE] +> For Fish shell, the function appends the command to the Fish history file and merges it to make it immediately accessible via the arrow keys. + +## Explanation + +These functions wrap the original near command and perform additional steps to read a command from a temporary log file, which is created by the NEAR CLI, and add it to your shell history. This allows you to easily access previous NEAR CLI commands using your shell's history mechanisms. + +Steps performed by the functions: + +- Run the original near command with all provided arguments. +- Check if the temporary log file exists. +- Read the command from the log file. +- If the command is not empty: + - For Bash and Zsh: Add the command to the shell history. + - For Fish: Append the command to the Fish history file and merge the history. +- Remove the temporary log file to prevent duplicate entries. + +> [!IMPORTANT] +> Ensure that your NEAR CLI is configured to write the final command to the temporary log file at the specified location. +> Replace near with `cargo run --` in the functions if you are running the NEAR CLI via cargo locally. diff --git a/src/common.rs b/src/common.rs index f2f88b1f1..077c8fb0e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,5 +1,6 @@ use std::collections::VecDeque; use std::convert::{TryFrom, TryInto}; +use std::fs::OpenOptions; use std::io::Write; use std::str::FromStr; @@ -18,6 +19,8 @@ pub type CliResult = color_eyre::eyre::Result<()>; use inquire::{Select, Text}; use strum::IntoEnumIterator; +const FINAL_COMMAND_FILE_NAME: &str = "near-cli-rs-final-command.log"; + pub fn get_near_exec_path() -> String { std::env::args() .next() @@ -2645,3 +2648,21 @@ fn input_account_id_from_used_account_list( update_used_account_list(credentials_home_dir, account_id.as_ref(), account_is_signer); Ok(Some(account_id)) } + +pub fn save_cli_command(cli_cmd_str: &str) { + let tmp_file_path = std::env::temp_dir().join(FINAL_COMMAND_FILE_NAME); + + let Ok(mut tmp_file) = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(tmp_file_path) + else { + eprintln!("Failed to open a temporary file to store a cli command"); + return; + }; + + if let Err(err) = writeln!(tmp_file, "{}", cli_cmd_str) { + eprintln!("Failed to store a cli command in a temporary file: {}", err); + } +} diff --git a/src/main.rs b/src/main.rs index 06de48f62..4274a0cb4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ clippy::large_enum_variant, clippy::too_many_arguments )] + use clap::Parser; #[cfg(feature = "self-update")] use color_eyre::eyre::WrapErr; @@ -118,13 +119,17 @@ fn main() -> crate::common::CliResult { let cli_cmd = match ::from_cli(Some(cli), (config,)) { interactive_clap::ResultFromCli::Ok(cli_cmd) | interactive_clap::ResultFromCli::Cancel(Some(cli_cmd)) => { + let cli_cmd_str = shell_words::join( + std::iter::once(&near_cli_exec_path).chain(&cli_cmd.to_cli_args()), + ); + eprintln!( "\n\nHere is your console command if you need to script it or re-run:\n {}\n", - shell_words::join( - std::iter::once(&near_cli_exec_path).chain(&cli_cmd.to_cli_args()) - ) - .yellow() + cli_cmd_str.yellow() ); + + crate::common::save_cli_command(&cli_cmd_str); + Ok(Some(cli_cmd)) } interactive_clap::ResultFromCli::Cancel(None) => { @@ -136,13 +141,16 @@ fn main() -> crate::common::CliResult { } interactive_clap::ResultFromCli::Err(optional_cli_cmd, err) => { if let Some(cli_cmd) = optional_cli_cmd { + let cli_cmd_str = shell_words::join( + std::iter::once(&near_cli_exec_path).chain(&cli_cmd.to_cli_args()), + ); + eprintln!( "\nHere is your console command if you need to script it or re-run:\n {}\n", - shell_words::join( - std::iter::once(&near_cli_exec_path).chain(&cli_cmd.to_cli_args()) - ) - .yellow() + cli_cmd_str.yellow() ); + + crate::common::save_cli_command(&cli_cmd_str); } Err(err) }