Skip to content

Add AvdManagerRunner for avdmanager CLI operations#282

Open
rmarinho wants to merge 37 commits intomainfrom
feature/avdmanager-runner
Open

Add AvdManagerRunner for avdmanager CLI operations#282
rmarinho wants to merge 37 commits intomainfrom
feature/avdmanager-runner

Conversation

@rmarinho
Copy link
Member

@rmarinho rmarinho commented Feb 23, 2026

Summary

Wraps avdmanager CLI operations for programmatic AVD management. Addresses #277.

Changes

New constructor overload with JDK path

public AvdManagerRunner (Func<string?> getSdkPath, Func<string?>? getJdkPath)

Sets JAVA_HOME and ANDROID_HOME on all child process ProcessStartInfo via ConfigureEnvironment().
The existing 1-arg constructor remains backward compatible.

New: CreateAvdAsync

  • Duplicate detection: checks ListAvdsAsync first, returns existing AVD if name matches
  • Orphaned AVD handling: detects .avd directory without .ini registration and auto-applies --force
  • stdin handling: answers "no" to avdmanager hardware profile customization prompt
  • Error reporting: captures stdout/stderr and throws InvalidOperationException on failure

New: ParseAvdListOutput (internal static)

Extracted from ListAvdsAsync for testability.

Updated: ListAvdsAsync / DeleteAvdAsync

Now call ConfigureEnvironment(psi) before ProcessUtils.StartProcess to pass JAVA_HOME.

Why

avdmanager.bat requires JAVA_HOME to locate the JVM. Without it, operations fail silently on Windows when the system JAVA_HOME is stale or missing.

Copilot AI review requested due to automatic review settings February 23, 2026 17:39
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “runner” layer to Xamarin.Android.Tools.AndroidSdk to execute Android SDK command-line tools programmatically, with an initial AvdManagerRunner implementation for creating/listing/deleting AVDs.

Changes:

  • Introduces AndroidToolRunner for running SDK tools with timeout/stdout/stderr capture (sync + async) and a background-start helper.
  • Adds AvdManagerRunner to locate and invoke avdmanager, parse list avd output, and enrich results from config.ini.
  • Adds supporting models (ToolRunnerResult, AvdInfo) and environment setup utilities (AndroidEnvironmentHelper).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Adds an avdmanager wrapper with list/create/delete and output parsing/enrichment.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Adds process execution utilities (sync/async + background start).
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Provides JAVA_HOME / ANDROID_HOME setup plus ABI/API/tag mapping helpers.
src/Xamarin.Android.Tools.AndroidSdk/Models/ToolRunnerResult.cs Introduces a result type for tool execution (typed + untyped).
src/Xamarin.Android.Tools.AndroidSdk/Models/AvdInfo.cs Adds an AVD info model used by AvdManagerRunner.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@rmarinho rmarinho added the copilot `copilot-cli` or other AIs were used to author this label Feb 23, 2026
@jonathanpeppers
Copy link
Member

I'd like to get the System.Diagnostics.Process code unified like mentioned here:

rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #282 feedback to use existing ProcessUtils instead of
the removed AndroidToolRunner. Simplifies API:

- Methods now throw InvalidOperationException on failure instead of
  returning result types with error states
- Uses ProcessUtils.RunToolAsync() for all tool invocations
- Simplified AvdInfo model
- Removed complex ToolRunnerResult<T> wrapper types

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho rmarinho force-pushed the feature/avdmanager-runner branch 6 times, most recently from b286a7a to 6e09e61 Compare March 3, 2026 13:16
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

Comment on lines +108 to +113
// Detect orphaned AVD directory (folder exists without .ini registration).
var avdDir = Path.Combine (
Environment.GetFolderPath (Environment.SpecialFolder.UserProfile),
".android", "avd", $"{name}.avd");
if (Directory.Exists (avdDir))
force = true;
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateAvdAsync hard-codes the AVD directory as $HOME/.android/avd/<name>.avd for orphan detection and for the returned AvdInfo.Path. avdmanager can place AVDs elsewhere when ANDROID_USER_HOME/ANDROID_AVD_HOME are set, so this can incorrectly force --force and/or return the wrong path. Consider deriving the AVD location from the tool output (or re-list after creation and return the matching entry) and basing orphan detection on the same resolved location/env vars.

