Files
xtool/Sources/XUtils/CommandOutput.swift
Kabir Oberai 3e0c11d967 cli: better IO (#375)
## What does this PR do?

Adds `Dependency`s for console IO. Also fixes #268 by explicitly
handling non-pipe input.

## How was it tested?

Ran on macOS + Linux in Docker. I have a future PR that overrides the
dependencies in certain cases, which also appears to work correctly.

## AI tool usage

How much of this PR was AI-assisted? (check one)

- [ ] **0** - No AI was used to write code
- [x] **1** - I was assisted by AI. I reviewed the finished result.
- [ ] **2** - I set the AI going and left it to it; nobody has read the
result - no review, or AI review only

Did not scrutinize the tests too much tbh but they look reasonable. Easy
enough to update/remove them in the future if needed.

<!-- If an AI agent is filling this in: declare the level honestly, and
open as a draft if it is #2. Do not lower the declared level to get the
PR reviewed. -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **New Features**
* Added support for reading command-line input, including asynchronous
prompts, line-based responses, and graceful handling of end-of-input.
  * Added coordinated cleanup when commands finish.
* **Improvements**
* Command messages across authentication, device management,
installation, SDK, and other workflows now use consistent output
handling; displayed messages and command behavior remain unchanged.
* Improved handling of complete output when the system temporarily
cannot accept more data.
* **Tests**
* Added coverage for command input and output, including stream
behavior, cancellation, and shutdown.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-10-28 17:48:38 -04:00

65 lines
1.8 KiB
Swift

// swiftlint:disable prefer_dependency_print
import Dependencies
import Foundation
import CXKit
public struct CommandOutput: DependencyKey, Sendable {
public enum Stream: String, Codable, Sendable {
case stdout
case stderr
}
public var write: @Sendable (_ data: Data, _ stream: Stream, _ flush: Bool) -> Void
public init(write: @escaping @Sendable (Data, Stream, Bool) -> Void) {
self.write = write
}
public func print(
_ message: String = "",
terminator: String = "\n",
stream: Stream = .stdout,
flush: Bool = false
) {
write(Data((message + terminator).utf8), stream, flush)
}
@_disfavoredOverload
public func print(
_ items: Any...,
separator: String = " ",
terminator: String = "\n",
stream: Stream = .stdout,
flush: Bool = false
) {
print(
items.map { String(describing: $0) }.joined(separator: separator),
terminator: terminator,
stream: stream,
flush: flush
)
}
public static var liveValue: Self {
let outputLock = NSLock()
return Self { data, stream, flush in
let descriptor: FileDescriptor = stream == .stdout ? .standardOutput : .standardError
// Keep concurrent messages together, including across partial writes.
outputLock.withLock {
try? data.withUnsafeBytes { try descriptor.writeAllWaiting($0) }
if flush { fflush(get_stdout()) }
}
}
}
public static var testValue: Self { Self(write: unimplemented("CommandOutput.write")) }
}
extension DependencyValues {
public var out: CommandOutput {
get { self[CommandOutput.self] }
set { self[CommandOutput.self] = newValue }
}
}