├── .github └── workflows │ ├── coverage.yml │ ├── release.yml │ ├── test.yml │ └── version-bump.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── PROJECT_SCOPE.md ├── README.md ├── coverage └── tarpaulin-report.html ├── demo.yml ├── demo ├── add-command.cast ├── add-command.gif ├── add-command2.gif ├── add-command3.gif ├── delete-command.cast ├── delete-command.gif ├── ls-command.cast ├── ls-command.gif ├── ls-command2.gif ├── search-command.cast ├── search-command.gif ├── tag-command.cast └── tag-command.gif ├── scripts ├── bump_version.sh ├── coverage.sh └── update_version.sh ├── shell ├── bash-integration.sh ├── fish-integration.fish └── zsh-integration.zsh ├── src ├── cli │ ├── args.rs │ ├── commands.rs │ └── mod.rs ├── db │ ├── mod.rs │ ├── models.rs │ └── store.rs ├── exec │ └── mod.rs ├── lib.rs ├── main.rs ├── shell │ ├── hooks.rs │ └── mod.rs ├── ui │ ├── add.rs │ ├── app.rs │ └── mod.rs ├── utils │ ├── mod.rs │ ├── params.rs │ └── time.rs └── version.rs └── tests ├── args_test.rs ├── cli_test.rs ├── commands_test.rs ├── db_test.rs ├── exec_test.rs ├── params_test.rs ├── shell_test.rs ├── test_utils.rs ├── time_test.rs └── ui_test.rs /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test: 14 | name: Coverage 15 | runs-on: ubuntu-latest 16 | container: 17 | image: xd009642/tarpaulin 18 | options: --security-opt seccomp=unconfined 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v3 22 | 23 | - name: Generate code coverage 24 | run: | 25 | cargo tarpaulin --verbose --workspace --timeout 120 --out Xml --output-dir ./coverage 26 | 27 | - name: Upload coverage reports to Codecov 28 | uses: codecov/codecov-action@v5 29 | with: 30 | files: ./coverage/cobertura.xml 31 | fail_ci_if_error: false 32 | verbose: true 33 | retry_max_attempts: 3 34 | retry_delay_seconds: 30 35 | token: ${{ secrets.CODECOV_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | pull_request: 8 | types: [closed] 9 | branches: [main] 10 | 11 | jobs: 12 | create-release: 13 | if: | 14 | (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || 15 | (github.event_name == 'pull_request' && github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'version-bump')) 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | outputs: 20 | upload_url: ${{ steps.create_release.outputs.upload_url }} 21 | version: ${{ steps.get_version.outputs.version }} 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Get version from Cargo.toml 28 | id: get_version 29 | run: | 30 | VERSION=$(grep '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/') 31 | echo "version=${VERSION}" >> $GITHUB_OUTPUT 32 | 33 | - name: Create Git tag 34 | if: github.event_name == 'pull_request' 35 | run: | 36 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 37 | git config --local user.name "github-actions[bot]" 38 | git tag -a "v${{ steps.get_version.outputs.version }}" -m "Release v${{ steps.get_version.outputs.version }}" 39 | git push origin "v${{ steps.get_version.outputs.version }}" 40 | 41 | - name: Create Release 42 | id: create_release 43 | uses: actions/create-release@v1 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | with: 47 | tag_name: v${{ steps.get_version.outputs.version }} 48 | release_name: Release v${{ steps.get_version.outputs.version }} 49 | draft: false 50 | prerelease: false 51 | 52 | build-release: 53 | needs: create-release 54 | runs-on: ${{ matrix.os }} 55 | permissions: 56 | contents: write 57 | strategy: 58 | matrix: 59 | include: 60 | - os: ubuntu-latest 61 | target: x86_64-unknown-linux-gnu 62 | artifact_name: command-vault 63 | asset_name: command-vault-linux-amd64 64 | - os: macos-latest 65 | target: x86_64-apple-darwin 66 | artifact_name: command-vault 67 | asset_name: command-vault-macos-x86_64 68 | - os: macos-latest 69 | target: aarch64-apple-darwin 70 | artifact_name: command-vault 71 | asset_name: command-vault-macos-arm64 72 | - os: windows-latest 73 | target: x86_64-pc-windows-msvc 74 | artifact_name: command-vault.exe 75 | asset_name: command-vault-windows-amd64.exe 76 | 77 | steps: 78 | - uses: actions/checkout@v4 79 | 80 | - name: Install Rust toolchain 81 | uses: dtolnay/rust-toolchain@stable 82 | with: 83 | targets: ${{ matrix.target }} 84 | 85 | - name: Build 86 | run: cargo build --release --target ${{ matrix.target }} 87 | 88 | - name: Prepare asset for upload (Windows) 89 | if: matrix.os == 'windows-latest' 90 | shell: pwsh 91 | run: | 92 | cd target/${{ matrix.target }}/release 93 | Compress-Archive -Path ${{ matrix.artifact_name }} -DestinationPath ../../../${{ matrix.asset_name }}.zip 94 | 95 | - name: Prepare asset for upload (Unix) 96 | if: matrix.os != 'windows-latest' 97 | shell: bash 98 | run: | 99 | cd target/${{ matrix.target }}/release 100 | tar czf ../../../${{ matrix.asset_name }}.tar.gz ${{ matrix.artifact_name }} 101 | 102 | - name: Upload Release Asset 103 | uses: actions/upload-release-asset@v1 104 | env: 105 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 106 | with: 107 | upload_url: ${{ needs.create-release.outputs.upload_url }} 108 | asset_path: ${{ matrix.os == 'windows-latest' && format('{0}.zip', matrix.asset_name) || format('{0}.tar.gz', matrix.asset_name) }} 109 | asset_name: ${{ matrix.os == 'windows-latest' && format('{0}.zip', matrix.asset_name) || format('{0}.tar.gz', matrix.asset_name) }} 110 | asset_content_type: application/octet-stream 111 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install Rust 20 | uses: dtolnay/rust-toolchain@stable 21 | with: 22 | components: clippy 23 | 24 | - name: Cache Dependencies 25 | uses: actions/cache@v3 26 | with: 27 | path: | 28 | ~/.cargo/registry 29 | ~/.cargo/git 30 | target 31 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 32 | 33 | - name: Build 34 | run: cargo build --verbose 35 | 36 | - name: Run tests 37 | run: cargo test --verbose 38 | -------------------------------------------------------------------------------- /.github/workflows/version-bump.yml: -------------------------------------------------------------------------------- 1 | name: Version Bump 2 | 3 | on: 4 | pull_request: 5 | types: [labeled, opened, synchronize, reopened] 6 | workflow_dispatch: 7 | inputs: 8 | bump: 9 | description: 'Version bump type (major, minor, patch)' 10 | required: true 11 | default: 'patch' 12 | type: choice 13 | options: 14 | - major 15 | - minor 16 | - patch 17 | 18 | jobs: 19 | version-bump: 20 | if: | 21 | github.event_name == 'workflow_dispatch' || 22 | (github.event_name == 'pull_request' && (contains(github.event.pull_request.labels.*.name, 'bump:major') || contains(github.event.pull_request.labels.*.name, 'bump:minor') || contains(github.event.pull_request.labels.*.name, 'bump:patch'))) 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | token: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - name: Install Rust toolchain 30 | uses: dtolnay/rust-toolchain@stable 31 | 32 | - name: Install cargo-edit 33 | run: cargo install cargo-edit 34 | 35 | - name: Determine version bump 36 | id: bump 37 | run: | 38 | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then 39 | echo "bump=${{ github.event.inputs.bump }}" >> $GITHUB_OUTPUT 40 | else 41 | if [[ "${{ join(github.event.pull_request.labels.*.name, ' ') }}" =~ "bump:major" ]]; then 42 | echo "bump=major" >> $GITHUB_OUTPUT 43 | elif [[ "${{ join(github.event.pull_request.labels.*.name, ' ') }}" =~ "bump:minor" ]]; then 44 | echo "bump=minor" >> $GITHUB_OUTPUT 45 | else 46 | echo "bump=patch" >> $GITHUB_OUTPUT 47 | fi 48 | fi 49 | 50 | - name: Get current version 51 | id: current_version 52 | run: | 53 | version=$(grep "^version" Cargo.toml | sed 's/version = "\(.*\)"/\1/') 54 | echo "version=$version" >> $GITHUB_OUTPUT 55 | 56 | - name: Bump version 57 | id: bump_version 58 | run: | 59 | cargo set-version --bump ${{ steps.bump.outputs.bump }} 60 | new_version=$(grep "^version" Cargo.toml | sed 's/version = "\(.*\)"/\1/') 61 | echo "version=$new_version" >> $GITHUB_OUTPUT 62 | 63 | - name: Commit changes 64 | run: | 65 | git config --local user.email "action@github.com" 66 | git config --local user.name "GitHub Action" 67 | git add Cargo.toml Cargo.lock 68 | git commit -m "chore: bump version ${{ steps.current_version.outputs.version }} -> ${{ steps.bump_version.outputs.version }}" 69 | 70 | - name: Push changes 71 | uses: ad-m/github-push-action@master 72 | with: 73 | github_token: ${{ secrets.GITHUB_TOKEN }} 74 | branch: ${{ github.head_ref || github.ref_name }} 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | lcov.info 3 | .specstory/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ### Fixed 6 | - Fixed parameter substitution when parameters have descriptions (e.g., `@param:Description`). 7 | The description part was not being properly removed from the command after substitution. 8 | - Fixed an unused assignment warning in the `prompt_parameters` function. 9 | 10 | ### Added 11 | - Added debug logging to help troubleshoot parameter substitution. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "command-vault" 3 | version = "0.3.0" 4 | edition = "2021" 5 | authors = ["Ozan Kaşikci"] 6 | license = "MIT" 7 | description = "An advanced command history manager with tagging and search capabilities" 8 | repository = "https://github.com/ozan/command-vault" 9 | documentation = "https://docs.rs/command-vault" 10 | readme = "README.md" 11 | keywords = ["cli", "command", "history", "commandline", "utilities" ] 12 | categories = ["command-line-utilities", "development-tools"] 13 | 14 | [package.metadata.docs.rs] 15 | all-features = true 16 | rustdoc-args = ["--cfg", "docsrs"] 17 | 18 | [dependencies] 19 | anyhow = "1.0" 20 | clap = { version = "4.4", features = ["derive"] } 21 | rusqlite = { version = "0.30", features = ["bundled"] } 22 | dirs = "5.0" 23 | chrono = { version = "0.4", features = ["serde"] } 24 | serde = { version = "1.0", features = ["derive"] } 25 | serde_json = "1.0" 26 | ratatui = "0.24.0" 27 | crossterm = "0.27.0" 28 | atty = "0.2" 29 | dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } 30 | regex = "1.10.2" 31 | colored = "2.0" 32 | shell-escape = "0.1.5" 33 | 34 | [[bin]] 35 | name = "command-vault" 36 | path = "src/main.rs" 37 | 38 | [dev-dependencies] 39 | tempfile = "3.8.1" 40 | ctor = "0.2.5" 41 | serial_test = "2.0" 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ozan Kaşikci 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PROJECT_SCOPE.md: -------------------------------------------------------------------------------- 1 | # Project Scope 2 | 3 | ## Project Overview 4 | Command Vault is a command manager for storing and executing complex commands. It offers a terminal user interface (TUI) that allows users to: 5 | - Store commands with parameters 6 | - Tag commands for better organization 7 | - Search through stored commands 8 | - Execute commands with parameter substitution 9 | - Cross-shell compatibility 10 | 11 | ## Key Features 12 | - Smart search for finding commands 13 | - Parameter substitution for dynamic command execution 14 | - Tag-based organization 15 | - Cross-shell support (bash, zsh, fish) 16 | - Local SQLite database for storing commands 17 | 18 | ## Maintenance Guidelines 19 | 20 | ### CHANGELOG.md 21 | The CHANGELOG.md file should be updated with each significant change to the codebase: 22 | - New features should be added under "### Added" 23 | - Bug fixes should be added under "### Fixed" 24 | - Breaking changes should be added under "### Changed" 25 | - Deprecated features should be added under "### Deprecated" 26 | 27 | Each entry should include: 28 | - A clear description of the change 29 | - Reference to relevant issue numbers (if applicable) 30 | - Any special notes for users 31 | 32 | ### Parameter Handling 33 | When making changes to parameter handling, be careful to: 34 | 1. Maintain the format `@param` for simple parameters and `@param:Description` for parameters with descriptions 35 | 2. Ensure descriptions are properly removed from the final command 36 | 3. Test with different types of parameters to ensure proper substitution 37 | 38 | ### Code Structure 39 | The codebase is organized into modules: 40 | - `src/db`: Database interactions and models 41 | - `src/exec`: Command execution 42 | - `src/cli`: Command-line interface 43 | - `src/ui`: Terminal UI components 44 | - `src/utils`: Utility functions 45 | - `src/shell`: Shell integration 46 | 47 | When adding new functionality, place it in the appropriate module. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Command Vault 2 | [![Crates.io](https://img.shields.io/crates/v/command-vault.svg?style=flat-square)](https://crates.io/crates/command-vault) 3 | [![Documentation](https://docs.rs/command-vault/badge.svg)](https://docs.rs/command-vault) 4 | ![example workflow](https://github.com/ozankasikci/command-vault/actions/workflows/test.yml/badge.svg) 5 | [![codecov](https://codecov.io/gh/ozankasikci/command-vault/branch/main/graph/badge.svg)](https://codecov.io/gh/ozankasikci/command-vault) 6 | 7 | Command Vault is a command manager for storing, and executing your complex commands. It provides a user-friendly interface to search, list, and delete commands, as well as tag commands for better organization. 8 | 9 | ## Table of Contents 10 | - [Features](#features) 11 | - [Usage](#usage) 12 | - [Add Commands](#add-commands) 13 | - [Search Commands](#search-commands) 14 | - [List Commands](#list-commands) 15 | - [Delete Commands](#delete-commands) 16 | - [Tag Commands](#tag-commands) 17 | - [Installation](#installation) 18 | - [From Releases](#from-releases) 19 | - [Shell Integration](#shell-integration) 20 | - [Building from Source](#building-from-source) 21 | - [Development](#development) 22 | - [Shell Aliases](#shell-aliases) 23 | - [License](#license) 24 | 25 | ## Features 26 | 27 | - 🔍 Smart search through command history 28 | - 🏷️ Tag commands for better organization 29 | - 🐚 Cross-shell support (Bash, Zsh) 30 | - 💾 Local SQLite database for fast searching 31 | - 🔐 Safe command execution with validation 32 | 33 | ## Usage 34 | 35 | ### Add Commands 36 | ```bash 37 | # Add a command with tags 38 | command-vault add --tags git,deploy -- git push origin main 39 | command-vault add -- echo "Hello, world!" 40 | 41 | # Add a command with parameters 42 | command-vault add "git commit -m @message:Commit message" 43 | command-vault add "curl -X POST @url:API endpoint -d @data:JSON payload" 44 | ``` 45 | ![Add Command](demo/add-command3.gif) 46 | 47 | ### Parameters 48 | You can add dynamic parameters to your commands using the `@parameter` syntax: 49 | - Simple parameter: `@name` 50 | 51 | Examples: 52 | ```bash 53 | # Git commit with message parameter 54 | git commit -m "@message" 55 | ``` 56 | 57 | When executing a command with parameters, Command Vault will prompt you to enter values for each parameter. 58 | 59 | ### Search Commands 60 | ```bash 61 | # Search commands 62 | command-vault search "git push" 63 | ``` 64 | ![Search Commands](demo/search-command.gif) 65 | 66 | ### List Commands 67 | ```bash 68 | # List recent commands 69 | command-vault ls 70 | ``` 71 | ![List Commands](demo/ls-command2.gif) 72 | 73 | ### Delete Commands 74 | ```bash 75 | # Delete a command 76 | command-vault delete 77 | ``` 78 | ![Delete Commands](demo/delete-command.gif) 79 | 80 | ### Tag Commands 81 | ```bash 82 | # Show tag command 83 | command-vault tag # Show tag related commands 84 | command-vault tag list # List tag related commands 85 | ``` 86 | ![Tag Commands](demo/tag-command.gif) 87 | 88 | ## Installation 89 | 90 | ### From Releases 91 | 92 | You can download the latest release for your platform from the [releases page](https://github.com/yourusername/command-vault/releases). 93 | 94 | #### Linux 95 | ```bash 96 | # Download the latest release (replace X.Y.Z with the version number) 97 | curl -LO https://github.com/ozankasikci/command-vault/releases/download/v0.3.0/command-vault-macos-arm64.tar.gz 98 | tar xzf command-vault-macos-arm64.tar.gz 99 | # Make it executable 100 | chmod +x command-vault-linux-amd64 101 | # Move it to your PATH 102 | sudo mv command-vault-linux-amd64 /usr/local/bin/command-vault 103 | 104 | # Initialize shell integration (add to your .bashrc or .zshrc) 105 | source "$(command-vault shell-init)" 106 | ``` 107 | 108 | #### macOS 109 | ```bash 110 | # Download the latest release (replace X.Y.Z with the version number) 111 | curl -LO https://github.com/ozankasikci/command-vault/releases/download/v0.3.0/command-vault-macos-arm64.tar.gz 112 | tar xzf command-vault-macos-arm64.tar.gz 113 | # Make it executable 114 | chmod +x command-vault-macos-arm64 115 | # Move it to your PATH 116 | sudo mv command-vault-macos-arm64 /usr/local/bin/command-vault 117 | # Initialize shell integration (add to your .bashrc or .zshrc) 118 | source "$(command-vault shell-init)" 119 | ``` 120 | 121 | #### Windows 122 | Download the Windows executable from the releases page and add it to your PATH. 123 | 124 | ### Shell Integration 125 | 126 | Command Vault needs to be integrated with your shell to automatically track commands. Add this to your shell's RC file: 127 | 128 | ```bash 129 | # For Bash (~/.bashrc) 130 | source "$(command-vault shell-init)" 131 | 132 | # For Zsh (~/.zshrc) 133 | source "$(command-vault shell-init)" 134 | ``` 135 | 136 | ### Building from Source 137 | 138 | If you prefer to build from source, you'll need Rust installed on your system: 139 | 140 | ```bash 141 | # Clone the repository 142 | git clone https://github.com/yourusername/command-vault.git 143 | cd command-vault 144 | 145 | # Build the project 146 | cargo build --release 147 | 148 | # The binary will be available in target/release/command-vault 149 | ``` 150 | 151 | Add the following to your shell's configuration file (`~/.bashrc` or `~/.zshrc`): 152 | ```bash 153 | source "$(command-vault shell-init)" 154 | ``` 155 | 156 | ## Development 157 | 158 | ### Running Tests 159 | ```bash 160 | cargo test 161 | ``` 162 | 163 | ### Code Coverage 164 | ```bash 165 | # Generate coverage report (requires cargo-tarpaulin) 166 | ./scripts/coverage.sh 167 | 168 | # View the report in your browser 169 | open coverage/tarpaulin-report.html 170 | ``` 171 | 172 | ## Shell Aliases 173 | 174 | For easier access, you can add aliases to your shell configuration: 175 | 176 | ### For Bash/Zsh (add to ~/.zshrc or ~/.bashrc) 177 | ```bash 178 | # to use as cmdv: 179 | alias cmdv='command-vault' 180 | # or to use as cv: 181 | alias cv='command-vault' 182 | ``` 183 | 184 | After adding the aliases, restart your shell or run: 185 | ```bash 186 | source ~/.zshrc # for Zsh 187 | source ~/.bashrc # for Bash 188 | ``` 189 | 190 | Now you can use shorter commands: 191 | ```bash 192 | cv add 'echo Hello' 193 | cmdv ls 194 | ``` 195 | 196 | ## License 197 | 198 | This project is licensed under the MIT License - see the LICENSE file for details. 199 | -------------------------------------------------------------------------------- /demo.yml: -------------------------------------------------------------------------------- 1 | # The configurations that used for the recording, feel free to edit them 2 | config: 3 | 4 | # Specify a command to be executed 5 | # like `/bin/bash -l`, `ls`, or any other commands 6 | # the default is bash for Linux 7 | # or powershell.exe for Windows 8 | command: bash -l 9 | 10 | # Specify the current working directory path 11 | # the default is the current working directory path 12 | cwd: /Users/ozan/Projects/command-vault 13 | 14 | # Export additional ENV variables 15 | env: 16 | recording: true 17 | 18 | # Explicitly set the number of columns 19 | # or use `auto` to take the current 20 | # number of columns of your shell 21 | cols: 96 22 | 23 | # Explicitly set the number of rows 24 | # or use `auto` to take the current 25 | # number of rows of your shell 26 | rows: 26 27 | 28 | # Amount of times to repeat GIF 29 | # If value is -1, play once 30 | # If value is 0, loop indefinitely 31 | # If value is a positive number, loop n times 32 | repeat: 0 33 | 34 | # Quality 35 | # 1 - 100 36 | quality: 100 37 | 38 | # Delay between frames in ms 39 | # If the value is `auto` use the actual recording delays 40 | frameDelay: auto 41 | 42 | # Maximum delay between frames in ms 43 | # Ignored if the `frameDelay` isn't set to `auto` 44 | # Set to `auto` to prevent limiting the max idle time 45 | maxIdleTime: 2000 46 | 47 | # The surrounding frame box 48 | # The `type` can be null, window, floating, or solid` 49 | # To hide the title use the value null 50 | # Don't forget to add a backgroundColor style with a null as type 51 | frameBox: 52 | type: floating 53 | title: Terminalizer 54 | style: 55 | border: 0px black solid 56 | # boxShadow: none 57 | # margin: 0px 58 | 59 | # Add a watermark image to the rendered gif 60 | # You need to specify an absolute path for 61 | # the image on your machine or a URL, and you can also 62 | # add your own CSS styles 63 | watermark: 64 | imagePath: null 65 | style: 66 | position: absolute 67 | right: 15px 68 | bottom: 15px 69 | width: 100px 70 | opacity: 0.9 71 | 72 | # Cursor style can be one of 73 | # `block`, `underline`, or `bar` 74 | cursorStyle: block 75 | 76 | # Font family 77 | # You can use any font that is installed on your machine 78 | # in CSS-like syntax 79 | fontFamily: "Monaco, Lucida Console, Ubuntu Mono, Monospace" 80 | 81 | # The size of the font 82 | fontSize: 12 83 | 84 | # The height of lines 85 | lineHeight: 1 86 | 87 | # The spacing between letters 88 | letterSpacing: 0 89 | 90 | # Theme 91 | theme: 92 | background: "transparent" 93 | foreground: "#afafaf" 94 | cursor: "#c7c7c7" 95 | black: "#232628" 96 | red: "#fc4384" 97 | green: "#b3e33b" 98 | yellow: "#ffa727" 99 | blue: "#75dff2" 100 | magenta: "#ae89fe" 101 | cyan: "#708387" 102 | white: "#d5d5d0" 103 | brightBlack: "#626566" 104 | brightRed: "#ff7fac" 105 | brightGreen: "#c8ed71" 106 | brightYellow: "#ebdf86" 107 | brightBlue: "#75dff2" 108 | brightMagenta: "#ae89fe" 109 | brightCyan: "#b1c6ca" 110 | brightWhite: "#f9f9f4" 111 | 112 | # Records, feel free to edit them 113 | records: 114 | - delay: 238 115 | content: "\r\nThe default interactive shell is now zsh.\r\nTo update your account to use zsh, please run `chsh -s /bin/zsh`.\r\nFor more details, please visit https://support.apple.com/kb/HT208050.\r\n\e[?1034hOzans-MacBook-Pro:command-vault ozan$ " 116 | - delay: 11912 117 | content: chsh -s /bin/zsh 118 | - delay: 320 119 | content: "\r\n" 120 | - delay: 159 121 | content: "Changing shell for ozan.\r\n" 122 | - delay: 9 123 | content: 'Password for ozan: ' 124 | - delay: 2893 125 | content: "\r\n" 126 | - delay: 183 127 | content: "chsh: no changes made\r\nOzans-MacBook-Pro:command-vault ozan$ " 128 | - delay: 9925 129 | content: terminalizer record demo 130 | - delay: 479 131 | content: "\r\n" 132 | - delay: 371 133 | content: "innerError Error: Cannot find module '../build/Debug/pty.node'\r\nRequire stack:\r\n- /Users/ozan/.nvm/versions/node/v22.12.0/lib/node_modules/terminalizer/node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/prebuild-loader.js\r\n- /Users/ozan/.nvm/versions/node/v22.12.0/lib/node_modules/terminalizer/node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/unixTerminal.js\r\n- /Users/ozan/.nvm/versions/node/v22.12.0/lib/node_modules/terminalizer/node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/index.js\r\n- /Users/ozan/.nvm/versions/node/v22.12.0/lib/node_modules/terminalizer/app.js\r\n- /Users/ozan/.nvm/versions/node/v22.12.0/lib/node_modules/terminalizer/bin/app.js\r\n\e[90m at Module._resolveFilename (node:internal/modules/cjs/loader:1077:15)\e[39m\r\n\e[90m at Module._load (node:internal/modules/cjs/loader:922:27)\e[39m\r\n\e[90m at Module.require (node:internal/modules/cjs/loader:1143:19)\e[39m\r\n\e[90m at require (node:internal/modules/cjs/helpers:121:18)\e[39m\r\n at Object. (/Users/ozan/.nvm/versions/node/v22.12.0/lib/node_modules/\e[4mterminalizer\e[24m/node_modules/\e[4m@homebridge\e[24m/node-pty-prebuilt-multiarch/lib/prebuild-loader.js:10:15)\r\n\e[90m at Module._compile (node:internal/modules/cjs/loader:1256:14)\e[39m\r\n\e[90m at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)\e[39m\r\n\e[90m at Module.load (node:internal/modules/cjs/loader:1119:32)\e[39m\r\n\e[90m at Module._load (node:internal/modules/cjs/loader:960:12)\e[39m\r\n\e[90m at Module.require (node:internal/modules/cjs/loader:1143:19)\e[39m {\r\n code: \e[32m'MODULE_NOT_FOUND'\e[39m,\r\n requireStack: [\r\n \e[32m'/Users/ozan/.nvm/versions/node/v22.12.0/lib/node_modules/terminalizer/node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/prebuild-loader.js'\e[39m,\r\n \e[32m'/Users/ozan/.nvm/versions/node/v22.12.0/lib/node_modules/terminalizer/node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/unixTerminal.js'\e[39m,\r\n \e[32m'/Users/ozan/.nvm/versions/node/v22.12.0/lib/node_modules/terminalizer/node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/index.js'\e[39m,\r\n \e[32m'/Users/ozan/.nvm/versions/node/v22.12.0/lib/node_modules/terminalizer/app.js'\e[39m,\r\n \e[32m'/Users/ozan/.nvm/versions/node/v22.12.0/lib/node_modules/terminalizer/bin/app.js'\e[39m\r\n ]\r\n}\r\n/Users/ozan/.nvm/versions/node/v22.12.0/lib/node_modules/terminalizer/node_modules/@homebridge/node-pty-prebuilt-multiarch/lib/prebuild-loader.js:15\r\n throw outerError;\r\n ^\r\n\r\nError: The module '/Users/ozan/.nvm/versions/node/v22.12.0/lib/node_modules/terminalizer/node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release/pty.node'\r\nwas compiled against a different Node.js version using\r\nNODE_MODULE_VERSION 127. This version of Node.js requires\r\nNODE_MODULE_VERSION 108. Please try re-compiling or re-installing\r\nthe module (for instance, using `npm rebuild` or `npm install`).\r\n\e[90m at Module._extensions..node (node:internal/modules/cjs/loader:1340:18)\e[39m\r\n\e[90m at Module.load (node:internal/modules/cjs/loader:1119:32)\e[39m\r\n\e[90m at Module._load (node:internal/modules/cjs/loader:960:12)\e[39m\r\n\e[90m at Module.require (node:internal/modules/cjs/loader:1143:19)\e[39m\r\n\e[90m at require (node:internal/modules/cjs/helpers:121:18)\e[39m\r\n at Object. (/Users/ozan/.nvm/versions/node/v22.12.0/lib/node_modules/\e[4mterminalizer\e[24m/node_modules/\e[4m@homebridge\e[24m/node-pty-prebuilt-multiarch/lib/prebuild-loader.js:6:11)\r\n\e[90m at Module._compile (node:internal/modules/cjs/loader:1256:14)\e[39m\r\n\e[90m at Module._extensions..js (node:internal/modules/cjs/loader:1310:10)\e[39m\r\n\e[90m at Module.load (node:internal/modules/cjs/loader:1119:32)\e[39m\r\n\e[90m at Module._load (node:internal/modules/cjs/loader:960:12)\e[39m {\r\n code: \e[32m'ERR_DLOPEN_FAILED'\e[39m\r\n}\r\n\r\nNode.js v18.17.1\r\nOzans-MacBook-Pro:command-vault ozan$ " 134 | - delay: 27550 135 | content: "\r\nOzans-MacBook-Pro:command-vault ozan$ " 136 | - delay: 1413 137 | content: "logout\r\n" -------------------------------------------------------------------------------- /demo/add-command.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozankasikci/command-vault/75d786a5792f994358edd7f50ca01b3c99f25160/demo/add-command.gif -------------------------------------------------------------------------------- /demo/add-command2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozankasikci/command-vault/75d786a5792f994358edd7f50ca01b3c99f25160/demo/add-command2.gif -------------------------------------------------------------------------------- /demo/add-command3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozankasikci/command-vault/75d786a5792f994358edd7f50ca01b3c99f25160/demo/add-command3.gif -------------------------------------------------------------------------------- /demo/delete-command.cast: -------------------------------------------------------------------------------- 1 | {"version": 2, "width": 98, "height": 17, "timestamp": 1734275413, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}} 2 | [0.864089, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r\u001b]2;ozan@Ozans-MacBook-Pro:~/Projects/command-vault\u0007\u001b]1;..command-vault\u0007"] 3 | [0.968259, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜ \u001b[36mcommand-vault\u001b[00m \u001b[01;34mgit:(\u001b[31mmain\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"] 4 | [0.968467, "o", "\u001b[?1h\u001b=\u001b[?2004h"] 5 | [1.871297, "o", "c"] 6 | [2.024346, "o", "\bcv"] 7 | [2.125925, "o", " "] 8 | [2.709242, "o", "l"] 9 | [3.257245, "o", "s"] 10 | [4.218634, "o", "\u001b[?1l\u001b>"] 11 | [4.218688, "o", "\u001b[?2004l\r\r\n"] 12 | [4.220233, "o", "\u001b]2;command-vault ls\u0007\u001b]1;cv\u0007"] 13 | [4.235895, "o", "\u001b[?1049h"] 14 | [4.236276, "o", "\u001b[?25l"] 15 | [4.237213, "o", "\u001b[2;2H\u001b[38;5;6m┌──────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[3;2H│Command Vault │\u001b[4;2H└──────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[5;2H\u001b[39m┌Commands──────────────────────────────────────────────────────────────────────────────────────┐\u001b[6;2H│\u001b[38;5;8m(10) \u001b[38;5;3m[2024-"] 16 | [4.23731, "o", "12-15 18:06:43] \u001b[39mecho\u001b[6;35Hcommand\u001b[6;43Hvault\u001b[6;49His\u001b[6;52Hawesome\u001b[6;97H│\u001b[7;2H│\u001b[38;5;8m(9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit\u001b[7;33Hlog\u001b[7;37H--oneline\u001b[7;47H--graph\u001b[7;55H--all\u001b[7;61H--decorate\u001b[7;72H--color\u001b[7;97H│\u001b[8;2H│\u001b[38;5;8m(8) \u001b[38;5;3m[2024-12-15 17:07:35] \u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local\u001b[8;65HModels\u001b[8;72Hollama\u001b[8;79Hrun\u001b[8;83Hllama3.2\u001b[8;92H\u001b[38;5;2m#ai \u001b[8;97H\u001b[39m│\u001b[9;2H│\u001b[38;5;8m(7) \u001b[38;5;3m[2024-12-15 16:24:04] \u001b[39mcargo\u001b[9;35Hinstall\u001b[9;43H--path\u001b[9;50H.\u001b[9;52H\u001b[38;5;2m#rust \u001b[9;97H\u001b[39m│\u001b[10;2H│\u001b[38;5;8m(6) \u001b[38;5;3m[2024-12-15 14:57:37] \u001b[39mdocker-compose\u001b[10;44H-f\u001b[10;47Hdocker-compose.prod.yml\u001b[10;71H--env-file\u001b[10;82H.env.production│\u001b[11;2H│\u001b[38;5;8m(5) \u001b[38;5;3m[2024-12-15 14:57:26] \u001b[39mdocker\u001b[11;36Hsystem\u001b[11;43Hprune\u001b[11;49H-af\u001b[11;53H--volumes\u001b[11;63H\u001b[38;5;2m#docker,cleanup \u001b[11;97H\u001b[39m│\u001b[12;2H└─────────────────────────────────────────────"] 17 | [4.237363, "o", "─────────────────────────────────────────────────┘\u001b[14;2H\u001b[38;5;7m┌──────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[15;2H│Press \u001b[38;5;3m?\u001b[38;5;7m for help │\u001b[16;2H└──────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 18 | [4.909899, "o", "\u001b[6;3H\u001b[7m\u001b[38;5;8m(10) \u001b[38;5;3m[2024-12-15 18:06:43] \u001b[39mecho command vault is awesome \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 19 | [5.385369, "o", "\u001b[6;3H\u001b[38;5;8m(10) \u001b[38;5;3m[2024-12-15 18:06:43] \u001b[39mecho command vault is awesome \u001b[7;3H\u001b[7m\u001b[38;5;8m(9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit log --oneline --graph --all --decorate --color \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 20 | [6.290082, "o", "\u001b[6;3H\u001b[7m\u001b[38;5;8m(10) \u001b[38;5;3m[2024-12-15 18:06:43] \u001b[39mecho command vault is awesome \u001b[7;3H\u001b[27m\u001b[38;5;8m(9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit log --oneline --graph --all --decorate --color \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 21 | [6.992432, "o", "\u001b[6;4H\u001b[7m\u001b[38;5;8m9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit log --o\u001b[6;41Heline --graph --all\u001b[6;61H--decorate\u001b[6;72H--color\u001b[7;4H\u001b[27m\u001b[38;5;8m8\u001b[7;20H\u001b[38;5;3m7\u001b[7;23H7\u001b[7;26H5\u001b[7;29H\u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local M\u001b[7;67Hdels\u001b[7;72Hollama run\u001b[7;83Hllama3.2\u001b[7;92H\u001b[38;5;2m#ai \u001b[8;4H\u001b[38;5;8m7\u001b[8;20H\u001b[38;5;3m6\u001b[8;22H24\u001b[8;25H04\u001b[8;29H\u001b[39mcargo install --path . \u001b[38;5;2m#rust \u001b[39m \u001b[8;65H \u001b[8;72H \u001b[8;79H \u001b[8;83H \u001b[8;92H \u001b[9;4H\u001b[38;5;8m6\u001b[9;20H\u001b[38;5;3m4\u001b[9;22H57\u001b[9;25H37\u001b[9;29H\u001b[39mdocker-compose \u001b[9;45Hf docker-compose.prod.yml\u001b[9;71H--env-file\u001b[9;82H.env.production\u001b[10;4H\u001b[38;5;8m5\u001b[10;25H\u001b[38;5;3m26\u001b[10;35H\u001b[39m system prune -af \u001b[10;54H-volum\u001b[10;61Hs \u001b[38;5;2m#docker,cleanup \u001b[39m \u001b[10;82H \u001b[11;4H\u001b[38;5;8m4\u001b[11;25H\u001b[38;5;3m08\u001b[11;29H\u001b[39mgit log --graph --pretty=format:\"%Cred%h%Creset -%C(yellow)%d%Creset\u001b[15;3H\u001b[38;5;2mCommand deleted successfully\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 22 | [9.810696, "o", "\u001b[6;3H\u001b[38;5;8m(9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit log --oneline --graph --all --decorate --color \u001b[7;3H\u001b[7m\u001b[38;5;8m(8) \u001b[38;5;3m[2024-12-15 17:07:35] \u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local Models ollama run llama3.2 \u001b[38;5;2m#ai \u001b[39m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 23 | [10.864405, "o", "\u001b[6;3H\u001b[7m\u001b[38;5;8m(9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit log --oneline --graph --all --decorate --color \u001b[7;3H\u001b[27m\u001b[38;5;8m(8) \u001b[38;5;3m[2024-12-15 17:07:35] \u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local Models ollama run llama3.2 \u001b[38;5;2m#ai \u001b[39m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 24 | [11.341574, "o", "\u001b[6;4H\u001b[7m\u001b[38;5;8m8\u001b[6;20H\u001b[38;5;3m7\u001b[6;23H7\u001b[6;26H5\u001b[6;29H\u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local M\u001b[6;67Hdels\u001b[6;72Hollama run\u001b[6;83Hllama3.2\u001b[6;92H\u001b[38;5;2m#ai \u001b[7;4H\u001b[27m\u001b[38;5;8m7\u001b[7;20H\u001b[38;5;3m6\u001b[7;22H24\u001b[7;25H04\u001b[7;29H\u001b[39mcargo install --path . \u001b[38;5;2m#rust \u001b[39m \u001b[7;65H \u001b[7;72H \u001b[7;79H \u001b[7;83H \u001b[7;92H \u001b[8;4H\u001b[38;5;8m6\u001b[8;20H\u001b[38;5;3m4\u001b[8;22H57\u001b[8;25H37\u001b[8;29H\u001b[39mdocker-compose \u001b[8;45Hf docker-compose.prod.yml\u001b[8;71H--env-file\u001b[8;82H.env.production\u001b[9;4H\u001b[38;5;8m5\u001b[9;25H\u001b[38;5;3m26\u001b[9;35H\u001b[39m system prune -af \u001b[9;54H-volum\u001b[9;61Hs \u001b[38;5;2m#docker,cleanup \u001b[39m \u001b[9;82H \u001b[10;4H\u001b[38;5;8m4\u001b[10;25H\u001b[38;5;3m08\u001b[10;29H\u001b[39mgit log --graph --pretty=format:\"%Cred%h%Creset -%C(yellow)%d%Creset\u001b[11;4H\u001b[38;5;8m3\u001b[11;23H\u001b[38;5;3m6\u001b[11;26H3\u001b[11;33H\u001b[39mreset --soft HEAD~1 \u001b[38;5;2m#git \u001b[39m \u001b[11;77H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 25 | [13.295578, "o", "\u001b[?25h\u001b[?1049l"] 26 | [13.299062, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] 27 | [13.299189, "o", "\u001b]2;ozan@Ozans-MacBook-Pro:~/Projects/command-vault\u0007\u001b]1;..command-vault\u0007"] 28 | [13.411456, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜ \u001b[36mcommand-vault\u001b[00m \u001b[01;34mgit:(\u001b[31mmain\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"] 29 | [13.411621, "o", "\u001b[?1h\u001b=\u001b[?2004h"] 30 | [13.916399, "o", "c"] 31 | [14.015642, "o", "\bcv"] 32 | [14.102038, "o", " "] 33 | [14.28185, "o", "l"] 34 | [14.431461, "o", "s"] 35 | [14.79025, "o", "\u001b[?1l\u001b>"] 36 | [14.790371, "o", "\u001b[?2004l\r\r\n"] 37 | [14.791301, "o", "\u001b]2;command-vault ls\u0007\u001b]1;cv\u0007"] 38 | [14.799774, "o", "\u001b[?1049h"] 39 | [14.799948, "o", "\u001b[?25l"] 40 | [14.800352, "o", "\u001b[2;2H\u001b[38;5;6m┌──────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[3;2H│Command Vault │\u001b[4;2H└──────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[5;2H\u001b[39m┌Commands──────────────────────────────────────────────────────────────────────────────────────┐\u001b[6;2H│\u001b[38;5;8m(8) \u001b[38;5;3m[2024-1"] 41 | [14.800436, "o", "2-15 17:07:35] \u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local\u001b[6;65HModels\u001b[6;72Hollama\u001b[6;79Hrun\u001b[6;83Hllama3.2\u001b[6;92H\u001b[38;5;2m#ai \u001b[6;97H\u001b[39m│\u001b[7;2H│\u001b[38;5;8m(7) \u001b[38;5;3m[2024-12-15 16:24:04] \u001b[39mcargo\u001b[7;35Hinstall\u001b[7;43H--path\u001b[7;50H.\u001b[7;52H\u001b[38;5;2m#rust \u001b[7;97H\u001b[39m│\u001b[8;2H│\u001b[38;5;8m(6) \u001b[38;5;3m[2024-12-15 14:57:37] \u001b[39mdocker-compose\u001b[8;44H-f\u001b[8;47Hdocker-compose.prod.yml\u001b[8;71H--env-file\u001b[8;82H.env.production│\u001b[9;2H│\u001b[38;5;8m(5) \u001b[38;5;3m[2024-12-15 14:57:26] \u001b[39mdocker\u001b[9;36Hsystem\u001b[9;43Hprune\u001b[9;49H-af\u001b[9;53H--volumes\u001b[9;63H\u001b[38;5;2m#docker,cleanup \u001b[9;97H\u001b[39m│\u001b[10;2H│\u001b[38;5;8m(4) \u001b[38;5;3m[2024-12-15 14:57:08] \u001b[39mgit\u001b[10;33Hlog\u001b[10;37H--graph\u001b[10;45H--pretty=format:\"%Cred%h%Creset\u001b[10;77H-%C(yellow)%d%Creset│\u001b[11;2H│\u001b[38;5;8m(3) \u001b[38;5;3m[2024-12-15 14:56:03] \u001b[39mgit\u001b[11;33Hreset\u001b[11;39H--soft\u001b[11;46HHEAD~1\u001b[11;53H\u001b[38;5;2m#git \u001b[11;97H\u001b[39m│\u001b[12;2H└─────────────────────────────────────────"] 42 | [14.800514, "o", "─────────────────────────────────────────────────────┘\u001b[14;2H\u001b[38;5;7m┌──────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[15;2H│Press \u001b[38;5;3m?\u001b[38;5;7m for help │\u001b[16;2H└──────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 43 | [16.379143, "o", "\u001b[?25h\u001b[?1049l"] 44 | [16.382042, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] 45 | [16.382406, "o", "\u001b]2;ozan@Ozans-MacBook-Pro:~/Projects/command-vault\u0007"] 46 | [16.382443, "o", "\u001b]1;..command-vault\u0007"] 47 | [16.502204, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜ \u001b[36mcommand-vault\u001b[00m \u001b[01;34mgit:(\u001b[31mmain\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"] 48 | [16.50236, "o", "\u001b[?1h\u001b="] 49 | [16.502396, "o", "\u001b[?2004h"] 50 | [18.010248, "o", "\u001b[?2004l\r\r\n"] 51 | -------------------------------------------------------------------------------- /demo/delete-command.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozankasikci/command-vault/75d786a5792f994358edd7f50ca01b3c99f25160/demo/delete-command.gif -------------------------------------------------------------------------------- /demo/ls-command.cast: -------------------------------------------------------------------------------- 1 | {"version": 2, "width": 98, "height": 17, "timestamp": 1734275280, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}} 2 | [0.84807, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r\u001b]2;ozan@Ozans-MacBook-Pro:~/Projects/command-vault\u0007\u001b]1;..command-vault\u0007"] 3 | [0.953517, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜ \u001b[36mcommand-vault\u001b[00m \u001b[01;34mgit:(\u001b[31mmain\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"] 4 | [0.95369, "o", "\u001b[?1h\u001b="] 5 | [0.953749, "o", "\u001b[?2004h"] 6 | [1.53922, "o", "c"] 7 | [1.718495, "o", "\bcv"] 8 | [2.021525, "o", " "] 9 | [2.204224, "o", "l"] 10 | [2.567519, "o", "s"] 11 | [3.096552, "o", "\u001b[?1l\u001b>\u001b[?2004l"] 12 | [3.096624, "o", "\r\r\n"] 13 | [3.09865, "o", "\u001b]2;command-vault ls\u0007"] 14 | [3.098953, "o", "\u001b]1;cv\u0007"] 15 | [3.119409, "o", "\u001b[?1049h"] 16 | [3.120094, "o", "\u001b[?25l"] 17 | [3.121074, "o", "\u001b[2;2H\u001b[38;5;6m┌──────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[3;2H│Command Vault │\u001b[4;2H└──────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[5;2H\u001b[39m┌Commands──────────────────────────────────────────────────────────────────────────────────────┐\u001b[6;2H│\u001b[38;5;8m(10) \u001b[38;5;3m[2024-"] 18 | [3.121124, "o", "12-15 18:06:43] \u001b[39mecho\u001b[6;35Hcommand\u001b[6;43Hvault\u001b[6;49His\u001b[6;52Hawesome\u001b[6;97H│\u001b[7;2H│\u001b[38;5;8m(9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit\u001b[7;33Hlog\u001b[7;37H--oneline\u001b[7;47H--graph\u001b[7;55H--all\u001b[7;61H--decorate\u001b[7;72H--color\u001b[7;97H│\u001b[8;2H│\u001b[38;5;8m(8) \u001b[38;5;3m[2024-12-15 17:07:35] \u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local\u001b[8;65HModels\u001b[8;72Hollama\u001b[8;79Hrun\u001b[8;83Hllama3.2\u001b[8;92H\u001b[38;5;2m#ai \u001b[8;97H\u001b[39m│\u001b[9;2H│\u001b[38;5;8m(7) \u001b[38;5;3m[2024-12-15 16:24:04] \u001b[39mcargo\u001b[9;35Hinstall\u001b[9;43H--path\u001b[9;50H.\u001b[9;52H\u001b[38;5;2m#rust \u001b[9;97H\u001b[39m│\u001b[10;2H│\u001b[38;5;8m(6) \u001b[38;5;3m[2024-12-15 14:57:37] \u001b[39mdocker-compose\u001b[10;44H-f\u001b[10;47Hdocker-compose.prod.yml\u001b[10;71H--env-file\u001b[10;82H.env.production│\u001b[11;2H│\u001b[38;5;8m(5) \u001b[38;5;3m[2024-12-15 14:57:26] \u001b[39mdocker\u001b[11;36Hsystem\u001b[11;43Hprune\u001b[11;49H-af\u001b[11;53H--volumes\u001b[11;63H\u001b[38;5;2m#docker,cleanup \u001b[11;97H\u001b[39m│\u001b[12;2H└─────────────────────────────────────────────"] 19 | [3.121256, "o", "─────────────────────────────────────────────────┘\u001b[14;2H\u001b[38;5;7m┌──────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[15;2H│Press \u001b[38;5;3m?\u001b[38;5;7m for help │\u001b[16;2H└──────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 20 | [3.711631, "o", "\u001b[6;3H\u001b[7m\u001b[38;5;8m(10) \u001b[38;5;3m[2024-12-15 18:06:43] \u001b[39mecho command vault is awesome \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 21 | [4.700081, "o", "\u001b[6;3H\u001b[38;5;8m(10) \u001b[38;5;3m[2024-12-15 18:06:43] \u001b[39mecho command vault is awesome \u001b[7;3H\u001b[7m\u001b[38;5;8m(9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit log --oneline --graph --all --decorate --color \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 22 | [4.906595, "o", "\u001b[7;3H\u001b[38;5;8m(9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit log --oneline --graph --all --decorate --color \u001b[8;3H\u001b[7m\u001b[38;5;8m(8) \u001b[38;5;3m[2024-12-15 17:07:35] \u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local Models ollama run llama3.2 \u001b[38;5;2m#ai \u001b[39m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 23 | [5.129664, "o", "\u001b[8;3H\u001b[38;5;8m(8) \u001b[38;5;3m[2024-12-15 17:07:35] \u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local Models ollama run llama3.2 \u001b[38;5;2m#ai \u001b[39m \u001b[9;3H\u001b[7m\u001b[38;5;8m(7) \u001b[38;5;3m[2024-12-15 16:24:04] \u001b[39mcargo install --path . \u001b[38;5;2m#rust \u001b[39m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 24 | [5.290805, "o", "\u001b[9;3H\u001b[38;5;8m(7) \u001b[38;5;3m[2024-12-15 16:24:04] \u001b[39mcargo install --path . \u001b[38;5;2m#rust \u001b[39m \u001b[10;3H\u001b[7m\u001b[38;5;8m(6) \u001b[38;5;3m[2024-12-15 14:57:37] \u001b[39mdocker-compose -f docker-compose.prod.yml --env-file .env.production\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 25 | [5.47685, "o", "\u001b[10;3H\u001b[38;5;8m(6) \u001b[38;5;3m[2024-12-15 14:57:37] \u001b[39mdocker-compose -f docker-compose.prod.yml --env-file .env.production\u001b[11;3H\u001b[7m\u001b[38;5;8m(5) \u001b[38;5;3m[2024-12-15 14:57:26] \u001b[39mdocker system prune -af --volumes \u001b[38;5;2m#docker,cleanup \u001b[39m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 26 | [5.660094, "o", "\u001b[6;4H\u001b[38;5;8m9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit log --o\u001b[6;41Heline --graph --all\u001b[6;61H--decorate\u001b[6;72H--color\u001b[7;4H\u001b[38;5;8m8\u001b[7;20H\u001b[38;5;3m7\u001b[7;23H7\u001b[7;26H5\u001b[7;29H\u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local M\u001b[7;67Hdels\u001b[7;72Hollama run\u001b[7;83Hllama3.2\u001b[7;92H\u001b[38;5;2m#ai \u001b[8;4H\u001b[38;5;8m7\u001b[8;20H\u001b[38;5;3m6\u001b[8;22H24\u001b[8;25H04\u001b[8;29H\u001b[39mcargo install --path . \u001b[38;5;2m#rust \u001b[39m \u001b[8;65H \u001b[8;72H \u001b[8;79H \u001b[8;83H \u001b[8;92H \u001b[9;4H\u001b[38;5;8m6\u001b[9;20H\u001b[38;5;3m4\u001b[9;22H57\u001b[9;25H37\u001b[9;29H\u001b[39mdocker-compose \u001b[9;45Hf docker-compose.prod.yml\u001b[9;71H--env-file\u001b[9;82H.env.production\u001b[10;4H\u001b[38;5;8m5\u001b[10;25H\u001b[38;5;3m26\u001b[10;35H\u001b[39m system prune -af \u001b[10;54H-volum\u001b[10;61Hs \u001b[38;5;2m#docker,cleanup \u001b[39m \u001b[10;82H \u001b[11;4H\u001b[7m\u001b[38;5;8m4\u001b[11;25H\u001b[38;5;3m08\u001b[11;29H\u001b[39mgit log --graph --pretty=format:\"%Cred%h%Creset -%C(yellow)%d%Creset\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 27 | [6.043755, "o", "\u001b[6;4H\u001b[38;5;8m8\u001b[6;20H\u001b[38;5;3m7\u001b[6;23H7\u001b[6;26H5\u001b[6;29H\u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local M\u001b[6;67Hdels\u001b[6;72Hollama run\u001b[6;83Hllama3.2\u001b[6;92H\u001b[38;5;2m#ai \u001b[7;4H\u001b[38;5;8m7\u001b[7;20H\u001b[38;5;3m6\u001b[7;22H24\u001b[7;25H04\u001b[7;29H\u001b[39mcargo install --path . \u001b[38;5;2m#rust \u001b[39m \u001b[7;65H \u001b[7;72H \u001b[7;79H \u001b[7;83H \u001b[7;92H \u001b[8;4H\u001b[38;5;8m6\u001b[8;20H\u001b[38;5;3m4\u001b[8;22H57\u001b[8;25H37\u001b[8;29H\u001b[39mdocker-compose \u001b[8;45Hf docker-compose.prod.yml\u001b[8;71H--env-file\u001b[8;82H.env.production\u001b[9;4H\u001b[38;5;8m5\u001b[9;25H\u001b[38;5;3m26\u001b[9;35H\u001b[39m system prune -af \u001b[9;54H-volum\u001b[9;61Hs \u001b[38;5;2m#docker,cleanup \u001b[39m \u001b[9;82H \u001b[10;4H\u001b[38;5;8m4\u001b[10;25H\u001b[38;5;3m08\u001b[10;29H\u001b[39mgit log --graph --pretty=format:\"%Cred%h%Creset -%C(yellow)%d%Creset\u001b[11;4H\u001b[7m\u001b[38;5;8m3\u001b[11;23H\u001b[38;5;3m6\u001b[11;26H3\u001b[11;33H\u001b[39mreset --soft HEAD~1 \u001b[38;5;2m#git \u001b[39m \u001b[11;77H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 28 | [6.232779, "o", "\u001b[6;4H\u001b[38;5;8m9\u001b[6;20H\u001b[38;5;3m8\u001b[6;23H6\u001b[6;26H2\u001b[6;29H\u001b[39mgit log --oneline --graph --all --dec\u001b[6;67Hrate\u001b[6;72H--color \u001b[6;83H \u001b[6;92H \u001b[7;4H\u001b[38;5;8m8\u001b[7;20H\u001b[38;5;3m7\u001b[7;22H07\u001b[7;25H35\u001b[7;29H\u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local\u001b[7;65HModels\u001b[7;72Hollama\u001b[7;79Hrun\u001b[7;83Hllama3.2\u001b[7;92H\u001b[38;5;2m#ai \u001b[8;4H\u001b[38;5;8m7\u001b[8;20H\u001b[38;5;3m6\u001b[8;22H24\u001b[8;25H04\u001b[8;29H\u001b[39mcargo install -\u001b[8;45Hpath . \u001b[38;5;2m#rust \u001b[39m \u001b[8;71H \u001b[8;82H \u001b[9;4H\u001b[38;5;8m6\u001b[9;25H\u001b[38;5;3m37\u001b[9;35H\u001b[39m-compose -f docker\u001b[9;54Hcompos\u001b[9;61H.prod.yml --env-file\u001b[9;82H.env.production\u001b[10;4H\u001b[38;5;8m5\u001b[10;25H\u001b[38;5;3m26\u001b[10;29H\u001b[39mdocker system prune -af --volumes \u001b[38;5;2m#docker,cleanup \u001b[39m \u001b[11;4H\u001b[7m\u001b[38;5;8m4\u001b[11;23H\u001b[38;5;3m7\u001b[11;26H8\u001b[11;33H\u001b[39mlog --graph --pretty=format:\"%Cred%h%Creset\u001b[11;77H-%C(yellow)%d%Creset\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 29 | [6.419751, "o", "\u001b[6;4H\u001b[38;5;8m10) \u001b[38;5;3m[2024-12-15 18:06:43] \u001b[39mecho comma\u001b[6;41Hd vault is awesome \u001b[6;61H \u001b[6;72H \u001b[7;4H\u001b[38;5;8m9\u001b[7;20H\u001b[38;5;3m8\u001b[7;23H6\u001b[7;26H2\u001b[7;29H\u001b[39mgit log --oneline --graph --all --dec\u001b[7;67Hrate\u001b[7;72H--color \u001b[7;83H \u001b[7;92H \u001b[8;4H\u001b[38;5;8m8\u001b[8;20H\u001b[38;5;3m7\u001b[8;22H07\u001b[8;25H35\u001b[8;29H\u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local\u001b[8;65HModels\u001b[8;72Hollama\u001b[8;79Hrun\u001b[8;83Hllama3.2\u001b[8;92H\u001b[38;5;2m#ai \u001b[9;4H\u001b[38;5;8m7\u001b[9;20H\u001b[38;5;3m6\u001b[9;22H24\u001b[9;25H04\u001b[9;29H\u001b[39mcargo install -\u001b[9;45Hpath . \u001b[38;5;2m#rust \u001b[39m \u001b[9;71H \u001b[9;82H \u001b[10;4H\u001b[38;5;8m6\u001b[10;25H\u001b[38;5;3m37\u001b[10;35H\u001b[39m-compose -f docker\u001b[10;54Hcompos\u001b[10;61H.prod.yml --env-file\u001b[10;82H.env.production\u001b[11;4H\u001b[7m\u001b[38;5;8m5\u001b[11;25H\u001b[38;5;3m26\u001b[11;29H\u001b[39mdocker system prune -af --volumes \u001b[38;5;2m#docker,cleanup \u001b[39m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 30 | [6.641539, "o", "\u001b[10;3H\u001b[7m\u001b[38;5;8m(6) \u001b[38;5;3m[2024-12-15 14:57:37] \u001b[39mdocker-compose -f docker-compose.prod.yml --env-file .env.production\u001b[11;3H\u001b[27m\u001b[38;5;8m(5) \u001b[38;5;3m[2024-12-15 14:57:26] \u001b[39mdocker system prune -af --volumes \u001b[38;5;2m#docker,cleanup \u001b[39m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 31 | [6.832905, "o", "\u001b[9;3H\u001b[7m\u001b[38;5;8m(7) \u001b[38;5;3m[2024-12-15 16:24:04] \u001b[39mcargo install --path . \u001b[38;5;2m#rust \u001b[39m \u001b[10;3H\u001b[27m\u001b[38;5;8m(6) \u001b[38;5;3m[2024-12-15 14:57:37] \u001b[39mdocker-compose -f docker-compose.prod.yml --env-file .env.production\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 32 | [7.01111, "o", "\u001b[8;3H\u001b[7m\u001b[38;5;8m(8) \u001b[38;5;3m[2024-12-15 17:07:35] \u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local Models ollama run llama3.2 \u001b[38;5;2m#ai \u001b[39m \u001b[9;3H\u001b[27m\u001b[38;5;8m(7) \u001b[38;5;3m[2024-12-15 16:24:04] \u001b[39mcargo install --path . \u001b[38;5;2m#rust \u001b[39m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 33 | [7.224031, "o", "\u001b[7;3H\u001b[7m\u001b[38;5;8m(9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit log --oneline --graph --all --decorate --color \u001b[8;3H\u001b[27m\u001b[38;5;8m(8) \u001b[38;5;3m[2024-12-15 17:07:35] \u001b[39mOLLAMA_MODELS=/Volumes/SSD/AI/Local Models ollama run llama3.2 \u001b[38;5;2m#ai \u001b[39m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 34 | [7.408061, "o", "\u001b[6;3H\u001b[7m\u001b[38;5;8m(10) \u001b[38;5;3m[2024-12-15 18:06:43] \u001b[39mecho command vault is awesome \u001b[7;3H\u001b[27m\u001b[38;5;8m(9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit log --oneline --graph --all --decorate --color \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 35 | [8.776738, "o", "\u001b[?25h\u001b[?1049l"] 36 | [8.779709, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] 37 | [8.779938, "o", "\u001b]2;ozan@Ozans-MacBook-Pro:~/Projects/command-vault\u0007\u001b]1;..command-vault\u0007"] 38 | [8.89536, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜ \u001b[36mcommand-vault\u001b[00m \u001b[01;34mgit:(\u001b[31mmain\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"] 39 | [8.895508, "o", "\u001b[?1h\u001b=\u001b[?2004h"] 40 | [12.246, "o", "\u001b[?2004l\r\r\n"] 41 | -------------------------------------------------------------------------------- /demo/ls-command.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozankasikci/command-vault/75d786a5792f994358edd7f50ca01b3c99f25160/demo/ls-command.gif -------------------------------------------------------------------------------- /demo/ls-command2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozankasikci/command-vault/75d786a5792f994358edd7f50ca01b3c99f25160/demo/ls-command2.gif -------------------------------------------------------------------------------- /demo/search-command.cast: -------------------------------------------------------------------------------- 1 | {"version": 2, "width": 98, "height": 17, "timestamp": 1734275349, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}} 2 | [0.91803, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r\u001b]2;ozan@Ozans-MacBook-Pro:~/Projects/command-vault\u0007\u001b]1;..command-vault\u0007"] 3 | [1.021698, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜ \u001b[36mcommand-vault\u001b[00m \u001b[01;34mgit:(\u001b[31mmain\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"] 4 | [1.021898, "o", "\u001b[?1h\u001b="] 5 | [1.022024, "o", "\u001b[?2004h"] 6 | [1.661387, "o", "c"] 7 | [1.739892, "o", "\bcv"] 8 | [1.964195, "o", " "] 9 | [2.115035, "o", "s"] 10 | [2.302631, "o", "e"] 11 | [2.431169, "o", "a"] 12 | [2.628189, "o", "r"] 13 | [2.869156, "o", "c"] 14 | [3.028174, "o", "h"] 15 | [3.24628, "o", " "] 16 | [3.604008, "o", "g"] 17 | [3.70318, "o", "i"] 18 | [4.112554, "o", "t"] 19 | [4.630715, "o", "\u001b[?1l\u001b>"] 20 | [4.630767, "o", "\u001b[?2004l\r\r\n"] 21 | [4.632095, "o", "\u001b]2;command-vault search git\u0007\u001b]1;cv\u0007"] 22 | [4.646451, "o", "\u001b[?1049h"] 23 | [4.646775, "o", "\u001b[?25l"] 24 | [4.647476, "o", "\u001b[2;2H\u001b[38;5;6m┌──────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[3;2H│Command Vault │\u001b[4;2H└──────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[5;2H\u001b[39m┌Commands──────────────────────────────────────────────────────────────────────────────────────┐\u001b[6;2H│\u001b[38;5;8m(9) \u001b[38;5;3m[2024-1"] 25 | [4.647563, "o", "2-15 18:06:32] \u001b[39mgit\u001b[6;33Hlog\u001b[6;37H--oneline\u001b[6;47H--graph\u001b[6;55H--all\u001b[6;61H--decorate\u001b[6;72H--color\u001b[6;97H│\u001b[7;2H│\u001b[38;5;8m(4) \u001b[38;5;3m[2024-12-15 14:57:08] \u001b[39mgit\u001b[7;33Hlog\u001b[7;37H--graph\u001b[7;45H--pretty=format:\"%Cred%h%Creset\u001b[7;77H-%C(yellow)%d%Creset│\u001b[8;2H│\u001b[38;5;8m(3) \u001b[38;5;3m[2024-12-15 14:56:03] \u001b[39mgit\u001b[8;33Hreset\u001b[8;39H--soft\u001b[8;46HHEAD~1\u001b[8;53H\u001b[38;5;2m#git \u001b[8;97H\u001b[39m│\u001b[9;2H│\u001b[38;5;8m(2) \u001b[38;5;3m[2024-12-15 13:59:34] \u001b[39mgit\u001b[9;33Hpush\u001b[9;38Horigin\u001b[9;45Hmain\u001b[9;50H\u001b[38;5;2m#git,deploy \u001b[9;97H\u001b[39m│\u001b[10;2H│\u001b[10;97H│\u001b[11;2H│\u001b[11;97H│\u001b[12;2H└──────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[14;2H\u001b[38;5;7m┌─────────────────────────────────────────"] 26 | [4.647646, "o", "─────────────────────────────────────────────────────┐\u001b[15;2H│Press \u001b[38;5;3m?\u001b[38;5;7m for help │\u001b[16;2H└──────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 27 | [5.317725, "o", "\u001b[6;3H\u001b[7m\u001b[38;5;8m(9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit log --oneline --graph --all --decorate --color \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 28 | [5.527536, "o", "\u001b[6;3H\u001b[38;5;8m(9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit log --oneline --graph --all --decorate --color \u001b[7;3H\u001b[7m\u001b[38;5;8m(4) \u001b[38;5;3m[2024-12-15 14:57:08] \u001b[39mgit log --graph --pretty=format:\"%Cred%h%Creset -%C(yellow)%d%Creset\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 29 | [5.732917, "o", "\u001b[7;3H\u001b[38;5;8m(4) \u001b[38;5;3m[2024-12-15 14:57:08] \u001b[39mgit log --graph --pretty=format:\"%Cred%h%Creset -%C(yellow)%d%Creset\u001b[8;3H\u001b[7m\u001b[38;5;8m(3) \u001b[38;5;3m[2024-12-15 14:56:03] \u001b[39mgit reset --soft HEAD~1 \u001b[38;5;2m#git \u001b[39m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 30 | [6.038479, "o", "\u001b[8;3H\u001b[38;5;8m(3) \u001b[38;5;3m[2024-12-15 14:56:03] \u001b[39mgit reset --soft HEAD~1 \u001b[38;5;2m#git \u001b[39m \u001b[9;3H\u001b[7m\u001b[38;5;8m(2) \u001b[38;5;3m[2024-12-15 13:59:34] \u001b[39mgit push origin main \u001b[38;5;2m#git,deploy \u001b[39m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 31 | [6.845727, "o", "\u001b[8;3H\u001b[7m\u001b[38;5;8m(3) \u001b[38;5;3m[2024-12-15 14:56:03] \u001b[39mgit reset --soft HEAD~1 \u001b[38;5;2m#git \u001b[39m \u001b[9;3H\u001b[27m\u001b[38;5;8m(2) \u001b[38;5;3m[2024-12-15 13:59:34] \u001b[39mgit push origin main \u001b[38;5;2m#git,deploy \u001b[39m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 32 | [7.23978, "o", "\u001b[7;3H\u001b[7m\u001b[38;5;8m(4) \u001b[38;5;3m[2024-12-15 14:57:08] \u001b[39mgit log --graph --pretty=format:\"%Cred%h%Creset -%C(yellow)%d%Creset\u001b[8;3H\u001b[27m\u001b[38;5;8m(3) \u001b[38;5;3m[2024-12-15 14:56:03] \u001b[39mgit reset --soft HEAD~1 \u001b[38;5;2m#git \u001b[39m \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 33 | [7.546489, "o", "\u001b[6;3H\u001b[7m\u001b[38;5;8m(9) \u001b[38;5;3m[2024-12-15 18:06:32] \u001b[39mgit log --oneline --graph --all --decorate --color \u001b[7;3H\u001b[27m\u001b[38;5;8m(4) \u001b[38;5;3m[2024-12-15 14:57:08] \u001b[39mgit log --graph --pretty=format:\"%Cred%h%Creset -%C(yellow)%d%Creset\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 34 | [8.021626, "o", "\u001b[?25h\u001b[?1049l"] 35 | [8.076258, "o", "\u001b[?1049h\u001b[?1h\u001b=\r"] 36 | [8.084464, "o", "* \u001b[33mcf64107\u001b[m\u001b[33m (\u001b[m\u001b[1;36mHEAD -> \u001b[m\u001b[1;32mmain\u001b[m\u001b[33m, \u001b[m\u001b[1;31morigin/main\u001b[m\u001b[33m)\u001b[m update ls commnand gif\u001b[m\r\n"] 37 | [8.084638, "o", "* \u001b[33mdfb06f9\u001b[m update add command demo\u001b[m\r\n"] 38 | [8.08473, "o", "* \u001b[33m5061205\u001b[m update add command demo\u001b[m\r\n"] 39 | [8.084844, "o", "* \u001b[33m58016fe\u001b[m update add command demo\u001b[m\r\n"] 40 | [8.084977, "o", "* \u001b[33meac2794\u001b[m\u001b[33m (\u001b[m\u001b[1;31mgit@github.com-ozankasikci/command-vault.git/main\u001b[m\u001b[33m)\u001b[m update add demo gif\u001b[m\r\n"] 41 | [8.08509, "o", "* \u001b[33m7ece2b8\u001b[m update add demo\u001b[m\r\n"] 42 | [8.085377, "o", "* \u001b[33m3dab8e5\u001b[m update add demo\u001b[m\r\n"] 43 | [8.085571, "o", "* \u001b[33m1631f02\u001b[m add ls and add gifs\u001b[m\r\n"] 44 | [8.085767, "o", "* \u001b[33m406368a\u001b[m add ls and add gifs\u001b[m\r\n"] 45 | [8.085843, "o", "* \u001b[33md622b3d\u001b[m add demo folder\u001b[m\r\n"] 46 | [8.08593, "o", "* \u001b[33m223d67d\u001b[m update readme\u001b[m\r\n"] 47 | [8.086017, "o", "* \u001b[33m6c32c01\u001b[m improve help\u001b[m\r\n"] 48 | [8.086086, "o", "* \u001b[33m5d435f3\u001b[m update: README.md\u001b[m\r\n"] 49 | [8.086169, "o", "* \u001b[33m815d352\u001b[m update: README.md\u001b[m\r\n"] 50 | [8.086642, "o", "* \u001b[33mb96a26f\u001b[m update readme\u001b[m\r\n"] 51 | [8.086739, "o", "* \u001b[33mf9675cc\u001b[m add demo.yml\u001b[m\r\n:\u001b[K"] 52 | [8.968314, "o", "\r\u001b[K\u001b[?1l\u001b>\u001b[?1049l"] 53 | [8.970896, "o", "\u001b[?25h\u001b[?1049l"] 54 | [8.971548, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] 55 | [8.971628, "o", "\u001b]2;ozan@Ozans-MacBook-Pro:~/Projects/command-vault\u0007\u001b]1;..command-vault\u0007"] 56 | [9.085612, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜ \u001b[36mcommand-vault\u001b[00m \u001b[01;34mgit:(\u001b[31mmain\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"] 57 | [9.085808, "o", "\u001b[?1h\u001b="] 58 | [9.085911, "o", "\u001b[?2004h"] 59 | [9.536984, "o", "c"] 60 | [9.706623, "o", "\bcv"] 61 | [9.812097, "o", " "] 62 | [9.98727, "o", "s"] 63 | [10.160854, "o", "e"] 64 | [10.234413, "o", "a"] 65 | [10.357068, "o", "r"] 66 | [10.672129, "o", "c"] 67 | [10.998831, "o", "h"] 68 | [11.126649, "o", " "] 69 | [11.236627, "o", "e"] 70 | [11.416991, "o", "c"] 71 | [11.532825, "o", "h"] 72 | [11.589086, "o", "o"] 73 | [12.140628, "o", "\u001b[?1l\u001b>"] 74 | [12.140777, "o", "\u001b[?2004l\r\r\n"] 75 | [12.141604, "o", "\u001b]2;command-vault search echo\u0007\u001b]1;cv\u0007"] 76 | [12.149498, "o", "\u001b[?1049h"] 77 | [12.149715, "o", "\u001b[?25l"] 78 | [12.150187, "o", "\u001b[2;2H\u001b[38;5;6m┌──────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[3;2H│Command Vault │\u001b[4;2H└──────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[5;2H\u001b[39m┌Commands──────────────────────────────────────────────────────────────────────────────────────┐\u001b[6;2H│\u001b[38;5;8m(10) \u001b[38;5;3m[2024-"] 79 | [12.150248, "o", "12-15 18:06:43] \u001b[39mecho\u001b[6;35Hcommand\u001b[6;43Hvault\u001b[6;49His\u001b[6;52Hawesome\u001b[6;97H│\u001b[7;2H│\u001b[7;97H│\u001b[8;2H│\u001b[8;97H│\u001b[9;2H│\u001b[9;97H│\u001b[10;2H│\u001b[10;97H│\u001b[11;2H│\u001b[11;97H│\u001b[12;2H└──────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[14;2H\u001b[38;5;7m┌──────────────────────────────────────────────────────────────────────────────────────────────┐\u001b[15;2H│Press \u001b[38;5;3m?\u001b[38;5;7m for help │\u001b[16;2H└───────────────────────────────────"] 80 | [12.152125, "o", "───────────────────────────────────────────────────────────┘\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 81 | [13.349462, "o", "\u001b[6;3H\u001b[7m\u001b[38;5;8m(10) \u001b[38;5;3m[2024-12-15 18:06:43] \u001b[39mecho command vault is awesome \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"] 82 | [14.264751, "o", "\u001b[?25h\u001b[?1049l"] 83 | [14.271342, "o", "command vault is awesome\r\n"] 84 | [14.271675, "o", "\u001b[?25h\u001b[?1049l"] 85 | [14.272657, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] 86 | [14.272763, "o", "\u001b]2;ozan@Ozans-MacBook-Pro:~/Projects/command-vault\u0007\u001b]1;..command-vault\u0007"] 87 | [14.385496, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜ \u001b[36mcommand-vault\u001b[00m \u001b[01;34mgit:(\u001b[31mmain\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"] 88 | [14.385658, "o", "\u001b[?1h\u001b="] 89 | [14.385723, "o", "\u001b[?2004h"] 90 | [15.744979, "o", "\u001b[?2004l\r\r\n"] 91 | -------------------------------------------------------------------------------- /demo/search-command.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozankasikci/command-vault/75d786a5792f994358edd7f50ca01b3c99f25160/demo/search-command.gif -------------------------------------------------------------------------------- /demo/tag-command.cast: -------------------------------------------------------------------------------- 1 | {"version": 2, "width": 98, "height": 17, "timestamp": 1734275753, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}} 2 | [0.914677, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r\u001b]2;ozan@Ozans-MacBook-Pro:~/Projects/command-vault\u0007\u001b]1;..command-vault\u0007"] 3 | [1.024376, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜ \u001b[36mcommand-vault\u001b[00m \u001b[01;34mgit:(\u001b[31mmain\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"] 4 | [1.024523, "o", "\u001b[?1h\u001b="] 5 | [1.024589, "o", "\u001b[?2004h"] 6 | [1.454686, "o", "c"] 7 | [1.533438, "o", "\bcv"] 8 | [1.650561, "o", " "] 9 | [1.908484, "o", "t"] 10 | [2.072619, "o", "a"] 11 | [2.277513, "o", "g"] 12 | [3.051516, "o", "\u001b[?1l\u001b>"] 13 | [3.051854, "o", "\u001b[?2004l\r\r\n"] 14 | [3.053563, "o", "\u001b]2;command-vault tag\u0007\u001b]1;cv\u0007"] 15 | [3.070542, "o", "Tag related operations\r\n\r\n\u001b[1m\u001b[4mUsage:\u001b[0m \u001b[1mcommand-vault tag\u001b[0m \r\n\r\n\u001b[1m\u001b[4mCommands:\u001b[0m\r\n \u001b[1madd\u001b[0m Add tags to a command\r\n \u001b[1mremove\u001b[0m Remove a tag from a command\r\n \u001b[1mlist\u001b[0m List all tags and their usage count\r\n \u001b[1msearch\u001b[0m Search commands by tag\r\n \u001b[1mhelp\u001b[0m Print this message or the help of the given subcommand(s)\r\n\r\n\u001b[1m\u001b[4mOptions:\u001b[0m\r\n \u001b[1m-h\u001b[0m, \u001b[1m--help\u001b[0m Print help\r\n"] 16 | [3.071075, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] 17 | [3.071191, "o", "\u001b]2;ozan@Ozans-MacBook-Pro:~/Projects/command-vault\u0007\u001b]1;..command-vault\u0007"] 18 | [3.179398, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;31m➜ \u001b[36mcommand-vault\u001b[00m \u001b[01;34mgit:(\u001b[31mmain\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"] 19 | [3.179533, "o", "\u001b[?1h\u001b="] 20 | [3.179552, "o", "\u001b[?2004h"] 21 | [3.826645, "o", "c"] 22 | [3.927952, "o", "\bcv"] 23 | [4.048875, "o", " "] 24 | [4.22507, "o", "t"] 25 | [4.355833, "o", "a"] 26 | [4.571449, "o", "g"] 27 | [4.760003, "o", " "] 28 | [5.659434, "o", "l"] 29 | [5.820422, "o", "i"] 30 | [5.887702, "o", "s"] 31 | [6.069306, "o", "t"] 32 | [6.945045, "o", "\u001b[?1l\u001b>"] 33 | [6.945257, "o", "\u001b[?2004l\r\r\n"] 34 | [6.946847, "o", "\u001b]2;command-vault tag list\u0007\u001b]1;cv\u0007"] 35 | [6.95821, "o", "\r\nTags and their usage:\r\n─────────────────────────────────────────────\r\nai: 1 command\r\ndocker,cleanup: 1 command\r\ndocker,compose: 1 command\r\ngit: 1 command\r\ngit,deploy: 1 command\r\ngit,log: 1 command\r\nrust: 1 command\r\nimportant: 0 commands\r\ntest: 0 commands\r\n"] 36 | [6.958822, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"] 37 | [6.958926, "o", "\u001b]2;ozan@Ozans-MacBook-Pro:~/Projects/command-vault\u0007\u001b]1;..command-vault\u0007"] 38 | [7.054331, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b[01;32m➜ \u001b[36mcommand-vault\u001b[00m \u001b[01;34mgit:(\u001b[31mmain\u001b[34m) \u001b[33m✗\u001b[00m \u001b[K"] 39 | [7.054511, "o", "\u001b[?1h\u001b=\u001b[?2004h"] 40 | [9.536971, "o", "\u001b[?2004l\r\r\n"] 41 | -------------------------------------------------------------------------------- /demo/tag-command.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozankasikci/command-vault/75d786a5792f994358edd7f50ca01b3c99f25160/demo/tag-command.gif -------------------------------------------------------------------------------- /scripts/bump_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to display usage 4 | usage() { 5 | echo "Usage: $0 [major|minor|patch]" 6 | echo "Bump the version number in Cargo.toml" 7 | echo "" 8 | echo "Arguments:" 9 | echo " major Bump major version (X.y.z -> X+1.0.0)" 10 | echo " minor Bump minor version (x.Y.z -> x.Y+1.0)" 11 | echo " patch Bump patch version (x.y.Z -> x.y.Z+1)" 12 | exit 1 13 | } 14 | 15 | # Check if cargo-edit is installed 16 | if ! command -v cargo-set-version &> /dev/null; then 17 | echo "cargo-edit is not installed. Installing..." 18 | cargo install cargo-edit 19 | fi 20 | 21 | # Validate arguments 22 | if [ "$#" -ne 1 ]; then 23 | usage 24 | fi 25 | 26 | VERSION_TYPE=$1 27 | 28 | case $VERSION_TYPE in 29 | major|minor|patch) 30 | # Get current version 31 | CURRENT_VERSION=$(grep "^version" Cargo.toml | sed 's/version = "\(.*\)"/\1/') 32 | echo "Current version: $CURRENT_VERSION" 33 | 34 | # Bump version using cargo-set-version 35 | cargo set-version --bump $VERSION_TYPE 36 | 37 | # Update README.md with new version 38 | ./scripts/update_version.sh 39 | 40 | # Get new version 41 | NEW_VERSION=$(grep "^version" Cargo.toml | sed 's/version = "\(.*\)"/\1/') 42 | echo "New version: $NEW_VERSION" 43 | 44 | # Stage changes 45 | git add Cargo.toml Cargo.lock 46 | git commit -m "chore: bump version to $NEW_VERSION" 47 | 48 | # Create tag 49 | git tag -a "v$NEW_VERSION" -m "Version $NEW_VERSION" 50 | 51 | # Push changes and tags 52 | echo "Pushing changes and tags..." 53 | git push && git push --tags 54 | 55 | echo "Version bump complete!" 56 | ;; 57 | *) 58 | usage 59 | ;; 60 | esac 61 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Install cargo-tarpaulin if not installed 4 | if ! command -v cargo-tarpaulin &> /dev/null; then 5 | echo "Installing cargo-tarpaulin..." 6 | cargo install cargo-tarpaulin 7 | fi 8 | 9 | # Run tests with coverage 10 | cargo tarpaulin --out Html --output-dir coverage 11 | 12 | echo "Coverage report generated in coverage/tarpaulin-report.html" 13 | -------------------------------------------------------------------------------- /scripts/update_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the new version from Cargo.toml 4 | NEW_VERSION=$(grep '^version = ' Cargo.toml | sed -E 's/version = "(.*)"/\1/') 5 | 6 | # Update the version in README.md install instructions 7 | # First try to replace X.Y.Z pattern 8 | sed -i '' "s/vX\.Y\.Z/v$NEW_VERSION/g" README.md 9 | # Then try to replace any existing version numbers 10 | sed -i '' -E "s/v[0-9]+\.[0-9]+\.[0-9]+/v$NEW_VERSION/g" README.md 11 | 12 | echo "Updated version to $NEW_VERSION in:" 13 | echo "- Cargo.toml" 14 | echo "- README.md" 15 | -------------------------------------------------------------------------------- /shell/bash-integration.sh: -------------------------------------------------------------------------------- 1 | # Command Vault Bash Integration 2 | 3 | # Function to log commands to command-vault 4 | _command_vault_log_command() { 5 | local exit_code=$? 6 | local cmd=$(HISTTIMEFORMAT= history 1) 7 | 8 | # Extract just the command part 9 | cmd=$(echo "$cmd" | sed -e 's/^[[:space:]]*[0-9]*[[:space:]]*//') 10 | 11 | # Trim whitespace 12 | cmd=$(echo "$cmd" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') 13 | 14 | # Skip empty commands 15 | if [ -z "$cmd" ]; then 16 | return 17 | fi 18 | 19 | # Skip commands that start with space (if configured to ignore those) 20 | if [[ "$cmd" =~ ^[[:space:]] ]]; then 21 | return 22 | fi 23 | 24 | # Log the command using command-vault 25 | command-vault add --exit-code $exit_code "$cmd" &>/dev/null 26 | } 27 | 28 | # Add the function to the PROMPT_COMMAND 29 | PROMPT_COMMAND="_command_vault_log_command${PROMPT_COMMAND:+;$PROMPT_COMMAND}" 30 | -------------------------------------------------------------------------------- /shell/fish-integration.fish: -------------------------------------------------------------------------------- 1 | # Command Vault Fish Integration 2 | 3 | # Function to log commands to command-vault 4 | function _command_vault_log_command --on-event fish_postexec 5 | set -l exit_code $status 6 | set -l cmd $argv[1] 7 | 8 | # Skip empty commands 9 | if test -z "$cmd" 10 | return 11 | end 12 | 13 | # Skip commands that start with space (if configured to ignore those) 14 | if string match -q " *" -- "$cmd" 15 | return 16 | end 17 | 18 | # Skip command-vault commands to prevent recursion 19 | if string match -q "command-vault *" -- "$cmd" 20 | return 21 | end 22 | 23 | # Log the command using command-vault 24 | command command-vault add --exit-code $exit_code "$cmd" &>/dev/null 25 | end 26 | 27 | # Initialize command-vault integration 28 | if status is-interactive 29 | # Register the event handler 30 | functions -q _command_vault_log_command 31 | or source (status filename) 32 | end -------------------------------------------------------------------------------- /shell/zsh-integration.zsh: -------------------------------------------------------------------------------- 1 | # Command Vault ZSH Integration 2 | 3 | # Function to log commands to command-vault 4 | _command_vault_log_command() { 5 | local exit_code=$? 6 | local cmd=$(fc -ln -1) 7 | 8 | # Trim whitespace 9 | cmd=$(echo "$cmd" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') 10 | 11 | # Skip empty commands 12 | if [ -z "$cmd" ]; then 13 | return 14 | fi 15 | 16 | # Skip commands that start with space (if configured to ignore those) 17 | if [[ "$cmd" =~ ^[[:space:]] ]]; then 18 | return 19 | fi 20 | 21 | # Log the command using command-vault 22 | command-vault add --exit-code $exit_code "$cmd" &>/dev/null 23 | } 24 | 25 | # Add the function to the precmd hook 26 | autoload -Uz add-zsh-hook 27 | add-zsh-hook precmd _command_vault_log_command 28 | -------------------------------------------------------------------------------- /src/cli/args.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | #[derive(Parser, Debug)] 4 | #[command(author, version, about, long_about = None)] 5 | pub struct Cli { 6 | #[command(subcommand)] 7 | pub command: Commands, 8 | 9 | /// Enable debug mode to see detailed command execution information 10 | #[arg(short, long)] 11 | pub debug: bool, 12 | } 13 | 14 | #[derive(Subcommand, Debug)] 15 | pub enum Commands { 16 | /// Add a command to history 17 | /// 18 | /// Parameters can be specified using @name:description=default syntax 19 | /// Examples: 20 | /// - Basic parameter: @filename 21 | /// - With description: @filename:Name of file to create 22 | /// - With default: @filename:Name of file to create=test.txt 23 | Add { 24 | /// Tags to add to the command 25 | #[arg(short, long)] 26 | tags: Vec, 27 | 28 | /// Command to add 29 | #[arg(trailing_var_arg = true, required = true)] 30 | command: Vec, 31 | }, 32 | 33 | /// Execute a command by id (in the current shell) 34 | Exec { 35 | /// Command ID to execute 36 | command_id: i64, 37 | 38 | /// Enable debug mode 39 | #[arg(long)] 40 | debug: bool, 41 | }, 42 | /// Search through command history 43 | Search { 44 | /// Search query 45 | #[arg(required = true)] 46 | query: String, 47 | 48 | /// Maximum number of results to show 49 | #[arg(short, long, default_value = "10")] 50 | limit: usize, 51 | }, 52 | /// List all commands in chronological order 53 | Ls { 54 | /// Maximum number of results to show. Use 0 to show all commands. 55 | #[arg(short, long, default_value = "50")] 56 | limit: usize, 57 | 58 | /// Sort in ascending order (oldest first) 59 | #[arg(short = 'a', long)] 60 | asc: bool, 61 | }, 62 | /// Tag related operations 63 | Tag { 64 | #[command(subcommand)] 65 | action: TagCommands, 66 | }, 67 | /// Initialize shell integration 68 | ShellInit { 69 | /// Shell to initialize (defaults to current shell) 70 | #[arg(short, long)] 71 | shell: Option, 72 | }, 73 | /// Delete a command from history 74 | Delete { 75 | /// Command ID to delete 76 | #[arg(required = true)] 77 | command_id: i64, 78 | }, 79 | } 80 | 81 | #[derive(Subcommand, Debug)] 82 | pub enum TagCommands { 83 | /// Add tags to a command 84 | Add { 85 | /// Command ID to tag 86 | #[arg(required = true)] 87 | command_id: i64, 88 | 89 | /// Tags to add 90 | #[arg(required = true)] 91 | tags: Vec, 92 | }, 93 | /// Remove a tag from a command 94 | Remove { 95 | /// Command ID to remove tag from 96 | #[arg(required = true)] 97 | command_id: i64, 98 | 99 | /// Tag to remove 100 | #[arg(required = true)] 101 | tag: String, 102 | }, 103 | /// List all tags and their usage count 104 | List, 105 | /// Search commands by tag 106 | Search { 107 | /// Tag to search for 108 | #[arg(required = true)] 109 | tag: String, 110 | 111 | /// Maximum number of results to show 112 | #[arg(short, long, default_value = "10")] 113 | limit: usize, 114 | }, 115 | } 116 | -------------------------------------------------------------------------------- /src/cli/commands.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Result, anyhow}; 2 | use chrono::{Local, Utc}; 3 | use std::io::{self, Stdout}; 4 | use crossterm::{ 5 | execute, 6 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 7 | }; 8 | use ratatui::{ 9 | backend::CrosstermBackend, 10 | layout::{Constraint, Direction, Layout}, 11 | style::{Color, Style}, 12 | text::{Line, Span}, 13 | widgets::{Block, Borders, Paragraph}, 14 | Terminal, 15 | }; 16 | use colored::*; 17 | 18 | use crate::db::{Command, Database}; 19 | use crate::ui::App; 20 | use crate::utils::params::parse_parameters; 21 | use crate::utils::params::substitute_parameters; 22 | use crate::exec::{ExecutionContext, execute_shell_command}; 23 | 24 | use super::args::{Commands, TagCommands}; 25 | 26 | fn print_commands(commands: &[Command]) -> Result<()> { 27 | let terminal_result = setup_terminal(); 28 | 29 | match terminal_result { 30 | Ok(mut terminal) => { 31 | let res = print_commands_ui(&mut terminal, commands); 32 | restore_terminal(&mut terminal)?; 33 | res 34 | } 35 | Err(_) => { 36 | // Fallback to simple text output 37 | println!("Command History:"); 38 | println!("─────────────────────────────────────────────"); 39 | for cmd in commands { 40 | let local_time = cmd.timestamp.with_timezone(&Local); 41 | println!("{} │ {}", local_time.format("%Y-%m-%d %H:%M:%S"), cmd.command); 42 | if !cmd.tags.is_empty() { 43 | println!(" Tags: {}", cmd.tags.join(", ")); 44 | } 45 | if !cmd.parameters.is_empty() { 46 | println!(" Parameters:"); 47 | for param in &cmd.parameters { 48 | let desc = param.description.as_deref().unwrap_or("None"); 49 | println!(" - {}: {} (default: {})", param.name, desc, "None"); 50 | } 51 | } 52 | println!(" Directory: {}", cmd.directory); 53 | println!(); 54 | } 55 | Ok(()) 56 | } 57 | } 58 | } 59 | 60 | fn print_commands_ui(terminal: &mut Terminal>, commands: &[Command]) -> Result<()> { 61 | terminal.draw(|f| { 62 | let chunks = Layout::default() 63 | .direction(Direction::Vertical) 64 | .margin(1) 65 | .constraints([Constraint::Min(0)]) 66 | .split(f.size()); 67 | 68 | let mut lines = vec![]; 69 | lines.push(Line::from(Span::styled( 70 | "Command History:", 71 | Style::default().fg(Color::Cyan), 72 | ))); 73 | lines.push(Line::from(Span::raw("─────────────────────────────────────────────"))); 74 | 75 | for cmd in commands { 76 | let local_time = cmd.timestamp.with_timezone(&Local); 77 | lines.push(Line::from(vec![ 78 | Span::styled(local_time.format("%Y-%m-%d %H:%M:%S").to_string(), Style::default().fg(Color::Yellow)), 79 | Span::raw(" │ "), 80 | Span::raw(&cmd.command), 81 | ])); 82 | lines.push(Line::from(vec![ 83 | Span::raw(" Directory: "), 84 | Span::raw(&cmd.directory), 85 | ])); 86 | if !cmd.tags.is_empty() { 87 | lines.push(Line::from(vec![ 88 | Span::raw(" Tags: "), 89 | Span::raw(cmd.tags.join(", ")), 90 | ])); 91 | } 92 | lines.push(Line::from(Span::raw("─────────────────────────────────────────────"))); 93 | } 94 | 95 | let paragraph = Paragraph::new(lines).block(Block::default().borders(Borders::ALL)); 96 | f.render_widget(paragraph, chunks[0]); 97 | })?; 98 | Ok(()) 99 | } 100 | 101 | fn setup_terminal() -> Result>> { 102 | enable_raw_mode()?; 103 | let mut stdout = io::stdout(); 104 | execute!(stdout, EnterAlternateScreen)?; 105 | let backend = CrosstermBackend::new(stdout); 106 | Terminal::new(backend).map_err(|e| e.into()) 107 | } 108 | 109 | fn restore_terminal(terminal: &mut Terminal>) -> Result<()> { 110 | disable_raw_mode()?; 111 | execute!(terminal.backend_mut(), LeaveAlternateScreen)?; 112 | terminal.show_cursor()?; 113 | Ok(()) 114 | } 115 | 116 | pub fn handle_command(command: Commands, db: &mut Database, debug: bool) -> Result<()> { 117 | match command { 118 | Commands::Add { command, tags } => { 119 | // Process command parts with special handling for git format strings 120 | let command_str = command.iter().enumerate().fold(String::new(), |mut acc, (i, arg)| { 121 | if i > 0 { 122 | acc.push(' '); 123 | } 124 | // Special case for git format strings 125 | if arg.starts_with("--pretty=format:") { 126 | acc.push_str(&format!("\"{}\"", arg)); 127 | } else { 128 | acc.push_str(arg); 129 | } 130 | acc 131 | }); 132 | 133 | // Don't allow empty commands 134 | if command_str.trim().is_empty() { 135 | return Err(anyhow!("Cannot add empty command")); 136 | } 137 | 138 | // Get the current directory 139 | let directory = std::env::current_dir()? 140 | .to_string_lossy() 141 | .to_string(); 142 | 143 | let timestamp = Local::now().with_timezone(&Utc); 144 | 145 | // Parse parameters from command string 146 | let parameters = parse_parameters(&command_str); 147 | 148 | let cmd = Command { 149 | id: None, 150 | command: command_str.clone(), 151 | timestamp, 152 | directory, 153 | tags, 154 | parameters, 155 | }; 156 | let id = db.add_command(&cmd)?; 157 | println!("Command added to history with ID: {}", id); 158 | 159 | // If command has parameters, show them 160 | if !cmd.parameters.is_empty() { 161 | println!("\nDetected parameters:"); 162 | for param in &cmd.parameters { 163 | let desc = param.description.as_deref().unwrap_or("None"); 164 | println!(" {} - Description: {}", param.name.yellow(), desc); 165 | } 166 | } 167 | } 168 | Commands::Search { query, limit } => { 169 | let commands = db.search_commands(&query, limit)?; 170 | let mut app = App::new(commands.clone(), db, debug); 171 | match app.run() { 172 | Ok(_) => (), 173 | Err(e) => { 174 | if e.to_string() == "Operation cancelled by user" { 175 | print!("\n{}", "Operation cancelled.".yellow()); 176 | return Ok(()); 177 | } 178 | eprintln!("Failed to start TUI mode: {}", e); 179 | print_commands(&commands)?; 180 | } 181 | } 182 | } 183 | Commands::Ls { limit, asc } => { 184 | let commands = db.list_commands(limit, asc)?; 185 | if commands.is_empty() { 186 | print!("No commands found."); 187 | return Ok(()); 188 | } 189 | 190 | // Check if TUI should be disabled (useful for testing or non-interactive environments) 191 | if std::env::var("COMMAND_VAULT_NO_TUI").is_ok() { 192 | for cmd in commands { 193 | print!("{}: {} ({})", cmd.id.unwrap_or(0), cmd.command, cmd.directory); 194 | } 195 | return Ok(()); 196 | } 197 | 198 | let mut app = App::new(commands.clone(), db, debug); 199 | match app.run() { 200 | Ok(_) => (), 201 | Err(e) => { 202 | if e.to_string() == "Operation cancelled by user" { 203 | print!("\n{}", "Operation cancelled.".yellow()); 204 | return Ok(()); 205 | } 206 | eprintln!("Failed to start TUI mode: {}", e); 207 | print_commands(&commands)?; 208 | } 209 | } 210 | } 211 | Commands::Tag { action } => match action { 212 | TagCommands::Add { command_id, tags } => { 213 | match db.add_tags_to_command(command_id, &tags) { 214 | Ok(_) => print!("Tags added successfully"), 215 | Err(e) => eprintln!("Failed to add tags: {}", e), 216 | } 217 | } 218 | TagCommands::Remove { command_id, tag } => { 219 | match db.remove_tag_from_command(command_id, &tag) { 220 | Ok(_) => print!("Tag removed successfully"), 221 | Err(e) => eprintln!("Failed to remove tag: {}", e), 222 | } 223 | } 224 | TagCommands::List => { 225 | match db.list_tags() { 226 | Ok(tags) => { 227 | if tags.is_empty() { 228 | print!("No tags found"); 229 | return Ok(()); 230 | } 231 | 232 | print!("\nTags and their usage:"); 233 | print!("─────────────────────────────────────────────"); 234 | for (tag, count) in tags { 235 | print!("{}: {} command{}", tag, count, if count == 1 { "" } else { "s" }); 236 | } 237 | } 238 | Err(e) => eprintln!("Failed to list tags: {}", e), 239 | } 240 | } 241 | TagCommands::Search { tag, limit } => { 242 | match db.search_by_tag(&tag, limit) { 243 | Ok(commands) => print_commands(&commands)?, 244 | Err(e) => eprintln!("Failed to search by tag: {}", e), 245 | } 246 | } 247 | }, 248 | Commands::Exec { command_id, debug } => { 249 | let command = db.get_command(command_id)? 250 | .ok_or_else(|| anyhow!("Command not found with ID: {}", command_id))?; 251 | 252 | // Create the directory if it doesn't exist 253 | if !std::path::Path::new(&command.directory).exists() { 254 | std::fs::create_dir_all(&command.directory)?; 255 | } 256 | 257 | let current_params = parse_parameters(&command.command); 258 | let final_command = substitute_parameters(&command.command, ¤t_params, None)?; 259 | 260 | let ctx = ExecutionContext { 261 | command: final_command.clone(), 262 | directory: command.directory.clone(), 263 | test_mode: std::env::var("COMMAND_VAULT_TEST").is_ok(), 264 | debug_mode: debug, 265 | }; 266 | 267 | println!("\n─────────────────────────────────────────────"); 268 | println!("Command to execute: {}", final_command); 269 | println!("Working directory: {}", command.directory); 270 | println!(); // Add extra newline before command output 271 | 272 | execute_shell_command(&ctx)?; 273 | } 274 | Commands::ShellInit { shell } => { 275 | let script_path = crate::shell::hooks::init_shell(shell)?; 276 | if !script_path.exists() { 277 | return Err(anyhow!("Shell integration script not found at: {}", script_path.display())); 278 | } 279 | print!("{}", script_path.display()); 280 | return Ok(()); 281 | }, 282 | Commands::Delete { command_id } => { 283 | // First check if the command exists 284 | if let Some(command) = db.get_command(command_id)? { 285 | // Show the command that will be deleted 286 | println!("Deleting command:"); 287 | print_commands(&[command])?; 288 | 289 | // Delete the command 290 | db.delete_command(command_id)?; 291 | println!("Command deleted successfully"); 292 | } else { 293 | return Err(anyhow!("Command with ID {} not found", command_id)); 294 | } 295 | } 296 | } 297 | Ok(()) 298 | } 299 | -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod args; 2 | pub mod commands; 3 | -------------------------------------------------------------------------------- /src/db/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod models; 2 | pub mod store; 3 | 4 | pub use models::Command; 5 | pub use store::Database; 6 | -------------------------------------------------------------------------------- /src/db/models.rs: -------------------------------------------------------------------------------- 1 | //! Database models for command-vault 2 | //! 3 | //! This module defines the core data structures used throughout the application. 4 | 5 | use chrono::{DateTime, Utc}; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | /// Represents a command with its metadata. 9 | /// 10 | /// A command includes the actual command string, execution directory, 11 | /// timestamp, tags, and parameters. 12 | /// 13 | /// # Example 14 | /// ```rust 15 | /// use command_vault::db::models::Command; 16 | /// use chrono::Utc; 17 | /// 18 | /// let cmd = Command { 19 | /// id: None, 20 | /// command: "git push origin main".to_string(), 21 | /// timestamp: Utc::now(), 22 | /// directory: "/project".to_string(), 23 | /// tags: vec!["git".to_string()], 24 | /// parameters: vec![], 25 | /// }; 26 | /// ``` 27 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 28 | pub struct Command { 29 | /// Unique identifier for the command 30 | pub id: Option, 31 | 32 | /// The actual command string 33 | pub command: String, 34 | 35 | /// When the command was created or last modified 36 | pub timestamp: DateTime, 37 | 38 | /// Directory where the command should be executed 39 | pub directory: String, 40 | 41 | /// Tags associated with the command 42 | #[serde(skip_serializing_if = "Vec::is_empty", default)] 43 | pub tags: Vec, 44 | 45 | /// Parameters that can be substituted in the command 46 | #[serde(skip_serializing_if = "Vec::is_empty", default)] 47 | pub parameters: Vec, 48 | } 49 | 50 | /// Represents a parameter that can be substituted in a command. 51 | /// 52 | /// Parameters allow commands to be more flexible by providing 53 | /// placeholders that can be filled in at runtime. 54 | /// 55 | /// # Example 56 | /// ```rust 57 | /// use command_vault::db::models::Parameter; 58 | /// 59 | /// let param = Parameter { 60 | /// name: "branch".to_string(), 61 | /// description: Some("Git branch name".to_string()), 62 | /// }; 63 | /// ``` 64 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 65 | pub struct Parameter { 66 | /// Name of the parameter (used in substitution) 67 | pub name: String, 68 | 69 | /// Optional description of what the parameter does 70 | pub description: Option, 71 | } 72 | 73 | impl Parameter { 74 | pub fn new(name: String) -> Self { 75 | Self { 76 | name, 77 | description: None, 78 | } 79 | } 80 | 81 | pub fn with_description(name: String, description: Option) -> Self { 82 | Self { 83 | name, 84 | description, 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/exec/mod.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | use std::process::Command as ProcessCommand; 3 | use std::env; 4 | use std::path::{Path, PathBuf}; 5 | use anyhow::Result; 6 | use crossterm::terminal; 7 | use dialoguer::{theme::ColorfulTheme, Input}; 8 | use crate::db::models::Command; 9 | use crate::shell::hooks::detect_current_shell; 10 | 11 | pub struct ExecutionContext { 12 | pub command: String, 13 | pub directory: String, 14 | pub test_mode: bool, 15 | pub debug_mode: bool, 16 | } 17 | 18 | pub fn wrap_command(command: &str, test_mode: bool) -> String { 19 | if test_mode { 20 | command.to_string() 21 | } else { 22 | // For interactive mode, handle shell initialization 23 | let shell_type = detect_current_shell(); 24 | let clean_command = command.trim_matches('"').to_string(); 25 | 26 | match shell_type.as_str() { 27 | "zsh" => format!( 28 | r#"setopt no_global_rcs; if [ -f ~/.zshrc ]; then ZDOTDIR=~ source ~/.zshrc; fi; {}"#, 29 | clean_command 30 | ), 31 | "fish" => format!( 32 | r#"if test -f ~/.config/fish/config.fish; source ~/.config/fish/config.fish 2>/dev/null; end; {}"#, 33 | clean_command 34 | ), 35 | "bash" | _ => format!( 36 | r#"if [ -f ~/.bashrc ]; then . ~/.bashrc >/dev/null 2>&1; fi; if [ -f ~/.bash_profile ]; then . ~/.bash_profile >/dev/null 2>&1; fi; {}"#, 37 | clean_command 38 | ), 39 | } 40 | } 41 | } 42 | 43 | fn is_path_traversal_attempt(command: &str, working_dir: &Path) -> bool { 44 | // Check if the command contains path traversal attempts 45 | if command.contains("..") { 46 | // Get the absolute path of the working directory 47 | if let Ok(working_dir) = working_dir.canonicalize() { 48 | // Try to resolve any path in the command relative to working_dir 49 | let potential_path = working_dir.join(command); 50 | if let Ok(resolved_path) = potential_path.canonicalize() { 51 | // Check if the resolved path is outside the working directory 52 | return !resolved_path.starts_with(working_dir); 53 | } 54 | } 55 | // If we can't resolve the paths, assume it's a traversal attempt 56 | return true; 57 | } 58 | false 59 | } 60 | 61 | pub fn execute_shell_command(ctx: &ExecutionContext) -> Result<()> { 62 | // Get the current shell 63 | let shell = if cfg!(windows) { 64 | String::from("cmd.exe") 65 | } else { 66 | env::var("SHELL").unwrap_or_else(|_| String::from("/bin/sh")) 67 | }; 68 | 69 | // Wrap the command for shell execution 70 | let wrapped_command = wrap_command(&ctx.command, ctx.test_mode); 71 | 72 | // Check for directory traversal attempts 73 | if is_path_traversal_attempt(&wrapped_command, Path::new(&ctx.directory)) { 74 | return Err(anyhow::anyhow!("Directory traversal attempt detected")); 75 | } 76 | 77 | // Create command with the appropriate shell 78 | let mut command = ProcessCommand::new(&shell); 79 | 80 | // In test mode, use simple shell execution 81 | if ctx.test_mode { 82 | command.args(&["-c", &wrapped_command]); 83 | } else { 84 | // Use -i for all shells in interactive mode to ensure proper initialization 85 | command.args(&["-i", "-c", &wrapped_command]); 86 | } 87 | 88 | // Set working directory 89 | command.current_dir(&ctx.directory); 90 | 91 | if ctx.debug_mode { 92 | println!("Full command: {:?}", command); 93 | } 94 | 95 | // Disable raw mode only in interactive mode 96 | if !ctx.test_mode { 97 | let _ = terminal::disable_raw_mode(); 98 | // Reset cursor position 99 | let mut stdout = io::stdout(); 100 | let _ = crossterm::execute!( 101 | stdout, 102 | crossterm::cursor::MoveTo(0, crossterm::cursor::position()?.1) 103 | ); 104 | println!(); // Add a newline before command output 105 | } 106 | 107 | // Execute the command and capture output 108 | let output = command.output()?; 109 | 110 | // Handle command output 111 | if !output.status.success() { 112 | let stderr = String::from_utf8_lossy(&output.stderr); 113 | return Err(anyhow::anyhow!( 114 | "Command failed with status: {}. stderr: {}", 115 | output.status, 116 | stderr 117 | )); 118 | } 119 | 120 | // Print stdout 121 | if !output.stdout.is_empty() { 122 | let stdout_str = String::from_utf8_lossy(&output.stdout); 123 | print!("{}", stdout_str); 124 | } 125 | 126 | // Print stderr 127 | if !output.stderr.is_empty() { 128 | let stderr_str = String::from_utf8_lossy(&output.stderr); 129 | eprint!("{}", stderr_str); 130 | } 131 | 132 | Ok(()) 133 | } 134 | 135 | pub fn execute_command(command: &Command) -> Result<()> { 136 | let test_mode = std::env::var("COMMAND_VAULT_TEST").is_ok(); 137 | let debug_mode = std::env::var("COMMAND_VAULT_DEBUG").is_ok(); 138 | let mut final_command = command.command.clone(); 139 | 140 | // If command has parameters, prompt for values first 141 | if !command.parameters.is_empty() { 142 | for param in &command.parameters { 143 | println!("Parameter: {}", param.name); 144 | println!(); 145 | 146 | let value = if test_mode { 147 | let value = std::env::var("COMMAND_VAULT_TEST_INPUT") 148 | .unwrap_or_else(|_| "test_value".to_string()); 149 | println!("Enter value: {}", value); 150 | println!(); 151 | value 152 | } else { 153 | let input: String = Input::with_theme(&ColorfulTheme::default()) 154 | .with_prompt("Enter value") 155 | .allow_empty(true) 156 | .interact_text()?; 157 | println!(); 158 | 159 | if input.contains(' ') { 160 | format!("'{}'", input.replace("'", "'\\''")) 161 | } else { 162 | input 163 | } 164 | }; 165 | 166 | final_command = final_command.replace(&format!("@{}", param.name), &value); 167 | } 168 | } 169 | 170 | let ctx = ExecutionContext { 171 | command: final_command, 172 | directory: command.directory.clone(), 173 | test_mode, 174 | debug_mode, 175 | }; 176 | 177 | // Print command details only once 178 | println!("─────────────────────────────────────────────"); 179 | println!(); 180 | println!("Command to execute: {}", ctx.command); 181 | println!("Working directory: {}", ctx.directory); 182 | println!(); 183 | 184 | execute_shell_command(&ctx) 185 | } 186 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cli; 2 | pub mod db; 3 | pub mod shell; 4 | pub mod ui; 5 | pub mod utils; 6 | pub mod exec; 7 | pub mod version; 8 | 9 | pub use db::Database; 10 | pub use version::VERSION; 11 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | use command_vault::{ 4 | cli::{args::Cli, commands::handle_command}, 5 | db::store::Database, 6 | }; 7 | use std::path::PathBuf; 8 | 9 | mod cli; 10 | mod db; 11 | mod shell; 12 | mod ui; 13 | mod utils; 14 | mod exec; 15 | 16 | fn main() -> Result<()> { 17 | // Enable colors globally 18 | colored::control::set_override(true); 19 | 20 | let args = Cli::parse(); 21 | 22 | let data_dir = dirs::data_dir() 23 | .ok_or_else(|| anyhow::anyhow!("Could not find data directory"))? 24 | .join("command-vault"); 25 | std::fs::create_dir_all(&data_dir)?; 26 | 27 | let db_path = data_dir.join("commands.db"); 28 | let mut db = Database::new(db_path.to_str().unwrap())?; 29 | 30 | let result = handle_command(args.command, &mut db, args.debug); 31 | 32 | // Re-enable colors before exiting 33 | colored::control::set_override(true); 34 | 35 | result 36 | } 37 | -------------------------------------------------------------------------------- /src/shell/hooks.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use std::env; 3 | use std::path::PathBuf; 4 | 5 | /// Get the directory containing shell integration scripts 6 | pub fn get_shell_integration_dir() -> PathBuf { 7 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 8 | path.push("shell"); 9 | path 10 | } 11 | 12 | /// Get the path to the ZSH integration script 13 | pub fn get_zsh_integration_path() -> PathBuf { 14 | let mut path = get_shell_integration_dir(); 15 | path.push("zsh-integration.zsh"); 16 | path 17 | } 18 | 19 | /// Get the path to the Bash integration script 20 | pub fn get_bash_integration_path() -> PathBuf { 21 | let mut path = get_shell_integration_dir(); 22 | path.push("bash-integration.sh"); 23 | path 24 | } 25 | 26 | /// Get the path to the Fish integration script 27 | pub fn get_fish_integration_path() -> PathBuf { 28 | let mut path = get_shell_integration_dir(); 29 | path.push("fish-integration.fish"); 30 | path 31 | } 32 | 33 | /// Detect the current shell from environment variables 34 | pub fn detect_current_shell() -> String { 35 | // First check for FISH_VERSION environment variable (highest priority) 36 | if env::var("FISH_VERSION").is_ok() { 37 | return "fish".to_string(); 38 | } 39 | 40 | // Then check SHELL environment variable 41 | if let Ok(shell_path) = env::var("SHELL") { 42 | let shell_path = shell_path.to_lowercase(); 43 | 44 | // Check for fish first (to match FISH_VERSION priority) 45 | if shell_path.contains("fish") { 46 | return "fish".to_string(); 47 | } 48 | 49 | // Then check for other shells 50 | if shell_path.contains("zsh") { 51 | return "zsh".to_string(); 52 | } 53 | if shell_path.contains("bash") { 54 | return "bash".to_string(); 55 | } 56 | } 57 | 58 | // Default to bash if no shell is detected or unknown shell 59 | "bash".to_string() 60 | } 61 | 62 | /// Get the shell integration script path for a specific shell 63 | pub fn get_shell_integration_script(shell: &str) -> Result { 64 | let shell_lower = shell.to_lowercase(); 65 | match shell_lower.as_str() { 66 | "zsh" => Ok(get_zsh_integration_path()), 67 | "bash" => Ok(get_bash_integration_path()), 68 | "fish" => Ok(get_fish_integration_path()), 69 | _ => Err(anyhow!("Unsupported shell: {}", shell)), 70 | } 71 | } 72 | 73 | /// Initialize shell integration 74 | pub fn init_shell(shell_override: Option) -> Result { 75 | let shell = if let Some(shell) = shell_override { 76 | shell 77 | } else { 78 | detect_current_shell() 79 | }; 80 | 81 | get_shell_integration_script(&shell) 82 | } 83 | -------------------------------------------------------------------------------- /src/shell/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod hooks; 2 | -------------------------------------------------------------------------------- /src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod app; 2 | pub mod add; 3 | 4 | pub use app::App; 5 | pub use add::AddCommandApp; 6 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod time; 2 | pub mod params; 3 | -------------------------------------------------------------------------------- /src/utils/params.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use colored::*; 3 | use crossterm::{ 4 | cursor::MoveTo, 5 | event::{self, Event, KeyCode}, 6 | QueueableCommand, 7 | style::Print, 8 | terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, 9 | }; 10 | use regex::Regex; 11 | use std::{ 12 | collections::HashMap, 13 | io::{stdout, Stdout, Write}, 14 | }; 15 | 16 | use crate::db::models::Parameter; 17 | 18 | pub fn parse_parameters(command: &str) -> Vec { 19 | let re = Regex::new(r"@([a-zA-Z_][a-zA-Z0-9_]*)(?::([^@\s][^@]*))?").unwrap(); 20 | let mut parameters = Vec::new(); 21 | 22 | for cap in re.captures_iter(command) { 23 | let name = cap[1].to_string(); 24 | let description = cap.get(2).map(|m| { 25 | let desc = m.as_str().trim_end(); 26 | if let Some(space_pos) = desc.find(char::is_whitespace) { 27 | &desc[..space_pos] 28 | } else { 29 | desc 30 | }.to_string() 31 | }); 32 | parameters.push(Parameter::with_description(name, description)); 33 | } 34 | 35 | parameters 36 | } 37 | 38 | pub fn substitute_parameters(command: &str, parameters: &[Parameter], test_input: Option<&str>) -> Result { 39 | let is_test = test_input.is_some() || std::env::var("COMMAND_VAULT_TEST").is_ok(); 40 | if is_test { 41 | let mut final_command = command.to_string(); 42 | let test_values: Vec<&str> = if let Some(input) = test_input { 43 | if input.is_empty() { 44 | parameters.iter() 45 | .map(|p| p.description.as_deref().unwrap_or("")) 46 | .collect() 47 | } else { 48 | input.split('\n').collect() 49 | } 50 | } else { 51 | // When no test input is provided, use descriptions 52 | parameters.iter() 53 | .map(|p| p.description.as_deref().unwrap_or("")) 54 | .collect() 55 | }; 56 | 57 | // First, remove all parameter descriptions from the command 58 | for param in parameters { 59 | if let Some(desc) = ¶m.description { 60 | // Match the exact pattern including the @ symbol 61 | let pattern = format!("@{}:{}", param.name, desc); 62 | final_command = final_command.replace(&pattern, &format!("@{}", param.name)); 63 | } 64 | } 65 | 66 | // Then replace parameters with values 67 | for (i, param) in parameters.iter().enumerate() { 68 | let value = if i < test_values.len() { 69 | test_values[i] 70 | } else { 71 | param.description.as_deref().unwrap_or("") 72 | }; 73 | 74 | let needs_quotes = value.is_empty() || 75 | value.contains(' ') || 76 | value.contains('*') || 77 | value.contains(';') || 78 | value.contains('|') || 79 | value.contains('>') || 80 | value.contains('<') || 81 | command.contains('>') || 82 | command.contains('<') || 83 | command.contains('|') || 84 | final_command.starts_with("grep"); 85 | 86 | let quoted_value = if needs_quotes && !value.starts_with('\'') && !value.starts_with('"') { 87 | format!("'{}'", value.replace('\'', "'\\''")) 88 | } else { 89 | value.to_string() 90 | }; 91 | 92 | final_command = final_command.replace(&format!("@{}", param.name), "ed_value); 93 | } 94 | 95 | if std::env::var("COMMAND_VAULT_DEBUG").is_ok() { 96 | eprintln!("[DEBUG] Final result: {}", final_command); 97 | } 98 | Ok(final_command) 99 | } else { 100 | prompt_parameters(command, parameters, test_input) 101 | } 102 | } 103 | 104 | pub fn prompt_parameters(command: &str, parameters: &[Parameter], test_input: Option<&str>) -> Result { 105 | let is_test = test_input.is_some() || std::env::var("COMMAND_VAULT_TEST").is_ok(); 106 | let result = (|| -> Result { 107 | let mut param_values: HashMap = HashMap::new(); 108 | 109 | for param in parameters { 110 | let value = if is_test { 111 | if let Some(input) = test_input { 112 | input.to_string() 113 | } else { 114 | param.description.clone().unwrap_or_default() 115 | } 116 | } else { 117 | enable_raw_mode()?; 118 | let mut stdout = stdout(); 119 | stdout.queue(Clear(ClearType::All))?; 120 | 121 | // Function to update the preview 122 | let update_preview = |stdout: &mut Stdout, current_value: &str| -> Result<()> { 123 | let mut preview_command = command.to_string(); 124 | 125 | // Add all previous parameter values 126 | for (name, value) in ¶m_values { 127 | let needs_quotes = value.is_empty() || 128 | value.contains(' ') || 129 | value.contains('*') || 130 | value.contains(';') || 131 | value.contains('|') || 132 | value.contains('>') || 133 | value.contains('<') || 134 | preview_command.starts_with("grep"); 135 | 136 | let quoted_value = if needs_quotes && !value.starts_with('\'') && !value.starts_with('"') { 137 | format!("'{}'", value.replace('\'', "'\\''")) 138 | } else { 139 | value.clone() 140 | }; 141 | 142 | preview_command = preview_command.replace(&format!("@{}", name), "ed_value); 143 | } 144 | 145 | // Add current parameter value 146 | let needs_quotes = current_value.is_empty() || 147 | current_value.contains(' ') || 148 | current_value.contains('*') || 149 | current_value.contains(';') || 150 | current_value.contains('|') || 151 | current_value.contains('>') || 152 | current_value.contains('<') || 153 | preview_command.starts_with("grep"); 154 | 155 | let quoted_value = if needs_quotes && !current_value.starts_with('\'') && !current_value.starts_with('"') { 156 | format!("'{}'", current_value.replace('\'', "'\\''")) 157 | } else { 158 | current_value.to_string() 159 | }; 160 | 161 | preview_command = preview_command.replace(&format!("@{}", param.name), "ed_value); 162 | 163 | stdout.queue(MoveTo(0, 0))? 164 | .queue(Print("─".repeat(45).dimmed()))?; 165 | stdout.queue(MoveTo(0, 1))? 166 | .queue(Clear(ClearType::CurrentLine))? 167 | .queue(Print(format!("{}: {}", 168 | "Command to execute".blue().bold(), 169 | preview_command.green() 170 | )))?; 171 | stdout.queue(MoveTo(0, 2))? 172 | .queue(Print(format!("{}: {}", 173 | "Working directory".cyan().bold(), 174 | std::env::current_dir()?.to_string_lossy().white() 175 | )))?; 176 | Ok(()) 177 | }; 178 | 179 | // Initial display 180 | update_preview(&mut stdout, "")?; 181 | 182 | stdout.queue(MoveTo(0, 4))? 183 | .queue(Print("─".repeat(45).dimmed()))?; 184 | stdout.queue(MoveTo(0, 5))? 185 | .queue(Print(format!("{}: {}", 186 | "Parameter".blue().bold(), 187 | param.name.green() 188 | )))?; 189 | if let Some(desc) = ¶m.description { 190 | stdout.queue(MoveTo(0, 6))? 191 | .queue(Print(format!("{}: {}", 192 | "Description".cyan().bold(), 193 | desc.white() 194 | )))?; 195 | } 196 | stdout.queue(MoveTo(0, 7))? 197 | .queue(Print(format!("{}: ", "Enter value".yellow().bold())))?; 198 | stdout.flush()?; 199 | 200 | let mut value = String::new(); 201 | let mut cursor_pos = 0; 202 | 203 | loop { 204 | if let Event::Key(key) = event::read()? { 205 | match key.code { 206 | KeyCode::Enter => break, 207 | KeyCode::Char('c') if key.modifiers.contains(event::KeyModifiers::CONTROL) => { 208 | // Handle Ctrl+C 209 | disable_raw_mode()?; 210 | stdout.queue(Clear(ClearType::All))?; 211 | stdout.queue(MoveTo(0, 0))?; 212 | stdout.flush()?; 213 | return Err(anyhow::anyhow!("Operation cancelled by user")); 214 | } 215 | KeyCode::Char(c) => { 216 | value.insert(cursor_pos, c); 217 | cursor_pos += 1; 218 | } 219 | KeyCode::Backspace if cursor_pos > 0 => { 220 | value.remove(cursor_pos - 1); 221 | cursor_pos -= 1; 222 | } 223 | KeyCode::Left if cursor_pos > 0 => { 224 | cursor_pos -= 1; 225 | } 226 | KeyCode::Right if cursor_pos < value.len() => { 227 | cursor_pos += 1; 228 | } 229 | _ => {} 230 | } 231 | 232 | // Update command preview 233 | update_preview(&mut stdout, &value)?; 234 | 235 | // Redraw the value line 236 | stdout.queue(MoveTo(0, 7))? 237 | .queue(Clear(ClearType::CurrentLine))? 238 | .queue(Print(format!("{}: {}", 239 | "Enter value".yellow().bold(), 240 | value 241 | )))?; 242 | stdout.queue(MoveTo((cursor_pos + 13) as u16, 7))?; 243 | stdout.flush()?; 244 | } 245 | } 246 | 247 | disable_raw_mode()?; 248 | value 249 | }; 250 | 251 | param_values.insert(param.name.clone(), value); 252 | } 253 | 254 | // First, remove all parameter descriptions from the command 255 | let mut final_command = command.to_string(); 256 | for param in parameters { 257 | if let Some(desc) = ¶m.description { 258 | // Match the exact pattern including the @ symbol 259 | let pattern = format!("@{}:{}", param.name, desc); 260 | final_command = final_command.replace(&pattern, &format!("@{}", param.name)); 261 | } 262 | } 263 | 264 | // Build final command with parameter values 265 | for (name, value) in ¶m_values { 266 | let needs_quotes = value.is_empty() || 267 | value.contains(' ') || 268 | value.contains('*') || 269 | value.contains(';') || 270 | value.contains('|') || 271 | value.contains('>') || 272 | value.contains('<') || 273 | command.contains('>') || 274 | command.contains('<') || 275 | command.contains('|') || 276 | final_command.starts_with("grep"); 277 | 278 | let quoted_value = if needs_quotes && !value.starts_with('\'') && !value.starts_with('"') { 279 | format!("'{}'", value.replace('\'', "'\\''")) 280 | } else { 281 | value.clone() 282 | }; 283 | 284 | final_command = final_command.replace(&format!("@{}", name), "ed_value); 285 | } 286 | 287 | if !is_test { 288 | let mut stdout = stdout(); 289 | stdout.queue(Clear(ClearType::All))?; 290 | stdout.queue(MoveTo(0, 0))?; 291 | stdout.flush()?; 292 | } 293 | 294 | Ok(final_command) 295 | })(); 296 | 297 | if !is_test { 298 | disable_raw_mode()?; 299 | } 300 | 301 | result 302 | } 303 | -------------------------------------------------------------------------------- /src/utils/time.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, TimeZone, Utc, NaiveDate}; 2 | 3 | pub fn parse_datetime(s: &str) -> Option> { 4 | // Try RFC3339 format first 5 | if let Ok(dt) = DateTime::parse_from_rfc3339(s) { 6 | return Some(dt.with_timezone(&Utc)); 7 | } 8 | 9 | // Try common date formats 10 | let date_formats = [ 11 | "%Y-%m-%d", 12 | "%Y/%m/%d", 13 | "%d-%m-%Y", 14 | "%d/%m/%Y", 15 | ]; 16 | 17 | let datetime_formats = [ 18 | "%Y-%m-%d %H:%M", 19 | "%Y-%m-%d %H:%M:%S", 20 | "%Y-%m-%d %H:%M:%S UTC", 21 | "%d/%m/%Y %H:%M", 22 | "%d/%m/%Y %H:%M:%S", 23 | ]; 24 | 25 | // Try date-only formats first 26 | for format in date_formats { 27 | if let Ok(naive_date) = NaiveDate::parse_from_str(s, format) { 28 | let naive_datetime = naive_date.and_hms_opt(0, 0, 0).unwrap(); 29 | return Some(Utc.from_utc_datetime(&naive_datetime)); 30 | } 31 | } 32 | 33 | // Then try datetime formats 34 | for format in datetime_formats { 35 | if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(s, format) { 36 | return Some(Utc.from_utc_datetime(&naive)); 37 | } 38 | } 39 | 40 | None 41 | } 42 | -------------------------------------------------------------------------------- /src/version.rs: -------------------------------------------------------------------------------- 1 | /// The current version of the application. 2 | /// This should match the version in Cargo.toml 3 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 4 | 5 | /// The name of the application 6 | pub const APP_NAME: &str = env!("CARGO_PKG_NAME"); 7 | 8 | /// The authors of the application 9 | pub const APP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); 10 | 11 | /// The description of the application 12 | pub const APP_DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); 13 | -------------------------------------------------------------------------------- /tests/args_test.rs: -------------------------------------------------------------------------------- 1 | use command_vault::cli::args::{Cli, Commands, TagCommands}; 2 | use clap::Parser; 3 | 4 | #[test] 5 | fn test_parse_args_add() { 6 | let args = vec!["cv", "add", "--", "ls", "-l"]; 7 | let cli = Cli::parse_from(args); 8 | match cli.command { 9 | Commands::Add { command, tags } => { 10 | assert_eq!(command, vec!["ls", "-l"]); 11 | assert!(tags.is_empty()); 12 | } 13 | _ => panic!("Expected Add command"), 14 | } 15 | } 16 | 17 | #[test] 18 | fn test_parse_args_add_with_tags() { 19 | let args = vec!["cv", "add", "-t", "file", "-t", "list", "--", "ls", "-l"]; 20 | let cli = Cli::parse_from(args); 21 | match cli.command { 22 | Commands::Add { command, tags } => { 23 | assert_eq!(command, vec!["ls", "-l"]); 24 | assert_eq!(tags, vec!["file", "list"]); 25 | } 26 | _ => panic!("Expected Add command"), 27 | } 28 | } 29 | 30 | #[test] 31 | fn test_parse_args_ls() { 32 | let args = vec!["cv", "ls"]; 33 | let cli = Cli::parse_from(args); 34 | match cli.command { 35 | Commands::Ls { limit, asc } => { 36 | assert_eq!(limit, 50); 37 | assert!(!asc); 38 | } 39 | _ => panic!("Expected Ls command"), 40 | } 41 | } 42 | 43 | #[test] 44 | fn test_parse_args_ls_with_limit() { 45 | let args = vec!["cv", "ls", "--limit", "5"]; 46 | let cli = Cli::parse_from(args); 47 | match cli.command { 48 | Commands::Ls { limit, asc } => { 49 | assert_eq!(limit, 5); 50 | assert!(!asc); 51 | } 52 | _ => panic!("Expected Ls command"), 53 | } 54 | } 55 | 56 | #[test] 57 | fn test_parse_args_exec() { 58 | let args = vec!["cv", "exec", "1"]; 59 | let cli = Cli::parse_from(args); 60 | match cli.command { 61 | Commands::Exec { command_id, debug } => { 62 | assert_eq!(command_id, 1); 63 | assert_eq!(debug, false); // Default value should be false 64 | } 65 | _ => panic!("Expected Exec command"), 66 | } 67 | 68 | // Test with debug flag 69 | let args = vec!["cv", "exec", "1", "--debug"]; 70 | let cli = Cli::parse_from(args); 71 | match cli.command { 72 | Commands::Exec { command_id, debug } => { 73 | assert_eq!(command_id, 1); 74 | assert_eq!(debug, true); 75 | } 76 | _ => panic!("Expected Exec command"), 77 | } 78 | } 79 | 80 | #[test] 81 | fn test_parse_args_search() { 82 | let args = vec!["cv", "search", "git"]; 83 | let cli = Cli::parse_from(args); 84 | match cli.command { 85 | Commands::Search { query, limit } => { 86 | assert_eq!(query, "git"); 87 | assert_eq!(limit, 10); 88 | } 89 | _ => panic!("Expected Search command"), 90 | } 91 | } 92 | 93 | #[test] 94 | fn test_parse_args_tag_add() { 95 | let args = vec!["cv", "tag", "add", "1", "--", "git", "vcs"]; 96 | let cli = Cli::parse_from(args); 97 | match cli.command { 98 | Commands::Tag { action } => { 99 | match action { 100 | TagCommands::Add { command_id, tags } => { 101 | assert_eq!(command_id, 1); 102 | assert_eq!(tags, vec!["git", "vcs"]); 103 | } 104 | _ => panic!("Expected Tag Add command"), 105 | } 106 | } 107 | _ => panic!("Expected Tag command"), 108 | } 109 | } 110 | 111 | #[test] 112 | fn test_parse_args_tag_remove() { 113 | let args = vec!["cv", "tag", "remove", "1", "--", "git"]; 114 | let cli = Cli::parse_from(args); 115 | match cli.command { 116 | Commands::Tag { action } => { 117 | match action { 118 | TagCommands::Remove { command_id, tag } => { 119 | assert_eq!(command_id, 1); 120 | assert_eq!(tag, "git"); 121 | } 122 | _ => panic!("Expected Tag Remove command"), 123 | } 124 | } 125 | _ => panic!("Expected Tag command"), 126 | } 127 | } 128 | 129 | #[test] 130 | fn test_parse_args_tag_list() { 131 | let args = vec!["cv", "tag", "list"]; 132 | let cli = Cli::parse_from(args); 133 | match cli.command { 134 | Commands::Tag { action } => { 135 | match action { 136 | TagCommands::List => (), 137 | _ => panic!("Expected Tag List command"), 138 | } 139 | } 140 | _ => panic!("Expected Tag command"), 141 | } 142 | } 143 | 144 | #[test] 145 | fn test_parse_args_tag_search() { 146 | let args = vec!["cv", "tag", "search", "--", "git"]; 147 | let cli = Cli::parse_from(args); 148 | match cli.command { 149 | Commands::Tag { action } => { 150 | match action { 151 | TagCommands::Search { tag, limit } => { 152 | assert_eq!(tag, "git"); 153 | assert_eq!(limit, 10); 154 | } 155 | _ => panic!("Expected Tag Search command"), 156 | } 157 | } 158 | _ => panic!("Expected Tag command"), 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /tests/cli_test.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use command_vault::cli::args::{Cli, Commands, TagCommands}; 3 | use clap::Parser; 4 | 5 | #[test] 6 | fn test_add_command_parsing() -> Result<()> { 7 | // Test basic command without tags 8 | let args = Cli::try_parse_from([ 9 | "command-vault", 10 | "add", 11 | "--", 12 | "git", 13 | "commit", 14 | "-m", 15 | "test", 16 | ])?; 17 | 18 | match args.command { 19 | Commands::Add { command, tags } => { 20 | assert_eq!(command.join(" "), "git commit -m test"); 21 | assert_eq!(tags, Vec::::new()); 22 | } 23 | _ => panic!("Expected Add command"), 24 | } 25 | 26 | // Test with tags 27 | let args = Cli::try_parse_from([ 28 | "command-vault", 29 | "add", 30 | "--tags", 31 | "git", 32 | "--tags", 33 | "vcs", 34 | "--", 35 | "git", 36 | "commit", 37 | "-m", 38 | "test", 39 | ])?; 40 | 41 | match args.command { 42 | Commands::Add { command, tags } => { 43 | assert_eq!(command.join(" "), "git commit -m test"); 44 | assert_eq!(tags, vec!["git", "vcs"]); 45 | } 46 | _ => panic!("Expected Add command"), 47 | } 48 | 49 | // Test with multiple words in command (no dashes) 50 | let args = Cli::try_parse_from([ 51 | "command-vault", 52 | "add", 53 | "--", 54 | "echo", 55 | "hello world", 56 | ])?; 57 | 58 | match args.command { 59 | Commands::Add { command, tags } => { 60 | assert_eq!(command.join(" "), "echo hello world"); 61 | assert_eq!(tags, Vec::::new()); 62 | } 63 | _ => panic!("Expected Add command"), 64 | } 65 | 66 | // Test add command without any command (missing --) 67 | let result = Cli::try_parse_from([ 68 | "command-vault", 69 | "add", 70 | ]); 71 | assert!(result.is_err()); 72 | assert!(result.unwrap_err().to_string().contains("the following required arguments were not provided")); 73 | 74 | Ok(()) 75 | } 76 | 77 | #[test] 78 | fn test_search_command_parsing() -> Result<()> { 79 | let args = Cli::try_parse_from([ 80 | "command-vault", 81 | "search", 82 | "git commit", 83 | "--limit", 84 | "5", 85 | ])?; 86 | 87 | match args.command { 88 | Commands::Search { query, limit } => { 89 | assert_eq!(query, "git commit"); 90 | assert_eq!(limit, 5); 91 | } 92 | _ => panic!("Expected Search command"), 93 | } 94 | Ok(()) 95 | } 96 | 97 | #[test] 98 | fn test_ls_command_parsing() -> Result<()> { 99 | let args = Cli::try_parse_from([ 100 | "command-vault", 101 | "ls", 102 | "--limit", 103 | "20", 104 | "--asc", 105 | ])?; 106 | 107 | match args.command { 108 | Commands::Ls { limit, asc } => { 109 | assert_eq!(limit, 20); 110 | assert!(asc); 111 | } 112 | _ => panic!("Expected Ls command"), 113 | } 114 | Ok(()) 115 | } 116 | 117 | #[test] 118 | fn test_ls_command_default_behavior() -> Result<()> { 119 | // Test ls with default values 120 | let args = Cli::try_parse_from([ 121 | "command-vault", 122 | "ls", 123 | ])?; 124 | 125 | match args.command { 126 | Commands::Ls { limit, asc } => { 127 | assert_eq!(limit, 50); // Default limit is 50 128 | assert!(!asc); // Default is descending order 129 | } 130 | _ => panic!("Expected Ls command"), 131 | } 132 | Ok(()) 133 | } 134 | 135 | #[test] 136 | fn test_tag_commands_parsing() -> Result<()> { 137 | // Test tag add 138 | let args = Cli::try_parse_from([ 139 | "command-vault", 140 | "tag", 141 | "add", 142 | "1", 143 | "important", 144 | "urgent", 145 | ])?; 146 | 147 | match args.command { 148 | Commands::Tag { action: TagCommands::Add { command_id, tags } } => { 149 | assert_eq!(command_id, 1); 150 | assert_eq!(tags, vec!["important", "urgent"]); 151 | } 152 | _ => panic!("Expected Tag Add command"), 153 | } 154 | 155 | // Test tag remove 156 | let args = Cli::try_parse_from([ 157 | "command-vault", 158 | "tag", 159 | "remove", 160 | "1", 161 | "urgent", 162 | ])?; 163 | 164 | match args.command { 165 | Commands::Tag { action: TagCommands::Remove { command_id, tag } } => { 166 | assert_eq!(command_id, 1); 167 | assert_eq!(tag, "urgent"); 168 | } 169 | _ => panic!("Expected Tag Remove command"), 170 | } 171 | 172 | // Test tag list 173 | let args = Cli::try_parse_from([ 174 | "command-vault", 175 | "tag", 176 | "list", 177 | ])?; 178 | 179 | match args.command { 180 | Commands::Tag { action: TagCommands::List } => (), 181 | _ => panic!("Expected Tag List command"), 182 | } 183 | 184 | // Test tag search 185 | let args = Cli::try_parse_from([ 186 | "command-vault", 187 | "tag", 188 | "search", 189 | "git", 190 | "--limit", 191 | "5", 192 | ])?; 193 | 194 | match args.command { 195 | Commands::Tag { action: TagCommands::Search { tag, limit } } => { 196 | assert_eq!(tag, "git"); 197 | assert_eq!(limit, 5); 198 | } 199 | _ => panic!("Expected Tag Search command"), 200 | } 201 | Ok(()) 202 | } 203 | 204 | #[test] 205 | fn test_exec_command_parsing() -> Result<()> { 206 | let args = Cli::try_parse_from([ 207 | "command-vault", 208 | "exec", 209 | "42", 210 | ])?; 211 | 212 | match args.command { 213 | Commands::Exec { command_id, debug } => { 214 | assert_eq!(command_id, 42); 215 | assert_eq!(debug, false); 216 | } 217 | _ => panic!("Expected Exec command"), 218 | } 219 | Ok(()) 220 | } 221 | 222 | #[test] 223 | fn test_parse_exec_command() { 224 | let args = vec!["command-vault", "exec", "123"]; 225 | let cli = Cli::try_parse_from(args).unwrap(); 226 | match cli.command { 227 | Commands::Exec { command_id, debug } => { 228 | assert_eq!(command_id, 123); 229 | assert_eq!(debug, false); 230 | } 231 | _ => panic!("Expected Exec command"), 232 | } 233 | 234 | // Test with debug flag 235 | let args = vec!["command-vault", "exec", "123", "--debug"]; 236 | let cli = Cli::try_parse_from(args).unwrap(); 237 | match cli.command { 238 | Commands::Exec { command_id, debug } => { 239 | assert_eq!(command_id, 123); 240 | assert_eq!(debug, true); 241 | } 242 | _ => panic!("Expected Exec command"), 243 | } 244 | } 245 | 246 | #[test] 247 | fn test_invalid_command_id() { 248 | let result = Cli::try_parse_from([ 249 | "command-vault", 250 | "exec", 251 | "not_a_number", 252 | ]); 253 | assert!(result.is_err()); 254 | let err = result.unwrap_err().to_string(); 255 | assert!(err.contains("invalid value 'not_a_number'")); 256 | } 257 | 258 | #[test] 259 | fn test_missing_required_args() { 260 | let result = Cli::try_parse_from([ 261 | "command-vault", 262 | "search", 263 | ]); 264 | assert!(result.is_err()); 265 | let err = result.unwrap_err().to_string(); 266 | assert!(err.contains("")); 267 | } 268 | 269 | #[test] 270 | fn test_search_command_default_limit() -> Result<()> { 271 | // Test search with default limit 272 | let args = Cli::try_parse_from([ 273 | "command-vault", 274 | "search", 275 | "git commit", 276 | ])?; 277 | 278 | match args.command { 279 | Commands::Search { query, limit } => { 280 | assert_eq!(query, "git commit"); 281 | assert_eq!(limit, 10); // Default limit is 10 282 | } 283 | _ => panic!("Expected Search command"), 284 | } 285 | Ok(()) 286 | } 287 | 288 | #[test] 289 | fn test_delete_command_parsing() -> Result<()> { 290 | // Test basic delete 291 | let args = Cli::try_parse_from([ 292 | "command-vault", 293 | "delete", 294 | "42", 295 | ])?; 296 | 297 | match args.command { 298 | Commands::Delete { command_id } => { 299 | assert_eq!(command_id, 42); 300 | } 301 | _ => panic!("Expected Delete command"), 302 | } 303 | 304 | // Test missing command ID 305 | let result = Cli::try_parse_from([ 306 | "command-vault", 307 | "delete", 308 | ]); 309 | assert!(result.is_err()); 310 | assert!(result.unwrap_err().to_string().contains("required")); 311 | 312 | Ok(()) 313 | } 314 | 315 | #[test] 316 | fn test_shell_init_command_parsing() -> Result<()> { 317 | // Test default shell initialization 318 | let args = Cli::try_parse_from([ 319 | "command-vault", 320 | "shell-init", 321 | ])?; 322 | 323 | match args.command { 324 | Commands::ShellInit { shell } => { 325 | assert!(shell.is_none()); 326 | } 327 | _ => panic!("Expected ShellInit command"), 328 | } 329 | 330 | // Test explicit shell override 331 | let args = Cli::try_parse_from([ 332 | "command-vault", 333 | "shell-init", 334 | "--shell", 335 | "fish", 336 | ])?; 337 | 338 | match args.command { 339 | Commands::ShellInit { shell } => { 340 | assert_eq!(shell, Some("fish".to_string())); 341 | } 342 | _ => panic!("Expected ShellInit command"), 343 | } 344 | 345 | Ok(()) 346 | } 347 | 348 | #[test] 349 | fn test_tag_commands_all() -> Result<()> { 350 | // Test tag remove 351 | let args = Cli::try_parse_from([ 352 | "command-vault", 353 | "tag", 354 | "remove", 355 | "1", 356 | "git", 357 | ])?; 358 | 359 | match args.command { 360 | Commands::Tag { action: TagCommands::Remove { command_id, tag } } => { 361 | assert_eq!(command_id, 1); 362 | assert_eq!(tag, "git"); 363 | } 364 | _ => panic!("Expected Tag Remove command"), 365 | } 366 | 367 | // Test tag list 368 | let args = Cli::try_parse_from([ 369 | "command-vault", 370 | "tag", 371 | "list", 372 | ])?; 373 | 374 | match args.command { 375 | Commands::Tag { action: TagCommands::List } => (), 376 | _ => panic!("Expected Tag List command"), 377 | } 378 | 379 | // Test tag search 380 | let args = Cli::try_parse_from([ 381 | "command-vault", 382 | "tag", 383 | "search", 384 | "git", 385 | "--limit", 386 | "5", 387 | ])?; 388 | 389 | match args.command { 390 | Commands::Tag { action: TagCommands::Search { tag, limit } } => { 391 | assert_eq!(tag, "git"); 392 | assert_eq!(limit, 5); 393 | } 394 | _ => panic!("Expected Tag Search command"), 395 | } 396 | 397 | // Test tag search with default limit 398 | let args = Cli::try_parse_from([ 399 | "command-vault", 400 | "tag", 401 | "search", 402 | "git", 403 | ])?; 404 | 405 | match args.command { 406 | Commands::Tag { action: TagCommands::Search { tag, limit } } => { 407 | assert_eq!(tag, "git"); 408 | assert_eq!(limit, 10); // Default limit is 10 409 | } 410 | _ => panic!("Expected Tag Search command"), 411 | } 412 | 413 | Ok(()) 414 | } 415 | 416 | #[test] 417 | fn test_add_command_with_parameters() -> Result<()> { 418 | // Test add command with basic parameter 419 | let args = Cli::try_parse_from([ 420 | "command-vault", 421 | "add", 422 | "--", 423 | "touch", 424 | "@filename", 425 | ])?; 426 | 427 | match args.command { 428 | Commands::Add { command, tags } => { 429 | assert_eq!(command.join(" "), "touch @filename"); 430 | assert_eq!(tags, Vec::::new()); 431 | } 432 | _ => panic!("Expected Add command"), 433 | } 434 | 435 | // Test add command with parameter description 436 | let args = Cli::try_parse_from([ 437 | "command-vault", 438 | "add", 439 | "--", 440 | "touch", 441 | "@filename:Name of file to create", 442 | ])?; 443 | 444 | match args.command { 445 | Commands::Add { command, tags } => { 446 | assert_eq!(command.join(" "), "touch @filename:Name of file to create"); 447 | assert_eq!(tags, Vec::::new()); 448 | } 449 | _ => panic!("Expected Add command"), 450 | } 451 | 452 | // Test add command with parameter default value 453 | let args = Cli::try_parse_from([ 454 | "command-vault", 455 | "add", 456 | "--", 457 | "touch", 458 | "@filename:Name of file to create=test.txt", 459 | ])?; 460 | 461 | match args.command { 462 | Commands::Add { command, tags } => { 463 | assert_eq!(command.join(" "), "touch @filename:Name of file to create=test.txt"); 464 | assert_eq!(tags, Vec::::new()); 465 | } 466 | _ => panic!("Expected Add command"), 467 | } 468 | 469 | Ok(()) 470 | } 471 | -------------------------------------------------------------------------------- /tests/commands_test.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use chrono::{TimeZone, Utc}; 3 | use command_vault::{ 4 | cli::{args::Commands, commands::handle_command}, 5 | db::{Command, models::Parameter}, 6 | }; 7 | use tempfile::tempdir; 8 | use std::env; 9 | 10 | mod test_utils; 11 | use test_utils::create_test_db; 12 | 13 | // Set up test environment 14 | #[ctor::ctor] 15 | fn setup() { 16 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 17 | } 18 | 19 | #[test] 20 | fn test_ls_empty() -> Result<()> { 21 | let (db, _db_dir) = create_test_db()?; 22 | let commands = db.list_commands(10, false)?; 23 | assert_eq!(commands.len(), 0); 24 | Ok(()) 25 | } 26 | 27 | #[test] 28 | fn test_handle_command_list() -> Result<()> { 29 | let (mut db, _db_dir) = create_test_db()?; 30 | let command = Command { 31 | id: None, 32 | command: "test command".to_string(), 33 | timestamp: Utc::now(), 34 | directory: "/test".to_string(), 35 | tags: vec![], 36 | parameters: Vec::new(), 37 | }; 38 | db.add_command(&command)?; 39 | let commands = db.list_commands(10, false)?; 40 | assert_eq!(commands.len(), 1); 41 | assert_eq!(commands[0].command, "test command"); 42 | Ok(()) 43 | } 44 | 45 | #[test] 46 | fn test_ls_with_limit() -> Result<()> { 47 | let (mut db, _db_dir) = create_test_db()?; 48 | for i in 0..5 { 49 | let command = Command { 50 | id: None, 51 | command: format!("command {}", i), 52 | timestamp: Utc::now(), 53 | directory: "/test".to_string(), 54 | tags: vec![], 55 | parameters: Vec::new(), 56 | }; 57 | db.add_command(&command)?; 58 | } 59 | let commands = db.list_commands(3, false)?; 60 | assert_eq!(commands.len(), 3); 61 | Ok(()) 62 | } 63 | 64 | #[test] 65 | fn test_ls_ordering() -> Result<()> { 66 | let (mut db, _db_dir) = create_test_db()?; 67 | let timestamps = vec![ 68 | Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(), 69 | Utc.with_ymd_and_hms(2022, 1, 2, 0, 0, 0).unwrap(), 70 | Utc.with_ymd_and_hms(2022, 1, 3, 0, 0, 0).unwrap(), 71 | ]; 72 | 73 | for (i, timestamp) in timestamps.iter().enumerate() { 74 | let command = Command { 75 | id: None, 76 | command: format!("command {}", i), 77 | timestamp: *timestamp, 78 | directory: "/test".to_string(), 79 | tags: vec![], 80 | parameters: Vec::new(), 81 | }; 82 | db.add_command(&command)?; 83 | } 84 | 85 | let commands = db.list_commands(10, false)?; 86 | assert_eq!(commands.len(), 3); 87 | assert_eq!(commands[0].command, "command 2"); 88 | assert_eq!(commands[1].command, "command 1"); 89 | assert_eq!(commands[2].command, "command 0"); 90 | Ok(()) 91 | } 92 | 93 | #[test] 94 | fn test_delete_command() -> Result<()> { 95 | let (mut db, _db_dir) = create_test_db()?; 96 | let command = Command { 97 | id: None, 98 | command: "test command".to_string(), 99 | timestamp: Utc::now(), 100 | directory: "/test".to_string(), 101 | tags: vec![], 102 | parameters: Vec::new(), 103 | }; 104 | let id = db.add_command(&command)?; 105 | db.delete_command(id)?; 106 | let commands = db.list_commands(10, false)?; 107 | assert_eq!(commands.len(), 0); 108 | Ok(()) 109 | } 110 | 111 | #[test] 112 | fn test_search_commands() -> Result<()> { 113 | let (mut db, _db_dir) = create_test_db()?; 114 | let command = Command { 115 | id: None, 116 | command: "test command".to_string(), 117 | timestamp: Utc::now(), 118 | directory: "/test".to_string(), 119 | tags: vec![], 120 | parameters: Vec::new(), 121 | }; 122 | db.add_command(&command)?; 123 | let commands = db.search_commands("test", 10)?; 124 | assert_eq!(commands.len(), 1); 125 | assert_eq!(commands[0].command, "test command"); 126 | Ok(()) 127 | } 128 | 129 | #[test] 130 | fn test_add_command_with_tags() -> Result<()> { 131 | let (mut db, _db_dir) = create_test_db()?; 132 | let temp_dir = tempdir()?; 133 | std::fs::create_dir_all(temp_dir.path())?; 134 | 135 | // Change to the test directory 136 | let original_dir = env::current_dir()?; 137 | let test_dir = temp_dir.path().canonicalize()?; 138 | env::set_current_dir(&test_dir)?; 139 | 140 | let command = vec!["test".to_string(), "command".to_string()]; 141 | let add_command = Commands::Add { 142 | command: command.clone(), 143 | tags: vec!["tag1".to_string(), "tag2".to_string()] 144 | }; 145 | 146 | handle_command(add_command, &mut db, false)?; 147 | 148 | let commands = db.list_commands(1, false)?; 149 | assert_eq!(commands.len(), 1); 150 | assert_eq!(commands[0].command, "test command"); 151 | assert_eq!(commands[0].tags, vec!["tag1", "tag2"]); 152 | 153 | // Restore the original directory 154 | env::set_current_dir(original_dir)?; 155 | 156 | Ok(()) 157 | } 158 | 159 | #[test] 160 | fn test_command_with_output() -> Result<()> { 161 | let (mut db, _db_dir) = create_test_db()?; 162 | 163 | // Test command that would produce output 164 | let command = vec!["echo".to_string(), "\"Hello, World!\"".to_string()]; 165 | let add_command = Commands::Add { 166 | command: command.clone(), 167 | tags: vec![] 168 | }; 169 | 170 | handle_command(add_command, &mut db, false)?; 171 | 172 | let commands = db.list_commands(1, false)?; 173 | assert_eq!(commands.len(), 1); 174 | assert_eq!(commands[0].command, "echo \"Hello, World!\""); 175 | 176 | Ok(()) 177 | } 178 | 179 | #[test] 180 | fn test_command_with_stderr() -> Result<()> { 181 | let (mut db, _db_dir) = create_test_db()?; 182 | 183 | // Test command that would produce stderr 184 | let command = vec!["ls".to_string(), "nonexistent_directory".to_string()]; 185 | let add_command = Commands::Add { 186 | command: command.clone(), 187 | tags: vec![] 188 | }; 189 | 190 | handle_command(add_command, &mut db, false)?; 191 | 192 | let commands = db.list_commands(1, false)?; 193 | assert_eq!(commands.len(), 1); 194 | assert_eq!(commands[0].command, "ls nonexistent_directory"); 195 | 196 | Ok(()) 197 | } 198 | 199 | #[test] 200 | fn test_git_log_format_command() -> Result<()> { 201 | let (mut db, _db_dir) = create_test_db()?; 202 | let temp_dir = tempdir()?; 203 | std::fs::create_dir_all(temp_dir.path())?; 204 | 205 | // Change to the test directory 206 | let original_dir = env::current_dir()?; 207 | let test_dir = temp_dir.path().canonicalize()?; 208 | env::set_current_dir(&test_dir)?; 209 | 210 | // Add the git log command with format string 211 | let format_str = "%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset"; 212 | let command = vec![ 213 | "git".to_string(), 214 | "log".to_string(), 215 | "--graph".to_string(), 216 | format!("--pretty=format:{}", format_str), 217 | "--abbrev-commit".to_string(), 218 | ]; 219 | 220 | let add_command = Commands::Add { 221 | command: command.clone(), 222 | tags: vec![] 223 | }; 224 | 225 | handle_command(add_command, &mut db, false)?; 226 | 227 | let commands = db.list_commands(1, false)?; 228 | assert_eq!(commands.len(), 1); 229 | assert_eq!( 230 | commands[0].command, 231 | format!("git log --graph \"--pretty=format:{}\" --abbrev-commit", format_str) 232 | ); 233 | 234 | // Restore the original directory 235 | env::set_current_dir(original_dir)?; 236 | 237 | Ok(()) 238 | } 239 | 240 | #[test] 241 | fn test_parameter_parsing() -> Result<()> { 242 | let (mut db, _db_dir) = create_test_db()?; 243 | 244 | // Test basic parameter 245 | let command = Command { 246 | id: None, 247 | command: "echo @message".to_string(), 248 | timestamp: Utc::now(), 249 | directory: "/test".to_string(), 250 | tags: vec![], 251 | parameters: vec![Parameter::with_description( 252 | "message".to_string(), 253 | Some("User_name".to_string()) 254 | )], 255 | }; 256 | let id = db.add_command(&command)?; 257 | let saved = db.get_command(id)?.unwrap(); 258 | assert_eq!(saved.parameters.len(), 1); 259 | assert_eq!(saved.parameters[0].name, "message"); 260 | assert_eq!(saved.parameters[0].description, Some("User_name".to_string())); 261 | 262 | // Test parameter with description 263 | let command = Command { 264 | id: None, 265 | command: "echo @message:User_name".to_string(), 266 | timestamp: Utc::now(), 267 | directory: "/test".to_string(), 268 | tags: vec![], 269 | parameters: vec![Parameter::with_description( 270 | "message".to_string(), 271 | Some("User_name".to_string()) 272 | )], 273 | }; 274 | let id = db.add_command(&command)?; 275 | let saved = db.get_command(id)?.unwrap(); 276 | assert_eq!(saved.parameters.len(), 1); 277 | assert_eq!(saved.parameters[0].name, "message"); 278 | assert_eq!(saved.parameters[0].description, Some("User_name".to_string())); 279 | 280 | Ok(()) 281 | } 282 | 283 | #[test] 284 | fn test_exec_command_with_parameters() -> Result<()> { 285 | // Ensure we're in test mode 286 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 287 | 288 | let (mut db, _db_dir) = create_test_db()?; 289 | let temp_dir = tempdir()?; 290 | let test_dir = temp_dir.path().canonicalize()?; 291 | 292 | // Add a command with parameters 293 | let command = Command { 294 | id: None, 295 | command: "echo @message".to_string(), 296 | timestamp: Utc::now(), 297 | directory: test_dir.to_string_lossy().to_string(), 298 | tags: vec![], 299 | parameters: vec![Parameter::with_description( 300 | "message".to_string(), 301 | Some("test message".to_string()) 302 | )], 303 | }; 304 | let id = db.add_command(&command)?; 305 | 306 | // Execute command with default parameter 307 | let exec_command = Commands::Exec { command_id: id, debug: false }; 308 | handle_command(exec_command, &mut db, false)?; 309 | 310 | // Verify command was saved correctly 311 | let saved = db.get_command(id)?.unwrap(); 312 | assert_eq!(saved.parameters.len(), 1); 313 | assert_eq!(saved.parameters[0].name, "message"); 314 | assert_eq!(saved.parameters[0].description, Some("test message".to_string())); 315 | 316 | Ok(()) 317 | } 318 | 319 | #[test] 320 | fn test_exec_command_not_found() -> Result<()> { 321 | let (mut db, _db_dir) = create_test_db()?; 322 | 323 | // Try to execute a non-existent command 324 | let exec_command = Commands::Exec { command_id: 999, debug: false }; 325 | let result = handle_command(exec_command, &mut db, false); 326 | 327 | // Verify that we get an error 328 | assert!(result.is_err()); 329 | assert!(result.unwrap_err().to_string().contains("Command not found")); 330 | 331 | Ok(()) 332 | } 333 | 334 | #[test] 335 | fn test_parameter_validation() -> Result<()> { 336 | let (mut db, _db_dir) = create_test_db()?; 337 | 338 | // Test invalid parameter name (starts with number) 339 | let command = Command { 340 | id: None, 341 | command: "echo @1name".to_string(), 342 | timestamp: Utc::now(), 343 | directory: "/test".to_string(), 344 | tags: vec![], 345 | parameters: vec![], 346 | }; 347 | let id = db.add_command(&command)?; 348 | let saved = db.get_command(id)?.unwrap(); 349 | assert_eq!(saved.parameters.len(), 0); // Invalid parameter should be ignored 350 | 351 | // Test invalid parameter name (special characters) 352 | let command = Command { 353 | id: None, 354 | command: "echo @name!".to_string(), 355 | timestamp: Utc::now(), 356 | directory: "/test".to_string(), 357 | tags: vec![], 358 | parameters: vec![], 359 | }; 360 | let id = db.add_command(&command)?; 361 | let saved = db.get_command(id)?.unwrap(); 362 | assert_eq!(saved.parameters.len(), 0); // Invalid parameter should be ignored 363 | 364 | Ok(()) 365 | } 366 | 367 | #[test] 368 | fn test_command_with_spaces_in_parameters() -> Result<()> { 369 | let (mut db, _db_dir) = create_test_db()?; 370 | let command = Command { 371 | id: None, 372 | command: "echo @message".to_string(), 373 | timestamp: Utc::now(), 374 | directory: "/test".to_string(), 375 | tags: vec!["test".to_string()], 376 | parameters: vec![Parameter::with_description( 377 | "message".to_string(), 378 | Some("A test message".to_string()) 379 | )], 380 | }; 381 | 382 | db.add_command(&command)?; 383 | let commands = db.list_commands(1, false)?; 384 | assert_eq!(commands.len(), 1); 385 | assert_eq!(commands[0].command, "echo @message"); 386 | assert_eq!(commands[0].parameters[0].name, "message"); 387 | assert_eq!(commands[0].parameters[0].description, Some("A test message".to_string())); 388 | Ok(()) 389 | } 390 | 391 | #[test] 392 | fn test_command_with_multiple_tags() -> Result<()> { 393 | let (mut db, _db_dir) = create_test_db()?; 394 | let command = Command { 395 | id: None, 396 | command: "test command".to_string(), 397 | timestamp: Utc::now(), 398 | directory: "/test".to_string(), 399 | tags: vec!["tag1".to_string(), "tag2".to_string(), "tag3".to_string()], 400 | parameters: Vec::new(), 401 | }; 402 | 403 | db.add_command(&command)?; 404 | let commands = db.list_commands(1, false)?; 405 | assert_eq!(commands.len(), 1); 406 | assert_eq!(commands[0].tags.len(), 3); 407 | assert!(commands[0].tags.contains(&"tag1".to_string())); 408 | assert!(commands[0].tags.contains(&"tag2".to_string())); 409 | assert!(commands[0].tags.contains(&"tag3".to_string())); 410 | Ok(()) 411 | } 412 | 413 | #[test] 414 | fn test_command_with_special_chars() -> Result<()> { 415 | let (mut db, _db_dir) = create_test_db()?; 416 | let command = Command { 417 | id: None, 418 | command: "grep -r \"@pattern\" @directory".to_string(), 419 | timestamp: Utc::now(), 420 | directory: "/test".to_string(), 421 | tags: vec!["search".to_string()], 422 | parameters: vec![ 423 | Parameter::with_description( 424 | "pattern".to_string(), 425 | Some("Search pattern".to_string()) 426 | ), 427 | Parameter::with_description( 428 | "directory".to_string(), 429 | Some("Directory to search in".to_string()) 430 | ), 431 | ], 432 | }; 433 | 434 | db.add_command(&command)?; 435 | let commands = db.list_commands(1, false)?; 436 | assert_eq!(commands.len(), 1); 437 | assert_eq!(commands[0].parameters.len(), 2); 438 | assert_eq!(commands[0].parameters[0].name, "pattern"); 439 | assert_eq!(commands[0].parameters[0].description, Some("Search pattern".to_string())); 440 | assert_eq!(commands[0].parameters[1].name, "directory"); 441 | assert_eq!(commands[0].parameters[1].description, Some("Directory to search in".to_string())); 442 | Ok(()) 443 | } 444 | 445 | #[test] 446 | fn test_handle_command_debug() -> Result<()> { 447 | let (mut db, _db_dir) = create_test_db()?; 448 | let temp_dir = tempdir()?; 449 | let test_dir = temp_dir.path().canonicalize()?; 450 | std::env::set_current_dir(&test_dir)?; 451 | 452 | // First add a simple command that works in any shell 453 | let add_command = Commands::Add { 454 | command: vec!["echo".to_string(), "test".to_string()], 455 | tags: vec![], 456 | }; 457 | handle_command(add_command, &mut db, true)?; 458 | 459 | // Then get the id of the added command 460 | let commands = db.list_commands(1, false)?; 461 | let id = commands[0].id.unwrap(); 462 | 463 | // Execute the command in debug mode 464 | let exec_command = Commands::Exec { command_id: id, debug: true }; 465 | handle_command(exec_command, &mut db, true)?; 466 | 467 | Ok(()) 468 | } 469 | 470 | #[test] 471 | fn test_handle_command_delete() -> Result<()> { 472 | let (mut db, _db_dir) = create_test_db()?; 473 | 474 | // Add a test command 475 | let command = Command { 476 | id: None, 477 | command: "test command".to_string(), 478 | timestamp: Utc::now(), 479 | directory: "/test".to_string(), 480 | tags: vec![], 481 | parameters: Vec::new(), 482 | }; 483 | let id = db.add_command(&command)?; 484 | 485 | // Verify command exists 486 | let commands = db.list_commands(10, false)?; 487 | assert_eq!(commands.len(), 1); 488 | 489 | // Delete the command 490 | handle_command(Commands::Delete { command_id: id }, &mut db, false)?; 491 | 492 | // Verify command was deleted 493 | let commands = db.list_commands(10, false)?; 494 | assert_eq!(commands.len(), 0); 495 | Ok(()) 496 | } 497 | 498 | #[test] 499 | fn test_handle_command_delete_nonexistent() -> Result<()> { 500 | let (mut db, _db_dir) = create_test_db()?; 501 | 502 | // Try to delete a command that doesn't exist 503 | let result = handle_command(Commands::Delete { command_id: 999 }, &mut db, false); 504 | 505 | // Verify we get an error 506 | assert!(result.is_err()); 507 | assert!(result.unwrap_err().to_string().contains("Command with ID 999 not found")); 508 | Ok(()) 509 | } 510 | 511 | #[test] 512 | fn test_handle_command_delete_with_tags() -> Result<()> { 513 | let (mut db, _db_dir) = create_test_db()?; 514 | 515 | // Add a test command with tags 516 | let command = Command { 517 | id: None, 518 | command: "test command".to_string(), 519 | timestamp: Utc::now(), 520 | directory: "/test".to_string(), 521 | tags: vec!["test".to_string(), "example".to_string()], 522 | parameters: Vec::new(), 523 | }; 524 | let id = db.add_command(&command)?; 525 | 526 | // Verify command exists with tags 527 | let commands = db.list_commands(10, false)?; 528 | assert_eq!(commands.len(), 1); 529 | assert_eq!(commands[0].tags.len(), 2); 530 | 531 | // Delete the command 532 | handle_command(Commands::Delete { command_id: id }, &mut db, false)?; 533 | 534 | // Verify command and its tags were deleted 535 | let commands = db.list_commands(10, false)?; 536 | assert_eq!(commands.len(), 0); 537 | 538 | // Verify tags were removed 539 | let tags = db.list_tags()?; 540 | assert_eq!(tags.len(), 0); 541 | Ok(()) 542 | } 543 | -------------------------------------------------------------------------------- /tests/db_test.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use chrono::Utc; 3 | use command_vault::db::{ 4 | models::{Command, Parameter}, 5 | Database, 6 | }; 7 | use std::fs; 8 | use tempfile::tempdir; 9 | 10 | fn create_test_command(command: &str, tags: Vec, parameters: Vec) -> Command { 11 | Command { 12 | id: None, 13 | command: command.to_string(), 14 | timestamp: Utc::now(), 15 | directory: "/test/dir".to_string(), 16 | tags, 17 | parameters, 18 | } 19 | } 20 | 21 | #[test] 22 | fn test_command_crud() -> Result<()> { 23 | let temp_dir = tempdir()?; 24 | let db_path = temp_dir.path().join("test.db"); 25 | let mut db = Database::new(db_path.to_str().unwrap())?; 26 | 27 | // Test adding a command 28 | let cmd = create_test_command( 29 | "echo test", 30 | vec!["test".to_string()], 31 | vec![], 32 | ); 33 | let id = db.add_command(&cmd)?; 34 | assert!(id > 0); 35 | 36 | // Test retrieving the command 37 | let retrieved = db.get_command(id)?.unwrap(); 38 | assert_eq!(retrieved.command, "echo test"); 39 | assert_eq!(retrieved.tags, vec!["test"]); 40 | assert!(retrieved.parameters.is_empty()); 41 | 42 | // Test updating the command 43 | let mut updated_cmd = retrieved.clone(); 44 | updated_cmd.command = "echo updated".to_string(); 45 | db.update_command(&updated_cmd)?; 46 | 47 | let retrieved_updated = db.get_command(id)?.unwrap(); 48 | assert_eq!(retrieved_updated.command, "echo updated"); 49 | 50 | // Test deleting the command 51 | db.delete_command(id)?; 52 | assert!(db.get_command(id)?.is_none()); 53 | 54 | Ok(()) 55 | } 56 | 57 | #[test] 58 | fn test_tag_operations() -> Result<()> { 59 | let temp_dir = tempdir()?; 60 | let db_path = temp_dir.path().join("test.db"); 61 | let mut db = Database::new(db_path.to_str().unwrap())?; 62 | 63 | // Add command with initial tags 64 | let cmd = create_test_command( 65 | "git status", 66 | vec!["git".to_string()], 67 | vec![], 68 | ); 69 | let id = db.add_command(&cmd)?; 70 | 71 | // Add more tags 72 | db.add_tags_to_command(id, &vec!["vcs".to_string(), "status".to_string()])?; 73 | let cmd = db.get_command(id)?.unwrap(); 74 | assert!(cmd.tags.contains(&"git".to_string())); 75 | assert!(cmd.tags.contains(&"vcs".to_string())); 76 | assert!(cmd.tags.contains(&"status".to_string())); 77 | 78 | // Remove a tag 79 | db.remove_tag_from_command(id, "status")?; 80 | let cmd = db.get_command(id)?.unwrap(); 81 | assert!(!cmd.tags.contains(&"status".to_string())); 82 | 83 | // Test tag search 84 | let results = db.search_by_tag("git", 10)?; 85 | assert_eq!(results.len(), 1); 86 | assert_eq!(results[0].command, "git status"); 87 | 88 | // Test listing tags 89 | let tags = db.list_tags()?; 90 | assert!(tags.iter().any(|(name, _)| name == "git")); 91 | assert!(tags.iter().any(|(name, _)| name == "vcs")); 92 | 93 | Ok(()) 94 | } 95 | 96 | #[test] 97 | fn test_command_with_parameters() -> Result<()> { 98 | let temp_dir = tempdir()?; 99 | let db_path = temp_dir.path().join("test.db"); 100 | let mut db = Database::new(db_path.to_str().unwrap())?; 101 | 102 | // Create command with parameters 103 | let params = vec![ 104 | Parameter::new("branch".to_string()), 105 | Parameter::with_description( 106 | "message".to_string(), 107 | Some("Commit message".to_string()), 108 | ), 109 | ]; 110 | let cmd = create_test_command( 111 | "git commit -m @message && git push origin @branch", 112 | vec!["git".to_string()], 113 | params.clone(), 114 | ); 115 | 116 | // Add and retrieve 117 | let id = db.add_command(&cmd)?; 118 | let retrieved = db.get_command(id)?.unwrap(); 119 | 120 | assert_eq!(retrieved.parameters.len(), 2); 121 | assert_eq!(retrieved.parameters[0].name, "branch"); 122 | assert_eq!(retrieved.parameters[1].name, "message"); 123 | assert_eq!(retrieved.parameters[1].description, Some("Commit message".to_string())); 124 | 125 | Ok(()) 126 | } 127 | 128 | #[test] 129 | fn test_command_search() -> Result<()> { 130 | let temp_dir = tempdir()?; 131 | let db_path = temp_dir.path().join("test.db"); 132 | let mut db = Database::new(db_path.to_str().unwrap())?; 133 | 134 | // Add multiple commands 135 | let commands = vec![ 136 | ("git status", vec!["git".to_string()]), 137 | ("git push", vec!["git".to_string()]), 138 | ("ls -la", vec!["system".to_string()]), 139 | ("echo test", vec!["test".to_string()]), 140 | ]; 141 | 142 | for (cmd, tags) in commands { 143 | let command = create_test_command(cmd, tags, vec![]); 144 | db.add_command(&command)?; 145 | } 146 | 147 | // Test exact match 148 | let results = db.search_commands("git status", 10)?; 149 | assert_eq!(results.len(), 1); 150 | assert_eq!(results[0].command, "git status"); 151 | 152 | // Test partial match 153 | let results = db.search_commands("git", 10)?; 154 | assert_eq!(results.len(), 2); 155 | 156 | // Test with limit 157 | let results = db.search_commands("git", 1)?; 158 | assert_eq!(results.len(), 1); 159 | 160 | // Test case sensitivity 161 | let results = db.search_commands("GIT", 10)?; 162 | assert!(!results.is_empty()); 163 | 164 | // Test tag search 165 | let results = db.search_by_tag("git", 10)?; 166 | assert_eq!(results.len(), 2); 167 | 168 | Ok(()) 169 | } 170 | 171 | #[test] 172 | fn test_edge_cases() -> Result<()> { 173 | let temp_dir = tempdir()?; 174 | let db_path = temp_dir.path().join("test.db"); 175 | let mut db = Database::new(db_path.to_str().unwrap())?; 176 | 177 | // Test empty command 178 | let cmd = create_test_command("", vec![], vec![]); 179 | let id = db.add_command(&cmd)?; 180 | let retrieved = db.get_command(id)?.unwrap(); 181 | assert_eq!(retrieved.command, ""); 182 | 183 | // Test very long command 184 | let long_cmd = "x".repeat(1000); 185 | let cmd = create_test_command(&long_cmd, vec![], vec![]); 186 | let id = db.add_command(&cmd)?; 187 | let retrieved = db.get_command(id)?.unwrap(); 188 | assert_eq!(retrieved.command, long_cmd); 189 | 190 | // Test special characters in command 191 | let special_cmd = "echo 'test' && ls -la | grep \"something\" > output.txt"; 192 | let cmd = create_test_command(special_cmd, vec![], vec![]); 193 | let id = db.add_command(&cmd)?; 194 | let retrieved = db.get_command(id)?.unwrap(); 195 | assert_eq!(retrieved.command, special_cmd); 196 | 197 | // Test non-existent command 198 | assert!(db.get_command(9999)?.is_none()); 199 | 200 | // Test deleting non-existent command 201 | assert!(db.delete_command(9999).is_err()); 202 | 203 | // Test adding tags to non-existent command 204 | assert!(db.add_tags_to_command(9999, &vec!["test".to_string()]).is_err()); 205 | 206 | // Test removing non-existent tag 207 | let cmd = create_test_command("test", vec!["tag1".to_string()], vec![]); 208 | let id = db.add_command(&cmd)?; 209 | assert!(db.remove_tag_from_command(id, "nonexistent").is_ok()); 210 | 211 | Ok(()) 212 | } 213 | 214 | #[test] 215 | fn test_database_init() -> Result<()> { 216 | let temp_dir = tempdir()?; 217 | let db_path = temp_dir.path().join("test.db"); 218 | let db = Database::new(db_path.to_str().unwrap())?; 219 | 220 | // Verify tables exist by attempting to use them 221 | let conn = rusqlite::Connection::open(db_path)?; 222 | 223 | // Check commands table 224 | let count: i64 = conn.query_row( 225 | "SELECT COUNT(*) FROM commands", 226 | [], 227 | |row| row.get(0), 228 | )?; 229 | assert_eq!(count, 0); 230 | 231 | // Check tags table 232 | let count: i64 = conn.query_row( 233 | "SELECT COUNT(*) FROM tags", 234 | [], 235 | |row| row.get(0), 236 | )?; 237 | assert_eq!(count, 0); 238 | 239 | // Check command_tags table 240 | let count: i64 = conn.query_row( 241 | "SELECT COUNT(*) FROM command_tags", 242 | [], 243 | |row| row.get(0), 244 | )?; 245 | assert_eq!(count, 0); 246 | 247 | // Verify indexes exist 248 | let indexes: Vec = conn 249 | .prepare("SELECT name FROM sqlite_master WHERE type='index'")? 250 | .query_map([], |row| row.get(0))? 251 | .collect::, _>>()?; 252 | 253 | assert!(indexes.contains(&"idx_commands_command".to_string())); 254 | assert!(indexes.contains(&"idx_tags_name".to_string())); 255 | 256 | Ok(()) 257 | } 258 | 259 | #[test] 260 | fn test_list_commands_no_limit() -> Result<()> { 261 | let temp_dir = tempdir()?; 262 | let db_path = temp_dir.path().join("test.db"); 263 | let mut db = Database::new(db_path.to_str().unwrap())?; 264 | 265 | // Add more than the default limit of commands 266 | for i in 0..100 { 267 | let command = Command { 268 | id: None, 269 | command: format!("command {}", i), 270 | timestamp: Utc::now(), 271 | directory: "/test".to_string(), 272 | tags: vec![], 273 | parameters: Vec::new(), 274 | }; 275 | db.add_command(&command)?; 276 | } 277 | 278 | // Test listing with no limit (0) 279 | let commands = db.list_commands(0, false)?; 280 | assert_eq!(commands.len(), 100); 281 | 282 | // Test listing with no limit and ascending order 283 | let commands = db.list_commands(0, true)?; 284 | assert_eq!(commands.len(), 100); 285 | 286 | // Verify order in ascending mode 287 | for i in 1..commands.len() { 288 | assert!(commands[i].timestamp >= commands[i-1].timestamp); 289 | } 290 | 291 | // Verify order in descending mode (default) 292 | let commands = db.list_commands(0, false)?; 293 | for i in 1..commands.len() { 294 | assert!(commands[i].timestamp <= commands[i-1].timestamp); 295 | } 296 | 297 | Ok(()) 298 | } 299 | 300 | #[test] 301 | fn test_tag_cleanup_after_deletion() -> Result<()> { 302 | let temp_dir = tempdir()?; 303 | let db_path = temp_dir.path().join("test.db"); 304 | let mut db = Database::new(db_path.to_str().unwrap())?; 305 | 306 | // Add two commands with overlapping tags 307 | let cmd1 = Command { 308 | id: None, 309 | command: "command 1".to_string(), 310 | timestamp: Utc::now(), 311 | directory: "/test".to_string(), 312 | tags: vec!["tag1".to_string(), "tag2".to_string()], 313 | parameters: Vec::new(), 314 | }; 315 | let cmd2 = Command { 316 | id: None, 317 | command: "command 2".to_string(), 318 | timestamp: Utc::now(), 319 | directory: "/test".to_string(), 320 | tags: vec!["tag2".to_string(), "tag3".to_string()], 321 | parameters: Vec::new(), 322 | }; 323 | 324 | let id1 = db.add_command(&cmd1)?; 325 | let id2 = db.add_command(&cmd2)?; 326 | 327 | // Verify initial tag state 328 | let tags = db.list_tags()?; 329 | assert_eq!(tags.len(), 3); 330 | assert!(tags.iter().any(|(name, count)| name == "tag1" && *count == 1)); 331 | assert!(tags.iter().any(|(name, count)| name == "tag2" && *count == 2)); 332 | assert!(tags.iter().any(|(name, count)| name == "tag3" && *count == 1)); 333 | 334 | // Delete first command 335 | db.delete_command(id1)?; 336 | 337 | // Verify tag1 is removed, tag2 count decreased, tag3 unchanged 338 | let tags = db.list_tags()?; 339 | assert_eq!(tags.len(), 2); 340 | assert!(!tags.iter().any(|(name, _)| name == "tag1")); // tag1 should be removed 341 | assert!(tags.iter().any(|(name, count)| name == "tag2" && *count == 1)); 342 | assert!(tags.iter().any(|(name, count)| name == "tag3" && *count == 1)); 343 | 344 | // Delete second command 345 | db.delete_command(id2)?; 346 | 347 | // Verify all tags are removed 348 | let tags = db.list_tags()?; 349 | assert_eq!(tags.len(), 0); 350 | 351 | Ok(()) 352 | } 353 | 354 | #[test] 355 | fn test_transaction_rollback() -> Result<()> { 356 | let temp_dir = tempdir()?; 357 | let db_path = temp_dir.path().join("test.db"); 358 | let mut db = Database::new(db_path.to_str().unwrap())?; 359 | 360 | // Add a command with tags 361 | let cmd = Command { 362 | id: None, 363 | command: "test command".to_string(), 364 | timestamp: Utc::now(), 365 | directory: "/test".to_string(), 366 | tags: vec!["tag1".to_string(), "tag2".to_string()], 367 | parameters: Vec::new(), 368 | }; 369 | let id = db.add_command(&cmd)?; 370 | 371 | // Verify initial state 372 | let command = db.get_command(id)?.unwrap(); 373 | assert_eq!(command.command, "test command"); 374 | assert_eq!(command.tags.len(), 2); 375 | 376 | // Try to update with invalid command (id = None) 377 | let mut invalid_cmd = command.clone(); 378 | invalid_cmd.id = None; 379 | let result = db.update_command(&invalid_cmd); 380 | assert!(result.is_err()); 381 | assert!(result.unwrap_err().to_string().contains("without id")); 382 | 383 | // Verify state wasn't changed 384 | let command = db.get_command(id)?.unwrap(); 385 | assert_eq!(command.command, "test command"); 386 | assert_eq!(command.tags.len(), 2); 387 | 388 | // Try to delete non-existent command 389 | let result = db.delete_command(9999); 390 | assert!(result.is_err()); 391 | assert!(result.unwrap_err().to_string().contains("not found")); 392 | 393 | // Verify original command still exists 394 | let command = db.get_command(id)?.unwrap(); 395 | assert_eq!(command.command, "test command"); 396 | assert_eq!(command.tags.len(), 2); 397 | 398 | // Try to add tags to non-existent command 399 | let result = db.add_tags_to_command(9999, &vec!["tag3".to_string()]); 400 | assert!(result.is_err()); 401 | assert!(result.unwrap_err().to_string().contains("not found")); 402 | 403 | // Verify original command's tags weren't changed 404 | let command = db.get_command(id)?.unwrap(); 405 | assert_eq!(command.tags.len(), 2); 406 | assert!(command.tags.contains(&"tag1".to_string())); 407 | assert!(command.tags.contains(&"tag2".to_string())); 408 | assert!(!command.tags.contains(&"tag3".to_string())); 409 | 410 | Ok(()) 411 | } 412 | 413 | #[test] 414 | fn test_parameter_handling() -> Result<()> { 415 | let temp_dir = tempdir()?; 416 | let db_path = temp_dir.path().join("test.db"); 417 | let mut db = Database::new(db_path.to_str().unwrap())?; 418 | 419 | // Test command with valid parameters 420 | let mut cmd = Command { 421 | id: None, 422 | command: "test command".to_string(), 423 | timestamp: Utc::now(), 424 | directory: "/test".to_string(), 425 | tags: vec![], 426 | parameters: vec![ 427 | Parameter::new("param1".to_string()), 428 | Parameter::with_description("param2".to_string(), Some("description".to_string())), 429 | ], 430 | }; 431 | let id = db.add_command(&cmd)?; 432 | 433 | // Verify parameters were stored correctly 434 | let stored = db.get_command(id)?.unwrap(); 435 | assert_eq!(stored.parameters.len(), 2); 436 | assert_eq!(stored.parameters[0].name, "param1"); 437 | assert_eq!(stored.parameters[1].name, "param2"); 438 | assert_eq!(stored.parameters[1].description, Some("description".to_string())); 439 | 440 | // Test updating parameters 441 | cmd.id = Some(id); 442 | cmd.parameters = vec![Parameter::new("new_param".to_string())]; 443 | db.update_command(&cmd)?; 444 | 445 | // Verify parameters were updated 446 | let updated = db.get_command(id)?.unwrap(); 447 | assert_eq!(updated.parameters.len(), 1); 448 | assert_eq!(updated.parameters[0].name, "new_param"); 449 | 450 | Ok(()) 451 | } 452 | 453 | #[test] 454 | fn test_concurrent_access() -> Result<()> { 455 | use std::thread; 456 | use std::sync::Arc; 457 | use std::sync::Mutex; 458 | 459 | let temp_dir = tempdir()?; 460 | let db_path = temp_dir.path().join("test.db"); 461 | 462 | // Create initial database and enable WAL mode 463 | let conn = rusqlite::Connection::open(db_path.to_str().unwrap())?; 464 | conn.pragma_update(None, "journal_mode", "WAL")?; 465 | conn.pragma_update(None, "busy_timeout", 5000)?; 466 | drop(conn); 467 | 468 | let mut db = Database::new(db_path.to_str().unwrap())?; 469 | 470 | // Add initial command 471 | let cmd = Command { 472 | id: None, 473 | command: "initial command".to_string(), 474 | timestamp: Utc::now(), 475 | directory: "/test".to_string(), 476 | tags: vec!["tag1".to_string()], 477 | parameters: vec![], 478 | }; 479 | let id = db.add_command(&cmd)?; 480 | let db_path = Arc::new(db_path.to_str().unwrap().to_string()); 481 | 482 | // Create multiple threads that try to modify the same command 483 | let mut handles = vec![]; 484 | let counter = Arc::new(Mutex::new(0)); 485 | 486 | for i in 0..5 { 487 | let db_path = Arc::clone(&db_path); 488 | let counter = Arc::clone(&counter); 489 | 490 | let handle = thread::spawn(move || -> Result<()> { 491 | let mut db = Database::new(&db_path)?; 492 | 493 | // Try to update the command with retries 494 | let mut retries = 3; 495 | while retries > 0 { 496 | if let Ok(_) = db.update_command(&Command { 497 | id: Some(id), 498 | command: format!("updated by thread {}", i), 499 | timestamp: Utc::now(), 500 | directory: "/test".to_string(), 501 | tags: vec![], 502 | parameters: vec![], 503 | }) { 504 | break; 505 | } 506 | retries -= 1; 507 | thread::sleep(std::time::Duration::from_millis(100)); 508 | } 509 | 510 | // Try to add a new tag with retries 511 | let mut retries = 3; 512 | while retries > 0 { 513 | if let Ok(_) = db.add_tags_to_command(id, &vec![format!("tag{}", i)]) { 514 | break; 515 | } 516 | retries -= 1; 517 | thread::sleep(std::time::Duration::from_millis(100)); 518 | } 519 | 520 | *counter.lock().unwrap() += 1; 521 | Ok(()) 522 | }); 523 | 524 | handles.push(handle); 525 | } 526 | 527 | // Wait for all threads to complete 528 | for handle in handles { 529 | handle.join().unwrap()?; 530 | } 531 | 532 | // Verify final state 533 | let final_cmd = db.get_command(id)?.unwrap(); 534 | assert!(final_cmd.command.starts_with("updated by thread")); 535 | assert_eq!(*counter.lock().unwrap(), 5); 536 | 537 | // Verify all tags were added (initial tag + 5 new tags) 538 | let tags = db.list_tags()?; 539 | assert!(tags.len() >= 5, "Expected at least 5 tags, got {}", tags.len()); 540 | 541 | Ok(()) 542 | } 543 | -------------------------------------------------------------------------------- /tests/exec_test.rs: -------------------------------------------------------------------------------- 1 | use command_vault::exec::execute_command; 2 | use command_vault::db::models::{Command, Parameter}; 3 | use std::env; 4 | use std::fs; 5 | use std::path::PathBuf; 6 | use tempfile::TempDir; 7 | use chrono::Utc; 8 | use std::thread; 9 | use std::time::Duration; 10 | use std::io::Cursor; 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | use super::*; 15 | 16 | fn create_test_command(command: &str) -> Command { 17 | Command { 18 | id: None, 19 | command: command.to_string(), 20 | directory: String::new(), 21 | timestamp: Utc::now(), 22 | tags: vec![], 23 | parameters: vec![], 24 | } 25 | } 26 | 27 | fn setup_test_env() { 28 | env::set_var("COMMAND_VAULT_TEST", "1"); 29 | env::set_var("COMMAND_VAULT_TEST_INPUT", "test_value"); 30 | env::set_var("SHELL", "/bin/sh"); 31 | } 32 | 33 | fn cleanup_test_env() { 34 | env::remove_var("COMMAND_VAULT_TEST"); 35 | env::remove_var("COMMAND_VAULT_TEST_INPUT"); 36 | env::remove_var("SHELL"); 37 | } 38 | 39 | fn ensure_directory_exists(path: &PathBuf) -> std::io::Result<()> { 40 | if !path.exists() { 41 | fs::create_dir_all(path)?; 42 | } 43 | // Small delay to ensure filesystem operations complete 44 | thread::sleep(Duration::from_millis(100)); 45 | Ok(()) 46 | } 47 | 48 | fn get_safe_temp_dir() -> std::io::Result<(TempDir, PathBuf)> { 49 | let temp_dir = TempDir::new()?; 50 | let temp_path = temp_dir.path().to_path_buf(); 51 | ensure_directory_exists(&temp_path)?; 52 | 53 | // Verify the directory exists and is accessible 54 | if !temp_path.exists() || !temp_path.is_dir() { 55 | return Err(std::io::Error::new( 56 | std::io::ErrorKind::Other, 57 | "Failed to create temporary directory" 58 | )); 59 | } 60 | 61 | Ok((temp_dir, temp_path)) 62 | } 63 | 64 | fn setup_test_dir(temp_path: &PathBuf) -> std::io::Result<()> { 65 | ensure_directory_exists(temp_path)?; 66 | 67 | // Create a test file 68 | let test_file = temp_path.join("test.txt"); 69 | fs::write(&test_file, "test content")?; 70 | 71 | // Small delay to ensure file is written 72 | thread::sleep(Duration::from_millis(100)); 73 | 74 | // Verify the file was created 75 | if !test_file.exists() { 76 | return Err(std::io::Error::new( 77 | std::io::ErrorKind::Other, 78 | "Failed to create test file" 79 | )); 80 | } 81 | 82 | Ok(()) 83 | } 84 | 85 | #[test] 86 | fn test_basic_command_execution() -> std::io::Result<()> { 87 | let (temp_dir, temp_path) = get_safe_temp_dir()?; 88 | let dir_path = temp_path.canonicalize()?.to_string_lossy().to_string(); 89 | 90 | let mut command = create_test_command("echo 'hello world'"); 91 | command.directory = dir_path; 92 | 93 | setup_test_env(); 94 | let result = execute_command(&command); 95 | cleanup_test_env(); 96 | 97 | assert!(result.is_ok(), "Command failed: {:?}", result.err()); 98 | drop(temp_dir); 99 | Ok(()) 100 | } 101 | 102 | #[test] 103 | fn test_command_with_working_directory() -> std::io::Result<()> { 104 | let (temp_dir, temp_path) = get_safe_temp_dir()?; 105 | let dir_path = temp_path.canonicalize()?.to_string_lossy().to_string(); 106 | 107 | setup_test_dir(&temp_path)?; 108 | 109 | let mut command = create_test_command("cat test.txt"); 110 | command.directory = dir_path; 111 | 112 | setup_test_env(); 113 | let result = execute_command(&command); 114 | cleanup_test_env(); 115 | 116 | assert!(result.is_ok(), "Command failed: {:?}", result.err()); 117 | drop(temp_dir); 118 | Ok(()) 119 | } 120 | 121 | #[test] 122 | fn test_command_with_parameters() -> std::io::Result<()> { 123 | // Create and verify temp directory 124 | let (temp_dir, temp_path) = get_safe_temp_dir()?; 125 | let dir_path = temp_path.canonicalize()?.to_string_lossy().to_string(); 126 | 127 | // Ensure the directory exists and is accessible 128 | ensure_directory_exists(&temp_path)?; 129 | 130 | // Set up test environment with a known test value 131 | setup_test_env(); 132 | env::set_var("COMMAND_VAULT_TEST_INPUT", "test_message"); 133 | 134 | // Create a simple command that just echoes the parameter 135 | let mut command = create_test_command("echo @message"); 136 | command.directory = dir_path; 137 | command.parameters = vec![ 138 | Parameter { 139 | name: "message".to_string(), 140 | description: Some("Test message".to_string()), 141 | }, 142 | ]; 143 | 144 | // Execute the command and verify it succeeds 145 | let result = execute_command(&command); 146 | assert!(result.is_ok(), "Command failed: {:?}", result.err()); 147 | 148 | // Clean up 149 | cleanup_test_env(); 150 | drop(temp_dir); 151 | 152 | Ok(()) 153 | } 154 | 155 | #[test] 156 | fn test_command_with_quoted_parameters() -> std::io::Result<()> { 157 | let (temp_dir, temp_path) = get_safe_temp_dir()?; 158 | let dir_path = temp_path.canonicalize()?.to_string_lossy().to_string(); 159 | 160 | let mut command = create_test_command("echo '@message'"); 161 | command.directory = dir_path; 162 | command.parameters = vec![ 163 | Parameter::with_description( 164 | "message".to_string(), 165 | Some("Test 'quoted' message".to_string()) 166 | ), 167 | ]; 168 | 169 | setup_test_env(); 170 | let result = execute_command(&command); 171 | cleanup_test_env(); 172 | 173 | assert!(result.is_ok(), "Command failed: {:?}", result.err()); 174 | drop(temp_dir); 175 | Ok(()) 176 | } 177 | 178 | #[test] 179 | fn test_command_with_multiple_env_vars() -> std::io::Result<()> { 180 | let (temp_dir, temp_path) = get_safe_temp_dir()?; 181 | let dir_path = temp_path.canonicalize()?.to_string_lossy().to_string(); 182 | 183 | let mut command = create_test_command("echo \"$TEST_VAR1 $TEST_VAR2\""); 184 | command.directory = dir_path; 185 | 186 | setup_test_env(); 187 | env::set_var("TEST_VAR1", "value1"); 188 | env::set_var("TEST_VAR2", "value2"); 189 | 190 | let result = execute_command(&command); 191 | 192 | env::remove_var("TEST_VAR1"); 193 | env::remove_var("TEST_VAR2"); 194 | cleanup_test_env(); 195 | 196 | assert!(result.is_ok(), "Command failed: {:?}", result.err()); 197 | drop(temp_dir); 198 | Ok(()) 199 | } 200 | 201 | #[test] 202 | fn test_command_with_directory_traversal() -> std::io::Result<()> { 203 | let (temp_dir, temp_path) = get_safe_temp_dir()?; 204 | let dir_path = temp_path.canonicalize()?.to_string_lossy().to_string(); 205 | 206 | // Create a test directory structure 207 | let test_dir = temp_path.join("test_dir"); 208 | ensure_directory_exists(&test_dir)?; 209 | 210 | // Create a test file in the test directory 211 | let test_file = test_dir.join("test.txt"); 212 | fs::write(&test_file, "test content")?; 213 | 214 | // Small delay to ensure file is written 215 | thread::sleep(Duration::from_millis(100)); 216 | 217 | // Attempt to traverse outside the test directory 218 | let mut command = create_test_command("cat ../test.txt"); 219 | command.directory = test_dir.canonicalize()?.to_string_lossy().to_string(); 220 | 221 | setup_test_env(); 222 | let result = execute_command(&command); 223 | cleanup_test_env(); 224 | 225 | assert!(result.is_err(), "Directory traversal should be prevented"); 226 | drop(temp_dir); 227 | Ok(()) 228 | } 229 | 230 | #[test] 231 | fn test_command_with_special_shell_chars() -> std::io::Result<()> { 232 | let (temp_dir, temp_path) = get_safe_temp_dir()?; 233 | let dir_path = temp_path.canonicalize()?.to_string_lossy().to_string(); 234 | 235 | // Create a test file first 236 | setup_test_dir(&temp_path)?; 237 | 238 | let mut command = create_test_command("echo test > output.txt && cat output.txt"); 239 | command.directory = dir_path; 240 | 241 | setup_test_env(); 242 | let result = execute_command(&command); 243 | cleanup_test_env(); 244 | 245 | assert!(result.is_ok(), "Command failed: {:?}", result.err()); 246 | drop(temp_dir); 247 | Ok(()) 248 | } 249 | 250 | #[test] 251 | fn test_parameter_handling() -> std::io::Result<()> { 252 | let (temp_dir, temp_path) = get_safe_temp_dir()?; 253 | let dir_path = temp_path.canonicalize()?.to_string_lossy().to_string(); 254 | 255 | let mut command = create_test_command("echo @p"); 256 | command.directory = dir_path.clone(); 257 | command.parameters = vec![ 258 | Parameter { 259 | name: "p".to_string(), 260 | description: Some("Test parameter".to_string()), 261 | }, 262 | ]; 263 | 264 | // Set up test environment 265 | setup_test_env(); 266 | env::set_var("COMMAND_VAULT_TEST_INPUT", "some-value"); 267 | 268 | // Execute command 269 | let result = execute_command(&command); 270 | assert!(result.is_ok(), "Command failed: {:?}", result.err()); 271 | 272 | // Clean up 273 | cleanup_test_env(); 274 | drop(temp_dir); 275 | Ok(()) 276 | } 277 | 278 | #[test] 279 | fn test_command_output_format() -> std::io::Result<()> { 280 | let (temp_dir, temp_path) = get_safe_temp_dir()?; 281 | let dir_path = temp_path.canonicalize()?.to_string_lossy().to_string(); 282 | 283 | let mut command = create_test_command("echo @p > output.txt"); 284 | command.directory = dir_path.clone(); 285 | command.parameters = vec![ 286 | Parameter { 287 | name: "p".to_string(), 288 | description: Some("Test parameter".to_string()), 289 | }, 290 | ]; 291 | 292 | // Set up test environment 293 | setup_test_env(); 294 | let test_value = "test_value"; 295 | env::set_var("COMMAND_VAULT_TEST_INPUT", test_value); 296 | 297 | // Execute command 298 | let result = execute_command(&command); 299 | assert!(result.is_ok(), "Command failed: {:?}", result.err()); 300 | 301 | // Verify the command executed correctly by checking the output file 302 | let output_path = PathBuf::from(&dir_path).join("output.txt"); 303 | let output_content = fs::read_to_string(output_path)?; 304 | assert_eq!(output_content.trim(), test_value); 305 | 306 | // Clean up 307 | cleanup_test_env(); 308 | drop(temp_dir); 309 | Ok(()) 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /tests/params_test.rs: -------------------------------------------------------------------------------- 1 | use command_vault::{ 2 | db::models::Parameter, 3 | utils::params::{parse_parameters, substitute_parameters}, 4 | }; 5 | 6 | #[test] 7 | fn test_parse_parameters_basic() { 8 | let command = "echo @name"; 9 | let params = parse_parameters(command); 10 | 11 | assert_eq!(params.len(), 1); 12 | assert_eq!(params[0].name, "name"); 13 | assert_eq!(params[0].description, None); 14 | } 15 | 16 | #[test] 17 | fn test_parse_parameters_with_description() { 18 | let command = "echo @name:new-name"; 19 | let params = parse_parameters(command); 20 | 21 | assert_eq!(params.len(), 1); 22 | assert_eq!(params[0].name, "name"); 23 | assert_eq!(params[0].description, Some("new-name".to_string())); 24 | } 25 | 26 | #[test] 27 | fn test_parse_multiple_parameters() { 28 | let command = "echo @name:new-name @age:30 @city"; 29 | let params = parse_parameters(command); 30 | 31 | assert_eq!(params.len(), 3); 32 | 33 | assert_eq!(params[0].name, "name"); 34 | assert_eq!(params[0].description, Some("new-name".to_string())); 35 | 36 | assert_eq!(params[1].name, "age"); 37 | assert_eq!(params[1].description, Some("30".to_string())); 38 | 39 | assert_eq!(params[2].name, "city"); 40 | assert_eq!(params[2].description, None); 41 | } 42 | 43 | #[test] 44 | fn test_parse_parameters_with_underscores() { 45 | let command = "echo @user_name:new-user"; 46 | let params = parse_parameters(command); 47 | 48 | assert_eq!(params.len(), 1); 49 | assert_eq!(params[0].name, "user_name"); 50 | assert_eq!(params[0].description, Some("new-user".to_string())); 51 | } 52 | 53 | 54 | 55 | #[test] 56 | fn test_parse_parameters_empty_command() { 57 | let command = "echo"; 58 | let params = parse_parameters(command); 59 | assert_eq!(params.len(), 0); 60 | } 61 | 62 | #[test] 63 | fn test_parse_parameters_invalid_names() { 64 | let command = "echo @123 @!invalid @valid_name"; 65 | let params = parse_parameters(command); 66 | assert_eq!(params.len(), 1); 67 | assert_eq!(params[0].name, "valid_name"); 68 | } 69 | 70 | #[test] 71 | fn test_substitute_parameters_with_special_chars() -> Result<(), Box> { 72 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 73 | 74 | let command = "grep @pattern /path/to/dir"; 75 | let parameters = vec![Parameter { 76 | name: "pattern".to_string(), 77 | description: None, 78 | }]; 79 | 80 | let result = substitute_parameters(command, ¶meters, Some("test-pattern"))?; 81 | assert_eq!(result, "grep 'test-pattern' /path/to/dir"); 82 | 83 | std::env::remove_var("COMMAND_VAULT_TEST"); 84 | Ok(()) 85 | } 86 | 87 | #[test] 88 | fn test_substitute_parameters_empty_value() -> Result<(), Box> { 89 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 90 | 91 | let command = "echo @message"; 92 | let parameters = vec![Parameter { 93 | name: "message".to_string(), 94 | description: None, 95 | }]; 96 | 97 | let result = substitute_parameters(command, ¶meters, Some(""))?; 98 | assert_eq!(result, "echo ''"); 99 | 100 | std::env::remove_var("COMMAND_VAULT_TEST"); 101 | Ok(()) 102 | } 103 | 104 | #[test] 105 | fn test_substitute_parameters_with_spaces() -> Result<(), Box> { 106 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 107 | 108 | let command = "echo @message"; 109 | let parameters = vec![ 110 | Parameter::with_description("message".to_string(), Some("A test message".to_string())), 111 | ]; 112 | 113 | let result = substitute_parameters(command, ¶meters, Some("hello world"))?; 114 | assert_eq!(result, "echo 'hello world'"); 115 | 116 | std::env::remove_var("COMMAND_VAULT_TEST"); 117 | Ok(()) 118 | } 119 | 120 | #[test] 121 | fn test_parse_parameters_from_params_rs() { 122 | let command = "docker run -p @port:8080 -v @volume @image"; 123 | let params = parse_parameters(command); 124 | 125 | assert_eq!(params.len(), 3); 126 | 127 | assert_eq!(params[0].name, "port"); 128 | assert_eq!(params[0].description, Some("8080".to_string())); 129 | 130 | assert_eq!(params[1].name, "volume"); 131 | assert_eq!(params[1].description, None); 132 | 133 | assert_eq!(params[2].name, "image"); 134 | assert_eq!(params[2].description, None); 135 | } 136 | 137 | #[test] 138 | fn test_substitute_parameters_basic() -> Result<(), Box> { 139 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 140 | 141 | let command = "echo @message"; 142 | let parameters = vec![Parameter::with_description("message".to_string(), None)]; 143 | 144 | let result = substitute_parameters(command, ¶meters, Some("hello"))?; 145 | assert_eq!(result, "echo hello"); 146 | 147 | std::env::remove_var("COMMAND_VAULT_TEST"); 148 | Ok(()) 149 | } 150 | 151 | #[test] 152 | fn test_substitute_parameters_with_defaults() -> Result<(), Box> { 153 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 154 | 155 | let command = "echo @message:default value"; 156 | let parameters = vec![Parameter { 157 | name: "message".to_string(), 158 | description: Some("default value".to_string()), 159 | }]; 160 | 161 | let result = substitute_parameters(command, ¶meters, Some(""))?; 162 | assert_eq!(result, "echo 'default value'"); 163 | 164 | std::env::remove_var("COMMAND_VAULT_TEST"); 165 | Ok(()) 166 | } 167 | 168 | #[test] 169 | fn test_substitute_parameters_multiple() -> Result<(), Box> { 170 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 171 | 172 | let command = "git commit -m @message --author @author"; 173 | let parameters = vec![ 174 | Parameter { 175 | name: "message".to_string(), 176 | description: None, 177 | }, 178 | Parameter { 179 | name: "author".to_string(), 180 | description: None, 181 | }, 182 | ]; 183 | 184 | let result = substitute_parameters(command, ¶meters, Some("test commit\nJohn Doe"))?; 185 | assert_eq!(result, "git commit -m 'test commit' --author 'John Doe'"); 186 | 187 | std::env::remove_var("COMMAND_VAULT_TEST"); 188 | Ok(()) 189 | } 190 | 191 | #[test] 192 | fn test_substitute_parameters_with_quotes() -> Result<(), Box> { 193 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 194 | 195 | let command = "echo @message"; 196 | let parameters = vec![Parameter { 197 | name: "message".to_string(), 198 | description: None, 199 | }]; 200 | 201 | let result = substitute_parameters(command, ¶meters, Some("hello * world"))?; 202 | assert_eq!(result, "echo 'hello * world'"); 203 | 204 | std::env::remove_var("COMMAND_VAULT_TEST"); 205 | Ok(()) 206 | } 207 | 208 | #[test] 209 | fn test_substitute_parameters_empty_command() -> Result<(), Box> { 210 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 211 | 212 | let command = ""; 213 | let parameters = vec![]; 214 | 215 | let result = substitute_parameters(command, ¶meters, None)?; 216 | assert_eq!(result, ""); 217 | 218 | std::env::remove_var("COMMAND_VAULT_TEST"); 219 | Ok(()) 220 | } 221 | 222 | #[test] 223 | fn test_substitute_parameters_no_parameters() -> Result<(), Box> { 224 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 225 | 226 | let command = "echo hello"; 227 | let parameters = vec![]; 228 | 229 | let result = substitute_parameters(command, ¶meters, None)?; 230 | assert_eq!(result, "echo hello"); 231 | 232 | std::env::remove_var("COMMAND_VAULT_TEST"); 233 | Ok(()) 234 | } 235 | 236 | #[test] 237 | fn test_substitute_parameters_with_git_commands() -> Result<(), Box> { 238 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 239 | 240 | let command = "git commit -m @message"; 241 | let parameters = vec![Parameter { 242 | name: "message".to_string(), 243 | description: None, 244 | }]; 245 | 246 | let result = substitute_parameters(command, ¶meters, Some("test commit"))?; 247 | assert_eq!(result, "git commit -m 'test commit'"); 248 | 249 | std::env::remove_var("COMMAND_VAULT_TEST"); 250 | Ok(()) 251 | } 252 | 253 | #[test] 254 | fn test_substitute_parameters_with_grep() -> Result<(), Box> { 255 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 256 | 257 | let command = "grep @pattern"; 258 | let parameters = vec![Parameter { 259 | name: "pattern".to_string(), 260 | description: None, 261 | }]; 262 | 263 | let result = substitute_parameters(command, ¶meters, Some("hello * world"))?; 264 | assert_eq!(result, "grep 'hello * world'"); 265 | 266 | std::env::remove_var("COMMAND_VAULT_TEST"); 267 | Ok(()) 268 | } 269 | 270 | #[test] 271 | fn test_substitute_parameters_with_multiple_occurrences() -> Result<(), Box> { 272 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 273 | let command = "echo @message && echo @message"; 274 | let parameters = vec![Parameter { 275 | name: "message".to_string(), 276 | description: None, 277 | }]; 278 | 279 | let result = substitute_parameters(command, ¶meters, Some("test")).unwrap(); 280 | assert_eq!(result, "echo test && echo test"); 281 | std::env::remove_var("COMMAND_VAULT_TEST"); 282 | Ok(()) 283 | } 284 | 285 | #[test] 286 | fn test_substitute_parameters_with_descriptions() -> Result<(), Box> { 287 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 288 | let command = "echo @message:A test message"; 289 | let parameters = vec![Parameter { 290 | name: "message".to_string(), 291 | description: Some("A test message".to_string()), 292 | }]; 293 | 294 | let result = substitute_parameters(command, ¶meters, Some("test")).unwrap(); 295 | assert_eq!(result, "echo test"); 296 | std::env::remove_var("COMMAND_VAULT_TEST"); 297 | Ok(()) 298 | } 299 | 300 | #[test] 301 | fn test_substitute_parameters_empty_value_removed_duplicate() -> Result<(), Box> { 302 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 303 | 304 | let command = "echo @message"; 305 | let parameters = vec![Parameter { 306 | name: "message".to_string(), 307 | description: None, 308 | }]; 309 | 310 | let result = substitute_parameters(command, ¶meters, Some(""))?; 311 | assert_eq!(result, "echo ''"); 312 | 313 | std::env::remove_var("COMMAND_VAULT_TEST"); 314 | Ok(()) 315 | } 316 | 317 | #[test] 318 | fn test_parse_parameters_with_adjacent_parameters() { 319 | let command = "echo @first@second"; 320 | let params = parse_parameters(command); 321 | assert_eq!(params.len(), 2); 322 | assert_eq!(params[0].name, "first"); 323 | assert_eq!(params[1].name, "second"); 324 | } 325 | 326 | #[test] 327 | fn test_parse_parameters_with_special_chars_in_description() { 328 | let command = "echo @name:special!"; 329 | let params = parse_parameters(command); 330 | assert_eq!(params.len(), 1); 331 | assert_eq!(params[0].name, "name"); 332 | assert_eq!(params[0].description, Some("special!".to_string())); 333 | } 334 | 335 | #[test] 336 | fn test_substitute_parameters_with_semicolon() -> Result<(), Box> { 337 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 338 | 339 | let command = "echo @cmd"; 340 | let parameters = vec![Parameter { 341 | name: "cmd".to_string(), 342 | description: None, 343 | }]; 344 | 345 | let result = substitute_parameters(command, ¶meters, Some("echo hello; ls"))?; 346 | assert_eq!(result, "echo 'echo hello; ls'"); 347 | 348 | std::env::remove_var("COMMAND_VAULT_TEST"); 349 | Ok(()) 350 | } 351 | 352 | #[test] 353 | fn test_substitute_parameters_with_pipe() -> Result<(), Box> { 354 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 355 | 356 | let command = "echo @cmd"; 357 | let parameters = vec![Parameter { 358 | name: "cmd".to_string(), 359 | description: None, 360 | }]; 361 | 362 | let result = substitute_parameters(command, ¶meters, Some("ls | grep test"))?; 363 | assert_eq!(result, "echo 'ls | grep test'"); 364 | 365 | std::env::remove_var("COMMAND_VAULT_TEST"); 366 | Ok(()) 367 | } 368 | 369 | #[test] 370 | fn test_substitute_parameters_with_redirection() -> Result<(), Box> { 371 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 372 | 373 | let command = "echo @cmd"; 374 | let parameters = vec![Parameter { 375 | name: "cmd".to_string(), 376 | description: None, 377 | }]; 378 | 379 | let result = substitute_parameters(command, ¶meters, Some("echo test > file.txt"))?; 380 | assert_eq!(result, "echo 'echo test > file.txt'"); 381 | 382 | std::env::remove_var("COMMAND_VAULT_TEST"); 383 | Ok(()) 384 | } 385 | 386 | #[test] 387 | fn test_substitute_parameters_with_existing_quotes() -> Result<(), Box> { 388 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 389 | 390 | let command = "echo @message"; 391 | let parameters = vec![Parameter { 392 | name: "message".to_string(), 393 | description: None, 394 | }]; 395 | 396 | let result = substitute_parameters(command, ¶meters, Some("'already quoted'"))?; 397 | assert_eq!(result, "echo 'already quoted'"); 398 | 399 | std::env::remove_var("COMMAND_VAULT_TEST"); 400 | Ok(()) 401 | } 402 | 403 | #[test] 404 | fn test_substitute_parameters_with_escaped_quotes() -> Result<(), Box> { 405 | std::env::set_var("COMMAND_VAULT_TEST", "1"); 406 | 407 | let command = "echo @message"; 408 | let parameters = vec![Parameter { 409 | name: "message".to_string(), 410 | description: None, 411 | }]; 412 | 413 | let result = substitute_parameters(command, ¶meters, Some("It's a test"))?; 414 | assert_eq!(result, "echo 'It'\\''s a test'"); 415 | 416 | std::env::remove_var("COMMAND_VAULT_TEST"); 417 | Ok(()) 418 | } 419 | 420 | #[test] 421 | fn test_parameter_new() { 422 | let param = Parameter::new("test".to_string()); 423 | assert_eq!(param.name, "test"); 424 | assert_eq!(param.description, None); 425 | } 426 | 427 | #[test] 428 | fn test_parameter_with_description() { 429 | let param = Parameter::with_description("test".to_string(), Some("A test parameter".to_string())); 430 | assert_eq!(param.name, "test"); 431 | assert_eq!(param.description, Some("A test parameter".to_string())); 432 | } 433 | 434 | #[test] 435 | fn test_parse_parameters_with_trailing_whitespace() { 436 | let command = "echo @name:test"; 437 | let params = parse_parameters(command); 438 | assert_eq!(params.len(), 1); 439 | assert_eq!(params[0].name, "name"); 440 | assert_eq!(params[0].description, Some("test".to_string())); 441 | } 442 | 443 | #[test] 444 | fn test_parse_parameters_with_multiple_colons() { 445 | let command = "echo @name:test:value"; 446 | let params = parse_parameters(command); 447 | assert_eq!(params.len(), 1); 448 | assert_eq!(params[0].name, "name"); 449 | assert_eq!(params[0].description, Some("test:value".to_string())); 450 | } 451 | 452 | #[test] 453 | fn test_parse_parameters_with_numbers_in_description() { 454 | let command = "echo @port:8080 @host:localhost:8080"; 455 | let params = parse_parameters(command); 456 | assert_eq!(params.len(), 2); 457 | assert_eq!(params[0].name, "port"); 458 | assert_eq!(params[0].description, Some("8080".to_string())); 459 | assert_eq!(params[1].name, "host"); 460 | assert_eq!(params[1].description, Some("localhost:8080".to_string())); 461 | } 462 | 463 | #[test] 464 | fn test_parse_parameters_with_dash_in_description() { 465 | let command = "git checkout @branch:feature-123"; 466 | let params = parse_parameters(command); 467 | assert_eq!(params.len(), 1); 468 | assert_eq!(params[0].name, "branch"); 469 | assert_eq!(params[0].description, Some("feature-123".to_string())); 470 | } -------------------------------------------------------------------------------- /tests/shell_test.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | use anyhow::Result; 4 | use serial_test::serial; 5 | use command_vault::shell::hooks::{ 6 | detect_current_shell, get_shell_integration_dir, get_shell_integration_script, 7 | get_zsh_integration_path, get_bash_integration_path, get_fish_integration_path, init_shell 8 | }; 9 | 10 | #[test] 11 | #[serial] 12 | fn test_detect_current_shell() { 13 | // Save original environment 14 | let original_shell = env::var("SHELL").ok(); 15 | let original_fish_version = env::var("FISH_VERSION").ok(); 16 | 17 | // Clean environment for testing 18 | env::remove_var("FISH_VERSION"); 19 | env::remove_var("SHELL"); 20 | 21 | // Test default case (no environment variables) 22 | assert_eq!( 23 | detect_current_shell(), 24 | "bash", 25 | "Should default to bash when no shell is set" 26 | ); 27 | 28 | // Test with FISH_VERSION (highest priority) 29 | env::remove_var("SHELL"); // Ensure SHELL is not set 30 | env::set_var("FISH_VERSION", "3.1.2"); 31 | assert_eq!( 32 | detect_current_shell(), 33 | "fish", 34 | "Should detect Fish via FISH_VERSION" 35 | ); 36 | 37 | // Test with SHELL environment variable 38 | env::remove_var("FISH_VERSION"); 39 | env::set_var("SHELL", "/bin/zsh"); 40 | assert_eq!( 41 | detect_current_shell(), 42 | "zsh", 43 | "Should detect Zsh via SHELL" 44 | ); 45 | 46 | // Test with SHELL environment variable (bash) 47 | env::set_var("SHELL", "/bin/bash"); 48 | assert_eq!( 49 | detect_current_shell(), 50 | "bash", 51 | "Should detect Bash via SHELL" 52 | ); 53 | 54 | // Test that FISH_VERSION takes precedence over SHELL 55 | env::set_var("SHELL", "/bin/zsh"); 56 | env::set_var("FISH_VERSION", "3.1.2"); 57 | assert_eq!( 58 | detect_current_shell(), 59 | "fish", 60 | "FISH_VERSION should take precedence over SHELL" 61 | ); 62 | 63 | // Restore original environment 64 | if let Some(shell) = original_shell { 65 | env::set_var("SHELL", shell); 66 | } else { 67 | env::remove_var("SHELL"); 68 | } 69 | if let Some(fish_version) = original_fish_version { 70 | env::set_var("FISH_VERSION", fish_version); 71 | } else { 72 | env::remove_var("FISH_VERSION"); 73 | } 74 | } 75 | 76 | #[test] 77 | fn test_get_shell_integration_dir() { 78 | let dir = get_shell_integration_dir(); 79 | assert!(dir.is_dir(), "Shell integration directory should exist"); 80 | assert!(dir.ends_with("shell"), "Directory should end with 'shell'"); 81 | 82 | // Check if shell scripts exist 83 | let zsh_script = dir.join("zsh-integration.zsh"); 84 | let bash_script = dir.join("bash-integration.sh"); 85 | let fish_script = dir.join("fish-integration.fish"); 86 | 87 | assert!(zsh_script.exists(), "ZSH integration script should exist"); 88 | assert!(bash_script.exists(), "Bash integration script should exist"); 89 | assert!(fish_script.exists(), "Fish integration script should exist"); 90 | } 91 | 92 | #[test] 93 | fn test_get_shell_specific_paths() { 94 | let zsh_path = get_zsh_integration_path(); 95 | let bash_path = get_bash_integration_path(); 96 | let fish_path = get_fish_integration_path(); 97 | 98 | assert!(zsh_path.ends_with("zsh-integration.zsh"), "ZSH path should end with correct filename"); 99 | assert!(bash_path.ends_with("bash-integration.sh"), "Bash path should end with correct filename"); 100 | assert!(fish_path.ends_with("fish-integration.fish"), "Fish path should end with correct filename"); 101 | assert!(zsh_path.exists(), "ZSH integration script should exist"); 102 | assert!(bash_path.exists(), "Bash integration script should exist"); 103 | assert!(fish_path.exists(), "Fish integration script should exist"); 104 | } 105 | 106 | #[test] 107 | fn test_get_shell_integration_script() -> Result<()> { 108 | // Test valid shells 109 | let zsh_script = get_shell_integration_script("zsh")?; 110 | let bash_script = get_shell_integration_script("bash")?; 111 | let fish_script = get_shell_integration_script("fish")?; 112 | 113 | assert!(zsh_script.ends_with("zsh-integration.zsh")); 114 | assert!(bash_script.ends_with("bash-integration.sh")); 115 | assert!(fish_script.ends_with("fish-integration.fish")); 116 | 117 | // Test case insensitivity 118 | let upper_zsh = get_shell_integration_script("ZSH")?; 119 | assert_eq!(upper_zsh, zsh_script); 120 | 121 | // Test error cases 122 | let unknown_result = get_shell_integration_script("unknown"); 123 | assert!(unknown_result.is_err()); 124 | assert!(unknown_result.unwrap_err().to_string().contains("Unsupported shell")); 125 | 126 | Ok(()) 127 | } 128 | 129 | #[test] 130 | #[serial] 131 | fn test_init_shell() { 132 | // Save original environment 133 | let original_shell = env::var("SHELL").ok(); 134 | let original_fish_version = env::var("FISH_VERSION").ok(); 135 | 136 | // Clean environment for testing 137 | env::remove_var("FISH_VERSION"); 138 | env::remove_var("SHELL"); 139 | 140 | // Test default shell (bash) 141 | env::set_var("SHELL", "/bin/bash"); 142 | let path = init_shell(None).unwrap(); 143 | assert!(path.ends_with("bash-integration.sh"), "Path should end with bash-integration.sh"); 144 | 145 | // Test with shell override 146 | let path = init_shell(Some("fish".to_string())).unwrap(); 147 | assert!(path.ends_with("fish-integration.fish"), "Path should end with fish-integration.fish"); 148 | 149 | // Clean up test environment 150 | env::remove_var("FISH_VERSION"); 151 | env::remove_var("SHELL"); 152 | 153 | // Restore original environment 154 | if let Some(shell) = original_shell { 155 | env::set_var("SHELL", shell); 156 | } 157 | if let Some(version) = original_fish_version { 158 | env::set_var("FISH_VERSION", version); 159 | } 160 | } 161 | 162 | #[test] 163 | #[serial] 164 | fn test_detect_current_shell_fish_env() { 165 | // Save original environment 166 | let original_shell = env::var("SHELL").ok(); 167 | let original_fish_version = env::var("FISH_VERSION").ok(); 168 | 169 | // Clean environment for testing 170 | env::remove_var("FISH_VERSION"); 171 | env::remove_var("SHELL"); 172 | 173 | // Test FISH_VERSION detection 174 | env::set_var("FISH_VERSION", "3.1.2"); 175 | assert_eq!( 176 | detect_current_shell(), 177 | "fish", 178 | "Should detect Fish via FISH_VERSION" 179 | ); 180 | 181 | // Test FISH_VERSION takes precedence over SHELL 182 | env::set_var("SHELL", "/bin/zsh"); 183 | env::set_var("FISH_VERSION", "3.1.2"); 184 | assert_eq!( 185 | detect_current_shell(), 186 | "fish", 187 | "FISH_VERSION should take precedence over SHELL" 188 | ); 189 | 190 | // Clean up test environment 191 | env::remove_var("FISH_VERSION"); 192 | env::remove_var("SHELL"); 193 | 194 | // Restore original environment 195 | if let Some(shell) = original_shell { 196 | env::set_var("SHELL", shell); 197 | } 198 | if let Some(version) = original_fish_version { 199 | env::set_var("FISH_VERSION", version); 200 | } 201 | } 202 | 203 | #[test] 204 | #[serial] 205 | fn test_init_shell_explicit_fish() -> Result<()> { 206 | // Test with explicit fish shell override 207 | let path = init_shell(Some("fish".to_string()))?; 208 | assert!(path.ends_with("fish-integration.fish")); 209 | Ok(()) 210 | } 211 | 212 | #[test] 213 | #[serial] 214 | fn test_detect_current_shell_fish_variants() { 215 | // Save original environment 216 | let original_shell = env::var("SHELL").ok(); 217 | let original_fish_version = env::var("FISH_VERSION").ok(); 218 | 219 | // Clean environment for testing 220 | env::remove_var("FISH_VERSION"); 221 | env::remove_var("SHELL"); 222 | 223 | // Test various fish shell paths 224 | let fish_paths = vec![ 225 | "/usr/local/bin/fish", 226 | "/opt/homebrew/bin/fish", 227 | "fish", 228 | "/usr/bin/fish", 229 | "/bin/fish", 230 | ]; 231 | 232 | for path in fish_paths { 233 | env::remove_var("FISH_VERSION"); // Ensure FISH_VERSION doesn't interfere 234 | env::remove_var("SHELL"); // Clean SHELL before setting 235 | env::set_var("SHELL", path); 236 | assert_eq!( 237 | detect_current_shell(), 238 | "fish", 239 | "Failed to detect fish shell at path: {}", 240 | path 241 | ); 242 | } 243 | 244 | // Clean up test environment 245 | env::remove_var("FISH_VERSION"); 246 | env::remove_var("SHELL"); 247 | 248 | // Restore original environment 249 | if let Some(shell) = original_shell { 250 | env::set_var("SHELL", shell); 251 | } 252 | if let Some(version) = original_fish_version { 253 | env::set_var("FISH_VERSION", version); 254 | } 255 | } 256 | 257 | #[test] 258 | fn test_shell_integration_paths() -> Result<()> { 259 | // Test shell integration directory 260 | let integration_dir = get_shell_integration_dir(); 261 | assert!(integration_dir.ends_with("shell")); 262 | 263 | // Test zsh integration path 264 | let zsh_path = get_zsh_integration_path(); 265 | assert!(zsh_path.ends_with("zsh-integration.zsh")); 266 | 267 | // Test bash integration path 268 | let bash_path = get_bash_integration_path(); 269 | assert!(bash_path.ends_with("bash-integration.sh")); 270 | 271 | // Test fish integration path 272 | let fish_path = get_fish_integration_path(); 273 | assert!(fish_path.ends_with("fish-integration.fish")); 274 | 275 | Ok(()) 276 | } 277 | 278 | #[test] 279 | fn test_init_shell_invalid_shell() -> Result<()> { 280 | // Test with an invalid shell 281 | let result = init_shell(Some("invalid_shell".to_string())); 282 | assert!(result.is_err()); 283 | assert!(result.unwrap_err().to_string().contains("Unsupported shell")); 284 | 285 | Ok(()) 286 | } 287 | -------------------------------------------------------------------------------- /tests/test_utils.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use chrono::{Datelike, Timelike}; 3 | use command_vault::db::Database; 4 | use command_vault::utils::time::parse_datetime; 5 | use tempfile::TempDir; 6 | 7 | pub fn create_test_db() -> Result<(Database, TempDir)> { 8 | let dir = tempfile::tempdir()?; 9 | let db = Database::new(dir.path().join("test.db").to_str().unwrap())?; 10 | Ok((db, dir)) 11 | } 12 | 13 | // Time parsing tests 14 | #[test] 15 | fn test_parse_datetime_rfc3339() { 16 | let input = "2024-03-14T15:30:00Z"; 17 | let result = parse_datetime(input).unwrap(); 18 | assert_eq!(result.year(), 2024); 19 | assert_eq!(result.month(), 3); 20 | assert_eq!(result.day(), 14); 21 | assert_eq!(result.hour(), 15); 22 | assert_eq!(result.minute(), 30); 23 | assert_eq!(result.second(), 0); 24 | } 25 | 26 | #[test] 27 | fn test_parse_datetime_common_formats() { 28 | let test_cases = vec![ 29 | // Date-only formats 30 | ("2024-03-14", 2024, 3, 14, 0, 0, 0), 31 | ("2024/03/14", 2024, 3, 14, 0, 0, 0), 32 | ("14-03-2024", 2024, 3, 14, 0, 0, 0), 33 | ("14/03/2024", 2024, 3, 14, 0, 0, 0), 34 | 35 | // Date-time formats 36 | ("2024-03-14 15:30", 2024, 3, 14, 15, 30, 0), 37 | ("2024-03-14 15:30:45", 2024, 3, 14, 15, 30, 45), 38 | ("14/03/2024 15:30", 2024, 3, 14, 15, 30, 0), 39 | ("14/03/2024 15:30:45", 2024, 3, 14, 15, 30, 45), 40 | ]; 41 | 42 | for (input, year, month, day, hour, minute, second) in test_cases { 43 | let result = parse_datetime(input).unwrap_or_else(|| panic!("Failed to parse date: {}", input)); 44 | assert_eq!(result.year(), year, "Year mismatch for {}", input); 45 | assert_eq!(result.month(), month, "Month mismatch for {}", input); 46 | assert_eq!(result.day(), day, "Day mismatch for {}", input); 47 | assert_eq!(result.hour(), hour, "Hour mismatch for {}", input); 48 | assert_eq!(result.minute(), minute, "Minute mismatch for {}", input); 49 | assert_eq!(result.second(), second, "Second mismatch for {}", input); 50 | } 51 | } 52 | 53 | #[test] 54 | fn test_parse_datetime_invalid() { 55 | let test_cases = vec![ 56 | "invalid", 57 | "2024", 58 | "2024-13-01", // Invalid month 59 | "2024-01-32", // Invalid day 60 | "03/14/24", // Two-digit year not supported 61 | "", 62 | ]; 63 | 64 | for input in test_cases { 65 | assert!(parse_datetime(input).is_none(), "Expected None for input: {}", input); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/time_test.rs: -------------------------------------------------------------------------------- 1 | use command_vault::utils::time::parse_datetime; 2 | use chrono::{DateTime, Utc}; 3 | 4 | #[test] 5 | fn test_parse_datetime_valid() { 6 | let input = "2024-01-01T12:00:00Z"; 7 | let result = parse_datetime(input); 8 | assert!(result.is_some()); 9 | let dt: DateTime = result.unwrap(); 10 | assert_eq!(dt.to_rfc3339(), "2024-01-01T12:00:00+00:00"); 11 | } 12 | 13 | #[test] 14 | fn test_parse_datetime_invalid() { 15 | let input = "invalid date"; 16 | let result = parse_datetime(input); 17 | assert!(result.is_none()); 18 | } 19 | 20 | #[test] 21 | fn test_parse_datetime_empty() { 22 | let input = ""; 23 | let result = parse_datetime(input); 24 | assert!(result.is_none()); 25 | } 26 | 27 | #[test] 28 | fn test_parse_datetime_different_formats() { 29 | let inputs = vec![ 30 | "2024-01-01T12:00:00+00:00", 31 | "2024-01-01T12:00:00-05:00", 32 | "2024-01-01 12:00:00 UTC", 33 | ]; 34 | 35 | for input in inputs { 36 | let result = parse_datetime(input); 37 | assert!(result.is_some(), "Failed to parse: {}", input); 38 | } 39 | } 40 | --------------------------------------------------------------------------------