Skip to content
Closed
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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- "bin/**"
- "tests/**"
- "scripts/**"
- "apps/macos-menu-bar/**"
- "gui/**"
- ".gitattributes"
- ".npmignore"
Expand All @@ -24,6 +25,7 @@ on:
- "bin/**"
- "tests/**"
- "scripts/**"
- "apps/macos-menu-bar/**"
- "gui/**"
- ".gitattributes"
- ".npmignore"
Expand Down Expand Up @@ -78,6 +80,14 @@ jobs:
- name: Privacy scan
run: bun run privacy:scan

- name: Test macOS menu bar app
if: runner.os == 'macOS'
run: bun run test:macos

- name: Build macOS menu bar app
if: runner.os == 'macOS'
run: bun run build:macos

- name: Check release helper syntax
run: bun build scripts/release.ts --target=bun --outdir=.tmp/ci-release-script-check

Expand Down
78 changes: 68 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Release

# Publish opencodex to npm — jawcode-style: triggered from the Actions tab with an explicit
# version, dist-tag, and a dry-run-first default. Bump package.json on main BEFORE dispatching
# (or use `bun run release <version>`, which does the bump+commit+push+dispatch for you); the
# workflow verifies the version matches before publishing.
# Publish opencodex to npm and package the OpenCodex macOS companion — jawcode-style: triggered
# from the Actions tab with an explicit version, dist-tag, and a dry-run-first default. Bump
# package.json on main BEFORE dispatching (or use `bun run release <version>`, which does the
# bump+commit+push+dispatch for you); the workflow verifies the version matches before publishing.
on:
workflow_dispatch:
inputs:
Expand All @@ -29,19 +29,53 @@ on:
required: false
type: string

permissions:
contents: write # create the matching GitHub Release + version tag after npm publish
actions: read # verify the release commit already passed Cross-platform CI
id-token: write # OIDC auth for Trusted Publishing + automatic provenance attestation
permissions: {}

concurrency:
group: release
cancel-in-progress: false

jobs:
package-macos:
runs-on: macos-latest
timeout-minutes: 15
permissions:
contents: read
outputs:
archive_name: ${{ steps.package.outputs.archive_name }}
checksum_name: ${{ steps.package.outputs.checksum_name }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false

- name: Build universal macOS release asset
id: package
env:
RELEASE_VERSION: ${{ inputs.version }}
UNIVERSAL: "1"
run: bash scripts/package-macos-release.sh

- name: Upload macOS release asset
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: opencodex-macos-release
path: |
dist/release/${{ steps.package.outputs.archive_name }}
dist/release/${{ steps.package.outputs.checksum_name }}
if-no-files-found: error
compression-level: 0
retention-days: 7

publish:
needs: package-macos
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write # create the matching GitHub Release + version tag after npm publish
actions: read # verify CI and download the macOS workflow artifact
id-token: write # OIDC auth for Trusted Publishing + automatic provenance attestation
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
Expand All @@ -59,6 +93,23 @@ jobs:
exit 1
fi

- name: Download macOS release asset
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: opencodex-macos-release
path: dist/release

- name: Verify macOS release asset
env:
MACOS_ARCHIVE: ${{ needs.package-macos.outputs.archive_name }}
MACOS_CHECKSUM: ${{ needs.package-macos.outputs.checksum_name }}
run: |
set -euo pipefail
cd dist/release
test -f "$MACOS_ARCHIVE"
test -f "$MACOS_CHECKSUM"
sha256sum --check "$MACOS_CHECKSUM"

# opencodex is bun-native (the prepublishOnly GUI build + typecheck run under bun).
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
Expand Down Expand Up @@ -195,7 +246,7 @@ jobs:
fi

# Tokenless publish via Trusted Publishing (OIDC) — NO NPM_TOKEN secret. npm auto-detects the
# OIDC environment (`id-token: write` above) and generates provenance automatically, so neither a
# OIDC environment (`id-token: write` on this job) and generates provenance automatically, so neither a
# token nor `--provenance` is needed. `npm publish` runs prepublishOnly first (typecheck + build
# the GUI into gui/dist), so even a dry-run fully verifies the build.
# PREREQUISITE: configure the Trusted Publisher for this repo + workflow on npmjs.com — possible
Expand Down Expand Up @@ -286,10 +337,14 @@ jobs:
GH_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ inputs.version }}
NPM_DIST_TAG: ${{ inputs.tag }}
MACOS_ARCHIVE: ${{ needs.package-macos.outputs.archive_name }}
MACOS_CHECKSUM: ${{ needs.package-macos.outputs.checksum_name }}
run: |
set -euo pipefail

release_tag="v${RELEASE_VERSION}"
macos_archive="dist/release/${MACOS_ARCHIVE}"
macos_checksum="dist/release/${MACOS_CHECKSUM}"

git fetch --force --tags origin

Expand Down Expand Up @@ -358,6 +413,9 @@ jobs:

{
printf '%s\n\n' "$npm_metadata"
printf '## macOS app\n\n'
printf 'Download `%s` and verify it with `%s`.\n' "$MACOS_ARCHIVE" "$MACOS_CHECKSUM"
printf 'The app is ad-hoc signed and not notarized, so macOS may require manual approval on first launch.\n\n'
if [ -n "$(printf '%s' "$pr_notes" | tr -d '[:space:]')" ]; then
printf '%s\n\n' "$pr_notes"
fi
Expand All @@ -376,5 +434,5 @@ jobs:
git push origin "refs/tags/${release_tag}"
fi

gh release create "$release_tag" --target "$GITHUB_SHA" --title "$release_tag" \
gh release create "$release_tag" "$macos_archive" "$macos_checksum" --target "$GITHUB_SHA" --title "$release_tag" \
--notes-file "$notes_file" ${prerelease_flag:+$prerelease_flag}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
dist/
.build/
.env
*.log
.DS_Store
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,25 @@ flowchart LR

Requires [Node](https://nodejs.org) 18+. The Bun runtime is bundled automatically on `npm install` — no separate Bun install needed. All three platforms work natively (no WSL needed on Windows).

### macOS menu bar companion

macOS 12+ users can build a native menu bar companion that shows proxy health, version, uptime,
PID, port, Bun runtime source, and launchd state. It can open the dashboard and start, restart, or
gracefully stop the proxy without keeping a terminal open.

```bash
bun run build:macos-app
open "dist/macos/OpenCodex.app"
```

Non-dry-run GitHub releases include a Universal macOS archive named
`OpenCodex-<version>-macos-universal.zip` plus its SHA-256 checksum. The archive is ad-hoc signed
and not notarized, so macOS may require manual approval on first launch.

The companion uses the installed `ocx` CLI as its control boundary and never edits PID files or
Codex configuration directly. See the [macOS menu bar guide](https://lidge-jun.github.io/opencodex/guides/macos-menu-bar/)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
for CLI discovery, universal builds, and launch-at-login setup.

## Quick start

```bash
Expand Down
32 changes: 32 additions & 0 deletions apps/macos-menu-bar/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>OpenCodex</string>
<key>CFBundleExecutable</key>
<string>OpenCodexMenuBar</string>
<key>CFBundleIdentifier</key>
<string>com.opencodex.menubar</string>
<key>CFBundleIconFile</key>
<string>OpenCodex.icns</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>OpenCodex</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>LSUIElement</key>
<true/>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
29 changes: 29 additions & 0 deletions apps/macos-menu-bar/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
name: "OpenCodexMenuBar",
platforms: [
.macOS(.v12),
],
products: [
.executable(name: "OpenCodexMenuBar", targets: ["OpenCodexMenuBar"]),
],
targets: [
.target(
name: "OpenCodexMenuBarCore",
path: "Sources/OpenCodexMenuBarCore"
),
.executableTarget(
name: "OpenCodexMenuBar",
dependencies: ["OpenCodexMenuBarCore"],
path: "Sources/OpenCodexMenuBar"
),
.testTarget(
name: "OpenCodexMenuBarCoreTests",
dependencies: ["OpenCodexMenuBarCore"],
path: "Tests/OpenCodexMenuBarCoreTests"
),
],
swiftLanguageVersions: [.v5]
)
43 changes: 43 additions & 0 deletions apps/macos-menu-bar/Sources/OpenCodexMenuBar/MenuText.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Foundation

enum Text {
static let isKorean = Locale.current.languageCode == "ko"

static let appName = "OpenCodex"
static var checking: String { isKorean ? "상태 확인 중…" : "Checking status…" }
static var running: String { isKorean ? "프록시 실행 중" : "Proxy running" }
static var stopped: String { isKorean ? "프록시 중지됨" : "Proxy stopped" }
static var degraded: String { isKorean ? "프록시 상태 확인 필요" : "Proxy needs attention" }
static var cliMissing: String { isKorean ? "ocx CLI를 찾을 수 없음" : "ocx CLI not found" }
static var failed: String { isKorean ? "상태를 확인할 수 없음" : "Status unavailable" }
static var openDashboard: String { isKorean ? "대시보드 열기" : "Open Dashboard" }
static var start: String { isKorean ? "프록시 시작" : "Start Proxy" }
static var restart: String { isKorean ? "프록시 재시작…" : "Restart Proxy…" }
static var stop: String { isKorean ? "프록시 종료…" : "Stop Proxy…" }
static var refresh: String { isKorean ? "상태 새로고침" : "Refresh Status" }
static var chooseCLI: String { isKorean ? "ocx CLI 선택…" : "Choose ocx CLI…" }
static var resetCLI: String { isKorean ? "CLI 자동 탐색 사용" : "Use Auto-detected CLI" }
static var quit: String { isKorean ? "메뉴바 앱 종료" : "Quit Menu Bar App" }
static var noService: String { isKorean ? "launchd 서비스 없음" : "No launchd service" }
static var serviceStale: String { isKorean ? "서비스 손상 — 재설치 필요" : "Service stale — repair required" }
static var repairService: String { isKorean ? "서비스 복구" : "Repair Service" }
static var serviceRunning: String { isKorean ? "launchd 서비스 사용 중" : "Launchd service enabled" }
static var serviceStopped: String { isKorean ? "launchd 서비스 중지됨" : "Launchd service stopped" }
static var runtime: String { isKorean ? "Bun 런타임" : "Bun runtime" }
static let cli = "ocx CLI"
static var starting: String { isKorean ? "프록시 시작 중…" : "Starting proxy…" }
static var restarting: String { isKorean ? "프록시 재시작 중…" : "Restarting proxy…" }
static var stopping: String { isKorean ? "프록시 종료 중…" : "Stopping proxy…" }
static var actionFailed: String { isKorean ? "프록시 제어 실패" : "Proxy Control Failed" }
static var invalidCLI: String { isKorean ? "실행 가능한 ocx 파일을 선택해 주세요." : "Choose an executable ocx file." }
static var restartTitle: String { isKorean ? "프록시를 재시작할까요?" : "Restart the proxy?" }
static var stopTitle: String { isKorean ? "프록시를 종료할까요?" : "Stop the proxy?" }
static var interruptionWarning: String {
isKorean
? "진행 중인 요청이 중단될 수 있습니다. 종료하면 Codex 설정은 네이티브 상태로 복원됩니다."
: "Active requests may be interrupted. Stopping also restores native Codex configuration."
}
static var cancel: String { isKorean ? "취소" : "Cancel" }
static var confirmRestart: String { isKorean ? "재시작" : "Restart" }
static var confirmStop: String { isKorean ? "종료" : "Stop" }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import AppKit

enum StatusBarIcon {
private static let size = NSSize(width: 22, height: 22)
private static let cropRatio: CGFloat = 0.09375
private static let verticalOffset: CGFloat = -1

static func make(accessibilityDescription: String) -> NSImage? {
guard let imageURL = Bundle.main.url(forResource: "OpenCodexMenuBar", withExtension: "png"),
let sourceImage = NSImage(contentsOf: imageURL) else {
return fallback(accessibilityDescription: accessibilityDescription)
}

let sourceRect = NSRect(origin: .zero, size: sourceImage.size).insetBy(
dx: sourceImage.size.width * cropRatio,
dy: sourceImage.size.height * cropRatio
)
let image = NSImage(size: size, flipped: false) { destinationRect in
let alignedRect = destinationRect.offsetBy(dx: 0, dy: verticalOffset)
sourceImage.draw(in: alignedRect, from: sourceRect, operation: .sourceOver, fraction: 1)
return true
}
image.isTemplate = true
return image
}

private static func fallback(accessibilityDescription: String) -> NSImage? {
let configuration = NSImage.SymbolConfiguration(pointSize: 15, weight: .medium)
let image = NSImage(systemSymbolName: "network", accessibilityDescription: accessibilityDescription)?
.withSymbolConfiguration(configuration)
image?.isTemplate = true
return image
}
}
Loading
Loading