Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ CIX_BOOTSTRAP_ADMIN_PASSWORD=change-me-on-first-login

# ── Networking + storage ──────────────────────────────────────────────────
CIX_PORT=21847
# Interface to listen on. Unset (the default) means every interface, which is
# what a container needs. Set to 127.0.0.1 to make the server reachable only
# from this machine — the right choice for a desktop install, where exposing a
# code index to the whole LAN is rarely intended. Bare address, no port.
# CIX_BIND_ADDR=127.0.0.1
CIX_CHROMA_PERSIST_DIR=~/.cix/data/chroma
CIX_SQLITE_PATH=~/.cix/data/sqlite/projects.db
CIX_GGUF_CACHE_DIR=~/.cix/data/models
Expand Down
164 changes: 164 additions & 0 deletions .github/workflows/release-mac.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
name: "Release: macOS app"

# Triggered by macOS-app tags (e.g. `mac/v0.1.0`). This is a third, independent
# tag stream alongside `server/v*` and `cli/v*`, and it releases exactly one
# thing: the menu bar app, ~4 MB, holding a single executable.
#
# The server it manages is NOT built here. It ships from the server/v* tag
# stream — the same tag and the same workflow run that publishes the Docker
# images (see release-server.yml's macos-runtime job) — so a Mac install and a
# container on the same version are the same server. cix.app downloads it into
# ~/.cix/runtime/ and updates it independently of itself.
#
# That means this workflow no longer needs a server/v* or cli/v* tag to be
# reachable: nothing here is stamped with them.
on:
push:
tags:
- "mac/v*"
workflow_dispatch:
inputs:
ref:
description: "Ref to build (branch, tag, or SHA)"
required: true
version:
description: "App version, without the mac/v prefix (e.g. 0.1.0)"
required: true

permissions:
contents: write

jobs:
build:
name: Build cix.app + DMG
# The bundled llama-server is macOS arm64 only (upstream ships no macOS
# x86_64 asset), so the whole app is Apple Silicon only and must be built
# natively. macos-latest is arm64.
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
ref: ${{ github.event.inputs.ref || github.ref }}

- name: Resolve version
id: ver
run: |
set -euo pipefail

if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
MAC_VERSION="${{ github.event.inputs.version }}"
else
MAC_VERSION="${GITHUB_REF_NAME#mac/v}"
fi
echo "mac=$MAC_VERSION" >> "$GITHUB_OUTPUT"
echo "app=$MAC_VERSION"

- name: Set up Go
uses: actions/setup-go@v7
with:
# Only the CLI module is built here — the launcher lives in it.
go-version-file: cli/go.mod
cache-dependency-path: cli/go.sum

- name: Build cix.app
env:
MAC_VERSION: ${{ steps.ver.outputs.mac }}
run: mac/scripts/build-app.sh

- name: Verify the bundle
run: |
set -euo pipefail
APP=mac/dist/cix.app

# Ad-hoc signatures do not satisfy Gatekeeper, but they do have to be
# internally consistent — --strict is what catches a bundle whose
# sealed resources drifted from what is on disk.
codesign --verify --strict --verbose=2 "$APP"

# Reports the launcher's own version, and that no runtime is installed
# — correct on a build machine, where the two halves have not met yet.
"$APP/Contents/MacOS/cix-launcher" -report

# The app must contain exactly one executable. A leftover cix-server
# or llama/ from a pre-split build would be sealed into the signature
# and shipped as ~90 MB nobody uses.
found="$(find "$APP/Contents/MacOS" -type f | wc -l | tr -d ' ')"
if [ "$found" != "1" ]; then
echo "::error title=Unexpected bundle contents::Contents/MacOS holds $found files, expected only cix-launcher"
find "$APP/Contents/MacOS" -type f
exit 1
fi

- name: Build DMG
env:
MAC_VERSION: ${{ steps.ver.outputs.mac }}
run: mac/scripts/make-dmg.sh

