mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 04:15:52 +00:00
Adds a lightweight self-update path to the CCS Bar menu-bar app so users are told when a newer build is available and can update with one click. - App: BarUpdateChecker fetches the published version.txt and compares it to the running bundle version (numeric semver). A throttled background check (post-launch, then every 6h) surfaces an "Update available" row in the menu; "Update Now" spawns `ccs bar install --launch --await-quit` then quits so the installer can swap the locked bundle and relaunch. A Settings toggle gates the auto-check (default on). Offline checks degrade silently. - CLI: `ccs bar install --await-quit` waits for a running CCS Bar to quit (300ms poll, 15s cap) before swapping, enabling the one-click relaunch. - Release: bar-release publishes a machine-readable version.txt asset beside the app zip so the check is cheap (no full download just to compare). - Shares the ccs-binary resolver between DashboardLauncher and the updater. - Bumps bar VERSION to 1.8.0; adds isNewer assertions to ccs-bar-check. Closes #1588
88 lines
3.0 KiB
Swift
88 lines
3.0 KiB
Swift
import AppKit
|
|
import CCSBarCore
|
|
import Foundation
|
|
|
|
// MARK: - Shared ccs binary resolver
|
|
|
|
/// Resolves the `ccs` binary path for GUI apps that don't inherit the shell
|
|
/// PATH. Shared by `DashboardLauncher` and `BarViewModel` so the candidate
|
|
/// list stays in one place (DRY).
|
|
enum CCSBinaryResolver {
|
|
/// Ordered list of candidate paths for the `ccs` binary. First executable
|
|
/// match wins. GUI apps don't inherit the shell PATH, so we must enumerate
|
|
/// common install locations explicitly.
|
|
static var candidates: [String] {
|
|
let home = NSHomeDirectory()
|
|
return [
|
|
"\(home)/.bun/bin/ccs",
|
|
"/opt/homebrew/bin/ccs",
|
|
"/usr/local/bin/ccs",
|
|
"\(home)/.local/bin/ccs",
|
|
"\(home)/.npm-global/bin/ccs",
|
|
"/usr/bin/ccs",
|
|
]
|
|
}
|
|
|
|
/// Returns the path of the first executable `ccs` binary found, or `nil`
|
|
/// when none of the candidates exist.
|
|
static func resolve() -> String? {
|
|
candidates.first { FileManager.default.isExecutableFile(atPath: $0) }
|
|
}
|
|
}
|
|
|
|
// MARK: - Dashboard launcher
|
|
|
|
/// Opens the CCS dashboard, starting the local server when it isn't running.
|
|
///
|
|
/// The dashboard is just a page served by the CCS web-server, so it can't load
|
|
/// if no server is up. When the discovered URL isn't reachable we launch
|
|
/// `ccs config`, which boots the server AND opens the dashboard in the browser
|
|
/// itself — so we must not also open it (that would double-open a tab).
|
|
enum DashboardLauncher {
|
|
private static func ccsBinary() -> String? {
|
|
CCSBinaryResolver.resolve()
|
|
}
|
|
|
|
private static func dashboardURL() -> URL? {
|
|
if case .success(let discovery) = BarDiscovery.load() { return discovery.resolvedURL }
|
|
return nil
|
|
}
|
|
|
|
/// Quick reachability probe so a stale `bar.json` URL doesn't send the user to
|
|
/// a dead page. Short timeout — the dashboard is local.
|
|
private static func isReachable(_ url: URL) async -> Bool {
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "HEAD"
|
|
request.timeoutInterval = 1.5
|
|
return (try? await URLSession.shared.data(for: request)) != nil
|
|
}
|
|
|
|
@MainActor
|
|
static func openOrStart() async {
|
|
if let url = dashboardURL(), await isReachable(url) {
|
|
NSWorkspace.shared.open(url)
|
|
return
|
|
}
|
|
|
|
// Server isn't up. Start it via `ccs config`; it opens the dashboard on its
|
|
// own. Avoid a shell here: candidate paths can include user-controlled
|
|
// components, and Process can pass the executable and arguments directly.
|
|
if let bin = ccsBinary() {
|
|
let proc = Process()
|
|
proc.executableURL = URL(fileURLWithPath: bin)
|
|
proc.arguments = ["config"]
|
|
let null = FileHandle(forWritingAtPath: "/dev/null")
|
|
proc.standardOutput = null
|
|
proc.standardError = null
|
|
try? proc.run()
|
|
return
|
|
}
|
|
|
|
// Can't find the ccs binary — best effort: open whatever URL we know so the
|
|
// user at least lands on the right place (or sees the connection is refused).
|
|
if let url = dashboardURL() {
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
}
|
|
}
|