commit 24d36cbad4d7c1bb5e1c927ab43664f37169d9c4 Author: michael1986 Date: Tue Dec 2 09:02:03 2025 +0100 first commit 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" ] +}