- name: Compute checksums
working-directory: mac/dist
# The updater verifies the download against this file. Without a
# Developer ID signature it is the only integrity check there is.
run: shasum -a 256 ./*.dmg > checksums.txt

- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: cix-macos-arm64
path: |
mac/dist/*.dmg
mac/dist/checksums.txt

- name: Create GitHub release
if: github.event_name == 'push'
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
name: "macOS app ${{ steps.ver.outputs.mac }}"
files: |
mac/dist/*.dmg
mac/dist/checksums.txt
generate_release_notes: true
# Server releases own the "latest" pointer — the Docker image is the
# primary deliverable of this project. Mac installs filter by the
# `mac/` tag prefix.
make_latest: "false"
body: |
## cix for macOS — Apple Silicon

A menu bar app for a local cix server: start and stop it, see what
its embedding provider is doing, and open the dashboard. Everything
runs on your machine.

### Install

1. Download the `.dmg` below and open it.
2. Drag **cix.app** onto **Applications**.
3. Open it from Applications.

On first launch the app downloads the server itself — `cix-server`,
the `cix` CLI and a Metal-accelerated `llama-server`, about 40 MB —
from the newest [server release](https://github.com/dvcdsys/code-index/releases?q=server%2Fv).
That is the same build the Docker images are cut from, and it
updates on its own schedule: a new server does not need a new app.

### First launch will be blocked — this is expected

cix is open source and is **not** signed with a paid Apple Developer
certificate, so macOS refuses the first launch with "cix cannot be
verified".

To allow it: **System Settings → Privacy & Security**, scroll to
**Security**, then click **Open Anyway** next to the message about cix.

On macOS 15 and later the old right-click → Open shortcut no longer
works — you have to use System Settings. You only need to do this
once per installed version.

### Verify the download

```bash
shasum -a 256 -c checksums.txt
```

Requires macOS 13 or later on Apple Silicon. There is no Intel build:
upstream llama.cpp publishes no macOS x86_64 release asset.
117 changes: 116 additions & 1 deletion .github/workflows/release-server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,88 @@ jobs:
openapi=doc
tags: ${{ steps.tags.outputs.tags }}

macos-runtime:
name: Build macOS runtime (arm64)
# The same server, packaged for a Mac instead of a container: cix-server,
# the cix CLI and a Metal llama-server, installed by cix.app into
# ~/.cix/runtime/<version>/. It ships from THIS tag, not from mac/v*, so a
# Mac and a container on the same version are running the same server.
#
# macos-latest is arm64, and it has to be: upstream llama.cpp publishes one
# macOS asset, macos-arm64, so there is no x86_64 build to make.
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
ref: ${{ github.event.inputs.ref || github.ref }}
# Full history: the CLI version comes from `git describe` against a
# tag that is not the tag being built.
fetch-depth: 0

- name: Resolve versions
id: ver
run: |
set -euo pipefail

if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
SERVER_VERSION="${{ github.event.inputs.version }}"
else
SERVER_VERSION="${GITHUB_REF_NAME#server/}"
fi
# The Docker tags are v-prefixed; the runtime version is not. It
# becomes a directory name under ~/.cix/runtime/ and is compared
# against the tag stream with the prefix already stripped.
SERVER_VERSION="${SERVER_VERSION#v}"

CLI_TAG="$(git describe --tags --match 'cli/v*' --abbrev=0 2>/dev/null || true)"
if [ -z "$CLI_TAG" ]; then
echo "::error title=No CLI version::The runtime bundles the cix CLI, and no cli/v* tag is reachable from this commit — it would ship stamped 0.0.0-dev. Cut server/v* tags on main."
exit 1
fi

{
echo "server=$SERVER_VERSION"
echo "cli=${CLI_TAG#cli/v}"
} >> "$GITHUB_OUTPUT"

echo "server=$SERVER_VERSION cli=$CLI_TAG"

- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: server/go.mod
cache-dependency-path: |
server/go.sum
cli/go.sum

- name: Set up Node
# The server binary embeds the built dashboard via go:embed, so this
# needs the frontend toolchain even though nothing here is JavaScript.
uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
cache-dependency-path: server/dashboard/package-lock.json

- name: Build the runtime
env:
SERVER_VERSION: ${{ steps.ver.outputs.server }}
CLI_VERSION: ${{ steps.ver.outputs.cli }}
# The script round-trips the tarball it writes before returning:
# extract, codesign --verify --strict every Mach-O, check llama-server's
# @rpath dependencies, and exec the server. That check is in the script
# rather than here so local builds get it too, and because the failure
# it catches is silent — a signature the kernel rejects is SIGKILL with
# empty stderr.
run: mac/scripts/build-runtime.sh

- name: Upload the runtime
uses: actions/upload-artifact@v7
with:
name: cix-runtime-darwin-arm64
path: mac/dist/*.tar.gz

prune:
name: Prune stale Docker Hub tags
needs: [docker-cpu, docker-cuda]
Expand All @@ -178,7 +260,10 @@ jobs:

release:
name: Publish release
needs: [docker-cpu, docker-cuda]
# macos-runtime is a hard dependency, not a nicety: cix.app installs its
# server from this release's assets, so a release published without the
# runtime attached is one no Mac can install or update to.
needs: [docker-cpu, docker-cuda, macos-runtime]
runs-on: ubuntu-latest
# Skip GitHub release creation on manual rebuilds — the release
# already exists for the version being rebuilt.
Expand All @@ -191,11 +276,30 @@ jobs:
id: ver
run: echo "version=${GITHUB_REF_NAME#server/}" >> "$GITHUB_OUTPUT"

- name: Download the macOS runtime
uses: actions/download-artifact@v8
with:
name: cix-runtime-darwin-arm64
path: dist

- name: Compute checksums
working-directory: dist
# cix.app verifies its download against this file. Without a Developer
# ID signature it is the only integrity check there is.
#
# sha256sum, not shasum: this job runs on ubuntu, and that is what
# release-cli.yml uses there. The macOS jobs use shasum because macOS
# ships no sha256sum. Same output format either way.
run: sha256sum ./*.tar.gz > checksums.txt

- name: Create GitHub release
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
with:
name: "Server ${{ steps.ver.outputs.version }}"
generate_release_notes: true
files: |
dist/*.tar.gz
dist/checksums.txt
body: |
## Docker Images

Expand All @@ -204,6 +308,17 @@ jobs:
| CPU (multi-arch) | `dvcdsys/code-index:${{ steps.ver.outputs.version }}` |
| CUDA 12.8 | `dvcdsys/code-index:${{ steps.ver.outputs.version }}-cu128` |

## macOS (Apple Silicon)

The same server for a Mac, installed and managed by
[cix.app](https://github.com/dvcdsys/code-index/releases?q=mac%2Fv).
The app downloads it on its own — the tarball is attached so the
checksums cover it, not because it needs downloading by hand.

| Asset | Contents |
|---|---|
| `cix-runtime-*-darwin-arm64.tar.gz` | `cix-server`, the `cix` CLI, Metal `llama-server` |

site-version:
name: Check the site advertises this release
needs: [docker-cpu, docker-cuda]
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ cli/dist/
# Server build artifacts + runtime logs
server/dist/
server/exec.log

# macOS app build artifacts (mac/scripts/build-app.sh, make-dmg.sh).
# The committed inputs live in mac/Resources/ and mac/Info.plist.in; everything
# under mac/dist/ is generated. Disk images are never tracked — the release
# workflow publishes them as GitHub release assets.
mac/dist/
*.dmg
# Root scratch dir only — anchored: a bare "tmp" would silently ignore any
# nested file/dir named tmp (same pitfall as the docs/ pattern below).
/tmp/
Expand Down
2 changes: 2 additions & 0 deletions cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/dvcdsys/code-index/cli
go 1.25.12

require (
fyne.io/systray v1.12.2
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
Expand Down Expand Up @@ -33,6 +34,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/jsonschema-go v0.4.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
Expand Down
4 changes: 4 additions & 0 deletions cli/go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
fyne.io/systray v1.12.2 h1:Y8DZxgLHsVQt6rY9Zrkkg+j67S7vv/1F2viOWKPpVeA=
fyne.io/systray v1.12.2/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
Expand Down Expand Up @@ -41,6 +43,8 @@ github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJ
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
Expand Down
Loading