Compare commits

...
Author SHA1 Message Date
lawrencecchenandClaude Fable 5 29fdeeb5fa Keep AppDelegate and TerminalController net-zero for the file-length hard cap
The billing command grew two over-900-line god files (AppDelegate by the
BillingClient.bootstrap call, TerminalController by the three billing.*
capability strings), which the file-length hard-cap rejects on any
growth. Fold each addition onto an adjacent existing line so neither
file grows versus main, without changing behavior.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-08 00:30:00 -07:00
lawrencecchen 45a613b994 Bump length budget for the two billing socket files after merging main 2026-07-08 00:23:20 -07:00
lawrencecchen 3c29727fad Merge remote-tracking branch 'origin/main' into feat-billing-cli 2026-07-08 00:22:25 -07:00
lawrencecchenandClaude Fable 5 71c0f315a3 Keep cmux.swift under the file-length cap and add source to billing status --json
The billing dispatch adds irreducible switch-registration lines to the
oversized CLI/cmux.swift, which the file-length hard-cap rejects on any
growth. Externalize the inline ping and capabilities usage strings into
CMUXCLI+MiscUsage.swift so cmux.swift shrinks below its merge-base line
count. Also include the source origin in `billing status --json` so the
JSON output matches the human summary.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-07 23:58:50 -07:00
lawrencecchen fdbc13b184 Ratchet CLI/cmux.swift length budget for billing command registration
The billing CLI subcommand adds 6 lines of irreducible dispatch/usage
registration to the central CMUXCLI command switch (case + usage lookup +
help text), pushing CLI/cmux.swift to 35606. Billing logic itself lives in
new files (CMUXCLI+Billing.swift, BillingClient.swift). Ratchet the budget
by exactly 6 to match; the file is far over the 900 incidental-growth cap so
growth must be recorded, not absorbed.
2026-07-07 23:38:28 -07:00
lawrencecchenandClaude Fable 5 07c94807f5 Add cmux billing CLI commands (live status, checkout, portal)
`cmux billing status [--json]` prints the signed-in user's plan fetched
live from the app's web origin (production = cmux.com), so an agent can
read billing state without stale caching. `cmux billing checkout
[--plan pro|team] [--url|--open]` starts checkout for the signed-in
user: the app calls the checkout endpoint server-side, captures the
Stripe URL from the redirect, and opens it (or prints it), so the
browser lands directly on the Stripe page rather than a cmux endpoint.
`cmux billing portal [--url|--open]` opens the Stripe customer portal.

