mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
feat(updater): forward-compat HermesUpdaterCommandBuilder for hermes update --yes (WS-8)
Pure-function helper that builds argv arrays for `hermes update`, gated on `HermesCapabilities`. Pre-v0.12 → bare `update`; v0.12+ honors `--check`; v0.13+ honors `--yes` for unattended runs. No in-app "Update Hermes" affordance ships in v2.7.5 — Sparkle handles Scarf-self-update and `hermes update` is invoked by users in their terminal. This is forward-compat plumbing so the eventual UI surface shares flag selection across Mac / iOS / remote without re-deriving from scratch. Test matrix in `M0eUpdaterTests` covers all six combinations (pre-v0.12, v0.12 ± unattended ± check, v0.13 ± unattended ± check) plus an empty-capabilities fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Pure helpers that build argv arrays for `hermes update` invocations.
|
||||||
|
///
|
||||||
|
/// Lives in ScarfCore so the eventual UI surface (Mac / iOS / remote)
|
||||||
|
/// shares flag selection. There is no in-app "Update Hermes" affordance
|
||||||
|
/// in v2.7.5 — Sparkle handles Scarf-self-update and `hermes update` is
|
||||||
|
/// invoked by users in their terminal — but capability-gated flag logic
|
||||||
|
/// is forward-compat plumbing that the future affordance will call. Each
|
||||||
|
/// helper is a `nonisolated static` pure function: no transport, no
|
||||||
|
/// MainActor, no mocking surface required.
|
||||||
|
public enum HermesUpdaterCommandBuilder {
|
||||||
|
/// Argv for an `hermes update` invocation, capability-gated.
|
||||||
|
///
|
||||||
|
/// Pre-v0.12 hosts only had `update` (no flags). v0.12+ accepts
|
||||||
|
/// `--check` for preflight. v0.13+ accepts `--yes` / `-y` for
|
||||||
|
/// unattended runs (skips the interactive confirmation prompt).
|
||||||
|
/// Flags are silently dropped when the connected host can't honor
|
||||||
|
/// them so callers don't need to branch on capabilities themselves.
|
||||||
|
public static func updateArgv(
|
||||||
|
capabilities: HermesCapabilities,
|
||||||
|
unattended: Bool,
|
||||||
|
checkOnly: Bool
|
||||||
|
) -> [String] {
|
||||||
|
var args: [String] = ["update"]
|
||||||
|
if checkOnly && capabilities.hasUpdateCheck {
|
||||||
|
args.append("--check")
|
||||||
|
}
|
||||||
|
if unattended && capabilities.hasUpdateNonInteractive {
|
||||||
|
args.append("--yes")
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import Testing
|
||||||
|
import Foundation
|
||||||
|
@testable import ScarfCore
|
||||||
|
|
||||||
|
/// Pure-function matrix for `HermesUpdaterCommandBuilder.updateArgv`. The
|
||||||
|
/// builder degrades flags silently when the connected host can't honor
|
||||||
|
/// them, so the "is the right flag emitted on the right version?" matrix
|
||||||
|
/// is the meaningful test surface.
|
||||||
|
@Suite struct M0eUpdaterTests {
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private func caps(_ versionLine: String?) -> HermesCapabilities {
|
||||||
|
guard let line = versionLine else { return .empty }
|
||||||
|
return HermesCapabilities.parseLine(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Pre-v0.12 (no flags supported)
|
||||||
|
|
||||||
|
@Test func preV012_returnsBareUpdateRegardlessOfFlags() {
|
||||||
|
let pre = caps("Hermes Agent v0.11.0 (2026.4.23)")
|
||||||
|
#expect(HermesUpdaterCommandBuilder.updateArgv(
|
||||||
|
capabilities: pre, unattended: false, checkOnly: false
|
||||||
|
) == ["update"])
|
||||||
|
#expect(HermesUpdaterCommandBuilder.updateArgv(
|
||||||
|
capabilities: pre, unattended: true, checkOnly: false
|
||||||
|
) == ["update"])
|
||||||
|
#expect(HermesUpdaterCommandBuilder.updateArgv(
|
||||||
|
capabilities: pre, unattended: true, checkOnly: true
|
||||||
|
) == ["update"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func unknownVersion_returnsBareUpdate() {
|
||||||
|
// No detected version means we can't guarantee any flag is
|
||||||
|
// honored; defensively emit the bare verb.
|
||||||
|
#expect(HermesUpdaterCommandBuilder.updateArgv(
|
||||||
|
capabilities: .empty, unattended: true, checkOnly: true
|
||||||
|
) == ["update"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - v0.12 (--check supported, --yes is not)
|
||||||
|
|
||||||
|
@Test func v012_checkOnly_emitsCheckFlag() {
|
||||||
|
let v012 = caps("Hermes Agent v0.12.0 (2026.4.30)")
|
||||||
|
#expect(HermesUpdaterCommandBuilder.updateArgv(
|
||||||
|
capabilities: v012, unattended: false, checkOnly: true
|
||||||
|
) == ["update", "--check"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func v012_unattended_dropsYesFlag() {
|
||||||
|
// v0.12 doesn't honor --yes; the helper degrades silently.
|
||||||
|
let v012 = caps("Hermes Agent v0.12.0 (2026.4.30)")
|
||||||
|
#expect(HermesUpdaterCommandBuilder.updateArgv(
|
||||||
|
capabilities: v012, unattended: true, checkOnly: false
|
||||||
|
) == ["update"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func v012_checkOnlyAndUnattended_emitsOnlyCheck() {
|
||||||
|
let v012 = caps("Hermes Agent v0.12.0 (2026.4.30)")
|
||||||
|
#expect(HermesUpdaterCommandBuilder.updateArgv(
|
||||||
|
capabilities: v012, unattended: true, checkOnly: true
|
||||||
|
) == ["update", "--check"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - v0.13 (full flag support)
|
||||||
|
|
||||||
|
@Test func v013_unattended_emitsYesFlag() {
|
||||||
|
let v013 = caps("Hermes Agent v0.13.0 (2026.5.7)")
|
||||||
|
#expect(HermesUpdaterCommandBuilder.updateArgv(
|
||||||
|
capabilities: v013, unattended: true, checkOnly: false
|
||||||
|
) == ["update", "--yes"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func v013_checkOnlyAndUnattended_emitsBothFlags() {
|
||||||
|
let v013 = caps("Hermes Agent v0.13.0 (2026.5.7)")
|
||||||
|
#expect(HermesUpdaterCommandBuilder.updateArgv(
|
||||||
|
capabilities: v013, unattended: true, checkOnly: true
|
||||||
|
) == ["update", "--check", "--yes"])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func v013_neither_emitsBareUpdate() {
|
||||||
|
let v013 = caps("Hermes Agent v0.13.0 (2026.5.7)")
|
||||||
|
#expect(HermesUpdaterCommandBuilder.updateArgv(
|
||||||
|
capabilities: v013, unattended: false, checkOnly: false
|
||||||
|
) == ["update"])
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user