Copilot uses AI. Check for mistakes.
Comment on lines +152 to +158
public async Task DeleteAvdAsync (string name, CancellationToken cancellationToken = default)
{
var avdManagerPath = RequireAvdManagerPath ();

using var stderr = new StringWriter ();
var psi = ProcessUtils.CreateProcessStartInfo (avdManagerPath, "delete", "avd", "--name", name);
ConfigureEnvironment (psi);
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DeleteAvdAsync doesn't validate name (null/empty/whitespace). Passing an empty name will invoke avdmanager with an invalid --name value and can lead to confusing errors; please add the same argument validation used in CreateAvdAsync.

Copilot uses AI. Check for mistakes.
rmarinho added a commit that referenced this pull request Mar 3, 2026
- Fix argument validation: CreateAvdAsync/DeleteAvdAsync now throw
  ArgumentException for empty strings, ArgumentNullException for null,
  and validate before RequireAvdManagerPath()
- Make AndroidEnvironmentHelper internal, remove unused GetToolEnvironment
- Add ANDROID_USER_HOME to ConfigureEnvironment for consistent AVD
  location across tools (matches SdkManager behavior)
- Add AndroidUserHome constant to EnvironmentVariableNames
- Fix hard-coded AVD directory: use GetAvdRootDirectory() that respects
  ANDROID_AVD_HOME and ANDROID_USER_HOME env vars
- Re-list after CreateAvdAsync to get actual path from avdmanager
- Add 5 new validation tests for Create/DeleteAvdAsync

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit that referenced this pull request Mar 3, 2026
- Fix argument validation: CreateAvdAsync/DeleteAvdAsync now throw
  ArgumentException for empty strings, ArgumentNullException for null,
  and validate before RequireAvdManagerPath()
- Make AndroidEnvironmentHelper internal, remove unused GetToolEnvironment
- Add ANDROID_USER_HOME to ConfigureEnvironment for consistent AVD
  location across tools (matches SdkManager behavior)
- Add AndroidUserHome constant to EnvironmentVariableNames
- Fix hard-coded AVD directory: use GetAvdRootDirectory() that respects
  ANDROID_AVD_HOME and ANDROID_USER_HOME env vars
- Re-list after CreateAvdAsync to get actual path from avdmanager
- Add 5 new validation tests for Create/DeleteAvdAsync

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho rmarinho force-pushed the feature/avdmanager-runner branch from 4c8ce13 to 3a788bb Compare March 3, 2026 18:23
Copy link
Member

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI Review Summary

Found 5 issues: 1 security concern, 1 error handling gap, 1 consistency gap, 1 missing constant, 1 convention violation.

  • Security / API design: additionalArgs as single string risks injection and breaks multi-arg callers (EmulatorRunner.cs:57)
  • Error handling: Bare catch block silently swallows exceptions (AdbRunner.cs:106)
  • Consistency: Exit code not checked in ListDevicesAsync / ListAvdNamesAsync while other methods do (AdbRunner.cs:71)
  • Naming: Raw "ANDROID_AVD_HOME" string instead of EnvironmentVariableNames constant (AvdManagerRunner.cs:212)
  • Code organization: Three types in one file (AdbDeviceInfo.cs)

👍 Excellent test coverage with thorough parsing tests. Good CancellationToken propagation throughout, correct OperationCanceledException rethrow. AndroidEnvironmentHelper is a clean factoring of shared env setup. Orphaned AVD detection in CreateAvdAsync is a smart touch.


Review generated by android-tools-reviewer from review guidelines by @jonathanpeppers.

AndroidEnvironmentHelper.ConfigureEnvironment (psi, getSdkPath (), getJdkPath?.Invoke ());
}

