Automate desktop release binaries
CI / Build (push) Waiting to run
CI / Test (push) Waiting to run
Release / Test release commit (push) Waiting to run
Release / Build macOS Apple Silicon (push) Blocked by required conditions
Release / Build Linux ARM64 (push) Blocked by required conditions
Release / Build Linux x86_64 (push) Blocked by required conditions
Release / Build Windows x86_64 CLI (push) Blocked by required conditions
Release / Upload GitHub release assets (push) Blocked by required conditions
Release / Publish npm packages (push) Blocked by required conditions

This commit is contained in:
2026-06-27 13:27:10 -05:00
parent 415275c5a1
commit 47ce998b4e
25 changed files with 3237 additions and 34 deletions
+28
View File
@@ -5,12 +5,16 @@ on:
paths: paths:
- 'src/**' - 'src/**'
- 'tests/**' - 'tests/**'
- 'cassady-desktop/**'
- 'docs/**' - 'docs/**'
- 'plans/**' - 'plans/**'
- '*.md' - '*.md'
- 'Cargo.toml' - 'Cargo.toml'
- 'Cargo.lock' - 'Cargo.lock'
- 'build.rs' - 'build.rs'
- 'package.json'
- 'npm/**'
- '.github/workflows/**'
permissions: permissions:
contents: read contents: read
@@ -26,6 +30,18 @@ jobs:
- name: Check out repository - name: Check out repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Linux desktop build dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
pkg-config \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
@@ -42,6 +58,18 @@ jobs:
- name: Check out repository - name: Check out repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Linux desktop build dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
pkg-config \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
+426
View File
@@ -0,0 +1,426 @@
name: Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build, for example v0.4.0'
required: true
type: string
replace_assets:
description: 'Delete existing release assets before uploading rebuilt assets'
required: true
default: true
type: boolean
publish_github_release:
description: 'Publish the GitHub release after uploading assets'
required: true
default: false
type: boolean
publish_npm:
description: 'Publish npm packages after release assets are built. Requires the NPM_TOKEN repository secret.'
required: true
default: false
type: boolean
permissions:
contents: write
concurrency:
group: release-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
jobs:
test:
name: Test release commit
runs-on: ubuntu-24.04
steps:
- name: Check out release ref
uses: actions/checkout@v4
with:
ref: ${{ env.RELEASE_TAG }}
- name: Install Linux desktop build dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
pkg-config \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo artifacts
uses: Swatinem/rust-cache@v2
- name: Confirm version matches tag
run: |
version=$(awk -F\" '/^version = / { print $2; exit }' Cargo.toml)
test "${RELEASE_TAG}" = "v${version}"
- name: Run tests
run: cargo test --locked --all-targets
build-macos:
name: Build macOS Apple Silicon
needs: test
runs-on: macos-14
steps:
- name: Check out release ref
uses: actions/checkout@v4
with:
ref: ${{ env.RELEASE_TAG }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: Cache Cargo artifacts
uses: Swatinem/rust-cache@v2
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: cassady-desktop/package-lock.json
- name: Install desktop frontend dependencies
working-directory: cassady-desktop
run: npm ci
- name: Install Tauri CLI
run: cargo install tauri-cli --version 2.11.3 --locked
- name: Build CLI binaries
run: cargo build -p cassady --release --locked --target aarch64-apple-darwin --bin cass --bin cassady
- name: Build desktop binary
working-directory: cassady-desktop
run: cargo tauri build --target aarch64-apple-darwin --no-bundle -- --locked
- name: Package archive
env:
TRIPLE: aarch64-apple-darwin
run: |
set -euo pipefail
name="cassady-${RELEASE_TAG}-${TRIPLE}"
mkdir -p "dist/${name}"
cp "target/${TRIPLE}/release/cass" "dist/${name}/cass"
cp "target/${TRIPLE}/release/cassady" "dist/${name}/cassady"
cp "target/${TRIPLE}/release/cassady-desktop" "dist/${name}/cassady-desktop"
cp README.md "dist/${name}/README.md"
tar -C dist -czf "dist/${name}.tar.gz" "${name}"
(cd dist && shasum -a 256 "${name}.tar.gz" > "${name}.tar.gz.sha256")
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: release-aarch64-apple-darwin
path: |
dist/cassady-${{ env.RELEASE_TAG }}-aarch64-apple-darwin.tar.gz
dist/cassady-${{ env.RELEASE_TAG }}-aarch64-apple-darwin.tar.gz.sha256
if-no-files-found: error
build-linux:
name: Build Linux ${{ matrix.label }}
needs: test
strategy:
fail-fast: false
matrix:
include:
- label: x86_64
runner: ubuntu-24.04
triple: x86_64-unknown-linux-gnu
- label: ARM64
runner: ubuntu-24.04-arm
triple: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.runner }}
steps:
- name: Check out release ref
uses: actions/checkout@v4
with:
ref: ${{ env.RELEASE_TAG }}
- name: Install Linux desktop build dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
pkg-config \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libxdo-dev \
libssl-dev \
patchelf
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.triple }}
- name: Cache Cargo artifacts
uses: Swatinem/rust-cache@v2
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: cassady-desktop/package-lock.json
- name: Install desktop frontend dependencies
working-directory: cassady-desktop
run: npm ci
- name: Install Tauri CLI
run: cargo install tauri-cli --version 2.11.3 --locked
- name: Build CLI binaries
run: cargo build -p cassady --release --locked --target "${{ matrix.triple }}" --bin cass --bin cassady
- name: Build desktop binary
working-directory: cassady-desktop
run: cargo tauri build --target "${{ matrix.triple }}" --no-bundle -- --locked
- name: Package archive
env:
TRIPLE: ${{ matrix.triple }}
run: |
set -euo pipefail
name="cassady-${RELEASE_TAG}-${TRIPLE}"
mkdir -p "dist/${name}"
cp "target/${TRIPLE}/release/cass" "dist/${name}/cass"
cp "target/${TRIPLE}/release/cassady" "dist/${name}/cassady"
cp "target/${TRIPLE}/release/cassady-desktop" "dist/${name}/cassady-desktop"
cp README.md "dist/${name}/README.md"
tar -C dist -czf "dist/${name}.tar.gz" "${name}"
(cd dist && shasum -a 256 "${name}.tar.gz" > "${name}.tar.gz.sha256")
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: release-${{ matrix.triple }}
path: |
dist/cassady-${{ env.RELEASE_TAG }}-${{ matrix.triple }}.tar.gz
dist/cassady-${{ env.RELEASE_TAG }}-${{ matrix.triple }}.tar.gz.sha256
if-no-files-found: error
build-windows-cli:
name: Build Windows x86_64 CLI
needs: test
runs-on: ubuntu-24.04
steps:
- name: Check out release ref
uses: actions/checkout@v4
with:
ref: ${{ env.RELEASE_TAG }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-gnu
- name: Cache Cargo artifacts
uses: Swatinem/rust-cache@v2
- name: Set up Zig
uses: goto-bus-stop/setup-zig@v2
with:
version: 0.14.1
- name: Install cargo-zigbuild
run: cargo install cargo-zigbuild --locked
- name: Build Windows CLI binaries
run: cargo zigbuild -p cassady --release --locked --target x86_64-pc-windows-gnu --bin cass --bin cassady
- name: Package archive
env:
TRIPLE: x86_64-pc-windows-gnu
run: |
set -euo pipefail
name="cassady-${RELEASE_TAG}-${TRIPLE}"
mkdir -p "dist/${name}"
cp "target/${TRIPLE}/release/cass.exe" "dist/${name}/cass.exe"
cp "target/${TRIPLE}/release/cassady.exe" "dist/${name}/cassady.exe"
cp README.md "dist/${name}/README.md"
(cd dist && zip -qr "${name}.zip" "${name}")
(cd dist && shasum -a 256 "${name}.zip" > "${name}.zip.sha256")
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: release-x86_64-pc-windows-gnu
path: |
dist/cassady-${{ env.RELEASE_TAG }}-x86_64-pc-windows-gnu.zip
dist/cassady-${{ env.RELEASE_TAG }}-x86_64-pc-windows-gnu.zip.sha256
if-no-files-found: error
github-release:
name: Upload GitHub release assets
needs:
- build-macos
- build-linux
- build-windows-cli
runs-on: ubuntu-24.04
env:
GH_TOKEN: ${{ github.token }}
REPLACE_ASSETS: ${{ github.event_name != 'workflow_dispatch' || inputs.replace_assets }}
PUBLISH_GITHUB_RELEASE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_github_release }}
steps:
- name: Check out release ref
uses: actions/checkout@v4
with:
ref: ${{ env.RELEASE_TAG }}
- name: Download release artifacts
uses: actions/download-artifact@v4
with:
pattern: release-*
path: dist
merge-multiple: true
- name: Verify checksums
run: |
set -euo pipefail
ls -lh dist/cassady-${RELEASE_TAG}-*
for sum in dist/cassady-${RELEASE_TAG}-*.sha256; do
(cd dist && shasum -a 256 -c "$(basename "$sum")")
done
- name: Choose release notes
id: notes
run: |
set -euo pipefail
tag="${RELEASE_TAG}"
if [ -f "docs/releases/${tag}.md" ]; then
echo "file=docs/releases/${tag}.md" >> "$GITHUB_OUTPUT"
elif [ -f "dist/RELEASE_NOTES_${tag}.md" ]; then
echo "file=dist/RELEASE_NOTES_${tag}.md" >> "$GITHUB_OUTPUT"
else
printf '## Cassady %s\n\nRelease assets for Cassady %s.\n' "$tag" "$tag" > "dist/RELEASE_NOTES_${tag}.md"
echo "file=dist/RELEASE_NOTES_${tag}.md" >> "$GITHUB_OUTPUT"
fi
- name: Create or update release assets
run: |
set -euo pipefail
tag="${RELEASE_TAG}"
notes="${{ steps.notes.outputs.file }}"
if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release create "$tag" \
--repo "$GITHUB_REPOSITORY" \
--title "Cassady ${tag}" \
--notes-file "$notes" \
--draft \
--verify-tag
else
gh release edit "$tag" \
--repo "$GITHUB_REPOSITORY" \
--title "Cassady ${tag}" \
--notes-file "$notes"
fi
if [ "$REPLACE_ASSETS" = "true" ]; then
gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' | while IFS= read -r asset; do
[ -z "$asset" ] && continue
gh release delete-asset "$tag" "$asset" --repo "$GITHUB_REPOSITORY" -y
done
fi
gh release upload "$tag" \
dist/cassady-${tag}-aarch64-apple-darwin.tar.gz \
dist/cassady-${tag}-aarch64-apple-darwin.tar.gz.sha256 \
dist/cassady-${tag}-x86_64-unknown-linux-gnu.tar.gz \
dist/cassady-${tag}-x86_64-unknown-linux-gnu.tar.gz.sha256 \
dist/cassady-${tag}-aarch64-unknown-linux-gnu.tar.gz \
dist/cassady-${tag}-aarch64-unknown-linux-gnu.tar.gz.sha256 \
dist/cassady-${tag}-x86_64-pc-windows-gnu.zip \
dist/cassady-${tag}-x86_64-pc-windows-gnu.zip.sha256 \
--repo "$GITHUB_REPOSITORY" \
--clobber
if [ "$PUBLISH_GITHUB_RELEASE" = "true" ]; then
release_id=$(gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json databaseId --jq .databaseId)
gh api \
--method PATCH \
"repos/${GITHUB_REPOSITORY}/releases/${release_id}" \
-F draft=false \
-F prerelease=false \
-f make_latest=true >/dev/null
fi
npm-publish:
name: Publish npm packages
needs:
- github-release
if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_npm }}
runs-on: ubuntu-24.04
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- name: Check out release ref
uses: actions/checkout@v4
with:
ref: ${{ env.RELEASE_TAG }}
- name: Download release artifacts
uses: actions/download-artifact@v4
with:
pattern: release-*
path: dist
merge-multiple: true
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org/
- name: Rehydrate target binaries from release archives
run: |
set -euo pipefail
if [ -z "${NODE_AUTH_TOKEN:-}" ]; then
echo "::error::Set the NPM_TOKEN repository secret before publishing npm packages."
exit 1
fi
for archive in dist/cassady-${RELEASE_TAG}-*.tar.gz; do
tar -xzf "$archive" -C dist
done
unzip -q -o "dist/cassady-${RELEASE_TAG}-x86_64-pc-windows-gnu.zip" -d dist
for triple in aarch64-apple-darwin x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
src="dist/cassady-${RELEASE_TAG}-${triple}"
dst="target/${triple}/release"
mkdir -p "$dst"
cp "$src/cass" "$dst/cass"
cp "$src/cassady" "$dst/cassady"
cp "$src/cassady-desktop" "$dst/cassady-desktop"
chmod 755 "$dst/cass" "$dst/cassady" "$dst/cassady-desktop"
done
triple=x86_64-pc-windows-gnu
src="dist/cassady-${RELEASE_TAG}-${triple}"
dst="target/${triple}/release"
mkdir -p "$dst"
cp "$src/cass.exe" "$dst/cass.exe"
cp "$src/cassady.exe" "$dst/cassady.exe"
- name: Publish to npm
run: npm run npm:publish
+71 -9
View File
@@ -2,7 +2,26 @@
## Release process: build artifacts and create a draft GitHub release ## Release process: build artifacts and create a draft GitHub release
Use this process when preparing a Cassady (`cass`) release. Always create the GitHub release as a **draft** first; do not publish the final release unless the user explicitly asks. Use this process when preparing a Cassady (`cass`) release. Prefer the GitHub Actions release workflow over local cross-builds so macOS/Linux/Windows assets are built on runners instead of by hand. Always create the GitHub release as a **draft** first; do not publish the final release unless the user explicitly asks.
### 0. Preferred automated release workflow
The release workflow is `.github/workflows/release.yml`. On a pushed `v*` tag it builds and uploads draft GitHub release assets automatically. For an existing tag or to publish the release/npm packages, run it manually:
```sh
gh workflow run release.yml \
--repo owenqwenstarsky/cassady \
-f tag="$TAG" \
-f replace_assets=true \
-f publish_github_release=false \
-f publish_npm=false
```
Set `publish_github_release=true` only when the user explicitly asks to publish the GitHub release. Set `publish_npm=true` only when the user explicitly asks to publish npm packages, and make sure the `NPM_TOKEN` repository secret exists first. The workflow deletes/replaces old release assets, uploads the four current archives/checksums, and publishes npm packages from those runner-built archives.
Keep release notes in `docs/releases/${TAG}.md` when the workflow should apply curated notes. If that file is missing, the workflow falls back to generated placeholder notes.
Use the manual steps below only for local debugging or when the workflow cannot be used.
### 1. Review prior releases for consistency ### 1. Review prior releases for consistency
@@ -17,7 +36,7 @@ Keep release notes consistent with the existing format:
- Title: `## Cassady vX.Y.Z` - Title: `## Cassady vX.Y.Z`
- One short summary paragraph. - One short summary paragraph.
- `### Downloads` with four bullets in this order: macOS Apple Silicon, Linux x86_64, Linux ARM64, Windows x86_64. - `### Downloads` with four bullets in this order: macOS Apple Silicon, Linux x86_64, Linux ARM64, Windows x86_64.
- Note that each archive contains both `cass` and `cassady`; mention SHA-256 files. - Note that macOS/Linux archives contain `cass`, `cassady`, and `cassady-desktop`; the Windows archive contains `cass` and `cassady`; mention SHA-256 files.
- `### Highlights` with concise user-facing bullets. - `### Highlights` with concise user-facing bullets.
- Optional `### Upgrade notes` only when compatibility, config, or migration details matter. - Optional `### Upgrade notes` only when compatibility, config, or migration details matter.
- `### Install from source` with a `cargo install --git ... --tag vX.Y.Z` command. - `### Install from source` with a `cargo install --git ... --tag vX.Y.Z` command.
@@ -41,22 +60,30 @@ If the version is wrong, update `Cargo.toml` and `Cargo.lock` first, then commit
cargo +stable test --locked --all-targets cargo +stable test --locked --all-targets
``` ```
### 4. Build all release binaries ### 4. Build all release binaries manually only when needed
Prerequisites for cross-builds: stable Rust, `cargo-zigbuild`, Zig, and the needed Rust targets. The runner workflow should normally do this. Manual prerequisites for cross-builds are stable Rust, `cargo-zigbuild`, Tauri CLI, Zig, Node/npm, and the needed Rust targets.
```sh ```sh
rustup target add aarch64-apple-darwin x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu x86_64-pc-windows-gnu rustup target add aarch64-apple-darwin x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu x86_64-pc-windows-gnu
cargo install cargo-zigbuild --locked cargo install cargo-zigbuild tauri-cli --locked
``` ```
Build the same four targets used by previous releases: Build the same four CLI targets used by previous releases, then build desktop binaries for macOS/Linux. Do not build or upload DMG/installer artifacts unless the user explicitly asks.
```sh ```sh
cargo +stable build --release --locked --target aarch64-apple-darwin cargo +stable build --release --locked --target aarch64-apple-darwin
cargo +stable zigbuild --release --locked --target x86_64-unknown-linux-gnu cargo +stable zigbuild --release --locked --target x86_64-unknown-linux-gnu
cargo +stable zigbuild --release --locked --target aarch64-unknown-linux-gnu cargo +stable zigbuild --release --locked --target aarch64-unknown-linux-gnu
cargo +stable zigbuild --release --locked --target x86_64-pc-windows-gnu cargo +stable zigbuild --release --locked --target x86_64-pc-windows-gnu
(cd cassady-desktop && cargo tauri build --target aarch64-apple-darwin)
# Build Linux desktop binaries with `cargo tauri build` on Linux/Tauri builders. The packaging step accepts either:
# target/x86_64-unknown-linux-gnu/release/cassady-desktop
# target/aarch64-unknown-linux-gnu/release/cassady-desktop
# or the Tauri builder output paths:
# target/tauri-linux-x86_64/release/cassady-desktop
# target/tauri-linux-aarch64/release/cassady-desktop
``` ```
### 5. Rebuild the `dist/` artifacts ### 5. Rebuild the `dist/` artifacts
@@ -75,11 +102,40 @@ rm -rf \
mkdir -p dist mkdir -p dist
desktop_binary_for() {
case "$1" in
aarch64-apple-darwin)
if [ -f "target/aarch64-apple-darwin/release/cassady-desktop" ]; then
printf '%s\n' "target/aarch64-apple-darwin/release/cassady-desktop"
else
printf '%s\n' "target/release/cassady-desktop"
fi
;;
x86_64-unknown-linux-gnu)
if [ -f "target/x86_64-unknown-linux-gnu/release/cassady-desktop" ]; then
printf '%s\n' "target/x86_64-unknown-linux-gnu/release/cassady-desktop"
else
printf '%s\n' "target/tauri-linux-x86_64/release/cassady-desktop"
fi
;;
aarch64-unknown-linux-gnu)
if [ -f "target/aarch64-unknown-linux-gnu/release/cassady-desktop" ]; then
printf '%s\n' "target/aarch64-unknown-linux-gnu/release/cassady-desktop"
else
printf '%s\n' "target/tauri-linux-aarch64/release/cassady-desktop"
fi
;;
esac
}
for target in aarch64-apple-darwin x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do for target in aarch64-apple-darwin x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
name="cassady-${TAG}-${target}" name="cassady-${TAG}-${target}"
desktop_bin=$(desktop_binary_for "$target")
test -f "$desktop_bin"
mkdir -p "dist/${name}" mkdir -p "dist/${name}"
cp "target/${target}/release/cass" "dist/${name}/cass" cp "target/${target}/release/cass" "dist/${name}/cass"
cp "target/${target}/release/cassady" "dist/${name}/cassady" cp "target/${target}/release/cassady" "dist/${name}/cassady"
cp "$desktop_bin" "dist/${name}/cassady-desktop"
cp README.md "dist/${name}/README.md" cp README.md "dist/${name}/README.md"
tar -C dist -czf "dist/${name}.tar.gz" "${name}" tar -C dist -czf "dist/${name}.tar.gz" "${name}"
done done
@@ -103,6 +159,7 @@ Sanity-check the generated artifacts:
ls -lh dist/cassady-${TAG}-*.tar.gz dist/cassady-${TAG}-*.zip dist/cassady-${TAG}-*.sha256 ls -lh dist/cassady-${TAG}-*.tar.gz dist/cassady-${TAG}-*.zip dist/cassady-${TAG}-*.sha256
for sum in dist/cassady-${TAG}-*.sha256; do (cd dist && shasum -a 256 -c "$(basename "$sum")"); done for sum in dist/cassady-${TAG}-*.sha256; do (cd dist && shasum -a 256 -c "$(basename "$sum")"); done
tar -tzf "dist/cassady-${TAG}-aarch64-apple-darwin.tar.gz" | head tar -tzf "dist/cassady-${TAG}-aarch64-apple-darwin.tar.gz" | head
tar -tzf "dist/cassady-${TAG}-x86_64-unknown-linux-gnu.tar.gz" | grep cassady-desktop
unzip -l "dist/cassady-${TAG}-x86_64-pc-windows-gnu.zip" | head unzip -l "dist/cassady-${TAG}-x86_64-pc-windows-gnu.zip" | head
``` ```
@@ -122,7 +179,7 @@ Cassady vX.Y.Z ...
- Linux ARM64: `cassady-vX.Y.Z-aarch64-unknown-linux-gnu.tar.gz` - Linux ARM64: `cassady-vX.Y.Z-aarch64-unknown-linux-gnu.tar.gz`
- Windows x86_64: `cassady-vX.Y.Z-x86_64-pc-windows-gnu.zip` - Windows x86_64: `cassady-vX.Y.Z-x86_64-pc-windows-gnu.zip`
Each archive contains both `cass` and `cassady`. SHA-256 checksum files are included for every archive. The macOS and Linux archives contain `cass`, `cassady`, and `cassady-desktop`; launch the desktop app with `cass desktop`. The Windows archive contains `cass` and `cassady`. SHA-256 checksum files are included for every archive.
### Highlights ### Highlights
@@ -148,6 +205,8 @@ cargo +stable build --release --locked --target aarch64-apple-darwin
cargo +stable zigbuild --release --locked --target x86_64-unknown-linux-gnu cargo +stable zigbuild --release --locked --target x86_64-unknown-linux-gnu
cargo +stable zigbuild --release --locked --target aarch64-unknown-linux-gnu cargo +stable zigbuild --release --locked --target aarch64-unknown-linux-gnu
cargo +stable zigbuild --release --locked --target x86_64-pc-windows-gnu cargo +stable zigbuild --release --locked --target x86_64-pc-windows-gnu
(cd cassady-desktop && cargo tauri build --target aarch64-apple-darwin)
# Linux desktop binaries built with `cargo tauri build` on Linux/Tauri builders and copied into the Linux archives.
``` ```
```` ````
@@ -199,7 +258,7 @@ gh release view "$TAG" --repo owenqwenstarsky/cassady --json tagName,name,isDraf
Do **not** publish the draft or mark it as the final/latest release unless the user explicitly asks. Do **not** publish the draft or mark it as the final/latest release unless the user explicitly asks.
## NPM package process: prepare and publish the CLI packages ## NPM package process: prepare and publish the binary packages
Use this process when preparing Cassady for npm. The npm distribution is a tiny wrapper package plus platform-specific binary packages. Do not publish to npm unless the user explicitly asks; when they ask for a publish-ready setup, make sure the packages can be published with one command. Use this process when preparing Cassady for npm. The npm distribution is a tiny wrapper package plus platform-specific binary packages. Do not publish to npm unless the user explicitly asks; when they ask for a publish-ready setup, make sure the packages can be published with one command.
@@ -209,6 +268,7 @@ The committed npm tooling generates packages under `dist/npm/` from the current
- Wrapper package: `cassady` - Wrapper package: `cassady`
- Exposes both npm binaries: `cass` and `cassady`. - Exposes both npm binaries: `cass` and `cassady`.
- `cass desktop` launches the bundled desktop binary when the installed platform package includes one.
- Depends on the platform packages through `optionalDependencies` at the exact same version. - Depends on the platform packages through `optionalDependencies` at the exact same version.
- Platform packages: - Platform packages:
- `@cassady/cli-darwin-arm64` for `aarch64-apple-darwin` - `@cassady/cli-darwin-arm64` for `aarch64-apple-darwin`
@@ -220,7 +280,7 @@ Before the first publish, make sure the npm account owns or has publish access t
### 1. Build the release binaries first ### 1. Build the release binaries first
The npm packages copy binaries from `target/<target>/release/`, so run the same verification and target builds used for the GitHub release first: Prefer the `.github/workflows/release.yml` workflow with `publish_npm=true`; it rebuilds release assets on runners, rehydrates the npm package inputs from those archives, and publishes with `NPM_TOKEN`. For manual publishing, the npm packages copy binaries from the release build outputs, so run the same verification and target builds used for the GitHub release first. macOS/Linux packages include `cassady-desktop` so `cass desktop` works from npm installs; Windows remains CLI-only for now.
```sh ```sh
cargo +stable test --locked --all-targets cargo +stable test --locked --all-targets
@@ -228,6 +288,8 @@ cargo +stable build --release --locked --target aarch64-apple-darwin
cargo +stable zigbuild --release --locked --target x86_64-unknown-linux-gnu cargo +stable zigbuild --release --locked --target x86_64-unknown-linux-gnu
cargo +stable zigbuild --release --locked --target aarch64-unknown-linux-gnu cargo +stable zigbuild --release --locked --target aarch64-unknown-linux-gnu
cargo +stable zigbuild --release --locked --target x86_64-pc-windows-gnu cargo +stable zigbuild --release --locked --target x86_64-pc-windows-gnu
(cd cassady-desktop && cargo tauri build --target aarch64-apple-darwin)
# Linux desktop binaries built with `cargo tauri build` on Linux/Tauri builders, as in the GitHub release process.
``` ```
### 2. Generate and verify the npm package directories ### 2. Generate and verify the npm package directories
Generated
+1
View File
@@ -415,6 +415,7 @@ dependencies = [
"futures-util", "futures-util",
"ignore", "ignore",
"include_dir", "include_dir",
"libc",
"nanoid", "nanoid",
"pulldown-cmark", "pulldown-cmark",
"ratatui", "ratatui",
+1
View File
@@ -28,6 +28,7 @@ dirs = "5"
futures-util = "0.3" futures-util = "0.3"
ignore = "0.4" ignore = "0.4"
include_dir = "0.7" include_dir = "0.7"
libc = "0.2"
nanoid = "0.4" nanoid = "0.4"
ratatui = { version = "0.30", default-features = false, features = ["crossterm", "underline-color"] } ratatui = { version = "0.30", default-features = false, features = ["crossterm", "underline-color"] }
regex = "1" regex = "1"
+6 -5
View File
@@ -9,11 +9,11 @@ The project installs two equivalent commands, `cass` and `cassady`; examples use
## Current scope and limitations ## Current scope and limitations
- Provider support includes OpenAI-compatible chat/completions APIs plus the `ChatGPT Codex` preset for users already signed in to Codex. - Provider support includes OpenAI-compatible chat/completions APIs plus the `ChatGPT Codex` preset for users already signed in to Codex.
- The primary interface is an interactive terminal UI. - The primary interface is an interactive terminal UI; v0.4.0 also includes a desktop app preview launched with `cass desktop` on macOS/Linux binary installs.
- An experimental Rust embedding API for headless sessions is available (added in v0.2.6); it is useful for early integrations but not yet a stable long-term library contract. - An experimental Rust embedding API for headless sessions is available (added in v0.2.6); it is useful for early integrations but not yet a stable long-term library contract.
- Config and conversation state live under `~/.cass`. - Config and conversation state live under `~/.cass`.
- Windows binaries are built for releases, but deeper Windows terminal, path, shell, and filesystem polish is planned for a later release. - Windows binaries are built for releases, but deeper Windows terminal, path, shell, and filesystem polish is planned for a later release.
- `cass update` can update release-archive installs from official GitHub releases; external package managers should still be updated through their own tools. - `cass update` can update release-archive installs from official GitHub releases, including the companion desktop binary when present in a prebuilt archive; external package managers should still be updated through their own tools.
## Install with npm ## Install with npm
@@ -30,7 +30,7 @@ cass --version
cassady --version cassady --version
``` ```
The npm package installs a small launcher plus the matching Rust-built binary package for your platform. Supported npm platforms are macOS Apple Silicon, Linux x86_64, Linux ARM64, and Windows x86_64. The npm package installs a small launcher plus the matching Rust-built binary package for your platform. macOS and Linux packages include the `cassady-desktop` companion used by `cass desktop`; Windows remains CLI-only for now. Supported npm platforms are macOS Apple Silicon, Linux x86_64, Linux ARM64, and Windows x86_64.
## Install from source ## Install from source
@@ -78,6 +78,7 @@ Run `cass check` any time to validate JSON config, provider/model references, ac
cass [--model MODEL] [--cwd PATH] cass [--model MODEL] [--cwd PATH]
cass --resume <chat-id> cass --resume <chat-id>
cass --resume cass --resume
cass desktop
cass check cass check
cass login cass login
cass logout cass logout
@@ -85,7 +86,7 @@ cass setup
cass update cass update
``` ```
`cass --resume` without an id lists saved chats for the current directory. `cass update` checks official GitHub releases and can update both `cass` and `cassady` in the current install directory. When Cassady exits a chat, it prints a resume command for that session. `cass --resume` without an id lists saved chats for the current directory. `cass desktop` launches the desktop app in the background when the `cassady-desktop` binary is installed next to `cass` or on `PATH`; after it starts, you can close the terminal. `cass update` checks official GitHub releases and can update `cass`, `cassady`, and the companion desktop binary when present in the selected prebuilt archive. When Cassady exits a chat, it prints a resume command for that session.
Common in-chat commands: Common in-chat commands:
@@ -160,7 +161,7 @@ let session = SessionBuilder::new()
.await?; .await?;
``` ```
See [Experimental Rust embedding API](docs/embedding.md) for session creation, streamed events, approval handling, cancellation, and current limitations. See [Experimental Rust embedding API](docs/embedding.md) for session creation, streamed events, approval handling, cancellation, and current limitations. The desktop app is an in-repo reference embedding built on that API.
## More documentation ## More documentation
+45
View File
@@ -0,0 +1,45 @@
# Cassady Desktop
Cassady Desktop is the v0.4.0 desktop preview. It uses the same Cassady config, providers, tools, approval policy, and `~/.cass/conversations/*.jsonl` sessions as the terminal CLI.
## Launch from a release archive
macOS and Linux release archives include three binaries:
- `cass`
- `cassady`
- `cassady-desktop`
Keep them in the same directory or put all three on `PATH`, then launch the desktop app with:
```sh
cass desktop
```
`cass desktop` starts `cassady-desktop` as a detached background process, so you can close the terminal after the window opens. For debugging, keep it attached with:
```sh
cass desktop --foreground
```
If the desktop binary is somewhere else, set:
```sh
CASSADY_DESKTOP_BIN=/path/to/cassady-desktop cass desktop
```
Run `cass setup` first if Cassady has no configured provider.
## Development
From the repository root:
```sh
cd cassady-desktop
npm install
cargo tauri build --target aarch64-apple-darwin
```
`cargo tauri build` runs the frontend build and embeds the generated assets into the standalone binary. A plain `cargo build -p cassady-desktop` is useful for Rust checks, but do not package that output because it may run in Tauri dev-asset mode and open to a blank window when copied elsewhere.
For a GUI dev session, run the Vite dev server and the Tauri app using your local Tauri workflow. Release packaging uses the raw `cassady-desktop` binary, not DMG, app bundle, AppImage, MSI, or other installer artifacts.
+13 -1
View File
@@ -5,9 +5,21 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0b0d10" /> <meta name="theme-color" content="#0b0d10" />
<title>Cassady</title> <title>Cassady</title>
<style>
html,
body,
#root {
width: 100%;
height: 100%;
margin: 0;
background: #0b0d10;
color: #8b929d;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
</style>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root">loading Cassady…</div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>
+12
View File
@@ -1,3 +1,15 @@
fn main() { fn main() {
// Tauri embeds the built frontend into release binaries. `cargo tauri build`
// runs the frontend build before Cargo, so make Cargo rerun this build script
// whenever the generated frontend changes; otherwise a standalone desktop
// binary can accidentally contain stale or missing assets and open to a
// blank WebView.
println!("cargo:rerun-if-changed=../dist/index.html");
println!("cargo:rerun-if-changed=../dist/assets");
println!("cargo:rerun-if-changed=../index.html");
println!("cargo:rerun-if-changed=../src");
println!("cargo:rerun-if-changed=../vite.config.ts");
println!("cargo:rerun-if-changed=../package.json");
println!("cargo:rerun-if-changed=../package-lock.json");
tauri_build::build() tauri_build::build()
} }
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -29,7 +29,7 @@
"withGlobalTauri": false "withGlobalTauri": false
}, },
"bundle": { "bundle": {
"active": true, "active": false,
"targets": "all", "targets": "all",
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
+1 -1
View File
@@ -18,7 +18,7 @@ export default function App() {
if (!cwd) { if (!cwd) {
return ( return (
<div className="vignette scanlines grain flex h-screen items-center justify-center bg-[var(--color-bg)] font-mono text-xs text-[var(--color-fg-dim)]"> <div className="vignette scanlines grain flex h-screen w-screen items-center justify-center bg-[var(--color-bg)] font-mono text-xs text-[var(--color-fg-dim)]">
loading… loading…
</div> </div>
); );
+1 -1
View File
@@ -139,7 +139,7 @@ export function ChatShell({ cwd }: { cwd: string }) {
}, [configError, chat, turn.blocks, turn.pendingApproval, cwd, handleNew]); }, [configError, chat, turn.blocks, turn.pendingApproval, cwd, handleNew]);
return ( return (
<div className="vignette scanlines grain relative flex h-screen flex-col bg-[var(--color-bg)]"> <div className="vignette scanlines grain relative flex h-screen w-screen min-w-0 flex-col overflow-hidden bg-[var(--color-bg)]">
<TopBar <TopBar
chat={chat} chat={chat}
onNewChat={() => void handleNew()} onNewChat={() => void handleNew()}
+1
View File
@@ -6,6 +6,7 @@ import path from "path";
const host = process.env.TAURI_DEV_HOST; const host = process.env.TAURI_DEV_HOST;
export default defineConfig({ export default defineConfig({
base: "./",
plugins: [react(), tailwindcss()], plugins: [react(), tailwindcss()],
resolve: { resolve: {
alias: { alias: {
+14 -3
View File
@@ -1,6 +1,6 @@
# Commands # Commands
Cassady installs two equivalent binaries: `cass` and `cassady`. This page uses `cass`, but the same options and subcommands apply to `cassady`. Cassady installs two equivalent CLI binaries: `cass` and `cassady`. This page uses `cass`, but the same options and subcommands apply to `cassady`. macOS/Linux release archives also include a `cassady-desktop` companion binary launched by `cass desktop`.
## Top-level forms ## Top-level forms
@@ -11,11 +11,12 @@ cass check [OPTIONS]
cass login [OPTIONS] cass login [OPTIONS]
cass logout [OPTIONS] cass logout [OPTIONS]
cass setup [OPTIONS] cass setup [OPTIONS]
cass desktop [OPTIONS]
cass update [OPTIONS] cass update [OPTIONS]
cass --resume [CHAT_ID] cass --resume [CHAT_ID]
``` ```
Run `cass --help`, `cass check --help`, `cass login --help`, `cass logout --help`, `cass setup --help`, or `cass update --help` for the help generated by the current binary. Run `cass --help`, `cass check --help`, `cass login --help`, `cass logout --help`, `cass setup --help`, `cass desktop --help`, or `cass update --help` for the help generated by the current binary.
## Startup behavior ## Startup behavior
@@ -76,11 +77,21 @@ Opens an interactive menu for removing saved providers from Cassady config. Remo
Runs the interactive setup wizard in a terminal. It configures providers, authentication sources, and first models. OpenAI-compatible providers use API key environment-variable references; `ChatGPT Codex` uses local Codex auth. It updates `config.json`, `providers.json`, and `models.json` while preserving unrelated entries where possible. Runs the interactive setup wizard in a terminal. It configures providers, authentication sources, and first models. OpenAI-compatible providers use API key environment-variable references; `ChatGPT Codex` uses local Codex auth. It updates `config.json`, `providers.json`, and `models.json` while preserving unrelated entries where possible.
### `cass desktop`
Launches the Cassady desktop app and immediately detaches it from the terminal, so the app window can keep running after you close the terminal. The desktop binary must be installed as `cassady-desktop` next to `cass` or somewhere on `PATH`. You can also set `CASSADY_DESKTOP_BIN=/path/to/cassady-desktop`.
Useful option:
- `--foreground`: keep the desktop process attached to the terminal for debugging.
Use `cass --cwd PATH desktop` to choose the launch cwd/workspace root for the desktop app.
### `cass update` ### `cass update`
Checks official GitHub releases for Cassady, including Cassady prereleases, and updates the current install directory. The updater runs before provider/model config is loaded, so it can be used even if `~/.cass` is missing or invalid. Checks official GitHub releases for Cassady, including Cassady prereleases, and updates the current install directory. The updater runs before provider/model config is loaded, so it can be used even if `~/.cass` is missing or invalid.
By default, Cassady selects the matching prebuilt archive for the current platform, downloads the archive and `.sha256` file, verifies SHA-256, stages both `cass` and `cassady`, and replaces same-directory binaries with rollback backups. If no prebuilt archive is available, it can build from the selected release source when you choose source mode. By default, Cassady selects the matching prebuilt archive for the current platform, downloads the archive and `.sha256` file, verifies SHA-256, stages `cass` and `cassady`, stages `cassady-desktop` when the archive contains it, and replaces same-directory binaries with rollback backups. If no prebuilt archive is available, it can build from the selected release source when you choose source mode.
Useful options: Useful options:
+1 -1
View File
@@ -2,7 +2,7 @@
Cassady v0.2.6 includes an experimental Rust API for running headless agent sessions from another Rust program. The API is intended for early integrations and may change before Cassady declares a stable library contract. Cassady v0.2.6 includes an experimental Rust API for running headless agent sessions from another Rust program. The API is intended for early integrations and may change before Cassady declares a stable library contract.
The embedding API uses the same provider configuration, global instructions, prompts, access modes, tools, and JSONL conversation storage as the `cass` terminal UI. By default it reads and writes under `~/.cass`, so run `cass setup` first or create compatible `config.json`, `providers.json`, and `models.json` files programmatically. The embedding API uses the same provider configuration, global instructions, prompts, access modes, tools, and JSONL conversation storage as the `cass` terminal UI. By default it reads and writes under `~/.cass`, so run `cass setup` first or create compatible `config.json`, `providers.json`, and `models.json` files programmatically. The v0.4.0 desktop app in `cassady-desktop/` is a reference embedding built on this API.
## Minimal example ## Minimal example
+3 -3
View File
@@ -37,7 +37,7 @@ Example path usage:
cass --cwd C:\Users\alex\project cass --cwd C:\Users\alex\project
``` ```
Current docs and examples are primarily terminal-CLI oriented. Deeper Windows polish for terminal behavior, path handling, shell behavior, filesystem edge cases, and release usability is planned for a later release. Avoid assuming every Windows path or shell edge case is polished in the current version. Current docs and examples are primarily terminal-CLI oriented. The desktop preview is not packaged as a native Windows desktop executable; use the Linux binary under WSL/WSLg if you want to try it on Windows for now. Deeper Windows polish for terminal behavior, path handling, shell behavior, filesystem edge cases, and release usability is planned for a later release. Avoid assuming every Windows path or shell edge case is polished in the current version.
## Config location ## Config location
@@ -59,7 +59,7 @@ That directory contains `config.json`, `providers.json`, `models.json`, `global.
## Release artifacts and updates ## Release artifacts and updates
When using release archives, each archive contains both `cass` and `cassady`. Put the extracted binaries somewhere on your `PATH` or run them by explicit path. When using release archives, each archive contains both `cass` and `cassady`. macOS and Linux archives also contain `cassady-desktop`; put all extracted binaries somewhere on your `PATH` or keep them in the same directory so `cass desktop` can launch the desktop app.
`cass update` can update release-archive installs from official GitHub releases. It supports the same prebuilt targets as the release process: `cass update` can update release-archive installs from official GitHub releases. It supports the same prebuilt targets as the release process:
@@ -68,6 +68,6 @@ When using release archives, each archive contains both `cass` and `cassady`. Pu
- Linux ARM64: `aarch64-unknown-linux-gnu` - Linux ARM64: `aarch64-unknown-linux-gnu`
- Windows x86_64: `x86_64-pc-windows-gnu` - Windows x86_64: `x86_64-pc-windows-gnu`
On macOS and Linux, the updater replaces same-directory `cass` and `cassady` binaries with backups and rollback on failure. On Windows, replacing a running `.exe` is more constrained; if automatic replacement is unavailable, Cassady leaves staged files in place and reports manual copy guidance instead of partially modifying the install. On macOS and Linux, the updater replaces same-directory `cass` and `cassady` binaries with backups and rollback on failure. For prebuilt archives that include `cassady-desktop`, it installs that companion binary too. On Windows, replacing a running `.exe` is more constrained; if automatic replacement is unavailable, Cassady leaves staged files in place and reports manual copy guidance instead of partially modifying the install.
If Cassady is installed through a package manager in the future, prefer that package manager's update command instead of `cass update`. If Cassady is installed through a package manager in the future, prefer that package manager's update command instead of `cass update`.
+48
View File
@@ -0,0 +1,48 @@
## Cassady v0.4.0
Cassady v0.4.0 introduces a native desktop app preview alongside the existing terminal CLI, using the same agent engine, config, providers, tools, approvals, and JSONL conversation storage.
### Downloads
- macOS Apple Silicon: `cassady-v0.4.0-aarch64-apple-darwin.tar.gz`
- Linux x86_64: `cassady-v0.4.0-x86_64-unknown-linux-gnu.tar.gz`
- Linux ARM64: `cassady-v0.4.0-aarch64-unknown-linux-gnu.tar.gz`
- Windows x86_64: `cassady-v0.4.0-x86_64-pc-windows-gnu.zip`
The macOS and Linux archives contain `cass`, `cassady`, and `cassady-desktop`; launch the desktop app with `cass desktop`. The Windows archive contains `cass` and `cassady`. SHA-256 checksum files are included for every archive.
The desktop preview is distributed as a raw binary in the macOS/Linux archives, not as a DMG, app bundle, MSI, or Windows desktop executable. Windows users who want the desktop preview should use the Linux binary under WSL/WSLg for now.
### Highlights
- Adds `cass desktop`, which launches the `cassady-desktop` companion binary in the background so the window keeps running after the terminal closes.
- Adds a Tauri-based Cassady desktop app with a windowed chat surface, composer, transcript, streaming assistant text, reasoning display, tool blocks, approvals, cancellation, and model/access-mode controls.
- Reuses the real Cassady embedding API, config, provider registry, safety policy, built-in tools, and `~/.cass/conversations/*.jsonl` storage so desktop and CLI sessions stay interoperable.
- Ports the `cassady-web` terminal aesthetic into the app: IBM Plex fonts, dark chrome, scanlines, grain, vignette, glow effects, and shadcn-style primitives.
- Expands the embedding API with runtime model, access-mode, and reasoning-effort setters for embedded frontends.
- Adds `cassady-api` provider support for local API-style embedding flows without requiring an API key.
### Upgrade notes
No migration is required. The desktop app uses the existing Cassady config; run `cass setup` first if no provider is configured. Keep `cassady-desktop` next to `cass` or on `PATH` so `cass desktop` can find it. The desktop app preview is currently distributed for macOS Apple Silicon and Linux.
### Install from source
```sh
cargo install --git https://github.com/owenqwenstarsky/cassady --tag v0.4.0
```
### Verification
Built and tested with the GitHub Actions release workflow:
```sh
cargo test --locked --all-targets
cargo build -p cassady --release --locked --target aarch64-apple-darwin --bin cass --bin cassady
(cd cassady-desktop && cargo tauri build --target aarch64-apple-darwin --no-bundle -- --locked)
cargo build -p cassady --release --locked --target x86_64-unknown-linux-gnu --bin cass --bin cassady
(cd cassady-desktop && cargo tauri build --target x86_64-unknown-linux-gnu --no-bundle -- --locked)
cargo build -p cassady --release --locked --target aarch64-unknown-linux-gnu --bin cass --bin cassady
(cd cassady-desktop && cargo tauri build --target aarch64-unknown-linux-gnu --no-bundle -- --locked)
cargo zigbuild -p cassady --release --locked --target x86_64-pc-windows-gnu --bin cass --bin cassady
```
+3 -3
View File
@@ -17,7 +17,7 @@ export const platforms = [
{ {
name: '@cassady/cli-darwin-arm64', name: '@cassady/cli-darwin-arm64',
dirName: 'cassady-cli-darwin-arm64', dirName: 'cassady-cli-darwin-arm64',
description: 'Cassady CLI binaries for macOS Apple Silicon.', description: 'Cassady CLI and desktop binaries for macOS Apple Silicon.',
triple: 'aarch64-apple-darwin', triple: 'aarch64-apple-darwin',
os: 'darwin', os: 'darwin',
cpu: 'arm64', cpu: 'arm64',
@@ -26,7 +26,7 @@ export const platforms = [
{ {
name: '@cassady/cli-linux-x64', name: '@cassady/cli-linux-x64',
dirName: 'cassady-cli-linux-x64', dirName: 'cassady-cli-linux-x64',
description: 'Cassady CLI binaries for Linux x86_64.', description: 'Cassady CLI and desktop binaries for Linux x86_64.',
triple: 'x86_64-unknown-linux-gnu', triple: 'x86_64-unknown-linux-gnu',
os: 'linux', os: 'linux',
cpu: 'x64', cpu: 'x64',
@@ -35,7 +35,7 @@ export const platforms = [
{ {
name: '@cassady/cli-linux-arm64', name: '@cassady/cli-linux-arm64',
dirName: 'cassady-cli-linux-arm64', dirName: 'cassady-cli-linux-arm64',
description: 'Cassady CLI binaries for Linux ARM64.', description: 'Cassady CLI and desktop binaries for Linux ARM64.',
triple: 'aarch64-unknown-linux-gnu', triple: 'aarch64-unknown-linux-gnu',
os: 'linux', os: 'linux',
cpu: 'arm64', cpu: 'arm64',
+38 -2
View File
@@ -27,6 +27,31 @@ function copyExecutable(source, destination) {
fs.chmodSync(destination, 0o755); fs.chmodSync(destination, 0o755);
} }
function findFirstExisting(candidates) {
return candidates.find((candidate) => fs.existsSync(candidate));
}
function desktopBinaryCandidates(platform) {
if (platform.os === 'win32') {
return [];
}
const fileName = `cassady-desktop${platform.exeSuffix}`;
const candidates = [path.join(repoRoot, 'target', platform.triple, 'release', fileName)];
if (platform.triple === 'aarch64-apple-darwin') {
candidates.push(path.join(repoRoot, 'target', 'release', fileName));
}
if (platform.triple === 'x86_64-unknown-linux-gnu') {
candidates.push(path.join(repoRoot, 'target', 'tauri-linux-x86_64', 'release', fileName));
}
if (platform.triple === 'aarch64-unknown-linux-gnu') {
candidates.push(path.join(repoRoot, 'target', 'tauri-linux-aarch64', 'release', fileName));
}
return candidates;
}
function repositoryMetadata() { function repositoryMetadata() {
return { return {
type: 'git', type: 'git',
@@ -54,6 +79,16 @@ function writePlatformPackage(platform) {
copyExecutable(source, destination); copyExecutable(source, destination);
} }
const desktopCandidates = desktopBinaryCandidates(platform);
const desktopSource = findFirstExisting(desktopCandidates);
if (desktopCandidates.length > 0) {
ensureFile(
desktopSource ?? desktopCandidates[0],
`Build the desktop binary first. Checked:\n${desktopCandidates.join('\n')}`
);
copyExecutable(desktopSource, path.join(binDir, `cassady-desktop${platform.exeSuffix}`));
}
writeJson(path.join(dir, 'package.json'), { writeJson(path.join(dir, 'package.json'), {
name: platform.name, name: platform.name,
version, version,
@@ -66,9 +101,10 @@ function writePlatformPackage(platform) {
files: ['bin'], files: ['bin'],
}); });
const binaryList = desktopCandidates.length > 0 ? '`cass`, `cassady`, and `cassady-desktop`' : '`cass` and `cassady`';
fs.writeFileSync( fs.writeFileSync(
path.join(dir, 'README.md'), path.join(dir, 'README.md'),
`# ${platform.name}\n\n${platform.description}\n\nThis package is installed automatically by the \`cassady\` npm package on supported platforms. It contains the Rust-built \`cass\` and \`cassady\` executables for \`${platform.triple}\`.\n` `# ${platform.name}\n\n${platform.description}\n\nThis package is installed automatically by the \`cassady\` npm package on supported platforms. It contains the Rust-built ${binaryList} executables for \`${platform.triple}\`.\n`
); );
} }
@@ -101,7 +137,7 @@ function writeWrapperPackage() {
fs.writeFileSync( fs.writeFileSync(
path.join(dir, 'README.md'), path.join(dir, 'README.md'),
`# Cassady / Cass\n\nCassady (\`cass\`) is a terminal coding agent written in Rust. This npm package is a tiny launcher that installs the matching platform-specific binary package and exposes both commands:\n\n\`\`\`sh\nnpm install -g cassady\ncass --version\ncassady --version\n\`\`\`\n\nThe platform-specific packages contain the Rust-built executables. Supported npm platforms are macOS Apple Silicon, Linux x86_64, Linux ARM64, and Windows x86_64.\n` `# Cassady / Cass\n\nCassady (\`cass\`) is a terminal coding agent written in Rust. This npm package is a tiny launcher that installs the matching platform-specific binary package and exposes both commands:\n\n\`\`\`sh\nnpm install -g cassady\ncass --version\ncassady --version\ncass desktop\n\`\`\`\n\nThe macOS and Linux platform packages also include the \`cassady-desktop\` binary used by \`cass desktop\`. Supported npm platforms are macOS Apple Silicon, Linux x86_64, Linux ARM64, and Windows x86_64.\n`
); );
const launcher = `'use strict'; const launcher = `'use strict';
+4
View File
@@ -25,6 +25,10 @@ pub async fn run() -> Result<()> {
return crate::update::run(args).await; return crate::update::run(args).await;
} }
if let Some(Command::Desktop(args)) = cli.command.clone() {
return crate::desktop::launch(&cli, &args);
}
if matches!(cli.command, Some(Command::Check)) { if matches!(cli.command, Some(Command::Check)) {
let report = crate::check::run(&cli)?; let report = crate::check::run(&cli)?;
print!("{}", report.render()); print!("{}", report.render());
+9
View File
@@ -50,10 +50,19 @@ pub enum Command {
Logout, Logout,
/// Configure an OpenAI-compatible provider and first model. /// Configure an OpenAI-compatible provider and first model.
Setup, Setup,
/// Launch the Cassady desktop app in the background.
Desktop(DesktopArgs),
/// Update Cassady from official GitHub releases. /// Update Cassady from official GitHub releases.
Update(UpdateArgs), Update(UpdateArgs),
} }
#[derive(Debug, Args, Clone, PartialEq, Eq)]
pub struct DesktopArgs {
/// Keep the desktop process attached to this terminal for debugging.
#[arg(long)]
pub foreground: bool,
}
#[derive(Debug, Args, Clone, PartialEq, Eq)] #[derive(Debug, Args, Clone, PartialEq, Eq)]
pub struct UpdateArgs { pub struct UpdateArgs {
/// Check the latest release without installing. /// Check the latest release without installing.
+160
View File
@@ -0,0 +1,160 @@
use crate::cli::{Cli, DesktopArgs};
use anyhow::{anyhow, bail, Context, Result};
use std::env;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
const DESKTOP_BIN_ENV: &str = "CASSADY_DESKTOP_BIN";
pub fn launch(cli: &Cli, args: &DesktopArgs) -> Result<()> {
let cwd = resolve_launch_cwd(cli.cwd.as_deref())?;
let desktop_bin = locate_desktop_binary()?;
if args.foreground {
let status = Command::new(&desktop_bin)
.current_dir(&cwd)
.status()
.with_context(|| format!("launching {}", desktop_bin.display()))?;
if !status.success() {
bail!("Cassady desktop exited with {status}");
}
return Ok(());
}
let pid = spawn_detached(&desktop_bin, &cwd)
.with_context(|| format!("launching {} in the background", desktop_bin.display()))?;
println!("Cassady desktop started in the background (pid {pid}). You can close this terminal.");
Ok(())
}
fn resolve_launch_cwd(cwd: Option<&Path>) -> Result<PathBuf> {
let cwd = match cwd {
Some(path) => path.to_path_buf(),
None => env::current_dir().context("resolving current directory")?,
};
cwd.canonicalize()
.with_context(|| format!("resolving cwd {}", cwd.display()))
}
fn locate_desktop_binary() -> Result<PathBuf> {
if let Some(path) = env::var_os(DESKTOP_BIN_ENV).filter(|value| !value.is_empty()) {
let path = PathBuf::from(path);
if is_runnable_file(&path) {
return Ok(path);
}
bail!(
"{DESKTOP_BIN_ENV} points to {}, but that file does not exist or is not executable",
path.display()
);
}
let binary_name = desktop_binary_name();
if let Ok(current_exe) = env::current_exe() {
if let Some(dir) = current_exe.parent() {
let candidate = dir.join(binary_name);
if is_runnable_file(&candidate) {
return Ok(candidate);
}
}
}
if let Some(path) = find_on_path(binary_name) {
return Ok(path);
}
Err(anyhow!(
"could not find the Cassady desktop binary `{binary_name}`. Install or unpack it next to `cass`, put it on PATH, or set {DESKTOP_BIN_ENV}=/path/to/{binary_name}."
))
}
fn desktop_binary_name() -> &'static str {
if cfg!(windows) {
"cassady-desktop.exe"
} else {
"cassady-desktop"
}
}
fn find_on_path(name: &str) -> Option<PathBuf> {
let paths = env::var_os("PATH")?;
env::split_paths(&paths)
.map(|dir| dir.join(name))
.find(|candidate| is_runnable_file(candidate))
}
fn is_runnable_file(path: &Path) -> bool {
path.is_file() && is_executable(path)
}
#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.map(|metadata| metadata.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable(path: &Path) -> bool {
path.is_file()
}
fn spawn_detached(binary: &Path, cwd: &Path) -> Result<u32> {
let mut command = Command::new(binary);
command
.current_dir(cwd)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
detach_command(&mut command);
let child = command.spawn()?;
Ok(child.id())
}
#[cfg(unix)]
fn detach_command(command: &mut Command) {
use std::os::unix::process::CommandExt;
unsafe {
command.pre_exec(|| {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
}
#[cfg(windows)]
fn detach_command(command: &mut Command) {
use std::os::windows::process::CommandExt;
const DETACHED_PROCESS: u32 = 0x0000_0008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
command.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
}
#[cfg(not(any(unix, windows)))]
fn detach_command(_command: &mut Command) {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn desktop_binary_name_has_platform_suffix() {
let name = desktop_binary_name();
if cfg!(windows) {
assert_eq!(name, "cassady-desktop.exe");
} else {
assert_eq!(name, "cassady-desktop");
}
}
#[test]
fn find_on_path_returns_none_for_missing_binary() {
let name = format!("definitely-not-cassady-desktop-{}", std::process::id());
assert!(find_on_path(&name).is_none());
}
}
+1
View File
@@ -7,6 +7,7 @@ pub mod cli;
pub mod codex_auth; pub mod codex_auth;
pub mod config; pub mod config;
pub mod conversation; pub mod conversation;
pub mod desktop;
pub mod docs; pub mod docs;
pub mod embedding; pub mod embedding;
pub mod error; pub mod error;
+57 -4
View File
@@ -92,6 +92,7 @@ struct InstallPlan {
struct StagedBinaries { struct StagedBinaries {
cass: PathBuf, cass: PathBuf,
cassady: PathBuf, cassady: PathBuf,
desktop: Option<PathBuf>,
} }
pub async fn run(args: UpdateArgs) -> Result<()> { pub async fn run(args: UpdateArgs) -> Result<()> {
@@ -538,6 +539,7 @@ async fn run_source_update(
.join("target") .join("target")
.join("release") .join("release")
.join(format!("cassady{suffix}")), .join(format!("cassady{suffix}")),
desktop: None,
}; };
if !built.cass.is_file() || !built.cassady.is_file() { if !built.cass.is_file() || !built.cassady.is_file() {
bail!("source build did not produce both cass and cassady binaries"); bail!("source build did not produce both cass and cassady binaries");
@@ -717,7 +719,12 @@ fn find_staged_binaries(root: &Path, suffix: &str) -> Result<StagedBinaries> {
.ok_or_else(|| anyhow!("archive does not contain {cass_name}"))?; .ok_or_else(|| anyhow!("archive does not contain {cass_name}"))?;
let cassady = find_file_named(root, &cassady_name)? let cassady = find_file_named(root, &cassady_name)?
.ok_or_else(|| anyhow!("archive does not contain {cassady_name}"))?; .ok_or_else(|| anyhow!("archive does not contain {cassady_name}"))?;
Ok(StagedBinaries { cass, cassady }) let desktop = find_file_named(root, &format!("cassady-desktop{suffix}"))?;
Ok(StagedBinaries {
cass,
cassady,
desktop,
})
} }
fn find_file_named(root: &Path, name: &str) -> Result<Option<PathBuf>> { fn find_file_named(root: &Path, name: &str) -> Result<Option<PathBuf>> {
@@ -835,16 +842,26 @@ fn build_install_plan(
let suffix = if cfg!(windows) { ".exe" } else { "" }; let suffix = if cfg!(windows) { ".exe" } else { "" };
let cass_target = install_dir.join(format!("cass{suffix}")); let cass_target = install_dir.join(format!("cass{suffix}"));
let cassady_target = install_dir.join(format!("cassady{suffix}")); let cassady_target = install_dir.join(format!("cassady{suffix}"));
let desktop_target = install_dir.join(format!("cassady-desktop{suffix}"));
let current_name = current_exe let current_name = current_exe
.file_name() .file_name()
.and_then(OsStr::to_str) .and_then(OsStr::to_str)
.ok_or_else(|| anyhow!("current executable path has no filename"))?; .ok_or_else(|| anyhow!("current executable path has no filename"))?;
let mut actions = Vec::new(); let mut action_inputs = vec![
for (name, target, staged_path) in [
("cass", cass_target, staged.map(|s| s.cass.clone())), ("cass", cass_target, staged.map(|s| s.cass.clone())),
("cassady", cassady_target, staged.map(|s| s.cassady.clone())), ("cassady", cassady_target, staged.map(|s| s.cassady.clone())),
] { ];
if staged.and_then(|s| s.desktop.as_ref()).is_some() {
action_inputs.push((
"cassady-desktop",
desktop_target,
staged.and_then(|s| s.desktop.clone()),
));
}
let mut actions = Vec::new();
for (name, target, staged_path) in action_inputs {
let is_current = current_name let is_current = current_name
== target == target
.file_name() .file_name()
@@ -1221,6 +1238,7 @@ mod tests {
} else { } else {
"cassady" "cassady"
}), }),
desktop: None,
}; };
fs::write(&staged.cass, b"new").unwrap(); fs::write(&staged.cass, b"new").unwrap();
fs::write(&staged.cassady, b"new").unwrap(); fs::write(&staged.cassady, b"new").unwrap();
@@ -1230,6 +1248,40 @@ mod tests {
assert_eq!(plan.actions[1].kind, InstallActionKind::AddCompanion); assert_eq!(plan.actions[1].kind, InstallActionKind::AddCompanion);
} }
#[test]
fn plans_install_for_staged_desktop_binary() {
let dir = tempfile::tempdir().unwrap();
let cass = dir
.path()
.join(if cfg!(windows) { "cass.exe" } else { "cass" });
fs::write(&cass, b"old").unwrap();
let staged_dir = tempfile::tempdir().unwrap();
let desktop_name = if cfg!(windows) {
"cassady-desktop.exe"
} else {
"cassady-desktop"
};
let staged = StagedBinaries {
cass: staged_dir
.path()
.join(if cfg!(windows) { "cass.exe" } else { "cass" }),
cassady: staged_dir.path().join(if cfg!(windows) {
"cassady.exe"
} else {
"cassady"
}),
desktop: Some(staged_dir.path().join(desktop_name)),
};
fs::write(&staged.cass, b"new").unwrap();
fs::write(&staged.cassady, b"new").unwrap();
fs::write(staged.desktop.as_ref().unwrap(), b"desktop").unwrap();
let plan = build_install_plan(dir.path(), &cass, Some(&staged)).unwrap();
assert_eq!(plan.actions.len(), 3);
assert_eq!(plan.actions[2].name, "cassady-desktop");
assert_eq!(plan.actions[2].kind, InstallActionKind::AddCompanion);
}
#[test] #[test]
#[cfg(not(windows))] #[cfg(not(windows))]
fn installs_staged_binaries_with_backup() { fn installs_staged_binaries_with_backup() {
@@ -1243,6 +1295,7 @@ mod tests {
let staged = StagedBinaries { let staged = StagedBinaries {
cass: staged_dir.path().join("cass"), cass: staged_dir.path().join("cass"),
cassady: staged_dir.path().join("cassady"), cassady: staged_dir.path().join("cassady"),
desktop: None,
}; };
fs::write(&staged.cass, b"new cass").unwrap(); fs::write(&staged.cass, b"new cass").unwrap();
fs::write(&staged.cassady, b"new cassady").unwrap(); fs::write(&staged.cassady, b"new cassady").unwrap();