diff --git a/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/HermesUpdaterCommandBuilder.swift b/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/HermesUpdaterCommandBuilder.swift new file mode 100644 index 0000000..fde28ee --- /dev/null +++ b/scarf/Packages/ScarfCore/Sources/ScarfCore/Services/HermesUpdaterCommandBuilder.swift @@ -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 + } +} diff --git a/scarf/Packages/ScarfCore/Tests/ScarfCoreTests/M0eUpdaterTests.swift b/scarf/Packages/ScarfCore/Tests/ScarfCoreTests/M0eUpdaterTests.swift new file mode 100644 index 0000000..c063028 --- /dev/null +++ b/scarf/Packages/ScarfCore/Tests/ScarfCoreTests/M0eUpdaterTests.swift @@ -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"]) + } +}