Add Unity Pipeline package support
This commit is contained in:
+161
@@ -0,0 +1,161 @@
|
||||
using System;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.PackageManager
|
||||
{
|
||||
/// <summary>Where an added package comes from, derived from its identifier string (CLI-203).</summary>
|
||||
public enum PackageSourceKind
|
||||
{
|
||||
/// <summary>Could not be classified (e.g. empty input).</summary>
|
||||
Unknown,
|
||||
|
||||
/// <summary>A registry package name, optionally pinned: <c>com.unity.foo</c> or <c>com.unity.foo@1.2.3</c>.</summary>
|
||||
Registry,
|
||||
|
||||
/// <summary>A git URL: <c>https://….git</c>, <c>git+…</c>, <c>ssh://…</c>, <c>git@host:…</c>, optionally <c>#revision</c>.</summary>
|
||||
Git,
|
||||
|
||||
/// <summary>A local package reference: <c>file:../RelativePath</c> or <c>file:/abs/path</c>.</summary>
|
||||
Local
|
||||
}
|
||||
|
||||
/// <summary>A parsed UPM <c>package_add</c> identifier — the normalized string handed to
|
||||
/// <c>Client.Add</c> plus the pieces extracted for plan/audit text.</summary>
|
||||
public sealed class ParsedPackageId
|
||||
{
|
||||
/// <summary>How the package will be resolved by UPM.</summary>
|
||||
public PackageSourceKind Kind { get; set; }
|
||||
|
||||
/// <summary>The identifier passed verbatim to <c>UnityEditor.PackageManager.Client.Add</c>.</summary>
|
||||
public string Identifier { get; set; }
|
||||
|
||||
/// <summary>Registry package name (Registry only); null otherwise.</summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>Registry version or git revision when given; null otherwise.</summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>Human-readable summary for plan/audit text.</summary>
|
||||
public string Description { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure, editor-independent classification of a <c>package_add</c> identifier into a registry name,
|
||||
/// git URL, or local path (CLI-203). Kept free of any Unity API so the add-path's input handling is
|
||||
/// unit-testable without a live editor; the command layer feeds the result to
|
||||
/// <c>UnityEditor.PackageManager.Client.Add</c>.
|
||||
/// </summary>
|
||||
public static class PackageIdentifier
|
||||
{
|
||||
/// <summary>Classify an identifier without parsing out its parts.</summary>
|
||||
public static PackageSourceKind Classify(string identifier)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(identifier))
|
||||
return PackageSourceKind.Unknown;
|
||||
|
||||
var id = identifier.Trim();
|
||||
|
||||
if (StartsWith(id, "file:"))
|
||||
return PackageSourceKind.Local;
|
||||
|
||||
// Explicit git forms and any URL scheme (UPM treats http(s) identifiers as git URLs).
|
||||
if (StartsWith(id, "git+") || StartsWith(id, "ssh://") || id.StartsWith("git@", StringComparison.Ordinal))
|
||||
return PackageSourceKind.Git;
|
||||
if (StartsWith(id, "http://") || StartsWith(id, "https://") || id.Contains("://"))
|
||||
return PackageSourceKind.Git;
|
||||
|
||||
// A bare "owner/repo.git" (no scheme) is still a git reference. Strip an optional
|
||||
// "#revision" before testing the suffix.
|
||||
var core = id;
|
||||
var hash = core.IndexOf('#');
|
||||
if (hash >= 0)
|
||||
core = core.Substring(0, hash);
|
||||
if (core.EndsWith(".git", StringComparison.OrdinalIgnoreCase))
|
||||
return PackageSourceKind.Git;
|
||||
|
||||
return PackageSourceKind.Registry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classify and split <paramref name="identifier"/>. Returns false with <paramref name="error"/>
|
||||
/// set for empty/unclassifiable input.
|
||||
/// </summary>
|
||||
public static bool TryParse(string identifier, out ParsedPackageId parsed, out string error)
|
||||
{
|
||||
parsed = null;
|
||||
error = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
error = "Package identifier is empty. Provide a name (com.unity.foo[@version]), a git URL, or a file: path.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var id = identifier.Trim();
|
||||
switch (Classify(id))
|
||||
{
|
||||
case PackageSourceKind.Local:
|
||||
parsed = new ParsedPackageId
|
||||
{
|
||||
Kind = PackageSourceKind.Local,
|
||||
Identifier = id,
|
||||
Description = $"local package '{id}'"
|
||||
};
|
||||
return true;
|
||||
|
||||
case PackageSourceKind.Git:
|
||||
{
|
||||
string revision = null;
|
||||
var hash = id.IndexOf('#');
|
||||
if (hash >= 0 && hash < id.Length - 1)
|
||||
revision = id.Substring(hash + 1);
|
||||
parsed = new ParsedPackageId
|
||||
{
|
||||
Kind = PackageSourceKind.Git,
|
||||
Identifier = id,
|
||||
Version = revision,
|
||||
Description = revision != null ? $"git package '{id}' @ {revision}" : $"git package '{id}'"
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
case PackageSourceKind.Registry:
|
||||
{
|
||||
var name = id;
|
||||
string version = null;
|
||||
|
||||
// Split on the version separator '@'. Use the last '@' so npm-scoped names
|
||||
// ("@scope/pkg@1.0.0") still split on the version, not the leading scope marker.
|
||||
var at = id.LastIndexOf('@');
|
||||
if (at > 0)
|
||||
{
|
||||
name = id.Substring(0, at);
|
||||
version = id.Substring(at + 1);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
error = $"Package name is empty in '{identifier}'.";
|
||||
return false;
|
||||
}
|
||||
|
||||
parsed = new ParsedPackageId
|
||||
{
|
||||
Kind = PackageSourceKind.Registry,
|
||||
Identifier = id,
|
||||
Name = name,
|
||||
Version = version,
|
||||
Description = version != null ? $"'{name}' @ {version}" : $"'{name}' (latest compatible)"
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
error = $"Could not classify package identifier '{identifier}'.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool StartsWith(string value, string prefix) =>
|
||||
value.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b95405e8a4f04c93acbdd564a74f8cc2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+644
@@ -0,0 +1,644 @@
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// UPM package management over the Client API (CLI-203): list / search / add / remove / resolve.
|
||||
///
|
||||
/// Threading: UPM <see cref="Client"/> operations are asynchronous (the <see cref="Request"/> 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 (<c>MainThreadRequired = false</c>) and marshal every
|
||||
/// UPM touch back onto the main thread via the server dispatcher (<see cref="RunOnMain"/>), so they
|
||||
/// can block without freezing the editor.
|
||||
///
|
||||
/// Command surface:
|
||||
/// - <c>package_list</c> (installed scope) reads the resolved set inline; <c>available</c>/<c>all</c>
|
||||
/// and <c>package_search</c> query the registry and wait for it — all return <b>synchronously</b>.
|
||||
/// - <c>package_add</c>/<c>package_remove</c> mutate the manifest and trigger a <b>domain reload</b>
|
||||
/// that tears down the in-flight request and the HTTP connection. They are <b>dual-mode</b>:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><b>async (default)</b> — kick the op off, return <c>in_progress</c> immediately, and let
|
||||
/// an <see cref="EditorApplication.update"/> poller finalize a Temp status file. Poll
|
||||
/// <c>package_status</c> until <c>completed</c>/<c>failed</c>, then <c>recompile_status</c>. This
|
||||
/// keeps the (single-request) server responsive and survives the reload.</description></item>
|
||||
/// <item><description><b>synchronous</b> (<c>wait=true</c>) — 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 <c>package_status</c> confirms it.</description></item>
|
||||
/// </list>
|
||||
/// Both write the status file, so a lost reply (or a reload mid-wait) is recoverable via
|
||||
/// <c>package_status</c>; <see cref="RecoverInterruptedOperation"/> settles an <c>in_progress</c>
|
||||
/// file on the next load.
|
||||
/// - <c>package_resolve</c> records its status too, so <c>package_status</c> validates it.
|
||||
///
|
||||
/// Mutating commands (add / remove) follow the shared <c>confirm</c>/<c>dry_run</c> convention.
|
||||
/// UPM operations are not part of Unity's Undo.
|
||||
/// </summary>
|
||||
[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<Request, PackageSummary> 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."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installed scope: <see cref="PackageInfo.GetAllRegisteredPackages"/> reflects the resolved
|
||||
/// set without a registry round-trip. Runs on the main thread (via <see cref="RunOnMain"/>).
|
||||
/// </summary>
|
||||
static PackageListResponse ListInstalled(bool includeIndirect)
|
||||
{
|
||||
var summaries = new List<PackageSummary>();
|
||||
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)."
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// available / all scope: these need the registry, so a <c>Client.SearchAll</c> request is
|
||||
/// started on the main thread and awaited here (on the command's background thread). For
|
||||
/// <c>all</c>, the installed set is merged in (and marked) so callers see one unified list.
|
||||
/// </summary>
|
||||
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<object>(() =>
|
||||
{
|
||||
if (request.Status != StatusCode.Success)
|
||||
return new PackageListResponse
|
||||
{
|
||||
Success = false,
|
||||
Scope = scope,
|
||||
Message = $"Registry query failed: {request.Error?.message ?? "unknown error"}"
|
||||
};
|
||||
|
||||
var packages = BuildScopedList(scope, request.Result, includeIndirect);
|
||||
return new PackageListResponse
|
||||
{
|
||||
Success = true,
|
||||
Scope = scope,
|
||||
Count = packages.Count,
|
||||
Packages = packages,
|
||||
Manifest = SafeReadManifest(),
|
||||
Message = $"{packages.Count} package(s)."
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combine the registry search result with the installed set per <paramref name="scope"/>:
|
||||
/// <c>available</c> returns every registry package (flagged whether it is installed);
|
||||
/// <c>all</c> returns installed packages (resolved info) plus the registry packages that are
|
||||
/// not installed.
|
||||
/// </summary>
|
||||
static List<PackageSummary> BuildScopedList(string scope, PackageInfo[] available, bool includeIndirect)
|
||||
{
|
||||
var installed = new Dictionary<string, PackageInfo>();
|
||||
foreach (var p in PackageInfo.GetAllRegisteredPackages())
|
||||
installed[p.name] = p;
|
||||
|
||||
var result = new List<PackageSummary>();
|
||||
|
||||
if (scope == "available")
|
||||
{
|
||||
foreach (var p in available ?? Array.Empty<PackageInfo>())
|
||||
result.Add(Map(p, isInstalled: installed.ContainsKey(p.name)));
|
||||
return result;
|
||||
}
|
||||
|
||||
// scope == "all": installed entries first (with their resolved info), then registry-only ones.
|
||||
var seen = new HashSet<string>();
|
||||
foreach (var entry in installed.Values)
|
||||
{
|
||||
if (!includeIndirect && !entry.isDirectDependency)
|
||||
continue;
|
||||
result.Add(Map(entry, isInstalled: true));
|
||||
seen.Add(entry.name);
|
||||
}
|
||||
foreach (var p in available ?? Array.Empty<PackageInfo>())
|
||||
{
|
||||
if (!seen.Contains(p.name))
|
||||
result.Add(Map(p, isInstalled: false));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[CliCommand("package_search",
|
||||
"Search packages available in the registry. Provide a name (e.g. com.unity.foo) or omit to list all. " +
|
||||
"Returns the full result synchronously (blocks until the registry query completes).",
|
||||
MainThreadRequired = false)]
|
||||
public static object PackageSearch(
|
||||
[CliArg("query", "Package name to search for. Omit/empty to list all available packages.")] string query = "",
|
||||
[CliArg("offline", "Search the local cache only.")] bool offline = false)
|
||||
{
|
||||
var trimmed = (query ?? string.Empty).Trim();
|
||||
|
||||
var request = RunOnMain(() => string.IsNullOrEmpty(trimmed)
|
||||
? Client.SearchAll(offline)
|
||||
: Client.Search(trimmed, offline));
|
||||
|
||||
if (!WaitForCompletion(request, out var waitError))
|
||||
return new PackageSearchResponse { Success = false, Query = trimmed, Message = waitError };
|
||||
|
||||
return RunOnMain<object>(() =>
|
||||
{
|
||||
if (request.Status != StatusCode.Success)
|
||||
return new PackageSearchResponse
|
||||
{
|
||||
Success = false,
|
||||
Query = trimmed,
|
||||
Message = $"Search failed: {request.Error?.message ?? "unknown error"}"
|
||||
};
|
||||
|
||||
var packages = MapMarkingInstalled(request.Result);
|
||||
return new PackageSearchResponse
|
||||
{
|
||||
Success = true,
|
||||
Query = trimmed,
|
||||
Count = packages.Count,
|
||||
Packages = packages,
|
||||
Message = $"{packages.Count} package(s)."
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Mutating operations (dual-mode, CAT-2509 gated) -----------------------------------
|
||||
|
||||
[CliCommand("package_add",
|
||||
"Add a UPM package by name@version, git URL, or 'file:' local path. Async by default (returns " +
|
||||
"in_progress; poll package_status); pass wait=true to block until added. A recompile/domain reload " +
|
||||
"follows — poll recompile_status. Requires confirm=true; use dry_run to preview.",
|
||||
MainThreadRequired = false)]
|
||||
public static object PackageAdd(
|
||||
[CliArg("identifier", "Package to add: 'com.unity.foo@1.2.3', a git URL, or 'file:../Path'.", Required = true)] string identifier = "",
|
||||
[CliArg("confirm", "Apply the change. Without it the call is refused.")] bool confirm = false,
|
||||
[CliArg("dry_run", "Preview the change without applying it.")] bool dryRun = false,
|
||||
[CliArg("wait", "Block until the operation completes and return the result (synchronous). Default: return immediately and poll package_status.")] bool wait = false)
|
||||
{
|
||||
if (!PackageIdentifier.TryParse(identifier, out var parsed, out var parseError))
|
||||
return PackageMutationResponse.Failed("add", identifier, parseError);
|
||||
|
||||
if (IsBusy())
|
||||
return PackageMutationResponse.Busy("add");
|
||||
|
||||
return ExecuteMutation(
|
||||
operation: "add",
|
||||
commandName: "package_add",
|
||||
argument: parsed.Identifier,
|
||||
planText: $"Add package {parsed.Description} [{parsed.Kind}]",
|
||||
confirm: confirm,
|
||||
dryRun: dryRun,
|
||||
wait: wait,
|
||||
start: () => Client.Add(parsed.Identifier),
|
||||
readPackage: req =>
|
||||
{
|
||||
var added = (req as AddRequest)?.Result;
|
||||
return added != null ? Map(added, isInstalled: true) : null;
|
||||
});
|
||||
}
|
||||
|
||||
[CliCommand("package_remove",
|
||||
"Remove a UPM package by name. Async by default (returns in_progress; poll package_status); pass " +
|
||||
"wait=true to block until removed. A recompile/domain reload follows — poll recompile_status. " +
|
||||
"Requires confirm=true; use dry_run to preview.",
|
||||
MainThreadRequired = false)]
|
||||
public static object PackageRemove(
|
||||
[CliArg("name", "Package name to remove (e.g. com.unity.foo).", Required = true)] string name = "",
|
||||
[CliArg("confirm", "Apply the change. Without it the call is refused.")] bool confirm = false,
|
||||
[CliArg("dry_run", "Preview the change without applying it.")] bool dryRun = false,
|
||||
[CliArg("wait", "Block until the operation completes and return the result (synchronous). Default: return immediately and poll package_status.")] bool wait = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return PackageMutationResponse.Failed("remove", name, "A package name is required.");
|
||||
|
||||
var packageName = name.Trim();
|
||||
|
||||
if (IsBusy())
|
||||
return PackageMutationResponse.Busy("remove");
|
||||
|
||||
return ExecuteMutation(
|
||||
operation: "remove",
|
||||
commandName: "package_remove",
|
||||
argument: packageName,
|
||||
planText: $"Remove package '{packageName}'",
|
||||
confirm: confirm,
|
||||
dryRun: dryRun,
|
||||
wait: wait,
|
||||
start: () => Client.Remove(packageName),
|
||||
readPackage: null);
|
||||
}
|
||||
|
||||
[CliCommand("package_resolve",
|
||||
"Resolve/refresh packages from the manifest (re-fetch and re-link). May trigger a recompile/domain " +
|
||||
"reload — poll recompile_status. Its outcome is recorded for package_status.",
|
||||
MainThreadRequired = true)]
|
||||
public static object PackageResolve()
|
||||
{
|
||||
WriteStatus(new PackageStatus
|
||||
{
|
||||
Status = "in_progress",
|
||||
Operation = "resolve",
|
||||
StartedAt = NowIso(),
|
||||
Message = "Resolving packages..."
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
// Client.Resolve() is fire-and-forget (returns void) and schedules a resolve on a later
|
||||
// tick, so the response flushes before any reload — no request to wait on.
|
||||
Client.Resolve();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var failed = new PackageStatus
|
||||
{
|
||||
Status = "failed",
|
||||
Operation = "resolve",
|
||||
Success = false,
|
||||
Error = ex.Message,
|
||||
Manifest = SafeReadManifest(),
|
||||
CompletedAt = NowIso(),
|
||||
Message = $"Resolve failed: {ex.Message}"
|
||||
};
|
||||
WriteStatus(failed);
|
||||
return PackageMutationResponse.Failed("resolve", null, failed.Error);
|
||||
}
|
||||
|
||||
var done = new PackageStatus
|
||||
{
|
||||
Status = "completed",
|
||||
Operation = "resolve",
|
||||
Success = true,
|
||||
RequiresRecompile = true,
|
||||
Manifest = SafeReadManifest(),
|
||||
CompletedAt = NowIso(),
|
||||
Message = "Resolve requested. If assemblies changed, a domain reload follows — poll recompile_status."
|
||||
};
|
||||
WriteStatus(done);
|
||||
return ToMutationResponse(done, plan: null);
|
||||
}
|
||||
|
||||
// ---- Status ----------------------------------------------------------------------------
|
||||
|
||||
[CliCommand("package_status",
|
||||
"Status of the last async package operation (add/remove/resolve): idle | in_progress | completed | " +
|
||||
"failed, with the added package, manifest, and any error.",
|
||||
MainThreadRequired = false)]
|
||||
public static string GetPackageStatus()
|
||||
{
|
||||
if (File.Exists(StatusFile))
|
||||
return File.ReadAllText(StatusFile);
|
||||
return "{\"status\":\"idle\"}";
|
||||
}
|
||||
|
||||
// ---- Mutation execution ----------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Gate (confirm / dry-run) and kick off the mutating op, then
|
||||
/// either return <c>in_progress</c> (async, default) or block until completion (<paramref name="wait"/>).
|
||||
/// Either way the operation is tracked and a status file is written, so the result is recoverable
|
||||
/// via <c>package_status</c> even if the domain reload severs a synchronous reply.
|
||||
/// </summary>
|
||||
static object ExecuteMutation(
|
||||
string operation, string commandName, string argument, string planText,
|
||||
bool confirm, bool dryRun, bool wait,
|
||||
Func<Request> start, Func<Request, PackageSummary> readPackage)
|
||||
{
|
||||
if (dryRun)
|
||||
return PackageMutationResponse.DryRunPreview(operation, argument, planText, $"Dry run — {planText}");
|
||||
if (!confirm)
|
||||
return PackageMutationResponse.Rejected(operation, argument,
|
||||
"Refused: this changes project packages. Re-run with confirm=true to apply, or dry_run=true to preview.");
|
||||
|
||||
// UPM operations are not part of Unity's Undo system, so there is no undo scope here.
|
||||
var request = RunOnMain(() =>
|
||||
{
|
||||
var r = start();
|
||||
// async mode finalizes via the update-loop poller; sync mode finalizes itself.
|
||||
BeginTracking(operation, argument, r, readPackage, subscribePoll: !wait);
|
||||
return r;
|
||||
});
|
||||
|
||||
if (request == null)
|
||||
return PackageMutationResponse.Failed(operation, argument, "Operation was confirmed but failed to start.");
|
||||
|
||||
if (!wait)
|
||||
return PackageMutationResponse.InProgress(operation, argument, planText);
|
||||
|
||||
// Synchronous: poll until complete, capturing status + result atomically on the main thread
|
||||
// (a reload can only fire between ticks, so the snapshot can't be interleaved).
|
||||
var deadline = DateTime.UtcNow.AddSeconds(WaitTimeoutSeconds);
|
||||
while (true)
|
||||
{
|
||||
var status = RunOnMain(TryFinalize);
|
||||
if (status != null)
|
||||
return ToMutationResponse(status, planText);
|
||||
|
||||
if (DateTime.UtcNow > deadline)
|
||||
{
|
||||
// Hand off to the update-loop poller so package_status still settles after we bail.
|
||||
RunOnMain<object>(() => { SubscribePoll(); return null; });
|
||||
return PackageMutationResponse.Failed(operation, argument,
|
||||
$"Timed out after {WaitTimeoutSeconds}s; the operation may still be in progress — poll package_status.");
|
||||
}
|
||||
|
||||
Thread.Sleep(PollIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Record the in-flight op, write the in_progress status, and (async only) start polling.</summary>
|
||||
static void BeginTracking(string operation, string argument, Request request,
|
||||
Func<Request, PackageSummary> readPackage, bool subscribePoll)
|
||||
{
|
||||
lock (s_Lock)
|
||||
{
|
||||
s_Request = request;
|
||||
s_Operation = operation;
|
||||
s_Argument = argument;
|
||||
s_ReadPackage = readPackage;
|
||||
s_InProgress = true;
|
||||
}
|
||||
|
||||
WriteStatus(new PackageStatus
|
||||
{
|
||||
Status = "in_progress",
|
||||
Operation = operation,
|
||||
Argument = argument,
|
||||
StartedAt = NowIso(),
|
||||
Message = $"{operation} in progress. Poll package_status."
|
||||
});
|
||||
|
||||
if (subscribePoll)
|
||||
SubscribePoll();
|
||||
}
|
||||
|
||||
static void SubscribePoll()
|
||||
{
|
||||
EditorApplication.update -= Poll;
|
||||
EditorApplication.update += Poll;
|
||||
}
|
||||
|
||||
/// <summary>Update-loop poller for async mode (and a timed-out sync wait): finalize when done.</summary>
|
||||
static void Poll() => TryFinalize();
|
||||
|
||||
/// <summary>
|
||||
/// If the tracked request has completed, write its final status, clear tracking, and return the
|
||||
/// status; otherwise return null. Main-thread only (reads Request members); guarded so the sync
|
||||
/// waiter and the update-loop poller can't double-finalize.
|
||||
/// </summary>
|
||||
static PackageStatus TryFinalize()
|
||||
{
|
||||
lock (s_Lock)
|
||||
{
|
||||
var request = s_Request;
|
||||
if (request == null)
|
||||
{
|
||||
EditorApplication.update -= Poll;
|
||||
return null;
|
||||
}
|
||||
if (!request.IsCompleted)
|
||||
return null;
|
||||
|
||||
var status = BuildStatus(s_Operation, s_Argument, request, s_ReadPackage);
|
||||
WriteStatus(status);
|
||||
|
||||
s_Request = null;
|
||||
s_Operation = null;
|
||||
s_Argument = null;
|
||||
s_ReadPackage = null;
|
||||
s_InProgress = false;
|
||||
EditorApplication.update -= Poll;
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
static PackageStatus BuildStatus(string operation, string argument, Request request,
|
||||
Func<Request, PackageSummary> readPackage)
|
||||
{
|
||||
var status = new PackageStatus
|
||||
{
|
||||
Operation = operation,
|
||||
Argument = argument,
|
||||
CompletedAt = NowIso(),
|
||||
Manifest = SafeReadManifest()
|
||||
};
|
||||
|
||||
if (request.Status == StatusCode.Success)
|
||||
{
|
||||
status.Status = "completed";
|
||||
status.Success = true;
|
||||
status.RequiresRecompile = true;
|
||||
try { status.Package = readPackage?.Invoke(request); }
|
||||
catch (Exception ex) { Debug.LogWarning($"[pipeline] package result projection failed: {ex.Message}"); }
|
||||
status.Message = $"{operation} completed.";
|
||||
}
|
||||
else
|
||||
{
|
||||
status.Status = "failed";
|
||||
status.Success = false;
|
||||
status.Error = request.Error?.message ?? "Unknown package manager error.";
|
||||
status.Message = $"{operation} failed: {status.Error}";
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
static PackageMutationResponse ToMutationResponse(PackageStatus s, string plan) => new PackageMutationResponse
|
||||
{
|
||||
Success = s.Success,
|
||||
Operation = s.Operation,
|
||||
Argument = s.Argument,
|
||||
Status = s.Status,
|
||||
Applied = s.Success,
|
||||
Plan = plan,
|
||||
Package = s.Package,
|
||||
Manifest = s.Manifest,
|
||||
RequiresRecompile = s.RequiresRecompile,
|
||||
Message = s.Message
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// On load, settle a status file left at "in_progress" by a domain reload (the expected outcome of
|
||||
/// a successful add/remove). The manifest change that triggered the reload has taken effect, so
|
||||
/// the op is recorded completed; the follow-on recompile is observed via recompile_status.
|
||||
/// </summary>
|
||||
static void RecoverInterruptedOperation()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(StatusFile))
|
||||
return;
|
||||
|
||||
var status = JsonConvert.DeserializeObject<PackageStatus>(File.ReadAllText(StatusFile));
|
||||
if (status == null || status.Status != "in_progress")
|
||||
return;
|
||||
|
||||
status.Status = "completed";
|
||||
status.Success = true;
|
||||
status.RequiresRecompile = true;
|
||||
status.CompletedAt = NowIso();
|
||||
status.Manifest = SafeReadManifest();
|
||||
status.Message = $"{status.Operation} completed (domain reload). Manifest updated; poll recompile_status for the recompile.";
|
||||
WriteStatus(status);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[pipeline] package status recovery failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Helpers ---------------------------------------------------------------------------
|
||||
|
||||
static bool IsBusy() => s_InProgress;
|
||||
|
||||
/// <summary>
|
||||
/// Block until <paramref name="request"/> completes, polling its (main-thread-only) state via
|
||||
/// <see cref="RunOnMain"/> while this thread sleeps between checks. Returns false with
|
||||
/// <paramref name="error"/> set on timeout. Used by the read-only registry queries.
|
||||
/// </summary>
|
||||
static bool WaitForCompletion(Request request, out string error)
|
||||
{
|
||||
error = null;
|
||||
var deadline = DateTime.UtcNow.AddSeconds(WaitTimeoutSeconds);
|
||||
while (!RunOnMain(() => request.IsCompleted))
|
||||
{
|
||||
if (DateTime.UtcNow > deadline)
|
||||
{
|
||||
error = $"Timed out after {WaitTimeoutSeconds}s waiting for the package manager.";
|
||||
return false;
|
||||
}
|
||||
Thread.Sleep(PollIntervalMs);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run <paramref name="fn"/> on the Unity main thread. When invoked off-thread (the normal case
|
||||
/// for the waiting commands), it marshals through the live server's dispatcher; when already on
|
||||
/// the main thread (e.g. a direct in-process/test call, or no server running) it runs inline.
|
||||
/// </summary>
|
||||
static T RunOnMain<T>(Func<T> fn)
|
||||
{
|
||||
var dispatcher = PipelineServerStartup.Server?.Dispatcher;
|
||||
if (dispatcher != null && dispatcher.IsInitialized && !dispatcher.IsMainThread())
|
||||
return dispatcher.Invoke(fn);
|
||||
return fn();
|
||||
}
|
||||
|
||||
static Dictionary<string, string> SafeReadManifest() =>
|
||||
PackageManifest.TryRead(out var deps, out _) ? deps : null;
|
||||
|
||||
static PackageSummary Map(PackageInfo p, bool isInstalled = false) => new PackageSummary
|
||||
{
|
||||
Name = p.name,
|
||||
Version = p.version,
|
||||
DisplayName = p.displayName,
|
||||
Source = p.source.ToString(),
|
||||
ResolvedPath = p.resolvedPath,
|
||||
IsDirectDependency = p.isDirectDependency,
|
||||
IsInstalled = isInstalled
|
||||
};
|
||||
|
||||
/// <summary>Map registry results, flagging which are currently installed.</summary>
|
||||
static List<PackageSummary> MapMarkingInstalled(IEnumerable<PackageInfo> packages)
|
||||
{
|
||||
var installed = new HashSet<string>();
|
||||
foreach (var p in PackageInfo.GetAllRegisteredPackages())
|
||||
installed.Add(p.name);
|
||||
|
||||
var list = new List<PackageSummary>();
|
||||
if (packages != null)
|
||||
foreach (var p in packages)
|
||||
list.Add(Map(p, installed.Contains(p.name)));
|
||||
return list;
|
||||
}
|
||||
|
||||
static string NowIso() => DateTime.UtcNow.ToString("o");
|
||||
|
||||
static void WriteStatus(PackageStatus status)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(StatusFile, JsonConvert.SerializeObject(status));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[Package] Failed to write status file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 248529bee7274235a46b64bf3f03da11
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.PackageManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads the project's UPM manifest (<c>Packages/manifest.json</c>) <c>dependencies</c> as a flat
|
||||
/// name → version map, satisfying CLI-203's "return manifest state". The JSON parsing is split into
|
||||
/// a pure <see cref="ReadDependencies(string)"/> seam so it is unit-testable without a live project;
|
||||
/// <see cref="TryRead"/> resolves the project path and is main-thread only (it reads
|
||||
/// <see cref="Application.dataPath"/>).
|
||||
/// </summary>
|
||||
public static class PackageManifest
|
||||
{
|
||||
/// <summary>Project-relative path to the UPM manifest.</summary>
|
||||
public const string RelativePath = "Packages/manifest.json";
|
||||
|
||||
/// <summary>Absolute path to the manifest for the current project. Main thread only.</summary>
|
||||
public static string ManifestPath
|
||||
{
|
||||
get
|
||||
{
|
||||
var projectRoot = Path.GetDirectoryName(Application.dataPath);
|
||||
return Path.Combine(projectRoot, "Packages", "manifest.json");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse the <c>dependencies</c> object of a manifest.json document into a name → version map.
|
||||
/// Returns an empty map for null/empty input or a manifest without dependencies.
|
||||
/// </summary>
|
||||
public static Dictionary<string, string> ReadDependencies(string manifestJson)
|
||||
{
|
||||
var result = new Dictionary<string, string>();
|
||||
if (string.IsNullOrWhiteSpace(manifestJson))
|
||||
return result;
|
||||
|
||||
var root = JObject.Parse(manifestJson);
|
||||
if (root["dependencies"] is JObject deps)
|
||||
{
|
||||
foreach (var kv in deps)
|
||||
result[kv.Key] = kv.Value?.ToString();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the current project's manifest dependencies. Returns false with
|
||||
/// <paramref name="error"/> set when the manifest is missing or unreadable.
|
||||
/// </summary>
|
||||
public static bool TryRead(out Dictionary<string, string> dependencies, out string error)
|
||||
{
|
||||
dependencies = null;
|
||||
error = null;
|
||||
try
|
||||
{
|
||||
var path = ManifestPath;
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
error = $"manifest not found at {path}";
|
||||
return false;
|
||||
}
|
||||
|
||||
dependencies = ReadDependencies(File.ReadAllText(path));
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0401cb232d5143bbb185ac248086a09f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.PackageManager
|
||||
{
|
||||
/// <summary>
|
||||
/// One package in a list/search/add result — a JSON-friendly projection of
|
||||
/// <c>UnityEditor.PackageManager.PackageInfo</c> (CLI-203).
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class PackageSummary
|
||||
{
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
[JsonProperty("version")] public string Version { get; set; }
|
||||
[JsonProperty("displayName")] public string DisplayName { get; set; }
|
||||
|
||||
/// <summary>Resolved source: Registry, Embedded, Local, Git, BuiltIn, LocalTarball, Unknown.</summary>
|
||||
[JsonProperty("source")] public string Source { get; set; }
|
||||
|
||||
[JsonProperty("resolvedPath")] public string ResolvedPath { get; set; }
|
||||
|
||||
/// <summary>True when the package is a direct manifest dependency (vs. a transitive one).</summary>
|
||||
[JsonProperty("isDirectDependency")] public bool IsDirectDependency { get; set; }
|
||||
|
||||
/// <summary>True when the package is currently installed/resolved in the project (vs. only
|
||||
/// available in the registry). Always true for an installed-scope listing.</summary>
|
||||
[JsonProperty("isInstalled")] public bool IsInstalled { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronous result of <c>package_list</c> (CLI-203). The command returns the full result in one
|
||||
/// call for every scope — <c>installed</c> reads the resolved set inline; <c>available</c>/<c>all</c>
|
||||
/// run a registry query and block until it completes — so listing never uses the trigger-then-poll
|
||||
/// path that the mutating commands do.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class PackageListResponse
|
||||
{
|
||||
[JsonProperty("success")] public bool Success { get; set; }
|
||||
|
||||
/// <summary>The scope that was listed: installed | available | all.</summary>
|
||||
[JsonProperty("scope")] public string Scope { get; set; }
|
||||
|
||||
[JsonProperty("count")] public int Count { get; set; }
|
||||
|
||||
/// <summary>The packages in the requested scope.</summary>
|
||||
[JsonProperty("packages")] public List<PackageSummary> Packages { get; set; }
|
||||
|
||||
/// <summary>Manifest dependencies (name → version).</summary>
|
||||
[JsonProperty("manifest")] public Dictionary<string, string> Manifest { get; set; }
|
||||
|
||||
[JsonProperty("message")] public string Message { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronous result of <c>package_search</c> (CLI-203). Like <c>package_list</c>, the registry
|
||||
/// query is awaited inside the command, so the matches are returned in one call. Each entry carries
|
||||
/// <see cref="PackageSummary.IsInstalled"/> so callers can tell which results are already installed.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class PackageSearchResponse
|
||||
{
|
||||
[JsonProperty("success")] public bool Success { get; set; }
|
||||
|
||||
/// <summary>The search query (empty when listing all available packages).</summary>
|
||||
[JsonProperty("query")] public string Query { get; set; }
|
||||
|
||||
[JsonProperty("count")] public int Count { get; set; }
|
||||
|
||||
/// <summary>The matching packages available in the registry.</summary>
|
||||
[JsonProperty("packages")] public List<PackageSummary> Packages { get; set; }
|
||||
|
||||
[JsonProperty("message")] public string Message { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persisted status of the last async mutating operation (add / remove / resolve), written to a Temp
|
||||
/// status file that survives the domain reload an add/remove/resolve triggers (CLI-203). Read back
|
||||
/// verbatim by <c>package_status</c> so a caller can poll an async op — or validate one whose
|
||||
/// synchronous reply was lost to the reload.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class PackageStatus
|
||||
{
|
||||
/// <summary>idle | in_progress | completed | failed.</summary>
|
||||
[JsonProperty("status")] public string Status { get; set; }
|
||||
|
||||
/// <summary>add | remove | resolve.</summary>
|
||||
[JsonProperty("operation")] public string Operation { get; set; }
|
||||
|
||||
/// <summary>The operation's subject (identifier/name), when applicable.</summary>
|
||||
[JsonProperty("argument")] public string Argument { get; set; }
|
||||
|
||||
[JsonProperty("success")] public bool Success { get; set; }
|
||||
|
||||
[JsonProperty("error")] public string Error { get; set; }
|
||||
|
||||
[JsonProperty("message")] public string Message { get; set; }
|
||||
|
||||
/// <summary>The operation triggers a recompile/domain reload; poll <c>recompile_status</c>.</summary>
|
||||
[JsonProperty("requiresRecompile")] public bool RequiresRecompile { get; set; }
|
||||
|
||||
/// <summary>The added package (add only); null for remove/resolve.</summary>
|
||||
[JsonProperty("package")] public PackageSummary Package { get; set; }
|
||||
|
||||
/// <summary>Manifest dependencies (name → version) after the operation.</summary>
|
||||
[JsonProperty("manifest")] public Dictionary<string, string> Manifest { get; set; }
|
||||
|
||||
[JsonProperty("startedAt")] public string StartedAt { get; set; }
|
||||
[JsonProperty("completedAt")] public string CompletedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a mutating package command — <c>package_add</c> / <c>package_remove</c> /
|
||||
/// <c>package_resolve</c> (CLI-203). These are dual-mode: by default the call returns
|
||||
/// <c>in_progress</c> immediately (poll <c>package_status</c>); with <c>wait=true</c> it blocks and
|
||||
/// this carries the final <c>completed</c>/<c>failed</c> outcome. It reports the <c>confirm</c>/<c>dry_run</c>
|
||||
/// gate via <see cref="DryRun"/> / <see cref="Applied"/>. A successful add/remove
|
||||
/// leaves a recompile/domain reload in flight (<see cref="RequiresRecompile"/>) — poll
|
||||
/// <c>recompile_status</c> to wait for the editor to be ready.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class PackageMutationResponse
|
||||
{
|
||||
[JsonProperty("success")] public bool Success { get; set; }
|
||||
|
||||
/// <summary>add | remove | resolve.</summary>
|
||||
[JsonProperty("operation")] public string Operation { get; set; }
|
||||
|
||||
/// <summary>The operation's subject (identifier/name), when applicable.</summary>
|
||||
[JsonProperty("argument")] public string Argument { get; set; }
|
||||
|
||||
/// <summary>in_progress | completed | dry_run | rejected | failed | busy.</summary>
|
||||
[JsonProperty("status")] public string Status { get; set; }
|
||||
|
||||
/// <summary>True only when the operation actually mutated the project.</summary>
|
||||
[JsonProperty("applied")] public bool Applied { get; set; }
|
||||
|
||||
[JsonProperty("dryRun")] public bool DryRun { get; set; }
|
||||
|
||||
/// <summary>Human-readable description of the intended/performed change.</summary>
|
||||
[JsonProperty("plan")] public string Plan { get; set; }
|
||||
|
||||
/// <summary>The operation leaves a recompile/domain reload in flight; poll <c>recompile_status</c>.</summary>
|
||||
[JsonProperty("requiresRecompile")] public bool RequiresRecompile { get; set; }
|
||||
|
||||
/// <summary>The added package (add only); null for remove/resolve.</summary>
|
||||
[JsonProperty("package")] public PackageSummary Package { get; set; }
|
||||
|
||||
/// <summary>Manifest dependencies (name → version) after the operation.</summary>
|
||||
[JsonProperty("manifest")] public Dictionary<string, string> Manifest { get; set; }
|
||||
|
||||
[JsonProperty("message")] public string Message { get; set; }
|
||||
|
||||
public static PackageMutationResponse InProgress(string operation, string argument, string plan) =>
|
||||
new PackageMutationResponse
|
||||
{
|
||||
Success = true,
|
||||
Operation = operation,
|
||||
Argument = argument,
|
||||
Status = "in_progress",
|
||||
Applied = true,
|
||||
Plan = plan,
|
||||
Message = $"{operation} started. Poll package_status until status is 'completed' or 'failed'."
|
||||
};
|
||||
|
||||
public static PackageMutationResponse Busy(string operation) =>
|
||||
new PackageMutationResponse
|
||||
{
|
||||
Success = false,
|
||||
Operation = operation,
|
||||
Status = "busy",
|
||||
Message = "Another package operation is in progress. Poll package_status, then retry."
|
||||
};
|
||||
|
||||
public static PackageMutationResponse DryRunPreview(string operation, string argument, string plan, string message) =>
|
||||
new PackageMutationResponse
|
||||
{
|
||||
Success = true,
|
||||
Operation = operation,
|
||||
Argument = argument,
|
||||
Status = "dry_run",
|
||||
DryRun = true,
|
||||
Plan = plan,
|
||||
Message = message
|
||||
};
|
||||
|
||||
public static PackageMutationResponse Rejected(string operation, string argument, string message) =>
|
||||
new PackageMutationResponse
|
||||
{
|
||||
Success = false,
|
||||
Operation = operation,
|
||||
Argument = argument,
|
||||
Status = "rejected",
|
||||
Message = message
|
||||
};
|
||||
|
||||
public static PackageMutationResponse Failed(string operation, string argument, string message) =>
|
||||
new PackageMutationResponse
|
||||
{
|
||||
Success = false,
|
||||
Operation = operation,
|
||||
Argument = argument,
|
||||
Status = "failed",
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8fa665b51bbb4f2d8543fc02de12c377
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user