225 lines
7.6 KiB
Swift
225 lines
7.6 KiB
Swift
import Foundation
|
|
|
|
public struct ClaudeSourcePlanningInput: Equatable, Sendable {
|
|
public let runtime: ProviderRuntime
|
|
public let selectedDataSource: ClaudeUsageDataSource
|
|
public let webExtrasEnabled: Bool
|
|
public let hasWebSession: Bool
|
|
public let hasCLI: Bool
|
|
public let hasOAuthCredentials: Bool
|
|
|
|
public init(
|
|
runtime: ProviderRuntime,
|
|
selectedDataSource: ClaudeUsageDataSource,
|
|
webExtrasEnabled: Bool,
|
|
hasWebSession: Bool,
|
|
hasCLI: Bool,
|
|
hasOAuthCredentials: Bool)
|
|
{
|
|
self.runtime = runtime
|
|
self.selectedDataSource = selectedDataSource
|
|
self.webExtrasEnabled = webExtrasEnabled
|
|
self.hasWebSession = hasWebSession
|
|
self.hasCLI = hasCLI
|
|
self.hasOAuthCredentials = hasOAuthCredentials
|
|
}
|
|
}
|
|
|
|
public enum ClaudeSourcePlanReason: String, Equatable, Sendable {
|
|
case explicitSourceSelection = "explicit-source-selection"
|
|
case appAutoPreferredOAuth = "app-auto-preferred-oauth"
|
|
case appAutoFallbackCLI = "app-auto-fallback-cli"
|
|
case appAutoFallbackWeb = "app-auto-fallback-web"
|
|
case cliAutoPreferredWeb = "cli-auto-preferred-web"
|
|
case cliAutoFallbackCLI = "cli-auto-fallback-cli"
|
|
}
|
|
|
|
public struct ClaudeFetchPlanStep: Equatable, Sendable {
|
|
public let dataSource: ClaudeUsageDataSource
|
|
public let inclusionReason: ClaudeSourcePlanReason
|
|
public let isPlausiblyAvailable: Bool
|
|
|
|
public init(
|
|
dataSource: ClaudeUsageDataSource,
|
|
inclusionReason: ClaudeSourcePlanReason,
|
|
isPlausiblyAvailable: Bool)
|
|
{
|
|
self.dataSource = dataSource
|
|
self.inclusionReason = inclusionReason
|
|
self.isPlausiblyAvailable = isPlausiblyAvailable
|
|
}
|
|
}
|
|
|
|
public struct ClaudeFetchPlan: Equatable, Sendable {
|
|
public let input: ClaudeSourcePlanningInput
|
|
public let orderedSteps: [ClaudeFetchPlanStep]
|
|
|
|
public init(input: ClaudeSourcePlanningInput, orderedSteps: [ClaudeFetchPlanStep]) {
|
|
self.input = input
|
|
self.orderedSteps = orderedSteps
|
|
}
|
|
|
|
public var availableSteps: [ClaudeFetchPlanStep] {
|
|
self.orderedSteps.filter(\.isPlausiblyAvailable)
|
|
}
|
|
|
|
public var isNoSourceAvailable: Bool {
|
|
self.availableSteps.isEmpty
|
|
}
|
|
|
|
public var preferredStep: ClaudeFetchPlanStep? {
|
|
switch self.input.selectedDataSource {
|
|
case .auto:
|
|
self.availableSteps.first
|
|
case .api, .oauth, .web, .cli:
|
|
self.orderedSteps.first
|
|
}
|
|
}
|
|
|
|
public var executionSteps: [ClaudeFetchPlanStep] {
|
|
switch self.input.selectedDataSource {
|
|
case .auto:
|
|
self.availableSteps
|
|
case .api, .oauth, .web, .cli:
|
|
self.orderedSteps
|
|
}
|
|
}
|
|
|
|
public var compatibilityStrategy: ClaudeUsageStrategy? {
|
|
guard let preferredStep else { return nil }
|
|
let useWebExtras = self.input.runtime == .app
|
|
&& preferredStep.dataSource == .cli
|
|
&& self.input.webExtrasEnabled
|
|
return ClaudeUsageStrategy(
|
|
dataSource: preferredStep.dataSource,
|
|
useWebExtras: useWebExtras)
|
|
}
|
|
|
|
public var orderLabel: String {
|
|
self.orderedSteps.map(\.dataSource.sourceLabel).joined(separator: "→")
|
|
}
|
|
|
|
public func debugLines() -> [String] {
|
|
var lines = ["planner_order=\(self.orderLabel)"]
|
|
lines.append("planner_selected=\(self.preferredStep?.dataSource.rawValue ?? "none")")
|
|
lines.append("planner_no_source=\(self.isNoSourceAvailable)")
|
|
for step in self.orderedSteps {
|
|
let availability = step.isPlausiblyAvailable ? "available" : "unavailable"
|
|
lines.append(
|
|
"planner_step.\(step.dataSource.rawValue)=\(availability) reason=\(step.inclusionReason.rawValue)")
|
|
}
|
|
return lines
|
|
}
|
|
}
|
|
|
|
public enum ClaudeCLIResolver {
|
|
#if DEBUG
|
|
@TaskLocal static var resolvedBinaryPathOverrideForTesting: String?
|
|
|
|
public static var currentResolvedBinaryPathOverrideForTesting: String? {
|
|
self.resolvedBinaryPathOverrideForTesting
|
|
}
|
|
|
|
public static func withResolvedBinaryPathOverrideForTesting<T>(
|
|
_ path: String?,
|
|
operation: () async throws -> T) async rethrows -> T
|
|
{
|
|
try await self.$resolvedBinaryPathOverrideForTesting.withValue(path) {
|
|
try await operation()
|
|
}
|
|
}
|
|
#endif
|
|
|
|
public static func resolvedBinaryPath(
|
|
environment: [String: String] = ProcessInfo.processInfo.environment,
|
|
loginPATH: [String]? = LoginShellPathCache.shared.current)
|
|
-> String?
|
|
{
|
|
#if DEBUG
|
|
if let override = self.resolvedBinaryPathOverrideForTesting {
|
|
return FileManager.default.isExecutableFile(atPath: override) ? override : nil
|
|
}
|
|
#endif
|
|
|
|
var normalizedEnvironment = environment
|
|
if let override = environment["CLAUDE_CLI_PATH"]?.trimmingCharacters(in: .whitespacesAndNewlines) {
|
|
if override.isEmpty {
|
|
normalizedEnvironment.removeValue(forKey: "CLAUDE_CLI_PATH")
|
|
} else {
|
|
normalizedEnvironment["CLAUDE_CLI_PATH"] = override
|
|
if FileManager.default.isExecutableFile(atPath: override) {
|
|
return override
|
|
}
|
|
}
|
|
}
|
|
|
|
return BinaryLocator.resolveClaudeBinary(
|
|
env: normalizedEnvironment,
|
|
loginPATH: loginPATH)
|
|
}
|
|
|
|
public static func isAvailable(environment: [String: String] = ProcessInfo.processInfo.environment) -> Bool {
|
|
self.resolvedBinaryPath(environment: environment) != nil
|
|
}
|
|
}
|
|
|
|
public enum ClaudeSourcePlanner {
|
|
public static func resolve(input: ClaudeSourcePlanningInput) -> ClaudeFetchPlan {
|
|
ClaudeFetchPlan(input: input, orderedSteps: self.makeSteps(input: input))
|
|
}
|
|
|
|
private static func makeSteps(input: ClaudeSourcePlanningInput) -> [ClaudeFetchPlanStep] {
|
|
switch input.selectedDataSource {
|
|
case .auto:
|
|
switch input.runtime {
|
|
case .app:
|
|
[
|
|
self.step(.oauth, reason: .appAutoPreferredOAuth, input: input),
|
|
self.step(.cli, reason: .appAutoFallbackCLI, input: input),
|
|
self.step(.web, reason: .appAutoFallbackWeb, input: input),
|
|
]
|
|
case .cli:
|
|
[
|
|
self.step(.web, reason: .cliAutoPreferredWeb, input: input),
|
|
self.step(.cli, reason: .cliAutoFallbackCLI, input: input),
|
|
]
|
|
}
|
|
case .api:
|
|
[self.step(.api, reason: .explicitSourceSelection, input: input)]
|
|
case .oauth:
|
|
[self.step(.oauth, reason: .explicitSourceSelection, input: input)]
|
|
case .web:
|
|
[self.step(.web, reason: .explicitSourceSelection, input: input)]
|
|
case .cli:
|
|
[self.step(.cli, reason: .explicitSourceSelection, input: input)]
|
|
}
|
|
}
|
|
|
|
private static func step(
|
|
_ dataSource: ClaudeUsageDataSource,
|
|
reason: ClaudeSourcePlanReason,
|
|
input: ClaudeSourcePlanningInput) -> ClaudeFetchPlanStep
|
|
{
|
|
ClaudeFetchPlanStep(
|
|
dataSource: dataSource,
|
|
inclusionReason: reason,
|
|
isPlausiblyAvailable: self.isPlausiblyAvailable(dataSource, input: input))
|
|
}
|
|
|
|
private static func isPlausiblyAvailable(
|
|
_ dataSource: ClaudeUsageDataSource,
|
|
input: ClaudeSourcePlanningInput) -> Bool
|
|
{
|
|
switch dataSource {
|
|
case .auto, .api:
|
|
false
|
|
case .oauth:
|
|
input.hasOAuthCredentials
|
|
case .web:
|
|
input.hasWebSession
|
|
case .cli:
|
|
input.hasCLI
|
|
}
|
|
}
|
|
}
|