From a64a3093cfd9f00b39659e43a24643e856f45837 Mon Sep 17 00:00:00 2001 From: Micah Alpern Date: Mon, 8 Jun 2026 16:11:54 -0700 Subject: [PATCH] Extract CLI support target --- Package.swift | 12 ++++++++ .../Commands/CompletionsCommand.swift | 1 + .../Service/ServiceStatusCommand.swift | 1 + Sources/KeyPathCLI/KeyPathTool.swift | 2 +- Sources/KeyPathCLI/Utilities/Output.swift | 2 +- .../KeyPathCLI/Utilities/UpdateChecker.swift | 6 ++-- .../CLISupport.swift} | 0 docs/TEST_HYGIENE_PLAN.md | 30 +++++++++++++++---- 8 files changed, 43 insertions(+), 11 deletions(-) rename Sources/{KeyPathAppKit/CLI/CLIFacade.swift => KeyPathCLISupport/CLISupport.swift} (100%) diff --git a/Package.swift b/Package.swift index e87f9970e..680ed09bc 100644 --- a/Package.swift +++ b/Package.swift @@ -61,6 +61,10 @@ let package = Package( name: "KeyPathPluginKit", targets: ["KeyPathPluginKit"] ), + .library( + name: "KeyPathCLISupport", + targets: ["KeyPathCLISupport"] + ), .executable( name: "keypath-cli", targets: ["KeyPathCLIMain"] @@ -123,6 +127,13 @@ let package = Package( .swiftLanguageMode(.v6) ] ), + .target( + name: "KeyPathCLISupport", + path: "Sources/KeyPathCLISupport", + swiftSettings: [ + .swiftLanguageMode(.v6) + ] + ), // Installation wizard (extracted from KeyPathAppKit for incremental compilation) .target( name: "KeyPathInstallationWizard", @@ -246,6 +257,7 @@ let package = Package( .target( name: "KeyPathCLI", dependencies: [ + "KeyPathCLISupport", "KeyPathAppKit", .product(name: "ArgumentParser", package: "swift-argument-parser") ], diff --git a/Sources/KeyPathCLI/Commands/CompletionsCommand.swift b/Sources/KeyPathCLI/Commands/CompletionsCommand.swift index a6db27554..bcd2b3f0e 100644 --- a/Sources/KeyPathCLI/Commands/CompletionsCommand.swift +++ b/Sources/KeyPathCLI/Commands/CompletionsCommand.swift @@ -1,6 +1,7 @@ import ArgumentParser import Foundation import KeyPathAppKit +import KeyPathCLISupport struct Completions: ParsableCommand { static let configuration = CommandConfiguration( diff --git a/Sources/KeyPathCLI/Commands/Service/ServiceStatusCommand.swift b/Sources/KeyPathCLI/Commands/Service/ServiceStatusCommand.swift index 608c35182..ab61a3899 100644 --- a/Sources/KeyPathCLI/Commands/Service/ServiceStatusCommand.swift +++ b/Sources/KeyPathCLI/Commands/Service/ServiceStatusCommand.swift @@ -1,6 +1,7 @@ import ArgumentParser import Foundation import KeyPathAppKit +import KeyPathCLISupport struct ServiceStatus: AsyncParsableCommand { static let configuration = CommandConfiguration( diff --git a/Sources/KeyPathCLI/KeyPathTool.swift b/Sources/KeyPathCLI/KeyPathTool.swift index 9ec75e9bc..152cb3fcd 100644 --- a/Sources/KeyPathCLI/KeyPathTool.swift +++ b/Sources/KeyPathCLI/KeyPathTool.swift @@ -1,6 +1,6 @@ import ArgumentParser import Foundation -import KeyPathAppKit +import KeyPathCLISupport public struct KeyPathCLI: AsyncParsableCommand { public init() {} diff --git a/Sources/KeyPathCLI/Utilities/Output.swift b/Sources/KeyPathCLI/Utilities/Output.swift index f1811f8b3..8fbf68bc4 100644 --- a/Sources/KeyPathCLI/Utilities/Output.swift +++ b/Sources/KeyPathCLI/Utilities/Output.swift @@ -1,5 +1,5 @@ import Foundation -import KeyPathAppKit +import KeyPathCLISupport public struct OutputContext: Sendable { public let isInteractive: Bool diff --git a/Sources/KeyPathCLI/Utilities/UpdateChecker.swift b/Sources/KeyPathCLI/Utilities/UpdateChecker.swift index 2fd284960..eb2419ac7 100644 --- a/Sources/KeyPathCLI/Utilities/UpdateChecker.swift +++ b/Sources/KeyPathCLI/Utilities/UpdateChecker.swift @@ -1,11 +1,9 @@ import Foundation -import KeyPathAppKit +import KeyPathCLISupport import KeyPathCore enum UpdateChecker { - private static let cacheFile: String = { - "\(KeyPathConstants.Config.directory)/.update-check" - }() + private static let cacheFile: String = "\(KeyPathConstants.Config.directory)/.update-check" private static let checkInterval: TimeInterval = 86400 // 24 hours diff --git a/Sources/KeyPathAppKit/CLI/CLIFacade.swift b/Sources/KeyPathCLISupport/CLISupport.swift similarity index 100% rename from Sources/KeyPathAppKit/CLI/CLIFacade.swift rename to Sources/KeyPathCLISupport/CLISupport.swift diff --git a/docs/TEST_HYGIENE_PLAN.md b/docs/TEST_HYGIENE_PLAN.md index bdf3d1c9d..3970046cb 100644 --- a/docs/TEST_HYGIENE_PLAN.md +++ b/docs/TEST_HYGIENE_PLAN.md @@ -481,6 +481,16 @@ Status: types and command-parse/output-contract tests; then move storage/config/packs facades only when their dependencies can move with them. Keep installer and simulator facades AppKit-backed until a measured lane shows they dominate. +- Started the extraction by creating a pure `KeyPathCLISupport` target and + moving `CLIVersion` plus `printErr` out of `KeyPathAppKit`. This is a + foothold only: it validates the new module boundary, but `KeyPathCLI` still + depends on `KeyPathAppKit`. +- Post-extraction validation: + - `swift build --target KeyPathCLISupport` passed in 1.33s; + - warm `swift build --product keypath-cli` passed in 85.79s and still + compiled the AppKit graph; + - `./Scripts/test-lane.sh cli` passed 341 tests with build=131s, test=3s, + total=135s, zero Swift warnings, zero app warnings, and zero app errors. - Do not split installer/wizard targets further unless a lane timing run shows they dominate a workflow we care about. @@ -638,6 +648,14 @@ Milestone 7 now has enough evidence to keep the bounded isolated Core lane: models, schema/version helpers, and low-level facade logic; command parsing tests can move there first. AppKit-backed installer/simulator/system facades should move later, only with dependency evidence. +- First extraction checkpoint: `KeyPathCLISupport` now owns pure CLI support + helpers (`CLIVersion`, `printErr`) and builds independently in 1.33s. This + does not yet make the CLI lane build-isolated; the warm CLI lane still spent + 131s in prebuild and passed 341 tests. Stop the CLI/AppKit extraction here + for this hygiene phase: moving result models, output contracts, command-parse + tests, and facade logic would be architectural refactoring with a narrower + payoff. Revisit only when CLI-heavy work becomes frequent enough to justify a + dedicated architecture milestone. Because the root-package build dominates this lane, keep the current filter and treat `unit` as fast model/parser/renderer coverage, not true Core isolation. The `core-isolated` lane remains the true Core-only fast path. @@ -687,8 +705,10 @@ The Mac mini workflow is deferred. Revisit it only after the MacBook Air loop is fast and boring enough that remote execution would solve a measured capacity problem instead of compensating for harness noise. -Next planned milestone: treat the current lane set as the stable local loop and -watch for regressions. CLI/AppKit extraction is worth revisiting only if a -measured workflow needs true build isolation; the current `cli` lane already -gives a fast, stable selection path. Installer/wizard splits should follow only -after the remaining lane timings justify the extra dependency work. +Next planned milestone: stop active test-hygiene performance work and treat the +current lane set as the stable local loop. Watch for regressions in warning +counts, log size, and lane timing while doing real product work. CLI/AppKit +extraction is worth revisiting only if a measured workflow needs true build +isolation; the current `cli` lane already gives a stable selection path. +Installer/wizard splits should follow only after lane timings justify the extra +dependency work.