using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Newtonsoft.Json;
using Unity.Pipeline.Commands;
using UnityEditor;
using UnityEditor.PackageManager;
using UnityEditor.PackageManager.Requests;
using UnityEngine;
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
namespace Unity.Pipeline.Editor.Commands.PackageManager
{
///
/// UPM package management over the Client API (CLI-203): list / search / add / remove / resolve.
///
/// Threading: UPM operations are asynchronous (the only
/// progresses while the editor ticks) and their members are main-thread only. Commands that wait on a
/// request therefore run off the main thread (MainThreadRequired = false) and marshal every
/// UPM touch back onto the main thread via the server dispatcher (), so they
/// can block without freezing the editor.
///
/// Command surface:
/// - package_list (installed scope) reads the resolved set inline; available/all
/// and package_search query the registry and wait for it — all return synchronously.
/// - package_add/package_remove mutate the manifest and trigger a domain reload
/// that tears down the in-flight request and the HTTP connection. They are dual-mode:
///
/// async (default) — kick the op off, return in_progress immediately, and let
/// an poller finalize a Temp status file. Poll
/// package_status until completed/failed, then recompile_status. This
/// keeps the (single-request) server responsive and survives the reload.
/// synchronous (wait=true) — block until the request completes and return the
/// full result. The result is captured in one main-thread hop the moment the request completes
/// (before the compile-triggered reload). Reliable for add; for remove the reload can fire fast
/// enough to drop the reply — the status file still settles, so package_status confirms it.
///
/// Both write the status file, so a lost reply (or a reload mid-wait) is recoverable via
/// package_status; settles an in_progress
/// file on the next load.
/// - package_resolve records its status too, so package_status validates it.
///
/// Mutating commands (add / remove) follow the shared confirm/dry_run convention.
/// UPM operations are not part of Unity's Undo.
///
[InitializeOnLoad]
public static class PackageManagerCommand
{
const string StatusFile = "Temp/pipeline_package_status.json";
// Bound on a synchronous wait, kept under the CLI's default request timeout so a stuck operation
// surfaces as a clean error rather than a dropped connection.
const int WaitTimeoutSeconds = 25;
const int PollIntervalMs = 50;
static readonly object s_Lock = new object();
// The in-flight mutating request (add/remove) and how to project its result. s_InProgress is a
// plain flag (read off-thread by IsBusy) so the busy check never touches main-thread-only Request
// members. Both are reset by a domain reload; RecoverInterruptedOperation settles the status file.
static volatile bool s_InProgress;
static Request s_Request;
static string s_Operation;
static string s_Argument;
static Func s_ReadPackage;
static PackageManagerCommand()
{
RecoverInterruptedOperation();
}
// ---- List / search (synchronous) -------------------------------------------------------
[CliCommand("package_list",
"List packages by scope: installed (default) | available (registry) | all (both). Returns the full " +
"result synchronously — available/all block until the registry query completes.",
MainThreadRequired = false)]
public static object PackageList(
[CliArg("scope", "Which packages to list: installed (default) | available | all.")] string scope = "installed",
[CliArg("include_indirect", "Include indirect (transitive) installed dependencies (applies to scope=installed/all).")]
bool includeIndirect = true,
[CliArg("offline", "For available/all: query the local cache instead of the registry.")] bool offline = false)
{
switch ((scope ?? "installed").Trim().ToLowerInvariant())
{
case "installed":
return RunOnMain(() => ListInstalled(includeIndirect));
case "available":
return ListFromRegistry("available", includeIndirect, offline);
case "all":
return ListFromRegistry("all", includeIndirect, offline);
default:
return new PackageListResponse
{
Success = false,
Scope = scope,
Message = $"Unknown scope '{scope}'. Use installed | available | all."
};
}
}
///
/// Installed scope: reflects the resolved
/// set without a registry round-trip. Runs on the main thread (via ).
///
static PackageListResponse ListInstalled(bool includeIndirect)
{
var summaries = new List();
foreach (var p in PackageInfo.GetAllRegisteredPackages())
{
if (includeIndirect || p.isDirectDependency)
summaries.Add(Map(p, isInstalled: true));
}
return new PackageListResponse
{
Success = true,
Scope = "installed",
Count = summaries.Count,
Packages = summaries,
Manifest = SafeReadManifest(),
Message = $"{summaries.Count} installed package(s)."
};
}
///
/// available / all scope: these need the registry, so a Client.SearchAll request is
/// started on the main thread and awaited here (on the command's background thread). For
/// all, the installed set is merged in (and marked) so callers see one unified list.
///
static object ListFromRegistry(string scope, bool includeIndirect, bool offline)
{
var request = RunOnMain(() => Client.SearchAll(offline));
if (!WaitForCompletion(request, out var waitError))
return new PackageListResponse { Success = false, Scope = scope, Message = waitError };
return RunOnMain