mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12610faba0 | |||
| 73b44202ba | |||
| eed55cbb0f | |||
| 14c97bee62 | |||
| 8d3fe70e2c | |||
| da88c98c7a |
@@ -47,3 +47,8 @@ scarf/standards/backups/
|
||||
|
||||
# Scarf project dashboards (user-specific)
|
||||
.scarf/
|
||||
|
||||
# Release artifacts — GitHub Releases hosts the binaries; no need to bloat git
|
||||
# history. RELEASE_NOTES.md stays tracked (committed with the version bump).
|
||||
releases/v*/*.zip
|
||||
releases/v*/appcast-entry.xml
|
||||
|
||||
@@ -39,6 +39,26 @@ scarf/scarf/ Xcode project root (PBXFileSystemSynchronizedRootGroup
|
||||
xcodebuild -project scarf/scarf.xcodeproj -scheme scarf -configuration Debug build
|
||||
```
|
||||
|
||||
## Releases
|
||||
|
||||
Shipped via a single local script. **Never run manual `xcodebuild archive` / `notarytool` / `gh release create` steps — use the script so nothing is skipped or misordered.**
|
||||
|
||||
```bash
|
||||
./scripts/release.sh <version> # full release: notarize → appcast → gh-pages → tag
|
||||
./scripts/release.sh <version> --draft # draft: everything builds + notarizes, but appcast/tag are skipped
|
||||
```
|
||||
|
||||
The script bumps version, archives Universal (arm64 + x86_64) + ARM64-only variants, signs with Developer ID, notarizes via `xcrun notarytool` (keychain profile `scarf-notary`), staples, EdDSA-signs the appcast entry with Sparkle's key, pushes the appcast to `gh-pages`, and creates a GitHub release with both zips attached. Draft mode stops after the release is uploaded so the current version stays "latest" until explicitly promoted.
|
||||
|
||||
**Release notes convention:** write them to `releases/v<version>/RELEASE_NOTES.md` BEFORE running the script — it's auto-included in the version-bump commit and used as the GitHub release body. If absent, a placeholder is used.
|
||||
|
||||
**Canonical prompts (any of these trigger the flow):**
|
||||
- "Release v1.6.2" — full release
|
||||
- "Release v1.6.2 as draft" — draft mode
|
||||
- "Prepare v1.6.2 release notes from recent commits, then release" — generate notes first, then run
|
||||
|
||||
**Prerequisites (one-time, already set up on Alan's machine):** Developer ID Application cert in login Keychain (team `3Q6X2L86C4`), notarytool keychain profile `scarf-notary`, Sparkle EdDSA private key in Keychain item `https://sparkle-project.org`, `gh-pages` branch + GitHub Pages enabled. See the header of [scripts/release.sh](scripts/release.sh) and the Releases section in [README.md](README.md) for details.
|
||||
|
||||
## Hermes Version
|
||||
|
||||
Targets Hermes v0.9.0 (v2026.4.13). Log lines may carry an optional `[session_id]` tag between the level and logger name — `HermesLogService.parseLine` treats the session tag as an optional capture group, so older untagged lines still parse.
|
||||
|
||||
@@ -101,6 +101,7 @@ 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):
|
||||
|
||||
- `Scarf-vX.X.X-Universal.zip` — Apple Silicon + Intel (recommended)
|
||||
- `Scarf-vX.X.X-ARM64.zip` — Apple Silicon only (smaller download)
|
||||
|
||||
1. Unzip and drag **Scarf.app** to Applications
|
||||
2. Launch normally — builds are Developer ID signed and notarized, so Gatekeeper accepts them on first launch
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
## What's New in 1.6.2
|
||||
|
||||
### Fixes
|
||||
|
||||
- **No more bogus "missing credentials" banner on Chat.** The orange "No AI provider credentials detected" warning was firing on the Chat tab whenever no session was selected, even for users whose credentials were configured and working. Root cause: the preflight check only inspected `~/.hermes/.env` and shell environment variables, missing the Credential Pools file at `~/.hermes/auth.json` (the in-app flow introduced in 1.6.0) and `api_key:` fields in `config.yaml`. The check now covers all four locations Hermes itself reads at runtime, so if you've added credentials via **Configure → Credential Pools**, the warning stays hidden.
|
||||
|
||||
### Polish
|
||||
|
||||
- Banner subtitle updated to point users at the in-app Credential Pools flow first, rather than prescribing `.env` edits.
|
||||
|
||||
---
|
||||
|
||||
**Upgrading from 1.6.1:** Sparkle will offer the update automatically. You can also trigger a check via **Scarf → Check for Updates…** or the menu bar icon.
|
||||
@@ -424,7 +424,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -436,7 +436,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||
MARKETING_VERSION = 1.6.1;
|
||||
MARKETING_VERSION = 1.6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
@@ -458,7 +458,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -470,7 +470,7 @@
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 14.6;
|
||||
MARKETING_VERSION = 1.6.1;
|
||||
MARKETING_VERSION = 1.6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
REGISTER_APP_GROUPS = YES;
|
||||
@@ -488,11 +488,11 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||
MARKETING_VERSION = 1.6.1;
|
||||
MARKETING_VERSION = 1.6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarfTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
@@ -509,11 +509,11 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||
MARKETING_VERSION = 1.6.1;
|
||||
MARKETING_VERSION = 1.6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarfTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
@@ -529,10 +529,10 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.6.1;
|
||||
MARKETING_VERSION = 1.6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarfUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
@@ -548,10 +548,10 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 17;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
DEVELOPMENT_TEAM = 3Q6X2L86C4;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.6.1;
|
||||
MARKETING_VERSION = 1.6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.scarfUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
|
||||
@@ -1356,10 +1356,16 @@ struct HermesFileService: Sendable {
|
||||
return env
|
||||
}
|
||||
|
||||
/// True if any known AI-provider credential is reachable — either already
|
||||
/// in the current process env, present in the login-shell env we queried,
|
||||
/// or present in `~/.hermes/.env`. Used by Chat to warn the user before
|
||||
/// `hermes acp` fails on send with "No Anthropic credentials found".
|
||||
/// True if any known AI-provider credential is reachable. Hermes itself
|
||||
/// resolves credentials from four locations at runtime, so the preflight
|
||||
/// mirrors that set to avoid false "no credentials" warnings:
|
||||
/// 1. Current process env + login-shell env (queried once at startup)
|
||||
/// 2. `~/.hermes/.env`
|
||||
/// 3. `~/.hermes/auth.json` — Credential Pools (v1.6+ blessed flow)
|
||||
/// 4. `~/.hermes/config.yaml` — embedded `api_key:` for auxiliary /
|
||||
/// delegation tasks
|
||||
/// Used by Chat to warn the user before `hermes acp` fails on send with
|
||||
/// "No Anthropic credentials found".
|
||||
nonisolated static func hasAnyAICredential() -> Bool {
|
||||
let credentialKeys = shellEnvKeys.filter { $0 != "PATH" && $0 != "ANTHROPIC_BASE_URL" && $0 != "OPENAI_BASE_URL" }
|
||||
let env = enrichedEnvironment()
|
||||
@@ -1386,6 +1392,36 @@ struct HermesFileService: Sendable {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Scan ~/.hermes/auth.json — the Credential Pools file written by the
|
||||
// Configure → Credential Pools UI. Schema is
|
||||
// { "credential_pool": { "<provider>": [ { "access_token": "...", ... }, ... ] } }
|
||||
// Defensive parse: any malformed input falls through to the next check.
|
||||
let authPath = HermesPaths.home + "/auth.json"
|
||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: authPath)),
|
||||
let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let pool = root["credential_pool"] as? [String: Any] {
|
||||
for (_, entries) in pool {
|
||||
guard let list = entries as? [[String: Any]] else { continue }
|
||||
for cred in list {
|
||||
if let token = cred["access_token"] as? String, !token.isEmpty {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Scan ~/.hermes/config.yaml for `api_key:` lines with a non-empty
|
||||
// value. Covers both `auxiliary.<task>.api_key` and `delegation.api_key`
|
||||
// without needing to parse the YAML structure — any leaf `api_key: ...`
|
||||
// with a value means Hermes has a credential to fall back on.
|
||||
if let text = try? String(contentsOfFile: HermesPaths.configYAML, encoding: .utf8) {
|
||||
for line in text.split(separator: "\n") {
|
||||
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
||||
guard trimmed.hasPrefix("api_key:") else { continue }
|
||||
let value = trimmed.dropFirst("api_key:".count)
|
||||
.trimmingCharacters(in: CharacterSet(charactersIn: "\"' "))
|
||||
if !value.isEmpty { return true }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ struct ChatView: View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("No AI provider credentials detected")
|
||||
.font(.callout)
|
||||
Text("Add `ANTHROPIC_API_KEY` (or similar) to `~/.hermes/.env` or your shell profile, then restart Scarf.")
|
||||
Text("Add credentials in **Configure → Credential Pools**, set `ANTHROPIC_API_KEY` (or similar) in `~/.hermes/.env`, or export it in your shell profile, then restart Scarf.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
+67
-44
@@ -54,8 +54,6 @@ 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}"
|
||||
@@ -76,8 +74,14 @@ require_cmd gh
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# git must be clean and on main
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
# git must be clean and on main. The one exception: the release dir
|
||||
# (releases/v<VERSION>/) may already exist and be untracked — the user may
|
||||
# have written RELEASE_NOTES.md there ahead of time, and the rest of the dir
|
||||
# is auto-populated + gitignored anyway. Git status abbreviates to the dir
|
||||
# path when all contents are untracked, so the whitelist matches both forms.
|
||||
ALLOW="^\?\? releases/v${VERSION}/"
|
||||
DIRTY="$(git status --porcelain | grep -Ev "$ALLOW" || true)"
|
||||
if [[ -n "$DIRTY" ]]; then
|
||||
die "working tree not clean — commit or stash first"
|
||||
fi
|
||||
CUR_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
@@ -111,68 +115,86 @@ if [[ -f "$NOTES_FILE" ]]; then
|
||||
fi
|
||||
git commit -m "chore: Bump version to ${VERSION}"
|
||||
|
||||
# ---------- build ----------
|
||||
# ---------- build variants ----------
|
||||
# Each release produces two zips: a Universal binary (recommended — works on
|
||||
# both Apple Silicon and Intel) and an ARM64-only variant (smaller download for
|
||||
# users who know they're on M-series silicon). Each variant is independently
|
||||
# notarized and stapled. The appcast only references the Universal zip since
|
||||
# it works everywhere; ARM64 is an alternative manual download.
|
||||
|
||||
log "Clean build directory"
|
||||
rm -rf "$BUILD_DIR"
|
||||
mkdir -p "$BUILD_DIR"
|
||||
mkdir -p "$BUILD_DIR" "$RELEASE_DIR"
|
||||
|
||||
log "Archive (universal arm64+x86_64)"
|
||||
xcodebuild \
|
||||
# build_variant <label> <archs> <output_zip>
|
||||
# label e.g. "Universal" or "ARM64" (used as subdir name + log prefix)
|
||||
# archs e.g. "arm64 x86_64" or "arm64" (space-separated ARCHS value)
|
||||
# output_zip absolute path where the stapled, distribution-ready zip is written
|
||||
build_variant() {
|
||||
local label="$1"
|
||||
local archs="$2"
|
||||
local out_zip="$3"
|
||||
local variant_dir="$BUILD_DIR/$label"
|
||||
local archive_path="$variant_dir/scarf.xcarchive"
|
||||
local export_dir="$variant_dir/export"
|
||||
local app_path="$export_dir/Scarf.app"
|
||||
local notarize_zip="$variant_dir/Scarf-notarize.zip"
|
||||
|
||||
mkdir -p "$variant_dir"
|
||||
|
||||
log "[$label] Archive (archs: $archs)"
|
||||
xcodebuild \
|
||||
-project "$PROJECT" \
|
||||
-scheme "$SCHEME" \
|
||||
-configuration Release \
|
||||
-archivePath "$ARCHIVE_PATH" \
|
||||
-archivePath "$archive_path" \
|
||||
-destination "generic/platform=macOS" \
|
||||
ONLY_ACTIVE_ARCH=NO \
|
||||
ARCHS="arm64 x86_64" \
|
||||
ARCHS="$archs" \
|
||||
archive
|
||||
|
||||
log "Export signed .app"
|
||||
xcodebuild \
|
||||
log "[$label] Export signed .app"
|
||||
xcodebuild \
|
||||
-exportArchive \
|
||||
-archivePath "$ARCHIVE_PATH" \
|
||||
-exportPath "$EXPORT_DIR" \
|
||||
-archivePath "$archive_path" \
|
||||
-exportPath "$export_dir" \
|
||||
-exportOptionsPlist "$EXPORT_OPTIONS"
|
||||
|
||||
# Xcode exports as scarf.app (PRODUCT_NAME = $TARGET_NAME = "scarf"). Rename the
|
||||
# wrapper to Scarf.app so users see properly-cased app in /Applications. Renaming
|
||||
# the bundle directory does NOT invalidate the signature (codesign signs contents,
|
||||
# not the wrapper folder name).
|
||||
if [[ -d "$EXPORT_DIR/scarf.app" && ! -d "$EXPORT_DIR/Scarf.app" ]]; then
|
||||
mv "$EXPORT_DIR/scarf.app" "$EXPORT_DIR/Scarf.app"
|
||||
fi
|
||||
APP_PATH="$EXPORT_DIR/Scarf.app"
|
||||
[[ -d "$APP_PATH" ]] || die "exported app not found at $APP_PATH"
|
||||
# Xcode exports as scarf.app (PRODUCT_NAME = $TARGET_NAME = "scarf"). Rename so
|
||||
# users see properly-cased Scarf.app in /Applications. Renaming the bundle
|
||||
# wrapper does NOT invalidate the signature — codesign signs contents, not the
|
||||
# wrapper folder name.
|
||||
if [[ -d "$export_dir/scarf.app" && ! -d "$app_path" ]]; then
|
||||
mv "$export_dir/scarf.app" "$app_path"
|
||||
fi
|
||||
[[ -d "$app_path" ]] || die "[$label] exported app not found at $app_path"
|
||||
|
||||
# ---------- 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
|
||||
log "[$label] Verify signature"
|
||||
codesign --verify --deep --strict --verbose=2 "$app_path"
|
||||
|
||||
# ---------- notarize ----------
|
||||
log "Zip for notarization"
|
||||
NOTARIZE_ZIP="$BUILD_DIR/Scarf-notarize.zip"
|
||||
ditto -c -k --keepParent "$APP_PATH" "$NOTARIZE_ZIP"
|
||||
log "[$label] Zip for notarization"
|
||||
ditto -c -k --keepParent "$app_path" "$notarize_zip"
|
||||
|
||||
log "Submit to notarytool (blocking)"
|
||||
xcrun notarytool submit "$NOTARIZE_ZIP" \
|
||||
log "[$label] 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 "[$label] Staple + validate"
|
||||
xcrun stapler staple "$app_path"
|
||||
xcrun stapler validate "$app_path"
|
||||
spctl --assess --type execute --verbose "$app_path"
|
||||
|
||||
log "Final gatekeeper assessment"
|
||||
spctl --assess --type execute --verbose "$APP_PATH"
|
||||
log "[$label] Package $(basename "$out_zip")"
|
||||
ditto -c -k --keepParent "$app_path" "$out_zip"
|
||||
}
|
||||
|
||||
# ---------- 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"
|
||||
ARM64_ZIP="$RELEASE_DIR/Scarf-v${VERSION}-ARM64.zip"
|
||||
|
||||
build_variant "Universal" "arm64 x86_64" "$UNIVERSAL_ZIP"
|
||||
build_variant "ARM64" "arm64" "$ARM64_ZIP"
|
||||
|
||||
# ---------- sign appcast entry ----------
|
||||
log "Sign appcast entry with EdDSA"
|
||||
@@ -241,7 +263,8 @@ fi
|
||||
gh release create "v${VERSION}" \
|
||||
--title "Scarf v${VERSION}" \
|
||||
"${GH_FLAGS[@]}" \
|
||||
"$UNIVERSAL_ZIP"
|
||||
"$UNIVERSAL_ZIP" \
|
||||
"$ARM64_ZIP"
|
||||
|
||||
# ---------- tag main (skipped for drafts) ----------
|
||||
if [[ $DRAFT -eq 0 ]]; then
|
||||
|
||||
Reference in New Issue
Block a user