Skip to content

Add CLIProxyAPI binary self-update flow#39

Merged
woosublee merged 20 commits into
mainfrom
worktree-cliproxyapi-binary-self-update
Jul 5, 2026
Merged

Add CLIProxyAPI binary self-update flow#39
woosublee merged 20 commits into
mainfrom
worktree-cliproxyapi-binary-self-update

Conversation

@woosublee

@woosublee woosublee commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add CLIProxyAPI binary release lookup, download, verification, pending apply, and active binary management
  • Isolate DEBUG builds under ~/.cliproxy-manager/dev with default dev port 18318
  • Move CLIProxyAPI update controls from Server settings to About > Updates and show current/target versions in update prompts
  • Harden update recovery for corrupt manifests, pending rollback, stale status refresh, duplicate pending/latest state, and verifier temp cleanup

Verification

  • DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift test — 351 tests passed
  • DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift build -c debug --product CLIProxyManager --scratch-path /tmp/cliproxy-manager-dev-open-build
  • Opened /tmp/CLIProxyManagerDev.app; production snapshot unchanged
  • Confirmed dev config uses port=18318, autostartServer=false; production 18317 listener unchanged
  • Reset dev active CLIProxyAPI to bundled 7.2.41 and confirmed pending update state for 7.2.50 testing

🤖 Generated with Claude Code

Summary by CodeRabbit

  • 새 기능

    • 앱 실행 시 CLIProxyAPI 바이너리 업데이트를 자동 확인하고, 새 버전이 있으면 다운로드·적용을 안내합니다.
    • 설정 화면의 업데이트 관련 항목이 개선되어, 현재/대기 중인 버전 상태를 더 쉽게 확인하고 적용할 수 있습니다.
    • 업데이트된 바이너리는 무결성 검증 후 적용되며, 필요 시 서버 재시작까지 자동으로 이어집니다.
  • 버그 수정

    • 버전 확인, 다운로드, 적용 실패 시 기존 바이너리를 유지하도록 안정성이 향상되었습니다.
    • 기본 포트와 개발/릴리스 환경 경로가 더 일관되게 처리됩니다.

woosublee and others added 19 commits July 1, 2026 16:46
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@woosublee, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 45165da6-ae7d-4c92-914d-ec8c2b010c55

📥 Commits

Reviewing files that changed from the base of the PR and between d7022bf and e4a86f1.

📒 Files selected for processing (11)
  • Sources/CLIProxyManagerApp/ViewModels/DashboardViewModel.swift
  • Sources/CLIProxyManagerApp/Views/DashboardView.swift
  • Sources/CLIProxyManagerApp/Views/GeneralSettingsView.swift
  • Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIArchiveVerifier.swift
  • Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIBinaryStore.swift
  • Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIReleaseClient.swift
  • Tests/CLIProxyManagerAppTests/CLIProxyAPIUpdateUITests.swift
  • Tests/CLIProxyManagerCoreTests/CLIProxyAPIBinaryStoreTests.swift
  • Tests/CLIProxyManagerCoreTests/CLIProxyAPIReleaseClientTests.swift
  • docs/superpowers/plans/2026-07-04-cliproxyapi-update-about-ui.md
  • docs/superpowers/specs/2026-07-04-cliproxyapi-update-about-ui-design.md
📝 Walkthrough

Walkthrough

CLIProxyAPI 바이너리를 앱(Sparkle) 업데이트와 분리해 자가 업데이트하는 기능이 추가됩니다. 버전 파싱, GitHub 릴리스 조회, 아카이브 검증, active/pending 바이너리 저장소, 업데이트 서비스 상태머신 및 About 설정/Dashboard UI가 구현됩니다. 또한 DEBUG 빌드용 관리 경로 분리(~/.cliproxy-manager/dev)와 기본 포트(18318/18317) 분기가 추가됩니다.

Changes

CLIProxyAPI 바이너리 자가 업데이트

