diff --git a/.github/scripts/generate-srcinfo.sh b/.github/scripts/generate-srcinfo.sh new file mode 100755 index 0000000..ad9a394 --- /dev/null +++ b/.github/scripts/generate-srcinfo.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Generate .SRCINFO from a PKGBUILD file. +# Usage: generate-srcinfo.sh +# +# Sources the PKGBUILD in a restricted subshell and outputs .SRCINFO format. + +set -uo pipefail + +if [ $# -lt 1 ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +pkgbuild="$1" + +if [ ! -f "$pkgbuild" ]; then + echo "Error: $pkgbuild not found" >&2 + exit 1 +fi + +( + # Override functions that might cause side effects + build() { :; } + package() { :; } + prepare() { :; } + check() { :; } + + source "$pkgbuild" + + # Determine pkgbase (defaults to first pkgname) + base="${pkgbase:-${pkgname[0]:-${pkgname:-}}}" + + # pkgbase section + echo "pkgbase = ${base}" + [ -n "${pkgdesc:-}" ] && printf '\tpkgdesc = %s\n' "$pkgdesc" + [ -n "${pkgver:-}" ] && printf '\tpkgver = %s\n' "$pkgver" + [ -n "${pkgrel:-}" ] && printf '\tpkgrel = %s\n' "$pkgrel" + [ -n "${url:-}" ] && printf '\turl = %s\n' "$url" + + # Array fields + for v in "${arch[@]}"; do printf '\tarch = %s\n' "$v"; done 2>/dev/null || true + for v in "${license[@]}"; do printf '\tlicense = %s\n' "$v"; done 2>/dev/null || true + for v in "${depends[@]}"; do printf '\tdepends = %s\n' "$v"; done 2>/dev/null || true + for v in "${makedepends[@]}"; do printf '\tmakedepends = %s\n' "$v"; done 2>/dev/null || true + for v in "${checkdepends[@]}"; do printf '\tcheckdepends = %s\n' "$v"; done 2>/dev/null || true + for v in "${optdepends[@]}"; do printf '\toptdepends = %s\n' "$v"; done 2>/dev/null || true + for v in "${provides[@]}"; do printf '\tprovides = %s\n' "$v"; done 2>/dev/null || true + for v in "${conflicts[@]}"; do printf '\tconflicts = %s\n' "$v"; done 2>/dev/null || true + for v in "${replaces[@]}"; do printf '\treplaces = %s\n' "$v"; done 2>/dev/null || true + for v in "${groups[@]}"; do printf '\tgroups = %s\n' "$v"; done 2>/dev/null || true + for v in "${options[@]}"; do printf '\toptions = %s\n' "$v"; done 2>/dev/null || true + for v in "${source[@]}"; do printf '\tsource = %s\n' "$v"; done 2>/dev/null || true + for v in "${sha256sums[@]}"; do printf '\tsha256sums = %s\n' "$v"; done 2>/dev/null || true + for v in "${sha512sums[@]}"; do printf '\tsha512sums = %s\n' "$v"; done 2>/dev/null || true + for v in "${md5sums[@]}"; do printf '\tmd5sums = %s\n' "$v"; done 2>/dev/null || true + + # Architecture-specific arrays + for v in "${source_x86_64[@]}"; do printf '\tsource_x86_64 = %s\n' "$v"; done 2>/dev/null || true + for v in "${source_aarch64[@]}"; do printf '\tsource_aarch64 = %s\n' "$v"; done 2>/dev/null || true + for v in "${sha256sums_x86_64[@]}"; do printf '\tsha256sums_x86_64 = %s\n' "$v"; done 2>/dev/null || true + for v in "${sha256sums_aarch64[@]}"; do printf '\tsha256sums_aarch64 = %s\n' "$v"; done 2>/dev/null || true + for v in "${sha512sums_x86_64[@]}"; do printf '\tsha512sums_x86_64 = %s\n' "$v"; done 2>/dev/null || true + for v in "${sha512sums_aarch64[@]}"; do printf '\tsha512sums_aarch64 = %s\n' "$v"; done 2>/dev/null || true + + echo "" + + # pkgname sections + if declare -p pkgname 2>/dev/null | grep -q 'declare -a'; then + for name in "${pkgname[@]}"; do + echo "pkgname = ${name}" + done + else + echo "pkgname = ${pkgname}" + fi +) 2>/dev/null diff --git a/.github/workflows/publish-aur.yml b/.github/workflows/publish-aur.yml new file mode 100644 index 0000000..50a45f9 --- /dev/null +++ b/.github/workflows/publish-aur.yml @@ -0,0 +1,93 @@ +name: Publish to AUR + +on: + release: + types: [published] + +jobs: + publish-aur: + name: Publish ${{ matrix.package }} to AUR + runs-on: ubuntu-latest + strategy: + matrix: + include: + - package: codeinput-bin + subdir: codeinput-bin + - package: codeinput + subdir: codeinput + steps: + - name: Checkout Source + uses: actions/checkout@v4 + + - name: Get version + id: version + run: | + tag="${GITHUB_REF#refs/tags/}" + version="${tag#v}" + echo "tag=${tag}" >> $GITHUB_OUTPUT + echo "version=${version}" >> $GITHUB_OUTPUT + + - name: Compute binary checksums + if: matrix.package == 'codeinput-bin' + id: checksums + run: | + curl -sL "https://github.com/code-input/cli/releases/download/${{ steps.version.outputs.tag }}/ci-linux-x86_64" -o ci-linux-x86_64 + echo "sha256_x86_64=$(sha256sum ci-linux-x86_64 | cut -d' ' -f1)" >> $GITHUB_OUTPUT + curl -sL "https://github.com/code-input/cli/releases/download/${{ steps.version.outputs.tag }}/ci-linux-aarch64" -o ci-linux-aarch64 + echo "sha256_aarch64=$(sha256sum ci-linux-aarch64 | cut -d' ' -f1)" >> $GITHUB_OUTPUT + + - name: Compute source checksum + if: matrix.package == 'codeinput' + id: src_checksum + run: | + curl -sL "https://github.com/code-input/cli/archive/refs/tags/${{ steps.version.outputs.tag }}.tar.gz" -o source.tar.gz + echo "sha256=$(sha256sum source.tar.gz | cut -d' ' -f1)" >> $GITHUB_OUTPUT + + - name: Update PKGBUILD + working-directory: dist/aur/${{ matrix.subdir }} + run: | + sed -i "s/^pkgver=.*/pkgver=${{ steps.version.outputs.version }}/" PKGBUILD + sed -i "s/^pkgrel=.*/pkgrel=1/" PKGBUILD + + if [ "${{ matrix.package }}" = "codeinput-bin" ]; then + sed -i "s/^sha256sums_x86_64=.*/sha256sums_x86_64=('${{ steps.checksums.outputs.sha256_x86_64 }}')/" PKGBUILD + sed -i "s/^sha256sums_aarch64=.*/sha256sums_aarch64=('${{ steps.checksums.outputs.sha256_aarch64 }}')/" PKGBUILD + else + sed -i "s/^sha256sums=.*/sha256sums=('${{ steps.src_checksum.outputs.sha256 }}')/" PKGBUILD + fi + + - name: Generate .SRCINFO + working-directory: dist/aur/${{ matrix.subdir }} + run: | + bash "${{ github.workspace }}/.github/scripts/generate-srcinfo.sh" PKGBUILD > .SRCINFO + + - name: Publish to AUR + env: + AUR_USERNAME: ${{ secrets.AUR_USERNAME }} + AUR_EMAIL: ${{ secrets.AUR_EMAIL }} + AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + PACKAGE: ${{ matrix.package }} + SUBDIR: ${{ matrix.subdir }} + run: | + # Setup SSH + mkdir -p ~/.ssh + echo "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur + chmod 600 ~/.ssh/aur + cat >> ~/.ssh/config <> $GITHUB_OUTPUT + echo "version=${version}" >> $GITHUB_OUTPUT + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Update version in packaging files + working-directory: dist/pypi + run: | + sed -i "s/^version = \".*\"/version = \"${{ steps.version.outputs.version }}\"/" pyproject.toml + + - name: Copy README + run: cp README.md dist/pypi/README.md + + - name: Build package + working-directory: dist/pypi + run: | + python -m pip install --upgrade build + python -m build + + - name: Publish to PyPI + working-directory: dist/pypi + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + python -m pip install --upgrade twine + twine upload dist/* diff --git a/.github/workflows/release-lsp.yml b/.github/workflows/release-lsp.yml index e087de1..d93cc42 100644 --- a/.github/workflows/release-lsp.yml +++ b/.github/workflows/release-lsp.yml @@ -1,14 +1,32 @@ name: Release LSP on: - push: - tags: - - '*' + workflow_dispatch: + workflow_run: + workflows: ["Release"] + types: + - completed jobs: + get-tag: + name: Get Release Tag + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + outputs: + tag: ${{ steps.get-tag.outputs.tag }} + steps: + - name: Get tag from latest release + id: get-tag + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TAG=$(gh api repos/${{ github.repository }}/releases/latest --jq '.tag_name') + echo "tag=$TAG" >> $GITHUB_OUTPUT + release: name: Release LSP - ${{ matrix.os }} (${{ matrix.arch }}) runs-on: ${{ matrix.runner }} + needs: get-tag strategy: matrix: include: @@ -52,13 +70,8 @@ jobs: steps: - name: Checkout Source uses: actions/checkout@v4 - - - name: Set variables - id: vars - shell: bash - run: | - echo "package_name=$(sed -En 's/name[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.toml | head -1)" >> $GITHUB_OUTPUT - echo "package_version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + with: + ref: ${{ needs.get-tag.outputs.tag }} - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable @@ -97,104 +110,47 @@ jobs: name: ${{ matrix.asset_name }} path: target/${{ matrix.target }}/release/${{ matrix.binary_name }} - create-release: - name: Create LSP Release + upload-lsp-binaries: + name: Upload LSP Binaries runs-on: ubuntu-latest - needs: release + needs: [get-tag, release] + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - name: Checkout Source - uses: actions/checkout@v4 - - - name: Set variables - id: vars - run: | - echo "package_name=$(sed -En 's/name[[:space:]]*=[[:space:]]*"([^"]+)"/\1/p' Cargo.toml | head -1)" >> $GITHUB_OUTPUT - echo "package_version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - - name: Download all artifacts uses: actions/download-artifact@v4 with: path: artifacts - - name: Remove Same Release - uses: omarabid-forks/action-rollback@stable - continue-on-error: true - with: - tag: ${{ steps.vars.outputs.package_version }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Create Release - id: create-release - uses: actions/create-release@latest - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ steps.vars.outputs.package_version }} - release_name: LSP Server ${{ steps.vars.outputs.package_version }} - body: ${{ steps.vars.outputs.package_name }} LSP Server - ${{ steps.vars.outputs.package_version }} - draft: false - prerelease: false - - name: Upload Linux x64 binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create-release.outputs.upload_url }} - asset_path: artifacts/ci-lsp-linux-x64/ci - asset_name: ci-lsp-linux-x64 - asset_content_type: application/octet-stream + run: | + cp artifacts/ci-lsp-linux-x64/ci ci-lsp-linux-x64 + gh release upload ${{ needs.get-tag.outputs.tag }} ci-lsp-linux-x64 --repo ${{ github.repository }} - name: Upload Linux arm64 binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create-release.outputs.upload_url }} - asset_path: artifacts/ci-lsp-linux-arm64/ci - asset_name: ci-lsp-linux-arm64 - asset_content_type: application/octet-stream + run: | + cp artifacts/ci-lsp-linux-arm64/ci ci-lsp-linux-arm64 + gh release upload ${{ needs.get-tag.outputs.tag }} ci-lsp-linux-arm64 --repo ${{ github.repository }} - name: Upload Windows x64 binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create-release.outputs.upload_url }} - asset_path: artifacts/ci-lsp-windows-x64.exe/ci.exe - asset_name: ci-lsp-windows-x64.exe - asset_content_type: application/octet-stream + run: | + cp artifacts/ci-lsp-windows-x64.exe/ci.exe ci-lsp-windows-x64.exe + gh release upload ${{ needs.get-tag.outputs.tag }} ci-lsp-windows-x64.exe --repo ${{ github.repository }} - name: Upload Windows arm64 binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create-release.outputs.upload_url }} - asset_path: artifacts/ci-lsp-windows-arm64.exe/ci.exe - asset_name: ci-lsp-windows-arm64.exe - asset_content_type: application/octet-stream + run: | + cp artifacts/ci-lsp-windows-arm64.exe/ci.exe ci-lsp-windows-arm64.exe + gh release upload ${{ needs.get-tag.outputs.tag }} ci-lsp-windows-arm64.exe --repo ${{ github.repository }} - name: Upload macOS x64 binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create-release.outputs.upload_url }} - asset_path: artifacts/ci-lsp-darwin-x64/ci - asset_name: ci-lsp-darwin-x64 - asset_content_type: application/octet-stream + run: | + cp artifacts/ci-lsp-darwin-x64/ci ci-lsp-darwin-x64 + gh release upload ${{ needs.get-tag.outputs.tag }} ci-lsp-darwin-x64 --repo ${{ github.repository }} - name: Upload macOS arm64 binary - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create-release.outputs.upload_url }} - asset_path: artifacts/ci-lsp-darwin-arm64/ci - asset_name: ci-lsp-darwin-arm64 - asset_content_type: application/octet-stream + run: | + cp artifacts/ci-lsp-darwin-arm64/ci ci-lsp-darwin-arm64 + gh release upload ${{ needs.get-tag.outputs.tag }} ci-lsp-darwin-arm64 --repo ${{ github.repository }} - name: Purge artifacts uses: omarabid-forks/purge-artifacts@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d319644..f90cb36 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,25 +116,85 @@ jobs: with: path: artifacts - - name: Create Release and Upload Assets + - name: Remove Same Release + uses: omarabid-forks/action-rollback@stable + continue-on-error: true + with: + tag: ${{ steps.vars.outputs.package_version }} env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG: ${{ steps.vars.outputs.package_version }} - PACKAGE_NAME: ${{ steps.vars.outputs.package_name }} - run: | - # Delete existing release if exists - gh release delete "$TAG" --yes || true - - # Create release - gh release create "$TAG" \ - --title "Version $TAG" \ - --notes "$PACKAGE_NAME - $TAG" \ - artifacts/ci-linux-x86_64/ci \ - artifacts/ci-linux-aarch64/ci \ - artifacts/ci-windows-x86_64.exe/ci.exe \ - artifacts/ci-windows-aarch64.exe/ci.exe \ - artifacts/ci-macos-x86_64/ci \ - artifacts/ci-macos-aarch64/ci + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create Release + id: create-release + uses: actions/create-release@latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.vars.outputs.package_version }} + release_name: ${{ steps.vars.outputs.package_name }} ${{ steps.vars.outputs.package_version }} + body: ${{ steps.vars.outputs.package_name }} - ${{ steps.vars.outputs.package_version }} + draft: false + prerelease: false + + - name: Upload Linux x86_64 binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: artifacts/ci-linux-x86_64/ci + asset_name: ci-linux-x86_64 + asset_content_type: application/octet-stream + + - name: Upload Linux aarch64 binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: artifacts/ci-linux-aarch64/ci + asset_name: ci-linux-aarch64 + asset_content_type: application/octet-stream + + - name: Upload Windows x86_64 binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: artifacts/ci-windows-x86_64.exe/ci.exe + asset_name: ci-windows-x86_64.exe + asset_content_type: application/octet-stream + + - name: Upload Windows aarch64 binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: artifacts/ci-windows-aarch64.exe/ci.exe + asset_name: ci-windows-aarch64.exe + asset_content_type: application/octet-stream + + - name: Upload macOS x86_64 binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: artifacts/ci-macos-x86_64/ci + asset_name: ci-macos-x86_64 + asset_content_type: application/octet-stream + + - name: Upload macOS aarch64 binary + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create-release.outputs.upload_url }} + asset_path: artifacts/ci-macos-aarch64/ci + asset_name: ci-macos-aarch64 + asset_content_type: application/octet-stream - name: Purge artifacts uses: omarabid-forks/purge-artifacts@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 120e748..fc98f17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,28 @@ All notable changes to this project will be documented in this file. -## [0.0.5] - 2025-03-02 +## [0.1.0] - 2026-03-07 ### Added + +- **VS Code Extension**: Companion extension for Visual Studio Code with LSP integration +- **Neovim Plugin**: Full Neovim support with telescope integration for browsing files by ownership +- **Vim Plugin**: Vim plugin for CODEOWNERS LSP integration +- **LSP Enhancements**: + - Inlay hints support for CODEOWNERS patterns + - Custom commands: `listFiles`, `listOwners`, `listTags`, `getFileOwnership` + - TCP transport support (`ci lsp --port `) + +### Changed + +- Refactored LSP server into modular structure for better maintainability +- Simplified VS Code extension by removing unused features +- Improved telescope file pickers with proper entry makers + +## [0.0.5] - 2026-03-02 + +### Added + - **LSP Server**: Language Server Protocol implementation for IDE integration - `textDocument/hover`: Shows owners and tags when hovering over files - `textDocument/codeLens`: Displays ownership annotations above files @@ -14,20 +33,24 @@ All notable changes to this project will be documented in this file. - **VS Code Extension**: Companion extension for Visual Studio Code (in `vscode-extension/`) ### Changed + - Upgraded `utoipa` to 5.4.0 with schema fixes - LSP feature is now opt-in via `--features lsp` cargo flag ### Fixed + - Safe serialization of CodeLens arguments - Output redirected to stderr for LSP compatibility -## [0.0.4] - 2024-12-XX +## [0.0.4] - 2025-12-XX ### Added + - `infer-owners` command for intelligent owner inference - ARM64 runners for release builds (Linux, Windows, macOS) ### Changed + - Dependency updates and formatting improvements ## [0.0.3] - Previous Release diff --git a/Cargo.toml b/Cargo.toml index 2d8a428..29b3fc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ tabled = "0.20.0" terminal_size = "0.4.3" clap = { version = "4.5.60", features = ["cargo", "derive"] } chrono = { version = "0.4.44", features = ["serde"] } -tower-lsp = "0.20" +tower-lsp-server = "0.23.0" tokio = { version = "1.49.0", features = ["full"] } url = "2.5.8" diff --git a/README.md b/README.md index 26f6dc6..5a3a240 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@
-[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/CodeInputCorp/cli/blob/master/LICENSE) -[![Tests](https://github.com/CodeInputCorp/cli/actions/workflows/tests.yml/badge.svg)](https://github.com/CodeInputCorp/cli/actions/workflows/tests.yml) -[![Build](https://github.com/CodeInputCorp/cli/actions/workflows/build.yml/badge.svg)](https://github.com/CodeInputCorp/cli/actions/workflows/build.yml) +[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/code-input/cli/blob/master/LICENSE) +[![Tests](https://github.com/code-input/cli/actions/workflows/tests.yml/badge.svg)](https://github.com/code-input/cli/actions/workflows/tests.yml) +[![Build](https://github.com/code-input/cli/actions/workflows/build.yml/badge.svg)](https://github.com/code-input/cli/actions/workflows/build.yml) `codeinput` is a CLI tool for parsing, analyzing, and managing CODEOWNERS files across your repository. It provides advanced querying capabilities, ownership analysis, and tag-based file organization. @@ -23,6 +23,7 @@ - [Installation Instructions](#installation-instructions) - [From Cargo](#from-cargo) - [From Source](#from-source) + - [Arch Linux (AUR)](#arch-linux-aur) - [From NPM](#from-npm) - [Quick Start](#quick-start) - [Commands](#commands) @@ -59,13 +60,13 @@ ### Pre-built Binaries -**Latest Release: `0.0.4`** +**Latest Release: `0.1.0`** -- **Linux x86_64**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.0.4/ci-linux-x86_64) -- **Linux ARM64**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.0.4/ci-linux-aarch64) -- **Windows x86_64**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.0.4/ci-windows-x86_64.exe) -- **macOS Intel**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.0.4/ci-macos-x86_64) -- **macOS Apple Silicon**: [Download](https://github.com/CodeInputCorp/cli/releases/download/v0.0.4/ci-macos-aarch64) +- **Linux x86_64**: [Download](https://github.com/code-input/cli/releases/download/v0.1.0/ci-linux-x86_64) +- **Linux ARM64**: [Download](https://github.com/code-input/cli/releases/download/v0.1.0/ci-linux-aarch64) +- **Windows x86_64**: [Download](https://github.com/code-input/cli/releases/download/v0.1.0/ci-windows-x86_64.exe) +- **macOS Intel**: [Download](https://github.com/code-input/cli/releases/download/v0.1.0/ci-macos-x86_64) +- **macOS Apple Silicon**: [Download](https://github.com/code-input/cli/releases/download/v0.1.0/ci-macos-aarch64) #### Installation Instructions @@ -87,12 +88,21 @@ cargo install ci ### From Source ```bash -git clone https://github.com/CodeInputCorp/cli.git +git clone https://github.com/code-input/cli.git cd cli cargo build --release --bin ci sudo cp target/release/ci /usr/local/bin/ ``` +### Arch Linux (AUR) + +Install from the [Arch User Repository](https://aur.archlinux.org/) using an AUR helper: + +```bash +yay -S codeinput # build from source +yay -S codeinput-bin # pre-built binary +``` + ### From NPM ```bash diff --git a/SECURITY.md b/SECURITY.md index ca12e19..4515d6d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,4 +8,4 @@ Remediation of security vulnerabilities is prioritized by the project team. The ## Security advisories -The project team is committed to transparency in the security issue disclosure process. We announce security issues via [project Github Release notes](https://github.com/CodeInputCorp/cli/releases). +The project team is committed to transparency in the security issue disclosure process. We announce security issues via [project Github Release notes](https://github.com/code-input/cli/releases). diff --git a/ci/Cargo.toml b/ci/Cargo.toml index 0e37b2e..9ab91b8 100644 --- a/ci/Cargo.toml +++ b/ci/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "ci" -version = "0.0.5" +version = "0.1.0" authors = ["Abid Omar "] edition = "2021" repository = "https://github.com/code-input/cli" +homepage = "https://codeinput.com" readme = "../README.md" license = "MIT" keywords = ["cli", "codeowners", "git", "tooling", "binary"] @@ -21,10 +22,10 @@ default = ["termlog"] termlog = ["codeinput/termlog"] journald = ["codeinput/journald"] syslog = ["codeinput/syslog"] -lsp = ["codeinput/tower-lsp", "codeinput/tokio", "tokio"] +lsp = ["codeinput/tower-lsp-server", "codeinput/tokio", "tokio"] [dependencies] -codeinput = { version = "0.0.5", path = "../codeinput" } +codeinput = { version = "0.1.0", path = "../codeinput" } human-panic = { workspace = true } better-panic = { workspace = true } log = { workspace = true } diff --git a/codeinput/Cargo.toml b/codeinput/Cargo.toml index ed7f8a8..c6d45fc 100644 --- a/codeinput/Cargo.toml +++ b/codeinput/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "codeinput" -version = "0.0.5" +version = "0.1.0" authors = ["Abid Omar "] edition = "2021" repository = "https://github.com/code-input/cli" +homepage = "https://codeinput.com" readme = "../README.md" license = "MIT" keywords = ["codeowners", "git", "parsing", "analysis", "tooling"] @@ -94,7 +95,7 @@ tabled = { workspace = true, optional = true } terminal_size = { workspace = true, optional = true } clap = { workspace = true, optional = true } chrono = { version = "0.4.44", features = ["serde"], optional = true } -tower-lsp = { workspace = true, optional = true } +tower-lsp-server = { workspace = true, optional = true } tokio = { workspace = true, optional = true } url = { workspace = true, optional = true } diff --git a/codeinput/src/core/commands/lsp.rs b/codeinput/src/core/commands/lsp.rs index 033a459..3c6ddc3 100644 --- a/codeinput/src/core/commands/lsp.rs +++ b/codeinput/src/core/commands/lsp.rs @@ -4,11 +4,11 @@ use crate::utils::error::Result; -#[cfg(feature = "tower-lsp")] +#[cfg(feature = "tower-lsp-server")] use crate::lsp::server::{run_lsp_server, run_lsp_server_tcp}; /// Run the LSP server -#[cfg(feature = "tower-lsp")] +#[cfg(feature = "tower-lsp-server")] pub fn run(port: Option) -> Result<()> { let rt = tokio::runtime::Runtime::new()?; rt.block_on(async { diff --git a/codeinput/src/core/commands/mod.rs b/codeinput/src/core/commands/mod.rs index 60ac098..7f09015 100644 --- a/codeinput/src/core/commands/mod.rs +++ b/codeinput/src/core/commands/mod.rs @@ -7,5 +7,5 @@ pub mod list_rules; pub mod list_tags; pub mod parse; -#[cfg(feature = "tower-lsp")] +#[cfg(feature = "tower-lsp-server")] pub mod lsp; diff --git a/codeinput/src/lib.rs b/codeinput/src/lib.rs index fe44923..8cd4e2b 100644 --- a/codeinput/src/lib.rs +++ b/codeinput/src/lib.rs @@ -12,5 +12,5 @@ pub mod core; pub mod utils; // LSP server module (requires full features - not just types) -#[cfg(all(not(feature = "types"), feature = "tokio", feature = "tower-lsp"))] +#[cfg(all(not(feature = "types"), feature = "tokio", feature = "tower-lsp-server"))] pub mod lsp; diff --git a/codeinput/src/lsp/commands.rs b/codeinput/src/lsp/commands.rs index 2c35a89..08da70e 100644 --- a/codeinput/src/lsp/commands.rs +++ b/codeinput/src/lsp/commands.rs @@ -1,4 +1,4 @@ -use tower_lsp::jsonrpc::Result as LspResult; +use tower_lsp_server::jsonrpc::Result as LspResult; use url::Url; use crate::core::types::{CodeownersCache, OwnerType}; @@ -7,6 +7,13 @@ use super::server::LspServer; use super::types::*; impl LspServer { + pub async fn get_file_ownership_command(&self, uri_str: String) -> LspResult> { + let Ok(uri) = Url::parse(&uri_str) else { + return Err(tower_lsp_server::jsonrpc::Error::invalid_params("Invalid URI")); + }; + Ok(self.get_file_ownership(&uri).await) + } + pub async fn list_files(&self, workspace_uri: Option) -> LspResult { let workspaces = self.workspaces.read().await; @@ -107,3 +114,4 @@ impl LspServer { } } + diff --git a/codeinput/src/lsp/handlers.rs b/codeinput/src/lsp/handlers.rs index 4b6d55f..8b2fd21 100644 --- a/codeinput/src/lsp/handlers.rs +++ b/codeinput/src/lsp/handlers.rs @@ -1,20 +1,20 @@ use serde::Serialize; use serde_json::Value; -use tower_lsp::jsonrpc::Result as LspResult; -use tower_lsp::lsp_types::*; -use tower_lsp::LanguageServer; +use tower_lsp_server::LanguageServer; +use tower_lsp_server::jsonrpc::Result as LspResult; +use tower_lsp_server::ls_types::*; use url::Url; -use super::server::{is_codeowners_file, uri_to_path, LspServer}; +use super::server::{LspServer, is_codeowners_file, uri_to_path}; -#[tower_lsp::async_trait] impl LanguageServer for LspServer { async fn initialize(&self, params: InitializeParams) -> LspResult { // Initialize workspaces from workspace folders let workspace_folders = params.workspace_folders.unwrap_or_default(); for folder in workspace_folders { - if let Err(e) = self.initialize_workspace(folder.uri, None).await { + let Ok(uri_url) = Url::parse(folder.uri.as_str()) else { continue; }; + if let Err(e) = self.initialize_workspace(uri_url, None).await { self.client .log_message( MessageType::WARNING, @@ -25,18 +25,23 @@ impl LanguageServer for LspServer { } // If no workspace folders, try to use root_uri - if let Some(root_uri) = params.root_uri { - if let Err(e) = self.initialize_workspace(root_uri, None).await { - self.client - .log_message( - MessageType::WARNING, - format!("Failed to initialize root workspace: {}", e), - ) - .await; + #[allow(deprecated)] + let root_uri = params.root_uri; + if let Some(root_uri) = root_uri { + if let Ok(uri_url) = Url::parse(root_uri.as_str()) { + if let Err(e) = self.initialize_workspace(uri_url, None).await { + self.client + .log_message( + MessageType::WARNING, + format!("Failed to initialize root workspace: {}", e), + ) + .await; + } } } Ok(InitializeResult { + offset_encoding: None, capabilities: ServerCapabilities { text_document_sync: Some(TextDocumentSyncCapability::Kind( TextDocumentSyncKind::FULL, @@ -51,6 +56,7 @@ impl LanguageServer for LspServer { "codeinput.listFiles".to_string(), "codeinput.listOwners".to_string(), "codeinput.listTags".to_string(), + "codeinput.getFileOwnership".to_string(), ], work_done_progress_options: WorkDoneProgressOptions { work_done_progress: None, @@ -86,7 +92,7 @@ impl LanguageServer for LspServer { } async fn did_open(&self, params: DidOpenTextDocumentParams) { - let file_uri = params.text_document.uri; + let Ok(file_uri) = Url::parse(params.text_document.uri.as_str()) else { return; }; // Check if this is a CODEOWNERS file and refresh cache if so if is_codeowners_file(&file_uri) { @@ -119,7 +125,7 @@ impl LanguageServer for LspServer { } async fn did_save(&self, params: DidSaveTextDocumentParams) { - let file_uri = params.text_document.uri; + let Ok(file_uri) = Url::parse(params.text_document.uri.as_str()) else { return; }; // If CODEOWNERS file was saved, refresh the cache if is_codeowners_file(&file_uri) { @@ -146,7 +152,7 @@ impl LanguageServer for LspServer { } async fn hover(&self, params: HoverParams) -> LspResult> { - let file_uri = params.text_document_position_params.text_document.uri; + let Ok(file_uri) = Url::parse(params.text_document_position_params.text_document.uri.as_str()) else { return Ok(None); }; // Get ownership info for this file if let Some(info) = self.get_file_ownership(&file_uri).await { @@ -193,7 +199,7 @@ impl LanguageServer for LspServer { } async fn code_lens(&self, params: CodeLensParams) -> LspResult>> { - let file_uri = params.text_document.uri; + let Ok(file_uri) = Url::parse(params.text_document.uri.as_str()) else { return Ok(None); }; // Get ownership info for this file if let Some(info) = self.get_file_ownership(&file_uri).await { @@ -259,25 +265,6 @@ impl LanguageServer for LspServer { } } - // Add unowned warning CodeLens - if info.is_unowned { - // Safely serialize arguments - if let Some(uri_val) = serde_json::to_value(file_uri.to_string()).ok() { - lenses.push(CodeLens { - range: Range { - start: Position::new(0, 0), - end: Position::new(0, 0), - }, - command: Some(Command { - title: "$(warning) Unowned file".to_string(), - command: "codeinput.addOwner".to_string(), - arguments: Some(vec![uri_val]), - }), - data: None, - }); - } - } - return Ok(Some(lenses)); } @@ -285,153 +272,98 @@ impl LanguageServer for LspServer { } async fn inlay_hint(&self, params: InlayHintParams) -> LspResult>> { - let file_uri = params.text_document.uri; - - self.client - .log_message( - MessageType::INFO, - format!("inlay_hint called for: {}", file_uri), - ) - .await; + let Ok(file_uri) = Url::parse(params.text_document.uri.as_str()) else { return Ok(None); }; - // Only show inlay hints in CODEOWNERS files let path = uri_to_path(&file_uri); - if let Ok(path) = path { - let file_name = path - .file_name() - .map(|n| n.to_string_lossy()) - .unwrap_or_default(); - self.client - .log_message(MessageType::INFO, format!("file_name: {}", file_name)) - .await; - if file_name.to_lowercase() != "codeowners" { - self.client - .log_message(MessageType::INFO, "Not a CODEOWNERS file, skipping") - .await; - return Ok(None); - } - } else { - self.client - .log_message(MessageType::WARNING, "Failed to convert URI to path") - .await; + let file_path = match path { + Ok(p) => p, + Err(_) => return Ok(None), + }; + + if file_path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or_default() + .to_lowercase() + != "codeowners" + { return Ok(None); } let workspaces = self.workspaces.read().await; - self.client - .log_message( - MessageType::INFO, - format!("Workspace count: {}", workspaces.len()), - ) - .await; - // Find the workspace that contains this file - for (root_uri, state) in workspaces.iter() { - self.client - .log_message( - MessageType::INFO, - format!("Checking workspace: {}", root_uri), - ) - .await; - let mut hints = vec![]; + for (_root_uri, state) in workspaces.iter() { + let file_entries: Vec<_> = state + .cache + .entries + .iter() + .filter(|e| e.source_file == file_path) + .collect(); - let file_path = match uri_to_path(&file_uri) { - Ok(p) => p, + if file_entries.is_empty() { + continue; + } + + // Read the CODEOWNERS file content asynchronously to get line lengths + let content = match tokio::fs::read_to_string(&file_path).await { + Ok(c) => c, Err(_) => continue, }; - // Read the CODEOWNERS file content - if let Ok(content) = std::fs::read_to_string(&file_path) { - self.client - .log_message( - MessageType::INFO, - format!("Read CODEOWNERS file, {} lines", content.lines().count()), - ) - .await; - for (line_num, line) in content.lines().enumerate() { - let trimmed = line.trim(); - - // Skip empty lines and comments - if trimmed.is_empty() || trimmed.starts_with('#') { - continue; - } + let lines: Vec<&str> = content.lines().collect(); + let mut hints = vec![]; - // Parse pattern from line - let parts: Vec<&str> = trimmed.split_whitespace().collect(); - if !parts.is_empty() { - let pattern = parts[0]; - - // Count files matching this pattern using the cache - let match_count = state - .cache - .entries - .iter() - .filter(|entry| { - let rel_path = entry.source_file.to_string_lossy(); - // Simple pattern matching - if pattern == "*" { - return true; - } - if pattern.ends_with('/') { - let dir_pattern = &pattern[..pattern.len() - 1]; - rel_path.starts_with(dir_pattern) - } else if pattern.starts_with('*') { - let suffix = &pattern[1..]; - rel_path.ends_with(suffix) - } else if pattern.ends_with("/*") { - let prefix = &pattern[..pattern.len() - 2]; - rel_path.starts_with(prefix) - } else { - rel_path.contains(pattern.trim_start_matches('/')) - } - }) - .count(); - - if match_count > 0 { - let line_length = line.len() as u32; - hints.push(InlayHint { - position: Position::new(line_num as u32, line_length), - label: InlayHintLabel::String(format!( - " {} file{}", - match_count, - if match_count == 1 { "" } else { "s" } - )), - kind: Some(InlayHintKind::TYPE), - text_edits: None, - tooltip: Some(InlayHintTooltip::String(format!( - "This pattern matches {} file(s) in the repository", - match_count - ))), - padding_left: Some(true), - padding_right: Some(false), - data: None, - }); - } + for entry in file_entries { + let line_idx = if entry.line_number > 0 { + entry.line_number - 1 + } else { + 0 + }; + let line_length = lines.get(line_idx).map(|l| l.len() as u32).unwrap_or(0); + + let matcher = crate::core::types::codeowners_entry_to_matcher(entry); + + let mut match_count = 0; + for file_entry in &state.cache.files { + if matcher + .override_matcher + .matched(&file_entry.path, false) + .is_whitelist() + { + match_count += 1; } } - self.client - .log_message( - MessageType::INFO, - format!("Generated {} inlay hints", hints.len()), - ) - .await; + hints.push(InlayHint { + position: Position::new(line_idx as u32, line_length), + label: InlayHintLabel::String(format!( + " {} file{}", + match_count, + if match_count == 1 { "" } else { "s" } + )), + kind: Some(InlayHintKind::TYPE), + text_edits: None, + tooltip: Some(InlayHintTooltip::String(format!( + "This pattern matches {} file(s) in the repository", + match_count + ))), + padding_left: Some(true), + padding_right: Some(false), + data: None, + }); + } + + if !hints.is_empty() { return Ok(Some(hints)); } } - self.client - .log_message( - MessageType::WARNING, - "No workspace found for CODEOWNERS file", - ) - .await; Ok(None) } async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) { // Handle removed workspaces - collect URIs first, then remove them - let removed_uris: Vec = params.event.removed.iter().map(|f| f.uri.clone()).collect(); + let removed_uris: Vec = params.event.removed.iter().filter_map(|f| Url::parse(f.uri.as_str()).ok()).collect(); { let mut workspaces = self.workspaces.write().await; for uri in removed_uris { @@ -441,7 +373,8 @@ impl LanguageServer for LspServer { // Handle added workspaces for folder in params.event.added { - if let Err(e) = self.initialize_workspace(folder.uri, None).await { + let Ok(uri_url) = Url::parse(folder.uri.as_str()) else { continue; }; + if let Err(e) = self.initialize_workspace(uri_url, None).await { self.client .log_message( MessageType::WARNING, @@ -455,10 +388,23 @@ impl LanguageServer for LspServer { async fn execute_command(&self, params: ExecuteCommandParams) -> LspResult> { fn to_value(v: T) -> LspResult { serde_json::to_value(v) - .map_err(|e| tower_lsp::jsonrpc::Error::invalid_params(e.to_string())) + .map_err(|e| tower_lsp_server::jsonrpc::Error::invalid_params(e.to_string())) } match params.command.as_str() { + "codeinput.getFileOwnership" => { + let uri_val = params + .arguments + .first() + .and_then(|v| v.as_str()) + .ok_or_else(|| { + tower_lsp_server::jsonrpc::Error::invalid_params( + "Expected a single URI string argument", + ) + })?; + let result = self.get_file_ownership_command(uri_val.to_string()).await?; + to_value(result).map(Some) + } "codeinput.listFiles" => { let result = self.list_files(None).await?; to_value(result).map(Some) @@ -471,8 +417,7 @@ impl LanguageServer for LspServer { let result = self.list_tags(None).await?; to_value(result).map(Some) } - _ => Err(tower_lsp::jsonrpc::Error::method_not_found()), + _ => Err(tower_lsp_server::jsonrpc::Error::method_not_found()), } } } - diff --git a/codeinput/src/lsp/server.rs b/codeinput/src/lsp/server.rs index 54351c9..10c4a02 100644 --- a/codeinput/src/lsp/server.rs +++ b/codeinput/src/lsp/server.rs @@ -3,8 +3,8 @@ use std::path::PathBuf; use std::sync::Arc; use tokio::sync::RwLock; -use tower_lsp::lsp_types::*; -use tower_lsp::Client; +use tower_lsp_server::ls_types::*; +use tower_lsp_server::Client; use url::Url; use crate::core::cache::sync_cache; @@ -138,8 +138,9 @@ impl LspServer { // Publish diagnostics for this workspace for (file_uri, file_diagnostics) in diagnostics { + let Ok(ls_uri) = file_uri.as_str().parse::() else { continue; }; self.client - .publish_diagnostics(file_uri, file_diagnostics, None) + .publish_diagnostics(ls_uri, file_diagnostics, None) .await; } } @@ -163,9 +164,9 @@ pub async fn run_lsp_server() -> Result<()> { let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); - let (service, socket) = tower_lsp::LspService::new(|client| LspServer::new(client)); + let (service, socket) = tower_lsp_server::LspService::new(|client| LspServer::new(client)); - tower_lsp::Server::new(stdin, stdout, socket) + tower_lsp_server::Server::new(stdin, stdout, socket) .serve(service) .await; @@ -184,8 +185,8 @@ pub async fn run_lsp_server_tcp(port: u16) -> Result<()> { loop { let (stream, _) = listener.accept().await?; let (read, write) = tokio::io::split(stream); - let (service, socket) = tower_lsp::LspService::new(|client| LspServer::new(client)); + let (service, socket) = tower_lsp_server::LspService::new(|client| LspServer::new(client)); - tokio::spawn(tower_lsp::Server::new(read, write, socket).serve(service)); + tokio::spawn(tower_lsp_server::Server::new(read, write, socket).serve(service)); } } diff --git a/dist/aur/codeinput-bin/.SRCINFO b/dist/aur/codeinput-bin/.SRCINFO new file mode 100644 index 0000000..a16b716 --- /dev/null +++ b/dist/aur/codeinput-bin/.SRCINFO @@ -0,0 +1,17 @@ +pkgbase = codeinput-bin + pkgdesc = A powerful CLI tool for parsing, analyzing, and managing CODEOWNERS files + pkgver = 0.1.0 + pkgrel = 1 + url = https://codeinput.com/cli + arch = x86_64 + arch = aarch64 + license = MIT + provides = codeinput + conflicts = codeinput + options = !strip + source_x86_64 = https://github.com/code-input/cli/releases/download/v0.1.0/ci-linux-x86_64 + sha256sums_x86_64 = SKIP + source_aarch64 = https://github.com/code-input/cli/releases/download/v0.1.0/ci-linux-aarch64 + sha256sums_aarch64 = SKIP + +pkgname = codeinput-bin diff --git a/dist/aur/codeinput-bin/PKGBUILD b/dist/aur/codeinput-bin/PKGBUILD new file mode 100644 index 0000000..30ad89c --- /dev/null +++ b/dist/aur/codeinput-bin/PKGBUILD @@ -0,0 +1,19 @@ +# Maintainer: Abid Omar +pkgname=codeinput-bin +pkgver=0.1.0 +pkgrel=1 +pkgdesc="A powerful CLI tool for parsing, analyzing, and managing CODEOWNERS files" +arch=('x86_64' 'aarch64') +url="https://codeinput.com/cli" +license=('MIT') +provides=('codeinput') +conflicts=('codeinput') +options=('!strip') +source_x86_64=("https://github.com/code-input/cli/releases/download/v${pkgver}/ci-linux-x86_64") +source_aarch64=("https://github.com/code-input/cli/releases/download/v${pkgver}/ci-linux-aarch64") +sha256sums_x86_64=('SKIP') +sha256sums_aarch64=('SKIP') + +package() { + install -Dm755 "${srcdir}/ci-linux-${CARCH}" "${pkgdir}/usr/bin/ci" +} diff --git a/dist/aur/codeinput/.SRCINFO b/dist/aur/codeinput/.SRCINFO new file mode 100644 index 0000000..20f39b5 --- /dev/null +++ b/dist/aur/codeinput/.SRCINFO @@ -0,0 +1,15 @@ +pkgbase = codeinput + pkgdesc = A powerful CLI tool for parsing, analyzing, and managing CODEOWNERS files + pkgver = 0.1.0 + pkgrel = 1 + url = https://codeinput.com/cli + arch = x86_64 + arch = aarch64 + license = MIT + depends = gcc + depends = openssl + makedepends = cargo + source = codeinput-0.1.0.tar.gz::https://github.com/code-input/cli/archive/refs/tags/v0.1.0.tar.gz + sha256sums = SKIP + +pkgname = codeinput diff --git a/dist/aur/codeinput/PKGBUILD b/dist/aur/codeinput/PKGBUILD new file mode 100644 index 0000000..4fb3625 --- /dev/null +++ b/dist/aur/codeinput/PKGBUILD @@ -0,0 +1,27 @@ +# Maintainer: Abid Omar +pkgname=codeinput +pkgver=0.1.0 +pkgrel=1 +pkgdesc="A powerful CLI tool for parsing, analyzing, and managing CODEOWNERS files" +arch=('x86_64' 'aarch64') +url="https://codeinput.com/cli" +license=('MIT') +depends=('gcc' 'openssl') +makedepends=('cargo') +source=("${pkgname}-${pkgver}.tar.gz::https://github.com/code-input/cli/archive/refs/tags/v${pkgver}.tar.gz") +sha256sums=('SKIP') + +prepare() { + cd "cli-${pkgver}" + cargo fetch --locked --target "$CARCH-unknown-linux-gnu" +} + +build() { + cd "cli-${pkgver}" + cargo build --release --bin ci +} + +package() { + cd "cli-${pkgver}" + install -Dm755 "target/release/ci" "${pkgdir}/usr/bin/ci" +} diff --git a/dist/pypi/pyproject.toml b/dist/pypi/pyproject.toml new file mode 100644 index 0000000..db9325f --- /dev/null +++ b/dist/pypi/pyproject.toml @@ -0,0 +1,36 @@ +[build-system] +requires = ["setuptools>=68.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "codeinput" +version = "0.1.0" +description = "A powerful CLI tool for parsing, analyzing, and managing CODEOWNERS files" +license = "MIT" +license-files = [] +requires-python = ">=3.8" +authors = [ + { name = "Abid Omar", email = "contact@omarabid.com" }, +] +keywords = ["codeowners", "cli", "git", "github", "ownership"] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + + "Operating System :: OS Independent", + "Programming Language :: Rust", + "Topic :: Software Development :: Version Control :: Git", + "Topic :: Utilities", +] + +[project.urls] +Homepage = "https://codeinput.com" +Repository = "https://github.com/code-input/cli" +Issues = "https://github.com/code-input/cli/issues" + +[project.scripts] +ci = "codeinput.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/dist/pypi/src/codeinput/__init__.py b/dist/pypi/src/codeinput/__init__.py new file mode 100644 index 0000000..28ee34c --- /dev/null +++ b/dist/pypi/src/codeinput/__init__.py @@ -0,0 +1,8 @@ +"""CodeInput CLI - A powerful CLI tool for CODEOWNERS file management.""" + +try: + from importlib.metadata import version as _version + + __version__ = _version("codeinput") +except Exception: + __version__ = "0.0.0" diff --git a/dist/pypi/src/codeinput/cli.py b/dist/pypi/src/codeinput/cli.py new file mode 100644 index 0000000..cf39691 --- /dev/null +++ b/dist/pypi/src/codeinput/cli.py @@ -0,0 +1,99 @@ +"""CodeInput CLI wrapper that downloads and runs the native binary.""" + +import platform +import stat +import subprocess +import sys +from importlib.metadata import version as get_version +from pathlib import Path + +BINARY_DIR = Path.home() / ".cache" / "codeinput" +GITHUB_REPO = "code-input/cli" +VERSION = get_version("codeinput") + + +def _get_platform_info(): + """Detect the current platform and return (os, arch) tuple.""" + system = platform.system().lower() + machine = platform.machine().lower() + + if system == "linux": + os_name = "linux" + elif system == "darwin": + os_name = "macos" + elif system == "windows": + os_name = "windows" + else: + raise RuntimeError(f"Unsupported operating system: {system}") + + if machine in ("x86_64", "amd64"): + arch = "x86_64" + elif machine in ("aarch64", "arm64"): + arch = "aarch64" + else: + raise RuntimeError(f"Unsupported architecture: {machine}") + + return os_name, arch + + +def _get_binary_name(os_name, arch): + """Return the binary filename for the given platform.""" + if os_name == "windows": + return f"ci-{os_name}-{arch}.exe" + return f"ci-{os_name}-{arch}" + + +def _get_binary_path(): + """Return the local path where the binary should be stored.""" + os_name, arch = _get_platform_info() + binary_name = _get_binary_name(os_name, arch) + return BINARY_DIR / VERSION / binary_name + + +def _download_binary(): + """Download the binary from GitHub releases.""" + os_name, arch = _get_platform_info() + binary_name = _get_binary_name(os_name, arch) + url = f"https://github.com/{GITHUB_REPO}/releases/download/v{VERSION}/{binary_name}" + + binary_path = _get_binary_path() + binary_path.parent.mkdir(parents=True, exist_ok=True) + + import urllib.request + import urllib.error + + try: + urllib.request.urlretrieve(url, binary_path) + except urllib.error.HTTPError as e: + print(f"Error downloading binary: {e}", file=sys.stderr) + print(f"URL: {url}", file=sys.stderr) + sys.exit(1) + + if os_name != "windows": + binary_path.chmod(binary_path.stat().st_mode | stat.S_IEXEC) + + return binary_path + + +def _ensure_binary(): + """Ensure the binary is available, downloading if necessary.""" + binary_path = _get_binary_path() + if binary_path.exists(): + return binary_path + return _download_binary() + + +def main(): + """Run the CodeInput CLI.""" + try: + binary_path = _ensure_binary() + except Exception as e: + print(f"Error setting up CodeInput binary: {e}", file=sys.stderr) + sys.exit(1) + + result = subprocess.run([str(binary_path)] + sys.argv[1:]) + sys.exit(result.returncode) + + +if __name__ == "__main__": + main() diff --git a/extensions/vscode/.gitignore b/extensions/vscode/.gitignore index d61721c..d67180e 100644 --- a/extensions/vscode/.gitignore +++ b/extensions/vscode/.gitignore @@ -22,6 +22,7 @@ Thumbs.db # IDE .idea/ +.vscode/ *.swp *.swo @@ -31,3 +32,6 @@ coverage/ # Temporary files *.tmp *.temp + +# Yarn cache +.yarn/ diff --git a/extensions/vscode/.vscodeignore b/extensions/vscode/.vscodeignore index 1681d2e..d35f90a 100644 --- a/extensions/vscode/.vscodeignore +++ b/extensions/vscode/.vscodeignore @@ -15,3 +15,5 @@ vsc-extension-quickstart.md **/*.map **/*.ts *.vsix +codeinput-*.vsix +favicon.svg diff --git a/extensions/vscode/.yarnrc.yml b/extensions/vscode/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/extensions/vscode/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/extensions/vscode/CHANGELOG.md b/extensions/vscode/CHANGELOG.md new file mode 100644 index 0000000..d83a612 --- /dev/null +++ b/extensions/vscode/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.1.0] - 2026-03-07 + +### Added +- Initial release +- LSP-based CODEOWNERS integration +- Status bar ownership indicator +- File ownership info command +- Automatic binary download +- Tag support for CODEOWNERS diff --git a/extensions/vscode/LICENSE b/extensions/vscode/LICENSE new file mode 100644 index 0000000..3781661 --- /dev/null +++ b/extensions/vscode/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Abid Omar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/vscode/README.md b/extensions/vscode/README.md new file mode 100644 index 0000000..b0bc7f4 --- /dev/null +++ b/extensions/vscode/README.md @@ -0,0 +1,65 @@ +# CodeInput - CODEOWNERS Integration for VS Code + +A Language Server Protocol (LSP) based extension that provides CODEOWNERS integration for Visual Studio Code. Automatically displays ownership information and tags for files in your workspace. + +## Features + +- **Status Bar Integration**: Shows ownership status at a glance with lock/alert icons +- **File Ownership Info**: View owners and tags for any file via command palette +- **Multi-format Support**: Works with GitHub, GitLab, and Bitbucket CODEOWNERS file locations +- **Tag Support**: Extended CODEOWNERS syntax with tag support (e.g., `*.rs @team #backend #critical`) +- **Automatic Binary Management**: Downloads the required LSP server binary automatically + +## Requirements + +The extension requires the `ci-lsp` binary. It will attempt to download it automatically, or you can specify a custom path in settings. + +## Installation + +1. Install the extension from the VS Code Marketplace +2. Open a workspace containing a CODEOWNERS file +3. The extension will activate automatically + +## Configuration + +| Setting | Description | Default | +| --------------------------- | --------------------------------------- | ------------------- | +| `codeinput.binaryPath` | Path to the codeinput LSP server binary | `ci-lsp` | +| `codeinput.cacheFile` | Name of the cache file | `.codeowners.cache` | +| `codeinput.showInStatusBar` | Show CODEOWNERS info in status bar | `true` | +| `codeinput.showDiagnostics` | Show diagnostics for unowned files | `true` | +| `codeinput.lspTransport` | Transport method for LSP (stdio/tcp) | `stdio` | +| `codeinput.lspPort` | TCP port for LSP communication | `8123` | + +## Commands + +| Command | Description | +| -------------------------------------------------- | ------------------------------------------- | +| `CodeInput: Show CODEOWNERS Info for Current File` | Display owners and tags for the active file | +| `CodeInput: Refresh CODEOWNERS Cache` | Force refresh the ownership cache | + +## CODEOWNERS File Locations + +The extension monitors these locations for CODEOWNERS files: + +- `CODEOWNERS` (root) +- `.github/CODEOWNERS` +- `.gitlab/CODEOWNERS` +- `docs/CODEOWNERS` + +## Extended Syntax + +CodeInput supports an extended CODEOWNERS syntax with tags: + +``` +# Standard GitHub syntax +*.rs @rust-team + +# Extended syntax with tags +src/api/** @backend-team #api #critical +*.md @docs-team #documentation +``` + +## License + +MIT diff --git a/extensions/vscode/icon.png b/extensions/vscode/icon.png new file mode 100644 index 0000000..7cc06ce Binary files /dev/null and b/extensions/vscode/icon.png differ diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index e728742..3bf9eb1 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -2,9 +2,18 @@ "name": "codeinput", "displayName": "CodeInput - CODEOWNERS Integration", "description": "LSP-based CODEOWNERS integration for VS Code", - "version": "0.0.1", + "version": "0.1.0", "publisher": "codeinput", "license": "MIT", + "icon": "icon.png", + "repository": { + "type": "git", + "url": "https://github.com/code-input/cli.git" + }, + "bugs": { + "url": "https://github.com/code-input/cli/issues" + }, + "homepage": "https://github.com/code-input/cli#readme", "engines": { "vscode": "^1.74.0" }, @@ -27,21 +36,6 @@ "main": "./out/extension.js", "contributes": { "commands": [ - { - "command": "codeinput.showOwners", - "title": "Show CODEOWNERS Owners", - "category": "CodeInput" - }, - { - "command": "codeinput.showTags", - "title": "Show CODEOWNERS Tags", - "category": "CodeInput" - }, - { - "command": "codeinput.addOwner", - "title": "Add CODEOWNERS Entry", - "category": "CodeInput" - }, { "command": "codeinput.refreshCache", "title": "Refresh CODEOWNERS Cache", @@ -91,22 +85,6 @@ "description": "TCP port for LSP communication (when using tcp transport)" } } - }, - "menus": { - "commandPalette": [ - { - "command": "codeinput.showOwners", - "when": "false" - }, - { - "command": "codeinput.showTags", - "when": "false" - }, - { - "command": "codeinput.addOwner", - "when": "false" - } - ] } }, "scripts": { diff --git a/extensions/vscode/src/client.ts b/extensions/vscode/src/client.ts index 8aa5287..69bf333 100644 --- a/extensions/vscode/src/client.ts +++ b/extensions/vscode/src/client.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import { + ExecuteCommandRequest, LanguageClient, LanguageClientOptions, ServerOptions, @@ -9,7 +10,7 @@ import { export interface FileOwnershipInfo { path: string; owners: Array<{ identifier: string; owner_type: string }>; - tags: Array<{ 0: string }>; + tags: string[]; is_unowned: boolean; } @@ -97,68 +98,22 @@ export class CodeInputClient { return undefined; } - // Use the hover request to get ownership info try { - const hover = await this.client.sendRequest('textDocument/hover', { - textDocument: { uri: uri.toString() }, - position: { line: 0, character: 0 } - }) as any; + const info = await this.client.sendRequest(ExecuteCommandRequest.type, { + command: 'codeinput.getFileOwnership', + arguments: [uri.toString()] + }) as FileOwnershipInfo | null | undefined; - if (hover && hover.contents) { - // Parse hover contents to extract ownership info - return this.parseHoverContents(hover.contents, uri); + if (info) { + return info; } } catch (error) { - console.error('Error getting file ownership:', error); + console.error('[CodeInput Client] Error:', error); } return undefined; } - private parseHoverContents(contents: any, uri: vscode.Uri): FileOwnershipInfo | undefined { - // The LSP server returns hover contents with owners and tags - // This is a simplified parser - in production you'd want more robust parsing - const info: FileOwnershipInfo = { - path: uri.fsPath, - owners: [], - tags: [], - is_unowned: false - }; - - if (Array.isArray(contents)) { - for (const item of contents) { - if (typeof item === 'string') { - if (item.includes('Owners:')) { - const ownersMatch = item.match(/\*\*Owners:\*\* (.+)/); - if (ownersMatch) { - const ownersStr = ownersMatch[1]; - if (ownersStr !== '(none)') { - info.owners = ownersStr.split(', ').map(o => ({ - identifier: o.replace(/`/g, ''), - owner_type: 'Unknown' - })); - } - } - } - if (item.includes('Tags:')) { - const tagsMatch = item.match(/\*\*Tags:\*\* (.+)/); - if (tagsMatch) { - const tagsStr = tagsMatch[1]; - info.tags = tagsStr.split(', ').map(t => ({ - 0: t.replace(/`#/g, '').replace(/`/g, '') - })); - } - } - if (item.includes('Warning')) { - info.is_unowned = true; - } - } - } - } - - return info; - } - public isRunning(): boolean { return this.client?.isRunning() === true; } diff --git a/extensions/vscode/src/extension.ts b/extensions/vscode/src/extension.ts index 902dd94..166c8a6 100644 --- a/extensions/vscode/src/extension.ts +++ b/extensions/vscode/src/extension.ts @@ -12,7 +12,6 @@ async function checkBinary(binaryPath: string): Promise { const proc = spawn(binaryPath, ['lsp', '--help']); proc.on('error', () => resolve(false)); proc.on('exit', (code) => resolve(code === 0)); - // Timeout after 2 seconds setTimeout(() => { proc.kill(); resolve(false); @@ -23,31 +22,19 @@ async function checkBinary(binaryPath: string): Promise { export async function activate(context: vscode.ExtensionContext) { console.log('CodeInput extension is now active'); - // Check if custom binary path is set const config = vscode.workspace.getConfiguration('codeinput'); - const userBinaryPath = config.get('binaryPath', 'ci-lsp'); + const binaryPath = config.get('binaryPath', 'ci-lsp'); let finalBinaryPath: string | undefined; let binarySource = 'unknown'; - // If user set a custom path, use it - if (userBinaryPath !== 'ci-lsp') { - console.log(`[CodeInput] Checking user-specified binary: ${userBinaryPath}`); - const exists = await checkBinary(userBinaryPath); - if (exists) { - finalBinaryPath = userBinaryPath; - binarySource = 'user-config'; - console.log(`[CodeInput] Using user-specified binary: ${finalBinaryPath}`); - } else { - console.error(`[CodeInput] User-specified binary not found: ${userBinaryPath}`); - vscode.window.showErrorMessage( - `Custom binary not found: ${userBinaryPath}. Please check the path or install codeinput-lsp.` - ); - return; - } + const exists = await checkBinary(binaryPath); + if (exists) { + finalBinaryPath = binaryPath; + binarySource = 'config'; + console.log(`[CodeInput] Using binary from config: ${finalBinaryPath}`); } else { - // Try to download ci-lsp first - console.log('[CodeInput] Attempting to download ci-lsp binary...'); + console.log(`[CodeInput] Binary not found at ${binaryPath}, attempting to download...`); const downloader = new BinaryDownloader(context); const downloadedPath = await downloader.ensureBinary(); @@ -56,24 +43,11 @@ export async function activate(context: vscode.ExtensionContext) { binarySource = 'downloaded'; console.log(`[CodeInput] Using downloaded binary: ${finalBinaryPath}`); } else { - console.log('[CodeInput] Download failed, checking for existing ci binary with LSP support...'); - // Download failed, check if `ci` with LSP exists - const ciExists = await checkBinary('ci'); - if (ciExists) { - binarySource = 'fallback-ci'; - finalBinaryPath = 'ci'; - console.log('[CodeInput] Using fallback ci binary with LSP support'); - vscode.window.showInformationMessage( - 'Using existing `ci` binary with LSP support. Consider downloading `ci-lsp` for better performance.' - ); - } else { - // Neither works - console.error('[CodeInput] No usable binary found (tried downloading ci-lsp and using ci)'); - vscode.window.showErrorMessage( - 'codeinput-lsp not found. Please install it: curl -L https://github.com/code-input/cli/releases/latest/download/ci-lsp-linux-x64 -o ~/bin/ci-lsp && chmod +x ~/bin/ci-lsp' - ); - return; - } + console.error('[CodeInput] No usable binary found'); + vscode.window.showErrorMessage( + 'codeinput binary not found. Install it or set codeinput.binaryPath in settings.' + ); + return; } } @@ -134,38 +108,6 @@ export async function activate(context: vscode.ExtensionContext) { }) ); - context.subscriptions.push( - vscode.commands.registerCommand('codeinput.showOwners', (uri: string, owners: any[]) => { - if (owners && owners.length > 0) { - const ownerList = owners.map(o => o.identifier).join(', '); - vscode.window.showInformationMessage(`Owners: ${ownerList}`); - } - }) - ); - - context.subscriptions.push( - vscode.commands.registerCommand('codeinput.showTags', (uri: string, tags: any[]) => { - if (tags && tags.length > 0) { - const tagList = tags.map(t => `#${t}`).join(', '); - vscode.window.showInformationMessage(`Tags: ${tagList}`); - } - }) - ); - - context.subscriptions.push( - vscode.commands.registerCommand('codeinput.addOwner', async (uri: string) => { - const owner = await vscode.window.showInputBox({ - prompt: 'Enter owner (e.g., @username or @org/team)', - placeHolder: '@username' - }); - - if (owner) { - vscode.window.showInformationMessage(`Would add owner ${owner} to ${uri}`); - // TODO: Implement adding owner to CODEOWNERS file - } - }) - ); - // Handle configuration changes context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(e => { diff --git a/extensions/vscode/src/statusbar.ts b/extensions/vscode/src/statusbar.ts index 40808d7..758ece4 100644 --- a/extensions/vscode/src/statusbar.ts +++ b/extensions/vscode/src/statusbar.ts @@ -12,7 +12,6 @@ export class StatusBarManager implements vscode.Disposable { vscode.StatusBarAlignment.Right, 100 ); - this.statusBarItem.command = 'codeinput.showInfo'; // Update when active editor changes this.disposables.push( @@ -55,7 +54,6 @@ export class StatusBarManager implements vscode.Disposable { const fileUri = editor.document.uri; - // Only show for file:// URIs if (fileUri.scheme !== 'file') { this.statusBarItem.hide(); return; @@ -64,56 +62,25 @@ export class StatusBarManager implements vscode.Disposable { try { const info = await this.client.getFileOwnership(fileUri); - if (info) { - if (info.is_unowned || info.owners.length === 0) { - this.statusBarItem.text = '$(warning) Unowned'; - this.statusBarItem.tooltip = 'This file has no CODEOWNERS assignment\nClick to see details'; - this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); - } else { - const ownerNames = info.owners.map(o => o.identifier).join(', '); - const ownerInitials = info.owners - .map(o => this.getInitials(o.identifier)) - .join(''); - - this.statusBarItem.text = `$(lock) ${ownerInitials}`; - this.statusBarItem.tooltip = `Owners: ${ownerNames}\nClick to see details`; - this.statusBarItem.backgroundColor = undefined; - } - - this.statusBarItem.show(); + if (info && !info.is_unowned && info.owners.length > 0) { + this.statusBarItem.text = '$(lock)'; + this.statusBarItem.tooltip = 'Has CODEOWNERS'; + this.statusBarItem.backgroundColor = undefined; } else { - // No CODEOWNERS info available (file not in cache) - this.statusBarItem.hide(); + this.statusBarItem.text = '$(alert)'; + this.statusBarItem.tooltip = 'No CODEOWNERS'; + this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); } + this.statusBarItem.show(); } catch (error) { console.error('Error updating status bar:', error); - this.statusBarItem.hide(); + this.statusBarItem.text = '$(alert)'; + this.statusBarItem.tooltip = 'No CODEOWNERS'; + this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground'); + this.statusBarItem.show(); } } - private getInitials(identifier: string): string { - // Extract initials from owner identifier - // @org/team -> T, @username -> U, email@domain.com -> E - - if (identifier.startsWith('@')) { - // Handle @org/team format - const parts = identifier.split('/'); - if (parts.length > 1) { - return parts[parts.length - 1].charAt(0).toUpperCase(); - } - // Handle @username format - return identifier.charAt(1).toUpperCase(); - } - - if (identifier.includes('@')) { - // Email format - return identifier.charAt(0).toUpperCase(); - } - - // Fallback - return identifier.charAt(0).toUpperCase(); - } - public dispose(): void { this.statusBarItem.dispose(); this.disposables.forEach(d => d.dispose());