diff --git a/.github/workflows/ci-cli.yml b/.github/workflows/ci-cli.yml new file mode 100644 index 0000000..b8a5c9b --- /dev/null +++ b/.github/workflows/ci-cli.yml @@ -0,0 +1,46 @@ +name: "CI: CLI" + +on: + push: + branches: [main] + paths: + - "cli/**" + - ".github/workflows/ci-cli.yml" + pull_request: + branches: [main] + paths: + - "cli/**" + - ".github/workflows/ci-cli.yml" + +permissions: + contents: read + +jobs: + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + defaults: + run: + working-directory: cli + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: cli/go.mod + cache-dependency-path: cli/go.sum + + - name: Vet + run: go vet ./... + + - name: Test + run: go test -race ./... + + - name: Build + run: go build ./... diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-server.yml similarity index 50% rename from .github/workflows/ci-go.yml rename to .github/workflows/ci-server.yml index 3502648..10c1466 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-server.yml @@ -1,20 +1,19 @@ -name: CI — Go server +name: "CI: Server" on: push: branches: [main] paths: - "server/**" - - ".github/workflows/ci-go.yml" + - ".github/workflows/ci-server.yml" pull_request: branches: [main] paths: - "server/**" - - ".github/workflows/ci-go.yml" + - ".github/workflows/ci-server.yml" -# Read-only token: this workflow runs vet/test/build only — no writes to -# the repo, no SARIF upload, no package publish. CodeQL flagged the -# missing block (.github/workflows/ci-go.yml:37 — go/missing-permissions). +# Read-only: vet/test/build only. CodeQL flagged the implicit block as +# go/missing-permissions, hence the explicit declaration. permissions: contents: read @@ -24,20 +23,21 @@ jobs: defaults: run: working-directory: server - steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - name: Set up Go + uses: actions/setup-go@v5 with: go-version-file: server/go.mod cache-dependency-path: server/go.sum - - name: go vet + - name: Vet run: go vet ./... - - name: go test + - name: Test run: go test -race ./... - - name: go build + - name: Build run: go build ./... diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..c7d12e8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,56 @@ +name: "CodeQL" + +# Advanced setup. Replaces GitHub's "default setup" which auto-detects +# and scans every language it finds — that included java-kotlin, ruby, +# rust, javascript-typescript, c-cpp, python false-positives from +# vendored CGO deps and the archived legacy/python-api/ tree. +# +# To stop the duplicate runs you also need to disable the default +# setup once: GitHub repo → Settings → Code security → Code scanning +# → "CodeQL analysis" → Switch to advanced (or Disable). + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "0 6 * * 1" # Mondays at 06:00 UTC, mirrors security.yml + +permissions: + contents: read + security-events: write + actions: read + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Keep tightly scoped: only languages that actually ship code. + # `actions` lints workflow YAML; `go` covers server + CLI. + # Do NOT add python (only legacy/python-api/, archived) or + # c-cpp (only transitive CGO deps, no first-party C). + language: [actions, go] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # security-extended adds rules beyond the default set; matches + # what the default setup runs. + queries: security-extended + + - name: Autobuild + if: matrix.language == 'go' + uses: github/codeql-action/autobuild@v3 + + - name: Analyze + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index b21277d..8b329f0 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -1,72 +1,50 @@ -name: Release CLI +name: "Release: CLI" +# Triggered by CLI-namespaced tags (e.g. `cli/v0.4.0`). Server releases +# use `server/v*`. Bare `v*` tags are the historical pre-split CLI line +# and are no longer wired to any workflow. on: push: tags: - - "v*" + - "cli/v*" permissions: contents: write jobs: - build-linux: - name: Build ${{ matrix.target }} - runs-on: ubuntu-latest + build: + name: Build (${{ matrix.target }}) + runs-on: ${{ matrix.runner }} strategy: + fail-fast: false matrix: include: - - target: linux-arm64 - goarch: arm64 - target: linux-amd64 + runner: ubuntu-latest + goos: linux goarch: amd64 - - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-go@v6 - with: - go-version-file: cli/go.mod - cache-dependency-path: cli/go.sum - - - name: Build - working-directory: cli - env: - GOOS: linux - GOARCH: ${{ matrix.goarch }} - CGO_ENABLED: "0" - run: | - VERSION="${{ github.ref_name }}" - go build \ - -ldflags="-s -w -X 'github.com/anthropics/code-index/cli/cmd.Version=${VERSION}'" \ - -o "dist/cix" . - - - name: Package - working-directory: cli - run: | - cd dist - tar -czf "cix-${{ matrix.target }}.tar.gz" cix - rm cix - - - uses: actions/upload-artifact@v7 - with: - name: cix-${{ matrix.target }} - path: cli/dist/cix-${{ matrix.target }}.* - - build-darwin: - name: Build ${{ matrix.target }} - runs-on: macos-latest - strategy: - matrix: - include: - - target: darwin-arm64 + cgo: "0" + - target: linux-arm64 + runner: ubuntu-latest + goos: linux goarch: arm64 + cgo: "0" - target: darwin-amd64 + runner: macos-latest + goos: darwin goarch: amd64 - + cgo: "1" + - target: darwin-arm64 + runner: macos-latest + goos: darwin + goarch: arm64 + cgo: "1" steps: - - uses: actions/checkout@v6 + - name: Checkout + uses: actions/checkout@v4 - - uses: actions/setup-go@v6 + - name: Set up Go + uses: actions/setup-go@v5 with: go-version-file: cli/go.mod cache-dependency-path: cli/go.sum @@ -74,49 +52,68 @@ jobs: - name: Build working-directory: cli env: - GOOS: darwin + GOOS: ${{ matrix.goos }} GOARCH: ${{ matrix.goarch }} - CGO_ENABLED: "1" + CGO_ENABLED: ${{ matrix.cgo }} run: | - VERSION="${{ github.ref_name }}" + # Strip `cli/` namespace prefix — binaries report `v0.4.0`, not `cli/v0.4.0`. + VERSION="${GITHUB_REF_NAME#cli/}" go build \ -ldflags="-s -w -X 'github.com/anthropics/code-index/cli/cmd.Version=${VERSION}'" \ -o "dist/cix" . - name: Package - working-directory: cli + working-directory: cli/dist run: | - cd dist tar -czf "cix-${{ matrix.target }}.tar.gz" cix rm cix - - uses: actions/upload-artifact@v7 + - name: Upload artifacts + uses: actions/upload-artifact@v4 with: name: cix-${{ matrix.target }} - path: cli/dist/cix-${{ matrix.target }}.* + path: cli/dist/cix-${{ matrix.target }}.tar.gz release: - name: Create Release - needs: [build-linux, build-darwin] + name: Publish release + needs: [build] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - name: Checkout + uses: actions/checkout@v4 - - uses: actions/download-artifact@v8 + - name: Download artifacts + uses: actions/download-artifact@v4 with: path: artifacts merge-multiple: true - - name: Checksums - run: | - cd artifacts - sha256sum * > checksums.txt + - name: Compute checksums + working-directory: artifacts + run: sha256sum * > checksums.txt + + - name: Extract version + id: ver + run: echo "version=${GITHUB_REF_NAME#cli/}" >> "$GITHUB_OUTPUT" - - name: Create GitHub Release + - name: Create GitHub release uses: softprops/action-gh-release@v2 with: + name: "CLI ${{ steps.ver.outputs.version }}" files: | artifacts/*.tar.gz artifacts/checksums.txt generate_release_notes: true - make_latest: "legacy" \ No newline at end of file + # Server releases own the "latest" pointer (Docker image is the + # primary deliverable). CLI installs filter by `cli/` tag prefix. + make_latest: "false" + body: | + ## Install + + ```bash + curl -fsSL https://raw.githubusercontent.com/dvcdsys/code-index/main/install.sh | bash + ``` + + Re-run the same command later to upgrade — the installer + picks the latest `cli/v*` release and skips if you already + have it (use `--force` to reinstall). diff --git a/.github/workflows/release-server.yml b/.github/workflows/release-server.yml index 4a86494..6df9827 100644 --- a/.github/workflows/release-server.yml +++ b/.github/workflows/release-server.yml @@ -1,5 +1,8 @@ -name: Release Server +name: "Release: Server" +# Triggered by server-namespaced tags (e.g. `server/v0.3.0`). +# Builds CPU (multi-arch) and CUDA (amd64-only) Docker images and +# publishes them to Docker Hub, then creates the GitHub release. on: push: tags: @@ -10,23 +13,26 @@ permissions: jobs: docker-cpu: - name: Build + push CPU image (multi-arch) + name: Build CPU image (multi-arch) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - name: Extract version id: ver run: echo "version=${GITHUB_REF_NAME#server/}" >> "$GITHUB_OUTPUT" - - uses: docker/setup-buildx-action@v3 + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and push CPU image + - name: Build and push uses: docker/build-push-action@v6 with: context: server @@ -39,23 +45,26 @@ jobs: dvcdsys/code-index:latest docker-cuda: - name: Build + push CUDA image (amd64) + name: Build CUDA image (amd64) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - name: Extract version id: ver run: echo "version=${GITHUB_REF_NAME#server/}" >> "$GITHUB_OUTPUT" - - uses: docker/setup-buildx-action@v3 + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and push CUDA image + - name: Build and push uses: docker/build-push-action@v6 with: context: server @@ -68,19 +77,21 @@ jobs: dvcdsys/code-index:cu128 release: - name: Create GitHub Release + name: Publish release needs: [docker-cpu, docker-cuda] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - name: Extract version id: ver run: echo "version=${GITHUB_REF_NAME#server/}" >> "$GITHUB_OUTPUT" - - name: Create GitHub Release + - name: Create GitHub release uses: softprops/action-gh-release@v2 with: + name: "Server ${{ steps.ver.outputs.version }}" generate_release_notes: true body: | ## Docker Images diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 03330d3..5b12ad8 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,4 +1,4 @@ -name: Security Scan +name: "Security" on: push: @@ -6,23 +6,25 @@ on: pull_request: branches: [main] schedule: - - cron: "0 6 * * 1" # щопонеділка о 6:00 UTC + - cron: "0 6 * * 1" # Mondays at 06:00 UTC permissions: contents: read - security-events: write # для завантаження SARIF у GitHub Security tab + security-events: write # required for SARIF upload to the Security tab jobs: govulncheck: - name: govulncheck (Go server) + name: govulncheck (server) runs-on: ubuntu-latest defaults: run: working-directory: server steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - name: Set up Go + uses: actions/setup-go@v5 with: go-version-file: server/go.mod cache-dependency-path: server/go.sum @@ -34,10 +36,11 @@ jobs: run: govulncheck ./... trivy: - name: trivy (vuln, second opinion) + name: Trivy (filesystem) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 - name: Run Trivy uses: aquasecurity/trivy-action@0.35.0 @@ -46,16 +49,15 @@ jobs: scan-ref: . # server/bench is a Phase 0 PoC module (chromem + tree-sitter # benchmarks). It pins an old golang.org/x/net via its own - # go.mod and replace directive, and is never shipped in the - # cix-server binary. Scan it separately if needed, not as part - # of the prod CVE gate. + # go.mod + replace directive and is never shipped in the + # cix-server binary, so it stays out of the prod CVE gate. skip-dirs: server/bench scanners: vuln severity: HIGH,CRITICAL format: sarif output: trivy-results.sarif - - name: Upload Trivy results to GitHub Security + - name: Upload SARIF uses: github/codeql-action/upload-sarif@v3 if: always() with: diff --git a/README.md b/README.md index 35e01e5..0fba1d5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -[![Release Server](https://github.com/dvcdsys/code-index/actions/workflows/release-server.yml/badge.svg)](https://github.com/dvcdsys/code-index/actions/workflows/release-server.yml) +[![CI: Server](https://github.com/dvcdsys/code-index/actions/workflows/ci-server.yml/badge.svg)](https://github.com/dvcdsys/code-index/actions/workflows/ci-server.yml) +[![CI: CLI](https://github.com/dvcdsys/code-index/actions/workflows/ci-cli.yml/badge.svg)](https://github.com/dvcdsys/code-index/actions/workflows/ci-cli.yml) +[![CodeQL](https://github.com/dvcdsys/code-index/actions/workflows/codeql.yml/badge.svg)](https://github.com/dvcdsys/code-index/actions/workflows/codeql.yml) +[![Security](https://github.com/dvcdsys/code-index/actions/workflows/security.yml/badge.svg)](https://github.com/dvcdsys/code-index/actions/workflows/security.yml) ``` ██████╗██╗██╗ ██╗ @@ -9,7 +12,8 @@ ╚═════╝╚═╝╚═╝ ╚═╝ Code IndeX ``` -[![Release CLI](https://github.com/dvcdsys/code-index/actions/workflows/release-cli.yml/badge.svg)](https://github.com/dvcdsys/code-index/actions/workflows/release-cli.yml) +[![Release: Server](https://github.com/dvcdsys/code-index/actions/workflows/release-server.yml/badge.svg)](https://github.com/dvcdsys/code-index/actions/workflows/release-server.yml) +[![Release: CLI](https://github.com/dvcdsys/code-index/actions/workflows/release-cli.yml/badge.svg)](https://github.com/dvcdsys/code-index/actions/workflows/release-cli.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker Hub](https://img.shields.io/docker/pulls/dvcdsys/code-index)](https://hub.docker.com/r/dvcdsys/code-index) @@ -560,14 +564,41 @@ cix watch stop && cix watch /path/to/project ## Releases -Cross-platform binaries are built with: +CLI and server ship on independent tag streams: + +| Component | Tag pattern | Workflow | Artifact | +|---|---|---|---| +| CLI (`cix`) | `cli/v*` (e.g. `cli/v0.4.0`) | `release-cli.yml` | `cix-{darwin,linux}-{amd64,arm64}.tar.gz` on a GitHub Release | +| Server (`cix-server`) | `server/v*` (e.g. `server/v0.3.0`) | `release-server.yml` | Docker images on Docker Hub (`:latest`, `:cu128`) | + +Bare `v*` tags are the historical pre-split CLI line — the installer +still falls back to them when no `cli/v*` release exists, but no new +bare-`v*` tags should be created. + +### Cutting a CLI release + +```bash +git tag cli/v0.4.0 +git push origin cli/v0.4.0 +``` + +GitHub Actions builds binaries for macOS + Linux (amd64 + arm64), +uploads them to a release named `cli/v0.4.0`, and the installer +automatically picks them up on the next run. + +### Cutting a server release + +See `doc/DOCKER_TAGS.md` and the T9 step in `.claude/CLAUDE.md`. + +### Local cross-build (no release) ```bash cd cli -make release VERSION=v0.1.0 +make release VERSION=v0.4.0 ``` -This produces archives for macOS and Linux (amd64 + arm64) in `cli/dist/`, plus a `checksums.txt`. Upload them to a GitHub Release and the `install.sh` installer will pick up the latest version automatically. +Produces archives in `cli/dist/` plus `checksums.txt`. Useful for +testing the artifact format before pushing a tag. Supported targets: `darwin-arm64`, `darwin-amd64`, `linux-arm64`, `linux-amd64`. diff --git a/cli/Makefile b/cli/Makefile index 595bb00..18d3e54 100644 --- a/cli/Makefile +++ b/cli/Makefile @@ -28,8 +28,13 @@ clean: # Builds binaries for macOS and Linux (amd64 + arm64), # packages each into a .tar.gz archive ready for GitHub Releases. # +# Production CLI releases use the `cli/v*` tag namespace and are built +# by .github/workflows/release-cli.yml — `make release` is for local +# cross-builds (smoke-testing the artifact format). +# # Usage: -# make release VERSION=v0.1.0 +# make release VERSION=v0.4.0 # local archives only +# git tag cli/v0.4.0 && git push --tags # actual production release # # Output: # dist/cix-darwin-arm64.tar.gz diff --git a/cli/cmd/root.go b/cli/cmd/root.go index b5dc78e..07e109d 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -52,6 +52,10 @@ var rootCmd = &cobra.Command{ Search by meaning, not just text. Works with any agent or terminal. Files are automatically re-indexed when changed via the file watcher.`, Run: func(cmd *cobra.Command, args []string) { + if showVersion, _ := cmd.Flags().GetBool("version"); showVersion { + fmt.Printf("cix %s\n", Version) + return + } printBanner() cmd.Help() }, diff --git a/cli/cmd/version.go b/cli/cmd/version.go new file mode 100644 index 0000000..d5190fd --- /dev/null +++ b/cli/cmd/version.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "fmt" + "runtime" + + "github.com/spf13/cobra" +) + +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Print cix CLI version", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("cix %s %s/%s\n", Version, runtime.GOOS, runtime.GOARCH) + }, +} + +func init() { + rootCmd.AddCommand(versionCmd) + rootCmd.Flags().BoolP("version", "v", false, "Print version and exit") +} diff --git a/install.sh b/install.sh index 976ac21..10a9443 100755 --- a/install.sh +++ b/install.sh @@ -2,8 +2,18 @@ set -euo pipefail # cix installer -# Usage: curl -fsSL https://raw.githubusercontent.com//cix/main/install.sh | bash -# or: ./install.sh [--version v1.0.0] [--bin-dir /usr/local/bin] +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/dvcdsys/code-index/main/install.sh | bash +# ./install.sh [--version cli/v0.4.0] [--bin-dir /usr/local/bin] [--force] +# +# Re-running upgrades to the latest CLI release. If the requested version +# is already installed the script exits early — pass --force to reinstall +# anyway. +# +# Tag scheme: CLI releases live under `cli/v*`, server releases under +# `server/v*`. Bare `v*` tags are the historical pre-split CLI line and +# are used as a fallback only when no `cli/v*` release exists yet. REPO="dvcdsys/code-index" BINARY_NAME="cix" @@ -13,12 +23,27 @@ DEFAULT_BIN_DIR="/usr/local/bin" VERSION="" BIN_DIR="$DEFAULT_BIN_DIR" +FORCE=0 + +usage() { + cat <] [--bin-dir ] [--force] + +Options: + --version Install a specific tag (e.g. cli/v0.4.0). Default: latest cli/v*. + --bin-dir Install directory. Default: ${DEFAULT_BIN_DIR}. + --force Reinstall even if the same version is already present. + -h, --help Show this help. +EOF +} while [[ $# -gt 0 ]]; do case "$1" in --version) VERSION="$2"; shift 2 ;; --bin-dir) BIN_DIR="$2"; shift 2 ;; - *) echo "Unknown argument: $1"; exit 1 ;; + --force) FORCE=1; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;; esac done @@ -31,7 +56,7 @@ case "$OS" in Darwin) OS="darwin" ;; Linux) OS="linux" ;; *) - echo "Unsupported OS: $OS (supported: macOS, Linux)" + echo "Unsupported OS: $OS (supported: macOS, Linux)" >&2 exit 1 ;; esac @@ -40,7 +65,7 @@ case "$ARCH" in x86_64) ARCH="amd64" ;; arm64|aarch64) ARCH="arm64" ;; *) - echo "Unsupported architecture: $ARCH (supported: x86_64, arm64)" + echo "Unsupported architecture: $ARCH (supported: x86_64, arm64)" >&2 exit 1 ;; esac @@ -51,42 +76,72 @@ PLATFORM="${OS}-${ARCH}" if [ -z "$VERSION" ]; then echo "Fetching latest CLI release..." - # Search for latest tag starting with cli/ - VERSION=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases" \ + RELEASES_JSON=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases?per_page=30") + + # 1) Prefer cli/v* tags (post-split scheme). + VERSION=$(printf '%s' "$RELEASES_JSON" \ | grep '"tag_name"' \ - | grep 'cli/' \ - | head -1 \ - | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') - + | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/' \ + | grep '^cli/v' \ + | head -1 || true) + + # 2) Fall back to bare v* (historical CLI line), explicitly excluding server/v*. if [ -z "$VERSION" ]; then - echo "Failed to fetch latest version from cli/* tags. Trying latest release..." - VERSION=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ + VERSION=$(printf '%s' "$RELEASES_JSON" \ | grep '"tag_name"' \ - | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') + | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/' \ + | grep '^v' \ + | head -1 || true) fi - + if [ -z "$VERSION" ]; then - echo "Failed to fetch version. Specify with --version." + echo "Failed to find a CLI release. Specify with --version ." >&2 exit 1 fi fi -# Strip cli/ prefix for display and download if present +# Strip cli/ prefix for display and binary `--version` comparison. CLEAN_VERSION="${VERSION#cli/}" -echo "Installing cix ${CLEAN_VERSION} (${PLATFORM})..." +# ── Skip if already installed at the same version ───────────────────────────── + +if [ "$FORCE" -ne 1 ] && command -v "$BINARY_NAME" >/dev/null 2>&1; then + # New binaries print "cix v0.4.0 darwin/arm64"; + # historical binaries print "cix version v0.2.7". + # Pick the first v-prefixed semver-looking token. + CURRENT=$("$BINARY_NAME" --version 2>/dev/null \ + | head -1 \ + | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+[A-Za-z0-9.+-]*' \ + | head -1 || true) + if [ -n "$CURRENT" ] && [ "$CURRENT" = "$CLEAN_VERSION" ]; then + echo "✓ cix ${CLEAN_VERSION} already installed at $(command -v "$BINARY_NAME")" + echo " Pass --force to reinstall." + exit 0 + fi + if [ -n "$CURRENT" ]; then + echo "Upgrading cix ${CURRENT} → ${CLEAN_VERSION}..." + else + echo "Installing cix ${CLEAN_VERSION} (existing binary version unknown)..." + fi +else + echo "Installing cix ${CLEAN_VERSION} (${PLATFORM})..." +fi # ── Download ────────────────────────────────────────────────────────────────── ARCHIVE="${BINARY_NAME}-${PLATFORM}.tar.gz" -# Note: GitHub release assets are attached to the tag. -# If tag is cli/v0.2.0, the download URL uses the full tag name. +# GitHub release download URLs preserve slashes in tag names verbatim, +# so `cli/v0.4.0` becomes `.../releases/download/cli/v0.4.0/...`. DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${VERSION}/${ARCHIVE}" TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT echo "Downloading ${DOWNLOAD_URL}..." -curl -fsSL "$DOWNLOAD_URL" -o "${TMP_DIR}/${ARCHIVE}" +if ! curl -fsSL "$DOWNLOAD_URL" -o "${TMP_DIR}/${ARCHIVE}"; then + echo "Failed to download ${DOWNLOAD_URL}" >&2 + echo "Check that the release exists and contains ${ARCHIVE}." >&2 + exit 1 +fi tar -xzf "${TMP_DIR}/${ARCHIVE}" -C "$TMP_DIR" @@ -94,7 +149,7 @@ tar -xzf "${TMP_DIR}/${ARCHIVE}" -C "$TMP_DIR" BINARY="${TMP_DIR}/${BINARY_NAME}" if [ ! -f "$BINARY" ]; then - echo "Binary not found in archive: ${BINARY_NAME}" + echo "Binary not found in archive: ${BINARY_NAME}" >&2 exit 1 fi @@ -109,18 +164,31 @@ fi # ── Verify ──────────────────────────────────────────────────────────────────── -if command -v "$BINARY_NAME" &>/dev/null; then - INSTALLED_VERSION=$("$BINARY_NAME" --version 2>&1 | head -1) - echo "" - echo "✓ cix installed: ${INSTALLED_VERSION}" - echo " Location: $(command -v $BINARY_NAME)" - echo "" - echo "Next steps:" - echo " cix config set api.url http://localhost:21847" - echo " cix config set api.key " - echo " cix init /path/to/your/project" +INSTALLED_PATH="${BIN_DIR}/${BINARY_NAME}" +INSTALLED_VERSION=$("$INSTALLED_PATH" --version 2>/dev/null \ + | head -1 \ + | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+[A-Za-z0-9.+-]*' \ + | head -1 || true) + +echo "" +if [ -n "$INSTALLED_VERSION" ]; then + echo "✓ cix ${INSTALLED_VERSION} installed at ${INSTALLED_PATH}" else + echo "✓ cix ${CLEAN_VERSION} installed at ${INSTALLED_PATH}" +fi + +# Warn if a different cix is shadowing this one on PATH. +PATH_BIN=$(command -v "$BINARY_NAME" 2>/dev/null || true) +if [ -n "$PATH_BIN" ] && [ "$PATH_BIN" != "$INSTALLED_PATH" ]; then echo "" - echo "✓ cix installed to ${BIN_DIR}/${BINARY_NAME}" - echo " Add ${BIN_DIR} to your PATH if needed." -fi \ No newline at end of file + echo "⚠ Another cix is first on PATH: ${PATH_BIN}" + echo " Add ${BIN_DIR} earlier in PATH or remove the other binary." +elif [ -z "$PATH_BIN" ]; then + echo " Add ${BIN_DIR} to your PATH if needed." +fi + +echo "" +echo "Next steps:" +echo " cix config set api.url http://localhost:21847" +echo " cix config set api.key " +echo " cix init /path/to/your/project"