diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..31ad8cd
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,87 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+permissions:
+ contents: write
+
+env:
+ ZIG_VERSION: '0.15.2'
+
+jobs:
+ build:
+ name: Build and Release
+ runs-on: macos-15
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ fetch-depth: 0
+
+ - name: Extract version from tag
+ run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
+
+ - name: Install Zig
+ uses: mlugg/setup-zig@v2
+ with:
+ version: ${{ env.ZIG_VERSION }}
+
+ - name: Verify prerequisites
+ run: |
+ echo "Zig: $(zig version)"
+ echo "Swift: $(swift --version)"
+ xcrun -sdk macosx metal --version
+
+ - name: Cache GhosttyKit
+ id: cache-ghostty
+ uses: actions/cache@v4
+ with:
+ path: |
+ Frameworks/GhosttyKit.xcframework
+ Frameworks/ghostty-resources
+ key: ghosttykit-${{ runner.os }}-${{ hashFiles('vendor/ghostty/**') }}-zig${{ env.ZIG_VERSION }}
+
+ - name: Build GhosttyKit
+ if: steps.cache-ghostty.outputs.cache-hit != 'true'
+ run: bash scripts/build-ghostty.sh
+
+ - name: Build release
+ run: make release
+
+ - name: Sign
+ run: bash scripts/sign.sh
+ env:
+ DEVELOPER_ID_APPLICATION: ${{ secrets.DEVELOPER_ID_APPLICATION }}
+
+ - name: Notarize
+ if: ${{ secrets.APPLE_ID != '' }}
+ run: |
+ xcrun notarytool submit Crow.app --apple-id "$APPLE_ID" \
+ --team-id "$APPLE_TEAM_ID" --password "$APPLE_APP_PASSWORD" --wait
+ xcrun stapler staple Crow.app
+ env:
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
+
+ - name: Create DMG
+ run: |
+ brew install create-dmg
+ bash scripts/create-dmg.sh
+
+ - name: Upload DMG artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: Crow-${{ env.VERSION }}-arm64.dmg
+ path: Crow-${{ env.VERSION }}-arm64.dmg
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: Crow-${{ env.VERSION }}-arm64.dmg
+ generate_release_notes: true
+ prerelease: ${{ contains(github.ref_name, '-') }}
diff --git a/.gitignore b/.gitignore
index 421b9a1..d7bd74c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ xcuserdata/
# Generated sources
Sources/Crow/Generated/
+Sources/CrowCLI/Generated/
# App bundle
*.app
diff --git a/Crow.entitlements b/Crow.entitlements
new file mode 100644
index 0000000..7cd9df0
--- /dev/null
+++ b/Crow.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.disable-library-validation
+
+
+
diff --git a/Makefile b/Makefile
index a0fcc68..161e45b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: build setup ghostty app release clean clean-all check help
+.PHONY: build setup ghostty app release sign dmg dist clean clean-all check help
FRAMEWORKS_DIR := Frameworks
XCFW := $(FRAMEWORKS_DIR)/GhosttyKit.xcframework
@@ -17,6 +17,9 @@ help:
@echo " ghostty Build GhosttyKit framework"
@echo " app Swift build only (debug)"
@echo " release Release build + .app bundle"
+ @echo " sign Code-sign Crow.app (ad-hoc or Developer ID)"
+ @echo " dmg Create distributable DMG"
+ @echo " dist Full distribution: release + sign + dmg"
@echo " clean Remove .build/ (keeps ghostty framework)"
@echo " clean-all Remove .build/ and Frameworks/ (full rebuild)"
@echo ""
@@ -53,6 +56,16 @@ release: $(XCFW)
bash scripts/generate-build-info.sh
bash scripts/bundle.sh
+# --- Distribution ---
+
+sign: release
+ bash scripts/sign.sh
+
+dmg: sign
+ bash scripts/create-dmg.sh
+
+dist: dmg
+
# --- Clean ---
clean:
diff --git a/README.md b/README.md
index c0ecfe3..0c6cef5 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,56 @@
A native macOS application for managing AI-powered development sessions. Orchestrates git worktrees, Claude Code instances, and GitHub/GitLab issue tracking in a unified interface with an embedded Ghostty terminal.
+## Installation
+
+### Download (Recommended)
+
+Download the latest DMG from [GitHub Releases](https://github.com/radiusmethod/crow/releases/latest), open it, and drag **Crow.app** to your Applications folder.
+
+> **Note:** If macOS shows "Crow can't be opened because Apple cannot check it for malicious software," right-click the app and select **Open**, then click **Open** in the dialog. This is expected for apps distributed outside the Mac App Store without a Developer ID signature.
+
+### Homebrew
+
+*Coming soon.* Once available:
+
+```bash
+brew install radiusmethod/tap/crow
+```
+
+### Build from Source
+
+See [Prerequisites](#prerequisites) and [Quick Start](#quick-start) below.
+
+### Platform Support
+
+| Platform | Architecture | Status |
+|----------|-------------|--------|
+| macOS 14+ (Sonoma) | Apple Silicon (arm64) | Supported |
+| macOS (Intel) | x86_64 | Not supported |
+| Linux | — | Not planned (heavy AppKit/SwiftUI/Metal dependency) |
+| Windows | — | Not planned |
+
+### Runtime Dependencies
+
+Regardless of installation method, the following tools must be installed separately:
+
+| Tool | Purpose | Install |
+|------|---------|---------|
+| `gh` | GitHub issue tracking and PR status | `brew install gh` |
+| `git` | Worktree management | Ships with Xcode CLT |
+| `claude` | Claude Code AI assistant | [claude.ai/download](https://claude.ai/download) |
+| `glab` | GitLab CLI (optional) | `brew install glab` |
+
+### Updating
+
+Crow does not currently include auto-update. To update:
+
+- **Download:** Re-download the latest DMG from [GitHub Releases](https://github.com/radiusmethod/crow/releases/latest) and replace the app in your Applications folder.
+- **Homebrew:** `brew upgrade crow` (once the tap is available).
+- **Source:** `git pull && make build`.
+
+Auto-update via Sparkle or GitHub API polling may be added in a future release based on user demand.
+
## Prerequisites
### System Requirements
diff --git a/Sources/CrowCLI/CrowCLI.swift b/Sources/CrowCLI/CrowCLI.swift
index d5e8f54..73a5f5d 100644
--- a/Sources/CrowCLI/CrowCLI.swift
+++ b/Sources/CrowCLI/CrowCLI.swift
@@ -7,7 +7,7 @@ struct Crow: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "crow",
abstract: "CLI for Crow — manage sessions, terminals, and metadata",
- version: "0.1.0",
+ version: CLIVersion.version,
subcommands: [
Setup.self,
NewSession.self,
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..6e8bf73
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.1.0
diff --git a/scripts/bundle.sh b/scripts/bundle.sh
index e908c20..1c77343 100755
--- a/scripts/bundle.sh
+++ b/scripts/bundle.sh
@@ -8,6 +8,22 @@ BUILD_DIR="$ROOT_DIR/.build/release"
APP_DIR="$ROOT_DIR/Crow.app"
FRAMEWORKS_DIR="$ROOT_DIR/Frameworks"
+# --- Version ---
+if [ -n "${VERSION:-}" ]; then
+ APP_VERSION="$VERSION"
+elif [ -f "$ROOT_DIR/VERSION" ]; then
+ APP_VERSION="$(tr -d '[:space:]' < "$ROOT_DIR/VERSION")"
+else
+ APP_VERSION="0.1.0"
+fi
+
+# Build number from git commit count
+if git -C "$ROOT_DIR" rev-parse HEAD >/dev/null 2>&1; then
+ BUILD_NUMBER=$(git -C "$ROOT_DIR" rev-list --count HEAD)
+else
+ BUILD_NUMBER="1"
+fi
+
echo "==> Generating build info..."
bash "$SCRIPT_DIR/generate-build-info.sh"
@@ -15,7 +31,7 @@ echo "==> Building release..."
cd "$ROOT_DIR"
swift build -c release
-echo "==> Creating app bundle..."
+echo "==> Creating app bundle (version $APP_VERSION, build $BUILD_NUMBER)..."
rm -rf "$APP_DIR"
mkdir -p "$APP_DIR/Contents/MacOS"
mkdir -p "$APP_DIR/Contents/Resources"
@@ -30,7 +46,7 @@ if [ -d "$FRAMEWORKS_DIR/ghostty-resources" ]; then
fi
# Create Info.plist
-cat > "$APP_DIR/Contents/Info.plist" << 'PLIST'
+cat > "$APP_DIR/Contents/Info.plist" << PLIST
@@ -44,9 +60,9 @@ cat > "$APP_DIR/Contents/Info.plist" << 'PLIST'
CFBundleDisplayName
Crow
CFBundleVersion
- 1
+ ${BUILD_NUMBER}
CFBundleShortVersionString
- 0.1.0
+ ${APP_VERSION}
CFBundlePackageType
APPL
LSMinimumSystemVersion
diff --git a/scripts/create-dmg.sh b/scripts/create-dmg.sh
new file mode 100755
index 0000000..f1a4102
--- /dev/null
+++ b/scripts/create-dmg.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+# Create a DMG from Crow.app
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+APP_DIR="${1:-$ROOT_DIR/Crow.app}"
+
+if [ ! -d "$APP_DIR" ]; then
+ echo "ERROR: App bundle not found at $APP_DIR"
+ exit 1
+fi
+
+# --- Version ---
+if [ -n "${VERSION:-}" ]; then
+ APP_VERSION="$VERSION"
+elif [ -f "$ROOT_DIR/VERSION" ]; then
+ APP_VERSION="$(tr -d '[:space:]' < "$ROOT_DIR/VERSION")"
+else
+ APP_VERSION="0.1.0"
+fi
+
+DMG_NAME="Crow-${APP_VERSION}-arm64.dmg"
+DMG_PATH="$ROOT_DIR/$DMG_NAME"
+
+# Remove existing DMG
+rm -f "$DMG_PATH"
+
+if command -v create-dmg >/dev/null 2>&1; then
+ echo "==> Creating DMG with create-dmg..."
+ create-dmg \
+ --volname "Crow" \
+ --window-pos 200 120 \
+ --window-size 600 400 \
+ --icon-size 100 \
+ --icon "Crow.app" 150 190 \
+ --app-drop-link 450 190 \
+ --no-internet-enable \
+ "$DMG_PATH" \
+ "$APP_DIR"
+else
+ echo "==> Creating DMG with hdiutil (install create-dmg for a nicer result)..."
+ STAGING_DIR="$(mktemp -d)"
+ cp -R "$APP_DIR" "$STAGING_DIR/"
+ ln -s /Applications "$STAGING_DIR/Applications"
+
+ hdiutil create -volname "Crow" \
+ -srcfolder "$STAGING_DIR" \
+ -ov -format UDZO \
+ "$DMG_PATH"
+
+ rm -rf "$STAGING_DIR"
+fi
+
+echo "==> DMG created: $DMG_PATH"
diff --git a/scripts/generate-build-info.sh b/scripts/generate-build-info.sh
index ad9dc7a..19bed82 100755
--- a/scripts/generate-build-info.sh
+++ b/scripts/generate-build-info.sh
@@ -1,15 +1,20 @@
#!/usr/bin/env bash
-# Generate BuildInfo.swift with git SHA and build date
+# Generate BuildInfo.swift and CLIVersion.swift with git SHA, build date, and version
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
-OUTPUT_DIR="$ROOT_DIR/Sources/Crow/Generated"
-OUTPUT_FILE="$OUTPUT_DIR/BuildInfo.swift"
-mkdir -p "$OUTPUT_DIR"
+# --- Version ---
+if [ -n "${VERSION:-}" ]; then
+ APP_VERSION="$VERSION"
+elif [ -f "$ROOT_DIR/VERSION" ]; then
+ APP_VERSION="$(tr -d '[:space:]' < "$ROOT_DIR/VERSION")"
+else
+ APP_VERSION="0.1.0"
+fi
-# Capture git info (fallback to "dev" if not in a git repo)
+# --- Git info ---
if git -C "$ROOT_DIR" rev-parse HEAD >/dev/null 2>&1; then
GIT_SHA=$(git -C "$ROOT_DIR" rev-parse HEAD)
GIT_SHORT_SHA=$(git -C "$ROOT_DIR" rev-parse --short HEAD)
@@ -20,13 +25,33 @@ fi
BUILD_DATE=$(date -u +"%Y-%m-%d")
+# --- Generate BuildInfo.swift (Crow app target) ---
+OUTPUT_DIR="$ROOT_DIR/Sources/Crow/Generated"
+OUTPUT_FILE="$OUTPUT_DIR/BuildInfo.swift"
+mkdir -p "$OUTPUT_DIR"
+
cat > "$OUTPUT_FILE" << EOF
// Auto-generated by scripts/generate-build-info.sh — do not edit
enum BuildInfo {
+ static let version = "$APP_VERSION"
static let gitCommitSHA = "$GIT_SHA"
static let gitCommitShortSHA = "$GIT_SHORT_SHA"
static let buildDate = "$BUILD_DATE"
}
EOF
-echo "Generated $OUTPUT_FILE (SHA: $GIT_SHORT_SHA, date: $BUILD_DATE)"
+echo "Generated $OUTPUT_FILE (version: $APP_VERSION, SHA: $GIT_SHORT_SHA, date: $BUILD_DATE)"
+
+# --- Generate CLIVersion.swift (CrowCLI target) ---
+CLI_OUTPUT_DIR="$ROOT_DIR/Sources/CrowCLI/Generated"
+CLI_OUTPUT_FILE="$CLI_OUTPUT_DIR/CLIVersion.swift"
+mkdir -p "$CLI_OUTPUT_DIR"
+
+cat > "$CLI_OUTPUT_FILE" << EOF
+// Auto-generated by scripts/generate-build-info.sh — do not edit
+enum CLIVersion {
+ static let version = "$APP_VERSION"
+}
+EOF
+
+echo "Generated $CLI_OUTPUT_FILE"
diff --git a/scripts/sign.sh b/scripts/sign.sh
new file mode 100755
index 0000000..899aca3
--- /dev/null
+++ b/scripts/sign.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+# Sign Crow.app with Developer ID or ad-hoc
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+APP_DIR="${1:-$ROOT_DIR/Crow.app}"
+ENTITLEMENTS="$ROOT_DIR/Crow.entitlements"
+
+if [ ! -d "$APP_DIR" ]; then
+ echo "ERROR: App bundle not found at $APP_DIR"
+ exit 1
+fi
+
+IDENTITY="${DEVELOPER_ID_APPLICATION:--}"
+
+if [ "$IDENTITY" = "-" ]; then
+ echo "==> Signing ad-hoc (no Developer ID)..."
+else
+ echo "==> Signing with: $IDENTITY"
+fi
+
+# Sign the main binary first, then the bundle (inside-out)
+codesign --force --sign "$IDENTITY" \
+ --entitlements "$ENTITLEMENTS" \
+ --options runtime \
+ "$APP_DIR/Contents/MacOS/CrowApp"
+
+codesign --force --sign "$IDENTITY" \
+ --entitlements "$ENTITLEMENTS" \
+ --options runtime \
+ "$APP_DIR"
+
+echo "==> Verifying signature..."
+codesign --verify --verbose=2 "$APP_DIR"
+echo "==> Signed: $APP_DIR"