Layer / File(s) Summary
버전/매니페스트 모델
Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIVersion.swift, CLIProxyAPIBinaryManifest.swift, Tests/.../CLIProxyAPIVersionTests.swift, CLIProxyAPIBinaryManifestTests.swift
CLIProxyAPIVersion이 문자열 파싱/비교를 제공하고, CLIProxyAPIBinaryManifest가 바이너리 출처/버전/체크섬 메타데이터를 Codable로 표현한다.
릴리스 조회 및 HTTP 세션
Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIReleaseClient.swift, ProxyHealthClient.swift, Tests/.../CLIProxyAPIReleaseClientTests.swift
CLIProxyAPIReleaseClient가 GitHub 최신 릴리스와 checksums.txt를 조회해 다운로드 URL/체크섬을 구성하고, 외부 업데이트용 URLSession이 추가된다.
아카이브 검증기
Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIArchiveVerifier.swift, Tests/.../CLIProxyAPIArchiveVerifierTests.swift
아카이브 SHA-256 검증, tar 추출, --version 파싱을 통한 매니페스트 생성 및 오류 처리를 구현한다.
바이너리 저장소 및 경로/서비스매니저 wiring
Sources/CLIProxyManagerCore/Config/ManagedPaths.swift, Proxy/CLIProxyAPIBinaryStore.swift, Proxy/ProxyServiceManager.swift, CLIProxyManagerApp/BundledProxyBinary.swift, Tests/.../CLIProxyAPIBinaryStoreTests.swift, ProxyServiceManagerTests.swift
ManagedPaths에 active/pending 경로가 추가되고, CLIProxyAPIBinaryStore가 pending 저장/적용/prepareActiveBinary를 구현하며, ProxyServiceManager가 이를 통해 바이너리를 준비한다.
업데이트 서비스 상태머신
Sources/CLIProxyManagerApp/Services/CLIProxyAPIUpdateService.swift, Tests/.../CLIProxyAPIUpdateServiceTests.swift
자동/수동 체크, 다운로드+검증, pending 적용, 상태 영속화를 오케스트레이션하는 @MainActor 서비스가 추가된다.
앱/설정 UI 통합
CLIProxyManagerApp.swift, Views/DashboardView.swift, SettingsView.swift, GeneralSettingsView.swift, Tests/.../CLIProxyAPIUpdateUITests.swift, README.md
서비스 주입, 업데이트 확인/적용 다이얼로그, About>Updates row가 추가되고 README에 업데이트 동작이 문서화된다.

Estimated code review effort: 5 (Critical) | ~120 minutes

개발 빌드 경로 분리 및 기본 포트

Layer / File(s) Summary
DEBUG 경로/포트 분기 및 wiring
Config/AppConfig.swift, Config/ManagedPaths.swift, Views/SettingsSheets.swift, Services/AutomaticShellInstallService.swift, Tests/.../AppConfigTests.swift, AppConfigStoreTests.swift, ShellFunctionRendererTests.swift, AutomaticShellInstallServiceTests.swift, DashboardViewModelTests.swift, ProviderSettingsViewModelTests.swift
DEBUG 빌드는 ~/.cliproxy-manager/dev 루트와 18318 포트를 사용하고, 자동 셸 설치는 DEBUG에서 비활성화되며 관련 뷰/테스트가 갱신된다.

Estimated code review effort: 3 (Moderate) | ~25 minutes

설계/계획 문서

