mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
The library the consolidated CLI is built on: - bundle/library model (VPhoneBundle, VPhoneLibrary, VPhoneVirtualMachineManifest), bundle ops + reporting, restore helpers (VPhoneRestoreOps) - host process primitives: VPhoneProcessRunner, VPhoneManagedProcess (spawn + stdout pattern-match + SIGKILL-escalating terminate), VPhoneLaunchLayout, VPhoneBootPatterns - VPhoneResources: bundled-.app vs dev asset resolution, and Python resolution that provisions a per-user venv (~/.vphone/venv) on demand so the app is portable — never depends on the repo's .venv - VPhoneVerbosity (quiet/info/debug/trace) and VPhoneVMPicker Manifest moves out of the executable target into VPhoneCore. Full unit-test suite under tests/VPhoneCoreTests. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01Y4VDqWf5pVakcFLqB23CKe
56 lines
2.1 KiB
Swift
56 lines
2.1 KiB
Swift
import Foundation
|
|
|
|
public enum VPhoneVMPickerError: Error, CustomStringConvertible, Equatable {
|
|
case emptyLibrary(root: String)
|
|
case notInteractive
|
|
case aborted
|
|
case invalidSelection
|
|
|
|
public var description: String {
|
|
switch self {
|
|
case .emptyLibrary(let root):
|
|
"no VMs found in \(root) — create one with 'vphone-cli vm create <name>'"
|
|
case .notInteractive:
|
|
"no VM name given and stdin is not a terminal — pass the name explicitly"
|
|
case .aborted:
|
|
"selection aborted"
|
|
case .invalidSelection:
|
|
"no valid selection made"
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum VPhoneVMPicker {
|
|
/// Resolve an existing-VM name. Returns `provided` unchanged when given;
|
|
/// otherwise shows a numbered menu of `names` and reads a 1-based index or an
|
|
/// exact name. All output goes through `write` (caller routes to stderr); input
|
|
/// via `read` (nil = EOF). I/O is injected so the logic is unit-testable.
|
|
public static func resolve(
|
|
provided: String?,
|
|
names: [String],
|
|
libraryRoot: String,
|
|
isInteractive: Bool,
|
|
maxRetries: Int = 5,
|
|
read: () -> String?,
|
|
write: (String) -> Void
|
|
) throws -> String {
|
|
if let provided, !provided.isEmpty { return provided }
|
|
guard isInteractive else { throw VPhoneVMPickerError.notInteractive }
|
|
guard !names.isEmpty else { throw VPhoneVMPickerError.emptyLibrary(root: libraryRoot) }
|
|
|
|
write("Select a VM:")
|
|
for (i, name) in names.enumerated() { write(" [\(i + 1)] \(name)") }
|
|
|
|
for _ in 0..<maxRetries {
|
|
write("Enter number or name: ")
|
|
guard let line = read() else { throw VPhoneVMPickerError.aborted }
|
|
let choice = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if choice.isEmpty { continue }
|
|
if let idx = Int(choice), idx >= 1, idx <= names.count { return names[idx - 1] }
|
|
if let match = names.first(where: { $0 == choice }) { return match }
|
|
write(" '\(choice)' is not a valid selection.")
|
|
}
|
|
throw VPhoneVMPickerError.invalidSelection
|
|
}
|
|
}
|