Update to Unity
Snapshot Oct 24, 2026 · 13:03 UTC · version 0.1.8-beta
Collection source: downloaded plugin package. These snapshots do not have a confirmed matching collection source. Differences in file lists alone do not establish changes to the package.
Instructions updated for unity-package-management
Full description — Now says: Adds, removes, upgrades, and discovers Unity (UPM) packages from outside the Editor. Use for headless or CI package installs, verifying package IDs and...
Observed in instructions or declared skills. Runtime behavior has not been tested.
Up to 10 plugins free · no card required.
All fields and original values.
Full description
Sep 30, 2026 · 23:16 UTC
Use when adding, removing, upgrading, or discovering Unity (UPM) packages programmatically from outside the Editor — headless or CI package installs via the C# UnityEditor.PackageManager.Client API, verifying package ids/versions against...
Oct 24, 2026 · 13:03 UTC
Adds, removes, upgrades, and discovers Unity (UPM) packages from outside the Editor. Use for headless or CI package installs, verifying package IDs and versions, or choosing which packages a game needs. The Unity CLI doesn't manage packa...
Skill instructions
Sep 30, 2026 · 23:16 UTC
Use when adding, removing, upgrading, or discovering Unity (UPM) packages programmatically from outside the Editor — headless or CI package installs via the C# UnityEditor.PackageManager.Client API, verifying package ids/versions against...
Oct 24, 2026 · 13:03 UTC
Adds, removes, upgrades, and discovers Unity (UPM) packages from outside the Editor. Use for headless or CI package installs, verifying package IDs and versions, or choosing which packages a game needs. The Unity CLI doesn't manage packa...
Supporting files
Sep 30, 2026 · 23:16 UTC
[{"relative_path":"references/select-packages.md","size_in_bytes":5868}]
Oct 24, 2026 · 13:03 UTC
[{"relative_path":"references/select-packages.md","size_in_bytes":6690}]
Compare saved observations
Download comparison JSONSKILL.md text changes
− removed · + added
--- before +++ after @@ -1,6 +1,6 @@ --- name: unity-package-management -description: Use when adding, removing, upgrading, or discovering Unity (UPM) packages programmatically from outside the Editor — headless or CI package installs via the C# UnityEditor.PackageManager.Client API, verifying package ids/versions against the Unity registry, or choosing which packages a game needs by genre, platform, and monetization. The Unity CLI does not manage UPM packages, so this skill covers that gap. Triggers on "install a Unity package", "add com.unity.*", "set up packages headless/CI", "which packages for a [genre] game". +description: Adds, removes, upgrades, and discovers Unity (UPM) packages from outside the Editor. Use for headless or CI package installs, verifying package IDs and versions, or choosing which packages a game needs. The Unity CLI doesn't manage packages. allowed-tools: - Bash - Read
Full technical diff · 3 changed fields
changed /description
"Use when adding, removing, upgrading, or discovering Unity (UPM) packages programmatically from outside the Editor — headless or CI package installs via the C# UnityEditor.PackageManager.Client API, verifying package ids/versions against the Unity registry, or choosing which packages a game needs by genre, platform, and monetization. The Unity CLI does not manage UPM packages, so this skill covers that gap. Triggers on \"install a Unity package\", \"add com.unity.*\", \"set up packages headless/CI\", \"which packages for a [genre] game\"."
"Adds, removes, upgrades, and discovers Unity (UPM) packages from outside the Editor. Use for headless or CI package installs, verifying package IDs and versions, or choosing which packages a game needs. The Unity CLI doesn't manage packages."
changed /included_files
[
{
"relative_path": "references/select-packages.md",
"size_in_bytes": 5868
}
][
{
"relative_path": "references/select-packages.md",
"size_in_bytes": 6690
}
]changed /skill_md_contents
"---\nname: unity-package-management\ndescription: Use when adding, removing, upgrading, or discovering Unity (UPM) packages programmatically from outside the Editor — headless or CI package installs via the C# UnityEditor.PackageManager.Client API, verifying package ids/versions against the Unity registry, or choosing which packages a game needs by genre, platform, and monetization. The Unity CLI does not manage UPM packages, so this skill covers that gap. Triggers on \"install a Unity package\", \"add com.unity.*\", \"set up packages headless/CI\", \"which packages for a [genre] game\".\nallowed-tools:\n - Bash\n - Read\n - Write\n - Edit\n---\n\n# Unity Package Management (headless, via the C# Client API)\n\nAdd, remove, upgrade, and discover UPM (Unity Package Manager) packages programmatically with\n`UnityEditor.PackageManager.Client`, driven headless from the terminal or CI. Do **not**\nhand-edit `Packages/manifest.json` — the Client API resolves dependencies and compatible\nversions correctly, whereas manual edits routinely break resolution.\n\nThis complements the **`unity-cli`** skill (editor install, project creation, build/test): the\nCLI has **no** package-management command, so all package work goes through the Editor's C# API.\n\n## When to use\n\n- Add / remove / upgrade one or more packages in an existing or freshly-created project.\n- Set up a project's packages non-interactively in CI.\n- Verify a package id exists, or find its available versions, before depending on it.\n- Decide which packages a game actually needs — see\n [references/select-packages.md](references/select-packages.md).\n\n## Choosing what to install\n\nInstall what the project actually needs, not everything; prefer packages the chosen template\nalready provides (URP templates already include the render pipeline, Input System, etc.). The\ngenre / look / platform / monetization → package mapping, plus how to search the registry, is\nin [references/select-packages.md](references/select-packages.md). Produce a **deduplicated\nlist of package ids** and read it back to the user before installing.\n\n## The `-quit` problem — why NOT `unity run` for installs\n\n`Client.Add` / `Client.AddAndRemove` are **asynchronous**: they return a `Request` that only\ncompletes on later `EditorApplication.update` ticks (the UPM child process marshals its result\nback on the Editor's main-loop pump, so a blocking `while (!req.IsCompleted)` busy-wait\ndeadlocks it). The Editor must **stay alive** after `-executeMethod` returns, until the request\nfinishes.\n\n`unity run` **cannot** be used for the installer: its default path injects `-quit` (see the reserved\nflags in the **`unity-cli`** skill). With `-quit`, the Editor quits the instant the method\nreturns — before UPM resolves — so packages never install and the callback never runs.\n\n**Solution:** launch the **Editor binary directly** in `-batchmode` **without** `-quit`. The\nEditor stays alive, `EditorApplication.update` keeps ticking, the poll callback runs, and it\ncalls `EditorApplication.Exit(code)` itself when done — which both quits and sets the process\nexit code.\n\n## The installer script\n\nWrite this to `Assets/Editor/ProjectBootstrap/PackageInstaller.cs`. It must live under an\n`Editor/` folder (or an Editor-only assembly) because it uses `UnityEditor`.\n\n```csharp\nusing System.Linq;\nusing UnityEditor;\nusing UnityEditor.PackageManager;\nusing UnityEditor.PackageManager.Requests;\nusing UnityEngine;\n\nnamespace ProjectBootstrap\n{\n // Installs (and optionally removes) a fixed set of packages via the PackageManager\n // Client API, headless-safe.\n public static class PackageInstaller\n {\n // EDIT this list to match the package selection (see references/select-packages.md).\n static readonly string[] PackagesToAdd =\n {\n \"com.unity.inputsystem\",\n \"com.unity.cinemachine\",\n \"com.unity.render-pipelines.universal\",\n // \"com.unity.package@1.2.3\" // pin a version with @ when a minimum is required\n };\n\n // Optionally drop packages in the same resolution pass (e.g. a template default you don't want).\n static readonly string[] PackagesToRemove = { };\n\n const double TimeoutSeconds = 600; // UPM resolution + downloads can be slow\n\n static AddAndRemoveRequest _request;\n static double _deadline;\n\n // Invoke with: -executeMethod ProjectBootstrap.PackageInstaller.Install (NO -quit)\n public static void Install()\n {\n if (PackagesToAdd.Length == 0 && PackagesToRemove.Length == 0)\n {\n Debug.Log(\"[PackageInstaller] Nothing to do.\");\n EditorApplication.Exit(0);\n return;\n }\n\n Debug.Log($\"[PackageInstaller] Adding: {string.Join(\", \", PackagesToAdd)}\");\n _request = Client.AddAndRemove(packagesToAdd: PackagesToAdd, packagesToRemove: PackagesToRemove);\n _deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;\n EditorApplication.update += Poll;\n }\n\n static void Poll()\n {\n if (_request == null) return;\n\n if (!_request.IsCompleted)\n {\n if (EditorApplication.timeSinceStartup > _deadline)\n {\n EditorApplication.update -= Poll;\n Debug.LogError(\"[PackageInstaller] Timed out waiting for UPM.\");\n EditorApplication.Exit(2);\n }\n return;\n }\n\n EditorApplication.update -= Poll;\n\n if (_request.Status == StatusCode.Success)\n {\n var names = _request.Result.Select(p => $\"{p.name}@{p.version}\");\n Debug.Log($\"[PackageInstaller] Resolved: {string.Join(\", \", names)}\");\n EditorApplication.Exit(0);\n }\n else\n {\n Debug.LogError($\"[PackageInstaller] Failed: {_request.Error?.message}\");\n EditorApplication.Exit(1);\n }\n }\n }\n}\n```\n\n`AddAndRemove` installs the whole set in a single UPM resolution pass — faster and less\nerror-prone than one `Client.Add` per package.\n\n**Add / remove / upgrade with one script:**\n- **Add**: list the id in `PackagesToAdd`.\n- **Remove**: list the id in `PackagesToRemove`.\n- **Upgrade / pin**: add the id with `@<version>` (e.g. `com.unity.cinemachine@2.9.7`). Without\n a version, resolution picks the latest compatible release.\n\n## Discovering / verifying packages\n\nTo confirm an id exists or list its versions before adding it, search the registry. The\nin-Editor `Client.SearchAll()` / `Client.Search(\"<id>\")` calls are also async, so they use the\n**same poll-and-`Exit` pattern and the same headless run** as the installer. Write\n`Assets/Editor/ProjectBootstrap/PackageSearch.cs`:\n\n```csharp\nusing System.Linq;\nusing UnityEditor;\nusing UnityEditor.PackageManager;\nusing UnityEditor.PackageManager.Requests;\nusing UnityEngine;\n\nnamespace ProjectBootstrap\n{\n public static class PackageSearch\n {\n const double TimeoutSeconds = 120;\n static SearchRequest _request;\n static double _deadline;\n\n // Invoke with: -executeMethod ProjectBootstrap.PackageSearch.SearchAll (NO -quit)\n public static void SearchAll()\n {\n _request = Client.SearchAll(); // or Client.Search(\"com.unity.cinemachine\")\n _deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;\n EditorApplication.update += Poll;\n }\n\n static void Poll()\n {\n if (_request == null) return;\n if (!_request.IsCompleted)\n {\n if (EditorApplication.timeSinceStartup > _deadline)\n {\n EditorApplication.update -= Poll;\n Debug.LogError(\"[PackageSearch] Timed out.\");\n EditorApplication.Exit(2);\n }\n return;\n }\n EditorApplication.update -= Poll;\n\n if (_request.Status == StatusCode.Success)\n {\n foreach (var p in _request.Result.OrderBy(p => p.name))\n Debug.Log($\"[PackageSearch] {p.name}@{p.versions.latestCompatible} {p.displayName}\");\n Debug.Log($\"[PackageSearch] {_request.Result.Length} packages found.\");\n EditorApplication.Exit(0);\n }\n else\n {\n Debug.LogError($\"[PackageSearch] Failed: {_request.Error?.message}\");\n EditorApplication.Exit(1);\n }\n }\n }\n}\n```\n\n`_request.Result` is a `PackageInfo[]`; each entry exposes `name`, `displayName`, `description`,\nand `versions` (`.latest`, `.latestCompatible`, `.all`). For a terminal-only check without the\nEditor (a **known** id, not free-text search), query the registry directly — see\n[references/select-packages.md](references/select-packages.md#discovering-and-verifying-packages).\n\n## Run it headless (direct Editor invocation, no `-quit`)\n\nResolve the Editor binary from the version, then run it in batch mode. The script owns quitting\nvia `EditorApplication.Exit`, so do **not** pass `-quit`:\n\n```bash\nVERSION=\"<version>\" # e.g. 6000.0.47f1 (or an installed version)\nPROJECT=\"<project-path>\"\nMETHOD=\"ProjectBootstrap.PackageInstaller.Install\" # or ...PackageSearch.SearchAll\n\n# Install directory of that editor (Hub layout), via the unity CLI\nED=$(unity editors path \"$VERSION\" --format json | python3 -c \"import sys,json;print(json.load(sys.stdin)['data']['path'])\")\n\n# Resolve the executable per-OS (handles both \"dir containing Unity.app\" and the \".app\" itself)\ncase \"$(uname)\" in\n Darwin) if [ -d \"$ED/Unity.app\" ]; then UNITY_BIN=\"$ED/Unity.app/Contents/MacOS/Unity\";\n elif [[ \"$ED\" == *.app ]]; then UNITY_BIN=\"$ED/Contents/MacOS/Unity\";\n else UNITY_BIN=\"$ED/Unity\"; fi ;;\n Linux) UNITY_BIN=\"$ED/Editor/Unity\" ;;\n *) UNITY_BIN=\"$ED/Editor/Unity.exe\" ;; # Windows (Git Bash / MSYS); use Editor\\Unity.exe in PowerShell\nesac\n\n\"$UNITY_BIN\" -batchmode -projectPath \"$PROJECT\" -executeMethod \"$METHOD\" -logFile -\necho \"Exit code: $?\" # 0 = success, 1 = UPM error, 2 = timeout\n```\n\n`-logFile -` streams the Editor log (including the `[PackageInstaller]` / `[PackageSearch]`\nlines) to stdout so you can watch resolution progress and read any UPM error. If\n`unity editors path` output shape differs on your build, get the directory from\n`unity editors --installed --format json` instead.\n\n## Verify\n\n```bash\n# Every requested id should appear as a dependency\ncat \"<project-path>/Packages/manifest.json\"\n```\n\nConfirm the run exited `0` and each package from the list is present in `manifest.json`. If a\npackage fails to resolve, `_request.Error.message` is logged; read it and check the id/version\nagainst the registry. The Editor's own log (including the `[PackageInstaller]` lines) is the\nstdout you streamed with `-logFile -` above — read it there, not via `unity logs` (which shows\nthe CLI's own log, not the Editor's).\n\n## Import & save headlessly (generate `.meta` files)\n\nAfter a script or tool writes new `.cs`/asset files, Unity must **import** them so it generates\nthe `.meta` file each asset needs — and every `.cs`/asset MUST be committed together with its\n`.meta`. Merely opening the project once (`unity open \"<project-path>\"`) imports and generates\nthem; use this method when you need it **headless** (in a script or CI).\n\nUnlike the package installer, this is **synchronous** — it finishes before returning — so it's\nsafe to run via `unity run` (its injected `-quit` is harmless; the method also calls\n`EditorApplication.Exit` for a clean exit code). Write\n`Assets/Editor/ProjectBootstrap/ProjectSaver.cs`:\n\n```csharp\nusing UnityEditor;\nusing UnityEngine;\n\nnamespace ProjectBootstrap\n{\n public static class ProjectSaver\n {\n // Invoke with: -executeMethod ProjectBootstrap.ProjectSaver.SaveAll\n public static void SaveAll()\n {\n AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);\n AssetDatabase.SaveAssets();\n Debug.Log(\"[ProjectSaver] Assets imported and saved.\");\n EditorApplication.Exit(0);\n }\n }\n}\n```\n\n```bash\nunity run \"<project-path>\" --editor-version <version> \\\n -- -executeMethod ProjectBootstrap.ProjectSaver.SaveAll\n```\n\n## Notes\n\n- These editor scripts are a bootstrap convenience. Leave them in\n `Assets/Editor/ProjectBootstrap/` (they do nothing unless invoked) or delete them after\n setup — your call; mention it to the user.\n- All scripts live under `Editor/` because they use `UnityEditor`; they never ship in a build.\n- Monetization / backend packages (`com.unity.purchasing`, `com.unity.services.levelplay`, the\n UGS packages) install through this same mechanism, but do the actual **integration** via the\n dedicated skills: **implement-in-app-purchases**, **levelplay-unity-integration**,\n **build-live-game**.\n""---\nname: unity-package-management\ndescription: Adds, removes, upgrades, and discovers Unity (UPM) packages from outside the Editor. Use for headless or CI package installs, verifying package IDs and versions, or choosing which packages a game needs. The Unity CLI doesn't manage packages.\nallowed-tools:\n - Bash\n - Read\n - Write\n - Edit\n---\n\n# Unity Package Management (headless, via the C# Client API)\n\nAdd, remove, upgrade, and discover UPM (Unity Package Manager) packages programmatically with\n`UnityEditor.PackageManager.Client`, driven headless from the terminal or CI. Do **not**\nhand-edit `Packages/manifest.json` — the Client API resolves dependencies and compatible\nversions correctly, whereas manual edits routinely break resolution.\n\nThis complements the **`unity-cli`** skill (editor install, project creation, build/test): the\nCLI has **no** package-management command, so all package work goes through the Editor's C# API.\n\n## When to use\n\n- Add / remove / upgrade one or more packages in an existing or freshly-created project.\n- Set up a project's packages non-interactively in CI.\n- Verify a package id exists, or find its available versions, before depending on it.\n- Decide which packages a game actually needs — see\n [references/select-packages.md](references/select-packages.md).\n\n## Choosing what to install\n\nInstall what the project actually needs, not everything; prefer packages the chosen template\nalready provides (URP templates already include the render pipeline, Input System, etc.). The\ngenre / look / platform / monetization → package mapping, plus how to search the registry, is\nin [references/select-packages.md](references/select-packages.md). Produce a **deduplicated\nlist of package ids** and read it back to the user before installing.\n\n## The `-quit` problem — why NOT `unity run` for installs\n\n`Client.Add` / `Client.AddAndRemove` are **asynchronous**: they return a `Request` that only\ncompletes on later `EditorApplication.update` ticks (the UPM child process marshals its result\nback on the Editor's main-loop pump, so a blocking `while (!req.IsCompleted)` busy-wait\ndeadlocks it). The Editor must **stay alive** after `-executeMethod` returns, until the request\nfinishes.\n\n`unity run` **cannot** be used for the installer: its default path injects `-quit` (see the reserved\nflags in the **`unity-cli`** skill). With `-quit`, the Editor quits the instant the method\nreturns — before UPM resolves — so packages never install and the callback never runs.\n\n**Solution:** launch the **Editor binary directly** in `-batchmode` **without** `-quit`. The\nEditor stays alive, `EditorApplication.update` keeps ticking, the poll callback runs, and it\ncalls `EditorApplication.Exit(code)` itself when done — which both quits and sets the process\nexit code.\n\n## The installer script\n\nWrite this to `Assets/Editor/ProjectBootstrap/PackageInstaller.cs`. It must live under an\n`Editor/` folder (or an Editor-only assembly) because it uses `UnityEditor`.\n\n```csharp\nusing System.Linq;\nusing UnityEditor;\nusing UnityEditor.PackageManager;\nusing UnityEditor.PackageManager.Requests;\nusing UnityEngine;\n\nnamespace ProjectBootstrap\n{\n // Installs (and optionally removes) a fixed set of packages via the PackageManager\n // Client API, headless-safe.\n public static class PackageInstaller\n {\n // EDIT this list to match the package selection (see references/select-packages.md).\n static readonly string[] PackagesToAdd =\n {\n \"com.unity.inputsystem\",\n \"com.unity.cinemachine\",\n \"com.unity.render-pipelines.universal\",\n // \"com.unity.package@1.2.3\" // pin a version with @ when a minimum is required\n };\n\n // Optionally drop packages in the same resolution pass (e.g. a template default you don't want).\n static readonly string[] PackagesToRemove = { };\n\n const double TimeoutSeconds = 600; // UPM resolution + downloads can be slow\n\n static AddAndRemoveRequest _request;\n static double _deadline;\n\n // Invoke with: -executeMethod ProjectBootstrap.PackageInstaller.Install (NO -quit)\n public static void Install()\n {\n if (PackagesToAdd.Length == 0 && PackagesToRemove.Length == 0)\n {\n Debug.Log(\"[PackageInstaller] Nothing to do.\");\n EditorApplication.Exit(0);\n return;\n }\n\n Debug.Log($\"[PackageInstaller] Adding: {string.Join(\", \", PackagesToAdd)}\");\n _request = Client.AddAndRemove(packagesToAdd: PackagesToAdd, packagesToRemove: PackagesToRemove);\n _deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;\n EditorApplication.update += Poll;\n }\n\n static void Poll()\n {\n if (_request == null) return;\n\n if (!_request.IsCompleted)\n {\n if (EditorApplication.timeSinceStartup > _deadline)\n {\n EditorApplication.update -= Poll;\n Debug.LogError(\"[PackageInstaller] Timed out waiting for UPM.\");\n EditorApplication.Exit(2);\n }\n return;\n }\n\n EditorApplication.update -= Poll;\n\n if (_request.Status == StatusCode.Success)\n {\n var names = _request.Result.Select(p => $\"{p.name}@{p.version}\");\n Debug.Log($\"[PackageInstaller] Resolved: {string.Join(\", \", names)}\");\n EditorApplication.Exit(0);\n }\n else\n {\n Debug.LogError($\"[PackageInstaller] Failed: {_request.Error?.message}\");\n EditorApplication.Exit(1);\n }\n }\n }\n}\n```\n\n`AddAndRemove` installs the whole set in a single UPM resolution pass — faster and less\nerror-prone than one `Client.Add` per package.\n\n**Add / remove / upgrade with one script:**\n- **Add**: list the id in `PackagesToAdd`.\n- **Remove**: list the id in `PackagesToRemove`.\n- **Upgrade / pin**: add the id with `@<version>` (e.g. `com.unity.cinemachine@2.9.7`). Without\n a version, resolution picks the latest compatible release.\n\n## Discovering / verifying packages\n\nTo confirm an id exists or list its versions before adding it, search the registry. The\nin-Editor `Client.SearchAll()` / `Client.Search(\"<id>\")` calls are also async, so they use the\n**same poll-and-`Exit` pattern and the same headless run** as the installer. Write\n`Assets/Editor/ProjectBootstrap/PackageSearch.cs`:\n\n```csharp\nusing System.Linq;\nusing UnityEditor;\nusing UnityEditor.PackageManager;\nusing UnityEditor.PackageManager.Requests;\nusing UnityEngine;\n\nnamespace ProjectBootstrap\n{\n public static class PackageSearch\n {\n const double TimeoutSeconds = 120;\n static SearchRequest _request;\n static double _deadline;\n\n // Invoke with: -executeMethod ProjectBootstrap.PackageSearch.SearchAll (NO -quit)\n public static void SearchAll()\n {\n _request = Client.SearchAll(); // or Client.Search(\"com.unity.cinemachine\")\n _deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;\n EditorApplication.update += Poll;\n }\n\n static void Poll()\n {\n if (_request == null) return;\n if (!_request.IsCompleted)\n {\n if (EditorApplication.timeSinceStartup > _deadline)\n {\n EditorApplication.update -= Poll;\n Debug.LogError(\"[PackageSearch] Timed out.\");\n EditorApplication.Exit(2);\n }\n return;\n }\n EditorApplication.update -= Poll;\n\n if (_request.Status == StatusCode.Success)\n {\n foreach (var p in _request.Result.OrderBy(p => p.name))\n Debug.Log($\"[PackageSearch] {p.name}@{p.versions.latestCompatible} {p.displayName}\");\n Debug.Log($\"[PackageSearch] {_request.Result.Length} packages found.\");\n EditorApplication.Exit(0);\n }\n else\n {\n Debug.LogError($\"[PackageSearch] Failed: {_request.Error?.message}\");\n EditorApplication.Exit(1);\n }\n }\n }\n}\n```\n\n`_request.Result` is a `PackageInfo[]`; each entry exposes `name`, `displayName`, `description`,\nand `versions` (`.latest`, `.latestCompatible`, `.all`). For a terminal-only check without the\nEditor (a **known** id, not free-text search), query the registry directly — see\n[references/select-packages.md](references/select-packages.md#discovering-and-verifying-packages).\n\n## Run it headless (direct Editor invocation, no `-quit`)\n\nResolve the Editor binary from the version, then run it in batch mode. The script owns quitting\nvia `EditorApplication.Exit`, so do **not** pass `-quit`:\n\n```bash\nVERSION=\"<version>\" # e.g. 6000.0.47f1 (or an installed version)\nPROJECT=\"<project-path>\"\nMETHOD=\"ProjectBootstrap.PackageInstaller.Install\" # or ...PackageSearch.SearchAll\n\n# Install directory of that editor (Hub layout), via the unity CLI\nED=$(unity editors path \"$VERSION\" --format json | python3 -c \"import sys,json;print(json.load(sys.stdin)['data']['path'])\")\n\n# Resolve the executable per-OS (handles both \"dir containing Unity.app\" and the \".app\" itself)\ncase \"$(uname)\" in\n Darwin) if [ -d \"$ED/Unity.app\" ]; then UNITY_BIN=\"$ED/Unity.app/Contents/MacOS/Unity\";\n elif [[ \"$ED\" == *.app ]]; then UNITY_BIN=\"$ED/Contents/MacOS/Unity\";\n else UNITY_BIN=\"$ED/Unity\"; fi ;;\n Linux) UNITY_BIN=\"$ED/Editor/Unity\" ;;\n *) UNITY_BIN=\"$ED/Editor/Unity.exe\" ;; # Windows (Git Bash / MSYS); use Editor\\Unity.exe in PowerShell\nesac\n\n\"$UNITY_BIN\" -batchmode -projectPath \"$PROJECT\" -executeMethod \"$METHOD\" -logFile -\necho \"Exit code: $?\" # 0 = success, 1 = UPM error, 2 = timeout\n```\n\n`-logFile -` streams the Editor log (including the `[PackageInstaller]` / `[PackageSearch]`\nlines) to stdout so you can watch resolution progress and read any UPM error. If\n`unity editors path` output shape differs on your build, get the directory from\n`unity editors --installed --format json` instead.\n\n## Verify\n\n```bash\n# Every requested id should appear as a dependency\ncat \"<project-path>/Packages/manifest.json\"\n```\n\nConfirm the run exited `0` and each package from the list is present in `manifest.json`. If a\npackage fails to resolve, `_request.Error.message` is logged; read it and check the id/version\nagainst the registry. The Editor's own log (including the `[PackageInstaller]` lines) is the\nstdout you streamed with `-logFile -` above — read it there, not via `unity logs` (which shows\nthe CLI's own log, not the Editor's).\n\n## Import & save headlessly (generate `.meta` files)\n\nAfter a script or tool writes new `.cs`/asset files, Unity must **import** them so it generates\nthe `.meta` file each asset needs — and every `.cs`/asset MUST be committed together with its\n`.meta`. Merely opening the project once (`unity open \"<project-path>\"`) imports and generates\nthem; use this method when you need it **headless** (in a script or CI).\n\nUnlike the package installer, this is **synchronous** — it finishes before returning — so it's\nsafe to run via `unity run` (its injected `-quit` is harmless; the method also calls\n`EditorApplication.Exit` for a clean exit code). Write\n`Assets/Editor/ProjectBootstrap/ProjectSaver.cs`:\n\n```csharp\nusing UnityEditor;\nusing UnityEngine;\n\nnamespace ProjectBootstrap\n{\n public static class ProjectSaver\n {\n // Invoke with: -executeMethod ProjectBootstrap.ProjectSaver.SaveAll\n public static void SaveAll()\n {\n AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);\n AssetDatabase.SaveAssets();\n Debug.Log(\"[ProjectSaver] Assets imported and saved.\");\n EditorApplication.Exit(0);\n }\n }\n}\n```\n\n```bash\nunity run \"<project-path>\" --editor-version <version> \\\n -- -executeMethod ProjectBootstrap.ProjectSaver.SaveAll\n```\n\n## Notes\n\n- These editor scripts are a bootstrap convenience. Leave them in\n `Assets/Editor/ProjectBootstrap/` (they do nothing unless invoked) or delete them after\n setup — your call; mention it to the user.\n- All scripts live under `Editor/` because they use `UnityEditor`; they never ship in a build.\n- Monetization / backend packages (`com.unity.purchasing`, `com.unity.services.levelplay`, the\n UGS packages) install through this same mechanism, but do the actual **integration** via the\n dedicated skills: **implement-in-app-purchases**, **levelplay-unity-integration**,\n **build-live-game**.\n"Full snapshot data
{
"description": "Adds, removes, upgrades, and discovers Unity (UPM) packages from outside the Editor. Use for headless or CI package installs, verifying package IDs and versions, or choosing which packages a game needs. The Unity CLI doesn't manage packages.",
"included_files": [
{
"relative_path": "references/select-packages.md",
"size_in_bytes": 6690
}
],
"name": "unity-package-management",
"skill_md_contents": "---\nname: unity-package-management\ndescription: Adds, removes, upgrades, and discovers Unity (UPM) packages from outside the Editor. Use for headless or CI package installs, verifying package IDs and versions, or choosing which packages a game needs. The Unity CLI doesn't manage packages.\nallowed-tools:\n - Bash\n - Read\n - Write\n - Edit\n---\n\n# Unity Package Management (headless, via the C# Client API)\n\nAdd, remove, upgrade, and discover UPM (Unity Package Manager) packages programmatically with\n`UnityEditor.PackageManager.Client`, driven headless from the terminal or CI. Do **not**\nhand-edit `Packages/manifest.json` — the Client API resolves dependencies and compatible\nversions correctly, whereas manual edits routinely break resolution.\n\nThis complements the **`unity-cli`** skill (editor install, project creation, build/test): the\nCLI has **no** package-management command, so all package work goes through the Editor's C# API.\n\n## When to use\n\n- Add / remove / upgrade one or more packages in an existing or freshly-created project.\n- Set up a project's packages non-interactively in CI.\n- Verify a package id exists, or find its available versions, before depending on it.\n- Decide which packages a game actually needs — see\n [references/select-packages.md](references/select-packages.md).\n\n## Choosing what to install\n\nInstall what the project actually needs, not everything; prefer packages the chosen template\nalready provides (URP templates already include the render pipeline, Input System, etc.). The\ngenre / look / platform / monetization → package mapping, plus how to search the registry, is\nin [references/select-packages.md](references/select-packages.md). Produce a **deduplicated\nlist of package ids** and read it back to the user before installing.\n\n## The `-quit` problem — why NOT `unity run` for installs\n\n`Client.Add` / `Client.AddAndRemove` are **asynchronous**: they return a `Request` that only\ncompletes on later `EditorApplication.update` ticks (the UPM child process marshals its result\nback on the Editor's main-loop pump, so a blocking `while (!req.IsCompleted)` busy-wait\ndeadlocks it). The Editor must **stay alive** after `-executeMethod` returns, until the request\nfinishes.\n\n`unity run` **cannot** be used for the installer: its default path injects `-quit` (see the reserved\nflags in the **`unity-cli`** skill). With `-quit`, the Editor quits the instant the method\nreturns — before UPM resolves — so packages never install and the callback never runs.\n\n**Solution:** launch the **Editor binary directly** in `-batchmode` **without** `-quit`. The\nEditor stays alive, `EditorApplication.update` keeps ticking, the poll callback runs, and it\ncalls `EditorApplication.Exit(code)` itself when done — which both quits and sets the process\nexit code.\n\n## The installer script\n\nWrite this to `Assets/Editor/ProjectBootstrap/PackageInstaller.cs`. It must live under an\n`Editor/` folder (or an Editor-only assembly) because it uses `UnityEditor`.\n\n```csharp\nusing System.Linq;\nusing UnityEditor;\nusing UnityEditor.PackageManager;\nusing UnityEditor.PackageManager.Requests;\nusing UnityEngine;\n\nnamespace ProjectBootstrap\n{\n // Installs (and optionally removes) a fixed set of packages via the PackageManager\n // Client API, headless-safe.\n public static class PackageInstaller\n {\n // EDIT this list to match the package selection (see references/select-packages.md).\n static readonly string[] PackagesToAdd =\n {\n \"com.unity.inputsystem\",\n \"com.unity.cinemachine\",\n \"com.unity.render-pipelines.universal\",\n // \"com.unity.package@1.2.3\" // pin a version with @ when a minimum is required\n };\n\n // Optionally drop packages in the same resolution pass (e.g. a template default you don't want).\n static readonly string[] PackagesToRemove = { };\n\n const double TimeoutSeconds = 600; // UPM resolution + downloads can be slow\n\n static AddAndRemoveRequest _request;\n static double _deadline;\n\n // Invoke with: -executeMethod ProjectBootstrap.PackageInstaller.Install (NO -quit)\n public static void Install()\n {\n if (PackagesToAdd.Length == 0 && PackagesToRemove.Length == 0)\n {\n Debug.Log(\"[PackageInstaller] Nothing to do.\");\n EditorApplication.Exit(0);\n return;\n }\n\n Debug.Log($\"[PackageInstaller] Adding: {string.Join(\", \", PackagesToAdd)}\");\n _request = Client.AddAndRemove(packagesToAdd: PackagesToAdd, packagesToRemove: PackagesToRemove);\n _deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;\n EditorApplication.update += Poll;\n }\n\n static void Poll()\n {\n if (_request == null) return;\n\n if (!_request.IsCompleted)\n {\n if (EditorApplication.timeSinceStartup > _deadline)\n {\n EditorApplication.update -= Poll;\n Debug.LogError(\"[PackageInstaller] Timed out waiting for UPM.\");\n EditorApplication.Exit(2);\n }\n return;\n }\n\n EditorApplication.update -= Poll;\n\n if (_request.Status == StatusCode.Success)\n {\n var names = _request.Result.Select(p => $\"{p.name}@{p.version}\");\n Debug.Log($\"[PackageInstaller] Resolved: {string.Join(\", \", names)}\");\n EditorApplication.Exit(0);\n }\n else\n {\n Debug.LogError($\"[PackageInstaller] Failed: {_request.Error?.message}\");\n EditorApplication.Exit(1);\n }\n }\n }\n}\n```\n\n`AddAndRemove` installs the whole set in a single UPM resolution pass — faster and less\nerror-prone than one `Client.Add` per package.\n\n**Add / remove / upgrade with one script:**\n- **Add**: list the id in `PackagesToAdd`.\n- **Remove**: list the id in `PackagesToRemove`.\n- **Upgrade / pin**: add the id with `@<version>` (e.g. `com.unity.cinemachine@2.9.7`). Without\n a version, resolution picks the latest compatible release.\n\n## Discovering / verifying packages\n\nTo confirm an id exists or list its versions before adding it, search the registry. The\nin-Editor `Client.SearchAll()` / `Client.Search(\"<id>\")` calls are also async, so they use the\n**same poll-and-`Exit` pattern and the same headless run** as the installer. Write\n`Assets/Editor/ProjectBootstrap/PackageSearch.cs`:\n\n```csharp\nusing System.Linq;\nusing UnityEditor;\nusing UnityEditor.PackageManager;\nusing UnityEditor.PackageManager.Requests;\nusing UnityEngine;\n\nnamespace ProjectBootstrap\n{\n public static class PackageSearch\n {\n const double TimeoutSeconds = 120;\n static SearchRequest _request;\n static double _deadline;\n\n // Invoke with: -executeMethod ProjectBootstrap.PackageSearch.SearchAll (NO -quit)\n public static void SearchAll()\n {\n _request = Client.SearchAll(); // or Client.Search(\"com.unity.cinemachine\")\n _deadline = EditorApplication.timeSinceStartup + TimeoutSeconds;\n EditorApplication.update += Poll;\n }\n\n static void Poll()\n {\n if (_request == null) return;\n if (!_request.IsCompleted)\n {\n if (EditorApplication.timeSinceStartup > _deadline)\n {\n EditorApplication.update -= Poll;\n Debug.LogError(\"[PackageSearch] Timed out.\");\n EditorApplication.Exit(2);\n }\n return;\n }\n EditorApplication.update -= Poll;\n\n if (_request.Status == StatusCode.Success)\n {\n foreach (var p in _request.Result.OrderBy(p => p.name))\n Debug.Log($\"[PackageSearch] {p.name}@{p.versions.latestCompatible} {p.displayName}\");\n Debug.Log($\"[PackageSearch] {_request.Result.Length} packages found.\");\n EditorApplication.Exit(0);\n }\n else\n {\n Debug.LogError($\"[PackageSearch] Failed: {_request.Error?.message}\");\n EditorApplication.Exit(1);\n }\n }\n }\n}\n```\n\n`_request.Result` is a `PackageInfo[]`; each entry exposes `name`, `displayName`, `description`,\nand `versions` (`.latest`, `.latestCompatible`, `.all`). For a terminal-only check without the\nEditor (a **known** id, not free-text search), query the registry directly — see\n[references/select-packages.md](references/select-packages.md#discovering-and-verifying-packages).\n\n## Run it headless (direct Editor invocation, no `-quit`)\n\nResolve the Editor binary from the version, then run it in batch mode. The script owns quitting\nvia `EditorApplication.Exit`, so do **not** pass `-quit`:\n\n```bash\nVERSION=\"<version>\" # e.g. 6000.0.47f1 (or an installed version)\nPROJECT=\"<project-path>\"\nMETHOD=\"ProjectBootstrap.PackageInstaller.Install\" # or ...PackageSearch.SearchAll\n\n# Install directory of that editor (Hub layout), via the unity CLI\nED=$(unity editors path \"$VERSION\" --format json | python3 -c \"import sys,json;print(json.load(sys.stdin)['data']['path'])\")\n\n# Resolve the executable per-OS (handles both \"dir containing Unity.app\" and the \".app\" itself)\ncase \"$(uname)\" in\n Darwin) if [ -d \"$ED/Unity.app\" ]; then UNITY_BIN=\"$ED/Unity.app/Contents/MacOS/Unity\";\n elif [[ \"$ED\" == *.app ]]; then UNITY_BIN=\"$ED/Contents/MacOS/Unity\";\n else UNITY_BIN=\"$ED/Unity\"; fi ;;\n Linux) UNITY_BIN=\"$ED/Editor/Unity\" ;;\n *) UNITY_BIN=\"$ED/Editor/Unity.exe\" ;; # Windows (Git Bash / MSYS); use Editor\\Unity.exe in PowerShell\nesac\n\n\"$UNITY_BIN\" -batchmode -projectPath \"$PROJECT\" -executeMethod \"$METHOD\" -logFile -\necho \"Exit code: $?\" # 0 = success, 1 = UPM error, 2 = timeout\n```\n\n`-logFile -` streams the Editor log (including the `[PackageInstaller]` / `[PackageSearch]`\nlines) to stdout so you can watch resolution progress and read any UPM error. If\n`unity editors path` output shape differs on your build, get the directory from\n`unity editors --installed --format json` instead.\n\n## Verify\n\n```bash\n# Every requested id should appear as a dependency\ncat \"<project-path>/Packages/manifest.json\"\n```\n\nConfirm the run exited `0` and each package from the list is present in `manifest.json`. If a\npackage fails to resolve, `_request.Error.message` is logged; read it and check the id/version\nagainst the registry. The Editor's own log (including the `[PackageInstaller]` lines) is the\nstdout you streamed with `-logFile -` above — read it there, not via `unity logs` (which shows\nthe CLI's own log, not the Editor's).\n\n## Import & save headlessly (generate `.meta` files)\n\nAfter a script or tool writes new `.cs`/asset files, Unity must **import** them so it generates\nthe `.meta` file each asset needs — and every `.cs`/asset MUST be committed together with its\n`.meta`. Merely opening the project once (`unity open \"<project-path>\"`) imports and generates\nthem; use this method when you need it **headless** (in a script or CI).\n\nUnlike the package installer, this is **synchronous** — it finishes before returning — so it's\nsafe to run via `unity run` (its injected `-quit` is harmless; the method also calls\n`EditorApplication.Exit` for a clean exit code). Write\n`Assets/Editor/ProjectBootstrap/ProjectSaver.cs`:\n\n```csharp\nusing UnityEditor;\nusing UnityEngine;\n\nnamespace ProjectBootstrap\n{\n public static class ProjectSaver\n {\n // Invoke with: -executeMethod ProjectBootstrap.ProjectSaver.SaveAll\n public static void SaveAll()\n {\n AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);\n AssetDatabase.SaveAssets();\n Debug.Log(\"[ProjectSaver] Assets imported and saved.\");\n EditorApplication.Exit(0);\n }\n }\n}\n```\n\n```bash\nunity run \"<project-path>\" --editor-version <version> \\\n -- -executeMethod ProjectBootstrap.ProjectSaver.SaveAll\n```\n\n## Notes\n\n- These editor scripts are a bootstrap convenience. Leave them in\n `Assets/Editor/ProjectBootstrap/` (they do nothing unless invoked) or delete them after\n setup — your call; mention it to the user.\n- All scripts live under `Editor/` because they use `UnityEditor`; they never ship in a build.\n- Monetization / backend packages (`com.unity.purchasing`, `com.unity.services.levelplay`, the\n UGS packages) install through this same mechanism, but do the actual **integration** via the\n dedicated skills: **implement-in-app-purchases**, **levelplay-unity-integration**,\n **build-live-game**.\n"
}SHA-256 of public snapshot: 2b6974c11ca9ec91937432a7004c7e3061a053060076187b09d149b9cba1f1a1