mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 02:26:37 +00:00
feat: Sparkle auto-updates + Developer ID notarization pipeline
Adds Sparkle 2 auto-updates and a local release script that produces signed, notarized, stapled builds for GitHub distribution. App Store submission was rejected because Scarf spawns the user-installed hermes binary and reads ~/.hermes/ directly — both forbidden by App Sandbox — so we commit to the GitHub-release path properly. - Sparkle SPM dep wired into the app target (link-only; hardened-runtime entitlement disable-library-validation lets Sparkle load at runtime). - Tracked Info.plist with SUFeedURL, SUPublicEDKey, and daily check interval; replaces the auto-generated plist so Sparkle keys live in version control rather than pbxproj INFOPLIST_KEY_* noise. - UpdaterService wraps SPUStandardUpdaterController and is injected via .environment(). Menu bar, standard app menu (CommandGroup after .appInfo), and a new Updates section in Settings → General each call updater.checkForUpdates(). - scripts/release.sh runs the full pipeline: version bump → universal archive → Developer ID export → notarytool submit (keychain profile scarf-notary) → staple → appcast EdDSA sign → gh-pages push → gh release → tag. scripts/ExportOptions.plist pins manual Developer ID signing for team 3Q6X2L86C4. - README: removes the right-click-Open workaround (notarized builds don't need it), notes Sparkle, adds a Releases section describing the pipeline and signing prerequisites. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# Xcode
|
# Xcode
|
||||||
build/
|
build/
|
||||||
|
.gh-pages-worktree/
|
||||||
DerivedData/
|
DerivedData/
|
||||||
*.pbxuser
|
*.pbxuser
|
||||||
!default.pbxuser
|
!default.pbxuser
|
||||||
|
|||||||
@@ -101,10 +101,11 @@ If a Hermes update changes the database schema or CLI output format, Scarf may n
|
|||||||
Download the latest build from [Releases](https://github.com/awizemann/scarf/releases):
|
Download the latest build from [Releases](https://github.com/awizemann/scarf/releases):
|
||||||
|
|
||||||
- `Scarf-vX.X.X-Universal.zip` — Apple Silicon + Intel (recommended)
|
- `Scarf-vX.X.X-Universal.zip` — Apple Silicon + Intel (recommended)
|
||||||
- `Scarf-vX.X.X-ARM64.zip` — Apple Silicon only (smaller)
|
|
||||||
|
|
||||||
1. Unzip and drag **Scarf.app** to Applications
|
1. Unzip and drag **Scarf.app** to Applications
|
||||||
2. On first launch, right-click and choose **Open** (or go to System Settings → Privacy & Security → Open Anyway)
|
2. Launch normally — builds are Developer ID signed and notarized, so Gatekeeper accepts them on first launch
|
||||||
|
|
||||||
|
Scarf checks for updates automatically on launch via [Sparkle](https://sparkle-project.org) and daily thereafter. You can disable automatic checks or trigger a manual check from **Settings → General → Updates** or the menu bar icon.
|
||||||
|
|
||||||
### Build from Source
|
### Build from Source
|
||||||
|
|
||||||
@@ -178,6 +179,7 @@ The app opens `state.db` in read-only mode to avoid WAL contention with Hermes.
|
|||||||
| Package | Purpose |
|
| Package | Purpose |
|
||||||
|---------|---------|
|
|---------|---------|
|
||||||
| [SwiftTerm](https://github.com/migueldeicaza/SwiftTerm) | Terminal emulator for the Chat feature |
|
| [SwiftTerm](https://github.com/migueldeicaza/SwiftTerm) | Terminal emulator for the Chat feature |
|
||||||
|
| [Sparkle](https://github.com/sparkle-project/Sparkle) | Auto-updates from the GitHub-hosted appcast |
|
||||||
|
|
||||||
Everything else uses system frameworks: SQLite3 C API, Foundation JSON, AttributedString markdown, SwiftUI Charts, GCD file watching.
|
Everything else uses system frameworks: SQLite3 C API, Foundation JSON, AttributedString markdown, SwiftUI Charts, GCD file watching.
|
||||||
|
|
||||||
@@ -327,6 +329,20 @@ Your agent can update the dashboard as part of cron jobs, after builds, or whene
|
|||||||
|
|
||||||
Each section defines a grid with 1–4 columns. Widgets flow left-to-right, wrapping to new rows. See [DASHBOARD_SCHEMA.md](scarf/docs/DASHBOARD_SCHEMA.md) for the full schema reference with examples of every widget type.
|
Each section defines a grid with 1–4 columns. Widgets flow left-to-right, wrapping to new rows. See [DASHBOARD_SCHEMA.md](scarf/docs/DASHBOARD_SCHEMA.md) for the full schema reference with examples of every widget type.
|
||||||
|
|
||||||
|
## Releases
|
||||||
|
|
||||||
|
Scarf ships through GitHub releases — the App Store is not supported because Scarf spawns the user-installed `hermes` binary and reads `~/.hermes/` directly, both of which App Sandbox forbids.
|
||||||
|
|
||||||
|
Each release goes through a single local script: [scripts/release.sh](scripts/release.sh). The script archives a universal binary, signs it with the Developer ID Application cert, submits to `notarytool`, staples the ticket, produces the distribution zip, signs an appcast entry with Sparkle's EdDSA key, pushes an updated `appcast.xml` to the `gh-pages` branch, creates the GitHub release, and tags `main`.
|
||||||
|
|
||||||
|
The Sparkle appcast is served from [awizemann.github.io/scarf/appcast.xml](https://awizemann.github.io/scarf/appcast.xml).
|
||||||
|
|
||||||
|
Signing prerequisites (one-time):
|
||||||
|
|
||||||
|
- `Developer ID Application` certificate in the login Keychain
|
||||||
|
- `scarf-notary` keychain profile registered via `xcrun notarytool store-credentials`
|
||||||
|
- Sparkle EdDSA private key in Keychain item `https://sparkle-project.org` (back this up — without it, shipped apps can never receive updates)
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome. Please open an issue to discuss what you'd like to change before submitting a PR.
|
Contributions are welcome. Please open an issue to discuss what you'd like to change before submitting a PR.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
53495AB62F7B992C00BD31AD /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 53SWIFTTERM0001 /* SwiftTerm */; };
|
53495AB62F7B992C00BD31AD /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 53SWIFTTERM0001 /* SwiftTerm */; };
|
||||||
|
53SPARKLE00010 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 53SPARKLE00011 /* Sparkle */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -33,9 +34,22 @@
|
|||||||
534959592F7B83B700BD31AD /* scarfUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = scarfUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
534959592F7B83B700BD31AD /* scarfUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = scarfUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
534959AA2F7B83B600BD31AD /* Exceptions for "scarf" folder in "scarf" target */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
|
membershipExceptions = (
|
||||||
|
Info.plist,
|
||||||
|
);
|
||||||
|
target = 5349593F2F7B83B600BD31AD /* scarf */;
|
||||||
|
};
|
||||||
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
534959422F7B83B600BD31AD /* scarf */ = {
|
534959422F7B83B600BD31AD /* scarf */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
534959AA2F7B83B600BD31AD /* Exceptions for "scarf" folder in "scarf" target */,
|
||||||
|
);
|
||||||
path = scarf;
|
path = scarf;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -57,6 +71,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
53495AB62F7B992C00BD31AD /* SwiftTerm in Frameworks */,
|
53495AB62F7B992C00BD31AD /* SwiftTerm in Frameworks */,
|
||||||
|
53SPARKLE00010 /* Sparkle in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -118,6 +133,7 @@
|
|||||||
name = scarf;
|
name = scarf;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
53SWIFTTERM0001 /* SwiftTerm */,
|
53SWIFTTERM0001 /* SwiftTerm */,
|
||||||
|
53SPARKLE00011 /* Sparkle */,
|
||||||
);
|
);
|
||||||
productName = scarf;
|
productName = scarf;
|
||||||
productReference = 534959402F7B83B600BD31AD /* scarf.app */;
|
productReference = 534959402F7B83B600BD31AD /* scarf.app */;
|
||||||
@@ -203,6 +219,7 @@
|
|||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
53SWIFTTERM0002 /* XCRemoteSwiftPackageReference "SwiftTerm" */,
|
53SWIFTTERM0002 /* XCRemoteSwiftPackageReference "SwiftTerm" */,
|
||||||
|
53SPARKLE00012 /* XCRemoteSwiftPackageReference "Sparkle" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 534959412F7B83B600BD31AD /* Products */;
|
productRefGroup = 534959412F7B83B600BD31AD /* Products */;
|
||||||
@@ -407,23 +424,20 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
|
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 11;
|
CURRENT_PROJECT_VERSION = 16;
|
||||||
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Scarf;
|
INFOPLIST_FILE = scarf/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
|
||||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Scarf uses the microphone for Hermes voice chat.";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||||
MARKETING_VERSION = 1.6.0;
|
MARKETING_VERSION = 1.6.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarf;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
@@ -444,23 +458,20 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
|
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 11;
|
CURRENT_PROJECT_VERSION = 16;
|
||||||
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Scarf;
|
INFOPLIST_FILE = scarf/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
|
||||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
|
||||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Scarf uses the microphone for Hermes voice chat.";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||||
MARKETING_VERSION = 1.6.0;
|
MARKETING_VERSION = 1.6.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarf;
|
PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
@@ -594,6 +605,14 @@
|
|||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
53SPARKLE00012 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/sparkle-project/Sparkle";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.6.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
53SWIFTTERM0002 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = {
|
53SWIFTTERM0002 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/migueldeicaza/SwiftTerm.git";
|
repositoryURL = "https://github.com/migueldeicaza/SwiftTerm.git";
|
||||||
@@ -605,6 +624,11 @@
|
|||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
53SPARKLE00011 /* Sparkle */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 53SPARKLE00012 /* XCRemoteSwiftPackageReference "Sparkle" */;
|
||||||
|
productName = Sparkle;
|
||||||
|
};
|
||||||
53SWIFTTERM0001 /* SwiftTerm */ = {
|
53SWIFTTERM0001 /* SwiftTerm */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 53SWIFTTERM0002 /* XCRemoteSwiftPackageReference "SwiftTerm" */;
|
package = 53SWIFTTERM0002 /* XCRemoteSwiftPackageReference "SwiftTerm" */;
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import Foundation
|
||||||
|
import Sparkle
|
||||||
|
|
||||||
|
/// Thin wrapper around Sparkle's `SPUStandardUpdaterController`.
|
||||||
|
///
|
||||||
|
/// Sparkle reads `SUFeedURL`, `SUPublicEDKey`, and check-interval defaults from Info.plist.
|
||||||
|
/// This service exposes the bits the UI needs: a "check now" trigger, a toggle for automatic
|
||||||
|
/// checks, and observable state for the Settings screen.
|
||||||
|
@MainActor
|
||||||
|
@Observable
|
||||||
|
final class UpdaterService: NSObject {
|
||||||
|
private let controller: SPUStandardUpdaterController
|
||||||
|
|
||||||
|
/// User-facing toggle. Mirrors `updater.automaticallyChecksForUpdates`.
|
||||||
|
var automaticallyChecksForUpdates: Bool {
|
||||||
|
get { controller.updater.automaticallyChecksForUpdates }
|
||||||
|
set { controller.updater.automaticallyChecksForUpdates = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Last time Sparkle checked the appcast (nil before the first check).
|
||||||
|
var lastUpdateCheckDate: Date? {
|
||||||
|
controller.updater.lastUpdateCheckDate
|
||||||
|
}
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
// startingUpdater: true → Sparkle scans for updates on launch per Info.plist schedule.
|
||||||
|
// Default delegates are sufficient for a non-sandboxed app.
|
||||||
|
self.controller = SPUStandardUpdaterController(
|
||||||
|
startingUpdater: true,
|
||||||
|
updaterDelegate: nil,
|
||||||
|
userDriverDelegate: nil
|
||||||
|
)
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Triggers a user-initiated update check. Sparkle handles the UI (alert, progress, install).
|
||||||
|
func checkForUpdates() {
|
||||||
|
controller.checkForUpdates(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// Updates section for the General tab. Wraps the Sparkle-backed `UpdaterService`
|
||||||
|
/// in the same row idioms used elsewhere in Settings (per CLAUDE.md guidance —
|
||||||
|
/// extract sections so individual tab bodies stay small).
|
||||||
|
struct UpdatesSection: View {
|
||||||
|
@Environment(UpdaterService.self) private var updater
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
SettingsSection(title: "Updates", icon: "arrow.down.circle") {
|
||||||
|
ReadOnlyRow(label: "Current Version", value: versionString)
|
||||||
|
ToggleRow(
|
||||||
|
label: "Check Automatically",
|
||||||
|
isOn: updater.automaticallyChecksForUpdates
|
||||||
|
) { newValue in
|
||||||
|
updater.automaticallyChecksForUpdates = newValue
|
||||||
|
}
|
||||||
|
ReadOnlyRow(label: "Last Checked", value: lastCheckedString)
|
||||||
|
checkNowRow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var versionString: String {
|
||||||
|
let info = Bundle.main.infoDictionary
|
||||||
|
let short = info?["CFBundleShortVersionString"] as? String ?? "?"
|
||||||
|
let build = info?["CFBundleVersion"] as? String ?? "?"
|
||||||
|
return "\(short) (\(build))"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lastCheckedString: String {
|
||||||
|
guard let date = updater.lastUpdateCheckDate else { return "Never" }
|
||||||
|
let formatter = RelativeDateTimeFormatter()
|
||||||
|
formatter.unitsStyle = .full
|
||||||
|
return formatter.localizedString(for: date, relativeTo: Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
private var checkNowRow: some View {
|
||||||
|
HStack {
|
||||||
|
Text("Check Now")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(width: 160, alignment: .trailing)
|
||||||
|
Button("Check for Updates…") { updater.checkForUpdates() }
|
||||||
|
.controlSize(.small)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.background(.quaternary.opacity(0.3))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,8 @@ struct GeneralTab: View {
|
|||||||
SettingsSection(title: "Locale", icon: "globe.americas") {
|
SettingsSection(title: "Locale", icon: "globe.americas") {
|
||||||
EditableTextField(label: "Timezone (IANA)", value: viewModel.config.timezone) { viewModel.setTimezone($0) }
|
EditableTextField(label: "Timezone (IANA)", value: viewModel.config.timezone) { viewModel.setTimezone($0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdatesSection()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Breadcrumb-style row that points users to the Credential Pools sidebar
|
/// Breadcrumb-style row that points users to the Credential Pools sidebar
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Scarf</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.utilities</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string></string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Scarf uses the microphone for Hermes voice chat.</string>
|
||||||
|
<key>SUFeedURL</key>
|
||||||
|
<string>https://awizemann.github.io/scarf/appcast.xml</string>
|
||||||
|
<key>SUPublicEDKey</key>
|
||||||
|
<string>sxHR0OGLmx9I4Fyx1GdPANR9WUiVAz/rI38x3cLYnMU=</string>
|
||||||
|
<key>SUEnableAutomaticChecks</key>
|
||||||
|
<true/>
|
||||||
|
<key>SUScheduledCheckInterval</key>
|
||||||
|
<integer>86400</integer>
|
||||||
|
<key>SUEnableInstallerLauncherService</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -4,5 +4,7 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.device.audio-input</key>
|
<key>com.apple.security.device.audio-input</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ struct ScarfApp: App {
|
|||||||
@State private var fileWatcher = HermesFileWatcher()
|
@State private var fileWatcher = HermesFileWatcher()
|
||||||
@State private var menuBarStatus = MenuBarStatus()
|
@State private var menuBarStatus = MenuBarStatus()
|
||||||
@State private var chatViewModel = ChatViewModel()
|
@State private var chatViewModel = ChatViewModel()
|
||||||
|
@State private var updater = UpdaterService()
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
@@ -13,6 +14,7 @@ struct ScarfApp: App {
|
|||||||
.environment(coordinator)
|
.environment(coordinator)
|
||||||
.environment(fileWatcher)
|
.environment(fileWatcher)
|
||||||
.environment(chatViewModel)
|
.environment(chatViewModel)
|
||||||
|
.environment(updater)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
fileWatcher.startWatching()
|
fileWatcher.startWatching()
|
||||||
menuBarStatus.startPolling()
|
menuBarStatus.startPolling()
|
||||||
@@ -23,9 +25,14 @@ struct ScarfApp: App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.defaultSize(width: 1100, height: 700)
|
.defaultSize(width: 1100, height: 700)
|
||||||
|
.commands {
|
||||||
|
CommandGroup(after: .appInfo) {
|
||||||
|
Button("Check for Updates…") { updater.checkForUpdates() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MenuBarExtra("Scarf", systemImage: menuBarStatus.icon) {
|
MenuBarExtra("Scarf", systemImage: menuBarStatus.icon) {
|
||||||
MenuBarMenu(status: menuBarStatus, coordinator: coordinator)
|
MenuBarMenu(status: menuBarStatus, coordinator: coordinator, updater: updater)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,6 +97,7 @@ final class MenuBarStatus {
|
|||||||
struct MenuBarMenu: View {
|
struct MenuBarMenu: View {
|
||||||
let status: MenuBarStatus
|
let status: MenuBarStatus
|
||||||
let coordinator: AppCoordinator
|
let coordinator: AppCoordinator
|
||||||
|
let updater: UpdaterService
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
@@ -116,6 +124,8 @@ struct MenuBarMenu: View {
|
|||||||
NSApplication.shared.activate()
|
NSApplication.shared.activate()
|
||||||
}
|
}
|
||||||
Divider()
|
Divider()
|
||||||
|
Button("Check for Updates…") { updater.checkForUpdates() }
|
||||||
|
Divider()
|
||||||
Button("Quit Scarf") {
|
Button("Quit Scarf") {
|
||||||
NSApplication.shared.terminate(nil)
|
NSApplication.shared.terminate(nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>method</key>
|
||||||
|
<string>developer-id</string>
|
||||||
|
<key>teamID</key>
|
||||||
|
<string>3Q6X2L86C4</string>
|
||||||
|
<key>signingStyle</key>
|
||||||
|
<string>manual</string>
|
||||||
|
<key>signingCertificate</key>
|
||||||
|
<string>Developer ID Application</string>
|
||||||
|
<key>destination</key>
|
||||||
|
<string>export</string>
|
||||||
|
<key>stripSwiftSymbols</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Executable
+204
@@ -0,0 +1,204 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Scarf release pipeline — local, manual, repeatable.
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/release.sh 1.7.0
|
||||||
|
#
|
||||||
|
# Prerequisites (one-time setup):
|
||||||
|
# 1. Developer ID Application cert installed in login Keychain.
|
||||||
|
# security find-identity -v -p codesigning | grep "Developer ID Application"
|
||||||
|
# 2. App Store Connect API key stored for notarytool as profile "scarf-notary":
|
||||||
|
# xcrun notarytool store-credentials "scarf-notary" \
|
||||||
|
# --key ~/.private/AuthKey_XXXX.p8 --key-id <KEY_ID> --issuer <ISSUER_ID>
|
||||||
|
# 3. Sparkle EdDSA keypair generated (private key in Keychain item "https://sparkle-project.org"):
|
||||||
|
# ./scripts/sparkle/generate_keys # or similar, from Sparkle SPM artifacts
|
||||||
|
# 4. gh-pages branch exists with an appcast.xml and GitHub Pages enabled.
|
||||||
|
# 5. gh CLI authed: `gh auth status`.
|
||||||
|
# 6. GH_PAGES_WORKTREE env var pointing at a gh-pages checkout, OR let the
|
||||||
|
# script create one automatically at .gh-pages-worktree/ via `git worktree add`.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---------- config ----------
|
||||||
|
VERSION="${1:?usage: ./scripts/release.sh <marketing-version> e.g. 1.7.0}"
|
||||||
|
TEAM_ID="3Q6X2L86C4"
|
||||||
|
BUNDLE_ID="com.scarf.app"
|
||||||
|
SCHEME="scarf"
|
||||||
|
PROJECT="scarf/scarf.xcodeproj"
|
||||||
|
NOTARY_PROFILE="scarf-notary"
|
||||||
|
SIGNING_IDENTITY="Developer ID Application"
|
||||||
|
APPCAST_URL="https://awizemann.github.io/scarf/appcast.xml"
|
||||||
|
DOWNLOAD_URL_BASE="https://github.com/awizemann/scarf/releases/download"
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
BUILD_DIR="$REPO_ROOT/build"
|
||||||
|
ARCHIVE_PATH="$BUILD_DIR/scarf.xcarchive"
|
||||||
|
EXPORT_DIR="$BUILD_DIR/export"
|
||||||
|
EXPORT_OPTIONS="$REPO_ROOT/scripts/ExportOptions.plist"
|
||||||
|
RELEASE_DIR="$REPO_ROOT/releases/v${VERSION}"
|
||||||
|
GH_PAGES_WORKTREE="${GH_PAGES_WORKTREE:-$REPO_ROOT/.gh-pages-worktree}"
|
||||||
|
|
||||||
|
# ---------- helpers ----------
|
||||||
|
log() { printf '\033[1;34m==> %s\033[0m\n' "$*"; }
|
||||||
|
warn() { printf '\033[1;33m[WARN] %s\033[0m\n' "$*" >&2; }
|
||||||
|
die() { printf '\033[1;31m[ERR] %s\033[0m\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
require_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing required tool: $1"; }
|
||||||
|
|
||||||
|
# ---------- preflight ----------
|
||||||
|
log "Preflight checks"
|
||||||
|
require_cmd xcodebuild
|
||||||
|
require_cmd xcrun
|
||||||
|
require_cmd ditto
|
||||||
|
require_cmd gh
|
||||||
|
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
# git must be clean and on main
|
||||||
|
if [[ -n "$(git status --porcelain)" ]]; then
|
||||||
|
die "working tree not clean — commit or stash first"
|
||||||
|
fi
|
||||||
|
CUR_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||||
|
[[ "$CUR_BRANCH" == "main" ]] || die "not on main (on $CUR_BRANCH)"
|
||||||
|
|
||||||
|
# identity present
|
||||||
|
security find-identity -v -p codesigning | grep -q "$SIGNING_IDENTITY" \
|
||||||
|
|| die "'$SIGNING_IDENTITY' certificate not in Keychain — create at developer.apple.com"
|
||||||
|
|
||||||
|
# notary profile present (can't list, only test by dry-running submit help)
|
||||||
|
xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" --output-format json >/dev/null 2>&1 \
|
||||||
|
|| die "notarytool profile '$NOTARY_PROFILE' not set up — see script header"
|
||||||
|
|
||||||
|
# locate sign_update (ships with Sparkle SPM artifacts)
|
||||||
|
SIGN_UPDATE="$(find ~/Library/Developer/Xcode/DerivedData -name sign_update -type f -perm +111 2>/dev/null | head -n1 || true)"
|
||||||
|
[[ -x "${SIGN_UPDATE:-}" ]] || die "sign_update not found — build the project once in Xcode so Sparkle artifacts resolve, then re-run"
|
||||||
|
|
||||||
|
# ---------- bump version ----------
|
||||||
|
log "Bumping version to $VERSION"
|
||||||
|
PBXPROJ="$PROJECT/project.pbxproj"
|
||||||
|
# CURRENT_PROJECT_VERSION (build number) bumps by 1 from existing
|
||||||
|
CUR_BUILD="$(awk -F'= ' '/CURRENT_PROJECT_VERSION/ {gsub(/[; ]/,"",$2); print $2; exit}' "$PBXPROJ")"
|
||||||
|
NEW_BUILD=$((CUR_BUILD + 1))
|
||||||
|
sed -i '' -E "s/MARKETING_VERSION = [0-9]+\.[0-9]+\.[0-9]+;/MARKETING_VERSION = ${VERSION};/g" "$PBXPROJ"
|
||||||
|
sed -i '' -E "s/CURRENT_PROJECT_VERSION = [0-9]+;/CURRENT_PROJECT_VERSION = ${NEW_BUILD};/g" "$PBXPROJ"
|
||||||
|
git add "$PBXPROJ"
|
||||||
|
git commit -m "chore: Bump version to ${VERSION}"
|
||||||
|
|
||||||
|
# ---------- build ----------
|
||||||
|
log "Clean build directory"
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
log "Archive (universal arm64+x86_64)"
|
||||||
|
xcodebuild \
|
||||||
|
-project "$PROJECT" \
|
||||||
|
-scheme "$SCHEME" \
|
||||||
|
-configuration Release \
|
||||||
|
-archivePath "$ARCHIVE_PATH" \
|
||||||
|
-destination "generic/platform=macOS" \
|
||||||
|
ONLY_ACTIVE_ARCH=NO \
|
||||||
|
ARCHS="arm64 x86_64" \
|
||||||
|
archive
|
||||||
|
|
||||||
|
log "Export signed .app"
|
||||||
|
xcodebuild \
|
||||||
|
-exportArchive \
|
||||||
|
-archivePath "$ARCHIVE_PATH" \
|
||||||
|
-exportPath "$EXPORT_DIR" \
|
||||||
|
-exportOptionsPlist "$EXPORT_OPTIONS"
|
||||||
|
|
||||||
|
APP_PATH="$EXPORT_DIR/Scarf.app"
|
||||||
|
[[ -d "$APP_PATH" ]] || die "exported app not found at $APP_PATH (expected Scarf.app — confirm PRODUCT_NAME)"
|
||||||
|
|
||||||
|
# ---------- verify signature ----------
|
||||||
|
log "Verify signature"
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
|
||||||
|
# spctl will fail here (not yet notarized) — that's fine, we check after stapling
|
||||||
|
spctl --assess --type execute --verbose "$APP_PATH" || true
|
||||||
|
|
||||||
|
# ---------- notarize ----------
|
||||||
|
log "Zip for notarization"
|
||||||
|
NOTARIZE_ZIP="$BUILD_DIR/Scarf-notarize.zip"
|
||||||
|
ditto -c -k --keepParent "$APP_PATH" "$NOTARIZE_ZIP"
|
||||||
|
|
||||||
|
log "Submit to notarytool (blocking)"
|
||||||
|
xcrun notarytool submit "$NOTARIZE_ZIP" \
|
||||||
|
--keychain-profile "$NOTARY_PROFILE" \
|
||||||
|
--wait \
|
||||||
|
--timeout 30m
|
||||||
|
|
||||||
|
log "Staple notarization ticket"
|
||||||
|
xcrun stapler staple "$APP_PATH"
|
||||||
|
xcrun stapler validate "$APP_PATH"
|
||||||
|
|
||||||
|
log "Final gatekeeper assessment"
|
||||||
|
spctl --assess --type execute --verbose "$APP_PATH"
|
||||||
|
|
||||||
|
# ---------- package distribution artifacts ----------
|
||||||
|
log "Package distribution zips"
|
||||||
|
mkdir -p "$RELEASE_DIR"
|
||||||
|
UNIVERSAL_ZIP="$RELEASE_DIR/Scarf-v${VERSION}-Universal.zip"
|
||||||
|
ditto -c -k --keepParent "$APP_PATH" "$UNIVERSAL_ZIP"
|
||||||
|
|
||||||
|
# ---------- sign appcast entry ----------
|
||||||
|
log "Sign appcast entry with EdDSA"
|
||||||
|
# sign_update prints: sparkle:edSignature="..." length="..."
|
||||||
|
SIG_OUTPUT="$("$SIGN_UPDATE" "$UNIVERSAL_ZIP")"
|
||||||
|
ED_SIGNATURE="$(echo "$SIG_OUTPUT" | sed -nE 's/.*sparkle:edSignature="([^"]+)".*/\1/p')"
|
||||||
|
FILE_LENGTH="$(echo "$SIG_OUTPUT" | sed -nE 's/.*length="([^"]+)".*/\1/p')"
|
||||||
|
[[ -n "$ED_SIGNATURE" && -n "$FILE_LENGTH" ]] || die "sign_update did not produce signature: $SIG_OUTPUT"
|
||||||
|
|
||||||
|
# ---------- update appcast on gh-pages ----------
|
||||||
|
log "Update appcast.xml on gh-pages worktree"
|
||||||
|
if [[ ! -d "$GH_PAGES_WORKTREE" ]]; then
|
||||||
|
git worktree add "$GH_PAGES_WORKTREE" gh-pages
|
||||||
|
fi
|
||||||
|
(
|
||||||
|
cd "$GH_PAGES_WORKTREE"
|
||||||
|
git pull --ff-only origin gh-pages
|
||||||
|
PUB_DATE="$(LC_TIME=en_US.UTF-8 date -u +"%a, %d %b %Y %H:%M:%S +0000")"
|
||||||
|
DOWNLOAD_URL="$DOWNLOAD_URL_BASE/v${VERSION}/Scarf-v${VERSION}-Universal.zip"
|
||||||
|
NEW_ITEM=$(cat <<EOF
|
||||||
|
<item>
|
||||||
|
<title>Version ${VERSION}</title>
|
||||||
|
<sparkle:version>${NEW_BUILD}</sparkle:version>
|
||||||
|
<sparkle:shortVersionString>${VERSION}</sparkle:shortVersionString>
|
||||||
|
<sparkle:minimumSystemVersion>14.6</sparkle:minimumSystemVersion>
|
||||||
|
<pubDate>${PUB_DATE}</pubDate>
|
||||||
|
<enclosure url="${DOWNLOAD_URL}"
|
||||||
|
sparkle:edSignature="${ED_SIGNATURE}"
|
||||||
|
length="${FILE_LENGTH}"
|
||||||
|
type="application/octet-stream" />
|
||||||
|
</item>
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
# Insert new item after <language>en</language> line
|
||||||
|
python3 - "$NEW_ITEM" <<'PY'
|
||||||
|
import sys, pathlib
|
||||||
|
new_item = sys.argv[1]
|
||||||
|
p = pathlib.Path("appcast.xml")
|
||||||
|
xml = p.read_text()
|
||||||
|
marker = "<language>en</language>"
|
||||||
|
if marker not in xml:
|
||||||
|
sys.exit("appcast.xml missing <language>en</language> marker")
|
||||||
|
xml = xml.replace(marker, marker + "\n" + new_item, 1)
|
||||||
|
p.write_text(xml)
|
||||||
|
PY
|
||||||
|
git add appcast.xml
|
||||||
|
git commit -m "release: v${VERSION}"
|
||||||
|
git push origin gh-pages
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- github release ----------
|
||||||
|
log "Create GitHub release and upload artifacts"
|
||||||
|
gh release create "v${VERSION}" \
|
||||||
|
--title "Scarf v${VERSION}" \
|
||||||
|
--notes "Release notes: https://github.com/awizemann/scarf/releases/tag/v${VERSION}" \
|
||||||
|
"$UNIVERSAL_ZIP"
|
||||||
|
|
||||||
|
log "Tag main and push"
|
||||||
|
git tag "v${VERSION}"
|
||||||
|
git push origin main --tags
|
||||||
|
|
||||||
|
log "Release v${VERSION} complete"
|
||||||
|
log " Download: $DOWNLOAD_URL_BASE/v${VERSION}/Scarf-v${VERSION}-Universal.zip"
|
||||||
|
log " Appcast: $APPCAST_URL"
|
||||||
Reference in New Issue
Block a user