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"