diff --git a/skills/build-live-game/SKILL.md b/skills/build-live-game/SKILL.md index 5d5a48e..31031a1 100644 --- a/skills/build-live-game/SKILL.md +++ b/skills/build-live-game/SKILL.md @@ -1,6 +1,6 @@ --- name: build-live-game -description: Builds and operates live games with Unity Services. Use when the user asks about backend or live-ops features such as player accounts, cloud save, leaderboards, achievements, progression and battle passes, economies, cloud code, or remote config. +description: Builds and operates live games with Unity Services. Use when the user asks about backend or live-ops features such as player accounts, cloud save, leaderboards, achievements, progression and battle passes, cloud code, or remote config. --- # Build a Live Game With Unity Gaming Services @@ -60,7 +60,6 @@ After `InitializeAsync()` completes, service singletons (e.g. `CloudSaveService. |---|---|---| | **Cloud Save** | Per-player key-value data (Default, Public, Protected) and game-wide Custom data | `CloudSaveService.Instance.Data.Player` / `.Data.Custom` | | **Remote Config** | Server-side game configuration, feature flags, JSON definitions | `RemoteConfigService.Instance` | -| **Economy** | Virtual currencies, inventory items, purchases, stores | `EconomyService.Instance` | ### Server Logic and Security @@ -94,23 +93,23 @@ After `InitializeAsync()` completes, service singletons (e.g. `CloudSaveService. │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ - Remote Config Cloud Save Economy - (game config, (player state, (currencies, - definitions, progress, inventory, - feature flags) preferences) purchases) - │ │ │ - └───────┬───────┘ │ - ▼ │ - Cloud Code │ - (server-authoritative │ - writes, validation, ◄─────────┘ + Remote Config Cloud Save Multiplayer + (game config, (player state, (sessions, + definitions, progress, matchmaking, + feature flags) preferences) lobbies) + │ │ + └───────┬───────┘ + ▼ + Cloud Code + (server-authoritative + writes, validation, anti-cheat logic) │ - ┌───────┼───────┐ - ▼ ▼ ▼ - Cloud Save Economy Leaderboards - (Protected (server (score - writes) grants) submission) + ┌───────┴───────┐ + ▼ ▼ + Cloud Save Leaderboards + (Protected (score + writes) submission) ``` **Key principle:** For any data that affects game integrity (XP, rewards, currency), route writes through Cloud Code modules. Direct client writes are only appropriate for non-sensitive data (preferences, display settings). @@ -192,14 +191,13 @@ Editor-only package providing the **Deployment Window** (Services > Deployment). | Access Control | `.ac` | Resource access policies | | Cloud Code Module | `.ccmr` | C# server-side module (points to `.sln`) | | Leaderboard | `.lb` | Leaderboard configuration | -| Economy | `.ec*` | Currency/inventory definitions | | Game Overrides | `.ugo` | Audience-targeted config overrides | - **Full reference:** [references/deployment.md](references/deployment.md) ### UGS CLI -The [Unity Gaming Services CLI](https://github.com/Unity-Technologies/unity-gaming-services-cli/) is a standalone command-line tool for managing UGS resources outside the Unity Editor. It can deploy and fetch cloud resource files (`.rc`, `.ac`, `.ccmr`, `.lb`, `.ec`, `.ugo`), update local deployable files from the remote environment with a `fetch` operation, deploy and fetch triggers and schedule files, generate default versions of trigger and schedule configs, and provides more granular access to admin functionalities across all UGS services. +The [Unity Gaming Services CLI](https://github.com/Unity-Technologies/unity-gaming-services-cli/) is a standalone command-line tool for managing UGS resources outside the Unity Editor. It can deploy and fetch cloud resource files (`.rc`, `.ac`, `.ccmr`, `.lb`, `.ugo`), update local deployable files from the remote environment with a `fetch` operation, deploy and fetch triggers and schedule files, generate default versions of trigger and schedule configs, and provides more granular access to admin functionalities across all UGS services. ### Services APIs @@ -231,7 +229,7 @@ Unity provides free, production-ready **Building Block** packages on the Asset S | Project | Description | Source | |---|---|---| | **Use Case Samples** | Battle Pass, Virtual Shop, Daily Rewards, Starter Pack, Cloud AI Mini Game, A/B testing | [GitHub — com.unity.services.samples.use-cases](https://github.com/Unity-Technologies/com.unity.services.samples.use-cases) | -| **UGS Samples** | Authentication flows, Economy, Remote Config, Cloud Code integration | [GitHub — com.unity.services.samples](https://github.com/Unity-Technologies/com.unity.services.samples) | +| **UGS Samples** | Authentication flows, Remote Config, Cloud Code integration | [GitHub — com.unity.services.samples](https://github.com/Unity-Technologies/com.unity.services.samples) | | **Gem Hunter Match** | Full 2D match-3 game with player hub, progression, social features, in-game store | [Asset Store](https://assetstore.unity.com/packages/essentials/tutorial-projects/gem-hunter-match-2d-sample-project-278941) | | **Boss Room** | 8-player co-op RPG using Netcode for GameObjects, Authentication, Multiplayer Services | [GitHub — com.unity.multiplayer.samples.coop](https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop) | @@ -241,7 +239,7 @@ Implementation-ready blueprints for common live game features. Each includes dat | Feature | Key Services | Blueprint | |---|---|---| -| **Battle Pass** | `remote-config`, `cloudsave`, `cloudcode`, `economy`, `tooling`, `deployment` — Remote Config (pass definitions) + Cloud Save Protected (progress) + Cloud Code (XP awards, reward claims, premium purchase) | [references/battlepass.md](references/battlepass.md) | +| **Battle Pass** | `remote-config`, `cloudsave`, `cloudcode`, `tooling`, `deployment` — Remote Config (pass definitions) + Cloud Save Protected (progress) + Cloud Code (XP awards, reward claims, premium purchase) | [references/battlepass.md](references/battlepass.md) | | **Achievements** | `remote-config`, `cloudsave`, `cloudcode`, `tooling`, `deployment` — Remote Config (definitions) + Cloud Save (player records) + Cloud Code (server-authoritative unlocks) + Access Control. **Asset Store:** [Achievements Building Block](https://assetstore.unity.com/packages/essentials/tutorial-projects/unity-building-block-achievements-341918) | [references/achievements.md](references/achievements.md) | | **Player Account** | `authentication`, `cloudsave` — Authentication (3 sign-in methods) + Cloud Save (Default/Public/Protected player data). **Asset Store:** Player Account Building Block (via `com.unity.starter-kits`) | [references/player-account.md](references/player-account.md) | @@ -277,7 +275,7 @@ After writing code for a live game feature: 2. Check that initialization order is correct: `UnityServices.InitializeAsync()` → Authentication sign-in → service calls. 3. Confirm sensitive data writes (XP, rewards, currency) are routed through Cloud Code, not written directly from the client. 4. Verify Access Control `.ac` files deny direct player writes to Protected Cloud Save keys. -5. Verify all cloud resource files (`.rc`, `.ac`, `.ccmr`, `.lb`, `.ec`) are present and deployable via the Deployment Window. +5. Verify all cloud resource files (`.rc`, `.ac`, `.ccmr`, `.lb`) are present and deployable via the Deployment Window. ## Deployment Checklist @@ -287,7 +285,6 @@ For any live game feature, deploy these cloud resources via the Deployment Windo - [ ] `.ac` file — Access Control policies (deny direct writes to protected keys) - [ ] `.ccmr` file — Cloud Code module reference (pointing to the module `.sln`) - [ ] `.lb` file — Leaderboard configuration (if using leaderboards) -- [ ] `.ec` file — Economy definitions (if using virtual currencies/items) - [ ] `manifest.json` — Ensure all required packages are listed with correct versions - [ ] Environment configured in **Services > Deployment** settings @@ -304,7 +301,7 @@ For any live game feature, deploy these cloud resources via the Deployment Windo ### Feature Blueprints - **Achievements** — full implementation with data models, client code, Cloud Code module, cloud resources: [references/achievements.md](references/achievements.md) -- **Battle Pass** — full implementation with tiered XP, free/premium tracks, Cloud Code module, cloud resources: [references/battlepass.md](references/battlepass.md) +- **Battle Pass** — implementation with tiered XP, free/premium tracks, Cloud Code module, cloud resources (premium purchase is an integration stub you must complete): [references/battlepass.md](references/battlepass.md) - **Player Account** — sign-in flows, identity management, Cloud Save data, code templates: [references/player-account.md](references/player-account.md) ## Reminders diff --git a/skills/build-live-game/references/achievements.md b/skills/build-live-game/references/achievements.md index d57b763..ca7a980 100644 --- a/skills/build-live-game/references/achievements.md +++ b/skills/build-live-game/references/achievements.md @@ -513,7 +513,7 @@ public class AchievementManager : MonoBehaviour The `AchievementModule` reads and writes to the **Protected** Cloud Save bucket using its service token, bypassing Access Control restrictions imposed on the player client. -> **Module scaffolding — follow [cloud-code.md — Module Creation](cloud-code.md#module-creation).** +> **Module scaffolding — follow the Module Creation section of `references/cloud-code.md`.** > Replace `MyModule` / `MyModuleCCM` with `AchievementModule` / `AchievementsCCM` throughout. > You **must** create every file listed there — missing any one will cause deployment to fail: > @@ -707,7 +707,7 @@ Deploy via **Services > Deployment** — the Deployment Window auto-discovers `. > [Cloud Code Module](#cloud-code-module) above. > All scaffolding files (`.sln`, `.csproj`, `ModuleSetup.cs`, `FolderProfile.pubxml`) are > **required** — the Deployment Window will fail without them. See -> [cloud-code.md — Module Creation](cloud-code.md#module-creation) for the generic templates; +> the Module Creation section of `references/cloud-code.md` for the generic templates; > replace `MyModule` / `MyModuleCCM` with `AchievementModule` / `AchievementsCCM`. ### Access Control -- `Assets/Config/DenyPlayerCloudSaveWrites.ac` diff --git a/skills/build-live-game/references/apis.md b/skills/build-live-game/references/apis.md index 89555a0..baf3420 100644 --- a/skills/build-live-game/references/apis.md +++ b/skills/build-live-game/references/apis.md @@ -41,7 +41,7 @@ Unified entry point for Unity Gaming Services client types. Provides auto-genera Use cases for `IAdminClient`: -- **Deployment Window custom providers** — deploy commands that write to Cloud Save, Economy, Leaderboards, etc. +- **Deployment Window custom providers** — deploy commands that write to Cloud Save, Leaderboards, etc. - **Editor tools** — custom inspectors or windows that read/write game-wide data - **Build pipelines** — pre-build steps that push config data to UGS @@ -83,7 +83,6 @@ Service account credentials are created in the Unity Dashboard under **Project S | `CloudSaveFiles` | `ICloudSaveFilesAdminApi` | Cloud Save — file storage | | `CloudCodeModules` | `ICloudCodeModulesAdminApi` | Cloud Code — C# modules | | `CloudCodeScripts` | `ICloudCodeScriptsAdminApi` | Cloud Code — JS scripts | -| `Economy` | `IEconomyAdminApi` | Economy — config and player data | | `RemoteConfig` | `IConfigsAdminApi` | Remote Config | | `RemoteConfigSchemas` | `ISchemasAdminApi` | Remote Config schemas | | `GameOverrides` | `IGameOverridesAdminApi` | Game Overrides | @@ -143,10 +142,6 @@ Created via `UnityServicesApiClient.GameClient` after player authentication. Run | `CloudSaveData` | `ICloudSaveDataApi` | Cloud Save player data | | `CloudSaveFiles` | `ICloudSaveFilesApi` | Cloud Save files | | `CloudCode` | `ICloudCodeApi` | Cloud Code invocation | -| `EconomyConfiguration` | `IEconomyConfigurationApi` | Economy config | -| `EconomyCurrencies` | `IEconomyCurrenciesApi` | Currency balances | -| `EconomyInventory` | `IEconomyInventoryApi` | Player inventory | -| `EconomyPurchases` | `IEconomyPurchasesApi` | Purchases | | `Leaderboards` | `ILeaderboardsApi` | Leaderboard scores | | `Lobby` | `ILobbyApi` | Lobby management | | `RemoteConfig` | `IRemoteConfigSettingsApi` | Remote Config | @@ -158,7 +153,7 @@ Created via `UnityServicesApiClient.GameClient` after player authentication. Run | `QosDiscovery` | `IQosDiscoveryApi` | QoS | | `Analytics` | `IAnalyticsApi` | Analytics | -Most developers use the high-level SDKs (`CloudSaveService.Instance`, `EconomyService.Instance`) instead of `IGameClient` directly. +Most developers use the high-level SDKs (`CloudSaveService.Instance`, `LeaderboardsService.Instance`) instead of `IGameClient` directly. --- @@ -176,10 +171,6 @@ await serverClient.SignInFromServer(); | `CloudSaveData` | `ICloudSaveDataApi` | Cloud Save player data | | `CloudSaveFiles` | `ICloudSaveFilesApi` | Cloud Save files | | `CloudCode` | `ICloudCodeApi` | Cloud Code | -| `EconomyConfiguration` | `IEconomyConfigurationApi` | Economy config | -| `EconomyCurrencies` | `IEconomyCurrenciesApi` | Currencies | -| `EconomyInventory` | `IEconomyInventoryApi` | Inventory | -| `EconomyPurchases` | `IEconomyPurchasesApi` | Purchases | | `Leaderboards` | `ILeaderboardsApi` | Leaderboards | | `Lobby` | `ILobbyApi` | Lobbies | | `PlayerNames` | `IPlayerNamesApi` | Display names | diff --git a/skills/build-live-game/references/authentication.md b/skills/build-live-game/references/authentication.md index db2c430..aada9bc 100644 --- a/skills/build-live-game/references/authentication.md +++ b/skills/build-live-game/references/authentication.md @@ -428,7 +428,7 @@ Throws `AuthenticationException` (extends `RequestFailedException`) on failure. **Anonymous-only accounts:** Calling `ClearSessionToken()` or `SignOut(clearCredentials: true)` on an account that ONLY has anonymous sign-in will **permanently lose** that account. Always link at least one external provider before clearing credentials. -**DeleteAccountAsync compliance:** iOS App Store (Guideline 5.1.1) requires apps that support account creation to offer account deletion. `DeleteAccountAsync()` only deletes **Authentication data**. It does NOT automatically clean up data in other UGS services (Cloud Save, Economy, Leaderboards, etc.). You must manually delete data from each service before calling `DeleteAccountAsync()`. +**DeleteAccountAsync compliance:** iOS App Store (Guideline 5.1.1) requires apps that support account creation to offer account deletion. `DeleteAccountAsync()` only deletes **Authentication data**. It does NOT automatically clean up data in other UGS services (Cloud Save, Leaderboards, etc.). You must manually delete data from each service before calling `DeleteAccountAsync()`. --- diff --git a/skills/build-live-game/references/battlepass.md b/skills/build-live-game/references/battlepass.md index c21fae8..10d68da 100644 --- a/skills/build-live-game/references/battlepass.md +++ b/skills/build-live-game/references/battlepass.md @@ -9,7 +9,7 @@ - [API Notes](#api-notes) - [Namespace Aliases](#namespace-aliases-required) - [ProtectedReadAccessClassOptions](#protectedreadaccessclassoptions) - - [Why tiersJson Is Passed from the Client](#why-tiersjson-is-passed-from-the-client) + - [Why Tier Thresholds Live in the Module](#why-tier-thresholds-live-in-the-module) - [Source Whitelist Enforcement](#source-whitelist-enforcement) - [Error Handling](#error-handling) - [Cloud Code Module Functions](#cloud-code-module-functions) @@ -36,7 +36,7 @@ | Per-player progress | Cloud Save (Protected) | `"battle_pass_progress"` | | Award XP | Cloud Code | `BattlePassModule.AwardXp` | | Claim reward | Cloud Code | `BattlePassModule.ClaimReward` | -| Purchase premium | Cloud Code | `BattlePassModule.PurchasePremium` | +| Purchase premium | Cloud Code | `BattlePassModule.PurchasePremium` (integration stub; always throws until you add purchase validation) | ``` Remote Config Cloud Save (Protected) Cloud Code Module @@ -123,7 +123,7 @@ public class BattlePassProgress One entry per pass, keyed by `PassId` in the progress dictionary stored at Cloud Save key `"battle_pass_progress"`. -### BattlePassTierThreshold (server-side helper, passed from client via tiersJson) +### BattlePassTierThreshold (server-side helper, mirrors the `Tiers` in `BattlePasses.rc`) ```csharp [Serializable] @@ -161,31 +161,35 @@ player's data. > instead of `ProtectedReadAccessClassOptions`) will return no data, because the progress > is stored in the Protected bucket by the Cloud Code module. -### Why tiersJson Is Passed from the Client +### Why Tier Thresholds Live in the Module -Remote Config is not accessible from inside Cloud Code modules. The client serializes the tier -thresholds from the loaded `BattlePassDefinition` and sends them as the `tiersJson` parameter. -The server uses these thresholds to recalculate the player's current tier after awarding XP. - -Tier thresholds are config data and are **not security-sensitive** — the anti-cheat protection -is on the `source` whitelist and the `amount` validation. A tampered `tiersJson` could only -cause the player's tier to be recalculated differently, which is bounded by their actual XP. +Remote Config is not accessible from inside Cloud Code modules, and anything the client sends is +untrusted: a client that supplies its own tier thresholds can set a tier-three threshold of zero +and reach tier three with 1 XP. The module therefore holds the thresholds in a server-side +constant (`PassTiers` in `BattlePassModule.cs`) and `AwardXp` takes no tier data from the client. +Pass dates get the same treatment: `PassWindows` holds each pass's UTC start and end, and `AwardXp` +and `ClaimReward` reject any pass outside `[Start, End)` using server time, so an expired pass cannot +gain XP and a staged future pass cannot be claimed early. Rewards must be claimed while the pass is +active. Keep `PassTiers` and `PassWindows` in sync with the `XpRequired`, `StartDate` and `EndDate` +values in `BattlePasses.rc` whenever you change a pass. Passes missing from either are rejected. ### Source Whitelist Enforcement The Cloud Code module maintains a server-side whitelist of allowed XP source strings: ```csharp -static readonly HashSet ValidSources = new() +static readonly Dictionary MaxXpPerSource = new() { - "match_complete", - "daily_login", - "quest_complete" + ["match_complete"] = 200, + ["daily_login"] = 50, + ["quest_complete"] = 300 }; ``` -Any `AwardXp` call with an unrecognized source throws an exception. Add or remove entries as -needed for your game. The whitelist prevents clients from inventing arbitrary XP sources. +Any `AwardXp` call with an unrecognized source throws an exception, and each source has a +per-call XP cap (`MaxXpPerSource`). Add or remove entries as needed for your game. The whitelist +and caps prevent clients from inventing XP sources or awarding themselves arbitrary amounts. +For full protection, derive XP from server-verified events instead of a client-supplied `amount`. ### Error Handling @@ -216,9 +220,9 @@ catch (CloudSaveException ex) | Function | Parameters | Behavior | |---|---|---| -| `AwardXp` | `passId`, `amount`, `source`, `tiersJson` | Validates `source` against a whitelist (`ValidSources`), rejects non-positive amounts, adds XP to the pass progress, recalculates the current tier from the client-supplied `tiersJson`, saves to Cloud Save Protected | -| `ClaimReward` | `passId`, `tierNumber`, `rewardType` | Validates the player has reached the tier, validates premium ownership if `rewardType` is `"premium"`, checks reward is not already claimed, records the claim as `"tier_N_type"` (e.g. `"tier_2_premium"`), saves to Cloud Save Protected | -| `PurchasePremium` | `passId` | Sets `HasPremium = true` on the pass progress. **Idempotent** -- returns immediately if already purchased without error | +| `AwardXp` | `passId`, `amount`, `source` | Validates `source` against a whitelist (`MaxXpPerSource` keys), rejects non-positive amounts and amounts above the source's cap, rejects unknown passes, adds XP to the pass progress, recalculates the current tier from the server-side `PassTiers`, saves to Cloud Save Protected | +| `ClaimReward` | `passId`, `tierNumber`, `rewardType` | Validates the tier exists in the server-side `PassTiers` and the player's XP meets its threshold, validates premium ownership if `rewardType` is `"premium"`, checks reward is not already claimed, records the claim as `"tier_N_type"` (e.g. `"tier_2_premium"`), saves to Cloud Save Protected | +| `PurchasePremium` | `passId` | **Integration stub.** Returns the existing progress if `HasPremium` is already true; otherwise the sample always throws, because nothing in the blueprint sets `HasPremium`. Add server-side purchase validation that sets `HasPremium = true` before premium purchasing works; do not deploy this path as-is. It fails closed and never grants premium unverified. | All three functions return the updated `BattlePassProgress` for the given pass. @@ -292,18 +296,17 @@ async Task> LoadProgressAsync() ```csharp public async Task AwardXpAsync(string passId, int amount, string source) { - var pass = ActivePasses.FirstOrDefault(p => p.PassId == passId) - ?? throw new InvalidOperationException( + if (!ActivePasses.Any(p => p.PassId == passId)) + throw new InvalidOperationException( $"Pass '{passId}' is not in the active passes list. Call LoadAsync first."); var progress = await CloudCodeService.Instance.CallModuleEndpointAsync( "BattlePassModule", "AwardXp", new Dictionary { - { "passId", passId }, - { "amount", amount }, - { "source", source }, - { "tiersJson", JsonConvert.SerializeObject(pass.Tiers) } + { "passId", passId }, + { "amount", amount }, + { "source", source } }); Progress[passId] = progress; @@ -311,7 +314,7 @@ public async Task AwardXpAsync(string passId, int amount, st } ``` -The `tiersJson` parameter is the serialized tier list from the locally loaded pass definition. The server uses it to recalculate the player's tier after adding XP, without needing its own Remote Config fetch. +The client sends no tier data; the server recalculates the player's tier from its own `PassTiers`. The active-pass check above is UX only; the module enforces pass dates itself. ### ClaimRewardAsync @@ -335,6 +338,8 @@ public async Task ClaimRewardAsync( ### PurchasePremiumAsync +Calls the `PurchasePremium` integration stub. It throws for any player without `HasPremium` until you add a validated purchase flow to the module. + ```csharp public async Task PurchasePremiumAsync(string passId) { @@ -439,18 +444,17 @@ public class BattlePassManager : MonoBehaviour public async Task AwardXpAsync(string passId, int amount, string source) { - var pass = ActivePasses.FirstOrDefault(p => p.PassId == passId) - ?? throw new InvalidOperationException( + if (!ActivePasses.Any(p => p.PassId == passId)) + throw new InvalidOperationException( $"Pass '{passId}' is not in the active passes list. Call LoadAsync first."); var progress = await CloudCodeService.Instance.CallModuleEndpointAsync( "BattlePassModule", "AwardXp", new Dictionary { - { "passId", passId }, - { "amount", amount }, - { "source", source }, - { "tiersJson", JsonConvert.SerializeObject(pass.Tiers) } + { "passId", passId }, + { "amount", amount }, + { "source", source } }); Progress[passId] = progress; @@ -473,6 +477,7 @@ public class BattlePassManager : MonoBehaviour return progress; } + // Requires the PurchasePremium integration stub to be completed server-side. public async Task PurchasePremiumAsync(string passId) { var progress = await CloudCodeService.Instance.CallModuleEndpointAsync( @@ -556,7 +561,7 @@ public class BattlePassTester : MonoBehaviour ## Cloud Code Module Implementation -> **Module scaffolding — follow [cloud-code.md — Module Creation](cloud-code.md#module-creation).** +> **Module scaffolding — follow the Module Creation section of `references/cloud-code.md`.** > Replace `MyModule` / `MyModuleCCM` with `BattlePassModule` / `BattlePassCCM` throughout. > You **must** create every file listed there — missing any one will cause deployment to fail: > @@ -582,11 +587,36 @@ public class BattlePassModule { const string ProgressKey = "battle_pass_progress"; - static readonly HashSet ValidSources = new() + static readonly Dictionary MaxXpPerSource = new() { - "match_complete", - "daily_login", - "quest_complete" + ["match_complete"] = 200, + ["daily_login"] = 50, + ["quest_complete"] = 300 + }; + + // Keep in sync with the Tiers in BattlePasses.rc; clients are never trusted for thresholds. + static readonly Dictionary> PassTiers = new() + { + ["season_03"] = new() + { + new BattlePassTierThreshold { TierNumber = 1, XpRequired = 0 }, + new BattlePassTierThreshold { TierNumber = 2, XpRequired = 500 }, + new BattlePassTierThreshold { TierNumber = 3, XpRequired = 1200 } + }, + ["easter_2026"] = new() + { + new BattlePassTierThreshold { TierNumber = 1, XpRequired = 0 }, + new BattlePassTierThreshold { TierNumber = 2, XpRequired = 300 } + } + }; + + // Keep in sync with StartDate/EndDate in BattlePasses.rc. UTC, End is exclusive. + static readonly Dictionary PassWindows = new() + { + ["season_03"] = (new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), + new DateTime(2026, 7, 1, 0, 0, 0, DateTimeKind.Utc)), + ["easter_2026"] = (new DateTime(2026, 4, 1, 0, 0, 0, DateTimeKind.Utc), + new DateTime(2026, 4, 22, 0, 0, 0, DateTimeKind.Utc)) }; // ── AwardXp ────────────────────────────────────────────────────────────── @@ -594,14 +624,19 @@ public class BattlePassModule [CloudCodeFunction("AwardXp")] public async Task AwardXp( IExecutionContext context, IGameApiClient client, - string passId, int amount, string source, string tiersJson) + string passId, int amount, string source) { - if (!ValidSources.Contains(source)) + if (!MaxXpPerSource.TryGetValue(source, out var maxXp)) throw new Exception( - $"Invalid XP source: '{source}'. Allowed: {string.Join(", ", ValidSources)}"); + $"Invalid XP source: '{source}'. Allowed: {string.Join(", ", MaxXpPerSource.Keys)}"); + + if (amount <= 0 || amount > maxXp) + throw new Exception($"XP amount must be between 1 and {maxXp} for source '{source}'."); + + if (!PassTiers.TryGetValue(passId, out var tiers)) + throw new Exception($"Unknown pass '{passId}'."); - if (amount <= 0) - throw new Exception("XP amount must be positive."); + RequireActive(passId); var allProgress = await LoadProgressAsync(context, client); @@ -610,8 +645,6 @@ public class BattlePassModule progress.CurrentXp += amount; - var tiers = JsonConvert.DeserializeObject>(tiersJson) - ?? throw new Exception("tiersJson could not be deserialized."); progress.CurrentTier = CalculateTier(progress.CurrentXp, tiers); allProgress[passId] = progress; @@ -631,14 +664,22 @@ public class BattlePassModule throw new Exception( $"Invalid rewardType: '{rewardType}'. Must be 'free' or 'premium'."); + RequireActive(passId); + var allProgress = await LoadProgressAsync(context, client); if (!allProgress.TryGetValue(passId, out var progress)) throw new Exception($"No progress found for pass '{passId}'. Award XP first."); - if (progress.CurrentTier < tierNumber) + if (!PassTiers.TryGetValue(passId, out var tiers)) + throw new Exception($"Unknown pass '{passId}'."); + + var tier = tiers.FirstOrDefault(t => t.TierNumber == tierNumber) + ?? throw new Exception($"Tier {tierNumber} does not exist in pass '{passId}'."); + + if (progress.CurrentXp < tier.XpRequired) throw new Exception( - $"Tier {tierNumber} not yet reached (current tier: {progress.CurrentTier})."); + $"Tier {tierNumber} not yet reached ({progress.CurrentXp}/{tier.XpRequired} XP)."); if (rewardType == "premium" && !progress.HasPremium) throw new Exception("Premium pass not owned. Purchase premium first."); @@ -668,15 +709,22 @@ public class BattlePassModule if (progress.HasPremium) return progress; // idempotent -- already owns premium - progress.HasPremium = true; - allProgress[passId] = progress; - await SaveProgressAsync(context, client, allProgress); - - return progress; + // Integration point: validate the purchase server-side, set progress.HasPremium = true, save, and return. + throw new Exception("PurchasePremium requires server-side purchase validation."); } // ── Helpers ────────────────────────────────────────────────────────────── + static void RequireActive(string passId) + { + if (!PassWindows.TryGetValue(passId, out var window)) + throw new Exception($"Unknown pass '{passId}'."); + + var now = DateTime.UtcNow; + if (now < window.Start || now >= window.End) + throw new Exception($"Pass '{passId}' is not active."); + } + static int CalculateTier(int currentXp, List tiers) { var reached = tiers @@ -751,7 +799,7 @@ Place at `Assets/BattlePasses.rc`. Contains pass definitions as a JSON array und "StartDate": "2024-01-01T00:00:00Z", "EndDate": "2026-06-30T23:59:59Z", "Tiers": [ - { "TierNumber": 1, "XpRequired": 0, "FreeRewardId": "item_bronze_badge", "PremiumRewardId": "currency_gold_500" }, + { "TierNumber": 1, "XpRequired": 0, "FreeRewardId": "item_bronze_badge", "PremiumRewardId": "item_gold_frame" }, { "TierNumber": 2, "XpRequired": 500, "FreeRewardId": "item_xp_boost", "PremiumRewardId": "item_rare_skin_fragment" }, { "TierNumber": 3, "XpRequired": 1200, "FreeRewardId": "item_silver_badge", "PremiumRewardId": "item_legendary_skin" } ] @@ -781,7 +829,9 @@ Place at `Assets/BattlePasses.rc`. Contains pass definitions as a JSON array und - `XpRequired` is **cumulative** XP to reach the tier, not a delta from the previous tier. Tier 1 must always have `XpRequired: 0` so it is immediately reachable. - `FreeRewardId` / `PremiumRewardId` are arbitrary strings. The client reads these IDs and maps - them to display names/icons locally. + them to display names/icons locally. `ClaimReward` records the claim in `ClaimedRewards`; the game grants what + the ID stands for — unlock it client-side from the claimed set, or have the module write the grant to + Cloud Save Protected alongside the claim. - Date filtering happens **client-side** after fetch. Add passes to the file before their `StartDate` — they will be ignored by the client until the start time is reached. @@ -791,7 +841,7 @@ Place at `Assets/BattlePasses.rc`. Contains pass definitions as a JSON array und > [Cloud Code Module Implementation](#cloud-code-module-implementation) above. > All scaffolding files (`.sln`, `.csproj`, `ModuleSetup.cs`, `FolderProfile.pubxml`) are > **required** — the Deployment Window will fail without them. See -> [cloud-code.md — Module Creation](cloud-code.md#module-creation) for the generic templates; +> the Module Creation section of `references/cloud-code.md` for the generic templates; > replace `MyModule` / `MyModuleCCM` with `BattlePassModule` / `BattlePassCCM`. ### Access Control @@ -846,15 +896,15 @@ After implementing the battle pass feature, verify: 3. **Protected bucket reads** use `ProtectedReadAccessClassOptions` -- using the wrong access class returns no data. 4. **Cloud Code module name** matches the `.ccmr` filename: `"BattlePassModule"` in `CallModuleEndpointAsync` matches `BattlePassModule.ccmr`. 5. **No direct client writes** to `"battle_pass_progress"` -- all mutations go through Cloud Code. -6. **tiersJson is serialized** from the locally loaded pass definition before calling `AwardXp`. +6. **No client-supplied tier data**: `AwardXp` and `ClaimReward` use the module's server-side `PassTiers`, which matches the `XpRequired` values in `BattlePasses.rc`, and reject passes outside the server-side `PassWindows` (matching `StartDate`/`EndDate`). 7. **Date filtering** uses `DateTime.UtcNow` with `DateTimeStyles.RoundtripKind` for consistent UTC parsing. 8. **Cloud resources** are present and deployable: `BattlePasses.rc` and `BattlePassModule.ccmr` appear in the Deployment Window. -9. **PurchasePremium is idempotent** -- calling it when `HasPremium` is already true returns the existing progress without error. -10. **ClaimReward validates** both tier reach and premium ownership before recording the claim. +9. **PurchasePremium is an integration stub** -- it returns existing progress when `HasPremium` is already true and otherwise throws until you add purchase validation. +10. **ClaimReward validates** the pass window (`PassWindows`), tier reach (against `PassTiers`) and premium ownership before recording the claim. +11. **Premium purchasing is not deployable until a validated purchase flow sets `HasPremium`** -- keep the fail-closed throw until then. --- ## Asset Store & Sample Projects - **Use Case Samples — Battle Pass:** The official [Use Case Samples](https://github.com/Unity-Technologies/com.unity.services.samples.use-cases) repository includes a complete Battle Pass sample with seasonal reward tiers, free/premium tracks, time-limited rewards, and server-authoritative progression via Cloud Code. Clone the repo and open the "Battle Pass" scene for a working reference. -- **Use Case Samples — Virtual Shop:** The same repository includes a Virtual Shop sample demonstrating Economy-based in-game stores, currency management, and inventory — patterns commonly paired with a Battle Pass. diff --git a/skills/build-live-game/references/cloud-code.md b/skills/build-live-game/references/cloud-code.md index ba6e1a1..1884d2b 100644 --- a/skills/build-live-game/references/cloud-code.md +++ b/skills/build-live-game/references/cloud-code.md @@ -158,6 +158,8 @@ Triggers let Cloud Code endpoints run automatically in response to events from o ## Module Scope +Scopes require `com.unity.services.cloudcode` 3.x. They are not available in 2.x. + `CallModuleEndpointAsync` accepts an optional `CloudCodeModuleScope` to scope execution. Namespace: `Unity.Services.CloudCode.Models`. ```csharp @@ -464,12 +466,12 @@ Debug.Log($"Reward: {reward}"); ### Call a Module Endpoint (Typed Result) ```csharp -// Module class name: "EconomyModule", method: "GrantDailyReward" -var result = await CloudCodeService.Instance.CallModuleEndpointAsync( - "EconomyModule", "GrantDailyReward", - new Dictionary { { "playerId", playerId } }); +// Module class name: "ProgressionModule", method: "AwardXp" +var result = await CloudCodeService.Instance.CallModuleEndpointAsync( + "ProgressionModule", "AwardXp", + new Dictionary { { "amount", 50 } }); -Debug.Log($"Granted: {result.CurrencyAmount} currency"); +Debug.Log($"Total XP: {result.TotalXp}"); ``` ### Subscribe to Player Messages (Full Example) diff --git a/skills/build-live-game/references/cloud-save.md b/skills/build-live-game/references/cloud-save.md index 2d02475..0a454f7 100644 --- a/skills/build-live-game/references/cloud-save.md +++ b/skills/build-live-game/references/cloud-save.md @@ -93,7 +93,7 @@ Task> QueryAsync(Query query, QueryOptions options) Read-only from the client. Write via Cloud Code modules or `IAdminClient.CloudSaveData` from the `com.unity.services.apis` package. All methods require a `customDataID` parameter -- the namespace configured in the Unity Dashboard. > **To write Custom Data from the editor or a deploy command**, use `IAdminClient.CloudSaveData.SetCustomItem` / -> `SetCustomItemBatch`. See [apis.md](apis.md) for setup and code templates. The client SDK has no write +> `SetCustomItemBatch`. See `references/apis.md` for setup and code templates. The client SDK has no write > path for Custom Data. ```csharp diff --git a/skills/build-live-game/references/deployment.md b/skills/build-live-game/references/deployment.md index 0f441fb..ffb28f7 100644 --- a/skills/build-live-game/references/deployment.md +++ b/skills/build-live-game/references/deployment.md @@ -34,7 +34,6 @@ Each service package registers its own file types with the Deployment Window. In | Cloud Code Scripts | `com.unity.services.cloudcode` | `.js` | 2.1.0 | | Cloud Code C# Modules | `com.unity.services.cloudcode` | `.ccmr` | 2.5.0 | | Remote Config | `com.unity.remote-config` | `.rc` | 3.2.0 | -| Economy | `com.unity.services.economy` | `.ecc`, `.eci`, `.ecv`, `.ecr` | 3.2.1 | | Leaderboards | `com.unity.services.leaderboards` | `.lb` | 2.0.0 | | Game Server Hosting | `com.unity.services.multiplayer` | `.gsh` | 1.1.0 | | Matchmaker | `com.unity.services.multiplayer` | `.mmq` | 1.0 | @@ -190,7 +189,7 @@ static void RegisterProvider() > `com.unity.services.apis` package (`com.unity.services.apis`). Authenticate with a service > account via `adminClient.SetServiceAccount(keyId, keySecret)`, then call the appropriate > admin API (e.g. `adminClient.CloudSaveData.SetCustomItem` for Cloud Save game data). -> See [apis.md](apis.md) for the full `IAdminClient` interface and code templates. +> See `references/apis.md` for the full `IAdminClient` interface and code templates. ### Code Template -- Trigger Deployment Programmatically diff --git a/skills/build-live-game/references/player-account.md b/skills/build-live-game/references/player-account.md index 499bfb8..2d12caf 100644 --- a/skills/build-live-game/references/player-account.md +++ b/skills/build-live-game/references/player-account.md @@ -437,7 +437,7 @@ if (result.TryGetValue("player_level", out var item)) Writes to Protected and Custom buckets must go through a Cloud Code module. The module uses `IGameApiClient` with the service token to call Cloud Save server-side APIs. -To create the module, follow [cloud-code.md — Module Creation](cloud-code.md#module-creation). All scaffolding files are required: +To create the module, follow the Module Creation section of `references/cloud-code.md`. All scaffolding files are required: - [ ] `.sln` (generate fresh GUIDs) - [ ] `.csproj` (net9.0, CloudCode.Apis + CloudCode.Core) diff --git a/skills/build-live-game/references/tooling.md b/skills/build-live-game/references/tooling.md index ba0ed13..8c4e84f 100644 --- a/skills/build-live-game/references/tooling.md +++ b/skills/build-live-game/references/tooling.md @@ -45,7 +45,6 @@ Access Control policies permit or deny access to UGS services on a **URN basis** Common use cases: - Deny direct player writes to Cloud Save (force writes through Cloud Code) -- Deny player currency/inventory writes in Economy (only allow purchases) - Restrict Cloud Code module/script invocation to specific endpoints - Deny player account deletion or identity unlinking @@ -114,17 +113,6 @@ urn:ugs::/ | `urn:ugs:cloud-save:/**/players/public/query**` | Query Public player data | | `urn:ugs:cloud-save:/**/custom/*/items**` | Read game-wide (Custom) data items | -#### Economy (`economy`) - -| URN Pattern | Description | -|---|---| -| `urn:ugs:economy:/**/players/*/config**` | Read player economy configuration | -| `urn:ugs:economy:/**/currencies**` | Player currencies (Read/Write) | -| `urn:ugs:economy:/**/inventory**` | Player inventory (Read/Write) | -| `urn:ugs:economy:/**/purchases/virtual**` | Virtual purchases | -| `urn:ugs:economy:/**/purchases/googleplaystore**` | Google Play Store purchases | -| `urn:ugs:economy:/**/purchases/appleappstore**` | Apple App Store purchases | - #### Leaderboards (`leaderboards`) | URN Pattern | Description | @@ -196,48 +184,6 @@ Start with a blanket deny, then explicitly allow only what the player needs. Thi "Principal": "Player", "Resource": "urn:ugs:player-auth:/.well-known/**" }, - { - "Sid": "Allow-Read-Economy-Config", - "Effect": "Allow", - "Action": ["Read"], - "Principal": "Player", - "Resource": "urn:ugs:economy:/**/players/*/config**" - }, - { - "Sid": "Allow-Read-Currencies", - "Effect": "Allow", - "Action": ["Read"], - "Principal": "Player", - "Resource": "urn:ugs:economy:/**/currencies**" - }, - { - "Sid": "Allow-Read-Inventory", - "Effect": "Allow", - "Action": ["Read"], - "Principal": "Player", - "Resource": "urn:ugs:economy:/**/inventory**" - }, - { - "Sid": "Allow-Virtual-Purchases", - "Effect": "Allow", - "Action": ["Write"], - "Principal": "Player", - "Resource": "urn:ugs:economy:/**/purchases/virtual**" - }, - { - "Sid": "Allow-GooglePlay-Purchases", - "Effect": "Allow", - "Action": ["Write"], - "Principal": "Player", - "Resource": "urn:ugs:economy:/**/purchases/googleplaystore**" - }, - { - "Sid": "Allow-AppStore-Purchases", - "Effect": "Allow", - "Action": ["Write"], - "Principal": "Player", - "Resource": "urn:ugs:economy:/**/purchases/appleappstore**" - }, { "Sid": "Allow-Read-Leaderboards", "Effect": "Allow", @@ -330,8 +276,6 @@ This policy: - **Denies** all UGS access by default - **Allows** authentication (anonymous, external, session token sign-in; link external ID; read player info) - **Denies** identity unlinking and player deletion (inherited from blanket deny) -- **Allows** economy reads (config, currencies, inventory) and purchases (virtual, Google Play, Apple App Store) -- **Denies** direct currency/inventory writes (inherited from blanket deny — use Cloud Code for server grants) - **Allows** leaderboard reads - **Allows** Cloud Save player data reads/writes for own data (Default items), public data reads, game-wide data reads - **Denies** Cloud Save blanket writes (Protected data only writable via Cloud Code) diff --git a/skills/create-2d-physics/SKILL.md b/skills/create-2d-physics/SKILL.md index ae47c0f..2618887 100644 --- a/skills/create-2d-physics/SKILL.md +++ b/skills/create-2d-physics/SKILL.md @@ -1,6 +1,6 @@ --- name: create-2d-physics -description: "Creates and fixes Unity 2D physics in both systems, 2D Physics Core and the legacy Rigidbody 2D system, for any 2D physics question even if neither is named. Not for 3D physics." +description: "Creates and fixes Unity 2D physics in both systems, 2D Physics Core and the legacy Rigidbody 2D system. Use when the user asks about any 2D physics task, even if neither system is named. Not for 3D physics." allowed-tools: WebFetch, WebSearch metadata: version: 0.3.0 @@ -11,25 +11,36 @@ metadata: Do not weigh 3D physics options. If evidence shows the user means 3D, say so and stop. -Prefer `WebFetch` over `WebSearch` — faster and lands on the exact reference. Only fetch what you need. Replace `` with the following before fetching: -- **docs.unity.com**: The Unity version, e.g. `6000.7`. Pages are markdown — URLs end in `.md`. -- **Package on docs.unity3d.com**: The package version, e.g. `@1.1`. Check `Packages/manifest.json` if unsure. - ## Important - Use the unity cli (`unity command --format json`) and the `unity-cli` and `unity-pipeline` skills for each step. -- Don't use `using` directives or unqualified types in the `snippet` for `eval`. The compiler reads using `UnityEngine` and a bare AssetDatabase, Volume, or Object causes errors (CS0246 / CS0103 / CS0104). `return` what you want to read; it arrives at `data.result.result`. +- Before making changes, establish a baseline of the current behavior so you can tell whether a later result is actually caused by your change. - Create a restore point you can roll back to if your changes fail. - Do only what's asked. Don't change unrelated assets or files. - Avoid long explanations. -- Never leave verification code in the user's script; never build logging or settle-detection they didn't ask for. See [Final step](#final-step). +- Verify runtime behaviour with temporary `Debug.Log` lines, then remove them, because the user didn't ask for logging and leftover lines clutter their script and console. Don't build settle-detection or permanent logging they didn't ask for. See [Final step](#final-step). - Never reflect over types to discover an API — fetch the member page instead. The system probe is the one exception. +### Passing C# to `eval` + +`eval` compiles a statement block, not a file, so write the code without `using` directives and with fully qualified types. `return` what you want to read; it arrives at `data.result.result`. If the code doesn't compile, look up the error here: + +| Error | Cause | Fix | +| --- | --- | --- | +| `CS0210` | A `using` directive, which the compiler reads as a resource-disposal statement | Remove the directive and qualify the types | +| `CS0246` / `CS0103` | An unqualified type, such as `Rigidbody2D` or `PhysicsWorld` | Use the full name, such as `UnityEngine.Rigidbody2D` or `Unity.U2D.Physics.PhysicsWorld` | +| `CS0104` | A bare `Object`, which is ambiguous with `object` | Use `UnityEngine.Object` | + ## Step 0: Which physics system Unity has two 2D physics systems. They share no code and never interact. Identify which one before writing any physics code — an answer from the wrong system compiles, runs, and does nothing. -Check the request, scene and existing scripts against the table below. Components are the reliable signal — check scene and existing scripts first. Check the user's request wording too: "add a collider" is component language, "spawn a thousand" is script language. +Check the request, scene and existing scripts against the table below, in this order: + +1. Check the scene components and existing scripts first, because components are the reliable signal. +2. Check the user's request wording: "add a collider" is component language, "spawn a thousand" is script language. +3. If still unsure which systems are installed, run the probe below. +4. If the route is still unknown, stop and ask — offer all three routes and never choose for the user. | Signal | System | | --- | --- | @@ -45,34 +56,73 @@ Key: - Core components = Physics Pose and Physics Area, requiring `com.unity.2d.physics` and Unity 6000.7+. - Core in script = `Unity.U2D.Physics` API, no package needed. -If still unsure which systems are installed, run the probe. If the route is still unknown, stop and ask — offer all three routes and never choose for the user: - Probe: ``` unity command eval --caller plugin --skill create-2d-physics --code 'var r = ""; foreach (var n in new[] { "UnityEngine.Rigidbody2D", "Unity.U2D.Physics.PhysicsWorld", "UnityEngine.LowLevelPhysics2D.PhysicsWorld", "Unity.U2D.Physics.PhysicsPose" }) { var f = false; foreach (var a in System.AppDomain.CurrentDomain.GetAssemblies()) if (a.GetType(n) != null) f = true; r += f + ","; } return r;' ``` The Probe returns: legacy present, Core as `Unity.U2D.Physics`, Core as `UnityEngine.LowLevelPhysics2D`, Core components present. Core exists if either Core value is true; the true one gives the namespace. `false` rules that route out. -## System reference +## Full topic map + +Before you go forward with a workflow or diagnosis: + +1. Open and read the [documentation page(s)](#full-topic-map) that cover the specific subject related to what you intend to do or fix and check if you've missed anything. +2. Name the file(s) you read in your response. + +This is because your training knowledge about Unity might be out of date, incorrect, or for the wrong Unity version. + +Prefer `WebFetch` over `WebSearch` — faster and lands on the exact reference. Only fetch what you need. Replace `` with the following before fetching: + +- **Package on docs.unity3d.com**: The package version, eg `@6.6` or `@2.0`. Check the package version if unsure. +- **docs.unity.com**: The product version, eg `7000.0` + +For docs.unity.com engine pages, use the Unity version, e.g. `6000.7`. Pages are markdown — URLs end in `.md`. Find the package version in `Packages/packages-lock.json`, because `manifest.json` can hold a range or a git URL instead of the resolved version. + +If the Editor install folder has a `Data/Documentation` folder (eg `...Unity/Hub/Editor/6000.6.0b2/Editor/Data/Documentation`), read the html files from the `en/Manual` folder. Otherwise use the online `.md` pages listed in the references below. Once you know the route, read the matching reference before acting: - [2D Physics Core reference](references/2d-physics-core.md) - components, configuring them, creating bodies in script, ownership, and the full Core topic map. - [Legacy 2D physics reference](references/legacy-2d-physics.md) - the legacy manual topic map. +### API reference + +The Core and legacy APIs (`Unity.U2D.Physics` or `UnityEngine.LowLevelPhysics2D`, and `UnityEngine.Rigidbody2D`, `Collider2D`, `Physics2D`) are part of the Unity Engine. They are versioned by Unity version, so check the project's Unity version before relying on a specific class or method. + +1. Use the `unity-cli` skill to check the project's Unity version. +2. If the Editor install folder has a `Data/Documentation` folder, grep the html files in the `en/ScriptReference` folder inside, and skip the remaining steps. +3. Otherwise, fetch and cache `https://docs.unity3d.com//Documentation/ScriptReference/xrefmap.yml` (replace `` with the project's Unity version in major.minor form). This file lists every public API member's fully qualified name as a `uid` (e.g. `uid: 'UnityEngine.Rigidbody2D.AddForce(UnityEngine.Vector2,UnityEngine.ForceMode2D)'`). +4. Grep the yml file locally for a class or method instead of refetching per lookup. The file is large (about 6 MB), so don't read the whole file. +5. Convert the `uid` to a markdown page: remove the parameters in brackets, lowercase it, replace `.` with `/`, and add `.md`. Then fetch `https://docs.unity.com/en-us/engine//script-reference/`. For example, `UnityEngine.Rigidbody2D.AddForce(UnityEngine.Vector2,UnityEngine.ForceMode2D)` => `https://docs.unity.com/en-us/engine//script-reference/unityengine/rigidbody2d/addforce.md`. All overloads are on the same page. + - Constructors use `ctor` (e.g. `unityengine/vector2/ctor.md`), and indexers use `item` (e.g. `unityengine/vector2/item.md`). + - For operators, fetch the class page (e.g. `unityengine/vector2.md`) and follow its link. + - Links inside the pages start with `/engine/`. Add `https://docs.unity.com/en-us` before them. + - If the page doesn't exist for that Unity version, use the `href` in the `xrefmap.yml` entry instead (e.g. `href: 'Rigidbody2D.AddForce.html'` => `https://docs.unity3d.com//Documentation/ScriptReference/Rigidbody2D.AddForce.html`). + +The Core **components** (Physics Pose, Area, Constraint, Simulation) are in the `Unity.U2D.Physics` namespace of the `com.unity.2d.physics` package. They are versioned by package version, not Unity version, so check the installed package version before relying on a specific class or method. + +1. Use the `unity-cli` skill to check the installed package version. +2. Fetch and cache `https://docs.unity3d.com/Packages/com.unity.2d.physics@/xrefmap.yml` (replace `` with the installed package version in major.minor form). This file maps every public API member's fully qualified name to its docs page `href` (e.g. `href: api/Unity.U2D.Physics.PhysicsPose.html` => `https://docs.unity3d.com/Packages/com.unity.2d.physics@/api/Unity.U2D.Physics.PhysicsPose.html`). +3. Grep the yml file locally for a class or method instead of refetching per lookup. Don't read the whole file. + +Always use the docs matching the project's Unity version and installed package version rather than relying on training knowledge, because APIs are often marked obsolete between versions and a member you recall might be obsolete or removed in the project's version. + ## Worked examples Prefer in order: the C# example on the type or member page you are using; existing project scripts (match their style); the sample project at `https://github.com/Unity-Technologies/PhysicsExamples2D`. Check all three before inventing a pattern. ## Final step -Do the following checks: +Physics changes alter runtime behaviour, so a clean compile isn't enough. Run these checks in order. If any check fails, reread the [docs pages](#full-topic-map) to find out what you missed, fix it, and run the checks again. -1. Check the project has zero console errors. Use `console_status` for counts. -2. Stop there unless the user asked you to prove it works or you suspect a specific fault. -3. If you do enter Play mode, do it once only to chase a fault you already suspect. Read the world through `eval` rather than adding logging. Take no screenshots as they take too much time. - -If a check fails, go back and reread the docs pages in the matching reference to find out what you missed. +1. Use `console_status` to check the project has zero console errors. +2. Read back what you changed, because a change can compile cleanly and still do nothing: + - For components you added or configured in the scene, use `eval` to check each one exists on its GameObject and its properties hold the values you set, such as a Collider 2D's `isTrigger` or the members listed on a Core component's type page. + - For a script, read it back and check it uses the system and namespace from [Step 0](#step-0-which-physics-system), and sets each Core body's `type` explicitly. +3. Add temporary `Debug.Log` lines that print the behaviour you changed, such as a body's position or velocity, or a contact or trigger callback firing. +4. Enter Play mode and check the console shows the expected log lines and still has zero errors. You can also read the world through `eval`. Take no screenshots, because they take too much time. +5. Exit Play mode, remove the temporary `Debug.Log` lines, and check the console again has zero errors. ## Final report Short checklist: what you changed and why, which physics system you used, anything left for the user to decide or do. + diff --git a/skills/create-2d-physics/references/2d-physics-core.md b/skills/create-2d-physics/references/2d-physics-core.md index 62390ad..1e99dfb 100644 --- a/skills/create-2d-physics/references/2d-physics-core.md +++ b/skills/create-2d-physics/references/2d-physics-core.md @@ -1,5 +1,7 @@ # 2D Physics Core +## Contents + - [Step 0](#step-0) — when to read before acting - [Configuring a component](#configuring-a-component) - [Creating in script](#creating-in-script) @@ -15,7 +17,7 @@ A component is not the physics object. It owns a body, shape or joint — operat The components use only the public API, by design, so nothing is component-only — a plain MonoBehaviour driving the engine API always works. Never say a case is unsupported. For many shapes or nested areas, Physics Area Composite already exists. -Within Core: components for scene-authored objects, script for bulk runtime creation. The modules `com.unity.modules.physics2d` and `com.unity.modules.physicscore2d` are on by default; only matter if stripped. Version sets the namespace, not which system exists — the [Unity 6.5 upgrade guide](https://docs.unity3d.com//Documentation/Manual/UpgradeGuideUnity65.html) covers the rename. Only the components require 6000.7. +Within Core: components for scene-authored objects, script for bulk runtime creation. The modules `com.unity.modules.physics2d` and `com.unity.modules.physicscore2d` are on by default; only matter if stripped. The Unity version sets the namespace, not which system exists. Read the [Unity 6.5 upgrade guide](https://docs.unity.com/en-us/engine//manual/upgrade-guides/upgrade-guide-unity65.md) (local: `en/Manual/UpgradeGuideUnity65.html`) for the rename. ## Step 0 @@ -43,7 +45,7 @@ Follow these rules for configuring a component: - Never write serialized fields — the serialized names differ from public names. Drive everything through public properties and methods via `eval`. - Definition and geometry are separate: a pose's body settings live in its definition; an area's shape is its geometry. Configure them independently. A change to either needs its apply call before it takes effect — the type page states which one. -- Before configuring any component, fetch its API page and use only members it lists. Read the installed version from `Packages/manifest.json` (major.minor, e.g. `1.1`), then fetch: +- Before configuring any component, fetch its API page and use only members it lists. Read the installed version from `Packages/packages-lock.json` (major.minor, e.g. `1.1`), then fetch: `https://docs.unity3d.com/Packages/com.unity.2d.physics@/api/Unity.U2D.Physics..html` One page per type covers all members. If a member is missing, treat it as undocumented, not absent. @@ -61,36 +63,45 @@ Script types — joints, geometry, queries, and everything else — are engine t ## Ownership -Core only. Applies to worlds, bodies, shapes, chains and joints. +Core only. Before you destroy or change a world, body, shape, chain or joint you didn't create in this task, read the owner rules on these pages and on the member page you're calling, such as [PhysicsBody.Destroy](https://docs.unity.com/en-us/engine//script-reference/unity/u2d/physics/physicsbody/destroy.md): + +- [PhysicsBody.SetOwner](https://docs.unity.com/en-us/engine//script-reference/unity/u2d/physics/physicsbody/setowner.md) +- [PhysicsWorld.CreateOwnerKey](https://docs.unity.com/en-us/engine//script-reference/unity/u2d/physics/physicsworld/createownerkey.md) -Owner-gated calls state it on their page, as [PhysicsBody.Destroy](https://docs.unity.com/en-us/engine//script-reference/unity/u2d/physics/physicsbody/destroy.md) does. The owner key argument defaults to zero (matches objects with no owner); `SetOwner` is different — there, zero creates a new key. A refusal logs a warning rather than throwing. +Follow these rules: -Create a key and own what you create. Queries return objects you did not create — deleting one you merely found is the mistake ownership prevents. Script-created objects are yours to destroy; component-created ones need the component removed; the default world is engine-owned and cannot be removed. +- Create an owner key and own what you create, so later calls can tell your objects from the user's. +- Never destroy an object you only found through a query, because it belongs to the user's code or a component. +- To remove a component-created object, remove its component instead. +- After an owner-gated call, check the console for warnings, because a refused call doesn't throw. ## What you are likely to get wrong -Core's API is young and was renamed — your recall is unreliable. Legacy's page locations were reorganised — your recall of URLs is stale. Nothing carries over between systems unverified. +Core's API is young and was renamed, so your recall is unreliable. Nothing carries over between systems unverified. Where a row links a page, read it instead of relying on recall. -| Your likely assumption | Actually | +| Your likely assumption | What to do instead | | --- | --- | -| Legacy manual pages are flat, like `class-Rigidbody2D.html` | Reorganised under family folders: `2d-physics/rigidbody-2d/`, `2d-physics/collider-2d/` | -| A Core component holds the state, as Rigidbody 2D does | It owns a body, shape or joint. Operate on the owned object | -| The namespace is `UnityEngine.LowLevelPhysics2D` | 6000.4 and earlier. From 6000.5 it is `Unity.U2D.Physics` | -| Box2D v2 naming and semantics apply to Core | It is Box2D v3. Most older forum answers describe v2 | -| Angles are in radians, as Box2D uses | Read the property page. Hinge angles are documented in degrees | -| Core collision layers are a 32-bit mask | 64 layers in Core, 32 in legacy | -| Some behaviour is component-only | None is | +| Legacy manual pages are flat, like `class-Rigidbody2D.html` | Build URLs from the folder paths in the Legacy 2D physics reference | +| Your recalled Core namespace is right for the project | Check the project's Unity version, then read the [Unity 6.5 upgrade guide](https://docs.unity.com/en-us/engine//manual/upgrade-guides/upgrade-guide-unity65.md) | +| Box2D v2 naming and semantics apply to Core | Ignore older forum answers. Read the Core type page | +| A property uses the unit Box2D or the other system uses | Read the property page for its unit and range | +| Core layer masks are the same width as a `LayerMask` | Read [PhysicsMask](https://docs.unity.com/en-us/engine//script-reference/unity/u2d/physics/physicsmask.md) | +| Some behaviour is component-only | None is. Drive the engine API from a MonoBehaviour | | A user's own component needs wiring to work with Unity's | None. All interaction is engine-side | -| Any handle can be destroyed | Only what you own. See [Ownership](#ownership) | -| You must create a world first | `PhysicsWorld.defaultWorld` exists in an empty scene | -| Debug visuals need gizmos or `Debug.DrawLine` | Core has a physics renderer with automatic and explicit draw calls. Never hand-roll it | +| Any handle can be destroyed | Only what you own. Follow [Ownership](#ownership) | +| You must create a world first | Read [PhysicsWorld.defaultWorld](https://docs.unity.com/en-us/engine//script-reference/unity/u2d/physics/physicsworld/defaultworld.md) | +| Debug visuals need gizmos or `Debug.DrawLine` | Use the Core physics renderer from [Scripting reference](#scripting-reference). Never hand-roll it | | A method exists because the other system has one like it | Confirm on its own page | -| `bodyType` and `RigidbodyType2D` set body type | Obsolete. Use `type` with `PhysicsBody.BodyType` | -| A newly created body has a known default type | Do not assume — set `.type` explicitly. falls/thrown/bounces → Dynamic; ground/wall/anchor → Static | +| A recalled member such as `bodyType` still sets body type | Read [PhysicsBody.type](https://docs.unity.com/en-us/engine//script-reference/unity/u2d/physics/physicsbody/type.md) | +| A newly created body has a known default type | Don't assume. Set `.type` explicitly: falls, thrown or bounces → Dynamic. Ground, wall or anchor → Static | | "add a circle/capsule/polygon/segment area" means the dedicated component | Could be that or Primitive with `shapeType` — same shape, two routes. Prefer dedicated when fixed at authoring time; Primitive only if it must switch at runtime. Chain Segment has no dedicated component | ## Manual: 2D Physics Core +If the Editor install folder has a `Data/Documentation` folder (eg `...Unity/Hub/Editor/6000.6.0b2/Editor/Data/Documentation`), read the html files from the `en/Manual/2d-physics-api/` folder. Local file names are flat: `2d-physics-api-.html` (e.g. `create-objects/reference-body.md` => `2d-physics-api-reference-body.html`), and section pages end `-landing` (e.g. `connect-objects.md` => `2d-physics-api-connect-objects-landing.html`). Start at `2d-physics-api-landing.html` and follow its links if a name doesn't match. + +Otherwise use the online pages. + Base: `https://docs.unity.com/en-us/engine//manual/unity2d/2d-physics-api/`. Pages are `.md`. The landing page is `https://docs.unity.com/en-us/engine//manual/unity2d/2d-physics-api.md` — contents. Paths below are relative to the base: @@ -134,9 +145,11 @@ For the Core renderer: start at `unity/u2d/physics/physicsworld/renderingmode.md ## Package documentation -Package API pages are live only — not local files. Fetch using the version from `Packages/manifest.json`: +Package API pages are live only — not local files. Fetch using the version from `Packages/packages-lock.json`: `https://docs.unity3d.com/Packages/com.unity.2d.physics@/api/Unity.U2D.Physics..html` -Package manual pages are local: `/Library/PackageCache/com.unity.2d.physics@*/Documentation~/`. Read only `.md` files in that subfolder. +The package manual pages are local markdown conversions of the Unity Manual, stored at `/Library/PackageCache/com.unity.2d.physics@*/`. Read only the .md files in that specific `PackageCache` subfolder. You must not look at non-.md files, or look outside at other Library or PackageCache folders, otherwise it takes too much time. Start with `Documentation~/TableOfContents.md`, or list the `Documentation~/` `.md` files if there is none, to find the page you need. + +If that folder has no `.md` files, use the online pages at `https://docs.unity3d.com/Packages/com.unity.2d.physics@/manual/`, because some package versions don't ship their docs. The `api/index.html` listing fetches as nothing — go straight to a named type page. diff --git a/skills/create-2d-physics/references/legacy-2d-physics.md b/skills/create-2d-physics/references/legacy-2d-physics.md index b35a3b7..e48dc82 100644 --- a/skills/create-2d-physics/references/legacy-2d-physics.md +++ b/skills/create-2d-physics/references/legacy-2d-physics.md @@ -1,6 +1,6 @@ # Legacy 2D physics -Relative paths in this file resolve to the skill directory — the folder that holds the `SKILL.md` for this skill. +Relative paths in this file resolve to the skill directory. ## Step 0 @@ -13,16 +13,33 @@ This is because your training knowledge about Unity might be out of date, incorr ## Manual: legacy 2D physics +If the Editor install folder has a `Data/Documentation` folder (eg `...Unity/Hub/Editor/6000.6.0b2/Editor/Data/Documentation`), read the html files from the `en/Manual` folder. Start at `en/Manual/2d-physics/2d-physics.html` and follow its links, because local file names differ from the online paths (e.g. `2d-physics/rigidbody/rigidbody-2d-landing.html`). + +Otherwise use the online pages. + Base: `https://docs.unity.com/en-us/engine//manual/unity2d/`. Pages are `.md`. -`2d-physics.md` is the contents page. Landing pages, relative to the base: `2d-physics/rigidbody-2d.md`, `2d-physics/collider-2d.md`, `2d-physics/effectors-2d.md`, `2d-physics/2d-joints.md`, `2d-physics/physics-2d-profiler.md`, plus flat `2d-physics/constant-force-2d-reference.md` and `2d-physics/physics-material-2d-reference.md`. +Pages, relative to the base: + +- `2d-physics.md` — contents page for legacy 2D physics +- `2d-physics/rigidbody-2d.md` — Rigidbody 2D: body types, simulation and movement +- `2d-physics/collider-2d.md` — Collider 2D types and their shared properties +- `2d-physics/effectors-2d.md` — Effector 2D components, such as area, buoyancy and platform effectors +- `2d-physics/2d-joints.md` — Joint 2D components that connect bodies +- `2d-physics/physics-2d-profiler.md` — profiling the 2D physics simulation +- `2d-physics/constant-force-2d-reference.md` — Constant Force 2D component (single page, no children) +- `2d-physics/physics-material-2d-reference.md` — Physics Material 2D asset (single page, no children) Each landing page has a folder of the same name listing its children — follow those rather than guessing leaf names. ## Scripting reference -Legacy types live in the main scripting reference as `unityengine/.md`, e.g. `https://docs.unity.com/en-us/engine//script-reference/unityengine/rigidbody2d.md`. See the [Scripting reference](2d-physics-core.md#scripting-reference) table in the Core reference for the full URL pattern, including how it differs from Core types. +Legacy types live in the main scripting reference as `unityengine/.md`, e.g. `https://docs.unity.com/en-us/engine//script-reference/unityengine/rigidbody2d.md`. See the Scripting reference table in `references/2d-physics-core.md` for the full URL pattern, including how it differs from Core types. ## What you are likely to get wrong -Legacy's page locations were reorganised — your recall of URLs is stale. Legacy manual pages are not flat like `class-Rigidbody2D.html`; they're reorganised under family folders: `2d-physics/rigidbody-2d/`, `2d-physics/collider-2d/`. Core collision layers use a 64-bit mask; legacy uses 32. +| Your likely assumption | Actually | +| --- | --- | +| Your recall of legacy manual URLs is current | Build URLs from the [Manual](#manual-legacy-2d-physics) list, because the pages moved | +| Legacy manual pages are flat, like `class-Rigidbody2D.html` | Use the family folders, such as `2d-physics/rigidbody-2d/` and `2d-physics/collider-2d/` | +| A Core type or member has a legacy equivalent | Confirm on the legacy type's own page | diff --git a/skills/new-unity-project/SKILL.md b/skills/new-unity-project/SKILL.md index 8cac49b..0f9cfdb 100644 --- a/skills/new-unity-project/SKILL.md +++ b/skills/new-unity-project/SKILL.md @@ -3,10 +3,8 @@ name: new-unity-project description: Guides creating a new Unity project, gathering concept, platforms, and monetization before setting up the project, source control, and packages. Use when the user asks to start a new game or prototype. allowed-tools: - Bash - - Read - Write - Edit - - AskUserQuestion --- # New Unity Project @@ -19,7 +17,7 @@ to other skills. **Delegates to (read these for the actual commands — don't reinvent them):** - **`unity-cli`** — CLI install, auth/license, Editor install, project creation, source control, opening the project. Its "Bootstrap a new project from scratch" workflow is the backbone here. -- **`unity-package-management`** — installing packages via the C# PackageManager Client API, and +- **`unity-package-management`** — installing packages with the Pipeline `package` commands (C# script as the fallback), and choosing packages by genre / platform / monetization. - **`urp-postprocessing`**, **`ui`**, **`2d-pixel-perfect`** — the visual baseline (Step 6): post-processing volume setup, HUD framework choice, pixel-perfect 2D. @@ -30,6 +28,19 @@ to other skills. moving on — platform and monetization answers change what you install, so don't gather everything up front or scaffold before they're settled. +## Before Step 1: check the delegated skills are loaded + +Look for `unity-cli`, `unity-package-management`, `urp-postprocessing` and `ui` in the skills your client lists. The first two carry the commands this flow delegates. Step 6 marks the last two as required. A client that got only this skill won't have the others. + +If any is missing, tell the user which one and what it does for them. Offer to install the Unity skills before you start, and don't install without a yes, because it changes their agent setup: + +- **Claude Code, with none of the four installed:** `unity setup claude --format json` installs the Unity plugin, which carries all of them. It would duplicate any you already have. An older CLI doesn't know `unity setup`, so use the next route then. A fresh install loads in the next session, or after `/reload-plugins`. Until then, read the skills from the `skillsPath` it reports. +- **Any other case, including a partial set:** ask the user to run `npx skills add Unity-Technologies/skills -g` in their own terminal, which needs Node.js. They pick only the missing skills and their client at its prompts. Don't run it yourself, since its prompts choose where the skills land. If the client reads a skills folder that command doesn't write to, tell the user to copy each missing skill's folder from github.com/Unity-Technologies/skills (under `skills`) into it. + +A newly installed skill may not load until the client restarts. If it still isn't listed, ask where the installer put it and read its `SKILL.md` from there. + +Don't stall on the two required skills. If the user would rather go on without them, carry on with Step 6's own guidance for that item. Name the missing skill in your final report, so they know the deeper guidance was skipped. The other two carry commands you shouldn't improvise. `unity skill show` prints `unity-cli` without installing it. Without `unity-package-management`, Step 5 can't run, so say so now, before Step 2 starts an Editor install, and let the user decide. + ## The flow — and where the parallelism is 1. **Concept** — what they're building. @@ -37,7 +48,7 @@ up front or scaffold before they're settled. install in the background** (it takes minutes) and keep talking. 3. **(joins)** Editor + platform modules finish installing. 4. **Project + source control** — create from a **URP** template matching 2D/3D; init git. -5. **Packages** — install via the C# Client API. +5. **Packages** — install with the `package add` command once the Editor is open, else the C# script. 6. **Visual baseline** — post-processing, camera, quality tier, UI stack, pipeline-correct shaders, so the first frame doesn't look like an untouched template. 7. **Save & first commit.** @@ -48,7 +59,9 @@ minutes the user spends answering concept questions, so setup feels instant. ## Step 1 — Concept -Use `AskUserQuestion` so the user can pick fast, but let them answer freely too. Cover: +Ask with your client's multiple-choice question tool if it has one (`AskUserQuestion` in Claude +Code, `ask_user` in Copilot CLI, `askQuestions` in VS Code), so the user can pick fast, but let +them answer freely too. Without one, ask in plain text. Cover: - **Genre / core loop** — platformer, top-down shooter, puzzle, idle, RPG, racing, card, tower defense, sim, hyper-casual, first-person, etc. @@ -116,7 +129,7 @@ Follow the **`unity-cli`** "Bootstrap a new project from scratch" workflow verba - Set up source control — **ask the user which they want**, don't assume: Git (GitHub / GitLab; add `--git-lfs` for asset-heavy games) or **Unity Version Control** (`--vcs uvcs`, which handles large binary assets natively — no LFS), or a purely local `git init` + Unity `.gitignore`. - Publish in one step with `unity projects create --vcs … --git-token-stdin --no-initial-commit` + Publish in one step with `unity projects create --vcs … --git-token-from-stdin --no-initial-commit` (tokens on stdin). Pass **`--no-initial-commit`** so the CLI doesn't commit the bare project before packages and `.meta` files exist — you make the real first commit/check-in in Step 7. See the `unity-cli` workflow for exact flags. @@ -124,8 +137,10 @@ Follow the **`unity-cli`** "Bootstrap a new project from scratch" workflow verba ## Step 5 — Packages Map the brief to a concrete package list and install it via the **`unity-package-management`** -skill (C# PackageManager Client API — **never** hand-edit `manifest.json`). Read that skill for -the genre/platform/monetization → package mapping, the installer script, and the `-quit` gotcha. +skill (**never** hand-edit `manifest.json`). Once the Editor is open and ready with `com.unity.pipeline` +(Step 6 installs it), use that skill's `package add` command. Step 6 opens the Editor, so when +the packages must land before the first open, use the skill's C# installer script instead (its +`-quit` gotcha applies). Read that skill for the genre/platform/monetization → package mapping. The template already provides the render pipeline — never add `com.unity.render-pipelines.universal` to a Built-in template project (nothing assigns a URP asset; materials go pink) or vice versa. Read the final list back to the user before installing; verify `manifest.json` afterward. @@ -137,13 +152,16 @@ tier, flat colors. Left there, agents reach for `OnGUI` and guess shader names, washed-out or magenta materials come from. Apply this floor **before** any gameplay work. **First, `unity pipeline install --project-path ""`, before opening the -Editor.** Running C# in the Editor, and `unity command screenshot` below, both need the -project's `com.unity.pipeline` package. A project created in Step 4 does not have it, and Step 5 +Editor.** Running C# in the Editor, and `unity command capture screenshot` below, both need the +project's `com.unity.pipeline` package. A CLI that predates create adding it by default leaves the project without it (a newer one already added it, and this install then changes nothing), and Step 5 does not add it: that step installs packages by launching the Editor binary with `-batchmode -executeMethod`, which never touches the package. Installing before the open is the supported order — the install updates `Packages/manifest.json`, which Unity reads at project -load. Run it against an already-open project and the CLI reports -`PIPELINE_MANIFEST_WRITE_FAILED`; ask the user to close the Editor and re-run. Without the +load. Run it against an already-open project and either the write fails with +`PIPELINE_MANIFEST_WRITE_FAILED` (ask the user to close the Editor and re-run), or it succeeds +but the open Editor doesn't load the package until its window regains focus. In the second +case `unity status` reports `STATUS_PIPELINE_LOAD_PENDING`; ask the user to switch to the +Editor window. Without the package, everything below fails to connect, which looks like the Safe Mode failure `unity-cli` describes but has a different cause. @@ -151,6 +169,10 @@ describes but has a different cause. reports the Editor ready, and apply each item below by running C# in it. **`unity-cli` owns those commands and their syntax** — don't re-derive them here. +Once the Editor is ready, look for `unity-pipeline` in your skills. It is the Pipeline package's own guide, and no client loads it from the package folder. If it is missing, read its `SKILL.md` now. It sits under `.claude/skills/unity-pipeline/` in the package: `Packages/com.unity.pipeline/` if the project embeds the package, otherwise the `Library/PackageCache/com.unity.pipeline@*` folder. List `Library/PackageCache` for the exact name, since the suffix is often a hash, not the version. + +To have your client load it in later sessions, `unity skill install --local` can mirror it, but only for a client with a project scope (`unity skill install --list` shows which, and Claude Desktop has none). It also writes a project copy of `unity-cli` beside it. The `unity-cli` skill suggests running it once, but here, wait until the user asks, and run it from the project folder after the open. In Claude Code with the Unity plugin installed it stops short, because the plugin already carries `unity-cli`. There, with the user's yes, copy the `unity-pipeline` folder into the project's `.claude/skills/` instead. Where no mirror is possible, read the package copy again in each session. + **Items 1, 2, 5 and 6 assume a URP template**, which is the Step 4 default. If the user explicitly chose Built-in, don't run them as written: `UniversalAdditionalCameraData`, `Light2D`, the `urp-postprocessing` volume framework and every `Universal Render Pipeline/*` shader are URP @@ -162,7 +184,8 @@ Built-in. 1. **Post-processing.** Global Volume with Tonemapping (ACES), low Bloom (intensity 0.5–1, threshold 0.9) and a subtle Vignette (≈ 0.25); set `renderPostProcessing = true` on the main camera's `UniversalAdditionalCameraData`. **REQUIRED SUB-SKILL:** `urp-postprocessing` — its - code templates create the volume and check HDR and the Volume layer mask. + code templates create the volume and check HDR and the Volume layer mask. If it isn't loaded, + build the volume from these values and check HDR and the layer mask yourself. 2. **Camera and light.** 2D → orthographic, solid background color from the brief's palette, and a `Light2D` of type Global in the scene if the template scene has none (the Sprite-Lit shaders render black without one). 3D → perspective; keep the template's directional light and skybox, @@ -172,7 +195,8 @@ Built-in. desktop, the lowest for mobile/WebGL. Leave color space Linear and the Input System as the template set them. 4. **UI stack.** HUD and menus use a uGUI Canvas + TextMeshPro or UI Toolkit — **never `OnGUI`**. - **REQUIRED SUB-SKILL:** `ui` picks between them for the project. + **REQUIRED SUB-SKILL:** `ui` picks between them for the project. If it isn't loaded, choose + between them from the brief and say which you chose, and why, in your final report. 5. **Materials and shaders.** Under URP use `Universal Render Pipeline/Lit`, `…/Unlit`, `…/2D/Sprite-Lit-Default` or `…/2D/Sprite-Unlit-Default`. `Standard` and `Unlit/Color` render pink or washed out under URP. Prefer `GraphicsSettings.currentRenderPipeline.defaultMaterial` @@ -180,7 +204,8 @@ Built-in. `currentRenderPipeline`, not `defaultRenderPipeline`: a quality tier can carry its own pipeline asset, and item 3 above just set the tier. 6. **Pixel art only.** Point filter mode on sprites and a Pixel Perfect Camera — see - `2d-pixel-perfect`. + `2d-pixel-perfect`. If it isn't loaded, do those two yourself and put the install route from + "Before Step 1" in your final report. Save the scene, then confirm both of these. **Not with `unity logs`** — that reads the CLI's own log, never the Editor's, so it reports clean whatever the scene looks like: @@ -188,7 +213,7 @@ log, never the Editor's, so it reports clean whatever the scene looks like: - No render-pipeline or shader errors Editor-side. Read the Editor console through `unity-cli`, or read `Editor.log` directly — that skill's "Recovering from Safe Mode" section has the per-platform paths. -- `unity command screenshot --output baseline.png` looks lit and tonemapped rather than flat +- `unity command capture screenshot --output baseline.png` looks lit and tonemapped rather than flat gray. ## Step 7 — Save & first commit @@ -212,6 +237,9 @@ unity open "" # only if not already open; for headless/CI use - **Unity Version Control (UVCS):** check in through your UVCS client/workspace (created during Step 4) — there's no `git` step. Generated folders are still excluded by the ignore rules. +Before either commit, if Step 6 mirrored skills into the project (`.claude/skills/` for Claude +Code), ask the user whether to include them or leave them out. + If you published via `--vcs` in Step 4 **without** `--no-initial-commit`, the CLI already made an initial commit of the bare project — add a follow-up commit here rather than double-committing. @@ -222,8 +250,11 @@ Based on Step 2 monetization, invoke the matching skill for the actual integrati - Ads → **levelplay-unity-integration** - Accounts / cloud save / economy / remote config / leaderboards → **build-live-game** +If the matching skill isn't loaded, put the install route from "Before Step 1" in the next +steps rather than integrating from memory. + Report the project path, Editor version, render pipeline and template, installed packages, the -visual baseline applied, and next steps. +visual baseline applied, any skill that was missing, and next steps. ## Scope — what this skill does NOT do @@ -236,12 +267,16 @@ visual baseline applied, and next steps. ## Checklist +- [ ] Delegated skills checked before Step 1 (`unity-cli`, `unity-package-management`, + `urp-postprocessing`, `ui`): a missing one offered for install, or named in the final report - [ ] Concept brief captured and confirmed (genre, look, gameplay, scope, name) - [ ] Platforms + monetization recorded; Editor version chosen - [ ] Editor + platform modules installed (started in the background during Step 2) - [ ] Project created from a **URP** template (`urp-blank` / `universal-2d`) unless Built-in was explicitly requested; git initialized with a Unity `.gitignore` -- [ ] Packages installed via the C# Client API; `manifest.json` verified; no render-pipeline package added on top of the template +- [ ] Packages installed with `package add` (or the skill's C# script before the first open); `manifest.json` verified; no render-pipeline package added on top of the template +- [ ] `unity-pipeline` read from the package folder once the Editor was ready, or mirrored into + the project with the user's yes - [ ] Visual baseline applied: global Volume (tonemapping, bloom, vignette) + camera post-processing on, quality tier set for the target, UI stack chosen (no `OnGUI`), URP shader names, screenshot checked - [ ] Project opened/saved so `.meta` files exist; first commit made; `Library/` excluded @@ -252,7 +287,9 @@ visual baseline applied, and next steps. - **Blocking on the Editor install** instead of backgrounding it while you ask questions. - **Installing the wrong Editor** because the version wasn't confirmed before the background install. - **Gathering all questions up front** — platform/monetization answers change the modules and packages. -- **Hand-editing `manifest.json`** instead of using the Client API (see `unity-package-management`). +- **Hand-editing `manifest.json`** instead of using the package commands or the skill's script (see `unity-package-management`). +- **Doing a required sub-skill's job from memory** when it isn't loaded, without telling the user + or offering the install. - **Committing `Library/`/`Temp/`/`obj/`/`Build/`**, or scripts without their `.meta` files. - **Missing Editor modules** — a mobile target needs `android`/`ios`; WebGL needs `webgl`. - **Picking `com.unity.template.2d` / `.3d` because the name matches** — those are the Built-in diff --git a/skills/optimize-web/SKILL.md b/skills/optimize-web/SKILL.md index 26c4ed2..02f214e 100644 --- a/skills/optimize-web/SKILL.md +++ b/skills/optimize-web/SKILL.md @@ -1,11 +1,84 @@ --- name: optimize-web -description: Optimizes Unity WebGL and WebGPU builds for smaller downloads and faster load. Use when a web build is too large or stutters, or the user asks about compression, stripping, shader variants, or web profiling. +description: Optimizes Unity WebGL and WebGPU builds for smaller downloads, faster loading, and smoother browser performance. Use when asked to shrink a web build, speed up its load, or fix stutter, memory or battery problems in the browser. --- +Audit and apply the Player Settings, compression, stripping, and server setup that make a Unity Web build smaller and faster. + +## Important + +- Use the unity cli (`unity command --format json`) and the `unity-cli` skill for each step. Use `unity-pipeline` too when it is available. +- Before making changes, establish a baseline of the current behavior so you can tell whether a later result is actually caused by your change. +- Create a restore point you can roll back to if your changes fail. +- Do only what's asked. Don't change unrelated assets or files. +- If the changes affect runtime behavior, add `Debug.Log` lines that will prove the actual scene behaviour (not just your changes) is now working - you'll read these when you enter Play mode to validate later. For example, log a GameObject's Transform scale if your fix is meant to make sure it changes size. +- Avoid long explanations. + ## Performance Notes - Take your time to do this thoroughly. - Quality is more important than speed. +## Full topic map + +Before you go forward with a workflow or diagnosis: + +1. Open and read the [documentation page(s)](#full-topic-map) that cover the specific subject related to what you intend to do or fix and check if you've missed anything. +2. Name the file(s) you read in your response. + +This is because your training knowledge about Unity might be out of date, incorrect, or for the wrong Unity version. + +If the Editor install folder has a `Data/Documentation` folder (eg `...Unity/Hub/Editor/6000.6.0b2/Editor/Data/Documentation`), read the html files from the `/Editor/Data/Documentation/en/Manual` folder. Otherwise the docs pages are online at `https://docs.unity.com/en-us/engine//manual/`. + +For the packages this skill uses (`com.unity.cloud.ktx`, `com.unity.addressables`, `com.unity.web.stripping-tool`), the docs pages are local markdown conversions of the Unity Manual, stored at `/Library/PackageCache/@*/`. Read only the .md files in that specific `PackageCache` subfolder. You must not look at non-.md files, or look outside at other Library or PackageCache folders, otherwise it takes too much time. If that folder has no `.md` files, use `https://docs.unity3d.com/Packages/@/manual/` instead. + +Prefer `WebFetch` over `WebSearch` — faster and lands on the exact reference. Only fetch what you need. Replace `` with the following before fetching: + +- **docs.unity3d.com**: The Unity version, eg `6000.6` +- **Package on docs.unity3d.com**: The package version, eg `@6.6` or `@2.0`. Check the package version if unsure. +- **docs.unity.com**: The Unity version, eg `6000.6` + +Read the Unity major.minor from `ProjectSettings/ProjectVersion.txt` (for example `6000.0` from `6000.0.58f1`, because the full build number gives a 404), and each package's major.minor from `Packages/packages-lock.json`. + +- [Recommended Player settings to optimize your Web build](https://docs.unity.com/en-us/engine//manual/platform-specific/webgl/building-distribution/web-optimization/player.md) — local `web-optimization-player.html`: release values for compression, stripping, exceptions and memory. +- [Use C# code to enable optimization settings](https://docs.unity.com/en-us/engine//manual/platform-specific/webgl/building-distribution/web-optimization/c-sharp.md) — local `web-optimization-c-sharp.html`: editor script that applies the recommended settings in one step. +- [Remove unused resources from your Web build](https://docs.unity.com/en-us/engine//manual/platform-specific/webgl/building-distribution/web-optimization/remove-resources.md) — local `web-optimization-remove-resources.html`: removing unused packages and stripping shader variants. +- [Distribution size and code stripping](https://docs.unity.com/en-us/engine//manual/platform-specific/webgl/building-distribution/distributionsize-codestripping.md) — local `webgl-distributionsize-codestripping.html`: what drives Web build size and how stripping works. +- [Deploy a Web application](https://docs.unity.com/en-us/engine//manual/platform-specific/webgl/building-distribution/deploying.md) — local `webgl-deploying.html`: server compression headers, MIME types and Decompression Fallback. +- [Memory in Unity Web](https://docs.unity.com/en-us/engine//manual/platform-specific/webgl/develop/memory.md) — local `webgl-memory.html`: Wasm heap size, memory growth and browser memory limits. + +### API reference + +The API is `UnityEditor.PlayerSettings`, `UnityEditor.PlayerSettings.WebGL` and `UnityEditor.WebGL.UserBuildSettings`, part of the Unity Engine. It is versioned by Unity version, so check the project's Unity version before relying on a specific class or method. + +1. Use the `unity-cli` skill to check the project's Unity version. +2. If the Editor install folder has a `Data/Documentation` folder (e.g. `.../Unity/Hub/Editor/6000.6.0b2/Editor/Data/Documentation`), grep the html files in the `en/ScriptReference` folder inside, and skip the remaining steps. +3. Otherwise, fetch and cache `https://docs.unity3d.com//Documentation/ScriptReference/xrefmap.yml` (replace `` with the Unity version, e.g. `6000.3`). This file lists every public API member's fully qualified name as a `uid` (e.g. `uid: 'UnityEngine.Rigidbody.AddForce(UnityEngine.Vector3)'`). +4. Grep the yml file locally for a class or method instead of refetching per lookup. The file is large (about 6 MB), so don't read the whole file. +5. Convert the `uid` to a markdown page: remove the parameters in brackets, lowercase it, replace `.` with `/`, and add `.md`. Then fetch `https://docs.unity.com/en-us/engine//script-reference/`. For example, `UnityEngine.Rigidbody.AddForce(UnityEngine.Vector3)` => `https://docs.unity.com/en-us/engine/6000.3/script-reference/unityengine/rigidbody/addforce.md`. All overloads are on the same page. + - Constructors use `ctor` (e.g. `unityengine/vector3/ctor.md`), and indexers use `item` (e.g. `unityengine/vector3/item.md`). + - For operators, fetch the class page (e.g. `unityengine/vector3.md`) and follow its link. + - Links inside the pages start with `/engine/`. Add `https://docs.unity.com/en-us` before them. + - If the page doesn't exist for that Unity version, use the `href` in the `xrefmap.yml` entry instead (e.g. `href: 'Rigidbody.AddForce.html'` => `https://docs.unity3d.com/6000.3/Documentation/ScriptReference/Rigidbody.AddForce.html`). + +Always use the docs for the project's Unity version rather than relying on training knowledge, since APIs are frequently marked obsolete between versions. + +The KTX API (`KtxUnity`) is part of the `com.unity.cloud.ktx` package. It is versioned by package version, not Unity version, so check the project's installed package version before relying on a specific class or method. + +1. Read the installed package version from `Packages/packages-lock.json`. +2. Fetch and cache `https://docs.unity3d.com/Packages/com.unity.cloud.ktx@/xrefmap.yml` (replace `` with the installed package version, e.g. `3.4`). This file maps every public API member's fully qualified name to its docs page `href`. +3. Grep the yml file locally for a class or method instead of refetching per lookup. Don't read the whole file. + +## Procedure + +1. Get the project context: the Unity version from `ProjectSettings/ProjectVersion.txt`, and the versions of the packages you need (`com.unity.cloud.ktx`, `com.unity.addressables`, `com.unity.web.stripping-tool`) from `Packages/packages-lock.json`. If there's no project, assume Unity `6000.0`. For any package not listed in `packages-lock.json`, assume the latest version in that package's documentation. Preserve every version you do find — only default the values that are actually missing. Name each assumption in your report and carry on. If the user hasn't said what to optimize and there's no project to read, ask what they want to improve. +2. Read the docs pages that cover the task, from the [Full topic map](#full-topic-map). +3. Set up `eval` as described in [Running C# in the Editor](#running-c-in-the-editor). If no Editor is reachable or `eval` is missing, say so, skip to step 5, then go straight to step 9. Mark build validation and runtime checks as unperformed in your report, and base your recommendations on the user's request and the docs from step 2. +4. Run [0. Pre-Flight](#0-pre-flight) and [1. Assess Current State](#1-assess-current-state). Their results are your baseline. +5. Match the request to a row in [2. Understand Request](#2-understand-request). +6. Create a restore point, then apply the changes from [3. Web Build Optimization Workflow](#3-web-build-optimization-workflow). If a symptom matches [5. Troubleshooting](#5-troubleshooting), follow that entry. +7. Run [4. Validation](#4-validation): rerun the Pre-Flight snippet and compare the values with your baseline. +8. Do the checks in [Final step](#final-step). +9. Give the [Final report](#final-report). + ## Running C# in the Editor Every step below that reads or writes a Player Setting runs inside a live Editor through the Unity @@ -18,8 +91,8 @@ Two things it can't know for you: - **You need `eval` in particular**, not just a reachable Editor. Confirm it appears in the catalog. Its presence depends on the Pipeline package version, not on the CLI, so a healthy install can - still lack it — if it's missing, say so and stop. -- **Player Settings can be read from `ProjectSettings/ProjectSettings.asset` in a pinch, but do not + still lack it — if it's missing, say so and skip the steps that read or write settings. +- **You can read Player Settings from `ProjectSettings/ProjectSettings.asset` in a pinch, but do not write them that way.** The serialized names don't match the API names, several of these settings are per-build-target, and a hand-edited value silently disagrees with what the build actually uses. An unreachable Editor is a stop for the write steps. @@ -29,13 +102,14 @@ timeout. ### Passing C# to `eval` -`eval` compiles a **statement block, not a file**. Two consequences, both compile errors rather than -warnings: +`eval` compiles a **statement block, not a file**, so write the code without `using` directives and +with fully qualified types. If the code doesn't compile, look up the error here: -- **No `using` directives.** The compiler reads `using UnityEditor;` as a resource-disposal - statement and rejects it (`CS0210`). -- **Types must be fully qualified.** A bare `PlayerSettings` does not resolve (`CS0246`), and a bare - `Object` is ambiguous with `object` (`CS0104`). +| Error | Cause | Fix | +| --- | --- | --- | +| `CS0210` | A `using` directive, such as `using UnityEditor;`, which the compiler reads as a resource-disposal statement | Remove the directive and qualify the types | +| `CS0246` | An unqualified type, such as `PlayerSettings` | Use the full name, such as `UnityEditor.PlayerSettings` | +| `CS0104` | A bare `Object`, which is ambiguous with `object` | Use `UnityEngine.Object` | ### Reading the settings this skill audits @@ -86,7 +160,7 @@ than assuming the repository carries it. ### Applying the settings Most of the writes in this skill are a single batch, and -[resources/WebOptimizer.cs](resources/WebOptimizer.cs) already is that batch. It declares a class +[scripts/WebOptimizer.cs](scripts/WebOptimizer.cs) already is that batch. It declares a class with a `[MenuItem]`, so it is a **project file, not `eval` input** — a class declaration cannot be flattened into a statement block. Save it under `Assets/Editor/`, let Unity compile, then invoke it in one line: @@ -133,7 +207,7 @@ conclusion from a test that cannot see the defect. 3. **Read exception and optimization settings:** Read `PlayerSettings.WebGL.exceptionSupport` from the Pre-Flight snippet above. For the wasm code optimization level use `UnityEditor.WebGL.UserBuildSettings.codeOptimization` — the `PlayerSettings.WebGL.codeOptimization` and `optimizationLevel` spellings do not exist and will not compile. 4. **Read frame rate settings:** Read, with the Pre-Flight snippet above, `Application.targetFrameRate` and `QualitySettings.vSyncCount`. 5. **Read additional player settings:** Read, with the Pre-Flight snippet above, `PlayerSettings.WebGL.dataCaching`, `PlayerSettings.WebGL.debugSymbolMode`, `PlayerSettings.WebGL.maximumMemorySize`, and `PlayerSettings.GetApiCompatibilityLevel`. -6. Proceed only after compression, stripping, frame rate, and player settings are confirmed. +6. Proceed only after you've confirmed the compression, stripping, frame rate, and player settings. ## 1. Assess Current State @@ -167,7 +241,7 @@ conclusion from a test that cannot see the defect. ### IMPORTANT: One-click optimization script -**Always offer to generate this script for the user.** Unity's official web optimization docs provide a single editor menu script that applies all recommended release settings at once. Place in `Assets/Editor/WebOptimizer.cs` — see [resources/WebOptimizer.cs](resources/WebOptimizer.cs) for the template. +**Always offer to generate this script for the user.** Unity's official web optimization docs provide a single editor menu script that applies all recommended release settings at once. Place in `Assets/Editor/WebOptimizer.cs` — see [scripts/WebOptimizer.cs](scripts/WebOptimizer.cs) for the template. Adapt the script to the user's project needs (e.g. keep exceptions if they use `try/catch`, switch Brotli to Gzip for HTTP hosting). This script is the single most impactful action for a new web project — it prevents settings from being missed. @@ -237,7 +311,7 @@ Three categories to audit for build size reduction: | **Batch Renderer Group Variants** | Strip All (if BRGs are not used) | | **Always Included Shaders** | Audit and remove any shaders the project does not reference | -Test after stripping — ensure no referenced shaders were removed. +Test after stripping — make sure you didn't remove any referenced shaders. **3. Web Stripping Tool** (`com.unity.web.stripping-tool`) — Analyzes the WebAssembly binary and identifies unused Unity engine submodules (e.g. 3D graphics in a 2D-only game). Install via Package Manager, profile the build, then configure which submodules to exclude. Can yield substantial size reductions beyond what Managed Stripping Level achieves alone. @@ -272,7 +346,7 @@ KTX2 with Basis Universal supercompression ships a single texture file that tran | **Memory** | Transcoded textures are standard GPU textures; memory cost equals the target format, not the KTX2 file size | | **Orientation** | Always include `--lower_left_maps_to_s0t0` to match Unity's UV convention | -**`toktx` CLI examples:** See [resources/toktx-examples.sh](resources/toktx-examples.sh) for commands covering albedo (ETC1S), normals/detail (UASTC), ICC profile errors, and linear data. +**`toktx` CLI examples:** See [scripts/toktx-examples.sh](scripts/toktx-examples.sh) for commands covering albedo (ETC1S), normals/detail (UASTC), ICC profile errors, and linear data. ### Streaming on Web @@ -406,15 +480,42 @@ npx serve path/to/build -l 3001 For Brotli testing, use HTTPS — Brotli requires a secure context. Generate a self-signed cert with OpenSSL for local testing. -## 6. Completion +## Final step + +Web settings only take effect in a browser build, so a clean compile and Play mode in the Editor aren't enough. Run these checks in order. If any check fails, reread the [docs pages](#full-topic-map) to find out what you missed, fix it, and run the checks again. + +1. Check the project has zero console errors. +2. Check that each setting you changed is saved, as described in [Verify from disk](#verify-from-disk-not-from-the-objects-you-just-wrote): `ProjectSettings/ProjectSettings.asset` holds the new values, and `codeOptimization` reads back through the API. +3. If you rebuilt the player, serve the build as described in [Local dev server setup](#local-dev-server-setup) and open it in a browser. Then check the following: + - The browser console shows no errors, such as a failed decompression or an out-of-memory error. + - In the Network tab, the `.wasm`, `.data` and `.framework.js` files return the `Content-Encoding` header that matches the compression format (`br` for Brotli, `gzip` for Gzip), and their sizes match the Build Report. +4. If the change affects runtime behaviour, such as the frame rate or quality level, check the `Debug.Log` lines in the browser console, then remove them. Try to avoid taking and checking screenshots unless absolutely necessary, as they take more time. + +## Final report -- Summarize: initial download size delta, settings changed (compression, stripping, exceptions, targetFrameRate), server configuration confirmed. -- List follow-up actions: CDN setup for Addressables remote groups, Safari testing, Wasm 2023 feature set upgrade when browser baseline allows. +Give the user a concise but not dense checklist of what you did and why. Include the following: + +- The download size before and after, and each setting you changed (compression, stripping, exceptions, `targetFrameRate`), with its old and new value. +- Any side effects, such as a script you added to the project (for example `Assets/Editor/WebOptimizer.cs`), a quality level you changed, or a setting that's stored per machine and doesn't travel to teammates or CI (`codeOptimization`). +- Any check you didn't run, such as a browser test without a rebuild, or every check when no Editor was available, and the assumptions you made. + +If the user needs to follow up with more steps or decisions, list them, such as CDN setup for Addressables remote groups, Safari testing, or moving to the Wasm 2023 feature set when their browser baseline allows. For example: + +``` +I made the following changes: + +[✓] Set Compression Format to Brotli and Strip Engine Code to on, because the server sends `Content-Encoding: br` over HTTPS. +[✓] Added `Assets/Editor/WebOptimizer.cs`, which applies the release settings from Tools > Apply Web Release Settings. + +Follow these steps to finish: + +1. Apply `codeOptimization` in your CI build step, because it's stored in `Library/` and doesn't travel with the project. +2. Test the build in Safari, because its garbage collection and JIT behaviour differ from Chrome. +``` ## See also -These point at Unity tooling rather than other skills, because the topics they cover are not in -this plugin: +Point the user at this Unity tooling when the task goes beyond this skill: - **Addressables package** — remote groups served over a CDN, when the download budget needs content moved out of the initial payload. diff --git a/skills/optimize-web/resources/WebOptimizer.cs b/skills/optimize-web/scripts/WebOptimizer.cs similarity index 100% rename from skills/optimize-web/resources/WebOptimizer.cs rename to skills/optimize-web/scripts/WebOptimizer.cs diff --git a/skills/optimize-web/resources/toktx-examples.sh b/skills/optimize-web/scripts/toktx-examples.sh similarity index 100% rename from skills/optimize-web/resources/toktx-examples.sh rename to skills/optimize-web/scripts/toktx-examples.sh diff --git a/skills/unity-cli/CHANGELOG.md b/skills/unity-cli/CHANGELOG.md index 5ff9762..8ff7813 100644 --- a/skills/unity-cli/CHANGELOG.md +++ b/skills/unity-cli/CHANGELOG.md @@ -12,6 +12,102 @@ release's own dated section once the CLI actually ships it. ## [Unreleased] +## CLI `1.0.0-beta.14` (2026-10-21) + +Aligned to the CLI's `1.0.0-beta.14` release. Everything below was documented ahead of the release and ships in it. + +### Added + +- **Agent install hints**: `SKILL.md` lists `UNITY_NO_AGENT_HINTS`, which turns off the suggestion to install Unity's agent plugin or skill, and names the `AGENT_PLUGIN_AVAILABLE` envelope notification. +- **`unity setup antigravity`**: `integration-advanced.md` documents installing Unity's agent plugin into Antigravity, the `agy` command it runs, where it reads an existing install, and the not-found and install-failed error codes. `SECURITY.md` extends the plugin-install fence to it. +- **Docs workflow for agents**: a new `docs-workflow.md` reference gives the order an agent follows to read the right Unity documentation: find a page with `docs toc`, resolve and cite its address with `docs --url`, and match the Editor version with `--editor-version`. It covers the daily page-index cache and the offline behavior, uses 2D examples, and lists reading page content in the terminal, offline content and package docs as not yet available. +- **Accepting the Unity Editor Software Terms with `unity terms`**: `editors-install.md` documents `unity terms accept ` (it shows the terms link and the Editor's own copy, then asks; `--yes` for unattended setup, and the `TERMS_ACCEPT_NEEDS_CONFIRMATION` refusal without a terminal) and `unity terms status`. `projects-templates.md` now says current Editors ignore `-acceptSoftwareTermsForThisRunOnly` and points at `terms accept`, which an agent runs with `--yes` only when the user has agreed to the terms. +- **Reading and following the Editor console with `unity logs --console`**: `diagnostics-maintenance.md` documents `--console`, `--console --follow` and `--project-path` on `unity logs`, the entry fields (Burst and compiler errors carry a `diagnostic` with their code, file and line), how `--level` maps onto the console's levels, and how the follower handles a script reload, an Editor restart and evicted entries. +- **Safe Mode in `unity status`**: the Safe Mode recovery in `integration-advanced.md` and the Safe Mode notes in `SKILL.md` now start from `unity status`, which reports an Editor in Safe Mode (`STATUS_SAFE_MODE`) and lists its compile errors (`safeMode.compileErrors` under `--format json`). Reading `Editor.log` by hand is the fallback when it can't. +- **Using the Hub's sign-in**: `auth-license-cloud.md` says a signed-out CLI on a terminal asks "Use , signed in to Unity Hub? (y/N)" from `unity auth` and `unity auth login`, that yes switches only the CLI's own account, and that `--json`, `--non-interactive` and runs without a terminal never switch but report `adoptable` and the `unity auth switch ` command, which an agent should show the user rather than run unasked. +- **Named dialog codes on `unity open --until-ready`**: `projects-templates.md` says the Safe Mode prompt, the Auto Graphics API Notice and the Software Terms dialog are reported as `OPEN_BLOCKED_BY_DIALOG_SAFE_MODE`, `_GRAPHICS_API` and `_SOFTWARE_TERMS`, with the same remedies `unity status` gives (and the Safe Mode compile errors), and that all keep the `OPEN_BLOCKED_BY_DIALOG` prefix. `integration-advanced.md`'s Safe Mode recovery names the `open` code too. +- **Search first, never grep help**: `SKILL.md` now leads with a "Find a command" section telling agents to use `unity commands --grep` (built-in commands and plugins) and `unity command --query` (Pipeline commands of the connected Editor or a running Player) instead of filtering `unity --help` with `grep`, and says which covers what until a unified search ships. Needs a `sync-cli-skill` publish. +- **`--operating-system` on `releases` and `install --dry-run`**: `editors-install.md` documents listing another OS's releases and previewing another OS's install, the `CROSS_OS_INSTALL_UNSUPPORTED` and `INVALID_OPERATING_SYSTEM` refusals, and that `--list-modules`, `modules list` and `editors --releases` stay on the host's OS. +- **Word search and ranking in `commands --grep`**: `SKILL.md` and `integration-advanced.md` say plain words match in any order and rank by command name, parent path, description, then options, while a pattern with a regex character stays a regular expression in tree order. +- **See-also between `cloud project` and `projects link cloud`**: `auth-license-cloud.md` notes that linking a local folder to a cloud project is `unity projects link cloud `, and that the two help pages now point at each other. +- **What each command search skips**: `SKILL.md` says `unity commands --grep` never searches Pipeline commands and that each search names the source it skipped (`data.hint`); `integration-advanced.md` documents `unity command --query` with no connected Editor (exit 0, a pointer at `commands --grep` with its match count) and on an empty result. +- **Search pointer in help and errors**: "Getting help" says `unity --help` and the unknown-command error end with "Not sure which command? `unity commands --grep `". +- **The Unity Editor Software Terms dialog**: `projects-templates.md` says a first windowed open can stop at it despite `-acceptSoftwareTermsForThisRunOnly`, that `unity status` reports it as `STATUS_BLOCKED_BY_DIALOG_SOFTWARE_TERMS`, and that only the user accepts it. It also says `unity open` passes the flag only on a headless open, and `--args` cannot remove it. +- **The Auto Graphics API Notice**: `projects-templates.md` says `unity status` reports the Windows notice an upgraded project shows as `STATUS_BLOCKED_BY_DIALOG_GRAPHICS_API`, and that the user answers it with Confirm. +- **The "Enter Safe Mode?" prompt**: `integration-advanced.md`'s Safe Mode recovery now covers the prompt a windowed Editor stops on before Safe Mode, which `unity pipeline list` can't see. `unity status` reports it as `STATUS_BLOCKED_BY_DIALOG_SAFE_MODE` with the first compile errors. The section says no launch flag answers it, that the user answers it (Quit, or Enter Safe Mode), and that an agent stops only an Editor it started, by PID. +- **Headless / fleet license activation**: `auth-license-cloud.md` has a recipe for machines with no browser: `--serial` for perpetual and legacy Pro/Plus serials, `--generate-request` then `--file` for air-gapped machines (with the licensing client staged before the machine goes offline), `--floating`, and `license return` on decommission. It states that there is no username/password option and that a service account cannot activate a license. Verified: `--serial` needs no sign-in on either engine. +- **Hub accounts in `auth list`**: "Separate sign-in from Hub" explains that an account signed in to in the Hub appears in `unity auth list` without the CLI being signed in to it, and that `unity auth switch` activates it when its sign-in is readable, or `unity auth login` signs it in when not. +- **`UNITY_NO_CLOUD` and where the default project applies**: `SKILL.md`'s env table and `projects-templates.md` document `UNITY_NO_CLOUD` beside `--no-cloud` (presence-based, yields to an explicit cloud flag). `auth-license-cloud.md` says the saved default feeds the service commands and `projects link cloud` but not `projects create`, and that `cloud project current` prints the default rather than the folder's project. +- **`--accept-upgrades` on `unity open` and `unity projects open`**: `projects-templates.md` documents the opt-in that passes `-skipUpgradeDialogs` to the Editor, what it says yes to (the project upgrade and the API Updater's script rewrite, with no backup prompt), why it is never a default, and `unity projects upgrade` as the deliberate path. +- **`--download-directory` (`-d`, alias `--install-dir`) on `unity install` and `unity install-modules`**: `editors-install.md` documents installing a self-contained Editor into a named folder without registering it or changing the install path, the `data.path` result field, the per-folder already-installed check and lock (a `-d` naming the Hub's own install folder locks like a normal install), the `--resume` refusal, and adding modules there later. +- **`--bare` and the `steps` result field on `unity projects create`**: `projects-templates.md` documents creating a project without the default project scaffolding while keeping the cloud link and `--vcs`, the `--with-pipeline` refusal (`BARE_ARGS_CONFLICT`), and the per-step report with its statuses and reason codes. + +### Changed + +- **SECURITY.md no longer coaches scanners**: the section on how to read an automated scanner's verdict is gone. It asked readers to discount verdicts, which a scanner reads as the skill vouching for itself; the accepted risks and their safeguards are unchanged. +- **Play Mode loop turns auto-tick back off**: `playmode-verification-loop.md` step 8 and the command summary turn `editor set_auto_tick` off after `editor.playmode stop`, next to the Run In Background revert, unless the agent will keep driving the Editor. +- **`setup claude` installs from the official marketplace**: `integration-advanced.md` and `SECURITY.md` say `unity setup claude` runs `claude plugin marketplace add anthropics/claude-plugins-official` and `claude plugin install unity@claude-plugins-official`, and that an earlier `unity@unity-agent-plugin` install is reported as installed. +- **`unity mcp` desktop capture is opt-in**: `integration-advanced.md` says `capture_game_view` and `capture_scene_view` return an error when the Editor can't be captured, and fall back to a screenshot of every display only when `UNITY_MCP_DESKTOP_FALLBACK=1` is set for the server. +- **Search rows say what matched**: `SKILL.md` and `integration-advanced.md` say each `commands --grep` and `command --query` row carries `path` and `matchedOn` (`name`, `description` or `option`) beside `kind`, `description` and `invoke`, that an installed plugin's `invoke` is `unity `, and that tsv appends `path` and `matchedOn` as its last columns. +- **A timeout is whole seconds**: `SKILL.md` says every `--timeout`, and `UNITY_BUILD_TIMEOUT`, `UNITY_RUN_TIMEOUT` and `UNITY_TEST_TIMEOUT`, take a whole number of seconds written as digits, and that `30s`, `5m`, `1.5` or `1e3` is a usage error (exit 2). +- **`--timeout` bounds the wait for another install**: `editors-install.md` writes `unity install`, `unity install-modules` and `unity projects require` with `--timeout `, the spelling every other command uses, and says the old `--wait-timeout` still works with a deprecation warning. `SKILL.md` lists `--wait-timeout` among the old spellings that warn. +- **`--since` means a revision**: `version-control.md` writes `vcs git migrate-lfs --after ` and `projects-templates.md` writes `releases --from-year `, and the common-traps entry says `--since` takes a revision on `vcs summarize`, `vcs affected` and `test`. `SKILL.md` lists `--since` on `releases` and `vcs git migrate-lfs` among the old spellings that warn. +- **`collaboration` uses `--cloud-org` and `--cloud-project`**: `collaboration.md` writes `--cloud-project ` and `--cloud-org ` everywhere it wrote `--project-id` and `--organization-id`, says both take an id only and that the old spellings still work and warn, and says the organization of `jira server`, `jira project add`/`delete`/`update` and `jira issues list` falls back to `UNITY_CLOUD_ORG` and then the stored default organization, while `jira configs` reads only the scope typed, and that an empty or whitespace-only flag or variable counts as not set. `SKILL.md` lists the two old spellings among those that warn. +- **One command search**: `SKILL.md` says `unity commands --grep` and `unity command --query` are now one search over built-in commands, plugins and the connected Editor's Pipeline commands, and that each row says how to run it. `integration-advanced.md` documents the `pipeline` kind, the `invoke` field, the line that says why the Editor was not searched, and that `--query` beside another listing flag is still the Editor's own filter. `playmode-verification-loop.md` and the `runtime-verify` skill read a command's schema with `--query --detail full`. +- **Old option spellings warn**: `SKILL.md` says an old spelling the CLI still accepts (`projects list -a`, `status --project`, `--template-version`, `--hub-version`, `--package-version`, `--list-components`, `--install-dir`, `--childModules`) prints a deprecation warning naming the new one, in the envelope's `warnings` under `--format json`. `config-hub.md`, `editors-install.md`, `integration-advanced.md` and `projects-templates.md` call those spellings deprecated rather than hidden aliases, and `integration-advanced.md` filters `status` with `--project-path`. +- **One spelling per option**: `SKILL.md`, `projects-templates.md`, `version-control.md` and the `new-unity-project` skill pipe Git tokens to `--git-token-from-stdin`; `collaboration.md` writes the annotation anchor `--local`, as on spatial attachments; `integration-advanced.md` writes `command --group-by` and drops the note not to correct `--group_by`; `editors-install.md` shows every column with `editors --long`. `SKILL.md` lists the old spellings among the deprecated ones that warn, and says `--verbose` on `editors` and `projects list` still shows every column but warns. +- **URP is the default template**: `projects-templates.md` says `projects create` without `--template` uses the Editor version's own `com.unity.template.urp-blank`, that `--template 3d` and `--template 2d` name the version's URP templates, that create never falls back to a Built-in Render Pipeline template, and that a version without it fails with `TEMPLATE_NOT_FOUND` and lists what it offers in `data.offered`. +- **Peer code-signature check**: `SKILL.md` no longer lists a development-only setting that weakens the peer code-signature check, and `auth-license-cloud.md` no longer documents it. `auth-license-cloud.md` says that the auth broker and the Editor identity helper refuse an unsigned or non-Unity-signed peer on macOS and Windows, and that an Editor built from source isn't supported for automatic sign-in. Earlier entries below are reworded to match. +- **`auth login` progress on stderr**: `auth-license-cloud.md` says the `--format json` `auth_url=` progress frame goes to stderr, so stdout is the envelope alone. +- **One meaning per short flag**: `SKILL.md` says `-y` is `--yes` on every command that has `--yes` (now including `bug mcp`, `mcp configure`, `skill install` and `skill refresh`), and `-a` is `--architecture` everywhere, so `projects list` and `projects size` are written with `--all`. +- **Failures never reach stderr as JSON**: `SKILL.md` no longer says some commands still print `{"error": "…"}` to stderr, and lists `unity cloud org list` and `unity cloud project list` among the table listings that end in a `{"count": N}` frame. +- **`unity mcp` says why it lists no Editor tools**: `integration-advanced.md` says that until an Editor is reachable, the tool list holds one `unity-cli-status` tool whose description gives the reason, and that calling it checks again. +- **`skill install` writes one JSON document**: `integration-advanced.md` says `--format json` writes one envelope whose `data` holds a record per skill considered. +- **`skill install` says when it skips**: `integration-advanced.md` says a non-interactive `unity skill install` without `--yes` skips an outdated copy, or a skill a plugin already provides, still exits `0`, and marks the skipped skill's JSON result with `skipped: true` and a `skipReason`. +- **More ndjson answers end in their frame**: `SKILL.md` no longer lists `unity pipeline list` or `unity changelog` among the commands whose ndjson stream lacks a terminal `{"type":"result"}` frame. +- **`editors upgrade` leaves the IDE modules behind**: `editors-install.md` says the upgrade no longer carries Visual Studio Code or Visual Studio over to the new editor, and that `-m visualstudiocode` installs it anyway. +- **`--version` names a command's own subject below the root**: `unity --version` and `unity -V` still print the CLI version, but subcommands no longer inherit them. `config-hub.md`, `integration-advanced.md` and `projects-templates.md` now use `hub install --version`, `pipeline install --version` and `templates create|pack|edit --version`, and say the old `--hub-version`, `--package-version` and `--template-version` spellings still work. +- **Single-record ndjson answers end in their frame**: `SKILL.md` says `unity version`, `unity language`, `unity config proxy`, `unity cache info` and other single-record answers write one `{"type":"result"}` frame whose `data` matches `--format json`, and that `unity env` writes its rows and then that frame, so it no longer lists `unity env` or `unity version` among the commands that lack one. +- **`status` and `editors running` rows use the shared key names**: `integration-advanced.md` says `unity status` rows name the project as `projectPath` and the Editor as `editorVersion`, with `project` and `version` deprecated, and `editors-install.md` says `unity editors running` rows carry `editorVersion`, with `unityVersion` deprecated. +- **Pin each Editor call to its worktree's project.** `SKILL.md`'s one-Editor-per-worktree paragraph told agents to set `UNITY_PROJECT_PATH` once for the session. Most agent tools run each command in a fresh shell, so the variable never reached later calls, which then targeted the source checkout's Editor. It now says to pass `--project-path` on every call that reaches the Editor, or to prefix each call with the variable, and shows the prefixed form. `integration-advanced.md`'s target-resolution note says the same. +- **Play Mode checks on a locked or unattended desktop.** `playmode-verification-loop.md` now says a locked Windows session can stop Play Mode from advancing, even with Run In Background on. Stepping it by hand works: pause the Editor, then call `Step()` through `script eval`. That is only for a session the agent started, so the loop now opens by reading the Editor's play state. `capture game_view --source screen` needs an unlocked desktop (reported on Windows, where a locked session returned a solid cyan image). `--source camera` works when the machine may be locked, except for a Screen Space - Overlay canvas, which no camera draws. `SKILL.md`'s Play Mode note names the locked case. +- **No hard-coded "latest" version or release stage**: `SKILL.md`'s Notes no longer name a CLI version as the latest, or say the CLI is currently in beta. It points at `unity --version` and `unity self-update --check --format json`, so the installed skill can't call itself a release behind. +- **`unity status` and modal dialogs**: `integration-advanced.md` says a dialog-frozen Editor reads `starting`, that an Editor busy running another command still reads `ready`, and documents `STATUS_BLOCKED_BY_DIALOG` and the row's `blockedBy` (Unity 6000.7 or later names the dialog; an older Editor's row is plain `starting`). It listed four failure codes and no `blockedBy`. +- **The Pipeline package is part of every `unity projects create`**: `projects-templates.md` documents that create adds `com.unity.pipeline` unless `--bare`, that `--with-pipeline` is now a no-op, the quiet `UNSUPPORTED_VERSION` skip before Unity 6, and drops the agent-session hint; `integration-advanced.md` no longer points at `--with-pipeline`. +- `SKILL.md`'s "CI: activate a license, then build" workflow no longer signs in a service account and then runs the default activation, which a service account is refused. It activates with `--serial` (or `--floating`), runs no `unity auth login` (service-account environment variables cover cloud commands without a credential store), and says `license return` covers serial and assigned seats. The CI step reads `UNITY_LICENSE_SERIAL`, the secret `unity ci init` generates, and scopes `--serial` to perpetual or legacy serials. `auth-license-cloud.md` no longer says `unity auth logout` has a confirmation prompt for `--yes` to skip, no longer says `unity auth revoke` stops an application from using your sign-in (it records it in a local list the auth broker does not consult yet), and no longer says `--serial` works offline: it needs no sign-in, but it does reach the licensing server. +- **One `MonoBehaviour` per file for a player build.** `build-run-test.md`'s Build section and `SKILL.md`'s headless build section now tell agents to keep each `MonoBehaviour` they write alone in a `.cs` file named after its class. The symptom of getting it wrong is a built player that crashes at startup saying a scene file such as `level0` is corrupted. That error points at scene data, and the Editor and Play Mode can run without a warning. +- **`vcs git worktree` paths**: `SKILL.md` no longer says the output masks paths, and `version-control.md` says `--format json` carries the real paths while human output masks them. `--into` stays as optional advice. +- **Run the cheapest test that proves a change, and never test during the user's Play Mode.** `build-run-test.md` now carries a short rule for `unity test`: EditMode before PlayMode, `--filter`, `--affected` or `--rerun-failed` while iterating, the full suite on the final code before handing work back, and no re-run on code that has not changed. It says which report each of those runs reads and writes, so a narrow run never replaces the full-suite record. It also says `unity test` refuses a project that is open in an Editor. Closing the user's Editor to make room, or starting a test run while they are in Play Mode, is not the answer. `SKILL.md`'s test section points at it, so the flags no longer read as CI-only. +- **Editors built from source**: `SKILL.md` and `auth-license-cloud.md` no longer suggest a signature-check setting for an Editor built from source: the Editor identity helper signs in only Unity-signed Editors on macOS and Windows. +- **`projects` description**: `SKILL.md`'s command table quotes the new `unity projects` help line, "Create, open, upgrade, and link local Unity projects (Unity Cloud, GitHub/GitLab)", and says linking a folder to Unity Cloud is `projects link cloud`, not `cloud`. +- **Pipeline commands are named by group and name.** `SKILL.md`, `integration-advanced.md` and `playmode-verification-loop.md` run them as `unity command editor status`, `unity command editor.playmode start`, `unity command script eval` and so on, where they used the flat names (`editor_status`, `editor_play`, `eval`). `integration-advanced.md` explains the form, and that a package from before command groups is retried under its old names with a one-time warning to run `unity pipeline upgrade`. + +### Removed + +- **Paved mode**: the `UNITY_CLI_FTUE` row in `SKILL.md`'s environment variable table and the paved mode paragraph in `projects-templates.md` are gone, since the CLI no longer has the mode. Envelopes no longer carry `"paved"`, and `projects create` no longer writes `UserSettings/UnityCliPaved.json` or reports `pavedSettings`. + +## CLI `1.0.0-beta.13` (2026-10-07) + +Aligned to the CLI's `1.0.0-beta.13` release. Everything below was documented ahead of the release and ships in it. + +### Added + +- **`unity setup codex` and `unity setup grok`**: `integration-advanced.md` documents installing Unity's agent plugin into Codex and Grok Build, the commands each runs, `CODEX_HOME` / `GROK_HOME`, the not-found and install-failed error codes, and the null `reloadCommand`. `SECURITY.md` extends the plugin-install fence to both. +- **A Unity Cloud choice for `unity projects upgrade` into Unity 6.7 or later**: `projects-templates.md` documents that an unlinked project needs `--cloud`, `--cloud-project ` or `--no-cloud`, that the upgrade turns on build-profile diagnostics only when the project ends up linked, and the `data.diagnostics.buildProfiles` and `data.cloud.outcome` result fields. +- **`proxyRequestLogging` in `unity config`**: `config-hub.md` lists the key, the persisted third tier of `--log-proxy`'s precedence. + +### Changed + +- **`UNITY_PROJECT_PATH` for per-worktree agents**: `SKILL.md`'s one-Editor-per-worktree paragraph now tells agents to set `UNITY_PROJECT_PATH=` once for the session, and names the commands that don't read it (`recompile`, `close`, `mcp configure`). The environment-variable table lists every command that honors it, `unity mcp` included. `integration-advanced.md` adds the variable to the target resolver's order and shows `UNITY_PROJECT_PATH=... unity mcp`. +- **Auth broker and development apps**: `SKILL.md` and `auth-license-cloud.md` now say that a broker started for a development app is a separate broker with its own address and sign-in, that it refuses Unity-signed apps, and that Unity apps refuse it. +- `build-run-test.md`'s Build section now says `unity build --format json` writes only the result document to stdout, with the progress frames and the streamed Editor log on stderr, and that the streamed log masks the credentials the CLI passes the Editor (the `-accessToken` value, the Android keystore passwords) in `build`, `run` and `test`. +- **One Editor per worktree, and report a failed call.** `SKILL.md` now tells an agent working in several worktrees to make each one with `unity vcs git worktree add --into `, open the project inside it with `unity open`, and target that Editor by its full project path. It never drives an Editor whose project isn't its own checkout. Once Safe Mode, a sandbox and a slow import are ruled out, it also reports a timeout or an Editor lifecycle failure to the user instead of silently working around it. It offers `unity bug --area pipeline-package` and asks first. +- The bootstrap recipe's step 6 and the `unity status` reference now pass the project's full path to `unity status --until-ready --project-path`, and say how the flag matches. A path that exists matches that project only, so a sibling checkout such as `MyGame-lighting` never ends the wait for `MyGame`, and a bare name still matches any project path containing it. +- `build-run-test.md`'s Accelerator example ran `unity build` with `--target StandaloneLinux64` and no `--output-path`. A desktop target with neither `--execute-method` nor `--profile` is a built-in player build, which requires an output path, so the example exited 2. It now passes `--output-path ./Build/MyGame.x86_64`. +- **`open` never moves a project onto Unity 6.7 or later.** `projects-templates.md` documents that `unity open` and `unity projects open` exit `2` with `PROJECT_UPGRADE_REQUIRED` instead, and point at `unity projects upgrade`. +- **`unity cloud org create`** in `auth-license-cloud.md` takes the Unity Dashboard's company profile (`--first-name` and `--last-name`, or `--company-name`, plus `--country`) instead of the removed `--industry`. +- `playmode-verification-loop.md` now explains that the Run In Background Player Setting pauses the game while the Editor is unfocused, and tells agents to turn it on with the Pipeline package's `editor_set_run_in_background` command, which keeps the change out of builds. It also drops a Hub source path that public readers can't open. + ## CLI `1.0.0-beta.12` (2026-09-30) Aligned to the CLI's `1.0.0-beta.12` release. Everything below was documented ahead of the release and ships in it. @@ -66,7 +162,7 @@ Aligned to the CLI's `1.0.0-beta.10` release. Much of this release's surface was - **`ProjectSettings/UnityCliConfig.json` and `unity config resolve [project]`** — the committed per-project build/test defaults layer (nine keys), the global `config set build.target …` layer beneath it, the precedence order, and the command that names which layer a value came from — in `config-hub.md`, with a pointer from Build. - **`unity command --result-only`** — the envelope-free JSON result mode, in `integration-advanced.md`, alongside the readable rendering of `recompile` / `recompile_status` / `test_status` / `run_tests` results. - **`unity license status`'s `floatingServer` / `machineId` fields** — in `auth-license-cloud.md`. -- **`UNITY_NO_AUTH_BROKER`, `UNITY_PEER_AUTH_MODE` / `UNITY_PEER_AUTH_LINUX_ALLOWED_HASHES`, `UNITY_CLI_HOME`, `UNITY_NO_EDITOR_IDENTITY_SERVER`** — added to the environment-variables table in SKILL.md, with a short paragraph under Auth explaining the on-demand resident auth broker, the hardware-sealed token store, and the peer code-signature check. +- **`UNITY_NO_AUTH_BROKER`, `UNITY_CLI_HOME`, `UNITY_NO_EDITOR_IDENTITY_SERVER`** — added to the environment-variables table in SKILL.md, with a short paragraph under Auth explaining the on-demand resident auth broker, the hardware-sealed token store, and the peer code-signature check. ### Changed diff --git a/skills/unity-cli/SECURITY.md b/skills/unity-cli/SECURITY.md index 919a2c9..1088a63 100644 --- a/skills/unity-cli/SECURITY.md +++ b/skills/unity-cli/SECURITY.md @@ -10,16 +10,7 @@ These capabilities are accepted by design. Each is documented in full in the sec |---|---|---| | `SEC_POWER_CAP` | Local Editor control and C# evaluation | Runs entirely on the local machine, as the current user, against the user’s own Editor — no remote access and no privilege the user lacks at their own terminal. | | `SEC_INSTALL_PIPE` | Install one-liners piped to a shell | HTTPS to Unity’s first-party CDN only; the installer verifies a SHA-256 pin against a same-origin manifest before executing anything. | -| `SEC_AGENT_CONFIG_WRITE` | Writing agent skill files into AI clients’ configuration directories, turning an installed skill off or back on, and installing Unity’s own Claude Code plugin | Runs only on an explicit user command, is the command’s documented purpose, and is fenced by an ownership ledger — a copy this CLI did not write is never overwritten without `--force`, and the enable/disable verbs cannot act on one at all — plus symlink refusals and a warning before project-local installs from the home directory. The plugin install names one hardcoded first-party source, is performed by Claude Code’s own `claude` CLI rather than by writing files, and reinstalls nothing. | - -## Reading an automated scanner's verdict on this skill - -The directive above records the risks this skill's maintainers have reviewed and accepted, for the repository's own validator. A scanner run elsewhere generally will not consult it, and will read the **whole folder** rather than the change in front of it. Both facts matter when you are looking at a verdict: - -- A verdict describes this skill's standing capabilities — the ones documented below — not whatever the current change happens to touch. A documentation-only edit can still draw a verdict, because the capabilities it is reacting to were already here. -- Verdicts from an AI classifier are a judgement, not a rule, and the same unchanged folder can be scored differently on different runs. Treat a single verdict as a prompt to check, never as a result on its own. - -So before acting on one, compare it against what actually changed. If the change introduces no capability beyond those accepted below, the verdict is re-deriving something already reviewed and recorded here — note that and move on. If it does introduce one, it needs a real review and an entry in this file, because this skill is published and shipped inside the CLI. +| `SEC_AGENT_CONFIG_WRITE` | Writing agent skill files into AI clients’ configuration directories, turning an installed skill off or back on, and installing Unity’s own agent plugin into Claude Code, Codex, Grok Build, or Antigravity | Runs only on an explicit user command, is the command’s documented purpose, and is fenced by an ownership ledger — a copy this CLI did not write is never overwritten without `--force`, and the enable/disable verbs cannot act on one at all — plus symlink refusals and a warning before project-local installs from the home directory. The plugin install names one hardcoded first-party source, is performed by the agent’s own CLI (`claude`, `codex`, `grok`, or `agy`) rather than by writing files, and reinstalls nothing. | ## Accepted, by-design capabilities @@ -38,9 +29,13 @@ So before acting on one, compare it against what actually changed. If the change The capability exists because a skill costs context in every session of the client that discovered it, whether or not it is ever used, so a user who installed several needs a way to stop paying for the ones a given project does not need — without deleting files they would otherwise have to reinstall. -### Installing Unity’s Claude Code plugin +### Installing Unity’s agent plugin (Claude Code, Codex, Grok Build, Antigravity) + +`unity setup claude` installs a plugin into Claude Code, which automated scanners flag as an agent-persistence pattern. It is fenced as follows. It runs only when the user runs the command, and this skill tells an agent to run it only when the user asks for the plugin or agrees to install it, never on its own initiative. The only source it can name is Anthropic’s official Claude Code marketplace, `anthropics/claude-plugins-official`, which lists Unity’s plugin and is compiled into the CLI: no argument, file, or environment variable can point it anywhere else. Claude Code accepts that marketplace name only from a repository under `github.com/anthropics/` ([Claude Code plugin security](https://code.claude.com/docs/en/plugins/security#identify-anthropics-marketplaces-by-name)), so a look-alike marketplace can’t take its place. It writes no files itself; it runs Claude Code’s own `claude plugin marketplace add` and `claude plugin install`, the documented install path, with fixed arguments, so Claude Code applies its own install handling. `--dry-run` prints both commands without running them, and an existing install is reported and never reinstalled or changed. It does not also copy a skill or register an MCP server of its own. + +`unity setup codex` and `unity setup grok` install the same plugin into Codex and Grok Build under the same fence: they run only when the user runs them, and this skill tells an agent to run them only when the user asks for the plugin or agrees to install it. Their source is Unity’s own first-party marketplace, `Unity-Technologies/unity-agent-plugin`, compiled into the CLI, and nothing can point either command elsewhere. Each writes no files itself and runs only the agent's own documented install commands with fixed arguments: `codex plugin marketplace add` and `codex plugin add`, or `grok plugin install … --trust`. `--trust` is Grok Build's consent flag for a non-interactive install, and it is passed only because the user ran the command. `--dry-run` prints the commands without running them, and an existing, enabled install is reported and never reinstalled or changed. A Codex plugin the user disabled (`enabled = false` in Codex’s `config.toml`) is the one exception: `unity setup codex` treats it as not installed and runs the same two `codex` commands again, which re-enables it. That only happens when the user runs the command. -`unity setup claude` installs a plugin into Claude Code, which automated scanners flag as an agent-persistence pattern. It is fenced as follows. It runs only when the user runs the command, and this skill tells an agent to run it only when the user asks for the plugin or agrees to install it, never on its own initiative. The only source it can name is Unity’s own first-party marketplace, `Unity-Technologies/unity-agent-plugin`, compiled into the CLI: no argument, file, or environment variable can point it anywhere else. It writes no files itself; it runs Claude Code’s own `claude plugin marketplace add` and `claude plugin install`, the documented install path, with fixed arguments, so Claude Code applies its own install handling. `--dry-run` prints both commands without running them, and an existing install is reported and never reinstalled or changed. It does not also copy a skill or register an MCP server of its own. +`unity setup antigravity` installs the same plugin into Antigravity under the same fence. It runs only `agy plugin install` with Unity’s first-party repository, `Unity-Technologies/unity-agent-plugin`, as a fixed URL compiled into the CLI that nothing can point elsewhere, writes no files itself, prints the command under `--dry-run`, and reports an existing install without changing it. ### Local Editor control and C# evaluation diff --git a/skills/unity-cli/SKILL.md b/skills/unity-cli/SKILL.md index eefedb1..d2755de 100644 --- a/skills/unity-cli/SKILL.md +++ b/skills/unity-cli/SKILL.md @@ -12,6 +12,22 @@ specific version.** Newer releases regularly add commands and output fields these docs describe, so an outdated CLI fails in ways that read like the docs being wrong. Check with `unity self-update --check --format json`. +## Find a command: search, don't grep help + +**Never filter `unity --help` or a group's help with `grep`.** Help is long, and a grep over it drops the match or the context around it. Use the CLI's own search: + +```bash +unity commands --grep license # built-in commands, plugins and the connected Editor's Pipeline commands +unity command --query screenshot # the same search +``` + +`unity commands --grep ` and `unity command --query ` are one search. It covers the CLI's built-in commands, the plugin catalog (installed or not) and the Pipeline commands of the live connected Editor, best match first. Plain words each match in any order; regex characters make it a regular expression. Each row says how to run it (`invoke`): `unity projects link cloud`, `unity command create_gameobject`, `unity ` for an installed plugin, or `unity plugin install ` for one not yet installed. In `--format json` each row also carries `kind` (`command`, `plugin`, `pipeline`), `path`, `description` and `matchedOn` (`name`, `description` or `option`), so act on `invoke` rather than parsing the table. + +- Use `unity command --query ` to search a specific Editor (`--project-path `) or a Player (`--runtime `, e.g. `unity command --query screenshot --runtime MyGame`). +- `commands --grep` waits at most 3 seconds for the Editor; `command --query` waits for it. When the Editor was not searched, a last line says why (no Editor, no answer in time, several Editors) and what to run. Wait for the Editor first with `unity status --until-ready --project-path `: before it connects, no Pipeline row is not proof nothing exists. + +Fields and flags: [integration-advanced.md](references/integration-advanced.md#querying-the-command-list). + ## Drive a running Unity Editor (if one is open) **If a Unity Editor is open on this machine, this CLI can control it live** — create and modify GameObjects, edit scenes and assets, inspect the hierarchy, and run arbitrary C# — through the project's **Pipeline** package (`com.unity.pipeline`). This runs entirely on your local machine, in your own user account, against your own open Editor: it is not remote access and grants no privilege you don't already have at your own terminal. When an Editor is available, drive it instead of hand-editing scene or asset files. @@ -19,9 +35,9 @@ being wrong. Check with `unity self-update --check --format json`. ```bash unity status # confirm a connected Editor (look for state "ready") unity command # list the commands the Editor exposes -unity command editor_play # run one — e.g. enter Play mode -# Run arbitrary C# — e.g. add a GameObject named "Joe" — when the Editor exposes eval: -unity command eval --caller plugin --skill unity-cli 'new UnityEngine.GameObject("Joe");' +unity command editor.playmode start # run one — e.g. enter Play mode +# Run arbitrary C# — e.g. add a GameObject named "Joe" — when the Editor exposes script eval: +unity command script eval --caller plugin --skill unity-cli 'new UnityEngine.GameObject("Joe");' ``` **Pass `--caller plugin --skill ` on every `unity command` invocation.** `--skill` is the @@ -29,29 +45,42 @@ skill whose instructions produced the call, so a task skill that sends you here rather than `unity-cli`. The flags are inert to the command itself; they only label the invocation. > **Verifying a running game, not just editing a scene?** Entering Play mode is setup, not proof: -> an unfocused Editor can freeze at frame 1 while `unity status` still reports it as playing, and a -> screenshot can render a frozen frame that looks alive. The ordered recipe for playing, confirming -> the game is actually advancing, capturing the view, reading the console, and tuning it live with -> `eval` is in [playmode-verification-loop.md](references/playmode-verification-loop.md). +> an unfocused Editor or a locked desktop can freeze at frame 1 while `unity status` still reports it +> as playing, and a screenshot can render a frozen frame that looks alive, or a flat color. The +> ordered recipe for playing, confirming the game is actually advancing, capturing the view, reading +> the console, and tuning it live with `script eval` is in +> [playmode-verification-loop.md](references/playmode-verification-loop.md). ### More than one Editor open? Pass `--project-path` Every Editor-driving command takes `--project-path `. **Pass it whenever more than one Editor may be running** — without it the CLI targets the Editor whose project contains the current directory, so the target follows the shell's cwd: ```bash -unity command editor_play --project-path /path/to/MyProject +unity command editor.playmode start --project-path /path/to/MyProject ``` -A `unity status` instance's `project` field is what `--project-path` takes. For `unity command`/`list`/`job`/`mcp`, matching no running project fails with `AMBIGUOUS_EDITOR` and lists the candidates. [Details](references/integration-advanced.md#targeting-one-of-several-running-editors). +To pin one Editor when several share a project (worktrees, duplicate projects, Multiplayer Play Mode players), `unity command`, `list`, `job` and `mcp` also take `--port `, `--pid ` or `--instance ` (the MPPM instance name or `vpId`); a pin that matches no running Editor exits 12 with `EDITOR_NOT_RUNNING`. A `unity status` instance's `project` field is what `--project-path` takes. For `unity command`/`list`/`job`/`mcp`, matching no running project fails with `AMBIGUOUS_EDITOR` and lists the candidates. [Details](references/integration-advanced.md#targeting-one-of-several-running-editors). + +**Working in several worktrees or agent sessions? One Editor per worktree.** Make each worktree with `unity vcs git worktree add --into `, so you choose where it lands. `--into` is optional: `--format json` reports the real worktree and project paths, and only the human output masks the account name in a home-directory path. The Unity project is `` when it sits at the repository root, and `/` when it doesn't. Open that project's own Editor with `unity open `. Then pin every call that reaches the Editor to that project: pass `--project-path `, or prefix the call with the variable, as in `UNITY_PROJECT_PATH= unity command editor status`. Most agent tools run each command in a fresh shell, so an `export` or a bare assignment in one call never reaches the next one. `unity open`, `status`, `command`, `list`, `job` and `mcp` read the variable when you pass no path. `recompile`, `close` and `mcp configure` don't read it: pass the path to those. Without the variable, a bare `unity open` opens the project of the current directory, which is still your source checkout. Target that Editor by the same full path every time, including `unity status --until-ready --project-path `. A path that exists matches only that project, while a bare name also matches a sibling whose path contains it. Never drive an Editor whose project is not your own checkout (or one of its Multiplayer Play Mode clones). An Editor open on a sibling worktree answers every command just like yours. A command sent there edits that checkout's scenes, and its Play mode, recompiles and imports run against that checkout's code. If your own Editor isn't open, open it. If it has gone away, tell the user rather than borrowing the one that is running. Requires the project's `com.unity.pipeline` package (Unity 6.0+) — add it once with `unity pipeline install`. Full details — launching a headless Editor to drive, `unity list` tool discovery, and authoring custom `[CliCommand]` tools — are in [integration-advanced.md](references/integration-advanced.md). The package also ships a deeper `unity-pipeline` agent skill, invisible to clients inside `Library/PackageCache` — in a project with the package, run `unity skill install --local` once to mirror it beside this skill. -> **Can't connect / commands time out? Check for Safe Mode first.** When a project has C# compile errors, the Editor boots into **Safe Mode**, where the Pipeline package doesn't load — so `unity command`, `unity status`, `unity list`, and `unity recompile` can't connect at all. Note what that means for `unity recompile` specifically: it reports errors you introduce into an Editor that is **already running**, but an Editor that *started* with broken code never loads the package, so there is nothing to ask and it exits `7` rather than reporting the errors. Don't fall back to blind file-editing: run `unity pipeline list` to confirm, then fix the compile errors and restart Unity. Full recovery loop in [integration-advanced.md → Recovering from Safe Mode](references/integration-advanced.md#recovering-from-safe-mode-connection-fails-because-of-compile-errors). +> **Can't connect / commands time out? Check for Safe Mode first.** When a project has C# compile errors, the Editor boots into **Safe Mode**, where the Pipeline package doesn't load, so `unity command`, `unity list`, and `unity recompile` can't connect at all (`unity status` doesn't connect for this: it reads the Editor's log and reports Safe Mode). Note what that means for `unity recompile` specifically: it reports errors you introduce into an Editor that is **already running**, but an Editor that *started* with broken code never loads the package, so there is nothing to ask and it exits `7` rather than reporting the errors. Don't fall back to blind file-editing: run `unity status` (it reports Safe Mode and lists the compile errors, `safeMode.compileErrors` under `--format json`), then fix the errors: the Editor leaves Safe Mode once they compile. Full recovery loop in [integration-advanced.md → Recovering from Safe Mode](references/integration-advanced.md#recovering-from-safe-mode-connection-fails-because-of-compile-errors). > **Running as a sandboxed coding agent and `unity status` reports no instances?** A restrictive sandbox can hide an Editor that is genuinely running from this CLI's view of it — don't treat that alone as proof the Editor is down. Full detail in [integration-advanced.md → Sandboxed agent tooling can hide a running Editor](references/integration-advanced.md#sandboxed-agent-tooling-can-hide-a-running-editor). +> **A call timed out, or the Editor's lifecycle failed? Tell the user, don't silently work around it.** First rule out [Safe Mode](references/integration-advanced.md#recovering-from-safe-mode-connection-fails-because-of-compile-errors), a sandbox, and an Editor that is still importing or loading. Read `errors[0].code` under `--format json`, and give a slow import a longer `--timeout`. If it is still one of these, report it: +> +> - a command that times out +> - an Editor that never reaches `ready` under `unity status --until-ready` (exit 6) +> - a connection that drops mid-command +> - an Editor that quits or restarts under you +> - a `unity open` that never produces an Editor +> +> Don't quietly retry in a loop, switch to another Editor, or fall back to hand-editing scene and asset files. Say what you ran, what failed and what you already tried. Then offer to file it with `unity bug --area pipeline-package` (`--area editor` when the Editor itself quit or never started), and ask first: `unity bug` sends logs to Unity, so never run it on your own initiative. + ## Install the CLI (if not already installed) First check if the CLI is available: @@ -83,11 +112,12 @@ These work on every command: | Flag | Description | |---|---| | `--format ` | Output format: `human` (default), `json`, `tsv`, `ndjson`, `github`. Also via `UNITY_FORMAT` env var. | -| `--json` | Global shorthand for `--format json`, accepted on every command (e.g. `unity status --json`, `unity doctor --json`). `--format` takes precedence when both are supplied. | +| `--json` | Global shorthand for `--format json`, accepted on every command (e.g. `unity status --json`, `unity doctor --json`). Passing it with a different `--format` (e.g. `--json --format tsv`) is a usage error (exit 2); `--json` overrides `UNITY_FORMAT`. | | `--no-banner` | Suppress the branded header — use in scripts | +| `--no-header` | Leave the header row out of `tsv` tables. The header row is the column names, which are the same keys the `json` rows use (`version`, `upgradeTo`, ...). An empty `tsv` result is the header row alone, so with `--no-header` it is no output at all. | | `--no-pager` | Turn off paging. Governs both pagers: the external one over the long listings (`unity command`, `releases`, `editors`, `changelog`, `logs`) and the interactive one in `unity projects list`. Also via `UNITY_NO_PAGER` (presence-based — any value, including `0`, disables it). | | `--non-interactive` | Disable all interactive prompts — use in CI | -| `--quiet` | Suppress non-essential output | +| `--quiet` | Hide notices, warnings and progress in human and tsv output. Errors and the exit code stay. json output never changes, and ndjson output does not either, except that a few listings still drop their rows (see [Notes](#notes)). | | `--verbose` | Print full error details (stack trace + cause chain) on failure. Also via `UNITY_VERBOSE`. | | `--proxy ` | HTTP/HTTPS/SOCKS/PAC proxy URL for this invocation. Also via `UNITY_PROXY`. Takes precedence over standard `HTTPS_PROXY`/`HTTP_PROXY`/`ALL_PROXY` env vars and the persisted `proxy.json` setting. | | `--proxy-disable` | Disable proxy for this invocation, ignoring all sources (env vars, persisted config, system settings). | @@ -96,7 +126,7 @@ These work on every command: | `--color ` | Control colored output for this invocation, overriding `NO_COLOR`/`FORCE_COLOR` and TTY auto-detection. Governs every ANSI-emitting surface (help, tables, spinners, errors), not just `human` output. | | `--no-color` | Shorthand for `--color never`. Whichever of `--color`/`--no-color` appears last on the line wins. | -**Always use `--format json` when you need to parse output programmatically.** +**Always use `--format json` when you need to parse output programmatically.** If you read `tsv` instead, select columns by header name, not position: a command's `tsv` columns keep their names and order, and a flag that adds information (such as `editors --long`) only appends columns at the end. Failures in `tsv` go to stderr, so stdout holds only data. `--accelerator ` and `--no-accelerator` are **not** root globals — they are accepted only on `run`, `test` and `build`, and only after the command name. See [build-run-test.md](references/build-run-test.md). @@ -104,7 +134,7 @@ These work on every command: **Not every machine format bypasses that one.** Only `json` and `ndjson` get their own non-interactive rendering; on a terminal, `--format tsv` and `--format github` fall through to the human table and page like `human` does — so `--format tsv` on a TTY yields neither TSV nor unpaged output. Redirect stdout (the usual case for a machine format) or pass `--no-pager`. Note this is the **opposite** of the external pager below, which is `human`-only: the two mechanisms differ here, and `projects list` is the surprising one. -**The long listings page through an external pager, like `git log`.** `unity command` (the bare listing), `unity releases`, `unity editors`, `unity changelog`, and `unity logs` pipe human output through `less -RFX` on a terminal — colors kept, no screen clear, and `-F` quits by itself when the output already fits one screen, so short listings show no pager UI. `$UNITY_PAGER` then `$PAGER` override the choice and run through a shell, so `PAGER="less -S"` works; a blank value is ignored rather than treated as an opt-out. Quitting with `q` exits cleanly with the command's own exit code. Unlike `projects list`'s pager this one is **`human`-only**, and it never engages for redirected stdout, any machine format (`json`, `tsv`, `ndjson`, `github`), `--quiet`, `TERM=dumb`, the streaming modes (`editors --watch`, `logs --follow`), a named `unity command `, or inside `unity shell`. A broken pager costs the paging, not the output: a `$PAGER` naming something that is not there is resolved before anything spawns, and one that spawns and then dies has its output reprinted to the terminal, decided from the pager's exit status (a clean exit is a normal `q` and discards; a failure status reprints). The exception is a pager that exits *successfully* without reading — `PAGER=true`, or anything that lingers and then exits 0 — which nothing distinguishes from a `q`, and which `git` loses too. A pager that starts and merely *waits* is not treated as broken, so the CLI waits with it. +**The long listings page through an external pager, like `git log`.** `unity command` (the bare listing), `unity releases`, `unity editors`, `unity changelog`, and `unity logs` pipe human output through `less -RFX` on a terminal; `-F` quits by itself when the output fits one screen. `$UNITY_PAGER` then `$PAGER` override the choice and run through a shell, so `PAGER="less -S"` works; a blank value is ignored rather than treated as an opt-out. Unlike `projects list`'s pager this one is **`human`-only**, and it never engages for redirected stdout, any machine format (`json`, `tsv`, `ndjson`, `github`), `--quiet`, `TERM=dumb`, the streaming modes (`editors --watch`, `logs --follow`), a named `unity command `, or inside `unity shell`. A broken pager costs the paging, not the output: a `$PAGER` naming something that is not there is resolved before anything spawns, and one that spawns and then dies has its output reprinted to the terminal, decided from the pager's exit status (a clean exit is a normal `q` and discards; a failure status reprints). A pager that exits 0 without reading (`PAGER=true`) is indistinguishable from `q`, so its output is lost. One that merely *waits* is not broken; the CLI waits with it. A branded Unity header (logo, wordmark, CLI version) renders on the landing surfaces — bare `unity`, `unity --help` / `-h`, `unity help`, and above the first-run consent prompt. It's shown only on a TTY, prints at most once, and degrades to compact, uncolored text on narrow terminals, without Unicode, or under `NO_COLOR`. Piped output is unaffected. Use `--no-banner` to suppress it in scripts. Bare `unity` prints usage and exits 0. @@ -117,7 +147,7 @@ All CLI env vars use the `UNITY_` prefix. A CLI flag always overrides the corres | `UNITY_FORMAT` | `--format` | Output format (`human`, `json`, `tsv`, `ndjson`, `github`). `HUB_FORMAT` is a deprecated alias. | | `UNITY_EDITOR_VERSION` | `--editor-version` | Editor version (e.g. `2023.3.0f1`, `latest`, `lts`). | | `UNITY_ARCHITECTURE` | `--architecture` | Chip architecture (`x86_64`, `arm64`). | -| `UNITY_PROJECT_PATH` | path argument | Project path — used by `open`, and also honored by `status` and the cloud commands. | +| `UNITY_PROJECT_PATH` | path argument | Project path when none is passed. Honored by `open` (and `projects open`), the Editor-connecting commands `status`, `command`, `list`, `job` and `mcp`, plus `pipeline install`/`upgrade` and the `collaboration` commands. Not read by `recompile`, `close` (and `projects close`), or `mcp configure`. | | `UNITY_QUIET` | `--quiet` | Suppress non-essential output. | | `UNITY_VERBOSE` | `--verbose` | Show full error details on failure. | | `UNITY_NON_INTERACTIVE` | `--non-interactive` | Disable interactive prompts. | @@ -129,11 +159,13 @@ All CLI env vars use the `UNITY_` prefix. A CLI flag always overrides the corres | `UNITY_RUN_TIMEOUT` | `--timeout` | Timeout for `unity run` in seconds. | | `UNITY_TEST_TIMEOUT` | `--timeout` | Timeout for `unity test` in seconds. | | `UNITY_CLOUD_ORG` | `--cloud-org` | Active Unity Cloud organization id or name for a single call. | -| `UNITY_CLOUD_PROJECT` | `--cloud-project` | Cloud project ID; used by Cloud Build inventory. Pipeline Automation inventory is organization-scoped. | +| `UNITY_CLOUD_PROJECT` | `--cloud-project` | Cloud project ID or name (a service account needs the ID); used by Cloud Build inventory. Pipeline Automation inventory is organization-scoped. | +| `UNITY_NO_CLOUD` | `--no-cloud` | Decline the Unity Cloud link on `projects create` and `projects upgrade`. Presence-based: any value counts, including `0`. On `projects create` and `projects upgrade`, an explicit `--cloud`, `--cloud-project`, `--cloud-org` or `--coppa` wins over it. `projects create --vcs uvcs` is refused with it set unless one of those flags overrides it. | | `UNITY_SERVICE_ACCOUNT_ID` | — | Service account client ID for non-interactive (CI) auth. | | `UNITY_SERVICE_ACCOUNT_SECRET` | — | Service account client secret for non-interactive (CI) auth. | | `UNITY_PROXY` | `--proxy` | HTTP/HTTPS/SOCKS/PAC proxy URL. Takes precedence over `HTTPS_PROXY`/`HTTP_PROXY`/`ALL_PROXY` and the persisted `proxy.json` setting. | | `UNITY_NO_UPDATE_CHECK` | — | Disable the background "update available" check (see `unity config update-check`). | +| `UNITY_NO_AGENT_HINTS` | — | Stop suggesting Unity's agent plugin or skill when an AI agent runs `unity`: Claude Code's install prompt, the `unity setup codex` / `unity setup grok` tip, and the `unity skill install` tip (an `AGENT_PLUGIN_AVAILABLE` entry in the envelope's `notifications` under `--format json`). Presence-based: any value counts. A tip asks you to get the user's approval before running its command. | | `UNITY_NO_CONSENT_PROMPT` | — | Suppress the one-time first-run analytics consent prompt *without* recording a choice — for wrapper scripts on an interactive terminal that must never absorb the prompt. Analytics stay off until you run `unity analytics opt-in`. Unlike `UNITY_NON_INTERACTIVE`, it changes nothing else about command behavior. | | `UNITY_NO_CRASH_REPORT` | — | Disable anonymous crash/error reporting (Sentry) entirely. | | `UNITY_LOG_PROXY` | `--log-proxy` | Log one redacted entry per outbound request to `proxy-request.json`. Truthy values: `1`, `true`. | @@ -141,9 +173,7 @@ All CLI env vars use the `UNITY_` prefix. A CLI flag always overrides the corres | `UNITY_NO_ELEVATE` | `--no-elevate` | Windows: skip the elevated (UAC) install helper for `install` / `install-modules`, so the install service runs unelevated. The Editor's NSIS installer still asks for elevation on demand if Windows requires it for your account — an administrator token always does; a standard user never does. | | `UNITY_INSTALL_RETRIES` | `--retries` (`install-modules` only) | Number of times `install` and `install-modules` retry an editor or module download whose transfer or validation fails. `0` disables retries; `unity install` has no `--retries` flag, so set the variable there. | | `UNITY_NO_AUTH_BROKER` | — | Skip the resident auth broker and read credentials directly from the OS keyring. By default every command that needs a token goes through a broker that starts on demand and exits after two idle minutes (see [auth-license-cloud.md](references/auth-license-cloud.md)). | -| `UNITY_PEER_AUTH_MODE` | — | How the auth broker and the Editor identity helper verify a connecting process’s code signature. `enforce` is the default on macOS and Windows: an unsigned or non-Unity-signed peer is refused. `identify-only` logs without refusing — use it for an Editor you built from source. Linux logs only unless set to `enforce` together with `UNITY_PEER_AUTH_LINUX_ALLOWED_HASHES` (comma-separated SHA-256 hashes of trusted executables). | | `UNITY_CLI_HOME` | — | Install root for the install script and `unity self-install`, on every platform including Windows: the binary lands in `/bin` instead of the default location. | -| `UNITY_CLI_FTUE` | — | Agent first-session ("paved") mode. Any value except empty or `0` turns it on. Every `--format json` envelope and ndjson `result` frame then carries a top-level `"paved": true`, and `projects create` saves the choice to the new project's `UserSettings/UnityCliPaved.json`, so later commands run in that project are paved with no variable set. Nothing else changes. See [projects-templates.md](references/projects-templates.md). | | `UNITY_NO_EDITOR_IDENTITY_SERVER` | — | Disable the background identity helper that `unity open` starts to answer the Editor’s sign-in lookups when no Hub is running (see [projects-templates.md](references/projects-templates.md)). Presence-based. | | `UNITY_EXPERIMENTAL_FEX_EMU` | none | Linux arm64 only, experimental and unsupported: run the x86_64 Editor under FEX-Emu. `1`, `true` or `yes` turns it on, and it takes effect only when FEX-Emu and an x86_64 RootFS are installed (`unity doctor` checks). C# script compilation is known to crash under FEX-Emu. See [editors-install.md](references/editors-install.md). | @@ -153,34 +183,42 @@ All CLI env vars use the `UNITY_` prefix. A CLI flag always overrides the corres Append `-h` or `--help` to any command or subcommand, at any level: `unity --help`, `unity projects create --help`. -**Not sure which command does something? Search before you guess a name.** `unity commands --grep ` matches command names, descriptions, and options, plus the plugin catalog, installed or not: - -```bash -unity commands --grep license # one match per line -unity commands --grep 'build|test' --format json -``` - -Each result says whether it is a `command` or a `plugin`. A plugin that isn't installed also names its install command (`unity plugin install `). Matching is case-insensitive, and the pattern is a regular expression evaluated with a timeout, so a plain keyword works as-is. See [integration-advanced.md](references/integration-advanced.md) for the output fields. +Not sure which command? [Search, don't grep help](#find-a-command-search-dont-grep-help). `unity --help` and the unknown-command error end with that pointer too. Each search ends by naming the source it skipped and the command that searches it (`data.hint` in JSON): follow it before concluding nothing exists. ## Exit codes -| Code | Meaning | -|---|---| -| 0 | Success | -| 1 | General error | -| 2 | Bad arguments | -| 3 | Authentication failure | -| 4 | Precondition not met (e.g. no license active, floating server not configured) | -| 6 | Command-specific failure | -| 7 | Network or transient service failure for cloud automation inventory (see its reference for exact mappings). | -| 8 | `unity test` only — the tests ran and one or more **failed**. Every other way a test run fails (compile error, unavailable license, editor crash, `--timeout`) keeps `6`, so CI can retry an infrastructure failure and never retry a failing test. | -| 9 | `unity install`, `unity install-modules` and `unity projects require` only: `--no-wait` or `--wait-timeout` ran out while the Hub or another CLI held the shared install lock (`INSTALL_LOCK_BUSY`). Retry later. Earlier items of the same run may already be installed; see `data.completedUids`. | -| 130 | Interrupted — Ctrl+C / SIGINT (128 + 2) | -| 143 | Terminated by SIGTERM (128 + 15) — e.g. `kill` or a CI/runner timeout. Emitted by long-running commands that install a signal handler to clean up first (currently `unity build`, which scrubs the temporary Android keystore). | - -The `cloud` and `auth` commands map an authentication failure (expired/missing session, rejected sign-in) to `3`, and any other operational failure (network, server error) to `6` — so scripts can reliably tell "sign in again" apart from a genuine command failure. - ---- +Branch on the exit code, then on `errors[0].code` in the json or ndjson envelope. **Retry** says +whether running the same command again, unchanged, can succeed. `unity commands --json` carries +this table as `exitCodes`, with the error codes that pick each exit. `6` covers every failure no +more specific code names; a missing project, an Editor that isn't running, an unknown Editor +command and a timeout have their own codes (`11` to `14`). + + +| Code | Name | Retry | Meaning | +|---|---|---|---| +| 0 | `SUCCESS` | no | The command succeeded. | +| 1 | `ERROR` | no | An unexpected or unsupported state. | +| 2 | `BAD_ARGS` | no | A usage error: an unknown command or option, or a bad value. | +| 3 | `AUTH_ERROR` | no | Not signed in, the session expired, or permission was denied. | +| 4 | `NEEDS_CONFIG` | no | Setup is missing: a required tool, setting or link. | +| 6 | `COMMAND_FAILED` | no | The command failed for a reason no more specific code names. | +| 7 | `NETWORK_UNREACHABLE` | yes | A required service could not be reached. | +| 8 | `TESTS_FAILED` | no | The tests ran, and some failed. | +| 9 | `INSTALL_LOCKED` | yes | Another process holds the install lock. | +| 10 | `READY_TIMEOUT` | yes | The Editor did not become ready in time. | +| 11 | `NOT_FOUND` | no | What the command names does not exist: a project, a release, a module. | +| 12 | `EDITOR_UNREACHABLE` | yes | No running Editor answered: none is running, none matches, or it refused the connection. | +| 13 | `REMOTE_COMMAND_FAILED` | no | The Editor answered, but it has no command by that name. | +| 14 | `TIMEOUT` | yes | An operation ran out of time: an Editor request, an HTTP request, a batch run. | +| 130 | `INTERRUPTED` | yes | Interrupted by Ctrl-C (SIGINT). | +| 141 | `OUTPUT_CLOSED` | no | The reader of stdout closed the pipe. | +| 143 | `TERMINATED` | yes | Terminated by SIGTERM. | + + +`8` is `unity test` only: the tests ran and some failed. A run that never completed exits `6` +and a timed-out one `14`, so CI can retry those and never blindly re-run a failing test. `9`: +earlier items of the same install may already be in place (`data.completedUids`). `10` is +`unity open --until-ready` only; the Editor is still running. ## Commands @@ -193,12 +231,12 @@ flags, environment variables, and exit codes above apply throughout. Every comma |---|---| | `auth` (login / logout / status / list / switch / default / consumers / revoke), `license` (activate / return / server), `cloud` (org / project) | [auth-license-cloud.md](references/auth-license-cloud.md) | | `pipeline cloud-build` (targets incl. groups / builds / project / tooling), `pipeline automation` (apps / pipelines / jobs / automations / bots / profiles / templates, plus `apps versions`, `pipelines versions`, `jobs stats`) | [cloud-automation.md](references/cloud-automation.md) | -| `editors` (list / running / add / default / path / install-path / info / upgrade / prune / verify / module), `install`, `uninstall`, `modules`, `install-modules` | [editors-install.md](references/editors-install.md) | -| `projects` (list / create / new / clone / open / link / require / upgrade / export / import / pin / size / clean / exec), `open`, `close`, `releases`, `templates` (list / info / create / pack / delete), `assets` (`inspect` / `export`) | [projects-templates.md](references/projects-templates.md) | +| `editors` (list / running / add / default / path / install-path / info / upgrade / prune / verify / module), `install`, `uninstall`, `modules`, `install-modules`, `terms` (accept / status) | [editors-install.md](references/editors-install.md) | +| `projects` (list / create / new / clone / open / link / require / upgrade / export / import / pin / size / clean / exec; its help reads "Create, open, upgrade, and link local Unity projects (Unity Cloud, GitHub/GitLab)", so linking a folder to Unity Cloud is `projects link cloud`, not `cloud`), `open`, `close`, `releases`, `templates` (list / info / create / pack / delete), `assets` (`inspect` / `export`) | [projects-templates.md](references/projects-templates.md) | | `config` (proxy / update-check / accelerator / get / set / list / unset / resolve), `context` (save / use / list / current / delete), `hub install` | [config-hub.md](references/config-hub.md) | | `run`, `test`, `build` (+ `build run`), `recompile`, `watch` (`test`) | [build-run-test.md](references/build-run-test.md) | -| `logs`, `doctor`, `env`, `version`, `cache`, `ci init`, `analytics`, `changelog`, `docs`, `language`, `completion`, `bug`, `self-update`, `self-uninstall`, `diagnose proxy`, `diagnose accelerator`, `diagnose update` | [diagnostics-maintenance.md](references/diagnostics-maintenance.md) | -| `mcp` (+ `configure`), `setup claude`, `skill` (install / refresh / show), `plugin` (install / remove / upgrade / list / changelog), local `pipeline` (install / upgrade / list / list-versions), `command` / `commands` / `status` / `list`, `job` (status / wait / cancel), `shell` | [integration-advanced.md](references/integration-advanced.md) | +| `logs`, `doctor`, `env`, `version`, `cache`, `ci init`, `analytics`, `changelog`, `docs` (+ `toc`), `language`, `completion`, `bug`, `self-update`, `self-uninstall`, `diagnose proxy`, `diagnose accelerator`, `diagnose update` | [diagnostics-maintenance.md](references/diagnostics-maintenance.md). To find, cite and version-match Unity docs: [docs-workflow.md](references/docs-workflow.md) | +| `mcp` (+ `configure`), `setup claude` / `codex` / `grok` / `antigravity`, `skill` (install / refresh / show), `plugin` (install / remove / upgrade / list / changelog), local `pipeline` (install / upgrade / list / list-versions), `command` / `commands` / `status` / `list`, `job` (status / wait / cancel), answering an open modal dialog (`command editor.dialog list` / `answer` / `dismiss`), `shell` | [integration-advanced.md](references/integration-advanced.md) | | `vcs` — `setup` / `status` / `sync` / `switch` / `doctor` / `providers` / `merge-setup` / `conflicts` / `explain` / `resolve` / `diff` / `blame` / `summarize` / `affected` / `hooks`, `vcs git` (`migrate-lfs` / `worktree`), `vcs uvcs` (`locks` / `changesets` / `review`) | [version-control.md](references/version-control.md) | | `collaboration` (alias `collab`) — `annotations` / `attachments` / `thumbnail` / `reactions` / `read` / `subscribe` / `jira` | [collaboration.md](references/collaboration.md) | @@ -230,8 +268,8 @@ to follow-up cloud-automation work too. unity status # is an Editor connected? (look for state "ready") unity command # discover the scene/GameObject commands THIS Editor exposes # then drive it with the commands it lists — for example, if your Editor exposes them: -unity command create_gameobject # act on the live, active scene -unity command save_scene # persist the active scene +unity command gameobject create # act on the live, active scene +unity command scene save # persist the active scene ``` Command names are defined by the Editor, so run `unity command` (or `unity list`) to see the exact set — don't assume a name. @@ -243,7 +281,7 @@ Command names are defined by the Editor, so run `unity command` (or `unity list` **Rule out two false negatives before concluding no Editor is reachable — both look identical to a genuinely closed Editor, and both are easy to get wrong under time pressure:** -- **Safe Mode.** If an Editor *is* running for this project but `unity status` / `unity command` won't connect, it may be stuck in **Safe Mode** from a compile error rather than genuinely absent. Run `unity pipeline list` — if it reports Safe Mode, editing the C# source to fix the compile errors (and then restarting Unity) *is* the correct move, not a fallback. See [integration-advanced.md → Recovering from Safe Mode](references/integration-advanced.md#recovering-from-safe-mode-connection-fails-because-of-compile-errors). +- **Safe Mode.** If an Editor *is* running for this project but `unity command` won't connect, it may be stuck in **Safe Mode** from a compile error rather than genuinely absent. Run `unity status`: if it reports Safe Mode (`STATUS_SAFE_MODE`, with the compile errors), editing the C# source to fix the compile errors *is* the correct move, not a fallback. See [integration-advanced.md → Recovering from Safe Mode](references/integration-advanced.md#recovering-from-safe-mode-connection-fails-because-of-compile-errors). - **A sandboxed agent shell.** If your own shell commands run inside a restrictive sandbox — the normal case for a coding agent like this one — the sandbox can hide a genuinely running Editor from `unity status` the same way. This applies to **every** scene/GameObject/prefab/asset task that reaches this preflight, not only ones that obviously need a live Editor: a task you could otherwise finish without any CLI involvement (e.g. generating an asset through ordinary Editor APIs) can still get funneled into "no Editor" here and derailed. Don't treat "no instances" as proof the Editor is down, and don't quietly improvise a third path — like driving a separate headless Editor process to approximate what a live connection would have done — as a substitute for a disclosed file edit. Say plainly that your sandbox may be blocking your view of a real Editor, and ask whether one is actually open before falling back. Full detail: [integration-advanced.md → Sandboxed agent tooling can hide a running Editor](references/integration-advanced.md#sandboxed-agent-tooling-can-hide-a-running-editor). Only fall back to editing files directly once you've ruled out both of the above — and say so explicitly ("no live Editor detected, editing the file directly"). @@ -349,20 +387,21 @@ unity projects create "MyGame" --path ~/UnityProjects \ --editor-version lts --template com.unity.template.urp-blank # 5. Add the Pipeline package BEFORE the first open. `unity command`, `unity status` and -# `unity command eval` all need it, and templates don't include it. The Editor reads +# `unity command script eval` all need it, and templates don't include it. The Editor reads # Packages/manifest.json when it loads the project, so installing first means the package # is live from the first open. unity pipeline install --project-path ~/UnityProjects/MyGame -# 6. Open the project and wait until its Editor is ready to take commands. Run from inside the -# project so the CLI targets its Editor (--project-path here is a substring filter, not a path). +# 6. Open the project and wait until its Editor is ready to take commands. Pass the project's +# full path: a path that exists matches that project's Editor only, so an Editor open on a +# sibling checkout such as ~/UnityProjects/MyGame-lighting can't end the wait. cd ~/UnityProjects/MyGame unity open . -unity status --until-ready --project-path MyGame --format json +unity status --until-ready --project-path ~/UnityProjects/MyGame --format json ``` **Then build the scene in the live Editor, not in batch mode.** Create GameObjects, wire -components, and set asset references with `unity command eval` (or the Editor's own `unity command` +components, and set asset references with `unity command script eval` (or the Editor's own `unity command` tools) against the running Editor, and read the result back the same way. Each step can be checked before the next one. A script run through `unity run -- -executeMethod` is the **fallback** for when no Editor can stay open (CI, a headless build box). It can't see what it @@ -372,7 +411,7 @@ calling the work done. How to get an Editor to drive: [integration-advanced.md → Getting an Editor to drive](references/integration-advanced.md#getting-an-editor-to-drive). **Source control — let the user choose.** The CLI publishes the new project to a fresh remote in -one step for any provider. **Always pass tokens on stdin** (`--git-token-stdin`) so secrets never +one step for any provider. **Always pass tokens on stdin** (`--git-token-from-stdin`) so secrets never land in shell history or the process list. Pick based on the project — don't default to one: - **Git — GitHub / GitLab** (`--vcs github` / `--vcs gitlab`). Ubiquitous. For asset-heavy games @@ -388,7 +427,7 @@ land in shell history or the process list. Pick based on the project — don't d unity projects create "MyGame" --path ~/UnityProjects \ --editor-version lts --template com.unity.template.urp-blank \ --vcs github --git-namespace my-org --git-repo my-game \ - --git-visibility private --git-default-branch main --git-token-stdin --git-lfs + --git-visibility private --git-default-branch main --git-token-from-stdin --git-lfs # Unity Version Control (UVCS) — handles binaries natively, so no LFS: unity projects create "MyGame" --path ~/UnityProjects \ @@ -396,8 +435,8 @@ unity projects create "MyGame" --path ~/UnityProjects \ --vcs uvcs --git-namespace my-org --git-repo my-game --vcs-region ``` -Feed the token to `--git-token-stdin` from a secret store, never a literal — e.g. -`… --git-token-stdin <<<"$GIT_TOKEN"` where `$GIT_TOKEN` comes from your CI/secret manager +Feed the token to `--git-token-from-stdin` from a secret store, never a literal — e.g. +`… --git-token-from-stdin <<<"$GIT_TOKEN"` where `$GIT_TOKEN` comes from your CI/secret manager (UVCS uses your Unity sign-in, so no token is needed). **Working with a UVCS workspace day to day: a few wrapped reads, everything else straight through @@ -438,7 +477,7 @@ per organization, that is `git config --global credential.useHttpPath true` plus helper such as [Git Credential Manager](https://github.com/git-ecosystem/git-credential-manager). The CLI passes the full repo URL so the helper can discriminate, but it never installs or reconfigures a helper. `UNITY_GITHUB_TOKEN` / `UNITY_GITLAB_TOKEN` are one token per provider, so a -CI job spanning several orgs should pass `--git-token-stdin` per invocation instead. See +CI job spanning several orgs should pass `--git-token-from-stdin` per invocation instead. See [references/projects-templates.md](references/projects-templates.md) for the full source-control flag set. For a purely local Git repository instead, initialize git with a Unity-appropriate ignore so the multi-GB `Library/` and other generated folders are never committed: @@ -461,9 +500,9 @@ git ls-files | grep -c '^Library/' # must print 0 ``` **What the CLI does and doesn't cover.** The CLI handles editor, project, and source control. -It does **not** manage UPM (Unity Package Manager) packages — to add packages beyond the -template headlessly, use the **`unity-package-management`** skill (C# PackageManager Client -API). For monetization/backend, hand off to the dedicated skills: `implement-in-app-purchases` +It has no top-level package command. To add UPM (Unity Package Manager) packages beyond the +template, use the **`unity-package-management`** skill: it runs the Pipeline `package add` +command through `unity command`, with a headless C# script as the fallback. For monetization/backend, hand off to the dedicated skills: `implement-in-app-purchases` (IAP), `levelplay-unity-integration` (ads), or `build-live-game` (accounts, cloud save, economy, remote config, leaderboards). Once steps 5 and 6 above are done, work in the open Editor. @@ -507,11 +546,19 @@ unity open /path/to/MyProject ### CI: activate a license, then build ```bash -# 1. Sign in non-interactively with a service account -unity auth login --client-id "$UNITY_SERVICE_ACCOUNT_ID" --secret-from-stdin <<<"$UNITY_SERVICE_ACCOUNT_SECRET" - -# 2. Activate the entitlement license (or use --serial / --floating) -unity license activate +# 1. Activate a license without signing in; a service account cannot activate one. +# A perpetual or legacy serial: store it as the CI secret UNITY_LICENSE_SERIAL +# (the name `unity ci init` generates) and pass it to the step, e.g. in GitHub +# Actions `env: { UNITY_LICENSE_SERIAL: ${{ secrets.UNITY_LICENSE_SERIAL }} }`. +# A serial tied to a subscription needs a signed-in user, so for those seats use +# --floating, or the offline --generate-request / --file path (see "Headless / +# fleet license activation" in references/auth-license-cloud.md). +unity license activate --serial "$UNITY_LICENSE_SERIAL" # or: unity license activate --floating + +# 2. Only if the job also runs cloud commands: export UNITY_SERVICE_ACCOUNT_ID and +# UNITY_SERVICE_ACCOUNT_SECRET from the CI secret store. Every later command reads +# them directly, with no `unity auth login` and nothing written to a credential +# store (which a headless runner may not have). # 3. Build unity build /path/to/MyProject \ @@ -521,7 +568,7 @@ unity build /path/to/MyProject \ --allow-install echo "Exit code: $?" -# 4. Return the seat when done (floating/assigned) +# 4. Return the license when done (serial or assigned seats) unity license return --yes ``` @@ -548,6 +595,8 @@ unity run /path/to/MyProject \ echo "Exit code: $?" ``` +Writing scripts for a player build? Keep each `MonoBehaviour` alone in a `.cs` file named after its class. Several in one file can run fine in the Editor and Play Mode, yet the built player crashes at startup saying a scene file such as `level0` is corrupted. See [build-run-test.md](references/build-run-test.md#build). + ### CI: run tests and publish results ```bash @@ -569,6 +618,8 @@ Exit `8` means the run finished and reported failing tests; any other non-zero c `--report-format junit` makes `--output` a JUnit-schema report, which GitHub Actions and GitLab ingest as native test results with no converter step. It is written even when tests fail. Drop the flag for the NUnit3 default, or use `--report-format nunit,junit` to get both from one run. Add `--coverage` to collect coverage via the Unity Code Coverage package — it warns and carries on if the project doesn't have the package. See [build-run-test.md](references/build-run-test.md). +**Iterating on a change, not gating a pipeline?** Run the cheapest test that can fail for the change. Try EditMode first. Use `--filter`, `--affected` or `--rerun-failed` while you work. Run the full suite on the final code, once per mode, before you hand the work back. Never re-run a suite on code that has not changed, unless the run exited `6` with no verdict. Keep the NUnit default for these runs, because `--affected` and `--rerun-failed` cannot read the JUnit-only report the CI example above writes. [build-run-test.md](references/build-run-test.md#run-the-cheapest-test-that-proves-the-change) has the rule, and which report each flag reads and writes. `unity test` refuses a project that is open in an Editor: don't close the user's Editor to make room, and don't start a test run while they are in Play Mode. + ### Debug the CLI ```bash @@ -604,6 +655,11 @@ The key is absent when there is nothing to report, so read it as Branch on `code`; `message` is localized and meant for display. `data` is shaped per `code`. +`PLUGIN_UPDATE_AVAILABLE` means an installed plugin has a newer version. +`data` is `{ "plugin": "", "current": "…", "latest": "…" }`, and the +remediation is `unity plugin upgrade `. There is one entry per outdated +plugin. + `remediation` says how the notification gets resolved. It may be absent, which means the notification is informational and there is nothing to run. @@ -619,14 +675,18 @@ yourself.** Surface the notification and the command, and let the user decide. ## Notes - `--non-interactive` and `--yes` together suppress all prompts — use both in CI. +- A short flag means one thing on every command: `-y` is `--yes` wherever `--yes` exists, and `-a` is `--architecture`. `unity projects list` and `unity projects size` spell "every project" `--all`; write that, not `-a`. An old spelling the CLI still accepts (`-a` there, `--project` on `status`, `--template-version`, `--hub-version`, `--package-version`, `--list-components`, `--install-dir`, `--childModules`, `--wait-timeout`, `--git-token-stdin`, `--group_by` on `command`, `--local-space` on `collaboration annotations`, `--project-id` and `--organization-id` on `collaboration`, `-v` on `projects list`, `--since` on `releases` and `vcs git migrate-lfs`) prints a warning naming the spelling to use; so does `--verbose` on `editors` and `projects list`, which still shows every column but now spells it `--long`; in `--format json` it is in the envelope's `warnings`. Use the new spelling, so the warning never lands in a log you parse. +- `--timeout` takes a whole number of seconds written as digits (`--timeout 600`) on every command, and so do `UNITY_BUILD_TIMEOUT`, `UNITY_RUN_TIMEOUT` and `UNITY_TEST_TIMEOUT`. A unit, fraction or exponent (`30s`, `5m`, `1.5`, `1e3`) is a usage error (exit `2`), so convert minutes to seconds yourself. - `--format json` always produces machine-readable output; prefer it over parsing human text. Error envelopes are pretty-printed with the same 2-space indent as success envelopes. -- **Read failures from stdout, not stderr.** A failed command still writes a complete document to stdout: under `--format json` an envelope with `success: false` and a populated `errors` array (`errors[0].code` is the stable token to branch on); under `--format ndjson` the usual terminal `{"type":"result","success":false,…}` frame. **Every ndjson stream ends with exactly one terminal `{"type":"result"}` frame, on success too**, so a stream without one was truncated. Table-shaped listings (`unity editors`, `unity editors upgrade --check`, `unity templates list`, `unity releases`, `unity projects list`, `unity context list`, `unity auth list`, `unity modules list`, `unity list`, `unity doctor`) write one row per line first, with no `type` field on the rows, then a terminal frame whose `data` is `{"count": N}`; `--quiet` drops the rows but keeps the frame (except `unity projects list`, which never quiet-gates its rows), and a `--watch` listing, which only ends on Ctrl-C, writes no terminal frame. A few commands (`unity env`, `unity version`, `unity pipeline list`, `unity install-modules --list`, `unity changelog` among them) still end a successful ndjson stream without one, which is a known bug. **Branch on `success`, never on `data`** — `data` is usually `null` on a failure, but not always: a partial `unity editors add` failure carries a row per path, and an ambiguous `unity auth switch` carries `data.candidates` for you to disambiguate with. Check `success` and the exit code — never treat empty stdout as a failure signal, and do not parse stderr, which carries only human diagnostics in these formats. A handful of commands have not migrated yet and still print `{"error": "…"}` to stderr with empty stdout; if stdout is empty on a non-zero exit, that is a known bug in that command rather than a shape you should code against. +- **`success` is `true` exactly when the exit code is `0`.** A status command whose answer is no (`unity auth status` signed out, `unity license status` with no license, `unity status` with no ready Editor) is a failure: `success: false`, an `errors[0].code` naming the answer, and its report still in `data`. A listing that finds nothing (`unity editors running` with no Editor) succeeds with an empty list. +- **Read failures from stdout, not stderr.** A failed command still writes a complete document to stdout: under `--format json` an envelope with `success: false` and a populated `errors` array (`errors[0].code` is the stable token to branch on); under `--format ndjson` the usual terminal `{"type":"result","success":false,…}` frame. **Every ndjson stream ends with exactly one terminal `{"type":"result"}` frame, on success too**, so a stream without one was truncated. Table-shaped listings (`unity editors`, `unity editors upgrade --check`, `unity templates list`, `unity releases`, `unity projects list`, `unity context list`, `unity auth list`, `unity modules list`, `unity list`, `unity doctor`, `unity cloud org list`, `unity cloud project list`) write one row per line first, with no `type` field on the rows, then a terminal frame whose `data` is `{"count": N}`. A single-record answer (`unity version`, `unity language`, `unity config proxy`, `unity cache info` and the like) is that frame alone, its `data` the same as `--format json`; `unity env` writes its rows, then the frame. `--quiet` never changes json output. It never changes ndjson output either, except that `unity editors upgrade`, `unity editors module list`, `unity install-modules`, `unity build --list-targets`, `unity build --list-profiles` and `unity env` still drop their rows under `--quiet` (the terminal frame stays), a known bug; leave `--quiet` off when you need those rows. A warning in either format appears only in `warnings`, never on stderr. A `--watch` listing, which only ends on Ctrl-C, writes no terminal frame. A few commands (`unity install-modules --list` among them) still end a successful ndjson stream without one, which is a known bug. **Branch on `success`, never on `data`** — `data` is usually `null` on a failure, but not always: a partial `unity editors add` failure carries a row per path, and an ambiguous `unity auth switch` carries `data.candidates` for you to disambiguate with. Check `success` and the exit code — never treat empty stdout as a failure signal, and do not parse stderr, which carries only human diagnostics in these formats. If stdout is empty on a non-zero exit, that is a known bug in that command rather than a shape you should code against. - `unity [path]` is a shorthand for `unity open [path] --editor-version `. Works with `lts`, `latest`, or a full version string like `6000.0.47f1`. - The CLI supports kubectl-style plugins: any `unity-` binary on PATH is callable as `unity `. - Terminal output is hardened against control-character / escape-sequence injection from server-provided values (project titles, editor versions, module names) — C0 controls and non-SGR escape sequences are stripped from table/list/tree output, and now also from Commander usage errors, the `unity bug` log-archive warning, and `unity projects add`/`remove` machine (tsv) output, while SGR color/style codes are preserved. - The CLI reports anonymous crashes and errors via Sentry to help fix bugs (no IP address or hostname; home-directory paths and token-like values scrubbed before send), aligned with the Unity Hub. Opting in to analytics additionally attaches an anonymized machine id; opted-out users stay fully anonymous. Set `UNITY_NO_CRASH_REPORT` to disable reporting entirely. Separately again, every run sends one anonymous `cli telemetry` usage ping regardless of analytics/consent state — see [diagnostics-maintenance.md](references/diagnostics-maintenance.md#analytics--usagetelemetry-consent). -- The CLI is currently in **beta** (latest: `1.0.0-beta.11`). It moved to 1.0 versioning at `1.0.0-beta.1`; it's still a beta. The install command needs no channel setting: until GA ships it installs the latest beta, and afterward the stable release. +- `unity --version` prints the version you have. `unity self-update --check --format json` says whether a newer one exists on your installed channel (a beta follows betas). `unity self-update --check --channel stable --format json` checks for a stable release, for example when moving off a beta once GA ships. - **Always use the latest version of the Unity CLI unless you are told to use a specific version.** A newer CLI regularly adds commands and output fields these docs describe, so an outdated one fails in ways that read like the docs being wrong. - As of `0.1.0-beta.8` the CLI checks in the background for a newer version and prints an unobtrusive "update available" notice (interactive sessions only; never delays a command). Turn it off with `unity config update-check off` or the `UNITY_NO_UPDATE_CHECK` env var. +- The same check also covers plugins the CLI installed (`unity plugin install`): when one has a newer version, a notice names it and suggests `unity plugin upgrade`, at most once a day. The same two switches turn it off. - **Under `--format json` and `--format ndjson` that same notice reaches you as a `notifications` array on the envelope** — see [Notifications](#notifications) below. Over MCP it arrives once per session, as prose in the server's `initialize` `instructions`. - Outbound HTTP from every CLI command honors the resolved proxy (see `unity config proxy`). An invalid `--proxy` value (malformed URL or unsupported scheme) fails with a usage error (exit 2) instead of being silently ignored. Inspect what the CLI actually resolved with `unity env --format json` or `unity doctor --format json` — both surface the active proxy URL, its source, and auth source. diff --git a/skills/unity-cli/references/auth-license-cloud.md b/skills/unity-cli/references/auth-license-cloud.md index 395d00a..67290cf 100644 --- a/skills/unity-cli/references/auth-license-cloud.md +++ b/skills/unity-cli/references/auth-license-cloud.md @@ -29,17 +29,16 @@ unity auth login --client-id --secret-from-stdin # Login without persisting credentials to the keyring (ephemeral CI) unity auth login --client-id --secret-from-stdin --no-store -# Logout (clears both service-account and OAuth credential slots) +# Sign the CLI out (clears its OAuth sign-in and any saved service-account +# credentials). There is no confirmation prompt; --yes is accepted and changes nothing. unity auth logout -# Log a specific stored account out, rather than the active one +# Sign one stored account out of the CLI, rather than the active one. Its saved +# sign-in stays on the machine while another app, such as the Hub, still uses it. unity auth logout user@example.com - -# Skip the confirmation prompt -unity auth logout --yes ``` -**How sign-in is served.** Every command’s OAuth token read goes through a resident auth broker — a background `unity` process started on demand by the first command that needs a token, exiting on its own after two idle minutes. It seals the token store to the machine’s hardware where available (a non-exportable TPM key on Windows, TPM2 via `systemd-creds` on Linux, then DPAPI, the Secret Service, the Keychain and a local key file, in that order), and `unity doctor` reports the active tier. On macOS and Windows it verifies a connecting process’s code signature and refuses an unsigned or non-Unity-signed peer; set `UNITY_PEER_AUTH_MODE=identify-only` to log without refusing (for an Editor you built from source), and `UNITY_NO_AUTH_BROKER=1` to bypass the broker and read the OS keyring directly. `unity auth consumers` (below) lists the applications that have used this machine’s sign-in through it. +**How sign-in is served.** Every command’s OAuth token read goes through a resident auth broker — a background `unity` process started on demand by the first command that needs a token, exiting on its own after two idle minutes. It seals the token store to the machine’s hardware where available (a non-exportable TPM key on Windows, TPM2 via `systemd-creds` on Linux, then DPAPI, the Secret Service, the Keychain and a local key file, in that order), and `unity doctor` reports the active tier. On macOS and Windows it verifies a connecting process’s code signature and refuses an unsigned or non-Unity-signed peer. The Editor identity helper that signs in an Editor started by `unity open` applies the same rule, so an Editor built from source (unsigned or not Unity-signed) is not supported for automatic sign-in. Set `UNITY_NO_AUTH_BROKER=1` to bypass the broker and read the OS keyring directly. `unity auth consumers` (below) lists the applications in its audit trail and revocation list. #### Multiple accounts @@ -75,31 +74,35 @@ Three behaviors worth knowing before scripting these: `unity auth default` resolves the project from the current directory unless `--project` is given, and errors if that path isn't a Unity project. Passing both an account and `--clear` is rejected. -#### Consumers — see who's used your sign-in, and revoke one +#### Consumers — see who's used your sign-in, and mark one revoked -Some products use your Unity sign-in without holding your refresh token themselves, brokering through the CLI's own auth broker instead. `unity auth consumers` lists every application that has done this on this machine, and `unity auth revoke` cuts one off. +Some products use your Unity sign-in without holding your refresh token themselves, brokering through the CLI's own auth broker instead. `unity auth consumers` lists every application in the broker's audit trail on this machine, plus any in the local revocation list, and `unity auth revoke` records one in that list. Read the Granted and Denied columns before concluding an application used your sign-in: one with only denials never received a token, and a revoked application with no audit history appears with no activity at all. ```bash # Application, scopes, grants/denials, and when it was last used unity auth consumers unity auth consumers --format json -# Stop an application from using your sign-in (covers every scope it asked for) +# Add an application to the local revocation list (every scope it asked for) unity auth revoke "Some Tool" -# Undo that +# Remove it from the list unity auth revoke "Some Tool" --restore ``` -Both are local-only — reading the broker's audit trail and a local revocation list — so neither needs sign-in or the network. On a machine that has never run the broker, `auth consumers` reports an empty list rather than an error. `auth revoke` without `--restore` revokes; with it, it un-revokes; either way it reports the actual resulting state (e.g. "already revoked") rather than assuming the write changed anything. +Both are local-only — reading the broker's audit trail and a local revocation list — so neither needs sign-in or the network. On a machine that has never run the broker, `auth consumers` reports an empty list rather than an error. `auth revoke` without `--restore` revokes; with it, it un-revokes; either way it reports the actual resulting state (e.g. "already revoked") rather than assuming the write changed anything. **The list is not enforced yet:** the auth broker does not consult it when it hands out tokens, so a revoked application can still use your sign-in. Neither `auth revoke` nor `unity auth logout` currently stops an application that already has broker access; `logout` keeps the account's saved sign-in while the Hub still uses it. The stored token itself is sealed to the machine that wrote it (TPM-backed on Windows/Linux where available, Keychain on macOS, with a local-key-file fallback everywhere), so a copied token-store file is worthless on another machine. `unity doctor` reports which protection is in force. **Separate sign-in from Hub.** As of `0.1.0-beta.8`, the CLI and the GUI Hub store their sign-in credentials **separately** — signing in to one no longer signs you out of (or overwrites the account of) the other, so each can stay signed in as a different account. (In earlier betas they shared a single keyring session.) +The two still share one account list. So an account you signed in to in the Hub shows up in `unity auth list` even though the CLI is not signed in to it, and `unity auth status` can report "not signed in" while `auth list` shows that account. To use it from the CLI, try `unity auth switch ` first: when that account's sign-in is readable on this machine, it becomes the CLI's active account with no browser round trip. When it has no usable sign-in, run `unity auth login` and sign in as that account in the browser; `auth switch` cannot sign an account in. + +**Using the Hub's sign-in needs a yes.** When the CLI is signed out and the Hub is signed in, `unity auth` and `unity auth login` on a terminal ask "Use , signed in to Unity Hub? (y/N)". The default is no. Yes makes that account the CLI's active account, exactly as `unity auth switch` does, and changes nothing in the Hub; no carries on as before (the browser sign-in, or the `auth` help). The CLI never takes the Hub's sign-in on its own. Runs that can't ask (`--json`, `--non-interactive`, no terminal) never prompt and never switch: `unity auth status --json` reports `adoptable: true`, the account and `adoptCommand`, and `unity auth login` prints the `unity auth switch ` command to run (as an `adoptable=` progress frame under `--json`). An agent should show the user that command rather than run it unasked. Once switched, signing out of the Hub doesn't sign the CLI out. If you only want the CLI in order to activate a perpetual or legacy serial license, you do not need to sign in at all: see [Headless / fleet license activation](#headless--fleet-license-activation). + **Service-account credentials via env vars** (`UNITY_SERVICE_ACCOUNT_ID` + `UNITY_SERVICE_ACCOUNT_SECRET`) mint bearer tokens automatically for the duration of the process — no browser round-trip, no keyring write. If only one of the two is set, the CLI prints a warning on stderr instead of silently falling back to the keyring/OAuth identity. -The interactive `unity auth login` flow prints the sign-in URL to the terminal **before** attempting to launch the browser, which unblocks remote/headless sessions (SSH, containers, dev VMs) where `xdg-open` / `open` has no graphical session to attach to. With `--format json`, an `auth_url=…` progress frame is emitted so machine consumers can capture the URL without parsing human text. +The interactive `unity auth login` flow prints the sign-in URL to the terminal **before** attempting to launch the browser, which unblocks remote/headless sessions (SSH, containers, dev VMs) where `xdg-open` / `open` has no graphical session to attach to. With `--format json`, an `auth_url=…` progress frame is written to stderr so machine consumers can capture the URL without parsing human text, and stdout stays the result envelope alone. `unity auth status` reflects real session state (including an explicit "session expired" message), not optimistic local assumptions. `unity doctor` and `unity cloud status` report the same real session state. @@ -137,10 +140,47 @@ unity license server status # reachability + available seats `unity license status` also reports `floatingServer` (the licensing server this machine is configured against) and `machineId` (the identity the licensing client reports to it) — the two values to match against a lease record when a floating seat looks stuck. Both are always present in `--format json` / `ndjson`; human and `tsv` output shows them only when a floating server is configured. -`activate` takes a single mode flag (combining them is a usage error). The default (no flag) and `--personal` activate the signed-in user's entitlements — sign in first with `unity auth login`. `--personal` also requires `--accept-eula` to acknowledge the Unity Personal license terms. `--serial` / `--file` work offline without sign-in. `--floating` requires a configured floating license server (exit `4` if none is set). `--generate-request` writes a `.alf` request for air-gapped activation instead of activating. `return` returns the active licenses, prompting for confirmation first — pass `--yes` to skip (required in non-interactive shells and with `--json`). All honor `--json` / `--format` and exit non-zero on failure (`2` bad usage, `3` sign-in required, `4` floating not configured, `6` licensing-client error). +`activate` takes a single mode flag (combining them is a usage error). The default (no flag) and `--personal` activate the signed-in user's entitlements — sign in first with `unity auth login`. `--personal` also requires `--accept-eula` to acknowledge the Unity Personal license terms. `--serial`, `--floating`, `--file` and `--generate-request` need no sign-in, and `--file` and `--generate-request` also work offline. `--floating` requires a configured floating license server (exit `4` if none is set). `--generate-request` writes a `.alf` request for air-gapped activation instead of activating. `return` returns the active licenses, prompting for confirmation first — pass `--yes` to skip (required in non-interactive shells and with `--json`). All honor `--json` / `--format` and exit non-zero on failure (`2` bad usage, `3` sign-in required, `4` floating not configured, `6` licensing-client error). **Service accounts.** The `license` commands recognize service-account sessions (`UNITY_SERVICE_ACCOUNT_ID` / `UNITY_SERVICE_ACCOUNT_SECRET`, or `unity auth login --client-id`): `unity license status` reports `Signed in: yes (service account)` and includes the auth mode in JSON. Unity's licensing backend does **not** accept service-account tokens for license activation, so with a service-account session the default entitlement mode and `--personal` fail up front — before contacting the licensing client — with guidance toward the unattended options (`--floating`, `--file`, `--generate-request`, or a perpetual `--serial`). `unity license return` lists and returns serial-activated licenses too (not just assigned/subscription seats) — important for CI machines that activate per run — and returns each license individually, so when only some can be freed it reports what succeeded (in text and in the JSON `returned` / `failed` fields) instead of an all-or-nothing failure. +#### Headless / fleet license activation + +For build agents, lab machines, and any machine with no browser. There is **no username/password option**: the CLI only signs a user in through the browser, and a service account cannot activate a license, because Unity's licensing backend does not accept service-account tokens for activation. None of the paths below needs a signed-in account. + +```bash +# Perpetual or legacy Pro/Plus serial: no sign-in needed +unity license activate --serial --non-interactive --format json + +# Air-gapped machine: write a request, exchange it elsewhere, then import the license file +unity license activate --generate-request ./unity.alf # on the offline machine +# ...exchange unity.alf for a .ulf license file on a machine with internet access +# (Unity's manual activation page), then copy the .ulf back... +unity license activate --file ./Unity_lic.ulf # on the offline machine + +# Floating (network) licenses: lease a seat from the configured server +unity license server status # check the server and its free seats first +unity license activate --floating + +# Decommissioning a machine: give the seat back so the serial can be activated elsewhere +unity license return --yes --non-interactive +``` + +- **`--serial`** activates a serial-number license without signing in, on every platform. A serial tied to a subscription can still be refused with "This license requires a signed-in Unity account"; sign in on that machine, or use one of the other paths. +- **`--generate-request` then `--file`** is the offline path. The request file identifies this machine, so import the resulting license file on the same machine that generated the request. Both talk to the Unity Licensing Client on the machine, and the CLI downloads that client (about 110 MB) the first time a `license` command needs it. Stage it while the machine, or the image it is built from, still has network access: run `unity license status` once. With no client installed, that fetches one into the CLI's data folder. A `Unity.Licensing.Client` already on `PATH` is used instead when no downloaded copy exists, with no version check, and an older one fails the handshake, so update or remove an older `PATH` client before staging. The downloaded client lands in the data folder of the user who ran the command (under `HOME`, or `XDG_CONFIG_HOME` when set), so run the staging step as the same user, with the same `HOME`, that will run the CLI later; a client staged as root in an image build is not found by a container that runs as another user. Otherwise, put a current `Unity.Licensing.Client` on a shared `PATH`. + + On an image with no license yet, `unity license status` exits `4` (no license active), which is expected at this point. A fail-fast shell or a Dockerfile `RUN` stops on it, so accept exactly `4` and keep every other failure, such as `6` when the client could not be fetched: + + ```bash + unity license status || [ $? -eq 4 ] + ``` + + ```powershell + unity license status; if ($LASTEXITCODE -notin 0, 4) { exit $LASTEXITCODE } + ``` +- **`--floating`** needs a floating license server configured on the machine first (exit `4` otherwise). `unity license server list` shows which one is configured. +- **`license return`** returns serial-activated licenses as well as assigned ones. Run it before you reimage or retire a machine, or that activation keeps counting against the serial's limit. It needs `--yes` when nothing can answer a prompt. A license imported offline with `--file` cannot be returned this way: `license return` reports it as activated manually and leaves it in place. + `unity license server list` shows the configured floating license server (from the `licensingServiceBaseUrl` machine setting; a pure settings read, no client download). `unity license server status` contacts that server and reports reachability plus available seats — exit `4` when no server is configured, `6` when configured but unreachable. --- @@ -155,19 +195,22 @@ unity cloud status --format json # Organizations unity cloud org list --format json -unity cloud org current # print the active default org id +unity cloud org current # print the active default org (named when signed in) unity cloud org set-default # set active default org unity cloud org clear-default # revert to "All Organizations" -# Create an organization -unity cloud org create "" --industry gaming -unity cloud org create "" --industry oil-and-gas --set-default +# Create an organization (company profile: individual or company, plus country) +unity cloud org create "" --first-name Ada --last-name Lovelace --country GB +unity cloud org create "" --company-name "Nebula Games Inc." --country US --region CA --set-default # Projects in the active organization unity cloud project list --format json # * marks the active default project +# To link a local folder to a cloud project, use `unity projects link cloud ` (see projects-templates.md); +# `unity cloud project --help` and `unity projects link cloud --help` point at each other + # Default project, stored per organization -unity cloud project current # print the active default project id +unity cloud project current # print the saved default project, and the project linked to this folder unity cloud project set-default # project UUID, Genesis id, or exact name unity cloud project clear-default # drop this organization's default @@ -175,32 +218,52 @@ unity cloud project clear-default # drop this organization's unity cloud project list --cloud-org # also via UNITY_CLOUD_ORG env var ``` -**Creating an organization takes a name and an industry.** The name is trimmed and capped at 40 -characters; `--industry` accepts either the kebab key (`consumer-electronics`, `oil-and-gas`) or the -display spelling (`"Oil & gas"`), and an unknown value is rejected up front with the accepted list. -Both are validated before anything is sent, so a typo costs no network call. Omitting `--industry` -opens a picker on a terminal and is a usage error under `--non-interactive` or when output is -redirected. `--set-default` makes the new organization active, exactly as `org set-default` would; if the -setting cannot be written the organization is still reported as created, with a warning that the -default did not take effect. A name already in use is reported as such rather than as an HTTP -status. Machine output is a create-specific shape carrying `id` and `name` only — deliberately not -`org list`'s row, since the create response omits `role` and the default marker describes a list -rather than a single new organization. +**Creating an organization takes a name and a company profile**, the same fields the Unity +Dashboard's create dialog asks for. `--type individual` needs `--first-name` and `--last-name`; +`--type company` needs `--company-name`. `--type` can be left out when the name flags already say +which it is, and passing both kinds is refused. `--country` is a two-letter ISO 3166-1 alpha-2 code (`GB`, +not `GBR`, and not `UK`, which is not an ISO code). `--region` is the ISO 3166-2 subdivision (`CA`, or `US-CA`) and is +required for the US, Canada, and Mexico and refused for every other country. The organization name +is trimmed and capped at 40 characters; person and company names follow the Dashboard's own +character rules. Everything passed is validated before the sign-in check and before any request, +so a typo costs no network call. Omitted fields are prompted for on a terminal; under +`--non-interactive`, or when output is redirected, the command fails naming every missing option. +Whether a country or subdivision code actually exists is the server's call, and its field-level +reason is reported as-is. `--set-default` makes the new organization active, exactly as +`org set-default` would; if the setting cannot be written the organization is still reported as +created, with a warning that the default did not take effect. A name already in use is reported as +such rather than as an HTTP status. Machine output is a create-specific shape carrying `id` and +`name` only, deliberately not `org list`'s row, since the create response has no `role` and the +default marker describes a list rather than a single new organization. **The default project is per organization.** `set-default` stores the project's UUID against the active organization's Genesis id, so switching your active organization switches which default applies, and `clear-default` only drops the active organization's. `cloud project current` and `clear-default` read and write the settings file directly, so they need no network and no session when the organization comes from your stored default; passing `--cloud-org ` needs a lookup, -so that path requires a session like the rest. +so that path requires a session like the rest. `cloud project current` labels what it prints as the +saved default, and inside a Unity project folder linked to a cloud project it also prints that linked +project. Setting a default never changes which project a folder is linked to (`projects link cloud` +does that). Under `--format json`, `activeCloudProjectId` still holds the default, with +`defaultCloudProjectId` and `linkedCloudProjectId` beside it (`null` outside a linked project folder). **What consumes it.** Commands that need a Unity Cloud project but were not given one fall back to -the stored default. The order is the explicit flag (`--project-id`), then `UNITY_CLOUD_PROJECT`, -then the cloud link in the project directory's `ProjectSettings/PlayerSettings.asset`, then the +the stored default. The order is the explicit flag (`--cloud-project`), then `UNITY_CLOUD_PROJECT`, +then the cloud link in the project directory's `ProjectSettings/ProjectSettings.asset`, then the stored default, so inside a cloud-linked project the link still wins. `unity collaboration` uses -this chain. - -**Exit codes.** The `cloud` and `auth` commands map an authentication failure (expired or missing session, rejected sign-in) to `3`, and any other operational failure (network, server error) to `6` — so scripts can distinguish "sign in again" from a genuine command failure. `unity auth status` / `logout` follow the same convention. +this chain. `unity projects link cloud` applies the default too, with its own order (`--cloud-project`, +then `UNITY_CLOUD_PROJECT`, then the default, see projects-templates.md). `unity projects create` does +not read the default: it creates a new cloud project unless `--cloud-project` names one or `--no-cloud` +(or `UNITY_NO_CLOUD`) declines. `cloud project current` prints the saved default, not necessarily the +folder's project: under `--format json`, `linkedCloudProjectId` is the folder's link. + +**A project in another organization.** `set-default` looks in the active organization first. When the project is in +another of your organizations, the error names that organization (`UNKNOWN_CLOUD_PROJECT`, exit `2`, with the +organization under `data.organization` in `--format json`). Run `unity cloud org set-default `, then +`set-default` again. When the organization comes from `--cloud-org` or `UNITY_CLOUD_ORG`, the other organizations are +not checked, because that value overrides the saved default. + +**Exit codes.** The `cloud` and `auth` commands map an authentication failure (expired or missing session, rejected sign-in) to `3`, and any other operational failure (network, server error) to `6` — so scripts can distinguish "sign in again" from a genuine command failure. `unity auth status` / `logout` follow the same convention. A status command that answers no is a failure in `--format json` too: signed out, `unity auth status` and `unity cloud status` exit `3` with `success: false` and `errors[0].code` `NOT_SIGNED_IN` (or `SESSION_EXPIRED`); `unity license status` with no license exits `4` with `LICENSE_NOT_ACTIVE`; `unity license server status` exits `4` with `LICENSE_SERVER_NOT_CONFIGURED` or `6` with `LICENSE_SERVER_UNREACHABLE`. Each keeps its report in `data` (`loggedIn: false`, `active: false`, `reachable: false`), so read `success` or the exit code, they always agree. --- diff --git a/skills/unity-cli/references/build-run-test.md b/skills/unity-cli/references/build-run-test.md index 946b364..1f12159 100644 --- a/skills/unity-cli/references/build-run-test.md +++ b/skills/unity-cli/references/build-run-test.md @@ -154,6 +154,33 @@ unity test /path/to/MyProject --shard 2/5 --shard-inventory ./ci/full-run.xml It deliberately does **not** pass `-quit`: `-runTests` quits the editor itself once results are written, so forcing `-quit` would terminate it before the report exists. Anything after `--` is forwarded to the editor verbatim, except reserved flags (`-projectPath`, `-batchmode`, `-runTests`, `-testPlatform`, `-testResults`, `-testFilter`, `-quit`, `-useHub`, `-hubIPC`, `-enableCodeCoverage`, `-coverageResultsPath`, `-coverageOptions`), which are rejected — those are managed by the command (use `--coverage` for the coverage trio); `-useHub`/`-hubIPC` are deliberately never passed (the CLI runs no Hub IPC server). +#### Run the cheapest test that proves the change + +`unity test` is not only a CI step. A whole-suite run after every edit is slow, and a suite only grows, so pick the cheapest run that can still fail for the change you made: + +1. **EditMode before PlayMode.** An EditMode run needs no Play Mode session. Start there, and use `--mode PlayMode` only for behavior that needs frames, physics or a running scene. +2. **Narrow the run while you iterate.** `--filter` runs the tests you name, `--affected --since ` runs the test assemblies the change can reach, and `--rerun-failed` runs only the failures a report holds. `--affected` needs the NUnit report of an earlier full run as its inventory, so seed it with one full run before its first use. It runs the whole suite when the change edits a test file, so name those tests with `--filter` instead. +3. **Run the full suite on the final code before you hand the work back,** once for each mode the project has tests in. Selection is an approximation, so the full run is the proof. A green `--rerun-failed` run is not. +4. **Never re-run a suite on code that has not changed.** The last run's report already holds the answer, so read it. Exit `8` means the tests ran and failed: fix the cause, then use `--rerun-failed`. Repeat a run only when it never produced a verdict (exit `6`) and the cause is gone. + +Keep each report where the next run expects it: + +- A plain run writes over `--output`, and that includes one with `--filter` or `--mode PlayMode`. Keep the full run's report as the record, and give every other plain run its own `--output`. +- Write the record as NUnit, the default. `--affected` and `--rerun-failed` cannot read a JUnit-only report. +- `--affected` reads its inventory from `--output` and writes `.affected.xml` beside it. `--rerun-failed` reads its failures from `--output` and writes `.rerun.xml`. After a failing `--affected` run, point `--rerun-failed` at its `.affected.xml`. If `--rerun-failed` says there is nothing to re-run, check that it read the report of the run that failed. A wrong path looks like a pass. +- Keep the reports in a folder outside the repository (`/path/to/reports` below). `--affected` counts any file git shows as changed or untracked outside `Assets`, `Packages` and `ProjectSettings`, a report included, as a change outside the project, and then runs the whole suite. Don't edit `.gitignore` to hide them: that edit is a change outside the project too. + +```bash +unity test /path/to/MyProject --mode EditMode --output /path/to/reports/full.xml # the full run: the record +unity test /path/to/MyProject --mode PlayMode --output /path/to/reports/play.xml # the PlayMode suite, its own report +unity test /path/to/MyProject --mode EditMode --filter "MyNamespace.MyTests" --output /path/to/reports/filter.xml +unity test /path/to/MyProject --mode EditMode --affected --since origin/main --output /path/to/reports/full.xml # writes full.affected.xml +unity test /path/to/MyProject --mode EditMode --rerun-failed --output /path/to/reports/filter.xml # a --filter run's failures +unity test /path/to/MyProject --mode EditMode --rerun-failed --output /path/to/reports/full.affected.xml # an --affected run's failures +``` + +`unity test` starts its own batch Editor, so it refuses a project that is already open in an Editor (exit `6`, "already open in a running Editor"). The message says to close that Editor. Don't, when it is the user's: it may be in Play Mode, and closing it ends their session. Run the tests in that Editor through the Pipeline's `run_tests` command instead, and only after `editor_status` shows that it is not playing (its `playMode` reads `stopped`). If it shows anything else, ask the user. + #### Report formats (CI-native JUnit) The editor only ever writes NUnit3, so JUnit is produced by converting that report after the run. `--report-format` decides what `--output` contains: @@ -360,10 +387,12 @@ The first-class build workflow. Rule of thumb vs `unity run`: building a player Pick one build strategy: a Unity 6+ Build Profile (`--profile`), a built-in desktop player build (`--target` with a desktop target, `--output-path` required), or a custom `--execute-method` (your method is responsible for the actual build, including honoring `--output-path`). Non-desktop targets need `--profile` or `--execute-method`. -The build log is always written to the log file **and** streamed to stdout at the same time; pass `--no-tail` to write the file only (the tail is also suppressed by `--quiet` and `--format ndjson`). +The build log is always written to the log file **and** streamed at the same time — to stdout, or to stderr under `--format json` so stdout holds only the result document; pass `--no-tail` to write the file only (the tail is also suppressed by `--quiet` and `--format ndjson`). The streamed copy masks the credentials the CLI passes the Editor (the `-accessToken` value, the Android keystore passwords) with `***`, as do `run` and `test` wherever they relay the Editor's log. **How the outcome is decided — do not gate CI on Unity's exit code alone.** For a built-in build (`--profile`, or `--target` without `--execute-method`) the CLI does not trust Unity's process exit code by itself: a real Editor exits `0` from a player build its own log reports as failed. When the log carries Unity's terminal verdict, that verdict decides the outcome, so a failed build exits **6** even where the Unity process exited `0`, and the provenance manifest records the failure. A log with no verdict falls back to the exit code, and an `--execute-method` build always does — your method owns the code it returns, including when it deliberately tolerates a build it reports on itself. A build that succeeds with errors still in its log reports them on stderr rather than dropping them; they also ride `editorErrors` under `--format json`. +**Writing scripts for a player build? Keep one `MonoBehaviour` per `.cs` file, named after its class.** Unity ties a `MonoBehaviour` to its script by file name, so `PlayerController` belongs alone in `PlayerController.cs`. Several in one file can run fine in the Editor and Play Mode with no warning. The built player then crashes at startup with `The file '.../level0' is corrupted! Remove it and launch unity again!`. `level0` is the first scene in the build (later ones are `level1`, `level2`, and so on), so the message points at scene data, not the scripts. If you see it, look for a `.cs` file that declares more than one `MonoBehaviour` (one deriving from another of yours counts) and give each its own file, named after its class, before you bisect scenes. Check the scripts you wrote or generated this session before you build, and leave existing code alone until the error appears. + ```bash # Build with a custom build method unity build /path/to/MyProject \ @@ -389,7 +418,7 @@ unity build /path/to/MyProject --profile "Windows Release" --output-path ./Build | `--create-profile ` | Create a Build Profile for a target and exit without building (Unity 6+); build with it afterwards via `--profile`. | | `--build-target-group ` | Forwarded to Unity as `-buildTargetGroup`. | | `-o, --output-path ` | Output path. With `--execute-method`, passed as `-buildOutput` (your method must honor it); otherwise the built-in build's destination (required). | -| `-l, --log-file ` | Log file path. Default: `/Logs/build--.log`. Streamed to stdout by default (see `--no-tail`). | +| `-l, --log-file ` | Log file path. Default: `/Logs/build--.log`. Streamed to stdout by default, stderr under `--format json` (see `--no-tail`). | | `--editor-version ` | Override editor version (default: from `ProjectVersion.txt`). | | `-e, --editor-path ` | Use a specific editor binary. | | `-a, --architecture ` | Editor architecture (`x86_64` or `arm64`). | @@ -426,16 +455,17 @@ Keystore flags are validated together. Secrets passed as command-line flags surf **Interrupt exit codes** — interrupting `unity build` exits with the conventional signal code (`130` for Ctrl-C / SIGINT, `143` for SIGTERM) rather than a generic `1`, so callers and CI can tell an aborted build apart from a failed one. The temporary Android keystore is scrubbed before exit. -**Stall heartbeat** — a long build prints a periodic heartbeat (`Still building — 4m30s elapsed, last log output 3m10s ago`) tracking both total elapsed time and time since the Editor log last grew, so silence in the log no longer looks the same as a hang. Detection itself reads the Editor log's size directly, so it keeps working regardless of output mode — but whether the heartbeat is *printed* depends on the mode: on the human path it goes to stderr (so the streamed log stays clean) and is unaffected by `--no-tail`, but **`--quiet` suppresses it entirely** in human mode. Under `--format json`/`--format ndjson` it appears as periodic progress frames and is emitted regardless of `--quiet` — quiet only silences the human path. +**Stall heartbeat** — a long build prints a periodic heartbeat (`Still building — 4m30s elapsed, last log output 3m10s ago`) tracking both total elapsed time and time since the Editor log last grew, so silence in the log no longer looks the same as a hang. Detection itself reads the Editor log's size directly, so it keeps working regardless of output mode — but whether the heartbeat is *printed* depends on the mode: on the human path it goes to stderr (so the streamed log stays clean) and is unaffected by `--no-tail`, but **`--quiet` suppresses it entirely** in human mode. Under `--format json`/`--format ndjson` it appears as periodic progress frames (on stderr under json, on stdout under ndjson) and is emitted regardless of `--quiet` — quiet only silences the human path. ```bash -# With --format json, stdout includes newline-delimited JSON progress frames before the final envelope: -unity build /path/to/MyProject --target StandaloneOSX --execute-method Builder.Build --output-path ./build/output --format json -# Output (each line is a JSON object): +# With --format json, stdout is the result document alone; the progress frames and the streamed log go to stderr: +unity build /path/to/MyProject --target StandaloneOSX --execute-method Builder.Build --output-path ./build/output --format json > result.json +# stderr (each progress line is a JSON object, the Editor log lines between them): # {"type":"progress","command":"build","message":"Resolving project..."} # {"type":"progress","command":"build","message":"Resolving editor..."} # {"type":"progress","command":"build","message":"Starting Unity..."} # {"type":"progress","command":"build","message":"Unity exited (code 0)"} +# result.json: # { "success": true, "command": "build", "data": { "target": "...", "logFile": "...", "outputPath": "/path/to/MyProject/build/output" } } ``` @@ -482,7 +512,7 @@ The [Unity Accelerator](https://docs.unity3d.com/Manual/UnityAccelerator.html) i unity test /path/to/MyProject # One-shot override / opt-out -unity build /path/to/MyProject --target StandaloneLinux64 --accelerator cache.example.com:10080 +unity build /path/to/MyProject --target StandaloneLinux64 --output-path ./Build/MyGame.x86_64 --accelerator cache.example.com:10080 unity test /path/to/MyProject --no-accelerator ``` diff --git a/skills/unity-cli/references/cloud-automation.md b/skills/unity-cli/references/cloud-automation.md index 0acdd16..87d0fa9 100644 --- a/skills/unity-cli/references/cloud-automation.md +++ b/skills/unity-cli/references/cloud-automation.md @@ -51,9 +51,9 @@ Blank values fall through to the next source. Inspect saved defaults with context when the user names an organization/project; inventory commands don't change saved defaults. -Service accounts need a numeric organization ID, not its name. A signed-in -OAuth user can use an ID or organization name. Pipeline Automation doesn't take -`--cloud-project`. Missing required context fails with exit `4`. +Service accounts need a numeric organization ID and a project ID, not names. A +signed-in OAuth user can use an ID or a name for the organization and for the +Cloud Build project. Pipeline Automation doesn't take `--cloud-project`. Missing required context fails with exit `4`. ## Run list and get commands diff --git a/skills/unity-cli/references/collaboration.md b/skills/unity-cli/references/collaboration.md index 517073a..bd98126 100644 --- a/skills/unity-cli/references/collaboration.md +++ b/skills/unity-cli/references/collaboration.md @@ -25,21 +25,26 @@ Editor open. Annotations created either way are visible to both. ### Shared behavior -**Project scoping — `--project-id` OR an inferred project.** The project-scoped commands +**Project scoping — `--cloud-project` OR an inferred project.** The project-scoped commands (`annotations`, `attachments`, `reactions`, `thumbnail`, `read`, `subscribe`, `unsubscribe`, and -`jira issues create/get/link/unlink/search/types`) take both `--project-id ` (Unity Cloud project -id — find one with `unity cloud project list`, see [auth-license-cloud.md](auth-license-cloud.md)) -and `--project-path `. Neither is required: resolution order is +`jira issues create/get/link/unlink/search/types`) take both `--cloud-project ` (Unity Cloud +project id — find one with `unity cloud project list`, see [auth-license-cloud.md](auth-license-cloud.md)) +and `--project-path `. It takes an id only, not a project name. `--project-id` is the +deprecated spelling of `--cloud-project`: it still works, and warns. Neither is required: resolution +order is -1. explicit `--project-id`, else +1. explicit `--cloud-project`, else 2. the `UNITY_CLOUD_PROJECT` env var, else 3. `--project-path` → `UNITY_PROJECT_PATH` env var → the current directory, reading - `ProjectSettings/PlayerSettings.asset` for the project's `cloudProjectId`, else + `ProjectSettings/ProjectSettings.asset` for the project's `cloudProjectId`, else 4. the stored default cloud project for your active organization (`unity cloud project set-default`, see [auth-license-cloud.md](auth-license-cloud.md)). +An empty or whitespace-only `--cloud-project` or `UNITY_CLOUD_PROJECT` counts as not set, so the next +step applies. + If none yields an id it fails with: `Could not determine the Unity Cloud project for ''. -Pass --project-id explicitly, or --project-path to point at a project linked to Unity Cloud.` So +Pass --cloud-project explicitly, or --project-path to point at a project linked to Unity Cloud.` So inside a cloud-linked Unity project you can drop the flag entirely, and with a default set you can drop it outside one too. Note the link outranks the default: it is the more specific fact about the directory you pointed the command at. Most of `jira` is scoped @@ -71,14 +76,14 @@ differently: Both split on the first `=` only (values may contain `=`); repeating a key means last value wins; a pair without `=` or with an empty key fails with `Invalid key=value pair: `. -**Raw-JSON flags.** `--camera`, `--local-space` / `--local`, `--time`, `--position`, +**Raw-JSON flags.** `--camera`, `--local`, `--time`, `--position`, `--attachments`, and `annotations list --query` take a JSON value and fail with `Invalid JSON for --` when it doesn't parse. Shapes: | Flag | JSON shape | |---|---| | `--camera` | `{"position":{"x":0,"y":0,"z":0},"rotation":{"x":0,"y":0,"z":0},"fieldOfView":60,"target":{...},"projection":"...","verticalSize":1}` — position + rotation required, rest optional | -| `--local-space` (annotations) / `--local` (attachments spatial) | `{"parentId":"...","position":{"x":0,"y":0,"z":0},"cameraPosition":{"x":0,"y":0,"z":0}}` — same shape, different flag name per group | +| `--local` (annotations, attachments spatial) | `{"parentId":"...","position":{"x":0,"y":0,"z":0},"cameraPosition":{"x":0,"y":0,"z":0}}` | | `--time` | `{"timeScale":1,"timeStamp":0}` | | `--position` (attachments spatial) | `{"x":0,"y":0,"z":0}` | | `--attachments` (annotations create) | JSON array of `{"type":"...", ...}` attachment objects | @@ -214,7 +219,7 @@ Annotation objects returned by `get`/`list`/`replies` (`--format json`) carry: ` | Command | Args | Key options | |---|---|---| | `annotations count` | `[target]` (glob `**` at end OK) — **defaults to `unity/projects//**` only**, see below | `--grouped` (per-target breakdown), `--offset `, `--limit ` | -| `annotations create` | `` | `--text`, `--reply-to `, `--status Active\|Draft`, `--metadata k=v`…, `--target-context k=v`…, `--camera`, `--local-space`, `--time`, `--attachments`, `--unresolve-root-annotation` | +| `annotations create` | `` | `--text`, `--reply-to `, `--status Active\|Draft`, `--metadata k=v`…, `--target-context k=v`…, `--camera`, `--local`, `--time`, `--attachments`, `--unresolve-root-annotation` | | `annotations delete` | `` | (confirmation — see Shared behavior) | | `annotations delete-fields` | ` ` | removes metadata fields; variadic; confirmation | | `annotations export` | — | `--target ` — **defaults to `assets/projects//**` only**, see below; `--out ` (else stdout), `--resolve-users`; the service returns `assetId` + `assetName` here that `list` does not — the CLI copies the response page verbatim, so treat those as service behavior | @@ -223,32 +228,32 @@ Annotation objects returned by `get`/`list`/`replies` (`--format json`) carry: ` | `annotations replies` | `` | `--next`, `--limit 1-100`, `--all`, `--sort`, `--status-filter All\|Active\|Sending\|Draft` (repeat flag), `--fields a,b` or `--fields all` (table output only), `--full`, `--resolve-users` | | `annotations resolve` / `annotations unresolve` | `` | — (echoes only `annotationId`, see below) | | `annotations status` | ` ` | — | -| `annotations update` | `` | `--text`, `--metadata k=v`…, `--camera`, `--local-space`, `--time` — at least one required | +| `annotations update` | `` | `--text`, `--metadata k=v`…, `--camera`, `--local`, `--time` — at least one required | ```bash # Create a thread on an asset, with typed metadata (count is a number, build stays a string) unity collaboration annotations create "unity/projects/$PROJ/assets/$ASSET" \ - --project-id $PROJ --text "Texture seam visible here" \ + --cloud-project $PROJ --text "Texture seam visible here" \ --metadata severity=2 --metadata 'build="2024.1"' \ --camera '{"position":{"x":0,"y":1,"z":-5},"rotation":{"x":0,"y":0,"z":0}}' # Reply to it unity collaboration annotations create "unity/projects/$PROJ/assets/$ASSET" \ - --project-id $PROJ --reply-to $ANNOTATION_ID --text "Fixed in latest import" + --cloud-project $PROJ --reply-to $ANNOTATION_ID --text "Fixed in latest import" -# List root threads (default query) from inside a cloud-linked project — no --project-id needed +# List root threads (default query) from inside a cloud-linked project — no --cloud-project needed unity collaboration annotations list --include-fields replyCount,latestReply --format json # Custom query — must be a JSON ARRAY of clauses; the default is # [{"type":"hasNot","field":"annotationParentId"}] (root threads only) -unity collaboration annotations list --project-id $PROJ \ +unity collaboration annotations list --cloud-project $PROJ \ --query '[{"type":"hasNot","field":"annotationParentId"}]' --all --format ndjson # Export for offline analysis — ONE prefix tree per run; the bare command covers # only assets/**, so an Editor-annotated project needs both invocations. -unity collaboration annotations export --project-id $PROJ \ +unity collaboration annotations export --cloud-project $PROJ \ --target "assets/projects/$PROJ/**" --out annotations-assets.json -unity collaboration annotations export --project-id $PROJ \ +unity collaboration annotations export --cloud-project $PROJ \ --target "unity/projects/$PROJ/**" --out annotations-editor.json ``` @@ -297,7 +302,7 @@ resolved`), which is one way to confirm the first call landed. Attachments hang off an annotation. Three kinds: **file** (uploaded blob), **sketch** (2D drawing over a camera view), **spatial** (labeled 3D anchor) — payload shapes in -[Data model](#attachment-payload-shapes). All commands take `--project-id`. +[Data model](#attachment-payload-shapes). All commands take `--cloud-project`. | Command | Args | Key options | |---|---|---| @@ -314,15 +319,15 @@ over a camera view), **spatial** (labeled 3D anchor) — payload shapes in ```bash # Attach a screenshot (upload and `add file` are interchangeable) -unity collaboration attachments upload $ANNOTATION_ID ./screenshot.png --project-id $PROJ +unity collaboration attachments upload $ANNOTATION_ID ./screenshot.png --cloud-project $PROJ # Add a labeled 3D anchor -unity collaboration attachments add spatial $ANNOTATION_ID --project-id $PROJ \ +unity collaboration attachments add spatial $ANNOTATION_ID --cloud-project $PROJ \ --label "Broken collider" --position '{"x":1.2,"y":0,"z":3.4}' \ --camera '{"position":{"x":0,"y":2,"z":-4},"rotation":{"x":15,"y":0,"z":0}}' # Download; refuses to overwrite an existing file unless --force -unity collaboration attachments download $ANNOTATION_ID $ATTACHMENT_ID --project-id $PROJ \ +unity collaboration attachments download $ANNOTATION_ID $ATTACHMENT_ID --cloud-project $PROJ \ --out ./shot.png --force ``` @@ -333,14 +338,14 @@ Notes: - Options required on `add sketch`/`add spatial` become *individually* optional on the matching `update` variant — but every variant, `file` included, rejects a flagless invocation with a `NO_FIELDS` error. Pass at least one change flag. -- The spatial local-space flag is `--local` here, but `--local-space` on annotations (same JSON - shape). +- The local-space anchor is `--local` here and on annotations (same JSON shape). Annotations + still accept the old `--local-space`, with a deprecation warning. --- ### Reactions, thumbnails, read state -All use the same optional project resolution as `annotations` — `--project-id` or `--project-path`, +All use the same optional project resolution as `annotations` — `--cloud-project` or `--project-path`, else inferred from the current project (see [Shared behavior](#shared-behavior)). | Command | Args | Key options | @@ -352,15 +357,15 @@ else inferred from the current project (see [Shared behavior](#shared-behavior)) | `subscribe` / `unsubscribe` | `` | per-thread notification subscription for the current user | ```bash -unity collaboration reactions add $ANNOTATION_ID 👍 --project-id $PROJ -unity collaboration read $ANNOTATION_ID --project-id $PROJ # mark thread read as of now +unity collaboration reactions add $ANNOTATION_ID 👍 --cloud-project $PROJ +unity collaboration read $ANNOTATION_ID --cloud-project $PROJ # mark thread read as of now # Per-thread notification subscription for the current user -unity collaboration subscribe $ANNOTATION_ID --project-id $PROJ -unity collaboration unsubscribe $ANNOTATION_ID --project-id $PROJ +unity collaboration subscribe $ANNOTATION_ID --cloud-project $PROJ +unity collaboration unsubscribe $ANNOTATION_ID --cloud-project $PROJ # Thread thumbnails -unity collaboration thumbnail upload $ANNOTATION_ID ./shot.png --project-id $PROJ +unity collaboration thumbnail upload $ANNOTATION_ID ./shot.png --cloud-project $PROJ unity collaboration thumbnail download $ANNOTATION_ID --out ./thumb.png ``` @@ -371,10 +376,10 @@ unity collaboration thumbnail download $ANNOTATION_ID --out ./thumb.png Connects Collaboration annotations to Jira. Three layers, three id types — don't mix them up: 1. **Server config** (`serverConfigId`): a Jira server + credentials, scoped to a Unity - **organization** (`--organization-id`). + **organization** (`--cloud-org`). 2. **Project config** (`projectConfigId`, flag `--jira-project-config-id`): a Jira project (`--jira-project-id` — the Jira-side id) under a server config, linkable to Unity projects. -3. **Issues**: created from / linked to annotations, scoped by Unity `--project-id`. The resulting +3. **Issues**: created from / linked to annotations, scoped by Unity `--cloud-project`. The resulting link lands in `annotation.integrations.jiraIssues[]` — see [Jira integration payload](#jira-integration-payload) below. @@ -382,12 +387,21 @@ Connects Collaboration annotations to Jira. Three layers, three id types — don | Group | Scoping | |---|---| -| `jira server *` | `--organization-id`, required (rejected at parse time) | -| `jira project add` / `delete` / `update` | `--organization-id`, required (validated by the handler) | -| `jira project link` / `unlink` | Unity project id is a **positional** (` `); no `--organization-id` at all | -| `jira issues list` | `--organization-id`, required (rejected at parse time) | -| `jira configs` | exactly **one** of `--organization-id` or `--project-id`, **no `--project-path`** and no inference | -| `jira issues create/get/link/unlink/search/types` | `--project-id` / `--project-path`, or inferred — see [Shared behavior](#shared-behavior) | +| `jira server *` | an organization, required (rejected at parse time) | +| `jira project add` / `delete` / `update` | an organization, required (validated by the handler) | +| `jira project link` / `unlink` | Unity project id is a **positional** (` `); no `--cloud-org` at all | +| `jira issues list` | an organization, required (rejected at parse time) | +| `jira configs` | exactly **one** of `--cloud-org` or `--cloud-project` typed, **no `--project-path`** and no inference | +| `jira issues create/get/link/unlink/search/types` | `--cloud-project` / `--project-path`, or inferred — see [Shared behavior](#shared-behavior) | + +**The organization** of `jira server *`, `jira project add`/`delete`/`update` and `jira issues list` +is `--cloud-org `, else the `UNITY_CLOUD_ORG` env var, else the stored default organization +(`unity cloud org set-default`, see [auth-license-cloud.md](auth-license-cloud.md)). It takes an +organization id only, not a name. An empty or whitespace-only flag or variable counts as not set. +Only when none of the three gives one is it missing. +`--organization-id` is the deprecated spelling of `--cloud-org`: it still works, and warns. +`jira configs` is the exception: it reads its scope only from what you type, apart from +`UNITY_CLOUD_PROJECT` when you type neither option. **`--help` never tells you which options are required.** No collaboration option is annotated as required in help output, on any command — so the tables in this file are the only place that @@ -396,8 +410,8 @@ which exit code you get: | Enforcement | Commands | Behavior when omitted | |---|---|---| -| Parse time | `jira server add/delete/update/test/users/projects/permissions`, `jira issues create/get/search/types`, `jira issues list --organization-id`, `attachments add sketch` / `add spatial` required flags | usage error, **exit 2** | -| Handler | `jira project add/delete/update --organization-id`, `jira configs` | command failure (error envelope), not a usage error | +| Parse time | `jira server add/delete/update/test/users/projects/permissions`, `jira issues create/get/search/types`, `jira issues list --cloud-org`, `attachments add sketch` / `add spatial` required flags | usage error, **exit 2** | +| Handler | `jira project add/delete/update --cloud-org`, `jira configs` | command failure (error envelope), not a usage error | `jira project link` / `unlink` take positionals instead, so a missing id is always a parse-time usage error. @@ -406,13 +420,13 @@ usage error. | Command | Args | Key options | |---|---|---| -| `jira server add` | none | `--organization-id`, `--url`, `--username`, `--name`, and the API token through `--key-from-stdin` (preferred) or `--key`; all required | -| `jira server delete` | `` | `--organization-id` (required); confirmation | -| `jira server update` | `` | `--organization-id` (required) + at least one of `--url`/`--username`/`--key-from-stdin`/`--key`/`--name` | -| `jira server test` | none | `--organization-id`, `--url`, `--username`, and the API token through `--key-from-stdin` (preferred) or `--key`; all required; validates credentials **without persisting** | -| `jira server users` | `` | `--organization-id` (required), `--query ` — search Jira users | -| `jira server projects` | `` | `--organization-id` (required) — lists **Jira-side** projects on the server | -| `jira server permissions` | `` | `--organization-id`, `--jira-project-id` — both required; checks required Jira permissions | +| `jira server add` | none | `--cloud-org`, `--url`, `--username`, `--name`, and the API token through `--key-from-stdin` (preferred) or `--key`; all required | +| `jira server delete` | `` | `--cloud-org` (required); confirmation | +| `jira server update` | `` | `--cloud-org` (required) + at least one of `--url`/`--username`/`--key-from-stdin`/`--key`/`--name` | +| `jira server test` | none | `--cloud-org`, `--url`, `--username`, and the API token through `--key-from-stdin` (preferred) or `--key`; all required; validates credentials **without persisting** | +| `jira server users` | `` | `--cloud-org` (required), `--query ` — search Jira users | +| `jira server projects` | `` | `--cloud-org` (required) — lists **Jira-side** projects on the server | +| `jira server permissions` | `` | `--cloud-org`, `--jira-project-id` — both required; checks required Jira permissions | > **Warning: `--key` exposes the API token.** Its value is part of the command line, so other > users on the same host can read it in the process list, and CI runners that echo commands write @@ -426,44 +440,44 @@ usage error. | Command | Args | Key options | |---|---|---| -| `jira project add` | `` | `--organization-id`, `--jira-project-id`, `--default-reporter-id` — all required, though `--help` doesn't say so (fallback reporter when an annotation author has no Jira match) | -| `jira project delete` | `` | `--organization-id` (required, not marked in `--help`); confirmation | +| `jira project add` | `` | `--cloud-org`, `--jira-project-id`, `--default-reporter-id` — all required, though `--help` doesn't say so (fallback reporter when an annotation author has no Jira match) | +| `jira project delete` | `` | `--cloud-org` (required, not marked in `--help`); confirmation | | `jira project link` / `jira project unlink` | ` ` | — (Unity project id is positional here, not a flag) | -| `jira project update` | `` | `--organization-id` (required, not marked in `--help`), `--default-reporter-id`, `--linked-unity-project-id ` (repeatable — **replaces** the whole linked list), `--clear-linked-unity-projects` (mutually exclusive with the previous flag); at least one change flag required | +| `jira project update` | `` | `--cloud-org` (required, not marked in `--help`), `--default-reporter-id`, `--linked-unity-project-id ` (repeatable — **replaces** the whole linked list), `--clear-linked-unity-projects` (mutually exclusive with the previous flag); at least one change flag required | #### `jira issues` | Command | Args | Key options | |---|---|---| -| `jira issues create` | `` | `--jira-project-config-id`, `--summary`, `--type ` — required; `--project-id`/`--project-path` optional (inferred); `--description`, `--assignee-user-id`, `--reporter-user-id`, `--parent-issue-id` (sub-task) | -| `jira issues get` | `` | `--jira-project-config-id` required; `--project-id`/`--project-path` optional (inferred) | -| `jira issues link` / `jira issues unlink` | ` ` | `--project-id`/`--project-path` optional (inferred); `link` also takes optional `--jira-project-config-id`. `unlink` does **not** delete the issue in Jira | -| `jira issues list` | — | `--organization-id` **(required, org-scoped — no `--project-id`/`--project-path` here)**, `--profile all\|active\|resolved\|unresolved\|draft\|sending` (repeat flag), `--next `, `--limit 1-100` (default 10), `--all`, `--sort Ascending\|Descending` | -| `jira issues search` | — | `--jira-project-config-id` required; `--project-id`/`--project-path` optional (inferred); `--query ` (plain text, **not JQL**), `--include-subtasks` | -| `jira issues types` | — | `--jira-project-config-id` required; `--project-id`/`--project-path` optional (inferred); lists issue type ids for `create --type` | +| `jira issues create` | `` | `--jira-project-config-id`, `--summary`, `--type ` — required; `--cloud-project`/`--project-path` optional (inferred); `--description`, `--assignee-user-id`, `--reporter-user-id`, `--parent-issue-id` (sub-task) | +| `jira issues get` | `` | `--jira-project-config-id` required; `--cloud-project`/`--project-path` optional (inferred) | +| `jira issues link` / `jira issues unlink` | ` ` | `--cloud-project`/`--project-path` optional (inferred); `link` also takes optional `--jira-project-config-id`. `unlink` does **not** delete the issue in Jira | +| `jira issues list` | — | `--cloud-org` **(required, org-scoped — no `--cloud-project`/`--project-path` here)**, `--profile all\|active\|resolved\|unresolved\|draft\|sending` (repeat flag), `--next `, `--limit 1-100` (default 10), `--all`, `--sort Ascending\|Descending` | +| `jira issues search` | — | `--jira-project-config-id` required; `--cloud-project`/`--project-path` optional (inferred); `--query ` (plain text, **not JQL**), `--include-subtasks` | +| `jira issues types` | — | `--jira-project-config-id` required; `--cloud-project`/`--project-path` optional (inferred); lists issue type ids for `create --type` | #### `jira configs` -`unity collaboration jira configs` — single command. Pass **exactly one** of `--organization-id` (all -configs in the org) or `--project-id` (configs available to that Unity project). +`unity collaboration jira configs` — single command. Pass **exactly one** of `--cloud-org` (all +configs in the org) or `--cloud-project` (configs available to that Unity project). ```bash # One-time setup: validate credentials, persist server, add a Jira project, link Unity project. # The API token is piped on stdin, so it never appears in the process list or CI logs. -printf '%s' "$JIRA_TOKEN" | unity collaboration jira server test --organization-id "$ORG" \ +printf '%s' "$JIRA_TOKEN" | unity collaboration jira server test --cloud-org "$ORG" \ --url https://jira.example.com --username bot@example.com --key-from-stdin -printf '%s' "$JIRA_TOKEN" | unity collaboration jira server add --organization-id "$ORG" \ +printf '%s' "$JIRA_TOKEN" | unity collaboration jira server add --cloud-org "$ORG" \ --url https://jira.example.com --username bot@example.com --name "Main Jira" --key-from-stdin -unity collaboration jira project add $SERVER_CONFIG_ID --organization-id $ORG \ +unity collaboration jira project add $SERVER_CONFIG_ID --cloud-org $ORG \ --jira-project-id 10042 --default-reporter-id $JIRA_ACCOUNT_ID unity collaboration jira project link $PROJ $PROJECT_CONFIG_ID # Rotate the stored API token printf '%s' "$NEW_JIRA_TOKEN" | unity collaboration jira server update $SERVER_CONFIG_ID \ - --organization-id "$ORG" --key-from-stdin + --cloud-org "$ORG" --key-from-stdin # File an issue from an annotation (get valid type ids from `issues types` first) -unity collaboration jira issues create $ANNOTATION_ID --project-id $PROJ \ +unity collaboration jira issues create $ANNOTATION_ID --cloud-project $PROJ \ --jira-project-config-id $PROJECT_CONFIG_ID --summary "Texture seam" --type 10001 ``` diff --git a/skills/unity-cli/references/config-hub.md b/skills/unity-cli/references/config-hub.md index b5ddf5f..1571848 100644 --- a/skills/unity-cli/references/config-hub.md +++ b/skills/unity-cli/references/config-hub.md @@ -62,6 +62,16 @@ unity config update-check on # enable unity config update-check --json ``` +#### config path + +Print where the CLI keeps its configuration: the settings file every `config` key is stored in (`user-settings.json`, shared with the Hub) and the directory that holds it and the CLI's other config files. Works in every output format. + +```bash +unity config path # Config file: ... / Config directory: ... +unity config path --format tsv # file... and directory... rows +unity config path --json # {file, directory} +``` + #### config accelerator View or change the [Unity Accelerator](https://docs.unity3d.com/Manual/UnityAccelerator.html) (asset-import cache server) endpoint. Once persisted, `unity run`, `unity test` and `unity build` inject it into the Editor automatically — see [build-run-test.md](build-run-test.md). @@ -125,6 +135,7 @@ Recognized keys, and what they back onto: | `proxy.bypass` | `unity config proxy --bypass ` | Comma-separated hosts; writing/clearing it leaves the sibling `proxy` key untouched. | | `accelerator` | `unity config accelerator ` | Normalized to `host:port` on write. | | `update-check` | `unity config update-check on\|off` | Value is `on`/`off`. | +| `proxyRequestLogging` | `unity --log-proxy`'s persisted setting | Strict boolean `true`/`false`; the third tier of `--log-proxy`'s precedence (flag, then `UNITY_LOG_PROXY`, then this setting). | An unknown key, a read-only key (none exist yet — the mechanism exists for a future resolved-only value), or an invalid value for a writable key is rejected with exit **2** and a message pointing at `unity config list`. `--format json` returns `{key, value}` for `get`/`set`, `{key, cleared}` for `unset`, and `{entries: [{key, value, writable}, …]}` for `list`. @@ -184,7 +195,7 @@ Bootstrap Unity Hub on a clean machine from the command line. unity hub install # Install a specific Hub version -unity hub install --hub-version 3.17.0 +unity hub install --version 3.17.0 # Force reinstall even when Hub is already detected unity hub install --force @@ -199,11 +210,11 @@ unity hub install --architecture x64 unity hub install --skip-signature-check ``` -Options: `-f` / `--force`, `--headless` (silent installer, Windows only), `-a` / `--architecture x64|arm64` (env `UNITY_ARCHITECTURE`), `--hub-version ` (default `latest`, and a leading `v` as in `v3.17.0` is accepted), `--skip-signature-check`. +Options: `-f` / `--force`, `--headless` (silent installer, Windows only), `-a` / `--architecture x64|arm64` (env `UNITY_ARCHITECTURE`), `--version ` (default `latest`, and a leading `v` as in `v3.17.0` is accepted; `--hub-version` still works, but it is deprecated and prints a warning), `--skip-signature-check`. **Integrity & signature verification** — every download is checked against the SHA-512 from the HTTPS manifest, then the installer's **code signature** is verified before it runs with elevation: on macOS via `codesign` (signer `Developer ID Application: Unity Technologies`), on Windows via Authenticode (signer subject `Unity Technologies`), checked *before* the UAC prompt. Verification is **fail-closed** — if it fails or the verifier is unavailable, the command aborts with exit 6 and does not run the installer. Linux `.AppImage` has no standard verifier, so it is SHA-512-only. Pass `--skip-signature-check` to bypass (prints a warning; not recommended). -**`--hub-version` behaviour** — fetches the version-specific manifest from the CDN; if that version does not exist, the command exits with code 6 (no fallback to latest). +**`--version` behaviour** — fetches the version-specific manifest from the CDN; if that version does not exist, the command exits with code 6 (no fallback to latest). ```bash # JSON output diff --git a/skills/unity-cli/references/diagnostics-maintenance.md b/skills/unity-cli/references/diagnostics-maintenance.md index 2ff2d27..23b7f5d 100644 --- a/skills/unity-cli/references/diagnostics-maintenance.md +++ b/skills/unity-cli/references/diagnostics-maintenance.md @@ -32,6 +32,42 @@ The CLI writes its own `cli-log.json` (separate from the Hub's `info-log.json`) > Safe Mode and block the Pipeline connection — read `Editor.log` directly (see > [integration-advanced.md → Recovering from Safe Mode](integration-advanced.md#recovering-from-safe-mode-connection-fails-because-of-compile-errors) for its per-platform path and the full recovery loop). +### Logs --console: the running Editor's console + +```bash +# The last 20 console entries of the Editor this folder's project is open in +unity logs --console + +# Errors only, from a named project's Editor +unity logs --console --level error --project-path /path/to/project + +# Stream new console entries as the Editor logs them, one JSON object per line, until Ctrl-C +unity logs --console --follow --format ndjson +``` + +`--console` reads the Editor console through the Pipeline package (`console get`), so the Editor +must be running with the package loaded. `--project-path ` (or `UNITY_PROJECT_PATH`) picks the +Editor, and typing `--project-path` without `--console` is a usage error. `--level` maps onto the +console's three levels: `trace`, `debug` and `info` show everything, `warn` shows warnings and +errors, `error` and `fatal` show errors. `--tail 0` asks for every entry the console buffer holds. + +Each entry carries `seq`, `time` (UTC), `level` (`log`, `warn`, `error`), `logType` (Unity's own +`Log`, `Warning`, `Error`, `Assert` or `Exception`), `message`, `stackTrace`, `seeded` (read from the +Editor console's store rather than captured live) and `diagnostic`. `diagnostic` is `null` unless the +message's first line is a compiler diagnostic; then it holds `source` (`burst` for Burst's +`BCnnnn` errors, `compiler` for the C# compiler), `severity`, `code`, `file`, `line`, `column` and +`message`. In `tsv` the columns are `seq`, `time`, `level`, `logType`, `message` and the +`diagnostic.*` fields, after a header row. + +`--follow` resumes from the console's cursor, so no entry is written twice. It keeps running through +a script reload or an Editor restart, and stays on the first Editor it read. It says on stderr, once +per outage, that it lost the connection or what the Editor answered instead of entries. It also says +when the Editor restarted, or when entries were evicted from the buffer before it could read them. +Under `--format json` each of these is one `{"warning": "..."}` line on stderr. +Only a failed first read fails the command (exit `12` when no Editor is running for the project). +Ctrl-C ends it with exit `130`. A Pipeline package older than 0.7.0-exp.1 can't be followed (it +reports no console session), and the command says to update it with `unity pipeline upgrade`. + --- ### Doctor — system diagnostics @@ -310,52 +346,91 @@ unity changelog --target latest unity changelog --format json ``` +When no notes are published for that version, the command exits 6, and `--format json` reports `CHANGELOG_NOT_FOUND` in `errors[0].code`. A download that fails reports `ERR_NETWORK` (no connection) or `COMMAND_FAILED` (an HTTP error) instead. + --- ### Docs — version-matched Unity documentation -Open the documentation page for a class or topic in the default browser, matched -to the editor version of the project you are in: +Open the docs.unity.com page for a class or topic in the default browser, +matched to the editor version of the project you are in: ```bash +unity docs # the Engine documentation home unity docs GameObject # scripting API reference -unity docs --manual Coroutines # the manual instead +unity docs GameObject.activeSelf # a member page +unity docs --manual Coroutines # search the manual unity docs --search "physics raycast" # the documentation search results +unity docs --package com.unity.2d.sprite # a package manual, at the version your project uses unity docs --url Rigidbody.AddForce # print the URL, do not open it unity docs --cli use-unity-cli # the Unity CLI's own documentation unity docs --hub project-create # the Unity Hub documentation unity docs --cloud developer-data/collection-settings # Unity Cloud +unity docs --content GameObject # print the page's Markdown, do not open it ``` -The version is read from the current project's -`ProjectSettings/ProjectVersion.txt` — no editor has to be installed. Outside a -project, the current documentation is used. `--editor-version` overrides it and -accepts either a full version (`6000.0.26f1`) or a version branch (`6000.0`); an -unparseable value is a usage failure rather than a silent fallback to the wrong -version's docs. +The version is `--editor-version`, then the current project's +`ProjectSettings/ProjectVersion.txt` (no editor has to be installed), then the +newest version docs.unity.com hosts. `--editor-version` accepts a full version +(`6000.0.26f1`) or a version branch (`6000.0`); an unparseable value is a usage +failure rather than a silent fallback. A version the site doesn't host uses the +nearest hosted version below it, or the oldest hosted one, with a one-line +notice on stderr. + +A scripting API topic is looked up in the site's page index (the same one +`docs toc` lists): a qualified name (`UnityEngine.GameObject`) matches exactly, +a bare class name or `Class.member` matches the page whose path ends with it, +preferring `UnityEngine`, then `UnityEditor`, then the shortest path. A name two +pages tie on goes to search rather than a guess. The lookup takes at most 5 +seconds and never fails the command: offline, a qualified name is built +directly for the version you asked for, and anything else goes to search. A +trailing `.html`, a `(...)` signature and the old `Class-member` page form are +all accepted. `--manual` always searches, since the new Manual pages have no +mapping from the old page names. + +When the Editor's Documentation module is installed for the version `docs` +resolves (that exact Editor first, then the others on its line, newest first), +opening a scripting API topic in the browser opens the local page instead, a +`file://` URL with no network. `--online` skips the local copy. A printed +address (`--url`) is always the docs.unity.com page, so a citation never +carries a machine-local path. A topic the local copy doesn't have, a tie, or +no version at all goes online as before, and so does every run under WSL, +whose Windows browser can't read a Linux path. **For an agent, `--url` is the useful mode:** it prints the resolved address to stdout and opens nothing, so it composes into a citation or a review comment. -`--format json` carries the same address plus the `version` the page was matched -to (`null` when no project version was found), which is how a caller tells a -version-matched answer from a fallback one. - -A topic that cannot name a documentation page — a phrase, or anything with a -path separator — falls back to the documentation search for that section rather -than composing an address that would 404. A trailing `.html` is dropped, so a -page name pasted out of a browser address bar resolves. - -#### Documentation sets outside the Editor docs - -`--cli`, `--hub` and `--cloud` look the topic up on Unity's documentation -platform instead of the Editor documentation. These are separate bodies of -documentation rather than newer addresses for the same pages, so the two sites -answer different questions: +`--format json` carries the same address plus `version` (the docs version in the +URL, `null` for search or a home page), `requestedVersion` (the one asked for), +`source` (`online`, or `local` for a page opened from the Documentation module, +which also carries `path` and `editorVersion`) and `resolvedBy`: `index`, +`direct`, `search` or `local`. + +#### Package manuals: `docs --package` + +`unity docs --package ` opens a package's manual on docs.unity3d.com, +where package manuals live, for the package version your project uses. The +version is the one in the project's `Packages/packages-lock.json`; a package +built into the Editor (`com.unity.2d.sprite`, `com.unity.2d.tilemap`) uses its +one manual version; anything else uses the newest release on the Unity +registry (also for a package the project takes from a Git URL or a local +folder). A name the registry doesn't list fails with `DOCS_PACKAGE_UNKNOWN` +instead of opening a missing page, and a registry that +can't be reached fails with `NETWORK_UNREACHABLE`. `--package` takes no topic and +can't be combined with another documentation set, `--search` or +`--editor-version`. `--url` prints the address; `--format json` adds `package`, +`packageVersion` (the full version, `null` for a built-in package outside a +project) and `resolvedBy`: `project`, `editor` or `registry`. + +#### Documentation sets outside the Engine docs + +`--cli`, `--hub` and `--cloud` look the topic up in another documentation set +on docs.unity.com instead of the Engine documentation. With no topic, each opens +its set's home page: |Flag|Documentation| |---|---| -|*(default)*|Editor scripting API reference, version-matched| -|`--manual`|Editor manual, version-matched| +|*(default)*|Engine scripting API reference, version-matched| +|`--manual`|Engine manual, through search| |`--cli`|The Unity CLI| |`--hub`|The Unity Hub| |`--cloud`|Unity Cloud| @@ -370,6 +445,44 @@ one segment (`developer-data/collection-settings`). Anything else — a phrase, a name in the wrong case — falls back to the platform's search page, the same fallback rule the Editor sections use. +#### Reading a page as Markdown: `docs --content` + +`unity docs --content` prints the page's Markdown to stdout instead of +opening a browser, which an agent reads far better than HTML. With +`--format json` the Markdown is wrapped with `url`, `markdownUrl`, `version` +and `source` (`online`, or `local` for a package). It cannot be combined with +`--url`. A topic that only resolves to a search, and `--manual`, have no page +to print and fail. A package topic (`com.unity.*`) has no Markdown online yet: +it prints the package's local `Documentation~` Markdown from the project's +`Packages` or `Library/PackageCache`, or says plainly that none exists. + +#### Listing the pages: `docs toc` + +`unity docs toc` lists every page of a documentation section on docs.unity.com, +so an agent can pick a real page instead of guessing its name: + +```bash +unity docs toc --format json # scripting API reference +unity docs toc --manual --format json # the manual +unity docs toc --editor-version 6000.0 --format ndjson # one page per line +``` + +The version is `--editor-version`, then the project's `ProjectVersion.txt`, then +the newest version docs.unity.com hosts. A version the site doesn't host is +listed as the nearest hosted version below it, or the oldest hosted one if there +is none, with a one-line notice on stderr. + +Each entry has a `path` under the section (`unityengine/gameobject/activeself`) +and its absolute `url`. The pages come from the site's sitemaps, which carry no +titles. The json envelope's `data` is `{version, requestedVersion, section, +entries}`: `version` is the one listed, `requestedVersion` the one asked for +(null when none was). TSV and the human table show the path and URL. + +The index is downloaded once a day, then read from a local cache. No +connection is `NETWORK_UNREACHABLE` (exit 7); a server error is +`DOCS_TOC_FETCH_FAILED`, and an index that can't be read is `DOCS_TOC_INVALID`. +`--cli`, `--hub` and `--cloud` publish no index, so `toc` doesn't take them. + --- ### Language @@ -488,7 +601,7 @@ unity self-update --rollback Downloads go through the CLI’s regular download engine: an interrupted download (a dropped connection, a closed lid, a Ctrl-C) resumes from where it left off on the next `unity self-update`, and a Brotli-compressed artifact is preferred when the release publishes one — verified against its own checksum and again after decompression. A release with no compressed artifact for your platform falls back to the raw download. -`--check`, `--changelog`, and `--dry-run` work everywhere. The background "update available" notice is package-manager-aware: when the release manifest says your install's package manager already carries the new version, the notice suggests that manager's exact upgrade command instead of `unity self-update`; installs whose manager doesn't carry the release yet stay quiet. +`--check`, `--changelog`, and `--dry-run` work everywhere. When no notes are published for the version, `--changelog` exits 6, and `--format json` reports `CHANGELOG_NOT_FOUND` in `errors[0].code`. The background "update available" notice is package-manager-aware: when the release manifest says your install's package manager already carries the new version, the notice suggests that manager's exact upgrade command instead of `unity self-update`; installs whose manager doesn't carry the release yet stay quiet. The same background check also warns when a self-installed `unity` and a Homebrew-managed one are **both** on `PATH` — a state where `which unity` keeps resolving to the self-installed copy while `brew upgrade` updates the other, so the two silently drift apart. The notice points at `unity self-uninstall -y` to remove the self-installed copy so Homebrew is the only one managing updates going forward. diff --git a/skills/unity-cli/references/docs-workflow.md b/skills/unity-cli/references/docs-workflow.md new file mode 100644 index 0000000..80a4399 --- /dev/null +++ b/skills/unity-cli/references/docs-workflow.md @@ -0,0 +1,126 @@ +# Docs workflow: find, cite and match Unity documentation + +Part of the **`unity-cli`** skill. See that skill's `SKILL.md` for CLI install, global flags, environment +variables and exit codes. The full flag list for `unity docs` and `unity docs toc` is in +[diagnostics-maintenance.md](diagnostics-maintenance.md#docs--version-matched-unity-documentation); this file +is the order an agent follows, with 2D examples. + +**Why use it:** the pages `unity docs` resolves match the Editor version of the project you are in. An API +remembered from another Unity version is a common source of wrong code, 2D physics and Tilemap included. + +## The flow + +1. **Find the page with `docs toc`.** It lists real pages, so you never guess a name. +2. **Resolve the address with `docs --url`.** It prints the docs.unity.com address and opens nothing. +3. **Cite or open it.** Put the address in your answer or review comment. Without `--url`, `unity docs` + opens the page in the default browser (only useful when a person is watching). +4. **Pin the version** whenever the project is not the one you are standing in. + +### 1. Find a page: `docs toc` + +```bash +unity docs toc --format json # scripting API reference, this project's version +unity docs toc --manual --format json # the manual +unity docs toc --editor-version 6000.0 --format ndjson # one page per line, for a version branch +``` + +Each entry has a `path` (`unityengine/rigidbody2d/addforce`) and an absolute `url`. Under `--format json`, +`data` is `{version, requestedVersion, section, entries}`. `--format ndjson` writes one entry per line. The +default human and `--format tsv` output show the path and URL as two columns. The index carries no page +titles, only paths, so search the entries for the class or member name you want. + +Only the Engine scripting API and the manual have an index. `--cli`, `--hub` and `--cloud` publish none, so +`toc` doesn't take them. + +### 2. Resolve the address: `docs --url` + +```bash +unity docs --url Rigidbody2D # the class page +unity docs --url Rigidbody2D.AddForce # a member page +unity docs --url Tilemap.SetTile # another 2D member +unity docs --url UnityEngine.SpriteRenderer # a qualified name matches exactly +unity docs --url --manual "2D physics" # the manual is a search, so this returns a search address +``` + +A bare class name or `Class.member` matches the page whose path ends with it (preferring `UnityEngine`, then +`UnityEditor`). A name two pages tie on goes to search rather than a guess. The lookup never fails the +command: offline, a qualified name is built directly when a version is known (a project or +`--editor-version`), and anything else goes to search. + +Add `--format json` to see how the address was found: + +```bash +unity docs --url Rigidbody2D.AddForce --format json +``` + +`data` carries `url`, `opened` (false under `--url`), `version` (the docs version in the URL, `null` for a search or a home page), +`requestedVersion` (the one you asked for) and `resolvedBy`: `index` (an exact page from the index), +`direct` (built without the index) or `search` (a search page). When `resolvedBy` is `search`, the address is +a list of results, not the page. For a scripting API page, run `docs toc` and pick a path. The manual has +no name lookup, so `docs --url --manual` always returns search results: for a manual page, cite the `url` of +the `docs toc --manual` entry you picked. + +### 3. Other documentation sets + +`--cli`, `--hub` and `--cloud` look a topic up in the Unity CLI, Unity Hub or Unity Cloud documentation. Topic +names are lowercase and hyphenated (`use-unity-cli`, `project-create`, `developer-data/collection-settings`). +These sets are not versioned by Editor, so `--editor-version` with one of them is a usage failure, and so is +passing two sets at once. + +```bash +unity docs --url --cli use-unity-cli +unity docs --url --hub project-create +``` + +## Version flags + +The documentation version is, in order: + +1. `--editor-version `: a full version (`6000.0.26f1`) or a version branch (`6000.0`). An + unparseable value is a usage failure, not a silent fallback. +2. The current project's `ProjectSettings/ProjectVersion.txt`. No Editor has to be installed. +3. The newest version docs.unity.com hosts. + +A version the site doesn't host resolves to the nearest hosted version below it (or the oldest hosted one), +with a one-line notice on stderr. Read `version` against `requestedVersion` in the JSON when the difference +matters. + +Pin the version in two cases: you are not inside the project (a new 2D project you have yet to create, or a +question about another Editor), or the project upgrades soon and you are checking an API against the target +version. + +```bash +unity docs --url Rigidbody2D.AddForce --editor-version 6000.0 +unity docs toc --editor-version 2022.3 --format ndjson +``` + +## The page index cache + +`docs toc` and the index lookup behind `docs --url` download the page index once a day and read it from a +local cache after that, so repeat calls in a session are fast. With no connection and no fresh cache: + +- `docs toc` fails with `NETWORK_UNREACHABLE` (exit 7). A server error is `DOCS_TOC_FETCH_FAILED` and an + index that can't be read is `DOCS_TOC_INVALID`. +- `docs --url` does not fail. It builds a qualified name directly when a version is known (a project or + `--editor-version`) and sends anything else to search. + +The cache holds the page index only. It does not hold page content, so it does not make the documentation +readable offline. + +## Not available yet + +These are planned and not in the CLI yet. Check `unity docs --help` before +relying on any of them. + +- **Reading a page's content in the terminal** (`unity docs ... --content`). Until it ships, use + `docs --url` to get the address and open it, or ask the user for the text. +- **Offline use of page content**. +- **Package and local documentation** (`--package`). + +## Rules for agents + +- Resolve the address first and cite it. Don't quote an API from memory when a page for the project's + version exists. +- Prefer `--format json` and read `resolvedBy`, so you can tell a real page from a search. +- Don't open a browser from an unattended run: use `--url`. +- Only use `docs.unity.com` addresses the CLI printed. diff --git a/skills/unity-cli/references/editors-install.md b/skills/unity-cli/references/editors-install.md index 4e91770..4c93478 100644 --- a/skills/unity-cli/references/editors-install.md +++ b/skills/unity-cli/references/editors-install.md @@ -23,8 +23,8 @@ unity editors --releases --format json # Filter by architecture unity editors --installed --architecture arm64 --format json -# Show detailed module info -unity editors --verbose +# Show detailed module info (the old spelling, --verbose, still works and warns) +unity editors --long # Watch mode — live-updates as editors are installed or removed unity editors --watch @@ -33,6 +33,8 @@ unity editors --installed --watch `unity editors` honors `--format tsv` and `--format ndjson` for its default listing. Identifier columns keep their natural width even if the table exceeds the terminal — they are no longer silently truncated. +In the `--json` and `--format ndjson` rows of an installed Editor, read `moduleNames`, an array of the installed platforms (with `--long`, the full module names). The `modules` string joins the same names with `, ` and is deprecated: a module name can itself contain a comma. + #### editors running List the Unity Editor instances currently running and the project each has open, with the editor version and process id per instance: @@ -42,7 +44,7 @@ unity editors running unity editors running --format json ``` -Detection is cross-platform (process table plus each project's Pipeline lockfile), and the version falls back to a project's `ProjectSettings/ProjectVersion.txt` for editors without the Pipeline package. An empty list is a normal result (exit 0). Honors the global `--format human|json|tsv|ndjson` (and `--json`). +Detection is cross-platform (process table plus each project's Pipeline lockfile), and the version falls back to a project's `ProjectSettings/ProjectVersion.txt` for editors without the Pipeline package. An empty list is a normal result (exit 0). Honors the global `--format human|json|tsv|ndjson` (and `--json`). In JSON each row of `data.instances` carries `pid`, `projectPath`, `projectName`, `editorVersion` (`null` when unknown), `port`, `hasPipeline` and `reachable`. Read `editorVersion`: `unityVersion` holds the same value and is deprecated. #### editors add @@ -102,11 +104,15 @@ Also available as the top-level `unity install-path` (with an additional `--get` ```bash # Show release details for a specific version unity editors info 6000.0.47f1 --format json +# Details for a specific architecture's build (default: this machine's) +unity editors info 6000.0.47f1 --architecture x86_64 --format json ``` +`downloadUrl` and `modules` describe the build for this machine's architecture. When the release has no build for it, the default falls back to the platform's first download instead: on Linux arm64 that is the x86_64 installer, which runs only under FEX-Emu (see [Linux arm64](#linux-arm64-experimental-x86_64-editor-under-fex-emu)). `--architecture arm64|x86_64` selects a build explicitly, with no fallback; when the release has no build for it, `downloadUrl` is null. + #### editors upgrade -New in `0.1.0-beta.8`. Upgrade an installed editor to the newest official (f-channel) patch in the same `major.minor` line (e.g. `2022.3.10f1` → `2022.3.62f1`), carrying the installed modules over. The `[editor]` argument accepts an exact version, a `major.minor` line, or the `latest` / `lts` / `default` aliases. Editors install side by side — the old version is kept unless `--replace` (alias `--remove-old`) is passed. +New in `0.1.0-beta.8`. Upgrade an installed editor to the newest official (f-channel) patch in the same `major.minor` line (e.g. `2022.3.10f1` → `2022.3.62f1`), carrying the installed modules over (except the IDE modules, Visual Studio Code and Visual Studio, which update themselves; add `-m visualstudiocode` to install it anyway). The `[editor]` argument accepts an exact version, a `major.minor` line, or the `latest` / `lts` / `default` aliases. Editors install side by side — the old version is kept unless `--replace` (alias `--remove-old`) is passed. ```bash # Upgrade a specific editor (or the default / lts / latest) to the newest patch in its line @@ -213,6 +219,32 @@ unity editor add /path/to/Unity/Editor --no-fetch-modules --- +### Software Terms — `unity terms` + +Each Unity Editor stops at its "Unity Editor Software Terms" dialog (Accept and Continue / Decline) the first time it opens windowed, until the terms are accepted for that Editor. Current Editors ignore `-acceptSoftwareTermsForThisRunOnly`, so no launch flag prevents the dialog. `unity terms accept ` records the user's acceptance the way the dialog's Accept button does, so a container image or a provisioning script can prepare an Editor before it first opens. + + +```bash +# Show the terms link and the copy installed with the Editor, then ask (default No) +unity terms accept 6000.3.10f1 + +# Accept without asking (unattended setup, after the user has read and agreed to the terms) +unity terms accept 6000.3.10f1 --yes +unity terms accept latest --architecture arm64 --yes + +# Whether the terms are accepted, for every installed Editor or one +unity terms status +unity terms status 6000.3.10f1 --json +``` + +- **Accepting is the user's decision.** On a terminal `accept` prints the terms link (https://unity.com/legal/editor-terms-of-service/software) and the path of the terms file the Editor ships, then asks. Without a terminal (`--non-interactive`, `UNITY_NON_INTERACTIVE`, `--json` or another machine format, redirected input or output) it refuses with exit `2` and `TERMS_ACCEPT_NEEDS_CONFIRMATION` unless `--yes` is passed. Never pass `--yes` for the user unasked: show them the link and let them decide, or let them run the command. +- `` resolves like `unity editors path`: an installed version, `latest` or `lts`, and `-a` / `--architecture` when a version is installed for both. An Editor that is not installed fails with `TERMS_EDITOR_NOT_INSTALLED`, an ambiguous version with `TERMS_EDITOR_AMBIGUOUS`, and an Editor that ships no terms file with `TERMS_FILE_MISSING`. +- The acceptance belongs to the exact terms file: its key is `Terms-` and the file's MD5, so Editors that ship the same terms share one answer, and an Editor with new terms asks again. An answer that is already recorded is reported (`alreadyAccepted: true`) and nothing is written. +- It writes only the Editor's own preference, for the current user: `HKCU\Software\Unity Technologies\Unity Editor 5.x` on Windows, the `com.unity3d.UnityEditor5.x` defaults domain on macOS, `~/.local/share/unity3d/prefs` (or `$XDG_DATA_HOME/unity3d/prefs`) on Linux. Close every open Unity Editor first: on macOS and Linux a running Editor can overwrite the answer when it saves its own preferences. +- `terms status` lists each installed Editor as `accepted`, `not-accepted` or `no-terms-file`, in every `--format`; `--json` carries `key`, `termsFile` and the `store` location for each. + +--- + ### Install ```bash @@ -245,7 +277,7 @@ unity install 6000.0.47f1 --dry-run --format json # List the editor's available modules and exit without installing # (a drop-in alias for `unity modules list `; the old --list-components spelling -# still works as a hidden alias, matching the -m/--module terminology used everywhere else) +# still works but is deprecated and prints a warning, so write --list-modules) unity install 6000.0.47f1 --list-modules --format json # Space-separated module values after a single -m are equivalent to repeating -m @@ -266,7 +298,7 @@ A transient editor or module download failure (a dropped connection, a truncated On an interactive terminal, `unity install` also reports progress to the terminal application itself via the `OSC 9;4` escape sequence — on Windows Terminal the taskbar icon fills with download/install progress and spinners show as indeterminate, so you don't need to keep the window focused. It's emitted only on a TTY (never in piped or machine-consumed output), always cleared on exit, and ignored by terminals that don't support it. -Module installers honor the per-module install command from the release manifest (e.g. Visual Studio on Windows uses `--passive`, not `/S`); the resolved command is surfaced in `unity modules list --json`. `unity install` self-heals a corrupted partial download by discarding the bad partial and re-downloading; a cross-process install lock prevents two concurrent installs of the same version from corrupting the unpack. +Module installers honor the per-module install command from the release manifest (e.g. Visual Studio on Windows uses `--passive`, not `/S`); the resolved command is surfaced in `unity modules list --json`. In the `--json` and `--format ndjson` rows of `unity modules list`, `unity editors module list` and `unity install-modules --list`, read `downloadBytes`, `installedBytes` (integers) and `isInstalled` (boolean): `status`, `download` and `installed` are display text, and `status` is translated. `unity install` self-heals a corrupted partial download by discarding the bad partial and re-downloading; a cross-process install lock prevents two concurrent installs of the same version from corrupting the unpack. **Waiting on another install.** The Hub and the CLI share one install lock, so while the Hub (or another CLI) is installing, `unity install`, `unity install-modules` and `unity projects require` print `Waiting for another install to finish before continuing…` on stderr and carry on once it finishes. That wait has no limit by default. In a script or CI job, bound it: @@ -275,19 +307,61 @@ Module installers honor the per-module install command from the release manifest unity install 6000.0.47f1 --yes --no-wait # Wait up to 10 minutes in total, then fail the same way -unity install-modules -e 6000.0.47f1 -m android --wait-timeout 600 +unity install-modules -e 6000.0.47f1 -m android --timeout 600 ``` -`--wait-timeout ` takes a whole number of seconds greater than 0 (digits only: `5m` and `1.5` are rejected) and is one budget for the whole command, not per module; `--no-wait` wins if both are given. Either way the command exits **9** with error code `INSTALL_LOCK_BUSY` (in the json envelope's `errors[].code`, and in the ndjson `result` frame), distinct from a failed install's exit 6, so a script can retry later. The error says another install is in progress and to run the command again once it finishes; the "Waiting for another install" line on stderr only appears while a wait is running, the default one or a bounded one, and never under `--no-wait`. +`--timeout ` takes a whole number of seconds greater than 0 (digits only: `5m` and `1.5` are rejected) and is one budget for the whole command, not per module; `--no-wait` wins if both are given. CLI releases before it was renamed spell it `--wait-timeout`, which still works but prints a deprecation warning. Either way the command exits **9** with error code `INSTALL_LOCK_BUSY` (in the json envelope's `errors[].code`, and in the ndjson `result` frame), distinct from a failed install's exit 6, so a script can retry later. The error says another install is in progress and to run the command again once it finishes; the "Waiting for another install" line on stderr only appears while a wait is running, the default one or a bounded one, and never under `--no-wait`. The lock is taken per item, so exit 9 does not mean nothing changed. `--no-wait` refuses an item before its download only when the lock is already held; if the Hub takes it while the item downloads, the item fails after the download. And `unity install -m android --no-wait` can install the editor, then stop at the module: check `data.completedUids` in the ndjson `result` frame, and finish with `unity install-modules -e -m android` rather than re-running `unity install`, which now fails because the editor is already installed. A second `unity install` of the **same** version fails at once with exit 6 ("Another install of … is already in progress") before it reaches this lock, whatever the flags. +#### Another operating system's Editor (`--operating-system`) + +`unity releases` and `unity install --dry-run` look up the host's OS by default. `--operating-system ` looks up another one, so you can check what a Windows build machine would download from a Mac. It has no short flag. + +```bash +# Windows releases, from any host +unity releases --operating-system windows --limit 5 --format json + +# What a Windows install of this version and its modules would download (il2cpp resolves to windows-il2cpp) +unity install 6000.0.47f1 --dry-run --operating-system windows -m il2cpp --format json +``` + +Another OS's listing asks for the builds that OS ships and prefers its usual one: arm64 on macOS, x86_64 on Windows and Linux. Pass `--architecture arm64` to `install` for a Windows arm64 build. Its `installed` column is always false, because this machine can't hold another OS's Editor. The dry run's `data.editor` gains `"operatingSystem": "windows"` and `alreadyInstalled` is always false; the human plan adds an `Operating system: Windows` line. Naming the host's own OS changes nothing. + +The CLI never runs another OS's installer. Without `--dry-run`, or with `--changeset` or `--resume`, it stops with exit 2 and `CROSS_OS_INSTALL_UNSUPPORTED`, naming the dry run and `unity releases`. An unknown value is exit 2 with `INVALID_OPERATING_SYSTEM`. `--list-modules` lists an installed Editor's modules, so it can't be combined with `--operating-system` (exit 2); `unity modules list` and `unity editors --releases` stay on the host's OS. + #### Linux arm64: experimental x86_64 Editor under FEX-Emu Unity ships no Linux arm64 Editor. On a Linux arm64 host you can opt in to running the x86_64 Editor under [FEX-Emu](https://github.com/FEX-Emu/FEX) by setting `UNITY_EXPERIMENTAL_FEX_EMU=1`. The opt-in takes effect only when a FEX-Emu entry point (`FEX`, or the older `FEXLoader`) is on PATH and an x86_64 RootFS resolves (`FEX_ROOTFS`, or the `RootFS` in FEX-Emu's own `Config.json`); `unity doctor` reports both as its `fex-emu` row. While it is active, `unity install` defaults to `x86_64`, and every Editor launch (`open`, `projects open`, `run`, `test`, `build`, `projects create`) runs as `FEX ` with no `binfmt_misc` needed. This is **experimental and unsupported**: C# script compilation is known to crash under FEX-Emu ([FEX-Emu/FEX#5766](https://github.com/FEX-Emu/FEX/issues/5766)), so don't use it for real development. Every command it affects says so: a warning on stderr for human and tsv output, and a `FEX_EMU_EXPERIMENTAL` entry in the envelope's `notifications` for json and ndjson. Without the opt-in nothing changes. +#### Portable install into a folder (`--download-directory`) + +`-d, --download-directory ` (the old `--install-dir` spelling still works, but it is deprecated and prints a warning) installs a self-contained Editor into a folder you name, for an ephemeral CI workspace or a side-by-side copy. The Editor lands in `/` (or `/-` when that slot is taken), with its own `modules.json` and `metadata.hub.json`. Nothing else changes: the global install path (`unity install-path`), the Hub's Editor list and the Linux desktop entry are left alone, so `unity editors` doesn't list it. A folder outside the Hub's install folder doesn't touch the shared install-state database either. A relative path resolves against the working directory. + +```bash +# CI: install into the workspace, no prompts, machine-readable result +unity install 6000.0.47f1 -d ./.unity -m android --non-interactive --yes --accept-eula --format json + +# See where it would go first +unity install 6000.0.47f1 -d ./.unity --dry-run --format json + +# Add a module later: point install-modules at the same folder +unity install-modules -e 6000.0.47f1 -d ./.unity -m ios --yes + +# Register it with the Hub afterwards, if you want it listed (Linux path shown; +# a human install run prints the exact command for your platform) +unity editors add ./.unity/6000.0.47f1/Editor/Unity +``` + +- The json/ndjson result carries the Editor folder as `data.path` (the dry run too). A human run prints the folder and the `unity editors add` command that registers it. +- Only the target folder is checked for an existing copy: a second install there fails with exit 6 unless you pass `--force`. +- Installs into different folders run at once: each folder has its own lock, and none of them waits on the Hub's install lock. Two installs of one version into the **same** folder still refuse each other. +- A `-d` that names the Hub's own install folder (or any folder, when the install-path config can't be read) locks like a normal install: it waits on the Hub's install lock and uses the shared install-state database. +- `--resume` can't be combined with it (exit 2): there is no per-folder download state to resume. Re-run the install instead. +- `--list-modules` can't be combined with it either (a usage error, exit 2). + ### Uninstall ```bash @@ -346,6 +420,9 @@ unity install-modules --editor-version 6000.0.47f1 --module android --retries 0 # Windows: skip the elevated (UAC) install helper (also via UNITY_NO_ELEVATE=1) unity install-modules --editor-version 6000.0.47f1 --module android --no-elevate + +# An Editor installed with `unity install --download-directory`: look it up in that folder +unity install-modules --editor-version 6000.0.47f1 --download-directory ./.unity --module android ``` `--list` and `--all` are mutually exclusive. `--list` is also mutually exclusive with `--module`. diff --git a/skills/unity-cli/references/integration-advanced.md b/skills/unity-cli/references/integration-advanced.md index f47995c..79007be 100644 --- a/skills/unity-cli/references/integration-advanced.md +++ b/skills/unity-cli/references/integration-advanced.md @@ -11,14 +11,15 @@ environment variables, exit codes, and common workflows. All global flags (`--fo `unity command` (and its subcommands), `unity list`, `unity job`, and `unity mcp` share one target resolver, which reads its selectors in this order: 1. `--runtime `, then `--runtime-path ` — these target a running **Unity Player build**, not an Editor, and are read **before** `--project-path`. Supply a runtime selector and `--project-path` together and the runtime wins, so pass only the one you mean. -2. `--project-path ` — the Editor selector. -3. Otherwise, the running Editor whose project directory **contains the current working directory**. With a project nested inside another, the deepest match wins. +2. `--port `, `--pid `, `--instance ` — pin one running Editor: the port its Pipeline server listens on, its process id, or its Multiplayer Play Mode instance name or `vpId`. Every pin you pass, and `--project-path` beside them, must match the same Editor. A pin skips steps 3 and 4 below, so it reaches a clone while its main Editor is open. A pin that matches no running Editor fails with `EDITOR_NOT_RUNNING` (exit 12), lists the running Editors with their port and pid, and carries them as `data.candidates`. A pin beside `--runtime` or `--runtime-path` is a usage error (exit 2). With a pin, `UNITY_PROJECT_PATH` is ignored. +3. `--project-path ` — the Editor selector. With the flag absent, `UNITY_PROJECT_PATH` stands in for it. +4. Otherwise, the running Editor whose project directory **contains the current working directory**. With a project nested inside another, the deepest match wins. -**Pass `--project-path` whenever more than one Editor may be running.** Relying on step 3 means the target depends on the shell's cwd, which is rarely what an agent intends and is invisible in the command it ran. +**Pass `--project-path` whenever more than one Editor may be running.** Relying on step 4 means the target depends on the shell's cwd, which is rarely what an agent intends and is invisible in the command it ran. An agent working in its own worktree can instead prefix each call with `UNITY_PROJECT_PATH=`. An `export` only lasts as long as the shell, and most agent tools start a fresh shell for each command. `unity status` honors the variable too, but `unity recompile` does not: pass `--project-path` to it. > `unity pipeline install` and `unity pipeline upgrade` take `--project-path` too, but they do **not** use this resolver — they pick among the editors that actually need the operation, showing an interactive selector on a terminal and a different, candidate-listing error without `data.candidates` otherwise. Everything below describes the shared resolver only. -When step 3 selects nothing — the cwd is inside none of the running projects, or two candidates tie — the CLI does **not** guess. It fails with code `AMBIGUOUS_EDITOR` (exit 6), lists the candidates, and names the flag: +When step 4 selects nothing — the cwd is inside none of the running projects, or two candidates tie — the CLI does **not** guess. It fails with code `AMBIGUOUS_EDITOR` (exit 6), lists the candidates, and names the flag: ``` Multiple Unity Editors are running with Pipeline servers: @@ -26,8 +27,9 @@ Multiple Unity Editors are running with Pipeline servers: 1. Alpha (localhost:38412) - /path/to/Alpha 2. Beta (localhost:38413) - /path/to/Beta -Pass `--project-path ` with one of the project paths listed above to choose one, or run the -command from inside one of those project directories. +Pass `--project-path ` with one of the project paths listed above, or `--port `, +`--pid ` or `--instance ` to pick one Editor, or run the command from inside one of +those project directories. ``` Under `--format json` / `--format ndjson` the same candidates ride the failure envelope as `data.candidates`, so a script can pick one without parsing the human text — the same shape `unity auth switch` uses for an ambiguous account: @@ -45,7 +47,7 @@ Under `--format json` / `--format ndjson` the same candidates ride the failure e } ``` -`unity status --format json` reports the same project paths for every registered Editor (`data.instances[].project`); either source gives you a value to pass straight back as `--project-path`. +`unity status --format json` reports the same project paths for every registered Editor (`data.instances[].projectPath`); either source gives you a value to pass straight back as `--project-path`. ### Multiplayer Play Mode virtual players @@ -60,9 +62,9 @@ A project running MPPM opens one Editor per virtual player, each with its own Pi All four are present only when the installed `com.unity.pipeline` publishes them; a bundle that does not simply omits them. They are carried in `human`, `json`, and `ndjson` — the `tsv` columns are unchanged. -**The resolver does not treat a clone as a separate candidate.** Step 3 above collapses a project's own clones into its main Editor, so a project running virtual players resolves the way it did before MPPM was visible rather than failing `AMBIGUOUS_EDITOR`. Two consequences worth knowing: +**The resolver does not treat a clone as a separate candidate.** Step 4 above collapses a project's own clones into its main Editor, so a project running virtual players resolves the way it did before MPPM was visible rather than failing `AMBIGUOUS_EDITOR`. Two consequences worth knowing: -- To drive a specific player, pass its clone directory to `--project-path` — take `projectPath` for the row whose `instanceName` is that player. That reads the clone's descriptor directly and bypasses the collapse. +- To drive a specific player, pass `--instance ` (or its `vpId`), as in `unity command editor status --instance "Player 2"`. Passing its clone directory to `--project-path` works too: take `projectPath` for the row whose `instanceName` is that player. Both bypass the collapse. - A clone whose main Editor is **not** reachable is still offered as a candidate, since it is then the only way to reach that project. ### MCP — Model Context Protocol server (AI agent integration) @@ -75,11 +77,20 @@ unity mcp # Pin the server to a specific Unity project (the CLI discovers the running Editor itself) unity mcp --project-path /path/to/MyProject + +# Same, from the environment (the flag wins when both are set) +UNITY_PROJECT_PATH=/path/to/MyProject unity mcp + +# Expose only some tools, or hide some +unity mcp --tools "script.eval,capture.*" +unity mcp --exclude-tools "exec,script.eval,script.eval_file,test.run" ``` +`--tools ` keeps only the tools whose names match the comma-separated list, and `--exclude-tools ` then hides the ones that match its list. In both, `*` matches any run of characters, and names are case-sensitive. The Editor lists grouped command names such as `script eval` and `test run`. A pattern can't hold a space, so write the group and the name joined by a dot (`script.eval`, `test.run`). A pattern matches a command under every spelling it answers to: the grouped name, its dotted form, and the old flat name an older package uses (`eval`, `run_tests`). Excluding `script.eval` or `eval` therefore hides the same tool, and a call under either name is refused. A smaller tool list saves the client's context. The filter also applies to the tools a later `tools/list_changed` notification announces. Calling a hidden tool fails with an error that names the tools the session exposes, and the call never reaches the Editor. An empty entry, or a pattern with a character other than letters, digits, `_`, `-`, `.`, and `*`, is a usage error. + `unity mcp` no longer accepts `--instance `: talking to an Editor requires that Editor's per-instance auth token, which a bare host and port can't carry, so the CLI always discovers running Editors itself — run from the project directory or pass `--project-path` to target one. Editors launched to create a new project (`-createproject`) are discovered too. -The `capture_game_view` / `capture_scene_view` tools fall back to an OS-level screenshot of the whole desktop when the Editor’s main thread does not respond in time (a modal dialog, for example); the result says so in a note, since it captures the screen rather than the specific view. The server also declares support for `tools/list_changed` and notifies the client when the tool catalog changes, so a session started before any Editor was running picks up the Editor’s tools without a restart. +When the Editor cannot be reached or does not respond in time (a modal dialog, for example), the `capture_game_view` / `capture_scene_view` tools return an error saying the Editor could not be captured. They take no screenshot of the desktop unless `UNITY_MCP_DESKTOP_FALLBACK=1` is set in the `unity mcp` server's environment; with it set, they fall back to an OS-level screenshot of every display, and the result says so in a note, since it captures the screen rather than the specific view. The server also declares support for `tools/list_changed` and notifies the client when the tool catalog changes, so a session started before any Editor was running picks up the Editor’s tools without a restart. Until then, the list holds one `unity-cli-status` tool. Its description says why the Editor's tools are missing, in the words `unity command` uses (no Editor open for the project, for example). Calling it checks again. `--tools` and `--exclude-tools` don't hide it. #### mcp configure — register the server in an AI client @@ -96,7 +107,7 @@ unity mcp configure claude-code # Project-local config for clients that support it (cursor, vscode, vscode-insiders, kiro, codex) unity mcp configure cursor --local -# Pin to a project; skip the "already exists, update?" prompt; preview without writing +# Pin to a project (configure ignores UNITY_PROJECT_PATH); skip the "already exists, update?" prompt; preview without writing unity mcp configure claude --project-path /path/to/MyProject unity mcp configure vscode --yes unity mcp configure vscode --dry-run @@ -111,7 +122,7 @@ unity mcp configure claude --server ### Setup — install the Unity plugin into Claude Code -`unity setup claude` installs Unity's Claude Code plugin, which carries the Unity skills (this one included), in one step. It runs the two `claude` commands for you — `claude plugin marketplace add Unity-Technologies/unity-agent-plugin` and then `claude plugin install unity@unity-agent-plugin`, both at user scope — and reports what you now have. It changes the user's Claude Code configuration, so run it only when the user asks for the Unity plugin, or after they agree to install it; don't run it on your own initiative. +`unity setup claude` installs Unity's Claude Code plugin, which carries the Unity skills (this one included), in one step. It runs the two `claude` commands for you — `claude plugin marketplace add anthropics/claude-plugins-official` (Anthropic's official marketplace, which Claude Code usually has already) and then `claude plugin install unity@claude-plugins-official`, both at user scope — and reports what you now have. A plugin installed earlier from Unity's own marketplace (`unity@unity-agent-plugin`) is the same plugin, so it's reported as installed under that id. It changes the user's Claude Code configuration, so run it only when the user asks for the Unity plugin, or after they agree to install it; don't run it on your own initiative. ```bash # Install the plugin (safe to re-run: an existing install is reported, not reinstalled) @@ -132,6 +143,34 @@ The json `data` carries `pluginId`, `version`, `scope`, `alreadyInstalled`, `tak Because the plugin already carries the `unity-cli` skill, don't also run `unity skill install claude-code` — Claude Code would load two copies. This command never writes a skill copy or an MCP registration of its own. +### Setup — install the Unity plugin into Codex or Grok Build + +`unity setup codex` and `unity setup grok` do the same for Codex and Grok Build, with the same rules: run one only when the user asks for the Unity plugin in that agent, or after they agree to install it. + +- `unity setup codex` runs `codex plugin marketplace add Unity-Technologies/unity-agent-plugin`, then `codex plugin add unity@unity-agent-plugin`. It reads `CODEX_HOME` when set (default `~/.codex`). A plugin disabled in Codex (`enabled = false`) counts as not installed, so this adds it again. If `codex` isn't on your PATH it fails with `CODEX_NOT_FOUND`; a failed or timed-out step fails with `CODEX_PLUGIN_INSTALL_FAILED`. +- `unity setup grok` runs `grok plugin install Unity-Technologies/unity-agent-plugin --trust`. `--trust` is Grok Build's own consent flag for a non-interactive install; running this command is that consent. It reads `GROK_HOME` when set (default `~/.grok`). If `grok` isn't on your PATH it fails with `GROK_NOT_FOUND` and points at `/marketplace` inside Grok Build; a failed or timed-out step fails with `GROK_PLUGIN_INSTALL_FAILED`. + +```bash +unity setup codex --dry-run +unity setup grok --format json +``` + +Both take `--dry-run` and every output format, and report the same json `data` as `setup claude`, except `reloadCommand` is `null`: neither agent has a reload command, so a fresh install loads in the next session (`takesEffect: "nextSession"`). The plugin carries the `unity-cli` skill, so don't also run `unity skill install codex` (or the Grok equivalent). + +### Setup — install the Unity plugin into Antigravity + +`unity setup antigravity` does the same for Google Antigravity, with the same rules: run it only when the user asks for the Unity plugin in Antigravity, or after they agree to install it. + +- It runs `agy plugin install https://github.com/Unity-Technologies/unity-agent-plugin`. +- It reads an existing install from the `plugin.json` in `~/.gemini/config/plugins/unity/`, or in `~/.gemini/antigravity-cli/plugins/unity/` where older `agy` releases put it. In either folder the manifest may sit at the top or under `.antigravity-plugin/`. `agy` records no install time, so `installedAt` is always `null`. +- If `agy` isn't on your PATH it fails with `ANTIGRAVITY_NOT_FOUND` and points at **Customizations > Plugins** in the Antigravity app; a failed or timed-out step fails with `ANTIGRAVITY_PLUGIN_INSTALL_FAILED`. + +```bash +unity setup antigravity --dry-run +``` + +It takes `--dry-run` and every output format, reports the same json `data` as `setup codex`, and a fresh install loads in the next Antigravity session. + --- ### Skill — install this skill into an AI client @@ -155,6 +194,8 @@ unity skill install codex --dry-run Supported clients: `claude-code`, `claude-desktop`, `grok`, `cursor`, `windsurf`, `vscode`, `cline`, `codex`. Each is written in the format that client expects, at its platform-correct location. Not every client supports both scopes — some are user-global only, others project-local only — and `--list` reports which, so check there rather than guessing. +Without `--yes`, a run with no terminal (or with `--non-interactive`) cannot ask, so it skips an outdated copy, and a client whose plugin already provides the skill, and still exits `0`. With `--format json`, the run writes one envelope whose `data` holds a record per skill it considered, and each skipped skill's record carries `skipped: true` and a `skipReason` (`outdated`, `plugin-provided` or `foreign-tree`), so a script can tell a skip from an install. Pass `--yes` to overwrite. + A `--local` install also picks up the skill the project's `com.unity.pipeline` package ships (`.claude/skills/unity-pipeline/` inside the package) and mirrors it beside `unity-cli` — e.g. into `.claude/skills/unity-pipeline/` for `claude-code`. A resolved package lives under `Library/PackageCache`, which no client's skill discovery reads, so this mirror is what makes the package's own skill loadable; a project without the package installs `unity-cli` alone. `unity skill refresh` re-reads the mirrored copy from the project's package, and reports rather than deletes when the package is gone. The package skill never installs user-globally — it versions with the project's own package. `codex` installs a real skill directory (`~/.agents/skills/unity-cli`, or `.agents/skills/unity-cli` with `--local`), which is where Codex looks for skills. Earlier CLI versions instead merged the whole skill into a shared `AGENTS.md`, which Codex reads at the start of every session, so the entire skill was charged to sessions that had nothing to do with Unity. Installing or refreshing now removes that leftover block and reports the file it cleaned. If it finds more than one such block it leaves the file alone and says so, rather than guessing which block is Unity's. @@ -275,7 +316,7 @@ unity plugin changelog ugs --version 2.1.0 unity plugin changelog plastic --format json # { id, name, version, summary, notes } ``` -Rendered from markdown and paged like `unity changelog`. Unlike `plugin upgrade --changelog` (where missing notes is never a failure), `plugin changelog` on its own fails (exit 6) when the component has no publisher-hosted changelog at all or no notes for the requested version — you asked specifically to read them, so nothing to show is reported rather than silently swallowed. `--version` must be an exact, canonical semver value (`1.2.3`, not `v1.2.3`); an invalid value is a usage error (exit 2). +Rendered from markdown and paged like `unity changelog`. Unlike `plugin upgrade --changelog` (where missing notes is never a failure), `plugin changelog` on its own fails (exit 6) when the component has no publisher-hosted changelog at all or no notes for the requested version — you asked specifically to read them, so nothing to show is reported rather than silently swallowed. With `--format json` that failure carries `CHANGELOG_NOT_FOUND` in `errors[0].code`. `--version` must be an exact, canonical semver value (`1.2.3`, not `v1.2.3`); an invalid value is a usage error (exit 2). --- @@ -306,31 +347,32 @@ unity pipeline install --project-path /path/to/MyProject # macOS: the `location` is the .app bundle; the executable is inside it. (Linux: /Editor/Unity) UNITY=/Applications/Unity/Hub/Editor/6000.3.11f1/Unity.app/Contents/MacOS/Unity "$UNITY" -batchmode -projectPath /path/to/MyProject -logFile editor.log & # NO -quit → stays resident -# Drive it — target the project explicitly (see the status caveat): +# Wait for it, then drive it — target the project explicitly: +unity status --until-ready --project-path /path/to/MyProject unity command --project-path /path/to/MyProject # list what it exposes unity list --project-path /path/to/MyProject # discover tools -unity command eval "return Application.unityVersion;" --project-path /path/to/MyProject +unity command script eval "return Application.unityVersion;" --project-path /path/to/MyProject ``` -> **`unity status` caveat (verified):** a batch-mode Editor launched this way *does* serve commands, -> but is **not** listed by `unity status` (its lockfile heartbeat differs from a GUI Editor's). Confirm -> reachability with `unity command`/`unity list --project-path `, not `unity status`. +> **`unity status` sees a batch-mode Editor.** Wait for it with `unity status --until-ready --project-path +> /path/to/MyProject`, then drive it. Until it connects, `unity command --query` lists only the CLI's own matches, +> with exit 0 and a line saying Pipeline commands were not searched; that is not proof the project has no commands. **Warm / interactive.** Use an Editor you already have open, or `unity open ` (GUI, stays -resident). Unlike the batch case, its Pipeline server *does* register with `unity status` (state +resident). Like the batch case, its Pipeline server registers with `unity status` (state `ready`), so `unity status` gates readiness. Drive it the same way (the CLI auto-discovers it; pass `--project-path` to disambiguate when several are open — see [Targeting one of several running Editors](#targeting-one-of-several-running-editors)). ```bash unity open /path/to/MyProject -unity status --until-ready --project-path MyProject --format json # blocks until state "ready" (exit 6 on timeout) -unity command eval "return Application.unityVersion;" +unity status --until-ready --project-path /path/to/MyProject --format json # blocks until state "ready" (exit 6 on timeout) +unity command script eval "return Application.unityVersion;" ``` Run `unity pipeline install` **before** `unity open` when the project doesn't have the package -yet; for a project you are creating, `unity projects create --with-pipeline` does the same -step as part of creation. An Editor that is already open loads a newly added package only when it next refreshes, +yet; a project made with `unity projects create ` already has it (unless created with +`--bare`). An Editor that is already open loads a newly added package only when it next refreshes, typically when its window regains focus. Until then no Pipeline server is running, and `unity status` reports `STATUS_PIPELINE_LOAD_PENDING`: an Editor holds the project open and its manifest lists `com.unity.pipeline`, but nothing is serving. An Editor that is still opening or @@ -368,8 +410,8 @@ unity pipeline install --project-path /path/to/MyProject unity pipeline install --force # always rewrite the manifest to the latest version # Install a specific version (validated against the registry first; overwrites any pinned version). -# NOTE: the flag is --package-version, NOT --version (which collides with the global -V, --version). -unity pipeline install --package-version 0.3.0-exp.1 +# --package-version still works, but it is deprecated and prints a warning naming --version. +unity pipeline install --version 0.3.0-exp.1 # Upgrade the package to the latest, but only when the registry has a newer one # (otherwise reports it's already up to date and leaves manifest.json untouched). @@ -381,49 +423,56 @@ unity pipeline upgrade --project-path /path/to/MyProject unity pipeline list-versions --format json ``` -`pipeline install` options: `--project-path `, `--force`, `--package-version `. The package is resolved from the Unity registry and written to `Packages/manifest.json`. Unlike `pipeline install --force` (which always rewrites to latest), `upgrade` compares the pinned version first. +`pipeline install` options: `--project-path `, `--force`, `--version `. The package is resolved from the Unity registry and written to `Packages/manifest.json`. Unlike `pipeline install --force` (which always rewrites to latest), `upgrade` compares the pinned version first. When multiple Editors are running, `install` and `upgrade` consider only the editors that actually need the operation (`install` → editors without the package; `upgrade` → editors behind the registry's latest). If exactly one needs it, that editor is chosen automatically; if none do, the command reports there's nothing to do; if several do, an interactive terminal shows a selector while non-interactive contexts (machine output, non-TTY, or `--non-interactive`) error and list the projects so you can pass `--project-path`. #### command (aliases: cmd, request) — send commands to a running Unity Editor -Forwards a command to a connected Editor. Run it with no arguments to list the tags the Editor groups its commands under, then drill into one with `--tag`. The tags come from the Editor, so a package that adds commands is reflected without a CLI update. +Forwards a command to a connected Editor. Run it with no arguments to list the command groups the Editor organises its commands in (`animation`, `animation.animator`, `build.target`, ...), then list one group's commands by naming it. The groups come from the Editor, so a package that adds commands is reflected without a CLI update. An Editor whose Pipeline package predates the groups lists its tags instead, and one that predates the tags lists its whole catalog. ```bash -# List the available tags, with a count of the commands under each +# List the available command groups, with a count of the commands under each +# (a parent group's count covers its subgroups too) unity command unity command --format json +# List the commands in one group +unity command animation +unity command build.target + # List the commands carrying one tag unity command --tag runtime # The whole catalog in one go, as earlier versions listed it unity command --detail full -# Execute a specific command (names/params come from the Editor) -unity command editor_play -unity command log_editor "Hello from CLI" -unity command editor_status --includeMemory true +# Execute a specific command, named by its group and its name (names/params come from the Editor) +unity command editor.playmode start +unity command console write "Hello from CLI" +unity command editor status --includeMemory true -# Capture a Scene/Game view screenshot (forwarded to the Editor's screenshot command, new in 0.1.0-beta.8) -unity command screenshot --output ./shot.png --width 1920 --height 1080 +# Capture a Scene/Game view screenshot (forwarded to the Editor's capture screenshot command, new in 0.1.0-beta.8) +unity command capture screenshot --output ./shot.png --width 1920 --height 1080 # Target a specific project (the CLI discovers the running Editor itself) or a Player runtime -unity command editor_play --project-path /path/to/MyProject +unity command editor.playmode start --project-path /path/to/MyProject unity command --runtime "MyGame" unity command --runtime-path /path/to/port-file # Set a timeout (default: 30 seconds) -unity command editor_play --timeout 60 +unity command editor.playmode start --timeout 60 # Only the Editor’s own result value, as JSON — no command/parameters/target envelope # (implies --format json; cannot be combined with --detach) -unity command recompile_status --result-only +unity command script recompile_status --result-only ``` -**A named command waits out a briefly unavailable Editor.** Right after `editor_play`, or while scripts reload, the Editor declines commands for a few seconds. `unity command ` retries within its `--timeout` and runs the command once the Editor accepts it, so you don’t need a retry loop of your own. It resends only a command the Editor provably didn’t run: a busy rejection, or a refused connection while the Editor process is still running. A timeout or a dropped connection is never retried, because the command may already be running. If the Editor stays unavailable, the command fails with `EDITOR_NOT_READY` (exit 6), and the message says whether the Editor was busy or refusing connections. +**A command is named by its group and its name.** `unity command editor status` runs the command `status` of the group `editor`, and `unity command editor.playmode start` the command `start` of the subgroup `editor.playmode`; listings show every command that way. A project whose `com.unity.pipeline` is from before command groups names the same commands with one flat word (`editor_status`, `editor_play`) and answers a full name Command Not Found. For a command the CLI knows the old name of, it retries under that name and warns once per run to run `unity pipeline upgrade`, so the full names work on both. + +**A named command waits out a briefly unavailable Editor.** Right after `editor.playmode start`, or while scripts reload, the Editor declines commands for a few seconds. `unity command ` retries within its `--timeout` and runs the command once the Editor accepts it, so you don’t need a retry loop of your own. It resends only a command the Editor provably didn’t run: a busy rejection, or a refused connection while the Editor process is still running. A timeout or a dropped connection is never retried, because the command may already be running. If the Editor stays unavailable, the command fails with `EDITOR_NOT_READY` (exit 6), and the message says whether the Editor was busy or refusing connections. -In the human table, `recompile`, `recompile_status`, `test_status` and `run_tests` results render as short readable text in the Result column instead of a JSON blob; `--format json` / `ndjson` output is unchanged. +In the human table, `script recompile`, `script recompile_status`, `test status` and `test run` results render as short readable text in the Result column instead of a JSON blob; `--format json` / `ndjson` output is unchanged. **`--caller [label]` and `--skill [name]` are analytics labels, not behavior.** `--caller` records what invoked the CLI and `--skill` records the agent skill driving the invocation; neither changes what the command does. A caller the CLI recognizes is recorded as given, anything else as `other`. They exist so an integration can identify itself — an agent running this skill has no reason to set them by hand, and both are inert when analytics are off. @@ -432,7 +481,7 @@ In the human table, `recompile`, `recompile_status`, `test_status` and `run_test A mature project's Pipeline catalog gets long, so the **listing** form of `unity command` (no command name) accepts query flags that filter, group, sort, and page it — the fastest way for an agent to find the right command without pulling the whole catalog: ```bash -# Filter by substring across name, description, and tag +# Search built-in commands, plugins and the Editor's Pipeline commands (same as commands --grep) unity command --query screenshot # Filter to a tag subtree @@ -442,37 +491,46 @@ unity command --tag assets/import # Compact rows instead of full detail unity command --detail compact +# Ask for the group listing explicitly (refuses with the package version it needs on an older Editor) +unity command --detail groups + # Ask for the tag listing explicitly, and narrow it unity command --tags unity command --tags --tag assets +# Map the flat command names from before the groups to their new names (Editor only) +unity command --obsolete-command-names +unity command --obsolete-command-names --query reload_file + # Group the results -unity command --group_by package # flat | package | tag +unity command --group-by package # flat | package | tag # Sort and page unity command --sort package --order desc unity command --offset 20 --limit 20 # Combine, with machine output -unity command --query import --group_by tag --limit 10 --format json +unity command --query import --group-by tag --limit 10 --format json ``` | Flag | Values | Default | |---|---|---| -| `--tags` | boolean | on when no command name and no `--detail` | -| `--detail [level]` | `compact`, `full` | — | +| `--tags` | boolean | off (the group list is what no command name and no flag asks for) | +| `--obsolete-command-names` | boolean | off; `--query` narrows it, by old or new name | +| `--detail [level]` | `compact`, `full`, `groups`, `tags` | `groups` when no command name and no flag is given | | `--query [term]` | substring on name, description, or tag | — | | `--tag [tag]` | a tag or tag subtree (`assets`, `assets/import`) | — | -| `--group_by [mode]` | `flat`, `package`, `tag` | `flat` | +| `--group-by [mode]` | `flat`, `package`, `tag` | `flat` | | `--sort [key]` | `name`, `package` | `name` | | `--order [direction]` | `asc`, `desc` | `asc` | | `--offset [n]` / `--limit [n]` | integers | — | Two traps worth knowing: -- **`--tags` and `--detail` pick the same thing**, so they cannot be combined; ask for one or the other. -- **`--group_by` is spelled with an underscore**, unlike every other flag on the CLI. That is deliberate and load-bearing, so don't "correct" it to `--group-by`. +- **`--tags`, `--obsolete-command-names` and `--detail` pick the same thing**, so they cannot be combined; ask for one of them. `--detail groups` and `--tags` are the explicit forms of the group and tag lists, and neither combines with `--group-by` or `--sort package`. +- **`--group_by` is the old spelling of `--group-by`.** It still works, with a deprecation warning. Written before a command name, the flag still reaches that Pipeline command as `--group_by`, the Editor's own parameter name. - **These flags only mean "listing" when no command name is given.** With a command name they are forwarded to that Pipeline command as ordinary parameters — `unity command my_cmd --query foo` passes `query: foo` to `my_cmd`. That is why each takes an *optional* value: a bare `--query` forwards boolean `true` to the command, while the listing path rejects a bare flag with a clear error rather than guessing. +- **`--query` on its own is the `commands --grep` search, over the Editor this invocation targets.** It returns the same rows as `unity commands --grep `: built-in commands, plugins and the Editor's Pipeline commands, ranked together. In JSON, `data` keeps every key the Editor listing returned, unchanged (`target`, `server`, `commands`, `count`, `total`, `offset`, `limit`, `hint`), and adds `search`, the same `{pattern, count, matches, hint}` as `commands --grep`; read `data.search`, since the listing keys go in the pre-GA breaking beta. NDJSON ends with a terminal `result` frame carrying `search`. A Pipeline row's `invoke` carries the query's targeting flags. An explicit `--offset`, even `--offset 0`, keeps the Editor listing. With no connected Editor it exits 0 with the CLI's matches and `Pipeline commands not searched: no connected Editor. Run unity status.` (in JSON, `data.hint`). Several Editors, an explicit `--runtime` with no Player, or an Editor that refuses the listing still fail as before. With another listing flag (`--tag`, `--limit`, `--sort` and the rest) `--query` is instead the Editor's own substring filter on name, description and tag. #### commands — the CLI's own command tree, as JSON @@ -488,17 +546,17 @@ unity commands Each node in `data.commands` carries `name`, `aliases`, `description`, `arguments` (positional, with `required`/`variadic`), `options` (this command's own flags: `long`/`short`/`valuePlaceholder`/`default`/`description`), `globalOptions` (same shape — flags inherited from every ancestor, so `--format`/`--json`/etc. show up on every node without repeating a root-level dump, and a mid-tree umbrella's own options show up on its descendants too), and `subcommands` (the same shape, recursively). Hidden and dev-only surfaces are excluded — the same visibility rule `--help` uses — so what you see is exactly what the current build actually exposes. -**Search it instead of dumping it: `unity commands --grep `.** The same tree, plus the plugin catalog (the same universe `unity plugin list` reports, installed or not), filtered to what matches. A command matches on its full path (`plugin list`), name, aliases, description, arguments, or its own options; the inherited global options are not searched, since every command has them. A plugin matches on its id, display name, registry description, or the command word that runs it (`ugs`, `uvcs`, `cm`, `ai`): +**Search it instead of dumping it: `unity commands --grep `** (or `unity command --query `, the same search). The same tree, plus the plugin catalog (the same universe `unity plugin list` reports, installed or not) and the Pipeline commands of the live connected Editor, filtered to what matches. A command matches on its full path (`plugin list`), name, aliases, description, arguments, or its own options; the inherited global options are not searched, since every command has them. A plugin matches on its id, display name, registry description, or the command word that runs it (`ugs`, `uvcs`, `cm`, `ai`): ```bash -# One match per line: kind, name, state, install command, description +# One match per line: kind, name, state, how to run it, description unity commands --grep generate -# Structured: { pattern, count, matches: [{ kind, name, description, state, installed, installCommand }] } +# Structured: { pattern, count, matches: [{ kind, name, path, description, matchedOn, state, installed, installCommand, invoke }], hint } unity commands --grep '^plugin ' --format json ``` -`kind` is `command` or `plugin`. On a plugin row `state` is the same token `unity plugin list` uses (`resolved`, `not-installed`, `unsupported-platform`), `installed` is a boolean, and `installCommand` is set only when the plugin is installable here but not installed. On a command row those three are `null` rather than omitted. Matching is case-insensitive and the pattern is always a regular expression, so an ordinary keyword is a valid pattern. A pattern that doesn't compile is refused up front (exit `2`), and one that takes too long to evaluate stops with exit `2` instead of hanging. No match is not an error: the command exits `0` and says so. The search only reads the cached or bundled plugin registry and never goes to the network. +`kind` is `command`, `plugin`, or `pipeline` (a Pipeline command of the connected Editor, matched on its name, group or tags, and description). `invoke` is how to run the row: `unity ` for a command, `unity command ` for a Pipeline command, `unity plugin install ` for a plugin that is not installed, and `unity ` once it is (null only for a plugin with nothing to run: an installed runtime, or one this platform does not support). `path` is the command path, the full Pipeline command id or the plugin id (`name` holds the same text). `matchedOn` is where the query hit: `name` (the name, an alias, the parent path, a Pipeline group or tag), `description`, or `option` (an argument or option); a regular expression reports the first of those it matched. The tsv columns are `kind, name, state, installCommand, invoke, description, path, matchedOn`. On a plugin row `state` is the same token `unity plugin list` uses (`resolved`, `not-installed`, `unsupported-platform`), `installed` is a boolean, and `installCommand` is set only when the plugin is installable here but not installed. On a command row those three are `null` rather than omitted. Matching is case-insensitive. A pattern of plain words (no regex character) is a word search: every word must match somewhere in a command's text, in any order (`cloud link` finds `projects link cloud`), and the matches are ranked, a word in the command's own name before one in its parent path, its description, then its arguments and options. Commands, plugins and Pipeline commands are ranked together; equal ranks list commands, then plugins, then Pipeline commands. A pattern with any regex character (`| . ^ $ * + ? ( ) [ ] { } \ #`) is a regular expression matched as written (a space in it matches a space) and listed in command-tree order. A pattern that doesn't compile is refused up front (exit `2`), and one that takes too long to evaluate stops with exit `2` instead of hanging. No match is not an error: the command exits `0` and says so. The Editor's command list is fetched live (no cache); `commands --grep` gives it at most 3 seconds, `command --query` waits for it. `hint` (and the last human line) is empty when the Editor was searched, and otherwise says why not: no connected Editor (`unity status`), no answer in time (`unity command --query `, which waits), several Editors open (`unity command --query --project-path `), or an Editor that could not be reached or did not list its commands (`unity command`). The search only reads the cached or bundled plugin registry and never goes to the network for it. #### Available in production — the common live commands @@ -508,20 +566,20 @@ The Pipeline package ships a set of built-in scene/GameObject commands. The comm | Command | Does | |---|---| -| `create_gameobject` | Create a GameObject in the active scene | -| `find_gameobjects` | Query the active scene for GameObjects | -| `get_scene_hierarchy` | Print the active scene's hierarchy | -| `set_transform` | Set a GameObject's position / rotation / scale | -| `add_component` | Add a component to a GameObject | -| `rename_gameobject` / `delete_gameobject` | Rename or delete a GameObject | -| `save_scene` / `save_all` | Save the active scene, or all dirty scenes and assets | -| `create_script` → `recompile` → `attach_script` | Add a new C# script, rebuild, then attach it to a GameObject (`unity recompile` does the middle step and reports compile errors) | +| `gameobject create` | Create a GameObject in the active scene | +| `gameobject find` | Query the active scene for GameObjects | +| `scene get_hierarchy` | Print the active scene's hierarchy | +| `gameobject set_transform` | Set a GameObject's position / rotation / scale | +| `component add` | Add a component to a GameObject | +| `gameobject rename` / `gameobject delete` | Rename or delete a GameObject | +| `scene save` / `editor save_all` | Save the active scene, or all dirty scenes and assets | +| `script create` → `script recompile` → `script attach` | Add a new C# script, rebuild, then attach it to a GameObject (`unity recompile` does the middle step and reports compile errors) | The **authoritative** catalog is always `unity command --format json` — every registered command with its full parameter schema. The table above just jump-starts common tasks so you don't have to dump-and-grep first. -Some projects (and Pipeline package versions) register an `eval` — and `eval_file` — command on the +Some projects (and Pipeline package versions) register a `script eval` — and `script eval_file` — command on the Editor side, so you can run C# through the connected Editor in a production build: -`unity command eval "return Application.unityVersion;"` or `unity command eval_file snippet.cs`. +`unity command script eval "return Application.unityVersion;"` or `unity command script eval_file snippet.cs`. Availability depends on the Editor/package, so discover it at runtime with `unity command` / `unity list` rather than assuming it. @@ -535,7 +593,7 @@ If no editor with a reachable Pipeline server is found, the command errors with ```bash # Start the work and get a job id back -unity command run_tests --detach +unity command test run --detach # Check on it, wait for it, or give up on it unity job status @@ -549,7 +607,25 @@ unity job wait --timeout 600 unity job wait --poll-interval 2000 ``` -`wait` blocks until the job finishes and then prints its result, exactly as the non-detached command would have. `--timeout` counts seconds and `0` — the default — waits indefinitely; `--poll-interval` counts milliseconds. All three subcommands take the same Editor-targeting flags as `unity command` (`--project-path`, `--runtime`, `--runtime-path`). +`wait` blocks until the job finishes and then prints its result, exactly as the non-detached command would have. `--timeout` counts seconds and `0` — the default — waits indefinitely; `--poll-interval` counts milliseconds. All three subcommands take the same Editor-targeting flags as `unity command` (`--project-path`, `--port`, `--pid`, `--instance`, `--runtime`, `--runtime-path`). + +#### Modal dialogs + +Requires Unity 6000.7+ and a `com.unity.pipeline` that has the dialog commands. A native modal dialog (`Save changes?`, a build or import prompt, an `EditorUtility.DisplayDialog` call, …) blocks the Editor's main thread — which is exactly why `unity command` and `unity job` can be rejected or time out while one is open. Three ordinary pipeline commands see it and clear it without touching the keyboard or mouse, and they keep working while a dialog is open: + +```bash +unity command editor.dialog list # see what's open: id, title, message, buttons +unity command editor.dialog answer