The commands route through the control socket so the app makes the
authenticated call with the user's Stack tokens; tokens never reach the
CLI or logs. Already-subscribed, unavailable, and not-signed-in states
return structured errors instead of opening a wrong page.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-07 22:57:23 -07:00
19 changed files with 744 additions and 18 deletions
+3 -3
View File
@@ -1,7 +1,7 @@
# cmux-owned Swift file length budget.
# Format: max_lines<TAB>relative path
# Reduce counts as files shrink. CI fails if tracked files exceed this budget.
35600 CLI/cmux.swift
35598 CLI/cmux.swift
18229 Sources/AppDelegate.swift
16443 Sources/ContentView.swift
15163 Sources/TerminalController.swift
@@ -242,7 +242,7 @@
519 Packages/macOS/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/stress-two-column-cockpit-sidebar.swift
518 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swift
518 Packages/macOS/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/Corpus/stress-git-review-queue-command-deck.swift
516 Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift
530 Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift
516 Sources/TerminalImageTransfer.swift
514 Packages/macOS/CmuxSwiftRender/Sources/CmuxSwiftRender/ExpressionEvaluator.swift
514 cmuxUITests/UpdatePillUITests.swift
@@ -255,7 +255,7 @@
506 Sources/App/MainWindowVisibilityController.swift
504 Packages/macOS/CmuxSettings/Tests/CmuxSettingsTests/UserDefaultsSettingsStoreTests.swift
504 cmuxTests/TerminalNotificationSocketActionTests.swift
503 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift
505 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift
503 Sources/Settings/ConfigSource.swift
503 Sources/TerminalNotificationQueue.swift
502 Sources/CmuxEventPublishing.swift
1 # cmux-owned Swift file length budget.
2 # Format: max_lines<TAB>relative path
3 # Reduce counts as files shrink. CI fails if tracked files exceed this budget.
4 35600 35598
5 18229
6 16443
7 15163
242 519
243 518
244 518
245 516 530
246 516
247 514
248 514
255 506
256 504
257 504
258 503 505
259 503
260 503
261 502
+226
View File
@@ -0,0 +1,226 @@
import Foundation
extension CMUXCLI {
static let billingUsage = """
Usage: cmux billing <status|checkout|portal> [options]
Fetch live billing data from the app's configured web API origin
(cmux.com in production) for the currently signed-in user. The CLI talks
to the running cmux app over the control socket; the app makes the
authenticated request with its Stack session.
cmux billing status [--json]
Print the live plan summary. --json prints the server JSON body.
cmux billing checkout [--plan pro|team] [--url | --open]
Start checkout for the signed-in user. Default: --plan pro --open.
cmux billing portal [--url | --open]
Open the Stripe customer portal for the signed-in user. Default: --open.
"""
func runBillingCommand(commandArgs: [String], client: SocketClient, jsonOutput globalJSONOutput: Bool) throws {
let sub = commandArgs.first?.lowercased() ?? "status"
let rest = Array(commandArgs.dropFirst())
switch sub {
case "help", "--help", "-h":
print(Self.billingUsage)
case "status":
let jsonOutput = globalJSONOutput || rest.contains("--json")
let remaining = rest.filter { $0 != "--json" }
try rejectUnexpectedBillingArguments(remaining, command: "billing status")
let response = try client.sendV2(method: "billing.status", responseTimeout: 75)
try handleBillingStructuredError(response)
let plan = (response["plan"] as? [String: Any]) ?? response
let source = response["source"] as? String
if jsonOutput {
var out = plan
if out["source"] == nil, let source { out["source"] = source }
print(jsonString(out))
return
}
printBillingStatus(plan, source: source)
case "checkout":
let (planOpt, rem0) = parseOption(rest, name: "--plan")
let mode = try billingURLMode(rem0, command: "billing checkout")
let plan = (planOpt ?? "pro").lowercased()
guard plan == "pro" || plan == "team" else {
throw CLIError(message: "billing checkout: --plan must be pro or team.")
}
let response = try client.sendV2(
method: "billing.checkout",
params: ["plan": plan],
responseTimeout: 75
)
try handleBillingURLResponse(response, mode: mode, noun: "checkout")
case "portal":
let mode = try billingURLMode(rest, command: "billing portal")
let response = try client.sendV2(method: "billing.portal", responseTimeout: 75)
try handleBillingURLResponse(response, mode: mode, noun: "portal")
default:
throw CLIError(message: """
Unknown billing subcommand: \(sub)
\(Self.billingUsage)
""")
}
}
private enum BillingURLMode {
case open
case url
}
private func billingURLMode(_ args: [String], command: String) throws -> BillingURLMode {
var mode = BillingURLMode.open
for arg in args {
switch arg {
case "--open":
mode = .open
case "--url":
mode = .url
default:
throw CLIError(message: "\(command): unknown argument '\(arg)'.\n\n\(Self.billingUsage)")
}
}
return mode
}
private func rejectUnexpectedBillingArguments(_ args: [String], command: String) throws {
if let first = args.first {
throw CLIError(message: "\(command): unknown argument '\(first)'.\n\n\(Self.billingUsage)")
}
}
private func printBillingStatus(_ plan: [String: Any], source: String?) {
let planId = billingString(plan["planId"]) ?? "free"
let teamPlanId = billingString(plan["teamPlanId"]) ?? "free"
let planName = billingDisplayPlanName(planId: planId, isPro: billingBool(plan["isPro"]))
let isPro = billingBool(plan["isPro"]) ?? false
let billingManagement = billingString(plan["billingManagement"]) ?? "none"
let teamBillingManagement = billingString(plan["teamBillingManagement"]) ?? "none"
let manualOverride = billingBool(plan["hasManualVmPlanOverride"]) ?? false
let billingAvailable = billingBool(plan["billingAvailable"]) ?? true
print("plan: \(planName)")
print("pro active: \(isPro ? "yes" : "no")")
print("team plan: \(billingDisplayPlanName(planId: teamPlanId, isPro: nil))")
print("billing available: \(billingAvailable ? "yes" : "no")")
print("billing management: \(billingManagement)")
print("team billing management: \(teamBillingManagement)")
print("manual VM override: \(manualOverride ? "yes" : "no")")
if let source, !source.isEmpty {
print("source: \(source)")
}
}
private func handleBillingURLResponse(_ response: [String: Any], mode: BillingURLMode, noun: String) throws {
try handleBillingStructuredError(response)
guard let url = response["url"] as? String, !url.isEmpty else {
let state = billingString(response["billing"])
?? billingString(response["welcome"])
?? billingString(response["error"])
?? "unavailable"
let source = (response["source"] as? String).map { " (source: \($0))" } ?? ""
throw CLIError(message: "Billing \(noun) is not available: \(state)\(source)")
}
switch mode {
case .url:
print(url)
case .open:
try openBillingURL(url)
print("Opened billing \(noun): \(url)")
if let source = response["source"] as? String, !source.isEmpty {
print("source: \(source)")
}
}
}
private func handleBillingStructuredError(_ response: [String: Any]) throws {
if let ok = response["ok"] as? Bool, ok == false {
let error = billingString(response["error"]) ?? "billing_unavailable"
let source = (response["source"] as? String).map { "source: \($0)" }
let detail = billingString(response["detail"])
let suffix = [detail, source].compactMap { $0 }.joined(separator: "\n")
let extra = suffix.isEmpty ? "" : "\n\(suffix)"
switch error {
case "not_signed_in":
throw CLIError(message: "You are not signed in to cmux. Run `cmux auth login`, then retry.\(extra)")
case "session_refresh_failed":
throw CLIError(message: "cmux could not refresh your session. Retry in a moment.\(extra)")
case "active_already_subscribed":
throw CLIError(message: "You already have an active subscription.\(extra)")
case "error", "unavailable", "invalid_plan":
let billing = billingString(response["billing"]) ?? error
throw CLIError(message: "Billing is not available: \(billing).\(extra)")
default:
throw CLIError(message: "Billing request failed: \(error).\(extra)")
}
}
}
private func openBillingURL(_ rawURL: String) throws {
guard URL(string: rawURL) != nil else {
throw CLIError(message: "Billing returned an invalid URL.")
}
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/open")
process.arguments = [rawURL]
try process.run()
process.waitUntilExit()
if process.terminationStatus != 0 {
throw CLIError(message: "Failed to open billing URL. Run with --url and open it manually.")
}
}
private func billingDisplayPlanName(planId: String, isPro: Bool?) -> String {
if isPro == true { return "Pro" }
switch planId.lowercased() {
case "pro":
return "Pro"
case "team":
return "Team"
case "free":
return "Free"
default:
return Self.sanitizeForTerminal(planId)
}
}
private func billingString(_ value: Any?) -> String? {
switch value {
case let string as String:
let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : Self.sanitizeForTerminal(trimmed)
case let number as NSNumber:
return number.stringValue
default:
return nil
}
}
private func billingBool(_ value: Any?) -> Bool? {
switch value {
case let bool as Bool:
return bool
case let number as NSNumber:
return number.boolValue
case let string as String:
switch string.lowercased() {
case "true", "yes", "1":
return true
case "false", "no", "0":
return false
default:
return nil
}
default:
return nil
}
}
}
+1
View File
@@ -58,6 +58,7 @@ extension CMUXCLI {
"ai-accounts",
"auth",
"bind-key",
"billing",
"break-pane",
"browser",
"browser-back",
+15
View File
@@ -0,0 +1,15 @@
import Foundation
extension CMUXCLI {
static let pingUsage = """
Usage: cmux ping
Check connectivity to the cmux socket server.
"""
static let capabilitiesUsage = """
Usage: cmux capabilities
Print server capabilities as JSON.
"""
}
+8 -10
View File
@@ -4174,6 +4174,9 @@ struct CMUXCLI {
case "ai-accounts":
try runAIAccountsCommand(commandArgs: commandArgs, client: client, jsonOutput: jsonOutput)
case "billing":
try runBillingCommand(commandArgs: commandArgs, client: client, jsonOutput: jsonOutput)
case "mobile":
let sub = commandArgs.first?.lowercased()
let rest = Array(commandArgs.dropFirst())
@@ -15120,18 +15123,12 @@ struct CMUXCLI {
return Self.remotesUsage
case "ai-accounts":
return Self.aiAccountsUsage
case "billing":
return Self.billingUsage
case "ping":
return """
Usage: cmux ping
Check connectivity to the cmux socket server.
"""
return Self.pingUsage
case "capabilities":
return """
Usage: cmux capabilities
Print server capabilities as JSON.
"""
return Self.capabilitiesUsage
case "canvas":
return """
Usage: cmux canvas <subcommand> [args] [--workspace <id|ref>]
@@ -35415,6 +35412,7 @@ export default CMUXSessionRestore;
auth <status|login|logout>
login | logout (aliases for auth login/logout)
vm <base|new|ls|status|snapshot|fork|restore|rm|exec|shell|ssh> [args...] (alias: cloud)
billing <status|checkout|portal> [--json] [--url|--open]
remotes <list|add|remove> [--route <host:port>] [--tag <tag>] [--json] (alias: remote)
ai-accounts <list|upload|remove> [--team <id>] [--json]
rpc <method> [json-params]
@@ -0,0 +1,16 @@
/// The billing control seam (part of the ``ControlCommandContext`` umbrella).
///
/// Billing calls perform authenticated web API requests in the app process so
/// Stack tokens never cross the socket boundary. The methods are `nonisolated`
/// because `billing.*` runs on the socket worker; each app-side witness owns
/// any main-actor/auth hops it needs.
public protocol ControlBillingContext: AnyObject {
/// Fetches the current signed-in user's live billing plan state.
nonisolated func controlBillingStatus() -> ControlCallResult
/// Starts checkout for the requested plan and returns either a URL or a structured billing state.
nonisolated func controlBillingCheckout(plan: String) -> ControlCallResult
/// Starts the customer portal flow and returns either a URL or a structured billing state.
nonisolated func controlBillingPortal() -> ControlCallResult
}
@@ -0,0 +1,33 @@
extension ControlCommandCoordinator {
nonisolated func billingStatus(context: (any ControlCommandContext)?) -> ControlCallResult {
guard let context = context as? any ControlBillingContext else {
return .ok(.object(["ok": .bool(false), "error": .string("unavailable")]))
}
return context.controlBillingStatus()
}
nonisolated func billingCheckout(
_ params: [String: JSONValue],
context: (any ControlCommandContext)?
) -> ControlCallResult {
let plan = string(params, "plan") ?? "pro"
guard plan == "pro" || plan == "team" else {
return .ok(.object([
"ok": .bool(false),
"error": .string("invalid_plan"),
"billing": .string("invalid_plan"),
]))
}
guard let context = context as? any ControlBillingContext else {
return .ok(.object(["ok": .bool(false), "error": .string("unavailable")]))
}
return context.controlBillingCheckout(plan: plan)
}
nonisolated func billingPortal(context: (any ControlCommandContext)?) -> ControlCallResult {
guard let context = context as? any ControlBillingContext else {
return .ok(.object(["ok": .bool(false), "error": .string("unavailable")]))
}
return context.controlBillingPortal()
}
}
@@ -26,6 +26,7 @@ public protocol ControlCommandContext:
ControlWorkspaceContext,
ControlSurfaceContext,
ControlSystemContext,
ControlBillingContext,
ControlProjectContext,
ControlDebugContext,
ControlSidebarContext,
@@ -138,6 +138,12 @@ public final class ControlCommandCoordinator {
return systemIdentify(request.params, context: context)
case "system.tree":
return systemTree(request.params, context: context)
case "billing.status":
return billingStatus(context: context)
case "billing.checkout":
return billingCheckout(request.params, context: context)
case "billing.portal":
return billingPortal(context: context)
case "surface.send_text":
return surfaceSendText(request.params, context: context)
case "surface.send_key":
@@ -16,12 +16,13 @@ public enum ControlCommandExecutionPolicy: Sendable, Equatable {
/// from the main thread.
case socketWorker(mainThreadCallable: Bool)
/// Classifies a method: every `vm.`-, `remotes.`-, and
/// `aiAccounts.`-prefixed method and the fixed socket-worker set run on the
/// Classifies a method: every `vm.`-, `remotes.`-, `aiAccounts.`-, and
/// `billing.`-prefixed method and the fixed socket-worker set run on the
/// worker; everything else runs on the main actor.
///
/// `remotes.*` (the `cmux remotes` device-registry verbs) and
/// `aiAccounts.*` (the team's subrouter AI-account verbs) make blocking,
/// `aiAccounts.*` (the team's subrouter AI-account verbs) and `billing.*`
/// (live plan/Stripe redirects from the app web origin) make blocking,
/// authenticated web API calls just like `vm.*`, so they must stay off the
/// main actor; prefix matches keep each verb family in lockstep without
/// listing each method.
@@ -29,6 +30,7 @@ public enum ControlCommandExecutionPolicy: Sendable, Equatable {
/// - Parameter method: The trimmed method name.
public init(forMethod method: String) {
if method.hasPrefix("vm.") || method.hasPrefix("remotes.") || method.hasPrefix("aiAccounts.")
|| method.hasPrefix("billing.")
|| Self.socketWorkerMethods.contains(method) {
self = .socketWorker(
mainThreadCallable: Self.mainThreadCallableSocketWorkerMethods.contains(method)
@@ -42,6 +42,20 @@ extension ControlFeedContext {
func controlFeedSnapshotItems(pendingOnly: Bool) -> [JSONValue] { [] }
}
extension ControlBillingContext {
func controlBillingStatus() -> ControlCallResult {
.ok(.object(["ok": .bool(false), "error": .string("unavailable")]))
}
func controlBillingCheckout(plan: String) -> ControlCallResult {
.ok(.object(["ok": .bool(false), "error": .string("unavailable")]))
}
func controlBillingPortal() -> ControlCallResult {
.ok(.object(["ok": .bool(false), "error": .string("unavailable")]))
}
}
extension ControlPaneContext {
func controlPaneList(routing: ControlRoutingSelectors) -> ControlPaneListSnapshot? { nil }
func controlPaneRoutingResolvesTabManager(routing: ControlRoutingSelectors) -> Bool { false }
@@ -0,0 +1,72 @@
import Testing
@testable import CmuxControlSocket
private final class FakeBillingControlCommandContext: ControlCommandContext {
nonisolated(unsafe) var checkoutPlan: String?
nonisolated func controlBillingStatus() -> ControlCallResult {
.ok(.object([
"source": .string("https://cmux.com"),
"plan": .object(["planId": .string("free")]),
]))
}
nonisolated func controlBillingCheckout(plan: String) -> ControlCallResult {
checkoutPlan = plan
return .ok(.object(["ok": .bool(true), "url": .string("https://checkout.stripe.com/c/test")]))
}
nonisolated func controlBillingPortal() -> ControlCallResult {
.ok(.object(["ok": .bool(true), "url": .string("https://billing.stripe.com/p/session")]))
}
}
@Suite("ControlCommandCoordinator billing domain")
struct ControlCommandCoordinatorBillingTests {
@Test @MainActor func statusDispatchesToBillingContext() {
let context = FakeBillingControlCommandContext()
let coordinator = ControlCommandCoordinator(context: context)
let result = coordinator.handleSocketWorkerV2(
ControlRequest(id: nil, method: "billing.status", params: [:]),
context: context
)
#expect(result == .ok(.object([
"source": .string("https://cmux.com"),
"plan": .object(["planId": .string("free")]),
])))
}
@Test @MainActor func checkoutDefaultsToPro() {
let context = FakeBillingControlCommandContext()
let coordinator = ControlCommandCoordinator(context: context)
_ = coordinator.handleSocketWorkerV2(
ControlRequest(id: nil, method: "billing.checkout", params: [:]),
context: context
)
#expect(context.checkoutPlan == "pro")
}
@Test @MainActor func checkoutRejectsUnknownPlanAsStructuredBillingState() {
let context = FakeBillingControlCommandContext()
let coordinator = ControlCommandCoordinator(context: context)
let result = coordinator.handleSocketWorkerV2(
ControlRequest(
id: nil,
method: "billing.checkout",
params: ["plan": .string("enterprise")]
),
context: context
)
#expect(result == .ok(.object([
"ok": .bool(false),
"error": .string("invalid_plan"),
"billing": .string("invalid_plan"),
])))
}
}
@@ -26,6 +26,14 @@ struct ControlCommandExecutionPolicyTests {
#expect(ControlCommandExecutionPolicy(forMethod: "aiAccounts.remove") == .socketWorker(mainThreadCallable: false))
}
@Test func billingPrefixedMethodsRunOnTheSocketWorker() {
// `cmux billing` verbs make blocking authenticated web API calls, so
// they must run on the worker while the app keeps Stack tokens local.
#expect(ControlCommandExecutionPolicy(forMethod: "billing.status") == .socketWorker(mainThreadCallable: false))
#expect(ControlCommandExecutionPolicy(forMethod: "billing.checkout") == .socketWorker(mainThreadCallable: false))
#expect(ControlCommandExecutionPolicy(forMethod: "billing.portal") == .socketWorker(mainThreadCallable: false))
}
@Test func fixedWorkerSetRunsOnTheSocketWorker() {
for method in [
"system.ping", "system.capabilities", "auth.status", "auth.sign_in_url",
+1 -1
View File
@@ -2061,7 +2061,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
self.auth = auth
VMClient.bootstrap(auth: auth.coordinator)
RemotesClient.bootstrap(auth: auth.coordinator)
AIAccountsClient.bootstrap(auth: auth.coordinator)
AIAccountsClient.bootstrap(auth: auth.coordinator); BillingClient.bootstrap(auth: auth.coordinator)
PhonePushClient.shared.configure(auth: auth.coordinator)
MobileHostService.shared.configure(auth: auth.coordinator)
DeviceRegistryClient.shared.configure(auth: auth.coordinator)
+255
View File
@@ -0,0 +1,255 @@
import CmuxAuthRuntime
import CmuxControlSocket
import Foundation
enum BillingClientError: Error {
case notSignedIn
case sessionRefreshFailed
case malformedResponse(String)
case backendUnreachable(url: String, detail: String)
}
actor BillingClient {
@MainActor private(set) static var shared: BillingClient!
@MainActor
static func bootstrap(auth: AuthCoordinator, session: URLSession = .shared) {
let redirectSession = URLSession(
configuration: .default,
delegate: BillingNoRedirectDelegate(),
delegateQueue: nil
)
shared = BillingClient(session: session, redirectSession: redirectSession, auth: auth)
}
private let session: URLSession
private let redirectSession: URLSession
private let auth: AuthCoordinator
init(session: URLSession = .shared, redirectSession: URLSession, auth: AuthCoordinator) {
self.session = session
self.redirectSession = redirectSession
self.auth = auth
}
func status() async -> JSONValue {
do {
let (data, http) = try await request("GET", path: "/api/billing/plan", followsRedirects: true)
guard (200...299).contains(http.statusCode) else {
return failure("http_status", source: sourceOrigin, status: http.statusCode, body: data)
}
let object = try decodeJSONValue(data)
return .object([
"source": .string(sourceOrigin),
"plan": object,
])
} catch let error as BillingClientError {
return failure(error)
} catch {
return failure("request_failed", source: sourceOrigin, detail: String(describing: error))
}
}
func checkout(plan: String) async -> JSONValue {
await redirect(path: "/api/billing/checkout", queryItems: [
URLQueryItem(name: "plan", value: plan),
])
}
func portal() async -> JSONValue {
await redirect(path: "/api/billing/portal", queryItems: [])
}
private func redirect(path: String, queryItems: [URLQueryItem]) async -> JSONValue {
do {
let (data, http) = try await request("GET", path: path, queryItems: queryItems, followsRedirects: false)
if (300...399).contains(http.statusCode),
let location = http.value(forHTTPHeaderField: "Location"),
let url = URL(string: location, relativeTo: AuthEnvironment.apiBaseURL)?.absoluteURL {
return redirectPayload(url)
}
if (200...299).contains(http.statusCode),
let object = try? decodeJSONObject(data),
let url = object["url"] as? String,
!url.isEmpty {
return .object(["ok": .bool(true), "source": .string(sourceOrigin), "url": .string(url)])
}
return failure("billing_unavailable", source: sourceOrigin, status: http.statusCode, body: data)
} catch let error as BillingClientError {
return failure(error)
} catch {
return failure("request_failed", source: sourceOrigin, detail: String(describing: error))
}
}
private func request(
_ method: String,
path: String,
queryItems: [URLQueryItem] = [],
followsRedirects: Bool
) async throws -> (Data, HTTPURLResponse) {
let tokens = try await currentTokens()
guard var components = URLComponents(url: AuthEnvironment.apiBaseURL, resolvingAgainstBaseURL: false) else {
throw BillingClientError.malformedResponse("bad api base URL")
}
components.path = (components.path.hasSuffix("/") ? String(components.path.dropLast()) : components.path) + path
if !queryItems.isEmpty {
components.queryItems = queryItems
}
guard let url = components.url else {
throw BillingClientError.malformedResponse("could not build URL for \(path)")
}
var request = URLRequest(url: url)
request.httpMethod = method
request.timeoutInterval = 30
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("Bearer \(tokens.accessToken)", forHTTPHeaderField: "Authorization")
request.setValue(tokens.refreshToken, forHTTPHeaderField: "X-Stack-Refresh-Token")
let activeSession = followsRedirects ? session : redirectSession
let data: Data
let response: URLResponse
do {
(data, response) = try await activeSession.data(for: request)
} catch let error as URLError {
switch error.code {
case .cannotConnectToHost, .cannotFindHost, .timedOut, .networkConnectionLost, .notConnectedToInternet:
throw BillingClientError.backendUnreachable(url: sourceOrigin, detail: error.localizedDescription)
default:
throw error
}
}
guard let http = response as? HTTPURLResponse else {
throw BillingClientError.malformedResponse("non-HTTP response")
}
return (data, http)
}
private func currentTokens() async throws -> (accessToken: String, refreshToken: String) {
do {
return try await auth.currentTokens()
} catch AuthError.networkError {
throw BillingClientError.sessionRefreshFailed
} catch {
throw BillingClientError.notSignedIn
}
}
private func redirectPayload(_ url: URL) -> JSONValue {
if let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
components.path.hasSuffix("/pricing") || components.path.hasSuffix("/app-pricing") {
let billing = components.queryItems?.first(where: { $0.name == "billing" })?.value
let welcome = components.queryItems?.first(where: { $0.name == "welcome" })?.value
if let billing, !billing.isEmpty {
return .object([
"ok": .bool(false),
"source": .string(sourceOrigin),
"error": .string(billing),
"billing": .string(billing),
])
}
if welcome == "active" || welcome == "active-already-subscribed" {
return .object([
"ok": .bool(false),
"source": .string(sourceOrigin),
"error": .string("active_already_subscribed"),
"welcome": .string(welcome ?? ""),
])
}
if let welcome, !welcome.isEmpty {
return .object([
"ok": .bool(false),
"source": .string(sourceOrigin),
"error": .string(welcome),
"welcome": .string(welcome),
])
}
return .object([
"ok": .bool(false),
"source": .string(sourceOrigin),
"error": .string("unavailable"),
])
}
return .object([
"ok": .bool(true),
"source": .string(sourceOrigin),
"url": .string(url.absoluteString),
])
}
private func failure(_ error: BillingClientError) -> JSONValue {
switch error {
case .notSignedIn:
return .object(["ok": .bool(false), "source": .string(sourceOrigin), "error": .string("not_signed_in")])
case .sessionRefreshFailed:
return .object(["ok": .bool(false), "source": .string(sourceOrigin), "error": .string("session_refresh_failed")])
case let .malformedResponse(message):
return .object([
"ok": .bool(false),
"source": .string(sourceOrigin),
"error": .string("malformed_response"),
"detail": .string(message),
])
case let .backendUnreachable(_, detail):
return .object([
"ok": .bool(false),
"source": .string(sourceOrigin),
"error": .string("billing_unreachable"),
"detail": .string(detail),
])
}
}
private func failure(
_ error: String,
source: String,
status: Int? = nil,
body: Data? = nil,
detail: String? = nil
) -> JSONValue {
var payload: [String: JSONValue] = ["ok": .bool(false), "source": .string(source), "error": .string(error)]
if let status {
payload["status"] = .int(Int64(status))
}
if let body, let serverError = serverErrorString(body), !serverError.isEmpty {
payload["detail"] = .string(serverError)
} else if let detail, !detail.isEmpty {
payload["detail"] = .string(detail)
}
return .object(payload)
}
private func decodeJSONValue(_ data: Data) throws -> JSONValue {
let parsed = try JSONSerialization.jsonObject(with: data, options: [])
guard let value = JSONValue(foundationObject: parsed) else {
throw BillingClientError.malformedResponse("response is not valid JSON")
}
return value
}
private func decodeJSONObject(_ data: Data) throws -> [String: Any] {
let parsed = try JSONSerialization.jsonObject(with: data, options: [])
guard let object = parsed as? [String: Any] else {
throw BillingClientError.malformedResponse("expected a JSON object")
}
return object
}
private func serverErrorString(_ data: Data) -> String? {
guard let parsed = try? JSONSerialization.jsonObject(with: data, options: []),
let object = parsed as? [String: Any] else {
return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
}
return (object["error"] as? String) ?? (object["message"] as? String)
}
private var sourceOrigin: String {
var components = URLComponents()
components.scheme = AuthEnvironment.apiBaseURL.scheme
components.host = AuthEnvironment.apiBaseURL.host
components.port = AuthEnvironment.apiBaseURL.port
return components.url?.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
?? AuthEnvironment.apiBaseURL.absoluteString.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
}
}
@@ -0,0 +1,14 @@
import Foundation
// URLSession owns delegate callbacks on its internal queues; this delegate has
// no mutable state, so sharing it with the session is safe.
final class BillingNoRedirectDelegate: NSObject, URLSessionTaskDelegate, @unchecked Sendable {
func urlSession(
_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest
) async -> URLRequest? {
nil
}
}
@@ -0,0 +1,45 @@
import CmuxControlSocket
import Foundation
extension TerminalController: ControlBillingContext {
nonisolated func controlBillingStatus() -> ControlCallResult {
billingCall {
await BillingClient.shared.status()
}
}
nonisolated func controlBillingCheckout(plan: String) -> ControlCallResult {
billingCall {
await BillingClient.shared.checkout(plan: plan)
}
}
nonisolated func controlBillingPortal() -> ControlCallResult {
billingCall {
await BillingClient.shared.portal()
}
}
private nonisolated func billingCall(_ work: @escaping () async -> JSONValue) -> ControlCallResult {
let semaphore = DispatchSemaphore(value: 0)
nonisolated(unsafe) var payload: JSONValue?
let task = Task {
payload = await work()
semaphore.signal()
}
if semaphore.wait(timeout: .now() + 60) == .timedOut {
task.cancel()
return .ok(.object([
"ok": .bool(false),
"error": .string("timeout"),
]))
}
guard let payload else {
return .ok(.object([
"ok": .bool(false),
"error": .string("malformed_response"),
]))
}
return .ok(payload)
}
}
+1 -1
View File
@@ -2379,7 +2379,7 @@ class TerminalController {
"vm.ssh_info",
"aiAccounts.list",
"aiAccounts.upload",
"aiAccounts.remove",
"aiAccounts.remove", "billing.status", "billing.checkout", "billing.portal",
"window.list",
"window.current",
"window.focus",
+20
View File
@@ -132,6 +132,8 @@
74AB3F34FE3B4974A3A5D264 /* AutoNamingHookPayloadAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46792DAA478444ED87B17B31 /* AutoNamingHookPayloadAdapterTests.swift */; };
C0DE35860000000000000001 /* BackgroundWorkspacePrimeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE35860000000000000002 /* BackgroundWorkspacePrimeCoordinator.swift */; };
A50012F1 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50012F0 /* Backport.swift */; };
B11100000000000000000001 /* BillingClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11100000000000000000002 /* BillingClient.swift */; };
B11100000000000000000003 /* BillingNoRedirectDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11100000000000000000004 /* BillingNoRedirectDelegate.swift */; };
B057B0017E57B0017E57B001 /* Bonsplit in Frameworks */ = {isa = PBXBuildFile; productRef = A5001262 /* Bonsplit */; };
D0B10010A1B2C3D4E5F60001 /* BonsplitTabBarDebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B10011A1B2C3D4E5F60001 /* BonsplitTabBarDebug.swift */; };
D0B10002A1B2C3D4E5F60001 /* BonsplitTabBarPassThrough.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B10003A1B2C3D4E5F60001 /* BonsplitTabBarPassThrough.swift */; };
@@ -340,6 +342,7 @@
6D9C51AB30A64B1ABC819143 /* CMUXCLI+AutoNamingGenericHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5257257034CA4729B1211167 /* CMUXCLI+AutoNamingGenericHooks.swift */; };
6D9C51AB30A64B1ABC819144 /* CMUXCLI+AutoNamingHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5257257034CA4729B1211168 /* CMUXCLI+AutoNamingHooks.swift */; };
6D9C51AB30A64B1ABC819145 /* CMUXCLI+AutoNamingSummarizers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5257257034CA4729B1211169 /* CMUXCLI+AutoNamingSummarizers.swift */; };
B11100000000000000000007 /* CMUXCLI+Billing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11100000000000000000008 /* CMUXCLI+Billing.swift */; };
C72280000000000000000002 /* CMUXCLI+ClaudeHookWorkspaceRouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72280000000000000000001 /* CMUXCLI+ClaudeHookWorkspaceRouting.swift */; };
2DA8D48D99FB520EB179B015 /* CMUXCLI+ClaudePushNotificationHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF59318F1D5B2195AC77C28 /* CMUXCLI+ClaudePushNotificationHook.swift */; };
C0D3F1F00000000000000101 /* CMUXCLI+CodexFireAndForgetHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D3F1F00000000000000102 /* CMUXCLI+CodexFireAndForgetHooks.swift */; };
@@ -352,6 +355,7 @@
B9000052A1B2C3D4E5F60719 /* CMUXCLI+InstallPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000053A1B2C3D4E5F60719 /* CMUXCLI+InstallPreview.swift */; };
489F4CF9B768C42D87B5EB2F /* CMUXCLI+KimiHooks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3D532FF0A00E20DA31667F /* CMUXCLI+KimiHooks.swift */; };
B9000071A1B2C3D4E5F60719 /* CMUXCLI+Memory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000070A1B2C3D4E5F60719 /* CMUXCLI+Memory.swift */; };
B11100000000000000000107 /* CMUXCLI+MiscUsage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11100000000000000000108 /* CMUXCLI+MiscUsage.swift */; };
D7AB0000000000000000000D /* CMUXCLI+MoveTabToNewWorkspace.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB0000000000000000000E /* CMUXCLI+MoveTabToNewWorkspace.swift */; };
B9000072A1B2C3D4E5F60719 /* CMUXCLI+OmpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9000073A1B2C3D4E5F60719 /* CMUXCLI+OmpExtension.swift */; };
C05555010000000000000001 /* CMUXCLI+PiExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05555010000000000000002 /* CMUXCLI+PiExtension.swift */; };
@@ -1229,6 +1233,7 @@
E4D1768B7041CDE4F9A084A4 /* TerminalClearScreenKeepScrollbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2B7555E04A04849992547A2 /* TerminalClearScreenKeepScrollbackTests.swift */; };
C2577000A1B2C3D4E5F60718 /* TerminalCmdClickUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2577001A1B2C3D4E5F60718 /* TerminalCmdClickUITests.swift */; };
C0DE00000000000000000C42 /* TerminalController+ControlAppFocusContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE00000000000000000C41 /* TerminalController+ControlAppFocusContext.swift */; };
B11100000000000000000005 /* TerminalController+ControlBillingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = B11100000000000000000006 /* TerminalController+ControlBillingContext.swift */; };
C0DE00000000000000000C82 /* TerminalController+ControlBrowserPanelContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE00000000000000000C81 /* TerminalController+ControlBrowserPanelContext.swift */; };
CA52E0020000000000000000 /* TerminalController+ControlCanvasContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA52F0020000000000000000 /* TerminalController+ControlCanvasContext.swift */; };
C0DE00000000000000000C6E /* TerminalController+ControlDebugContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE00000000000000000C6D /* TerminalController+ControlDebugContext.swift */; };
@@ -1677,6 +1682,8 @@
46792DAA478444ED87B17B31 /* AutoNamingHookPayloadAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoNamingHookPayloadAdapterTests.swift; sourceTree = "<group>"; };
C0DE35860000000000000002 /* BackgroundWorkspacePrimeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorkspacePrimeCoordinator.swift; sourceTree = "<group>"; };
A50012F0 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
B11100000000000000000002 /* BillingClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BillingClient.swift; sourceTree = "<group>"; };
B11100000000000000000004 /* BillingNoRedirectDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BillingNoRedirectDelegate.swift; sourceTree = "<group>"; };
D0B10011A1B2C3D4E5F60001 /* BonsplitTabBarDebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BonsplitTabBarDebug.swift; sourceTree = "<group>"; };
D0B10003A1B2C3D4E5F60001 /* BonsplitTabBarPassThrough.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BonsplitTabBarPassThrough.swift; sourceTree = "<group>"; };
AA1B2C3D4E5F60719 /* BonsplitTabDragUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BonsplitTabDragUITests.swift; sourceTree = "<group>"; };
@@ -1872,6 +1879,7 @@
5257257034CA4729B1211167 /* CMUXCLI+AutoNamingGenericHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+AutoNamingGenericHooks.swift"; sourceTree = "<group>"; };
5257257034CA4729B1211168 /* CMUXCLI+AutoNamingHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+AutoNamingHooks.swift"; sourceTree = "<group>"; };
5257257034CA4729B1211169 /* CMUXCLI+AutoNamingSummarizers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+AutoNamingSummarizers.swift"; sourceTree = "<group>"; };
B11100000000000000000008 /* CMUXCLI+Billing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+Billing.swift"; sourceTree = "<group>"; };
C72280000000000000000001 /* CMUXCLI+ClaudeHookWorkspaceRouting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+ClaudeHookWorkspaceRouting.swift"; sourceTree = "<group>"; };
4CF59318F1D5B2195AC77C28 /* CMUXCLI+ClaudePushNotificationHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+ClaudePushNotificationHook.swift"; sourceTree = "<group>"; };
C0D3F1F00000000000000102 /* CMUXCLI+CodexFireAndForgetHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+CodexFireAndForgetHooks.swift"; sourceTree = "<group>"; };
@@ -1884,6 +1892,7 @@
B9000053A1B2C3D4E5F60719 /* CMUXCLI+InstallPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+InstallPreview.swift"; sourceTree = "<group>"; };
7A3D532FF0A00E20DA31667F /* CMUXCLI+KimiHooks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+KimiHooks.swift"; sourceTree = "<group>"; };
B9000070A1B2C3D4E5F60719 /* CMUXCLI+Memory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+Memory.swift"; sourceTree = "<group>"; };
B11100000000000000000108 /* CMUXCLI+MiscUsage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+MiscUsage.swift"; sourceTree = "<group>"; };
D7AB0000000000000000000E /* CMUXCLI+MoveTabToNewWorkspace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+MoveTabToNewWorkspace.swift"; sourceTree = "<group>"; };
B9000073A1B2C3D4E5F60719 /* CMUXCLI+OmpExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+OmpExtension.swift"; sourceTree = "<group>"; };
C05555010000000000000002 /* CMUXCLI+PiExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMUXCLI+PiExtension.swift"; sourceTree = "<group>"; };
@@ -2704,6 +2713,7 @@
F2B7555E04A04849992547A2 /* TerminalClearScreenKeepScrollbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalClearScreenKeepScrollbackTests.swift; sourceTree = "<group>"; };
C2577001A1B2C3D4E5F60718 /* TerminalCmdClickUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalCmdClickUITests.swift; sourceTree = "<group>"; };
C0DE00000000000000000C41 /* TerminalController+ControlAppFocusContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TerminalController+ControlAppFocusContext.swift"; sourceTree = "<group>"; };
B11100000000000000000006 /* TerminalController+ControlBillingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TerminalController+ControlBillingContext.swift"; sourceTree = "<group>"; };
C0DE00000000000000000C81 /* TerminalController+ControlBrowserPanelContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TerminalController+ControlBrowserPanelContext.swift"; sourceTree = "<group>"; };
CA52F0020000000000000000 /* TerminalController+ControlCanvasContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TerminalController+ControlCanvasContext.swift"; sourceTree = "<group>"; };
C0DE00000000000000000C6D /* TerminalController+ControlDebugContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TerminalController+ControlDebugContext.swift"; sourceTree = "<group>"; };
@@ -3195,6 +3205,8 @@
9CEC6EF35B71AE59FA45BA69 /* VMClient.swift */,
REE0CA0000000000000000E1 /* AIAccountsClient.swift */,
REE0CA0000000000000000F1 /* AIAccountCredentialSources.swift */,
B11100000000000000000002 /* BillingClient.swift */,
B11100000000000000000004 /* BillingNoRedirectDelegate.swift */,
REE0CA0000000000000000A1 /* RemotesClient.swift */,
REE0CA0000000000000000D1 /* RemoteRouteSpec.swift */,
D1F0A00100000000000000A2 /* PhonePushClient.swift */,
@@ -3591,6 +3603,7 @@
C0DE00000000000000000C45 /* TerminalController+ControlNotificationContext.swift */,
C0DE1A040000000000000002 /* TerminalController+ControlLayoutContext.swift */,
C0DE00000000000000000C61 /* TerminalController+ControlWorkspaceContext.swift */,
B11100000000000000000006 /* TerminalController+ControlBillingContext.swift */,
D7AB00000000000000B000 /* TerminalController+MobileNotificationSync.swift */,
D7AB00000000000000B010 /* TerminalController+MobileScrollPrefetch.swift */,
C0DE00000000000000000C81 /* TerminalController+ControlBrowserPanelContext.swift */,
@@ -4109,6 +4122,8 @@
children = (
B9000001A1B2C3D4E5F60719 /* cmux.swift */,
REE0CA0000000000000000C1 /* CMUXCLI+Remotes.swift */,
B11100000000000000000008 /* CMUXCLI+Billing.swift */,
B11100000000000000000108 /* CMUXCLI+MiscUsage.swift */,
1E4DB33FA55F1B13C60EFFC8 /* SSHPTYAttachReconnectInputFilter.swift */,
E60610200000000000000001 /* SSHPTYAttachReconnectInputFilterControl.swift */,
E60610100000000000000001 /* SSHPTYAttachReconnectInputFilterSequenceMatch.swift */,
@@ -5066,6 +5081,8 @@
D9FEC58D5BACCF76459F1BBE /* AuthEnvironment.swift in Sources */,
C0DE35860000000000000001 /* BackgroundWorkspacePrimeCoordinator.swift in Sources */,
A50012F1 /* Backport.swift in Sources */,
B11100000000000000000001 /* BillingClient.swift in Sources */,
B11100000000000000000003 /* BillingNoRedirectDelegate.swift in Sources */,
D0B10010A1B2C3D4E5F60001 /* BonsplitTabBarDebug.swift in Sources */,
D0B10002A1B2C3D4E5F60001 /* BonsplitTabBarPassThrough.swift in Sources */,
D7032A070000000000000001 /* BrowserAuthPromptTextFormatter.swift in Sources */,
@@ -5699,6 +5716,7 @@
C7A502000000000000000002 /* TaskManagerWindowController.swift in Sources */,
7490C00F7490C00F7490C00F /* TaskVMInfoMemoryPressureFootprintSampler.swift in Sources */,
C0DE00000000000000000C42 /* TerminalController+ControlAppFocusContext.swift in Sources */,
B11100000000000000000005 /* TerminalController+ControlBillingContext.swift in Sources */,
C0DE00000000000000000C82 /* TerminalController+ControlBrowserPanelContext.swift in Sources */,
CA52E0020000000000000000 /* TerminalController+ControlCanvasContext.swift in Sources */,
C0DE00000000000000000C6E /* TerminalController+ControlDebugContext.swift in Sources */,
@@ -5907,6 +5925,7 @@
6D9C51AB30A64B1ABC819143 /* CMUXCLI+AutoNamingGenericHooks.swift in Sources */,
6D9C51AB30A64B1ABC819144 /* CMUXCLI+AutoNamingHooks.swift in Sources */,
6D9C51AB30A64B1ABC819145 /* CMUXCLI+AutoNamingSummarizers.swift in Sources */,
B11100000000000000000007 /* CMUXCLI+Billing.swift in Sources */,
C72280000000000000000002 /* CMUXCLI+ClaudeHookWorkspaceRouting.swift in Sources */,
2DA8D48D99FB520EB179B015 /* CMUXCLI+ClaudePushNotificationHook.swift in Sources */,
C0D3F1F00000000000000101 /* CMUXCLI+CodexFireAndForgetHooks.swift in Sources */,
@@ -5919,6 +5938,7 @@
B9000052A1B2C3D4E5F60719 /* CMUXCLI+InstallPreview.swift in Sources */,
489F4CF9B768C42D87B5EB2F /* CMUXCLI+KimiHooks.swift in Sources */,
B9000071A1B2C3D4E5F60719 /* CMUXCLI+Memory.swift in Sources */,
B11100000000000000000107 /* CMUXCLI+MiscUsage.swift in Sources */,
D7AB0000000000000000000D /* CMUXCLI+MoveTabToNewWorkspace.swift in Sources */,
B9000072A1B2C3D4E5F60719 /* CMUXCLI+OmpExtension.swift in Sources */,
C05555010000000000000001 /* CMUXCLI+PiExtension.swift in Sources */,