Layer / File(s) Summary
설계 및 계획 문서
docs/superpowers/specs/*, docs/superpowers/plans/*
자가 업데이트, 개발 경로 분리, About UI 이전에 대한 설계/구현 계획 문서가 신규 작성된다.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant Service as CLIProxyAPIUpdateService
  participant Release as CLIProxyAPIReleaseClient
  participant Verifier as CLIProxyAPIArchiveVerifier
  participant Store as CLIProxyAPIBinaryStore
  participant Server as ProxyServiceManager

  App->>Service: checkAutomaticallyOnLaunch()
  Service->>Release: latestRelease()
  Release-->>Service: CLIProxyAPIRelease
  Service-->>App: availableUpdate 표시
  App->>Service: downloadAvailableUpdate()
  Service->>Release: downloadArchive()
  Release-->>Service: archive Data
  Service->>Verifier: verify(archiveData, release)
  Verifier-->>Service: CLIProxyAPIBinaryVerificationResult
  Service->>Store: savePending(binary, manifest)
  Service-->>App: pendingUpdate 표시
  App->>Service: applyPendingNow()
  Service->>Store: applyPending()
  Store-->>Service: active 승격 완료
  App->>Server: restartServer() (실행 중일 때)
Loading

Possibly related PRs

  • woosublee/CLIProxyManager#26: BundledProxyBinary.manifestURL(...)bundledManifestURL wiring이 이 PR에서 확장한 번들 리소스 URL 해석 로직과 직접 연관됨.
  • woosublee/CLIProxyManager#32: SettingsView/AboutSettingsViewCLIProxyAPIUpdateService를 추가 주입하는 변경이 해당 PR의 About > Updates(Sparkle) UI 변경과 같은 영역을 다룸.
  • woosublee/CLIProxyManager#36: 번들 cliproxyapi/manifest.json을 읽어 업데이트 흐름에 wiring하는 이 PR의 코드가 해당 PR에서 다루는 매니페스트 아티팩트와 직접 연결됨.

Poem

토끼가 굴 속에서 코드를 캐네 🐰
낡은 바이너리는 이제 안녕,
체크섬 맞춰 새 옷 갈아입고
서버는 살짝 재시작 인사하네
dev 폴더엔 당근이 숨어있고
18318번 포트로 깡총깡총!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed CLIProxyAPI 바이너리의 자체 업데이트 흐름 추가라는 핵심 변경을 간결하게 잘 요약합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-cliproxyapi-binary-self-update

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an in-app update mechanism for the CLIProxyAPI binary, allowing it to be updated independently of the main CLIProxyManager app. It includes background update checks, checksum verification, and immediate or deferred update application, with UI controls integrated into the 'About' settings and automatic prompts on the dashboard. Additionally, debug builds are isolated to a separate dev directory. The code review feedback focuses on improving robustness and memory efficiency: adding a User-Agent header to GitHub API requests, supporting GoReleaser-style filename prefixes in the checksum parser, using a streaming approach to compute SHA256 hashes instead of loading large binaries into memory, and displaying the actual failure reason in the UI when an update check fails.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

}

public func latestRelease() async throws -> CLIProxyAPIRelease {
let data = try await httpClient.get(latestReleaseURL, headers: ["Accept": "application/vnd.github+json"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

GitHub's REST API strictly requires a valid User-Agent header for all requests. Without it, requests can be rejected with a 403 Forbidden error. Adding a custom User-Agent header (such as the app's name) ensures reliable API communication.

        let data = try await httpClient.get(latestReleaseURL, headers: [
            "Accept": "application/vnd.github+json",
            "User-Agent": "CLIProxyManager"
        ])

Comment on lines +79 to +88
static func checksum(for assetName: String, in data: Data) -> String? {
guard let text = String(data: data, encoding: .utf8) else { return nil }
for line in text.split(whereSeparator: { $0 == "\n" || $0 == "\r" }) {
let parts = line.split(whereSeparator: { $0 == " " || $0 == "\t" }).map(String.init)
if parts.count >= 2, parts[1] == assetName {
return parts[0]
}
}
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Standard checksum files (like those generated by sha256sum or GoReleaser) sometimes prefix binary filenames with a * character (e.g., hash *filename). Stripping this prefix dynamically makes the checksum parser more robust against different generation formats.

Suggested change
static func checksum(for assetName: String, in data: Data) -> String? {
guard let text = String(data: data, encoding: .utf8) else { return nil }
for line in text.split(whereSeparator: { $0 == "\n" || $0 == "\r" }) {
let parts = line.split(whereSeparator: { $0 == " " || $0 == "\t" }).map(String.init)
if parts.count >= 2, parts[1] == assetName {
return parts[0]
}
}
return nil
}
static func checksum(for assetName: String, in data: Data) -> String? {
guard let text = String(data: data, encoding: .utf8) else { return nil }
for line in text.split(whereSeparator: { $0 == "\n" || $0 == "\r" }) {
let parts = line.split(whereSeparator: { $0 == " " || $0 == "\t" }).map(String.init)
if parts.count >= 2 {
let filename = parts[1].hasPrefix("*") ? String(parts[1].dropFirst()) : parts[1]
if filename == assetName {
return parts[0]
}
}
}
return nil
}

Comment on lines +178 to +186
private func validateBinary(at url: URL, manifest: CLIProxyAPIBinaryManifest) throws {
if try Data(contentsOf: url).sha256HexDigest() != manifest.vendoredBinarySha256 {
throw CLIProxyAPIBinaryStoreError.binaryChecksumMismatch
}
let size = try url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? -1
if size != manifest.vendoredBinarySizeBytes {
throw CLIProxyAPIBinaryStoreError.binarySizeMismatch
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Loading the entire binary file into memory via Data(contentsOf:) to compute its SHA256 hash is memory-inefficient and can cause memory spikes, especially for larger binaries (around 40MB+). Using a streaming approach with FileHandle is much more memory-efficient.

Suggested change
private func validateBinary(at url: URL, manifest: CLIProxyAPIBinaryManifest) throws {
if try Data(contentsOf: url).sha256HexDigest() != manifest.vendoredBinarySha256 {
throw CLIProxyAPIBinaryStoreError.binaryChecksumMismatch
}
let size = try url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? -1
if size != manifest.vendoredBinarySizeBytes {
throw CLIProxyAPIBinaryStoreError.binarySizeMismatch
}
}
private func validateBinary(at url: URL, manifest: CLIProxyAPIBinaryManifest) throws {
if try url.sha256HexDigest() != manifest.vendoredBinarySha256 {
throw CLIProxyAPIBinaryStoreError.binaryChecksumMismatch
}
let size = try url.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? -1
if size != manifest.vendoredBinarySizeBytes {
throw CLIProxyAPIBinaryStoreError.binarySizeMismatch
}
}

Comment on lines +230 to +234
extension Data {
func sha256HexDigest() -> String {
SHA256.hash(data: self).map { String(format: "%02x", $0) }.joined()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add a streaming SHA256 helper extension on URL to allow memory-efficient file hashing without loading the entire file into memory.

Suggested change
extension Data {
func sha256HexDigest() -> String {
SHA256.hash(data: self).map { String(format: "%02x", $0) }.joined()
}
}
extension Data {
func sha256HexDigest() -> String {
SHA256.hash(data: self).map { String(format: "%02x", $0) }.joined()
}
}
extension URL {
func sha256HexDigest() throws -> String {
let fileHandle = try FileHandle(forReadingFrom: self)
defer { try? fileHandle.close() }
var sha256 = SHA256()
let bufferSize = 64 * 1024
while let data = try fileHandle.read(upToCount: bufferSize), !data.isEmpty {
sha256.update(data: data)
}
return sha256.finalize().map { String(format: "%02x", $0) }.joined()
}
}

Comment on lines +77 to +95
let binaryData = try Data(contentsOf: binaryURL)
let size = try binaryURL.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? binaryData.count
let manifest = CLIProxyAPIBinaryManifest(
name: "cliproxyapi",
version: metadata.version,
commit: metadata.commit,
builtAt: metadata.builtAt,
sourceKind: .userUpdated,
source: release.assetURL.absoluteString,
upstreamRepository: "router-for-me/CLIProxyAPI",
upstreamTag: release.tagName,
upstreamAsset: release.assetName,
upstreamAssetSha256: release.assetSha256,
vendoredBinaryName: "cliproxyapi",
vendoredBinarySha256: binaryData.sha256HexDigest(),
vendoredBinarySizeBytes: size,
vendoredFromArchivePath: "cli-proxy-api",
downloadedAt: ISO8601DateFormatter().string(from: Date())
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid loading the entire binary into memory to compute its size and SHA256 hash. Using the streaming sha256HexDigest() helper on URL is much more memory-efficient.

        let size = try binaryURL.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0
        let manifest = try CLIProxyAPIBinaryManifest(
            name: "cliproxyapi",
            version: metadata.version,
            commit: metadata.commit,
            builtAt: metadata.builtAt,
            sourceKind: .userUpdated,
            source: release.assetURL.absoluteString,
            upstreamRepository: "router-for-me/CLIProxyAPI",
            upstreamTag: release.tagName,
            upstreamAsset: release.assetName,
            upstreamAssetSha256: release.assetSha256,
            vendoredBinaryName: "cliproxyapi",
            vendoredBinarySha256: binaryURL.sha256HexDigest(),
            vendoredBinarySizeBytes: size,
            vendoredFromArchivePath: "cli-proxy-api",
            downloadedAt: ISO8601DateFormatter().string(from: Date())
        )

Comment on lines +194 to +195
case .failed:
return "Current version: \(currentVersion) · Last check failed."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Pattern-matching the associated error message in the .failed state allows displaying the actual failure reason to the user, which significantly improves usability.

Suggested change
case .failed:
return "Current version: \(currentVersion) · Last check failed."
case .failed(let message):
return "Current version: \(currentVersion) · Last check failed: \(message)"

Comment on lines +63 to +71
XCTAssertEqual(
cliproxyAPIUpdateDescription(
currentVersion: "7.2.41",
state: .failed("Network unavailable"),
availableUpdate: nil,
pendingUpdate: nil
),
"Current version: 7.2.41 · Last check failed."
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update the unit test to assert the new, more descriptive failure message format.

Suggested change
XCTAssertEqual(
cliproxyAPIUpdateDescription(
currentVersion: "7.2.41",
state: .failed("Network unavailable"),
availableUpdate: nil,
pendingUpdate: nil
),
"Current version: 7.2.41 · Last check failed."
)
XCTAssertEqual(
cliproxyAPIUpdateDescription(
currentVersion: "7.2.41",
state: .failed("Network unavailable"),
availableUpdate: nil,
pendingUpdate: nil
),
"Current version: 7.2.41 · Last check failed: Network unavailable"
)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (5)
Tests/CLIProxyManagerAppTests/AutomaticShellInstallServiceTests.swift (1)

7-21: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

컴파일 타임 분기 테스트라 항상 한쪽 브랜치만 실행됨

#if DEBUG 분기가 테스트 자체 내부에도 있어서, 실제로 실행되는 빌드 설정과 다른 쪽 어서션은 절대 검증되지 않습니다. 유닛 테스트가 보통 Debug 구성으로 빌드/실행되는 점을 감안하면 #else 분기(Release 시 정상 설치)는 CI에서 사실상 검증되지 않을 가능성이 높습니다. 큰 문제는 아니지만, Release 구성으로도 테스트를 한 번 돌리는 CI job이 있는지 확인해볼 가치가 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/CLIProxyManagerAppTests/AutomaticShellInstallServiceTests.swift` around
lines 7 - 21, The testRuntimeDefaultDoesNotInstallInDebugBuild test is using a
compile-time `#if` DEBUG branch, so only one assertion path is ever exercised in a
given build and the Release expectation is not really validated. Move the
Debug/Release behavior coverage out of a single conditional test in
AutomaticShellInstallServiceTests and either split it into separate
build-specific tests or ensure CI runs the test suite in both Debug and Release
configurations. Keep the assertions around
AutomaticShellInstallService.runtimeDefault and StubShellInstaller, but make
sure both install and non-install behavior are actually exercised by the test
matrix.
Sources/CLIProxyManagerApp/Services/CLIProxyAPIUpdateService.swift (1)

152-166: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

applyPendingNow()는 실패 시 recordFailure를 호출하지 않아 상태/에러 메시지가 일관되지 않음

check(suppressDeferredVersion:)downloadAvailableUpdate()는 실패 시 recordFailure(error)를 호출해 state.failed로 전환하고 lastErrorMessage/영속화된 lastFailureMessage/lastFailureAt을 갱신합니다. 반면 applyPendingNow()store.applyPending()이 던지는 오류를 그대로 다시 던지기만 하고 이런 상태 갱신을 하지 않습니다. 적용 실패(예: 매니페스트 쓰기 실패, 체크섬 불일치) 시 UI가 이 서비스의 state/lastErrorMessage에만 의존한다면 실패 정보가 누락될 수 있습니다.

♻️ 제안: recordFailure로 상태 일관성 확보
 func applyPendingNow() throws {
     refreshStoredStatus()
-    try store.applyPending()
+    do {
+        try store.applyPending()
+    } catch {
+        recordFailure(error)
+        throw error
+    }
     pendingUpdate = nil
     availableUpdate = nil
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/CLIProxyManagerApp/Services/CLIProxyAPIUpdateService.swift` around
lines 152 - 166, `CLIProxyAPIUpdateService.applyPendingNow()` handles
`store.applyPending()` failures without updating the service failure state,
unlike `check(suppressDeferredVersion:)` and `downloadAvailableUpdate()`. Wrap
the pending-apply flow in the same failure handling path by calling
`recordFailure(error)` when `store.applyPending()` throws, so `state`,
`lastErrorMessage`, and persisted failure metadata stay consistent. Keep the
existing success path in `applyPendingNow()` unchanged, and use the existing
`recordFailure` helper to centralize error-state updates.
Sources/CLIProxyManagerCore/Proxy/ProxyHealthClient.swift (1)

32-39: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

대용량 아카이브 다운로드 시 불필요한 디스크 캐싱 가능성.

requestCachePolicy = .reloadIgnoringLocalCacheData는 응답을 읽을 때 로컬 캐시를 무시할 뿐, 응답이 이후 URLCache.shared에 저장되는 것까지 막지는 않습니다. 릴리스 아카이브는 한 번만 사용되는 대용량(수십MB) 바이너리이므로, 캐시에 불필요하게 저장되어 디스크를 낭비하거나 다른 캐시 항목을 밀어낼 수 있습니다.

♻️ 제안: URL 캐시 비활성화
     public static func makeExternalUpdateSession() -> URLSession {
         let configuration = URLSessionConfiguration.default
         configuration.timeoutIntervalForRequest = 30
         configuration.timeoutIntervalForResource = 300
         configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
+        configuration.urlCache = nil
         return URLSession(configuration: configuration)
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/CLIProxyManagerCore/Proxy/ProxyHealthClient.swift` around lines 32 -
39, The URLSession created in makeExternalUpdateSession still allows large
release archive responses to be cached on disk even with
reloadIgnoringLocalCacheData. Update the URLSessionConfiguration in
ProxyHealthClient.makeExternalUpdateSession to disable caching for this one-shot
download session, so archive fetches do not get stored in URLCache.shared. Keep
the timeout settings as-is and make the cache behavior explicit in the same
configuration block.
Tests/CLIProxyManagerCoreTests/CLIProxyAPIReleaseClientTests.swift (1)

118-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

테스트 헬퍼 중복 (XCTAssertThrowsErrorAsync)

동일한 헬퍼 함수가 CLIProxyAPIArchiveVerifierTests.swift에도 그대로 복사되어 있습니다. 공용 테스트 유틸리티 파일로 추출하면 유지보수가 쉬워집니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/CLIProxyManagerCoreTests/CLIProxyAPIReleaseClientTests.swift` around
lines 118 - 130, The async test helper XCTAssertThrowsErrorAsync is duplicated
in CLIProxyAPIReleaseClientTests and CLIProxyAPIArchiveVerifierTests, so extract
it into a shared test utility used by both suites. Move the helper into a common
test support file, keep the same async throwing assertion behavior, and update
the tests to call that shared helper instead of maintaining separate copies.
Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIArchiveVerifier.swift (1)

79-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

"router-for-me/CLIProxyAPI" 매직 스트링 중복

이 저장소 식별자는 CLIProxyAPIReleaseClient.swift(컨텍스트 스니펫 기준 최신 릴리스 URL 구성에 사용)에도 동일하게 하드코딩되어 있습니다. 공유 상수로 추출하면 향후 저장소 변경 시 한 곳만 수정하면 됩니다.

♻️ 제안: 공유 상수로 추출
+public enum CLIProxyAPIRepository {
+    public static let identifier = "router-for-me/CLIProxyAPI"
+}
-            upstreamRepository: "router-for-me/CLIProxyAPI",
+            upstreamRepository: CLIProxyAPIRepository.identifier,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIArchiveVerifier.swift` around
lines 79 - 95, The repository identifier is hardcoded in multiple places, so
extract the shared value into a common constant and reuse it here in
CLIProxyAPIArchiveVerifier when building CLIProxyAPIBinaryManifest, as well as
in CLIProxyAPIReleaseClient; replace the inline "router-for-me/CLIProxyAPI"
string with that shared symbol so future repository changes only need one
update.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-07-04-cliproxyapi-update-about-ui.md`:
- Around line 35-37: Update the plan text so it matches the current branch
state: `AboutSettingsView` already takes `viewModel` and
`cliProxyAPIUpdateService` and includes the `CLIProxyAPI binary` row, so the
current wording should not imply that this UI still lives in
`ServerSettingsView`. Revise the affected bullet in the plan doc to reflect the
present `GeneralSettingsView`/`AboutSettingsView` setup, or explicitly label it
as describing the previous state; use the `AboutSettingsView` and
`ServerSettingsView` references to make the scope clear.

In `@docs/superpowers/specs/2026-07-04-cliproxyapi-update-about-ui-design.md`:
- Around line 198-206: `cliProxyAPIPendingUpdatePromptTitle`의 함수 서명을 문서와 호출부에서
하나로 맞추세요. 현재 문서의 선언에는 `currentVersion:`이 포함돼 있지만, `GeneralSettingsView.swift`
스니펫과 단계별 수정안은 `pendingUpdate:`만 사용하므로, `cliProxyAPIPendingUpdatePromptTitle` 정의와
이를 호출하는 쪽 모두 같은 매개변수 형태로 정리해 계약 불일치를 없애세요.

In `@Sources/CLIProxyManagerApp/Views/DashboardView.swift`:
- Around line 131-158: The CLIProxyAPI update apply flow is duplicated in
DashboardView and GeneralSettingsView, and the success message has already
drifted between the two implementations. Extract the shared apply logic from the
button action into a single DashboardViewModel method such as
applyCLIProxyAPIPendingUpdate(using:), keeping the try applyPendingNow(),
conditional restartServer(), and success/error settingsMessage handling in one
place. Then have both views call that shared method so the update behavior and
user-facing text stay consistent.
- Around line 108-130: In DashboardView’s confirmationDialog download action,
handle the failure path from CLIProxyAPIUpdateService.downloadAvailableUpdate()
instead of only checking pendingUpdate; since the service records failures in
state and does not throw, add explicit user feedback when pendingUpdate remains
nil after the Task completes, similar to the apply flow’s settingsMessage
handling. Use the existing symbols showCLIProxyAPIUpdatePrompt,
showCLIProxyAPIApplyPrompt, cliProxyAPIUpdateService.state, and
downloadAvailableUpdate() to surface an error message or alert so the user can
tell the download failed.

In `@Sources/CLIProxyManagerApp/Views/GeneralSettingsView.swift`:
- Around line 352-379: This confirmationDialog apply flow duplicates the same
apply/restart logic already present in DashboardView, and the success messages
have already drifted apart. Extract the shared apply behavior into a common
method on DashboardViewModel (or a shared helper used by both
GeneralSettingsView and DashboardView), then have this Button action call that
shared entry point and keep the status messaging consistent from a single place.

In `@Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIBinaryStore.swift`:
- Around line 50-122: `CLIProxyAPIBinaryStore`의 `applyPending()`와
`prepareActiveBinary()`가 동일한 파일 집합(`clipProxyBinary`, `activeClipProxyManifest`,
`pendingClipProxyDirectory`)을 갱신하므로, 두 경로를 같은 잠금으로 직렬화하도록 수정하세요.
`ProxyServiceManager.lifecycleLock`이 `ProxyServiceManager` 내부만 보호하는 현재 구조를 점검하고,
`CLIProxyAPIUpdateService.applyPendingNow()`와
`prepareActiveBinary(bundledBinaryURL:bundledManifestURL:)`가 공통으로 획득하는 파일 락 또는
단일 직렬화 진입점을 추가해 동시 실행을 막으세요. 이동/복구/삭제가 섞이지 않도록 락 획득과 해제를 두 메서드의 전체 작업 범위에 적용하세요.

---

Nitpick comments:
In `@Sources/CLIProxyManagerApp/Services/CLIProxyAPIUpdateService.swift`:
- Around line 152-166: `CLIProxyAPIUpdateService.applyPendingNow()` handles
`store.applyPending()` failures without updating the service failure state,
unlike `check(suppressDeferredVersion:)` and `downloadAvailableUpdate()`. Wrap
the pending-apply flow in the same failure handling path by calling
`recordFailure(error)` when `store.applyPending()` throws, so `state`,
`lastErrorMessage`, and persisted failure metadata stay consistent. Keep the
existing success path in `applyPendingNow()` unchanged, and use the existing
`recordFailure` helper to centralize error-state updates.

In `@Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIArchiveVerifier.swift`:
- Around line 79-95: The repository identifier is hardcoded in multiple places,
so extract the shared value into a common constant and reuse it here in
CLIProxyAPIArchiveVerifier when building CLIProxyAPIBinaryManifest, as well as
in CLIProxyAPIReleaseClient; replace the inline "router-for-me/CLIProxyAPI"
string with that shared symbol so future repository changes only need one
update.

In `@Sources/CLIProxyManagerCore/Proxy/ProxyHealthClient.swift`:
- Around line 32-39: The URLSession created in makeExternalUpdateSession still
allows large release archive responses to be cached on disk even with
reloadIgnoringLocalCacheData. Update the URLSessionConfiguration in
ProxyHealthClient.makeExternalUpdateSession to disable caching for this one-shot
download session, so archive fetches do not get stored in URLCache.shared. Keep
the timeout settings as-is and make the cache behavior explicit in the same
configuration block.

In `@Tests/CLIProxyManagerAppTests/AutomaticShellInstallServiceTests.swift`:
- Around line 7-21: The testRuntimeDefaultDoesNotInstallInDebugBuild test is
using a compile-time `#if` DEBUG branch, so only one assertion path is ever
exercised in a given build and the Release expectation is not really validated.
Move the Debug/Release behavior coverage out of a single conditional test in
AutomaticShellInstallServiceTests and either split it into separate
build-specific tests or ensure CI runs the test suite in both Debug and Release
configurations. Keep the assertions around
AutomaticShellInstallService.runtimeDefault and StubShellInstaller, but make
sure both install and non-install behavior are actually exercised by the test
matrix.

In `@Tests/CLIProxyManagerCoreTests/CLIProxyAPIReleaseClientTests.swift`:
- Around line 118-130: The async test helper XCTAssertThrowsErrorAsync is
duplicated in CLIProxyAPIReleaseClientTests and CLIProxyAPIArchiveVerifierTests,
so extract it into a shared test utility used by both suites. Move the helper
into a common test support file, keep the same async throwing assertion
behavior, and update the tests to call that shared helper instead of maintaining
separate copies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9241af23-976c-46ed-9bda-44addd039f54

📥 Commits

Reviewing files that changed from the base of the PR and between d5e7015 and d7022bf.

📒 Files selected for processing (39)
  • README.md
  • Sources/CLIProxyManagerApp/BundledProxyBinary.swift
  • Sources/CLIProxyManagerApp/CLIProxyManagerApp.swift
  • Sources/CLIProxyManagerApp/Services/AutomaticShellInstallService.swift
  • Sources/CLIProxyManagerApp/Services/CLIProxyAPIUpdateService.swift
  • Sources/CLIProxyManagerApp/ViewModels/DashboardViewModel.swift
  • Sources/CLIProxyManagerApp/Views/DashboardView.swift
  • Sources/CLIProxyManagerApp/Views/GeneralSettingsView.swift
  • Sources/CLIProxyManagerApp/Views/SettingsSheets.swift
  • Sources/CLIProxyManagerApp/Views/SettingsView.swift
  • Sources/CLIProxyManagerCore/Config/AppConfig.swift
  • Sources/CLIProxyManagerCore/Config/ManagedPaths.swift
  • Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIArchiveVerifier.swift
  • Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIBinaryManifest.swift
  • Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIBinaryStore.swift
  • Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIReleaseClient.swift
  • Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIVersion.swift
  • Sources/CLIProxyManagerCore/Proxy/ProxyHealthClient.swift
  • Sources/CLIProxyManagerCore/Proxy/ProxyServiceManager.swift
  • Tests/CLIProxyManagerAppTests/AutomaticShellInstallServiceTests.swift
  • Tests/CLIProxyManagerAppTests/CLIProxyAPIUpdateServiceTests.swift
  • Tests/CLIProxyManagerAppTests/CLIProxyAPIUpdateUITests.swift
  • Tests/CLIProxyManagerAppTests/DashboardViewModelTests.swift
  • Tests/CLIProxyManagerAppTests/ProviderSettingsViewModelTests.swift
  • Tests/CLIProxyManagerCoreTests/AppConfigStoreTests.swift
  • Tests/CLIProxyManagerCoreTests/AppConfigTests.swift
  • Tests/CLIProxyManagerCoreTests/CLIProxyAPIArchiveVerifierTests.swift
  • Tests/CLIProxyManagerCoreTests/CLIProxyAPIBinaryManifestTests.swift
  • Tests/CLIProxyManagerCoreTests/CLIProxyAPIBinaryStoreTests.swift
  • Tests/CLIProxyManagerCoreTests/CLIProxyAPIReleaseClientTests.swift
  • Tests/CLIProxyManagerCoreTests/CLIProxyAPIVersionTests.swift
  • Tests/CLIProxyManagerCoreTests/ProxyServiceManagerTests.swift
  • Tests/CLIProxyManagerCoreTests/ShellFunctionRendererTests.swift
  • docs/superpowers/plans/2026-07-01-cliproxyapi-binary-self-update.md
  • docs/superpowers/plans/2026-07-02-dev-managed-path.md
  • docs/superpowers/plans/2026-07-04-cliproxyapi-update-about-ui.md
  • docs/superpowers/specs/2026-07-01-cliproxyapi-binary-self-update-design.md
  • docs/superpowers/specs/2026-07-02-dev-managed-path-design.md
  • docs/superpowers/specs/2026-07-04-cliproxyapi-update-about-ui-design.md

Comment thread docs/superpowers/plans/2026-07-04-cliproxyapi-update-about-ui.md Outdated
Comment thread Sources/CLIProxyManagerApp/Views/DashboardView.swift
Comment on lines +131 to +158
.confirmationDialog(
cliProxyAPIPendingUpdatePromptTitle(pendingUpdate: cliProxyAPIUpdateService.pendingUpdate),
isPresented: $showCLIProxyAPIApplyPrompt,
titleVisibility: .visible
) {
Button(cliProxyAPIApplyButtonTitle(
pendingUpdate: cliProxyAPIUpdateService.pendingUpdate,
isServerRunning: viewModel.serverControlState.isRunning
)) {
Task {
do {
try cliProxyAPIUpdateService.applyPendingNow()
if viewModel.serverControlState.isRunning {
await viewModel.restartServer()
}
viewModel.settingsMessage = "CLIProxyAPI binary updated. Restarting the app is not required."
} catch {
viewModel.settingsMessage = "CLIProxyAPI update failed: \(error.localizedDescription)"
}
}
}
Button("Apply on next server start") {
viewModel.settingsMessage = "CLIProxyAPI update will be applied on next server start."
}
Button("Cancel", role: .cancel) {}
} message: {
Text(cliProxyAPIPendingUpdatePromptMessage(currentVersion: cliProxyAPIUpdateService.currentVersionText))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

GeneralSettingsView.swift(352-379)와 apply 로직이 그대로 중복되어 이미 문구가 갈라짐.

두 파일 모두 try applyPendingNow() → 서버 실행 중이면 restartServer() → 성공/실패 메시지 설정이라는 동일한 흐름을 독립적으로 구현했습니다. 그 결과 성공 메시지가 서로 다릅니다: 여기는 "CLIProxyAPI binary updated. Restarting the app is not required.", GeneralSettingsView는 "CLIProxyAPI update applied."입니다. DashboardViewModel에 공용 메서드(예: applyCLIProxyAPIPendingUpdate(using:))로 추출해 두 뷰에서 재사용하면 문구 불일치와 향후 유지보수 시 로직 이탈 위험을 없앨 수 있습니다.

♻️ 제안 리팩터
// DashboardViewModel.swift
func applyCLIProxyAPIPendingUpdate(using service: CLIProxyAPIUpdateService) async {
    do {
        try service.applyPendingNow()
        if serverControlState.isRunning {
            await restartServer()
        }
        settingsMessage = "CLIProxyAPI binary updated. Restarting the app is not required."
    } catch {
        settingsMessage = "CLIProxyAPI update failed: \(error.localizedDescription)"
    }
}

이후 양쪽 뷰의 버튼 클로저는 Task { await viewModel.applyCLIProxyAPIPendingUpdate(using: cliProxyAPIUpdateService) }로 단순화합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/CLIProxyManagerApp/Views/DashboardView.swift` around lines 131 - 158,
The CLIProxyAPI update apply flow is duplicated in DashboardView and
GeneralSettingsView, and the success message has already drifted between the two
implementations. Extract the shared apply logic from the button action into a
single DashboardViewModel method such as applyCLIProxyAPIPendingUpdate(using:),
keeping the try applyPendingNow(), conditional restartServer(), and
success/error settingsMessage handling in one place. Then have both views call
that shared method so the update behavior and user-facing text stay consistent.

Comment thread Sources/CLIProxyManagerApp/Views/GeneralSettingsView.swift
Comment thread Sources/CLIProxyManagerCore/Proxy/CLIProxyAPIBinaryStore.swift Outdated
Co-Authored-By: Claude <noreply@anthropic.com>
@woosublee

Copy link
Copy Markdown
Owner Author

Review feedback addressed in e4a86f1:

  • Added User-Agent: CLIProxyManager to GitHub release, checksum, and archive download requests.
  • Made checksum parsing handle *filename entries.
  • Added streaming URL.sha256HexDigest() and switched binary validation/archive manifest hashing to streaming file reads.
  • Surface CLIProxyAPI download failures in Dashboard/About via settingsMessage.
  • Centralized pending-update apply/restart/status-message logic in DashboardViewModel.applyCLIProxyAPIPendingUpdate(using:).
  • Serialized CLIProxyAPIBinaryStore mutable file operations with a shared lock.
  • Aligned the About UI spec/plan helper signature/context notes.

Verification:

  • DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift test --filter CLIProxyAPIUpdateUITests — 13 tests passed
  • DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift test --filter CLIProxyAPIReleaseClientTests --filter CLIProxyAPIBinaryStoreTests/testBinaryStoreSerializesMutableOperationsWithSharedLock --filter CLIProxyAPIArchiveVerifierTests — 13 tests passed
  • DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift test — 353 tests passed
  • git diff --check — passed

CodeRabbit is currently rate-limited/pending; the branch is pushed and ready for the next bot review when available.

@woosublee woosublee merged commit 0f3b635 into main Jul 5, 2026
1 check passed
@woosublee woosublee deleted the worktree-cliproxyapi-binary-self-update branch July 5, 2026 13:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant