diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..47ebacb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + labels: ["dependencies"] + commit-message: + prefix: "chore(ci)" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f86f0f..db3ea67 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,6 +12,10 @@ jobs: - uses: actions/checkout@v4 - name: Toolchain run: swift --version + - name: Lint + run: | + brew install swiftlint --quiet + swiftlint --strict - name: Build (release) run: swift build -c release - name: Test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1218ce5..7a8f103 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -104,6 +104,37 @@ jobs: --title "MLX Control ${VERSION}" \ --notes "${NOTES:-See CHANGELOG.md}" + # ── Homebrew Cask 자동 업데이트 ── + # homebrew-mlxcontrol 탭 레포가 있을 때 활성화. 없으면 이 스텝은 스킵됨. + - name: Update Homebrew Cask + env: + GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} # repo 쓰기 권한 PAT + if: env.GH_TOKEN != '' + run: | + VERSION="${GITHUB_REF_NAME#v}" + DMG="MLXControl-${VERSION}.dmg" + SHA=$(shasum -a 256 "$DMG" | awk '{print $1}') + + # homebrew-mlxcontrol 레포 클론 → cask 파일 수정 → PR + gh repo clone wonsss/homebrew-mlxcontrol /tmp/tap + cd /tmp/tap + sed -i '' \ + -e "s|version \".*\"|version \"${VERSION}\"|" \ + -e "s|sha256 \".*\"|sha256 \"${SHA}\"|" \ + Casks/mlxcontrol.rb + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + BRANCH="bump-v${VERSION}" + git checkout -b "$BRANCH" + git add Casks/mlxcontrol.rb + git commit -m "chore: bump mlxcontrol to v${VERSION}" + git push origin "$BRANCH" + gh pr create \ + --repo wonsss/homebrew-mlxcontrol \ + --title "chore: bump mlxcontrol to v${VERSION}" \ + --body "Auto-updated by release workflow. SHA256: \`${SHA}\`" \ + --base main --head "$BRANCH" + # ── 키체인 정리 ── - name: Cleanup keychain if: always() diff --git a/.swiftlint.yml b/.swiftlint.yml index 640e963..aa5e824 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,17 +1,26 @@ -# SwiftLint config (local dev). Install: brew install swiftlint — then run: swiftlint +# SwiftLint config. Install: brew install swiftlint — then run: swiftlint included: - Sources - Tests disabled_rules: - todo - trailing_comma + - switch_case_alignment # SwiftUI inline switch expressions trigger false positives + - implicit_optional_initialization # = nil is explicit intent here, not redundancy +opt_in_rules: + - empty_count line_length: - warning: 120 - error: 180 + warning: 130 + error: 200 ignores_comments: true + ignores_interpolated_strings: true identifier_name: min_length: 1 type_body_length: warning: 600 file_length: - warning: 1000 + warning: 1400 + error: 2000 +cyclomatic_complexity: + warning: 15 + error: 25 diff --git a/Sources/MLXControl/MLXControlApp.swift b/Sources/MLXControl/MLXControlApp.swift index da19605..c77508e 100644 --- a/Sources/MLXControl/MLXControlApp.swift +++ b/Sources/MLXControl/MLXControlApp.swift @@ -46,7 +46,7 @@ enum Config { "/opt/homebrew/bin/" + name, "/usr/local/bin/" + name, ] - for c in candidates { if FileManager.default.isExecutableFile(atPath: c) { return c } } + for c in candidates where FileManager.default.isExecutableFile(atPath: c) { return c } let paths = ProcessInfo.processInfo.environment["PATH"]? .split(separator: ":").map(String.init) ?? [] return paths.map { $0 + "/" + name } @@ -88,7 +88,7 @@ func ramFeasibility(modelBytes: Int?, freeGB: Double) -> RAMFeasibility? { guard let b = modelBytes, freeGB > 0 else { return nil } let modelGB = Double(b) / 1_073_741_824 if modelGB < freeGB * 0.8 { return .ok } - if modelGB < freeGB { return .warn } + if modelGB < freeGB { return .warn } return .insufficient } @@ -180,7 +180,6 @@ enum MoveToApplications { try fm.copyItem(atPath: src, toPath: dest) } catch { // On copy failure, retry via Process (no AppleScript string injection) - let rm = Process(); rm.executableURL = URL(fileURLWithPath: "/bin/rm") rm.arguments = ["-rf", dest] try? rm.run(); rm.waitUntilExit() @@ -671,7 +670,8 @@ final class ServerController { let alert = NSAlert() alert.messageText = "Delete model" - alert.informativeText = "\(repo)\n\nReclaims about \(folderSize(dir))\nPermanently removes it from the disk cache." + let sizeStr = folderSize(dir) + alert.informativeText = "\(repo)\n\nReclaims about \(sizeStr)\nPermanently removes it from the disk cache." alert.alertStyle = .warning alert.addButton(withTitle: "Delete") alert.addButton(withTitle: "Cancel") @@ -781,7 +781,7 @@ final class ServerController { } private static func runSearch(_ q: String) async -> [ModelHit] { - var comps = URLComponents(string: "https://huggingface.co/api/models")! + guard var comps = URLComponents(string: "https://huggingface.co/api/models") else { return [] } comps.queryItems = [ .init(name: "author", value: "mlx-community"), .init(name: "search", value: q), @@ -864,7 +864,8 @@ struct Sparkline: View { Path { p in p.move(to: CGPoint(x: 0, y: h)) pts.forEach { p.addLine(to: $0) } - p.addLine(to: CGPoint(x: pts.last!.x, y: h)); p.closeSubpath() + if let last = pts.last { p.addLine(to: CGPoint(x: last.x, y: h)) } + p.closeSubpath() }.fill(.green.opacity(0.15)) Path { p in p.move(to: pts[0]); pts.dropFirst().forEach { p.addLine(to: $0) }