{"id":20139,"plugin_id":"plugins_6aa1c02597c081918e358d72f65bd772","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:16:01.794Z","digest":"b5c012ea34009978c30438634e6b4c5289675ae4472acf95e95241523207844d","against":null,"payload":{"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\".","included_files":[{"relative_path":"references/select-packages.md","size_in_bytes":5868}],"name":"unity-package-management","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"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}