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
174 changes: 174 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
name: Build & Release

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

on:
push:
branches:
- main

jobs:
build:
name: Build macOS App
runs-on: self-hosted
permissions:
contents: write
packages: write
actions: write
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Cache Xcode derived data
uses: irgaly/xcode-cache@v1
with:
key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.ref_name }}
restore-keys: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.ref_name }}

- name: Install create-dmg
run: npm install --global create-dmg

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.x"

- name: Install Python dependencies
run: pip install markdown

- name: Import code signing certificate
uses: apple-actions/import-codesign-certs@v6
with:
p12-file-base64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
p12-password: ${{ secrets.P12_PASSWORD }}
Comment on lines +46 to +50

- name: Bump version (release only)
if: github.event_name == 'release'
run: |
# Strip leading 'v' from release tag (v1.2.3 -> 1.2.3) — Apple's
# MARKETING_VERSION must be numeric.
RELEASE_VERSION="${GITHUB_REF_NAME#v}"
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "$GITHUB_ENV"
./scripts/ci/update-version.sh "${RELEASE_VERSION}"

- name: Build archive
run: |
gem install xcpretty
# CURRENT_PROJECT_VERSION is overridden per-build so every CI build
# has a unique build number (github.run_number), without committing
# churn to project.pbxproj.
set -o pipefail && xcodebuild -destination platform=macOS \
-project RxCode.xcodeproj \
-scheme RxCode \
-configuration Release \
-archivePath output/output.xcarchive \
-allowProvisioningUpdates \
CODE_SIGN_IDENTITY="${{ secrets.SIGNING_CERTIFICATE_NAME }}" \
CODE_SIGN_STYLE=Manual \
OTHER_CODE_SIGN_FLAGS="--options=runtime --timestamp" \
CURRENT_PROJECT_VERSION=${{ github.run_number }} \
archive | xcpretty

- name: Sign Sparkle
run: ./scripts/ci/sign-sparkle.sh
env:
SIGNING_CERTIFICATE_NAME: ${{ secrets.SIGNING_CERTIFICATE_NAME }}

- name: Notarize
run: ./scripts/ci/notary.sh
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PWD: ${{ secrets.APPLE_ID_PWD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}

- name: Generate appcast
run: ./scripts/ci/generate-appcast.sh
env:
SPARKLE_KEY: ${{ secrets.SPARKLE_KEY }}
VERSION: ${{ github.ref_name }}
BUILD_NUMBER: ${{ github.run_number }}
RELEASE_NOTE: ${{ github.event.release.body }}

# Upload artifact for Pull Requests (1 day retention)
- name: Upload artifact for PR
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v7
with:
name: RxCode-PR-${{ github.event.pull_request.number }}
path: RxCode.dmg
retention-days: 1

# Upload to release for release events
- name: Upload DMG to Release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v3
with:
files: RxCode.dmg
token: ${{ secrets.GITHUB_TOKEN }}

# Upload appcast as an artifact for the deploy job
- name: Upload appcast for deploy job
if: github.event_name == 'release'
uses: actions/upload-artifact@v7
with:
name: appcast-${{ github.sha }}
path: appcast.xml
retention-days: 1

- name: Upload release notes for deploy job
if: github.event_name == 'release'
uses: actions/upload-artifact@v7
with:
name: release_notes-${{ github.sha }}
Comment on lines +125 to +129
path: release_notes.html
retention-days: 1

deploy:
name: Deploy to GitHub Pages
needs: build
if: github.event_name == 'release'
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Download appcast from build job
uses: actions/download-artifact@v8
with:
name: appcast-${{ github.sha }}

- name: Download release notes from build job
uses: actions/download-artifact@v8
with:
name: release_notes-${{ github.sha }}

- name: Prepare pages directory
run: |
mkdir -p pages
cp appcast.xml pages/
cp release_notes.html pages/
# Pin custom domain — without this, deploy-pages strips the CNAME
# GitHub wrote when the domain was set in Settings → Pages.
echo "update.code.rxlab.app" > pages/CNAME

- name: Setup Pages
uses: actions/configure-pages@v6

- name: Upload pages artifact
uses: actions/upload-pages-artifact@v5
with:
path: "pages"

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
44 changes: 44 additions & 0 deletions .github/workflows/create-release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Create Release
on:
workflow_dispatch:
push:

jobs:
create-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "22"
- name: Setup Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Dry Run Release
uses: cycjimmy/semantic-release-action@v6
if: github.event_name == 'push'
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
with:
dry_run: true
branches: |
[
'+([0-9])?(.{+([0-9]),x}).x',
'main',
{name: '*', prerelease: true}
]
extra_plugins: |
@semantic-release/exec
- name: Create Release
uses: cycjimmy/semantic-release-action@v6
if: github.event_name == 'workflow_dispatch'
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
with:
branch: main
extra_plugins: |
@semantic-release/exec
66 changes: 66 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Tests

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

on:
push:

jobs:
test-packages:
name: swift test (Packages)
runs-on: self-hosted

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Install xcpretty
run: gem install xcpretty

- name: Swift version
run: swift --version

- name: Run package tests
working-directory: Packages
run: |
set -o pipefail
swift test 2>&1 | xcpretty

build-smoke:
name: xcodebuild build (Release)
runs-on: self-hosted

steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Cache Xcode derived data
uses: irgaly/xcode-cache@v1
with:
key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.ref_name }}
restore-keys: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.ref_name }}

