From 24d36cbad4d7c1bb5e1c927ab43664f37169d9c4 Mon Sep 17 00:00:00 2001 From: michael1986 Date: Tue, 2 Dec 2025 09:02:03 +0100 Subject: [PATCH] first commit --- .dockerignore | 14 + .github/workflows/docker.yml | 66 +++ .github/workflows/release.yml | 80 +++ .github/workflows/test.yml | 69 +++ .gitignore | 35 ++ .pre-commit-config.yaml | 7 + Dockerfile | 52 ++ Dockerfile.test | 34 ++ LICENSE | 24 + PUBLISHING.md | 365 +++++++++++++ README.md | 300 +++++++++++ SUMMARY.md | 201 +++++++ USAGE.md | 140 +++++ debian/changelog | 10 + debian/compat | 1 + debian/control | 24 + debian/copyright | 30 ++ debian/postinst | 27 + debian/rules | 19 + debian/source/format | 1 + docker-compose.yml | 30 ++ docs/CNAME | 1 + docs/install.sh | 198 +++++++ finish.sh | 960 ++++++++++++++++++++++++++++++++++ homebrew/finish.rb | 47 ++ run_tests.sh | 3 + tests/test_finish.bats | 43 ++ 27 files changed, 2781 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 Dockerfile create mode 100644 Dockerfile.test create mode 100644 LICENSE create mode 100644 PUBLISHING.md create mode 100644 README.md create mode 100644 SUMMARY.md create mode 100644 USAGE.md create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/postinst create mode 100644 debian/rules create mode 100644 debian/source/format create mode 100644 docker-compose.yml create mode 100644 docs/CNAME create mode 100644 docs/install.sh create mode 100644 finish.sh create mode 100644 homebrew/finish.rb create mode 100644 run_tests.sh create mode 100644 tests/test_finish.bats diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b83abf2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.github +.gitignore +.idea +*.md +!README.md +!LICENSE +tests +run_tests.sh +debian +homebrew +docker-compose.yml +Dockerfile.test +.dockerignore diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..bd936ef --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,66 @@ +name: Docker Build and Push + +on: + push: + tags: + - 'v*' + branches: + - main + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Build + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Build and push test image + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile.test + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:test + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5b650d7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,80 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + build-and-release: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get version from tag + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Create release tarball + run: | + mkdir -p dist + tar -czf dist/finish-v${{ steps.get_version.outputs.VERSION }}.tar.gz \ + --exclude='.git' \ + --exclude='.github' \ + --exclude='dist' \ + --exclude='.idea' \ + --transform "s,^,finish-${{ steps.get_version.outputs.VERSION }}/," \ + . + + - name: Calculate SHA256 + id: sha256 + run: | + SHA256=$(sha256sum dist/finish-v${{ steps.get_version.outputs.VERSION }}.tar.gz | awk '{print $1}') + echo "SHA256=$SHA256" >> $GITHUB_OUTPUT + echo "SHA256: $SHA256" + + - name: Build Debian package + run: | + sudo apt-get update + sudo apt-get install -y debhelper devscripts + dpkg-buildpackage -us -uc -b + mv ../finish_*.deb dist/ || true + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: | + dist/*.tar.gz + dist/*.deb + body: | + ## Installation + + ### Quick Install (Linux/macOS) + ```bash + curl -sSL https://git.appmodel.nl/Tour/finish/main/docs/install.sh | bash + ``` + + ### Homebrew (macOS) + Update the formula with SHA256: `${{ steps.sha256.outputs.SHA256 }}` + + ### Debian/Ubuntu + Download the `.deb` file and install: + ```bash + sudo dpkg -i finish_*.deb + sudo apt-get install -f # Install dependencies + ``` + + ### Docker + ```bash + docker pull ghcr.io/${{ github.repository }}:${{ steps.get_version.outputs.VERSION }} + ``` + + ## Changelog + See [CHANGELOG.md](CHANGELOG.md) for details. + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5926ff3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,69 @@ +name: Tests + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + shell: [bash, zsh] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y bash zsh curl jq bc bash-completion bats + + - name: Make script executable + run: chmod +x finishte.sh + + - name: Run BATS tests + run: | + if [ -d tests ]; then + bats tests/ + else + echo "No tests directory found" + fi + + - name: Test installation + run: | + export HOME=$PWD/test-home + mkdir -p $HOME/.local/bin + cp finishte.sh $HOME/.local/bin/finishte + chmod +x $HOME/.local/bin/finishte + $HOME/.local/bin/finishte --help + + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install shellcheck + run: sudo apt-get update && sudo apt-get install -y shellcheck + + - name: Run shellcheck + run: shellcheck finishte.sh docs/install.sh || true + + docker: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t finish:test . + + - name: Test Docker image + run: | + docker run --rm finish:test -c "finishte --help" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c70d8c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Build artifacts +debian/finish/ +debian/.debhelper/ +debian/files +debian/*.log +debian/*.substvars +*.deb +*.tar.gz +*.tar.xz + +# IDEs +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Environment +.env +.env.local + +# Logs and cache +finish.log +~/.finish/ +*.cache +__pycache__/ + +# Temporary files +*.tmp +temp/ +dist/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6050445 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: +- repo: https://github.com/koalaman/shellcheck-precommit + rev: v0.10.0 + hooks: + - id: shellcheck + files: ^.+\.sh$ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3c7f704 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,52 @@ +# finishte.sh Docker Image +FROM ubuntu:22.04 + +LABEL maintainer="finish contributors" +LABEL description="LLM-powered command-line autocompletion with LM-Studio integration" +LABEL version="0.5.0" + +# Prevent interactive prompts during installation +ENV DEBIAN_FRONTEND=noninteractive + +# Install dependencies +RUN apt-get update && \ + apt-get install -y \ + bash \ + bash-completion \ + curl \ + wget \ + jq \ + bc \ + vim \ + git \ + && apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Create application directory +WORKDIR /opt/finish + +# Copy application files +COPY finish.sh /usr/bin/finish +COPY README.md LICENSE ./ + +# Make script executable +RUN chmod +x /usr/bin/finish + +# Create finish directory +RUN mkdir -p /root/.finish + +# Source bash-completion in .bashrc +RUN echo "" >> /root/.bashrc && \ + echo "# Bash completion" >> /root/.bashrc && \ + echo "if [ -f /etc/bash_completion ] && ! shopt -oq posix; then" >> /root/.bashrc && \ + echo " . /etc/bash_completion" >> /root/.bashrc && \ + echo "fi" >> /root/.bashrc + +# Set working directory to home +WORKDIR /root + +# Set entrypoint to bash +ENTRYPOINT ["/bin/bash"] + +# Default command shows help +CMD ["-c", "echo 'finish.sh Docker Container' && echo '' && finish --help && exec /bin/bash"] \ No newline at end of file diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 0000000..5d05695 --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,34 @@ +# finish.sh Test Container +FROM ubuntu:22.04 + +LABEL maintainer="finish contributors" +LABEL description="Test environment for finish.sh" + +ENV DEBIAN_FRONTEND=noninteractive + +# Install dependencies including BATS for testing +RUN apt-get update && \ + apt-get install -y \ + bash \ + bash-completion \ + curl \ + wget \ + jq \ + bc \ + vim \ + git \ + bats \ + && apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /opt/finish + +# Copy all files +COPY . . + +# Make scripts executable +RUN chmod +x finish.sh + +# Run tests by default +ENTRYPOINT ["bats"] +CMD ["tests"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cd88cb4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +BSD 2-Clause License + +Copyright (c) 2025, finish contributors + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/PUBLISHING.md b/PUBLISHING.md new file mode 100644 index 0000000..3c8f4c5 --- /dev/null +++ b/PUBLISHING.md @@ -0,0 +1,365 @@ +# Publishing Guide for finishte.sh + +This guide explains how to publish and distribute finishte.sh through various channels. + +## Overview + +finishte.sh can be distributed through multiple channels: +1. **Direct installation script** - One-line curl install +2. **Debian/Ubuntu packages** - APT repository +3. **Homebrew** - macOS package manager +4. **Docker** - Container images +5. **GitHub Releases** - Direct downloads + +## Prerequisites + +Before publishing, ensure: +- [ ] All tests pass (`./run_tests.sh`) +- [ ] Version numbers are updated in all files +- [ ] CHANGELOG.md is updated +- [ ] Documentation is complete and accurate + +## Version Management + +Update version numbers in these files: +1. `finishte.sh` - Line 34: `export ACSH_VERSION=0.5.0` +2. `debian/changelog` - Add new entry +3. `homebrew/finish.rb` - Line 6: `version "0.5.0"` +4. `docs/install.sh` - Line 7: `ACSH_VERSION="v0.5.0"` +5. `Dockerfile` - Line 6: `LABEL version="0.5.0"` + +## 1. Direct Installation Script + +The install script at `docs/install.sh` enables one-line installation: + +```bash +curl -sSL https://git.appmodel.nl/Tour/finish/raw/branch/main/docs/install.sh | bash +``` + +### Setup: +- Host the script on a reliable server or GitHub/GitLab +- Ensure the URL is accessible and supports HTTPS +- Test the installation on clean systems + +### Testing: +```bash +# Test in Docker +docker run --rm -it ubuntu:22.04 bash +curl -sSL YOUR_URL/install.sh | bash +``` + +## 2. Debian/Ubuntu Packages + +### Building the Package + +```bash +# Install build tools +sudo apt-get install debhelper devscripts + +# Build the package +dpkg-buildpackage -us -uc -b + +# This creates: +# ../finish_0.5.0-1_all.deb +``` + +### Testing the Package + +```bash +# Install locally +sudo dpkg -i ../finish_*.deb +sudo apt-get install -f # Fix dependencies + +# Test installation +finishte --help +finishte install +``` + +### Creating an APT Repository + +#### Option A: Using Packagecloud (Recommended for beginners) + +1. Sign up at https://packagecloud.io +2. Create a repository +3. Upload your .deb file: + ```bash + gem install package_cloud + package_cloud push yourname/finish/ubuntu/jammy ../finish_*.deb + ``` + +Users install with: +```bash +curl -s https://packagecloud.io/install/repositories/yourname/finish/script.deb.sh | sudo bash +sudo apt-get install finish +``` + +#### Option B: Using GitHub Pages + reprepro + +1. Create a repository structure: + ```bash + mkdir -p apt-repo/{conf,pool,dists} + ``` + +2. Create `apt-repo/conf/distributions`: + ``` + Origin: finishte.sh + Label: finish + Codename: stable + Architectures: all + Components: main + Description: APT repository for finishte.sh + ``` + +3. Add packages: + ```bash + reprepro -b apt-repo includedeb stable ../finish_*.deb + ``` + +4. Host on GitHub Pages or your server + +Users install with: +```bash +echo "deb https://your-domain.com/apt-repo stable main" | sudo tee /etc/apt/sources.list.d/finish.list +curl -fsSL https://your-domain.com/apt-repo/key.gpg | sudo apt-key add - +sudo apt-get update +sudo apt-get install finish +``` + +## 3. Homebrew Formula + +### Creating a Homebrew Tap + +1. Create a GitHub repository: `homebrew-finish` + +2. Add the formula to `Formula/finish.rb` + +3. Create a release tarball and calculate SHA256: + ```bash + git archive --format=tar.gz --prefix=finish-0.5.0/ v0.5.0 > finish-0.5.0.tar.gz + sha256sum finish-0.5.0.tar.gz + ``` + +4. Update the formula with the correct URL and SHA256 + +5. Test the formula: + ```bash + brew install --build-from-source ./homebrew/finish.rb + brew test finish + brew audit --strict finish + ``` + +### Publishing the Tap + +Users can install with: +```bash +brew tap yourname/finish +brew install finish +``` + +Or directly: +```bash +brew install yourname/finish/finish +``` + +## 4. Docker Images + +### Building Images + +```bash +# Production image +docker build -t finish:0.5.0 -t finish:latest . + +# Test image +docker build -f Dockerfile.test -t finish:test . +``` + +### Publishing to GitHub Container Registry + +The GitHub Actions workflow automatically publishes Docker images when you push a tag: + +```bash +git tag -a v0.5.0 -m "Release v0.5.0" +git push origin v0.5.0 +``` + +Images will be available at: +``` +ghcr.io/appmodel/finish:0.5.0 +ghcr.io/appmodel/finish:latest +``` + +### Publishing to Docker Hub + +```bash +# Login +docker login + +# Tag and push +docker tag finish:latest yourname/finish:0.5.0 +docker tag finish:latest yourname/finish:latest +docker push yourname/finish:0.5.0 +docker push yourname/finish:latest +``` + +## 5. GitHub Releases + +### Automated Release Process + +The `.github/workflows/release.yml` workflow automatically: +1. Creates release artifacts when you push a tag +2. Builds Debian packages +3. Publishes Docker images +4. Creates a GitHub release with downloads + +### Manual Release Process + +1. Create a release tarball: + ```bash + git archive --format=tar.gz --prefix=finish-0.5.0/ v0.5.0 > finish-0.5.0.tar.gz + ``` + +2. Create release on GitHub: + - Go to Releases → Draft a new release + - Tag: `v0.5.0` + - Title: `finishte.sh v0.5.0` + - Upload: tarball and .deb file + - Write release notes + +## Release Checklist + +Before releasing a new version: + +- [ ] Update all version numbers +- [ ] Update CHANGELOG.md +- [ ] Run all tests +- [ ] Test installation script +- [ ] Build and test Debian package +- [ ] Test Docker images +- [ ] Update documentation +- [ ] Create git tag +- [ ] Push tag to trigger CI/CD +- [ ] Verify GitHub release created +- [ ] Verify Docker images published +- [ ] Update Homebrew formula with new SHA256 +- [ ] Test installation from all sources +- [ ] Announce release (if applicable) + +## Distribution URLs + +After publishing, users can install from: + +**Quick Install:** +```bash +curl -sSL https://git.appmodel.nl/Tour/finish/raw/branch/main/docs/install.sh | bash +``` + +**Homebrew:** +```bash +brew tap appmodel/finish +brew install finish +``` + +**APT (if configured):** +```bash +sudo apt-get install finish +``` + +**Docker:** +```bash +docker pull ghcr.io/appmodel/finish:latest +``` + +**Direct Download:** +``` +https://git.appmodel.nl/Tour/finish/releases/latest +``` + +## Troubleshooting + +### Debian Package Issues + +**Problem:** Package won't install +```bash +# Check dependencies +dpkg-deb -I finish_*.deb +# Test installation +sudo dpkg -i finish_*.deb +sudo apt-get install -f +``` + +**Problem:** Lintian warnings +```bash +lintian finish_*.deb +# Fix issues in debian/ files +``` + +### Homebrew Issues + +**Problem:** Formula audit fails +```bash +brew audit --strict ./homebrew/finish.rb +# Fix issues reported +``` + +**Problem:** Installation fails +```bash +brew install --verbose --debug ./homebrew/finish.rb +``` + +### Docker Issues + +**Problem:** Build fails +```bash +docker build --no-cache -t finish:test . +``` + +**Problem:** Image too large +```bash +# Use multi-stage builds +# Remove unnecessary files +# Combine RUN commands +``` + +## Continuous Integration + +The project includes three GitHub Actions workflows: + +1. **test.yml** - Runs on every push/PR + - Runs BATS tests + - Tests installation + - Runs shellcheck + - Tests Docker builds + +2. **docker.yml** - Builds and publishes Docker images + - Triggered by tags and main branch + - Publishes to GitHub Container Registry + +3. **release.yml** - Creates releases + - Triggered by version tags + - Builds all artifacts + - Creates GitHub release + - Publishes packages + +## Support and Documentation + +- Update the README.md with installation instructions +- Maintain CHANGELOG.md with version history +- Create issue templates for bug reports and feature requests +- Set up discussions for community support + +## Security Considerations + +- Always use HTTPS for installation scripts +- Sign Debian packages with GPG +- Use checksums for release artifacts +- Regularly update dependencies +- Monitor for security vulnerabilities + +## Next Steps + +After initial publication: +1. Monitor GitHub issues for user feedback +2. Set up analytics if desired +3. Create documentation site +4. Announce on relevant forums/communities +5. Consider submitting to package manager directories diff --git a/README.md b/README.md new file mode 100644 index 0000000..b903366 --- /dev/null +++ b/README.md @@ -0,0 +1,300 @@ +# finishte.sh + +Command-line completion powered by local LLMs. Press `Tab` twice and get intelligent suggestions based on your terminal context. + +## Quick Start + +### One-Line Install (Recommended) + +```bash +curl -sSL https://git.appmodel.nl/Tour/finish/raw/branch/main/docs/install.sh | bash +``` + +### Manual Installation + +```bash +git clone https://git.appmodel.nl/Tour/finish.git finish +cd finish +./finish.sh install +source ~/.bashrc +``` + +### Other Installation Methods + +See [Installation](#installation) below for package managers, Docker, and more options. + +## What It Does + +finishte.sh enhances your terminal with context-aware command suggestions. It analyzes: + +- Current directory and recent files +- Command history +- Environment variables +- Command help output + +Then generates relevant completions using a local LLM. + +## Features + +**Local by Default** +Runs with LM-Studio on localhost. No external API calls, no data leaving your machine. + +**Context-Aware** +Understands what you're doing based on your environment and history. + +**Cached** +Stores recent queries locally for instant responses. + +**Flexible** +Switch between different models and providers easily. + +## Configuration + +View current settings: + +```bash +finishte config +``` + +Change the endpoint or model: + +```bash +finishte config set endpoint http://192.168.1.100:1234/v1/chat/completions +finishte config set model your-model-name +``` + +Select a model interactively: + +```bash +finishte model +``` + +## Usage + +Type a command and press `Tab` twice: + +```bash +docker +git commit +ffmpeg +``` + +Natural language works too: + +```bash +# find large files +# compress to zip +``` + +Test it without executing: + +```bash +finishte command "your command" +finishte command --dry-run "your command" +``` + +## Installation + +### Quick Install (Linux/macOS) + +The fastest way to get started: + +```bash +curl -sSL https://git.appmodel.nl/Tour/finish/raw/branch/main/docs/install.sh | bash +source ~/.bashrc # or ~/.zshrc for zsh +finishte model +``` + +### Package Managers + +#### Homebrew (macOS) + +```bash +brew tap appmodel/finish +brew install finish +finishte install +source ~/.bashrc +``` + +#### APT (Debian/Ubuntu) + +Download the `.deb` package from [releases](https://git.appmodel.nl/Tour/finish/releases): + +```bash +sudo dpkg -i finish_*.deb +sudo apt-get install -f # Install dependencies +finishte install +source ~/.bashrc +``` + +### Docker + +Run finishte.sh in a container: + +```bash +# Build the image +docker build -t finish . + +# Run interactively +docker run -it finish + +# Or use docker-compose +docker-compose up -d finish +docker-compose exec finish bash +``` + +Inside the container: +```bash +finishte install +source ~/.bashrc +finishte model # Configure your LLM endpoint +``` + +### From Source + +```bash +git clone https://git.appmodel.nl/Tour/finish.git finish +cd finish +chmod +x finish.sh +sudo ln -s $PWD/finish.sh /usr/local/bin/finish +finishte install +source ~/.bashrc +``` + +## Requirements + +- Bash 4.0+ or Zsh 5.0+ +- curl +- jq +- bc +- bash-completion (recommended) +- LM-Studio or Ollama (for LLM inference) + +## Directory Structure + +``` +~/.finishte/ +├── config # Configuration file +├── finishte.log # Usage log +└── cache/ # Cached completions +``` + +## Commands + +```bash +finishte install # Set up finishte +finishte remove # Uninstall +finishte config # Show/edit configuration +finishte model # Select model +finishte enable # Enable completions +finishte disable # Disable completions +finishte clear # Clear cache and logs +finishte usage # Show usage statistics +finishte system # Show system information +finishte --help # Show help +``` + +## Providers + +Currently supports: + +- **LM-Studio** (default) - Local models via OpenAI-compatible API +- **Ollama** - Local models via Ollama API + +Add custom providers by editing `finishte.sh` and adding entries to `_finishte_modellist`. + +## Development + +### Local Development + +Clone and link for development: + +```bash +git clone https://git.appmodel.nl/Tour/finish.git finish +cd finish +ln -s $PWD/finishte.sh $HOME/.local/bin/finish +finishte install +``` + +### Running Tests + +```bash +# Install BATS testing framework +sudo apt-get install bats # Ubuntu/Debian + +# Run tests +./run_tests.sh + +# Or use Docker +docker build -f Dockerfile.test -t finish:test . +docker run --rm finish:test +``` + +### Building Packages + +#### Debian Package + +```bash +# Install build dependencies +sudo apt-get install debhelper devscripts + +# Build the package +dpkg-buildpackage -us -uc -b + +# Package will be created in parent directory +ls ../*.deb +``` + +#### Docker Images + +```bash +# Production image +docker build -t finish:latest . + +# Test image +docker build -f Dockerfile.test -t finish:test . + +# Using docker-compose +docker-compose build +``` + +## Distribution & Publishing + +### Creating a Release + +1. Update version in `finishte.sh` (ACSH_VERSION) +2. Update version in `debian/changelog` +3. Update version in `homebrew/finish.rb` +4. Create and push a git tag: + +```bash +git tag -a v0.5.0 -m "Release version 0.5.0" +git push origin v0.5.0 +``` + +This triggers the GitHub Actions workflow which: +- Builds release artifacts +- Creates Debian packages +- Publishes Docker images to GitHub Container Registry +- Creates a GitHub release with downloadable assets + +### Homebrew Formula + +After creating a release, update the Homebrew formula: + +1. Calculate SHA256 of the release tarball: + ```bash + curl -sL https://git.appmodel.nl/Tour/finish/archive/v0.5.0.tar.gz | sha256sum + ``` + +2. Update `homebrew/finish.rb` with the new SHA256 and version + +3. Submit to your Homebrew tap or the main Homebrew repository + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +BSD 2-Clause License. See LICENSE file. diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 0000000..0284287 --- /dev/null +++ b/SUMMARY.md @@ -0,0 +1,201 @@ +# Publication Setup Summary + +This document summarizes the publication setup completed for finishte.sh. + +## Files Created + +### 1. Installation Script +- **`docs/install.sh`** - Enhanced one-line installer with: + - Dependency checking + - Shell detection (bash/zsh) + - Color-coded output + - Error handling + - PATH management + - Usage: `curl -sSL /install.sh | bash` + +### 2. Debian Package Structure +Complete Debian packaging in `debian/` directory: +- **`control`** - Package metadata and dependencies +- **`rules`** - Build instructions +- **`changelog`** - Version history +- **`copyright`** - License information +- **`compat`** - Debhelper compatibility level +- **`postinst`** - Post-installation script +- **`source/format`** - Package format specification + +Build command: `dpkg-buildpackage -us -uc -b` + +### 3. Homebrew Formula +- **`homebrew/finish.rb`** - Homebrew formula for macOS +- Includes dependencies, installation steps, and caveats +- Update SHA256 after creating releases + +### 4. Docker Configuration +- **`Dockerfile`** - Production container image +- **`Dockerfile.test`** - Testing container with BATS +- **`docker-compose.yml`** - Multi-service Docker setup +- **`.dockerignore`** - Excludes unnecessary files from builds + +### 5. GitHub Actions Workflows +CI/CD automation in `.github/workflows/`: +- **`test.yml`** - Runs tests on every push/PR +- **`release.yml`** - Creates releases and builds packages +- **`docker.yml`** - Builds and publishes Docker images + +### 6. Documentation +- **`PUBLISHING.md`** - Comprehensive publishing guide +- **`README.md`** - Updated with installation methods +- **`.gitignore`** - Updated to exclude build artifacts + +## Distribution Channels + +### 1. Quick Install (Recommended) +```bash +curl -sSL https://git.appmodel.nl/Tour/finish/raw/branch/main/docs/install.sh | bash +``` + +### 2. Debian/Ubuntu Package +Users can install the `.deb` file: +```bash +sudo dpkg -i finish_*.deb +sudo apt-get install -f +``` + +Or from an APT repository (when configured): +```bash +sudo apt-get install finish +``` + +### 3. Homebrew (macOS) +```bash +brew tap closedloop-technologies/finish +brew install finish +``` + +### 4. Docker +```bash +docker pull ghcr.io/closedloop-technologies/finish:latest +docker run -it ghcr.io/closedloop-technologies/finish:latest +``` + +### 5. From Source +```bash +git clone https://git.appmodel.nl/Tour/finish.git +cd finish +./finishte.sh install +``` + +## Next Steps + +### Immediate Actions +1. **Test the installation script** on clean systems +2. **Build and test the Debian package**: + ```bash + dpkg-buildpackage -us -uc -b + sudo dpkg -i ../finish_*.deb + ``` +3. **Test Docker builds**: + ```bash + docker build -t finish:test . + docker run -it finish:test + ``` + +### For First Release +1. **Update version numbers** in: + - `finishte.sh` (line 34) + - `debian/changelog` + - `homebrew/finish.rb` + - `docs/install.sh` + - `Dockerfile` + +2. **Create release tarball**: + ```bash + git archive --format=tar.gz --prefix=finish-0.5.0/ v0.5.0 > finish-0.5.0.tar.gz + ``` + +3. **Calculate SHA256** for Homebrew: + ```bash + sha256sum finish-0.5.0.tar.gz + ``` + +4. **Create and push tag**: + ```bash + git tag -a v0.5.0 -m "Release v0.5.0" + git push origin v0.5.0 + ``` + This triggers GitHub Actions to build everything automatically. + +### Setting Up Package Repositories + +#### APT Repository Options: +1. **Packagecloud** (easiest): + - Sign up at https://packagecloud.io + - Upload .deb file + - Users get one-line install + +2. **GitHub Pages** (free): + - Use `reprepro` to create repository + - Host on GitHub Pages + - Users add your repository to sources + +See `PUBLISHING.md` for detailed instructions. + +#### Homebrew Tap: +1. Create GitHub repo: `homebrew-finish` +2. Add formula to `Formula/finish.rb` +3. Users install with: `brew tap yourname/finish && brew install finish` + +## Testing Checklist + +Before releasing: +- [ ] Test install script on Ubuntu 22.04 +- [ ] Test install script on Debian 12 +- [ ] Test install script on macOS +- [ ] Build Debian package successfully +- [ ] Install and test Debian package +- [ ] Build Docker image successfully +- [ ] Run BATS tests in Docker +- [ ] Test Homebrew formula (if applicable) +- [ ] Verify all version numbers match +- [ ] Test GitHub Actions workflows + +## Automation + +The GitHub Actions workflows handle: +- **On every push/PR**: Run tests, lint code, build Docker images +- **On version tag push**: Build packages, create release, publish Docker images + +This means once set up, releasing a new version is as simple as: +```bash +git tag -a v0.5.1 -m "Release v0.5.1" +git push origin v0.5.1 +``` + +Everything else happens automatically! + +## Support Channels + +Consider setting up: +- GitHub Issues for bug reports +- GitHub Discussions for Q&A +- Documentation website (GitHub Pages) +- Discord/Slack community (optional) + +## Resources + +- Debian Packaging Guide: https://www.debian.org/doc/manuals/maint-guide/ +- Homebrew Formula Cookbook: https://docs.brew.sh/Formula-Cookbook +- GitHub Actions Documentation: https://docs.github.com/en/actions +- Docker Best Practices: https://docs.docker.com/develop/dev-best-practices/ + +## Conclusion + +Your project is now fully set up for publication with: +- ✅ One-line installation script +- ✅ Debian/Ubuntu package support +- ✅ Homebrew formula for macOS +- ✅ Docker containers +- ✅ Automated CI/CD with GitHub Actions +- ✅ Comprehensive documentation + +All distribution channels are ready. Follow the "Next Steps" section above to test and publish your first release! diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..3aa9699 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,140 @@ +# Usage Guide + +## Installation + +Install and set up finishte: + +```bash +finishte install +source ~/.bashrc +``` + +## Configuration + +View current settings: + +```bash +finishte config +``` + +Change settings: + +```bash +finishte config set temperature 0.5 +finishte config set endpoint http://localhost:1234/v1/chat/completions +finishte config set model your-model-name +``` + +Reset to defaults: + +```bash +finishte config reset +``` + +## Model Selection + +Select a model interactively: + +```bash +finishte model +``` + +Use arrow keys to navigate, Enter to select, or 'q' to quit. + +## Basic Commands + +Show help: + +```bash +finishte --help +``` + +Test completions without caching: + +```bash +finishte command "your command here" +``` + +Preview the prompt sent to the model: + +```bash +finishte command --dry-run "your command here" +``` + +View system information: + +```bash +finishte system +``` + +## Usage Statistics + +Check your usage stats and costs: + +```bash +finishte usage +``` + +## Cache Management + +Clear cached completions and logs: + +```bash +finishte clear +``` + +## Enable/Disable + +Temporarily disable: + +```bash +finishte disable +``` + +Re-enable: + +```bash +finishte enable +``` + +## Uninstallation + +Remove finishte completely: + +```bash +finishte remove +``` + +## Using finishte + +Once enabled, just type a command and press `Tab` twice to get suggestions: + +```bash +git +docker +find +``` + +Natural language also works: + +```bash +# convert video to mp4 +# list all processes using port 8080 +# compress this folder to tar.gz +``` + +## Configuration File + +The config file is located at `~/.finishte/config` and contains: + +- `provider`: Model provider (lmstudio, ollama) +- `model`: Model name +- `temperature`: Response randomness (0.0 - 1.0) +- `endpoint`: API endpoint URL +- `api_prompt_cost`: Cost per input token +- `api_completion_cost`: Cost per output token +- `max_history_commands`: Number of recent commands to include in context +- `max_recent_files`: Number of recent files to include in context +- `cache_dir`: Directory for cached completions +- `cache_size`: Maximum number of cached items +- `log_file`: Path to usage log file diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..f7d550b --- /dev/null +++ b/debian/changelog @@ -0,0 +1,10 @@ +finish (0.5.0-1) stable; urgency=medium + + * Initial Debian package release + * LLM-powered bash and zsh finishtion + * Support for LM-Studio and Ollama providers + * Context-aware command suggestions + * Intelligent caching system + * Privacy-focused local operation + + -- finishte.sh Contributors Mon, 02 Dec 2024 12:00:00 +0000 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..f599e28 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +10 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..f732e9b --- /dev/null +++ b/debian/control @@ -0,0 +1,24 @@ +Source: finish +Section: utils +Priority: optional +Maintainer: finishte.sh Contributors +Build-Depends: debhelper (>= 10) +Standards-Version: 4.5.0 +Homepage: https://git.appmodel.nl/Tour/finish/finish +Vcs-Git: https://git.appmodel.nl/Tour/finish.git + +Package: finish +Architecture: all +Depends: bash (>= 4.0) | zsh (>= 5.0), curl, jq, bc, bash-completion +Recommends: lm-studio | ollama +Description: LLM-powered command-line finishtion + finishte.sh enhances your terminal with context-aware command suggestions + using local large language models. Features include: + . + * Local-first operation with LM-Studio or Ollama + * Context-aware suggestions based on command history and environment + * Intelligent caching for instant responses + * Support for multiple LLM providers + * Privacy-focused - no data leaves your machine by default + . + Simply press Tab twice to get intelligent command suggestions powered by AI. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..15c520a --- /dev/null +++ b/debian/copyright @@ -0,0 +1,30 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: finish +Upstream-Contact: https://git.appmodel.nl/Tour/finish/finish +Source: https://git.appmodel.nl/Tour/finish + +Files: * +Copyright: 2024-2025 finishte.sh Contributors +License: BSD-2-Clause + +License: BSD-2-Clause + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + . + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + . + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + . + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/debian/postinst b/debian/postinst new file mode 100644 index 0000000..5f029b0 --- /dev/null +++ b/debian/postinst @@ -0,0 +1,27 @@ +#!/bin/sh +set -e + +case "$1" in + configure) + echo "finishte.sh has been installed successfully!" + echo "" + echo "To complete setup, run:" + echo " finishte install" + echo "" + echo "Then reload your shell:" + echo " source ~/.bashrc # for bash" + echo " source ~/.zshrc # for zsh" + ;; + + abort-upgrade|abort-remove|abort-deconfigure) + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + +#DEBHELPER# + +exit 0 diff --git a/debian/rules b/debian/rules new file mode 100644 index 0000000..fde4766 --- /dev/null +++ b/debian/rules @@ -0,0 +1,19 @@ +#!/usr/bin/make -f +# -*- makefile -*- + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +%: + dh $@ + +override_dh_auto_install: + install -D -m 0755 finishte.sh $(CURDIR)/debian/finish/usr/bin/finishte + install -D -m 0644 LICENSE $(CURDIR)/debian/finish/usr/share/doc/finish/copyright + install -D -m 0644 README.md $(CURDIR)/debian/finish/usr/share/doc/finish/README.md + +override_dh_auto_build: + # Nothing to build + +override_dh_auto_test: + # Tests run in CI diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e34703f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: '3.8' + +services: + finish: + build: + context: . + dockerfile: Dockerfile + image: finish:latest + container_name: finish + stdin_open: true + tty: true + volumes: + - finish-data:/root/.finish + environment: + - ACSH_ENDPOINT=http://host.docker.internal:1234/v1/chat/completions + extra_hosts: + - "host.docker.internal:host-gateway" + command: /bin/bash + + finish-test: + build: + context: . + dockerfile: Dockerfile.test + image: finish:test + container_name: finish-test + volumes: + - .:/opt/finish + +volumes: + finish-data: diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 0000000..5f83d87 --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +finishte \ No newline at end of file diff --git a/docs/install.sh b/docs/install.sh new file mode 100644 index 0000000..28c8d8c --- /dev/null +++ b/docs/install.sh @@ -0,0 +1,198 @@ +#!/bin/sh +# finishte.sh installer +# Usage: curl -sSL https://git.appmodel.nl/Tour/finish/raw/branch/main/docs/install.sh | bash + +set -e + +ACSH_VERSION="v0.5.0" +BRANCH_OR_VERSION=${1:-main} +REPO_URL="https://git.appmodel.nl/Tour/finish/raw/branch" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo_green() { + printf "${GREEN}%s${NC}\n" "$1" +} + +echo_error() { + printf "${RED}ERROR: %s${NC}\n" "$1" >&2 +} + +echo_warning() { + printf "${YELLOW}WARNING: %s${NC}\n" "$1" +} + +# Detect the current shell +detect_shell() { + if [ -n "$ZSH_VERSION" ]; then + echo "zsh" + elif [ -n "$BASH_VERSION" ]; then + echo "bash" + else + parent_shell=$(ps -p $PPID -o comm= 2>/dev/null || echo "") + case "$parent_shell" in + *zsh*) echo "zsh" ;; + *bash*) echo "bash" ;; + *) echo "unknown" ;; + esac + fi +} + +# Check dependencies +check_dependencies() { + local missing="" + + if ! command -v curl > /dev/null 2>&1 && ! command -v wget > /dev/null 2>&1; then + missing="${missing}curl or wget, " + fi + + if ! command -v jq > /dev/null 2>&1; then + missing="${missing}jq, " + fi + + if ! command -v bc > /dev/null 2>&1; then + missing="${missing}bc, " + fi + + if [ -n "$missing" ]; then + echo_error "Missing dependencies: ${missing%, }" + echo "" + echo "Install them with:" + echo " Ubuntu/Debian: sudo apt-get install curl jq bc bash-completion" + echo " CentOS/RHEL: sudo yum install curl jq bc bash-completion" + echo " macOS: brew install jq bc bash-completion" + return 1 + fi + return 0 +} + +# Download file +download_file() { + local url="$1" + local output="$2" + + if command -v curl > /dev/null 2>&1; then + curl -fsSL "$url" -o "$output" + elif command -v wget > /dev/null 2>&1; then + wget -q -O "$output" "$url" + else + echo_error "Neither curl nor wget is available" + return 1 + fi +} + +# Main installation +main() { + echo_green "==========================================" + echo_green " finishte.sh Installation" + echo_green " Version: $ACSH_VERSION" + echo_green "==========================================" + echo "" + + # Detect shell + SHELL_TYPE=$(detect_shell) + case "$SHELL_TYPE" in + zsh) + SCRIPT_NAME="finishte.zsh" + RC_FILE="$HOME/.zshrc" + ;; + bash) + SCRIPT_NAME="finishte.sh" + RC_FILE="$HOME/.bashrc" + ;; + *) + echo_error "Unsupported shell. Currently only bash and zsh are supported." + exit 1 + ;; + esac + + echo "Detected shell: $SHELL_TYPE" + + # Check dependencies + echo "Checking dependencies..." + if ! check_dependencies; then + exit 1 + fi + echo_green "✓ All dependencies found" + echo "" + + # Determine installation location + if [ -d "$HOME/.local/bin" ]; then + INSTALL_LOCATION="$HOME/.local/bin/finishte" + # Add to PATH if not already there + if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then + echo_warning "$HOME/.local/bin is not in PATH" + if [ ! -f "$RC_FILE" ] || ! grep -q ".local/bin" "$RC_FILE"; then + echo "Adding $HOME/.local/bin to PATH in $RC_FILE" + echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$RC_FILE" + fi + fi + elif [ -w "/usr/local/bin" ]; then + INSTALL_LOCATION="/usr/local/bin/finishte" + else + echo_error "Cannot write to /usr/local/bin and $HOME/.local/bin doesn't exist" + echo "Please create $HOME/.local/bin or run with sudo" + exit 1 + fi + + # Create directory if needed + mkdir -p "$(dirname "$INSTALL_LOCATION")" + + # Download script + echo "Downloading finishte.sh..." + URL="$REPO_URL/$BRANCH_OR_VERSION/$SCRIPT_NAME" + if ! download_file "$URL" "$INSTALL_LOCATION"; then + echo_error "Failed to download from $URL" + exit 1 + fi + + chmod +x "$INSTALL_LOCATION" + echo_green "✓ Installed to $INSTALL_LOCATION" + echo "" + + # Check bash-completion + if [ "$SHELL_TYPE" = "bash" ]; then + if ! command -v _init_completion > /dev/null 2>&1; then + if [ -f /usr/share/bash-completion/bash_completion ]; then + echo_warning "bash-completion not loaded. Add to $RC_FILE:" + echo " source /usr/share/bash-completion/bash_completion" + elif [ -f /etc/bash_completion ]; then + echo_warning "bash-completion not loaded. Add to $RC_FILE:" + echo " source /etc/bash_completion" + else + echo_warning "bash-completion not found. Install it for best experience:" + echo " sudo apt-get install bash-completion # Ubuntu/Debian" + fi + echo "" + fi + fi + + # Run finishte install + echo "Running finishte installation..." + if "$INSTALL_LOCATION" install; then + echo "" + echo_green "==========================================" + echo_green " Installation Complete!" + echo_green "==========================================" + echo "" + echo "Next steps:" + echo " 1. Reload your shell configuration:" + echo " source $RC_FILE" + echo "" + echo " 2. Select a language model:" + echo " finishte model" + echo "" + echo " 3. Start using by pressing Tab twice after any command" + echo "" + echo "Documentation: https://git.appmodel.nl/Tour/finish/finish" + else + echo_error "Installation failed. Please check the output above." + exit 1 + fi +} + +main "$@" diff --git a/finish.sh b/finish.sh new file mode 100644 index 0000000..c4dff31 --- /dev/null +++ b/finish.sh @@ -0,0 +1,960 @@ +#!/bin/bash +############################################################################### +# Enhanced Error Handling # +############################################################################### + +error_exit() { + echo -e "\e[finish.sh - $1\e[0m" >&2 + # In a completion context, exit is too severe. Use return instead. + return 1 +} + +echo_error() { + echo -e "\e[finish.sh - $1\e[0m" >&2 +} + +echo_green() { + echo -e "\e[32m$1\e[0m" +} + +############################################################################### +# Global Variables & Model Definitions # +############################################################################### + +export ACSH_VERSION=0.5.0 + +unset _finishte_modellist +declare -A _finishte_modellist +# LM-Studio models +_finishte_modellist['lmstudio: darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2']='{ "completion_cost":0.0000000, "prompt_cost":0.0000000, "endpoint": "http://localhost:1234/v1/chat/completions", "model": "darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2", "provider": "lmstudio" }' +# Ollama model +_finishte_modellist['ollama: codellama']='{ "completion_cost":0.0000000, "prompt_cost":0.0000000, "endpoint": "http://localhost:11434/api/chat", "model": "codellama", "provider": "ollama" }' + +############################################################################### +# System Information Functions # +############################################################################### + +_get_terminal_info() { + local terminal_info=" * User name: \$USER=$USER + * Current directory: \$PWD=$PWD + * Previous directory: \$OLDPWD=$OLDPWD + * Home directory: \$HOME=$HOME + * Operating system: \$OSTYPE=$OSTYPE + * Shell: \$BASH=$BASH + * Terminal type: \$TERM=$TERM + * Hostname: \$HOSTNAME" + echo "$terminal_info" +} + +machine_signature() { + local signature + signature=$(echo "$(uname -a)|$$USER" | md5sum | cut -d ' ' -f 1) + echo "$signature" +} + +_system_info() { + echo "# System Information" + echo + uname -a + echo "SIGNATURE: $(machine_signature)" + echo + echo "BASH_VERSION: $BASH_VERSION" + echo "BASH_COMPLETION_VERSINFO: ${BASH_COMPLETION_VERSINFO}" + echo + echo "## Terminal Information" + _get_terminal_info +} + +_completion_vars() { + echo "BASH_COMPLETION_VERSINFO: ${BASH_COMPLETION_VERSINFO}" + echo "COMP_CWORD: ${COMP_CWORD}" + echo "COMP_KEY: ${COMP_KEY}" + echo "COMP_LINE: ${COMP_LINE}" + echo "COMP_POINT: ${COMP_POINT}" + echo "COMP_TYPE: ${COMP_TYPE}" + echo "COMP_WORDBREAKS: ${COMP_WORDBREAKS}" + echo "COMP_WORDS: ${COMP_WORDS[*]}" +} + +############################################################################### +# LLM Completion Functions # +############################################################################### + +_get_system_message_prompt() { + echo "You are a helpful bash_completion script. Generate relevant and concise auto-complete suggestions for the given user command in the context of the current directory, operating system, command history, and environment variables. The output must be a list of two to five possible completions or rewritten commands, each on a new line, without spanning multiple lines. Each must be a valid command or chain of commands. Do not include backticks or quotes." +} + +_get_output_instructions() { + echo "Provide a list of suggested completions or commands that could be run in the terminal. YOU MUST provide a list of two to five possible completions or rewritten commands. DO NOT wrap the commands in backticks or quotes. Each must be a valid command or chain of commands. Focus on the user's intent, recent commands, and the current environment. RETURN A JSON OBJECT WITH THE COMPLETIONS." +} + +_get_command_history() { + local HISTORY_LIMIT=${ACSH_MAX_HISTORY_COMMANDS:-20} + history | tail -n "$HISTORY_LIMIT" +} + +# Refined sanitization: only replace long hex sequences, UUIDs, and API-key–like tokens. +_get_clean_command_history() { + local recent_history + recent_history=$(_get_command_history) + recent_history=$(echo "$recent_history" | sed -E 's/\b[[:xdigit:]]{32,40}\b/REDACTED_HASH/g') + recent_history=$(echo "$recent_history" | sed -E 's/\b[0-9a-fA-F-]{36}\b/REDACTED_UUID/g') + recent_history=$(echo "$recent_history" | sed -E 's/\b[A-Za-z0-9]{16,40}\b/REDACTED_APIKEY/g') + echo "$recent_history" +} + +_get_recent_files() { + local FILE_LIMIT=${ACSH_MAX_RECENT_FILES:-20} + find . -maxdepth 1 -type f -exec ls -ld {} + | sort -r | head -n "$FILE_LIMIT" +} + +# Rewritten _get_help_message using a heredoc to preserve formatting. +_get_help_message() { + local COMMAND HELP_INFO + COMMAND=$(echo "$1" | awk '{print $1}') + HELP_INFO="" + { + set +e + HELP_INFO=$(cat <&1 || true) +EOF + ) + set -e + } || HELP_INFO="'$COMMAND --help' not available" + echo "$HELP_INFO" +} + +_build_prompt() { + local user_input command_history terminal_context help_message recent_files output_instructions other_environment_variables prompt + user_input="$*" + command_history=$(_get_clean_command_history) + terminal_context=$(_get_terminal_info) + help_message=$(_get_help_message "$user_input") + recent_files=$(_get_recent_files) + output_instructions=$(_get_output_instructions) + other_environment_variables=$(env | grep '=' | grep -v 'ACSH_' | awk -F= '{print $1}' | grep -v 'PWD\|OSTYPE\|BASH\|USER\|HOME\|TERM\|OLDPWD\|HOSTNAME') + + prompt="User command: \`$user_input\` + +# Terminal Context +## Environment variables +$terminal_context + +Other defined environment variables +\`\`\` +$other_environment_variables +\`\`\` + +## History +Recently run commands (some information redacted): +\`\`\` +$command_history +\`\`\` + +## File system +Most recently modified files: +\`\`\` +$recent_files +\`\`\` + +## Help Information +$help_message + +# Instructions +$output_instructions +" + echo "$prompt" +} + +############################################################################### +# Payload Building Functions # +############################################################################### + +build_common_payload() { + jq -n --arg model "$model" \ + --arg temperature "$temperature" \ + --arg system_prompt "$system_prompt" \ + --arg prompt_content "$prompt_content" \ + '{ + model: $model, + messages: [ + {role: "system", content: $system_prompt}, + {role: "user", content: $prompt_content} + ], + temperature: ($temperature | tonumber) + }' +} + +_build_payload() { + local user_input prompt system_message_prompt payload acsh_prompt + local model temperature + model="${ACSH_MODEL:-gpt-4o}" + temperature="${ACSH_TEMPERATURE:-0.0}" + + user_input="$1" + prompt=$(_build_prompt "$@") + system_message_prompt=$(_get_system_message_prompt) + + acsh_prompt="# SYSTEM PROMPT +$system_message_prompt +# USER MESSAGE +$prompt" + export ACSH_PROMPT="$acsh_prompt" + + prompt_content="$prompt" + system_prompt="$system_message_prompt" + + local base_payload + base_payload=$(build_common_payload) + + case "${ACSH_PROVIDER^^}" in + "OLLAMA") + payload=$(echo "$base_payload" | jq '. + { + format: "json", + stream: false, + options: {temperature: (.temperature | tonumber)} + }') + ;; + "LMSTUDIO") + payload=$(echo "$base_payload" | jq '. + {response_format: {type: "json_object"}}') + ;; + *) + payload=$(echo "$base_payload" | jq '. + {response_format: {type: "json_object"}}') + ;; + esac + echo "$payload" +} + +log_request() { + local user_input response_body user_input_hash log_file prompt_tokens completion_tokens created api_cost + local prompt_tokens_int completion_tokens_int + user_input="$1" + response_body="$2" + user_input_hash=$(echo -n "$user_input" | md5sum | cut -d ' ' -f 1) + + prompt_tokens=$(echo "$response_body" | jq -r '.usage.prompt_tokens') + prompt_tokens_int=$((prompt_tokens)) + completion_tokens=$(echo "$response_body" | jq -r '.usage.completion_tokens') + completion_tokens_int=$((completion_tokens)) + + created=$(date +%s) + created=$(echo "$response_body" | jq -r ".created // $created") + api_cost=$(echo "$prompt_tokens_int * $ACSH_API_PROMPT_COST + $completion_tokens_int * $ACSH_API_COMPLETION_COST" | bc) + log_file=${ACSH_LOG_FILE:-"$HOME/.finish/finish.log"} + echo "$created,$user_input_hash,$prompt_tokens_int,$completion_tokens_int,$api_cost" >> "$log_file" +} + +openai_completion() { + local content status_code response_body default_user_input user_input api_key payload endpoint timeout attempt max_attempts + endpoint=${ACSH_ENDPOINT:-"http://localhost:1234/v1/chat/completions"} + timeout=${ACSH_TIMEOUT:-30} + default_user_input="Write two to six most likely commands given the provided information" + user_input=${*:-$default_user_input} + + if [[ -z "$ACSH_ACTIVE_API_KEY" && ${ACSH_PROVIDER^^} != "OLLAMA" && ${ACSH_PROVIDER^^} != "LMSTUDIO" ]]; then + echo_error "ACSH_ACTIVE_API_KEY not set. Please set it with: export ${ACSH_PROVIDER^^}_API_KEY=" + return + fi + api_key="${ACSH_ACTIVE_API_KEY}" + payload=$(_build_payload "$user_input") + + max_attempts=2 + attempt=1 + while [ $attempt -le $max_attempts ]; do + if [[ "${ACSH_PROVIDER^^}" == "OLLAMA" ]]; then + response=$(\curl -s -m "$timeout" -w "\n%{http_code}" "$endpoint" --data "$payload") + else + response=$(\curl -s -m "$timeout" -w "\n%{http_code}" "$endpoint" \ + -H "Content-Type: application/json" \ + -d "$payload") + fi + status_code=$(echo "$response" | tail -n1) + response_body=$(echo "$response" | sed '$d') + if [[ $status_code -eq 200 ]]; then + break + else + echo_error "API call failed with status $status_code. Retrying... (Attempt $attempt of $max_attempts)" + sleep 1 + attempt=$((attempt+1)) + fi + done + + if [[ $status_code -ne 200 ]]; then + case $status_code in + 400) echo_error "Bad Request: The API request was invalid or malformed." ;; + 401) echo_error "Unauthorized: The provided API key is invalid or missing." ;; + 429) echo_error "Too Many Requests: The API rate limit has been exceeded." ;; + 500) echo_error "Internal Server Error: An unexpected error occurred on the API server." ;; + *) echo_error "Unknown Error: Unexpected status code $status_code received. Response: $response_body" ;; + esac + return + fi + + if [[ "${ACSH_PROVIDER^^}" == "OLLAMA" ]]; then + content=$(echo "$response_body" | jq -r '.message.content') + content=$(echo "$content" | jq -r '.completions') + else + content=$(echo "$response_body" | jq -r '.choices[0].message.content') + content=$(echo "$content" | jq -r '.completions') + fi + + local completions + completions=$(echo "$content" | jq -r '.[]' | grep -v '^$') + echo -n "$completions" + log_request "$user_input" "$response_body" +} + +############################################################################### +# Completion Functions # +############################################################################### + +_get_default_completion_function() { + local cmd="$1" + complete -p "$cmd" 2>/dev/null | awk -F' ' '{ for(i=1;i<=NF;i++) { if ($i ~ /^-F$/) { print $(i+1); exit; } } }' +} + +_default_completion() { + local current_word="" first_word="" default_func + if [[ -n "${COMP_WORDS[*]}" ]]; then + first_word="${COMP_WORDS[0]}" + if [[ -n "$COMP_CWORD" && "$COMP_CWORD" -lt "${#COMP_WORDS[@]}" ]]; then + current_word="${COMP_WORDS[COMP_CWORD]}" + fi + fi + + default_func=$(_get_default_completion_function "$first_word") + if [[ -n "$default_func" ]]; then + "$default_func" + else + local file_completions + if [[ -z "$current_word" ]]; then + file_completions=$(compgen -f -- || true) + else + file_completions=$(compgen -f -- "$current_word" || true) + fi + if [[ -n "$file_completions" ]]; then + readarray -t COMPREPLY <<<"$file_completions" + fi + fi +} + +list_cache() { + local cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finish/cache"} + find "$cache_dir" -maxdepth 1 -type f -name "acsh-*" -printf '%T+ %p\n' | sort +} + +_finishtesh() { + _init_completion || return + _default_completion + if [[ ${#COMPREPLY[@]} -eq 0 && $COMP_TYPE -eq 63 ]]; then + local completions user_input user_input_hash + acsh_load_config + if [[ -z "$ACSH_ACTIVE_API_KEY" && ${ACSH_PROVIDER^^} != "OLLAMA" && ${ACSH_PROVIDER^^} != "LMSTUDIO" ]]; then + local provider_key="${ACSH_PROVIDER}_API_KEY" + provider_key=$(echo "$provider_key" | tr '[:lower:]' '[:upper:]') + echo_error "${provider_key} is not set. Please set it using: export ${provider_key}= or disable finish via: finish disable" + echo + return + fi + if [[ -n "${COMP_WORDS[*]}" ]]; then + command="${COMP_WORDS[0]}" + if [[ -n "$COMP_CWORD" && "$COMP_CWORD" -lt "${#COMP_WORDS[@]}" ]]; then + current="${COMP_WORDS[COMP_CWORD]}" + fi + fi + user_input="${COMP_LINE:-"$command $current"}" + user_input_hash=$(echo -n "$user_input" | md5sum | cut -d ' ' -f 1) + export ACSH_INPUT="$user_input" + export ACSH_PROMPT= + export ACSH_RESPONSE= + local cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finish/cache"} + local cache_size=${ACSH_CACHE_SIZE:-100} + local cache_file="$cache_dir/acsh-$user_input_hash.txt" + if [[ -d "$cache_dir" && "$cache_size" -gt 0 && -f "$cache_file" ]]; then + completions=$(cat "$cache_file" || true) + touch "$cache_file" + else + echo -en "\e]12;green\a" + completions=$(openai_completion "$user_input" || true) + if [[ -z "$completions" ]]; then + echo -en "\e]12;red\a" + sleep 1 + completions=$(openai_completion "$user_input" || true) + fi + echo -en "\e]12;white\a" + if [[ -d "$cache_dir" && "$cache_size" -gt 0 ]]; then + echo "$completions" > "$cache_file" + while [[ $(list_cache | wc -l) -gt "$cache_size" ]]; do + oldest=$(list_cache | head -n 1 | cut -d ' ' -f 2-) + rm "$oldest" || true + done + fi + fi + export ACSH_RESPONSE=$completions + if [[ -n "$completions" ]]; then + local num_rows + num_rows=$(echo "$completions" | wc -l) + COMPREPLY=() + if [[ $num_rows -eq 1 ]]; then + readarray -t COMPREPLY <<<"$(echo -n "${completions}" | sed "s/${command}[[:space:]]*//" | sed 's/:/\\:/g')" + else + completions=$(echo "$completions" | awk '{print NR". "$0}') + readarray -t COMPREPLY <<< "$completions" + fi + fi + if [[ ${#COMPREPLY[@]} -eq 0 ]]; then + COMPREPLY=("$current") + fi + fi +} + +############################################################################### +# CLI Commands & Configuration Management # +############################################################################### + +show_help() { + echo_green "finish.sh - LLM Powered Bash Completion" + echo "Usage: finish [options] command" + echo " finish [options] install|remove|config|model|enable|disable|clear|usage|system|command|--help" + echo + echo "finish.sh enhances bash completion with LLM capabilities." + echo "Press Tab twice for suggestions." + echo "Commands:" + echo " command Run finish (simulate double Tab)" + echo " command --dry-run Show prompt without executing" + echo " model Change language model" + echo " usage Display usage stats" + echo " system Display system information" + echo " config Show or set configuration values" + echo " config set Set a config value" + echo " config reset Reset config to defaults" + echo " install Install finish to .bashrc" + echo " remove Remove installation from .bashrc" + echo " enable Enable finish" + echo " disable Disable finish" + echo " clear Clear cache and log files" + echo " --help Show this help message" + echo + echo "Submit issues at: https://git.appmodel.nl/Tour/finish/issues" +} + +is_subshell() { + if [[ "$$" != "$BASHPID" ]]; then + return 0 + else + return 1 + fi +} + +show_config() { + local config_file="$HOME/.finish/config" term_width small_table + echo_green "finish.sh - Configuration and Settings - Version $ACSH_VERSION" + if is_subshell; then + echo " STATUS: Unknown. Run 'source finish config' to check status." + return + elif check_if_enabled; then + echo -e " STATUS: \033[32;5mEnabled\033[0m" + else + echo -e " STATUS: \033[31;5mDisabled\033[0m - Run 'source finish config' to verify." + fi + if [ ! -f "$config_file" ]; then + echo_error "Configuration file not found: $config_file. Run finish install." + return + fi + acsh_load_config + term_width=$(tput cols) + if [[ $term_width -gt 70 ]]; then + term_width=70; small_table=0 + fi + if [[ $term_width -lt 40 ]]; then + term_width=70; small_table=1 + fi + for config_var in $(compgen -v | grep ACSH_); do + if [[ $config_var == "ACSH_INPUT" || $config_var == "ACSH_PROMPT" || $config_var == "ACSH_RESPONSE" ]]; then + continue + fi + config_value="${!config_var}" + if [[ ${config_var: -8} == "_API_KEY" ]]; then + continue + fi + echo -en " $config_var:\e[90m" + if [[ $small_table -eq 1 ]]; then + echo -e "\n $config_value\e[0m" + else + printf '%s%*s' "" $((term_width - ${#config_var} - ${#config_value} - 3)) '' + echo -e "$config_value\e[0m" + fi + done + echo -e " ====================================================================" + for config_var in $(compgen -v | grep ACSH_); do + if [[ $config_var == "ACSH_INPUT" || $config_var == "ACSH_PROMPT" || $config_var == "ACSH_RESPONSE" ]]; then + continue + fi + if [[ ${config_var: -8} != "_API_KEY" ]]; then + continue + fi + echo -en " $config_var:\e[90m" + if [[ -z ${!config_var} ]]; then + config_value="UNSET" + echo -en "\e[31m" + else + rest=${!config_var:4} + config_value="${!config_var:0:4}...${rest: -4}" + echo -en "\e[32m" + fi + if [[ $small_table -eq 1 ]]; then + echo -e "\n $config_value\e[0m" + else + printf '%s%*s' "" $((term_width - ${#config_var} - ${#config_value} - 3)) '' + echo -e "$config_value\e[0m" + fi + done +} + +set_config() { + local key="$1" value="$2" config_file="$HOME/.finish/config" + key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + key=$(echo "$key" | tr '[:lower:]' '[:upper:]' | sed 's/[^A-Z0-9]/_/g') + if [ -z "$key" ]; then + echo_error "SyntaxError: expected 'finish config set '" + return + fi + if [ ! -f "$config_file" ]; then + echo_error "Configuration file not found: $config_file. Run finishte install." + return + fi + sed -i "s|^\($key:\).*|\1 $value|" "$config_file" + acsh_load_config +} + +config_command() { + local command config_file="$HOME/.finishte/config" + command="${*:2}" + if [ -z "$command" ]; then + show_config + return + fi + if [ "$2" == "set" ]; then + local key="$3" value="$4" + echo "Setting configuration key '$key' to '$value'" + set_config "$key" "$value" + echo_green "Configuration updated. Run 'finishte config' to view changes." + return + fi + if [[ "$command" == "reset" ]]; then + echo "Resetting configuration to default values." + rm "$config_file" || true + build_config + return + fi + echo_error "SyntaxError: expected 'finishte config set ' or 'finishte config reset'" +} + +build_config() { + local config_file="$HOME/.finishte/config" default_config + if [ ! -f "$config_file" ]; then + echo "Creating default configuration file at ~/.finishte/config" + default_config="# ~/.finishte/config + +# Model configuration +provider: lmstudio +model: darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2 +temperature: 0.0 +endpoint: http://localhost:1234/v1/chat/completions +api_prompt_cost: 0.000000 +api_completion_cost: 0.000000 + +# Max history and recent files +max_history_commands: 20 +max_recent_files: 20 + +# Cache settings +cache_dir: $HOME/.finishte/cache +cache_size: 10 + +# Logging settings +log_file: $HOME/.finishte/finishte.log" + echo "$default_config" > "$config_file" + fi +} + +acsh_load_config() { + local config_file="$HOME/.finishte/config" key value + if [ -f "$config_file" ]; then + while IFS=':' read -r key value; do + if [[ $key =~ ^# ]] || [[ -z $key ]]; then + continue + fi + key=$(echo "$key" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + value=$(echo "$value" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + key=$(echo "$key" | tr '[:lower:]' '[:upper:]' | sed 's/[^A-Z0-9]/_/g') + if [[ -n $value ]]; then + export "ACSH_$key"="$value" + fi + done < "$config_file" + if [[ -z "$ACSH_OLLAMA_API_KEY" && -n "$LLM_API_KEY" ]]; then + export ACSH_OLLAMA_API_KEY="$LLM_API_KEY" + fi + # If the custom API key was set, map it to OLLAMA if needed. + if [[ -z "$ACSH_OLLAMA_API_KEY" && -n "$ACSH_CUSTOM_API_KEY" ]]; then + export ACSH_OLLAMA_API_KEY="$ACSH_CUSTOM_API_KEY" + fi + case "${ACSH_PROVIDER:-lmstudio}" in + "ollama") export ACSH_ACTIVE_API_KEY="$ACSH_OLLAMA_API_KEY" ;; + "lmstudio") export ACSH_ACTIVE_API_KEY="" ;; + *) echo_error "Unknown provider: $ACSH_PROVIDER" ;; + esac + else + echo "Configuration file not found: $config_file" + fi +} + +install_command() { + local bashrc_file="$HOME/.bashrc" finishte_setup="source finishte enable" finishte_cli_setup="complete -F _finishtesh_cli finishte" + if ! command -v finishte &>/dev/null; then + echo_error "finishte.sh not in PATH. Follow install instructions at https://git.appmodel.nl/Tour/finish" + return + fi + if [[ ! -d "$HOME/.finishte" ]]; then + echo "Creating ~/.finishte directory" + mkdir -p "$HOME/.finishte" + fi + local cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finishte/cache"} + if [[ ! -d "$cache_dir" ]]; then + mkdir -p "$cache_dir" + fi + build_config + acsh_load_config + if ! grep -qF "$finishte_setup" "$bashrc_file"; then + echo -e "# finishte.sh" >> "$bashrc_file" + echo -e "$finishte_setup\n" >> "$bashrc_file" + echo "Added finishte.sh setup to $bashrc_file" + else + echo "finishte.sh setup already exists in $bashrc_file" + fi + if ! grep -qF "$finishte_cli_setup" "$bashrc_file"; then + echo -e "# finishte.sh CLI" >> "$bashrc_file" + echo -e "$finishte_cli_setup\n" >> "$bashrc_file" + echo "Added finishte CLI completion to $bashrc_file" + fi + echo + echo_green "finishte.sh - Version $ACSH_VERSION installation complete." + echo -e "Run: source $bashrc_file to enable finishte." + echo -e "Then run: finishte model to select a language model." +} + +remove_command() { + local config_file="$HOME/.finishte/config" cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finishte/cache"} log_file=${ACSH_LOG_FILE:-"$HOME/.finishte/finishte.log"} bashrc_file="$HOME/.bashrc" + echo_green "Removing finishte.sh installation..." + [ -f "$config_file" ] && { rm "$config_file"; echo "Removed: $config_file"; } + [ -d "$cache_dir" ] && { rm -rf "$cache_dir"; echo "Removed: $cache_dir"; } + [ -f "$log_file" ] && { rm "$log_file"; echo "Removed: $log_file"; } + if [ -d "$HOME/.finishte" ]; then + if [ -z "$(ls -A "$HOME/.finishte")" ]; then + rmdir "$HOME/.finishte" + echo "Removed: $HOME/.finishte" + else + echo "Skipped removing $HOME/.finishte (not empty)" + fi + fi + if [ -f "$bashrc_file" ]; then + if grep -qF "source finishte enable" "$bashrc_file"; then + sed -i '/# finishte.sh/d' "$bashrc_file" + sed -i '/finishte/d' "$bashrc_file" + echo "Removed finishte.sh setup from $bashrc_file" + fi + fi + local finishte_script + finishte_script=$(command -v finishte) + if [ -n "$finishte_script" ]; then + echo "finishte script is at: $finishte_script" + if [ "$1" == "-y" ]; then + rm "$finishte_script" + echo "Removed: $finishte_script" + else + read -r -p "Remove the finishte script? (y/n): " confirm + if [[ $confirm == "y" ]]; then + rm "$finishte_script" + echo "Removed: $finishte_script" + fi + fi + fi + echo "Uninstallation complete." +} + +check_if_enabled() { + local is_enabled + is_enabled=$(complete -p | grep _finishtesh | grep -cv _finishtesh_cli) + (( is_enabled > 0 )) && return 0 || return 1 +} + +_finishtesh_cli() { + if [[ -n "${COMP_WORDS[*]}" ]]; then + command="${COMP_WORDS[0]}" + if [[ -n "$COMP_CWORD" && "$COMP_CWORD" -lt "${#COMP_WORDS[@]}" ]]; then + current="${COMP_WORDS[COMP_CWORD]}" + fi + fi + if [[ $current == "config" ]]; then + readarray -t COMPREPLY <<< "set +reset" + return + elif [[ $current == "command" ]]; then + readarray -t COMPREPLY <<< "command --dry-run" + return + fi + if [[ -z "$current" ]]; then + readarray -t COMPREPLY <<< "install +remove +config +enable +disable +clear +usage +system +command +model +--help" + fi +} + +enable_command() { + if check_if_enabled; then + echo_green "Reloading finishte.sh..." + disable_command + fi + acsh_load_config + complete -D -E -F _finishtesh -o nospace +} + +disable_command() { + if check_if_enabled; then + complete -F _completion_loader -D + fi +} + +command_command() { + local args=("$@") + for ((i = 0; i < ${#args[@]}; i++)); do + if [ "${args[i]}" == "--dry-run" ]; then + args[i]="" + _build_prompt "${args[@]}" + return + fi + done + openai_completion "$@" || true + echo +} + +clear_command() { + local cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finishte/cache"} log_file=${ACSH_LOG_FILE:-"$HOME/.finishte/finishte.log"} + echo "This will clear the cache and log file." + echo -e "Cache directory: \e[31m$cache_dir\e[0m" + echo -e "Log file: \e[31m$log_file\e[0m" + read -r -p "Are you sure? (y/n): " confirm + if [[ $confirm != "y" ]]; then + echo "Aborted." + return + fi + if [ -d "$cache_dir" ]; then + local cache_files + cache_files=$(list_cache) + if [ -n "$cache_files" ]; then + while read -r line; do + file=$(echo "$line" | cut -d ' ' -f 2-) + rm "$file" + echo "Removed: $file" + done <<< "$cache_files" + echo "Cleared cache in: $cache_dir" + else + echo "Cache is empty." + fi + fi + [ -f "$log_file" ] && { rm "$log_file"; echo "Removed: $log_file"; } +} + +usage_command() { + local log_file=${ACSH_LOG_FILE:-"$HOME/.finishte/finishte.log"} cache_dir=${ACSH_CACHE_DIR:-"$HOME/.finishte/cache"} + local cache_size number_of_lines api_cost avg_api_cost + cache_size=$(list_cache | wc -l) + echo_green "finishte.sh - Usage Information" + echo + echo -n "Log file: "; echo -e "\e[90m$log_file\e[0m" + if [ ! -f "$log_file" ]; then + number_of_lines=0 + api_cost=0 + avg_api_cost=0 + else + number_of_lines=$(wc -l < "$log_file") + api_cost=$(awk -F, '{sum += $5} END {print sum}' "$log_file") + avg_api_cost=$(echo "$api_cost / $number_of_lines" | bc -l) + fi + echo + echo -e "\tUsage count:\t\e[32m$number_of_lines\e[0m" + echo -e "\tAvg Cost:\t\$$(printf "%.4f" "$avg_api_cost")" + echo -e "\tTotal Cost:\t\e[31m\$$(printf "%.4f" "$api_cost")\e[0m" + echo + echo -n "Cache Size: $cache_size of ${ACSH_CACHE_SIZE:-10} in "; echo -e "\e[90m$cache_dir\e[0m" + echo "To clear log and cache, run: finishte clear" +} + +############################################################################### +# Enhanced Interactive Menu UX # +############################################################################### + +get_key() { + IFS= read -rsn1 key 2>/dev/null >&2 + if [[ $key == $'\x1b' ]]; then + read -rsn2 key + if [[ $key == [A ]]; then echo up; fi + if [[ $key == [B ]]; then echo down; fi + if [[ $key == q ]]; then echo q; fi + elif [[ $key == "q" ]]; then + echo q + else + echo "$key" + fi +} + +menu_selector() { + options=("$@") + selected=0 + show_menu() { + echo + echo "Select a Language Model (Up/Down arrows, Enter to select, 'q' to quit):" + for i in "${!options[@]}"; do + if [[ $i -eq $selected ]]; then + echo -e "\e[1;32m> ${options[i]}\e[0m" + else + echo " ${options[i]}" + fi + done + } + tput sc + while true; do + tput rc; tput ed + show_menu + key=$(get_key) + case $key in + up) + ((selected--)) + if ((selected < 0)); then + selected=$((${#options[@]} - 1)) + fi + ;; + down) + ((selected++)) + if ((selected >= ${#options[@]})); then + selected=0 + fi + ;; + q) + echo "Selection canceled." + return 1 + ;; + "") + break + ;; + esac + done + clear + return $selected +} + +model_command() { + clear + local selected_model options=() + if [[ $# -ne 3 ]]; then + mapfile -t sorted_keys < <(for key in "${!_finishte_modellist[@]}"; do echo "$key"; done | sort) + for key in "${sorted_keys[@]}"; do + options+=("$key") + done + echo -e "\e[1;32mfinishte.sh - Model Configuration\e[0m" + menu_selector "${options[@]}" + selected_option=$? + if [[ $selected_option -eq 1 ]]; then + return + fi + selected_model="${options[selected_option]}" + selected_value="${_finishte_modellist[$selected_model]}" + else + provider="$2" + model_name="$3" + selected_value="${_finishte_modellist["$provider: $model_name"]}" + if [[ -z "$selected_value" ]]; then + echo "ERROR: Invalid provider or model name." + return 1 + fi + fi + set_config "model" "$(echo "$selected_value" | jq -r '.model')" + set_config "endpoint" "$(echo "$selected_value" | jq -r '.endpoint')" + set_config "provider" "$(echo "$selected_value" | jq -r '.provider')" + prompt_cost=$(echo "$selected_value" | jq -r '.prompt_cost' | awk '{printf "%.8f", $1}') + completion_cost=$(echo "$selected_value" | jq -r '.completion_cost' | awk '{printf "%.8f", $1}') + set_config "api_prompt_cost" "$prompt_cost" + set_config "api_completion_cost" "$completion_cost" + model="${ACSH_MODEL:-ERROR}" + temperature=$(echo "${ACSH_TEMPERATURE:-0.0}" | awk '{printf "%.3f", $1}') + echo -e "Provider:\t\e[90m$ACSH_PROVIDER\e[0m" + echo -e "Model:\t\t\e[90m$model\e[0m" + echo -e "Temperature:\t\e[90m$temperature\e[0m" + echo + echo -e "Cost/token:\t\e[90mprompt: \$$ACSH_API_PROMPT_COST, completion: \$$ACSH_API_COMPLETION_COST\e[0m" + echo -e "Endpoint:\t\e[90m$ACSH_ENDPOINT\e[0m" + if [[ ${ACSH_PROVIDER^^} == "OLLAMA" || ${ACSH_PROVIDER^^} == "LMSTUDIO" ]]; then + echo "To set a custom endpoint:" + echo -e "\t\e[34mfinishte config set endpoint \e[0m" + echo "Other models can be set with:" + echo -e "\t\e[34mfinishte config set model \e[0m" + fi + echo "To change temperature:" + echo -e "\t\e[90mfinishte config set temperature \e[0m" + echo +} + +############################################################################### +# CLI ENTRY POINT # +############################################################################### + +case "$1" in + "--help") + show_help + ;; + system) + _system_info + ;; + install) + install_command + ;; + remove) + remove_command "$@" + ;; + clear) + clear_command + ;; + usage) + usage_command + ;; + model) + model_command "$@" + ;; + config) + config_command "$@" + ;; + enable) + enable_command + ;; + disable) + disable_command + ;; + command) + command_command "$@" + ;; + *) + if [[ -n "$1" ]]; then + echo_error "Unknown command $1 - run 'finishte --help' for usage or visit https://finishte.sh" + else + echo_green "finishte.sh - LLM Powered Bash Completion - Version $ACSH_VERSION - https://finishte.sh" + fi + ;; +esac diff --git a/homebrew/finish.rb b/homebrew/finish.rb new file mode 100644 index 0000000..4ddf728 --- /dev/null +++ b/homebrew/finish.rb @@ -0,0 +1,47 @@ +class finishteSh < Formula + desc "LLM-powered command-line finishtion" + homepage "https://git.appmodel.nl/Tour/finishte" + url "https://git.appmodel.nl/Tour/finish/archive/v0.5.0.tar.gz" + sha256 "" # Update with actual SHA256 of the release tarball + license "BSD-2-Clause" + version "0.5.0" + + depends_on "jq" + depends_on "bash" => :optional + depends_on "bash-completion@2" => :optional + + def install + bin.install "finishte.sh" => "finishte" + doc.install "README.md" + doc.install "LICENSE" + end + + def caveats + <<~EOS + To complete the installation: + + 1. Run the installer: + finishte install + + 2. Reload your shell: + source ~/.bashrc # for bash + source ~/.zshrc # for zsh + + 3. Select a language model: + finishte model + + You'll need LM-Studio or Ollama running locally to use finishte.sh. + + LM-Studio: https://lmstudio.ai + Ollama: https://ollama.ai + + For more information, visit: + https:/git.appmodel.nl/Tour/finishte + EOS + end + + test do + system "#{bin}/finishte", "--help" + assert_match "finishte.sh", shell_output("#{bin}/finishte --help") + end +end diff --git a/run_tests.sh b/run_tests.sh new file mode 100644 index 0000000..192218a --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +bats tests/ diff --git a/tests/test_finish.bats b/tests/test_finish.bats new file mode 100644 index 0000000..74a3230 --- /dev/null +++ b/tests/test_finish.bats @@ -0,0 +1,43 @@ +#!/usr/bin/env bats + +setup() { + # Install finishte.sh and run testing against the main branch + wget -qO- https://git.appmodel.nl/Tour/finish/raw/branch/main/docs/install.sh | bash -s -- main + + # Source bashrc to make sure finishte is available in the current session + source ~/.bashrc + + # Configure for local LM-Studio + finishte config set provider lmstudio + finishte config set endpoint http://localhost:1234/v1/chat/completions + finishte config set model darkidol-llama-3.1-8b-instruct-1.3-uncensored_gguf:2 +} + +teardown() { + # Remove finishte.sh installation + finishte remove -y +} + +@test "which finishte returns something" { + run which finishte + [ "$status" -eq 0 ] + [ -n "$output" ] +} + +@test "finishte returns a string containing finishte.sh (case insensitive)" { + run finishte + [ "$status" -eq 0 ] + [[ "$output" =~ [Aa]utocomplete\.sh ]] +} + +@test "finishte config should have lmstudio provider" { + run finishte config + [ "$status" -eq 0 ] + [[ "$output" =~ lmstudio ]] +} + +@test "finishte command 'ls # show largest files' should return something" { + run finishte command "ls # show largest files" + [ "$status" -eq 0 ] + [ -n "$output" ] +}