public Process StartAvd (string avdName, bool coldBoot = false, string? additionalArgs = null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Security / API designstring? additionalArgs is a command injection risk and a correctness bug. If a caller passes "-no-window -no-audio", it becomes a single argument to the emulator (not two), silently breaking. Change to IEnumerable<string>? additionalArgs and use args.AddRange(additionalArgs) so each flag is a separate argument handled safely by ProcessUtils.CreateProcessStartInfo's ArgumentList.

Rule: Structured args, not string interpolation (Postmortem #50)

}
} catch (OperationCanceledException) {
throw;
} catch {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Error handling — This bare catch block swallows all exceptions without logging. Even if failures are expected (emulator may not support the command), the exception should be captured and logged for diagnostics. At minimum: catch (Exception ex) { /* log ex */ }. Silent swallowing makes debugging null AVD names nearly impossible.

Rule: No empty catch blocks (Postmortem #11)

var adb = RequireAdb ();
using var stdout = new StringWriter ();
var psi = CreateAdbProcess (adb, "devices", "-l");
await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 ConsistencyListDevicesAsync discards the exit code from ProcessUtils.StartProcess, but AvdManagerRunner.ListAvdsAsync and DeleteAvdAsync both check it and throw on failure. Same gap in EmulatorRunner.ListAvdNamesAsync. If adb devices fails (e.g., daemon not running), this silently returns an empty list instead of surfacing the error.

Rule: Check exit codes consistently (Postmortem #48)

{
// ANDROID_AVD_HOME takes highest priority
var avdHome = Environment.GetEnvironmentVariable ("ANDROID_AVD_HOME");
if (!string.IsNullOrEmpty (avdHome))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Naming — Raw "ANDROID_AVD_HOME" string literal. The PR already adds EnvironmentVariableNames.AndroidUserHome — add an AndroidAvdHome constant too and use it here. Typos in env var names cause silent, hard-to-debug failures.

Rule: Environment variable constants (Postmortem #18)

/// <summary>
/// Represents the type of an Android device.
/// </summary>
public enum AdbDeviceType
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Code organization — Three public types (AdbDeviceType, AdbDeviceStatus, AdbDeviceInfo) in one file. Per repo conventions, each public type should be in its own .cs file named after the type: AdbDeviceType.cs, AdbDeviceStatus.cs, AdbDeviceInfo.cs.

Rule: One type per file (Postmortem #9)

rmarinho added a commit that referenced this pull request Mar 5, 2026
- Fix argument validation: CreateAvdAsync/DeleteAvdAsync now throw
  ArgumentException for empty strings, ArgumentNullException for null,
  and validate before RequireAvdManagerPath()
- Make AndroidEnvironmentHelper internal, remove unused GetToolEnvironment
- Add ANDROID_USER_HOME to ConfigureEnvironment for consistent AVD
  location across tools (matches SdkManager behavior)
- Add AndroidUserHome constant to EnvironmentVariableNames
- Fix hard-coded AVD directory: use GetAvdRootDirectory() that respects
  ANDROID_AVD_HOME and ANDROID_USER_HOME env vars
- Re-list after CreateAvdAsync to get actual path from avdmanager
- Add 5 new validation tests for Create/DeleteAvdAsync

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit that referenced this pull request Mar 5, 2026
- Fix argument validation: CreateAvdAsync/DeleteAvdAsync now throw
  ArgumentException for empty strings, ArgumentNullException for null,
  and validate before RequireAvdManagerPath()
- Make AndroidEnvironmentHelper internal, remove unused GetToolEnvironment
- Add ANDROID_USER_HOME to ConfigureEnvironment for consistent AVD
  location across tools (matches SdkManager behavior)
- Add AndroidUserHome constant to EnvironmentVariableNames
- Fix hard-coded AVD directory: use GetAvdRootDirectory() that respects
  ANDROID_AVD_HOME and ANDROID_USER_HOME env vars
- Re-list after CreateAvdAsync to get actual path from avdmanager
- Add 5 new validation tests for Create/DeleteAvdAsync

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho rmarinho force-pushed the feature/avdmanager-runner branch from 7a84734 to 37c4ffe Compare March 5, 2026 12:53
rmarinho added a commit that referenced this pull request Mar 5, 2026
…ed args

- AdbRunner: check exit codes consistently in ListDevicesAsync, WaitForDeviceAsync, StopEmulatorAsync
- AdbRunner: bare catch {} → catch (Exception) in GetEmulatorAvdNameAsync
- AdbDeviceInfo.cs: split 3 public types into separate files (AdbDeviceType.cs, AdbDeviceStatus.cs)
- EnvironmentVariableNames: add AndroidAvdHome constant, use in AvdManagerRunner
- EmulatorRunner.StartAvd: additionalArgs string? → IEnumerable<string>? for proper arg separation
- EmulatorRunner.StartAvd: set RedirectStandardOutput/Error=false to prevent pipe deadlock

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho and others added 2 commits March 5, 2026 15:37
- GetToolEnvironment(): Set ANDROID_HOME, JAVA_HOME, and PATH

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho and others added 28 commits March 5, 2026 15:37
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ing on StringWriter

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… add tests

- Replace manual ProcessStartInfo with ProcessUtils.CreateProcessStartInfo
- Use AndroidEnvironmentHelper.ConfigureEnvironment instead of inline env setup
- Extract ParseDeviceListOutput as internal static for testability
- Return IReadOnlyList<AdbDeviceInfo> from ListDevicesAsync
- Add AdbRunnerTests (7 tests): parsing, path discovery, null/missing sdk

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the adb device parsing, display name formatting, status mapping, and
emulator merging logic from dotnet/android's GetAvailableAndroidDevices
MSBuild task into the shared android-tools library.

Changes:
- AdbDeviceInfo: Add Type, Status, Description, AvdName, Product, TransportId
  fields using AdbDeviceType/AdbDeviceStatus enums
- AdbRunner.ParseAdbDevicesOutput: Regex-based parser matching dotnet/android
  (handles device/offline/unauthorized/no permissions states)
- AdbRunner.BuildDeviceDescription: Priority order AVD name > model > product >
  device > serial, with underscore-to-space cleanup
- AdbRunner.FormatDisplayName: Title case + API capitalization for AVD names
- AdbRunner.MapAdbStateToStatus: Maps adb states to AdbDeviceStatus enum
- AdbRunner.MergeDevicesAndEmulators: Merges running + non-running emulators,
  deduplicates by AVD name (case-insensitive), sorts online first
- AdbRunner.GetEmulatorAvdNameAsync: Queries AVD name via adb emu avd name
- AdbRunnerTests: 33 tests ported from dotnet/android test cases

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The bare catch block was swallowing cancellation tokens, causing
cancelled operations to silently continue spawning adb processes
instead of propagating the cancellation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- StartAvd(): Start an emulator for an AVD
- ListAvdNamesAsync(): List installed AVD names

Minimal implementation without external dependencies.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add XML documentation to all public members
- Use 'is not null' instead of '!= null'
- Improve code formatting
- Remove unused variable assignment

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add optional Func<string?> getJdkPath constructor parameter
- ConfigureEnvironment() sets JAVA_HOME and ANDROID_HOME on all ProcessStartInfo
- Applied to StartAvd and ListAvdNamesAsync
- Backward compatible: existing 1-arg constructor still works

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…riter

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… add tests

- Replace manual ProcessStartInfo with ProcessUtils.CreateProcessStartInfo
- Use AndroidEnvironmentHelper.ConfigureEnvironment instead of duplicated method
- Extract ParseListAvdsOutput as internal static for testability
- Return IReadOnlyList<string> from ListAvdNamesAsync
- Add RequireEmulatorPath helper
- Add EmulatorRunnerTests (7 tests): parsing, path discovery, null/missing sdk

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ListAvdsAsync(): List configured AVDs
- DeleteAvdAsync(): Delete an AVD

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add XML documentation to all public members
- Split AvdInfo into its own file (one type per file)
- Use 'is not null' instead of '!= null'
- Improve code formatting

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add optional Func<string?> getJdkPath constructor parameter
- ConfigureEnvironment() sets JAVA_HOME and ANDROID_HOME on all ProcessStartInfo
- Add CreateAvdAsync with duplicate detection, orphaned AVD directory handling, stdin 'no' for hardware profile prompt
- Extract ParseAvdListOutput as internal static for testability
- Backward compatible: existing 1-arg constructor still works

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add 'using' to StringWriter instances in ListAvdsAsync and CreateAvdAsync
  to ensure proper disposal
- Add AvdManagerRunnerTests with 5 focused tests for ParseAvdListOutput:
  multiple AVDs, Windows newlines, empty output, no AVDs, missing device

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Convert AvdManagerRunner.cs and AvdInfo.cs to file-scoped namespaces
  per repo conventions for new files
- Remove accidentally tracked nupkg (already in .gitignore)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ironmentVariableNames, exit code checks

- AvdManagerPath: enumerate versioned cmdline-tools dirs descending
  (mirrors SdkManager.FindSdkManagerPath pattern), then latest, then
  legacy tools/bin
- Use ProcessUtils.CreateProcessStartInfo with separate args instead
  of building Arguments string manually (all 3 methods)
- Use EnvironmentVariableNames.AndroidHome/.JavaHome constants instead
  of hard-coded strings
- Check exit codes in ListAvdsAsync and DeleteAvdAsync, throw on failure
- Return IReadOnlyList<AvdInfo> from ListAvdsAsync
- Set Path on AvdInfo returned from CreateAvdAsync for consistency
- Extract RequireAvdManagerPath helper

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Delegate to AndroidEnvironmentHelper.ConfigureEnvironment instead of inline setup
- Add 5 path discovery tests: versioned dir, higher version priority, latest fallback, null/missing sdk

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix argument validation: CreateAvdAsync/DeleteAvdAsync now throw
  ArgumentException for empty strings, ArgumentNullException for null,
  and validate before RequireAvdManagerPath()
- Make AndroidEnvironmentHelper internal, remove unused GetToolEnvironment
- Add ANDROID_USER_HOME to ConfigureEnvironment for consistent AVD
  location across tools (matches SdkManager behavior)
- Add AndroidUserHome constant to EnvironmentVariableNames
- Fix hard-coded AVD directory: use GetAvdRootDirectory() that respects
  ANDROID_AVD_HOME and ANDROID_USER_HOME env vars
- Re-list after CreateAvdAsync to get actual path from avdmanager
- Add 5 new validation tests for Create/DeleteAvdAsync

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eTool

- ProcessUtils.ThrowIfFailed: consistent exit code checking with stderr/stdout context
- ProcessUtils.ValidateNotNullOrEmpty: reusable null/empty argument validation
- ProcessUtils.FindCmdlineTool: version-aware cmdline-tools directory search with legacy fallback

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace cmdline-tools path lookup with ProcessUtils.FindCmdlineTool (~20 lines removed)
- Replace duplicated null/empty validation with ProcessUtils.ValidateNotNullOrEmpty
- Replace inline exit code checks with ProcessUtils.ThrowIfFailed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit that referenced this pull request Mar 5, 2026
- Fix argument validation: CreateAvdAsync/DeleteAvdAsync now throw
  ArgumentException for empty strings, ArgumentNullException for null,
  and validate before RequireAvdManagerPath()
- Make AndroidEnvironmentHelper internal, remove unused GetToolEnvironment
- Add ANDROID_USER_HOME to ConfigureEnvironment for consistent AVD
  location across tools (matches SdkManager behavior)
- Add AndroidUserHome constant to EnvironmentVariableNames
- Fix hard-coded AVD directory: use GetAvdRootDirectory() that respects
  ANDROID_AVD_HOME and ANDROID_USER_HOME env vars
- Re-list after CreateAvdAsync to get actual path from avdmanager
- Add 5 new validation tests for Create/DeleteAvdAsync

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho rmarinho force-pushed the feature/avdmanager-runner branch from 797964e to 271b5c1 Compare March 5, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot `copilot-cli` or other AIs were used to author this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants