diff --git a/macos-bar/Sources/CCSBarApp/BarMenuView.swift b/macos-bar/Sources/CCSBarApp/BarMenuView.swift index b5d29124..14a09437 100644 --- a/macos-bar/Sources/CCSBarApp/BarMenuView.swift +++ b/macos-bar/Sources/CCSBarApp/BarMenuView.swift @@ -195,6 +195,11 @@ struct BarMenuView: View { if viewModel.isRefreshing { ProgressView().controlSize(.small) } + if let v = BarVersionDisplay.string() { + Text(v) + .font(.caption2) + .foregroundStyle(.secondary) + } } .padding(.horizontal, 14) .padding(.vertical, 10) diff --git a/macos-bar/Sources/CCSBarCheck/main.swift b/macos-bar/Sources/CCSBarCheck/main.swift index 0932ca85..a49e9b92 100644 --- a/macos-bar/Sources/CCSBarCheck/main.swift +++ b/macos-bar/Sources/CCSBarCheck/main.swift @@ -1436,6 +1436,15 @@ do { check(selectedIdx == 1, "screenPicker: exact hit selects right screen (index 1) for #1502 anchor") } +// MARK: BarVersionDisplay — pure display-string helper + +check(BarVersionDisplay.displayString(for: nil) == nil, "version nil raw -> nil (no dangling 'v')") +check(BarVersionDisplay.displayString(for: "") == nil, "version empty raw -> nil (no dangling 'v')") +check(BarVersionDisplay.displayString(for: "1.5.0") == "v1.5.0", "version '1.5.0' -> 'v1.5.0'") +check(BarVersionDisplay.displayString(for: "2.0.0") == "v2.0.0", "version '2.0.0' -> 'v2.0.0'") +// Outside a bundle, string() must not crash and returns nil (no assertion on value — bundle absent). +let _ = BarVersionDisplay.string() + // cleanup try? FileManager.default.removeItem(atPath: tmp) diff --git a/macos-bar/Sources/CCSBarCore/BarVersionDisplay.swift b/macos-bar/Sources/CCSBarCore/BarVersionDisplay.swift new file mode 100644 index 00000000..54bdee79 --- /dev/null +++ b/macos-bar/Sources/CCSBarCore/BarVersionDisplay.swift @@ -0,0 +1,28 @@ +import Foundation + +/// Pure helper for the version string shown in the bar panel header. +/// +/// Kept tiny so it can be tested in CCSBarCheck without a bundle present: +/// the display-string logic is pure-Foundation and has no AppKit dependency. +public enum BarVersionDisplay { + + /// Converts a raw version string to a "v{version}" display string. + /// + /// - Returns: `"v\(raw)"` when `raw` is non-nil and non-empty; `nil` otherwise. + /// + /// This is the logic under test. The actual `Bundle.main` lookup is in `string()`. + public static func displayString(for raw: String?) -> String? { + guard let v = raw, !v.isEmpty else { return nil } + return "v\(v)" + } + + /// Returns the display string for the running app's bundle version, or `nil` + /// when the key is absent (e.g. `swift run` outside a bundle). + /// + /// Never produces a dangling "v" prefix: if `CFBundleShortVersionString` is + /// missing or empty, returns `nil` so the caller can omit the label entirely. + public static func string() -> String? { + let raw = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + return displayString(for: raw) + } +} diff --git a/src/commands/bar/install-subcommand.ts b/src/commands/bar/install-subcommand.ts index af5b2670..c51d740a 100644 --- a/src/commands/bar/install-subcommand.ts +++ b/src/commands/bar/install-subcommand.ts @@ -681,7 +681,7 @@ export async function handleBarInstall( if (barIsRunning && !forceLaunch) { console.log( - '[!] CCS Bar is currently running the previous version. Quit and reopen it (or run `ccs bar`) to use the update.' + '[!] CCS Bar is currently running an older build. Quit it from the menu bar, then run `ccs bar` to relaunch the updated app.' ); } else if (forceLaunch) { await launchBar([]); diff --git a/tests/unit/commands/bar-command.test.ts b/tests/unit/commands/bar-command.test.ts index 4ad796f7..9c1f7c8d 100644 --- a/tests/unit/commands/bar-command.test.ts +++ b/tests/unit/commands/bar-command.test.ts @@ -2622,8 +2622,9 @@ describe('bar install: already-running detection (Finding 3)', () => { const allOutput = consoleOutput.join('\n'); // Must print the restart hint - expect(allOutput).toMatch(/currently running the previous version/i); - expect(allOutput).toMatch(/Quit and reopen/i); + expect(allOutput).toMatch(/currently running an older build/i); + expect(allOutput).toMatch(/Quit it from the menu bar/i); + expect(allOutput).toMatch(/run `ccs bar` to relaunch/i); }); it('when not running after install: prompt path unchanged', async () => { @@ -2646,7 +2647,7 @@ describe('bar install: already-running detection (Finding 3)', () => { expect(promptCalled).toBe(true); const allOutput = consoleOutput.join('\n'); // The restart hint must NOT appear when bar is not running - expect(allOutput).not.toMatch(/currently running the previous version/i); + expect(allOutput).not.toMatch(/currently running an older build/i); }); it('pgrep error treated as not running: prompt path unchanged', async () => { @@ -2670,7 +2671,7 @@ describe('bar install: already-running detection (Finding 3)', () => { // pgrep error treated as not running → normal prompt flow expect(promptCalled).toBe(true); const allOutput = consoleOutput.join('\n'); - expect(allOutput).not.toMatch(/currently running the previous version/i); + expect(allOutput).not.toMatch(/currently running an older build/i); }); it('--launch with bar already running: launchBar still invoked', async () => { @@ -2693,7 +2694,7 @@ describe('bar install: already-running detection (Finding 3)', () => { expect(launchCalled).toBe(true); // Restart hint must NOT appear when --launch is passed const allOutput = consoleOutput.join('\n'); - expect(allOutput).not.toMatch(/currently running the previous version/i); + expect(allOutput).not.toMatch(/currently running an older build/i); }); });