Skip to content

Commit 63024bc

Browse files
committed
auto-updates
1 parent c02111a commit 63024bc

11 files changed

Lines changed: 141 additions & 3 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ jobs:
3030
- name: Run release script
3131
env:
3232
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33+
SPARKLE_PRIVATE_ED_KEY: ${{ secrets.SPARKLE_PRIVATE_ED_KEY }}
3334
run: bash scripts/release.sh "$GITHUB_REF_NAME"

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ xcuserdata/
66
DerivedData/
77
.swiftpm/
88
.netrc
9+
sparkle_private_key

Package.resolved

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,21 @@ let package = Package(
1212
targets: ["AgentTally"]
1313
)
1414
],
15+
dependencies: [
16+
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.9.1")
17+
],
1518
targets: [
1619
.executableTarget(
17-
name: "AgentTally"
20+
name: "AgentTally",
21+
dependencies: [
22+
.product(name: "Sparkle", package: "Sparkle")
23+
],
24+
linkerSettings: [
25+
.unsafeFlags([
26+
"-Xlinker", "-rpath",
27+
"-Xlinker", "@executable_path/../Frameworks",
28+
])
29+
]
1830
)
1931
]
2032
)

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
`AgentTally` is a standalone macOS menu bar app for tracking AI agent spending.
66

7-
Right now, only Claude Code is supported. The app shows your Claude Code spend for today and the current month, and refreshes every 60s.
7+
The app shows Claude Code and Codex spend for today and the current month, and refreshes every 60s.
88

99
<p align="center">
1010
<img src="docs/menu-bar.png" alt="AgentTally menu bar screenshot">
@@ -25,6 +25,9 @@ Then:
2525

2626
On first launch, macOS may ask you to confirm opening the app.
2727

28+
AgentTally checks GitHub Releases for updates once per day and includes a
29+
`Check for Updates...` menu item.
30+
2831
## Development
2932

