diff --git a/src/WinDroid.Adb/Services/AdbDeviceService.cs b/src/WinDroid.Adb/Services/AdbDeviceService.cs
new file mode 100644
index 0000000..6eb5a31
--- /dev/null
+++ b/src/WinDroid.Adb/Services/AdbDeviceService.cs
@@ -0,0 +1,65 @@
+using WinDroid.Adb.Models;
+
+namespace WinDroid.Adb.Services;
+
+///
+/// Runs the adb devices command by delegating to .
+///
+///
+/// This wrapper only launches the command and returns the raw
+/// . It does not resolve the ADB path (the caller
+/// supplies an already-resolved path, typically from
+/// ) and it does not parse the output. Parsing of
+/// the device listing is intentionally left to a later stage.
+///
+public sealed class AdbDeviceService
+{
+ private readonly ProcessRunner _processRunner;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The process runner used to execute ADB.
+ ///
+ /// is .
+ ///
+ public AdbDeviceService(ProcessRunner processRunner)
+ {
+ ArgumentNullException.ThrowIfNull(processRunner);
+
+ _processRunner = processRunner;
+ }
+
+ ///
+ /// Runs adb devices using the supplied ADB executable path and returns
+ /// the raw result.
+ ///
+ ///
+ /// Path to the ADB executable, already resolved by the caller. It is passed
+ /// through unchanged; this method does not revalidate or normalize it.
+ ///
+ ///
+ /// Optional maximum run time, forwarded to .
+ /// means no timeout.
+ ///
+ ///
+ /// Token forwarded to to cancel execution.
+ ///
+ ///
+ /// The unmodified produced by
+ /// , including its output, exit code, and timeout
+ /// or cancellation state.
+ ///
+ ///
+ /// is , empty, or whitespace.
+ ///
+ public Task GetDevicesAsync(
+ string adbPath,
+ TimeSpan? timeout = null,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(adbPath);
+
+ return _processRunner.RunAsync(adbPath, ["devices"], timeout, cancellationToken);
+ }
+}