- name: Install xcpretty
run: gem install xcpretty

- name: Build (no signing)
run: |
set -o pipefail && xcodebuild \
-project RxCode.xcodeproj \
-scheme RxCode \
-configuration Release \
-destination platform=macOS \
CODE_SIGNING_ALLOWED=NO \
build | xcpretty
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,12 @@ scripts/.sparkle_private_key

# Sparkle CLI tools (reinstallable via setup_sparkle.sh)
scripts/sparkle_tools/

# CI build outputs
output/
test-results/
RxCode.dmg
sparkle.key
release_notes.html
release_notes.md

3 changes: 3 additions & 0 deletions .releaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
plugins: ['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', '@semantic-release/github']
}
18 changes: 9 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t

## Project Overview

Clarc is a native macOS desktop client for the Codex CLI. Written in Swift + SwiftUI with two external dependencies: SwiftTerm (terminal emulation) and Sparkle (auto-update).
RxCode is a native macOS desktop client for the Codex CLI. Written in Swift + SwiftUI with two external dependencies: SwiftTerm (terminal emulation) and Sparkle (auto-update).

## Writing Rules

Expand All @@ -14,26 +14,26 @@ Clarc is a native macOS desktop client for the Codex CLI. Written in Swift + Swi

```bash
# Open in Xcode (build/run with Cmd+R)
open Clarc.xcodeproj
open RxCode.xcodeproj

# CLI build
xcodebuild -project Clarc.xcodeproj -scheme Clarc -configuration Debug build
xcodebuild -project RxCode.xcodeproj -scheme RxCode -configuration Debug build

# Release build
xcodebuild -project Clarc.xcodeproj -scheme Clarc -configuration Release build
xcodebuild -project RxCode.xcodeproj -scheme RxCode -configuration Release build
```

- Minimum deployment target: macOS 15.0+
- No test suite (UI app)
- Bundle ID: `com.idealapp.Clarc`
- Bundle ID: `com.idealapp.RxCode`
- External dependencies: SwiftTerm (terminal emulation), Sparkle (auto-update)

## Architecture

### Core Patterns

- **Observable AppState** (`App/AppState.swift`): `@MainActor @Observable` single state container. Manages all app state including projects, sessions, chat, and permission approvals.
- **App entry point** (`App/ClarcApp.swift`): Defines WindowGroup (main), WindowGroup(id: "project-window") for dedicated per-project windows, Settings window, and Command menu (theme, update).
- **App entry point** (`App/RxCodeApp.swift`): Defines WindowGroup (main), WindowGroup(id: "project-window") for dedicated per-project windows, Settings window, and Command menu (theme, update).
- **Actor-based services**: All services are implemented as `actor` for concurrency safety. Isolated without locks.
- **SwiftUI only**: No Storyboards or XIBs. 100% declarative UI.

Expand All @@ -43,8 +43,8 @@ The codebase is split into two Swift packages under `Packages/`:

| Package | Role |
| -------------- | ---------------------------------------------------------------- |
| `ClarcCore` | Shared models, theme, utilities — no UI dependencies |
| `ClarcChatKit` | Chat UI components (ChatView, MessageBubble, InputBarView, etc.) |
| `RxCodeCore` | Shared models, theme, utilities — no UI dependencies |
| `RxCodeChatKit` | Chat UI components (ChatView, MessageBubble, InputBarView, etc.) |

### Service Layer (`Services/`)

Expand All @@ -53,7 +53,7 @@ The codebase is split into two Swift packages under `Packages/`:
| `ClaudeService` | Spawns Codex CLI as a subprocess, parses stdout NDJSON stream, buffers text deltas at 50ms intervals |
| `PermissionServer` | Network framework-based local HTTP server (ports 19836–19846). Receives CLI PreToolUse hook requests and holds the connection until UI approval |
| `GitHubService` | OAuth Device Flow authentication, Keychain token storage, SSH key generation/registration, repo cloning |
| `PersistenceService` | JSON file-based persistence at `~/Library/Application Support/Clarc/`. Per-project/session directory structure |
| `PersistenceService` | JSON file-based persistence at `~/Library/Application Support/RxCode/`. Per-project/session directory structure |
| `MarketplaceService` | Parallel fetch of plugin catalog from 4 Anthropic GitHub repos, 5-minute cache |
| `RateLimitService` | Anthropic usage API polling, OAuth token refresh, usage tracking |
| `UpdateService` | Sparkle-based auto-update manager. Starts updater on launch; exposes `checkForUpdates()` for menu-initiated checks |
Expand Down
Loading
Loading