3033
To build from source, you need:
@@ -49,3 +52,17 @@ mise run dev
4952
```
5053

5154
`mise` manages the Bun toolchain for this project and uses the system Swift toolchain. The build tasks install the local `ccusage` dependency, compile the helper, and stage both binaries into `AgentTally.app`.
55+
56+
## Releases
57+
58+
Release archives are signed for Sparkle updates with an EdDSA key. For local
59+
releases, the private key is read from the macOS Keychain. For GitHub Actions,
60+
set `SPARKLE_PRIVATE_ED_KEY` to the value exported by:
61+
62+
```sh
63+
.build/artifacts/sparkle/Sparkle/bin/generate_keys -x sparkle_private_key
64+
```
65+
66+
The app bundle is ad-hoc code signed by default so Sparkle can verify release
67+
archives without requiring an Apple Developer ID certificate. Set
68+
`CODESIGN_IDENTITY` to use a real signing identity later.

Sources/AgentTally/App/AppDelegate.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import AppKit
22
import Foundation
3+
import Sparkle
34

45
@MainActor
56
final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
@@ -13,6 +14,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
1314
private var lastSuccessfulAgentData: [AgentKind: AgentRawData] = [:]
1415
private var lastUsageDataFingerprints: [AgentKind: UsageDataFingerprint] = [:]
1516
private let loginItemManager = LoginItemManager()
17+
private let updaterController = SPUStandardUpdaterController(
18+
startingUpdater: true,
19+
updaterDelegate: nil,
20+
userDriverDelegate: nil
21+
)
1622
private var startAtLoginViewState = StartAtLoginViewState.make(status: .notRegistered)
1723

1824
func applicationDidFinishLaunching(_ notification: Notification) {
@@ -60,6 +66,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
6066
refreshMenuIfNeeded()
6167
}
6268

69+
@objc
70+
private func checkForUpdatesMenuItemSelected(_ sender: NSMenuItem) {
71+
updaterController.checkForUpdates(sender)
72+
}
73+
6374
@objc
6475
private func quitMenuItemSelected() {
6576
NSApplication.shared.terminate(nil)
@@ -253,6 +264,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
253264
return #selector(startAtLoginMenuItemSelected(_:))
254265
case .refresh:
255266
return #selector(refreshMenuItemSelected)
267+
case .checkForUpdates:
268+
return #selector(checkForUpdatesMenuItemSelected(_:))
256269
case .quit:
257270
return #selector(quitMenuItemSelected)
258271
}

Sources/AgentTally/Presentation/MenuRowsBuilder.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public enum MenuCheckState: Equatable {
99
public enum MenuActionKind: Equatable {
1010
case startAtLogin
1111
case refresh
12+
case checkForUpdates
1213
case quit
1314
}
1415

@@ -90,6 +91,9 @@ public enum MenuRowsBuilder {
9091
}
9192

9293
rows.append(.separator)
94+
rows.append(
95+
.action(title: "Check for Updates...", kind: .checkForUpdates, keyEquivalent: "", state: .off)
96+
)
9397
rows.append(
9498
.action(
9599
title: "Open at Login",

Tests/MenuRowsHarness.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,17 @@ func testMenuRowsBuilder() throws {
107107
"Codex not installed should not show spending section"
108108
)
109109

110+
try expect(
111+
rows.contains(
112+
.action(
113+
title: "Check for Updates...",
114+
kind: .checkForUpdates,
115+
keyEquivalent: "",
116+
state: .off
117+
)
118+
),
119+
"menu should contain update check action"
120+
)
110121
try expect(
111122
rows.contains(
112123
.action(

scripts/bundle_app.sh

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,18 @@ configuration="${1:-debug}"
55
app_dir=".build/${configuration}/AgentTally.app"
66
contents_dir="${app_dir}/Contents"
77
macos_dir="${contents_dir}/MacOS"
8+
frameworks_dir="${contents_dir}/Frameworks"
89
executable=".build/${configuration}/AgentTally"
910
helper=".build/agenttally-usage-helper"
1011
plist_path="${contents_dir}/Info.plist"
12+
codesign_identity="${CODESIGN_IDENTITY:--}"
13+
sparkle_framework="$(
14+
find .build/artifacts/sparkle/Sparkle/Sparkle.xcframework \
15+
-type d \
16+
-name Sparkle.framework \
17+
-print \
18+
-quit 2>/dev/null
19+
)"
1120

1221
if [[ ! -x "${executable}" ]]; then
1322
echo "missing executable: ${executable}" >&2
@@ -19,9 +28,19 @@ if [[ ! -x "${helper}" ]]; then
1928
exit 1
2029
fi
2130

31+
if [[ -z "${sparkle_framework}" || ! -d "${sparkle_framework}" ]]; then
32+
echo "missing Sparkle.framework; run swift package resolve/build first" >&2
33+
exit 1
34+
fi
35+
2236
rm -rf "${app_dir}"
23-
mkdir -p "${macos_dir}"
37+
mkdir -p "${macos_dir}" "${frameworks_dir}"
2438

2539
cp "${executable}" "${macos_dir}/AgentTally"
2640
cp "${helper}" "${macos_dir}/agenttally-usage-helper"
41+
ditto "${sparkle_framework}" "${frameworks_dir}/Sparkle.framework"
2742
bash scripts/generate_info_plist.sh "${plist_path}"
43+
44+
codesign --force --sign "${codesign_identity}" "${macos_dir}/agenttally-usage-helper"
45+
codesign --force --sign "${codesign_identity}" "${macos_dir}/AgentTally"
46+
codesign --force --sign "${codesign_identity}" "${app_dir}"

scripts/generate_info_plist.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ set -euo pipefail
33

44
version="${AGENTTALLY_VERSION:-0.0.0-dev}"
55
plist_path="${1:?output plist path required}"
6+
sparkle_feed_url="${SPARKLE_FEED_URL:-https://github.com/stephanos/agenttally-macos/releases/latest/download/appcast.xml}"
7+
sparkle_public_ed_key="${SPARKLE_PUBLIC_ED_KEY:-NUtyqJx9cL1Uf2b9gHKY3SzJbp/aizxf46tYylyIBBI=}"
68

79
cat >"${plist_path}" <<EOF
810
<?xml version="1.0" encoding="UTF-8"?>
@@ -29,6 +31,18 @@ cat >"${plist_path}" <<EOF
2931
<string>13.0</string>
3032
<key>LSUIElement</key>
3133
<true/>
34+
<key>SUAllowsAutomaticUpdates</key>
35+
<false/>
36+
<key>SUAutomaticallyUpdate</key>
37+
<false/>
38+
<key>SUEnableAutomaticChecks</key>
39+
<true/>
40+
<key>SUFeedURL</key>
41+
<string>${sparkle_feed_url}</string>
42+
<key>SUPublicEDKey</key>
43+
<string>${sparkle_public_ed_key}</string>
44+
<key>SUScheduledCheckInterval</key>
45+
<integer>86400</integer>
3246
</dict>
3347
</plist>
3448
EOF

0 commit comments

Comments
 (0)