From dd3e4e3e30e6e6f5bf76fb0c01a43afb0eed9fca Mon Sep 17 00:00:00 2001 From: Ziyi Zhang <90444077+ziyiunity@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:28:53 -0400 Subject: [PATCH] fix(unity-cli): refresh the skill from the public skills repo This copy was last aligned to CLI 1.0.0-beta.4 (2026-08-06). The public repo's copy tracks beta.8 (2026-09-01), so the plugin has been shipping a skill that describes a month-old CLI across four releases. Adopts the public copy for all nine shared files and brings over references/collaboration.md, which only existed there. The public copy is ahead by 862 lines against 77 the other way, and every one of those 77 is content the public copy restructured rather than dropped: the two-step install became one section, the help and command-routing tables moved, and `unity upgrade` was renamed `unity self-update` with the old name kept as an alias -- a rename this copy never picked up. --- skills/unity-cli/CHANGELOG.md | 91 +++- skills/unity-cli/SECURITY.md | 14 +- skills/unity-cli/SKILL.md | 126 +++-- .../references/auth-license-cloud.md | 57 ++- skills/unity-cli/references/build-run-test.md | 106 +++- skills/unity-cli/references/collaboration.md | 477 ++++++++++++++++++ .../references/diagnostics-maintenance.md | 114 ++++- .../unity-cli/references/editors-install.md | 37 ++ .../references/integration-advanced.md | 127 ++++- .../references/projects-templates.md | 268 +++++++++- 10 files changed, 1339 insertions(+), 78 deletions(-) create mode 100644 skills/unity-cli/references/collaboration.md diff --git a/skills/unity-cli/CHANGELOG.md b/skills/unity-cli/CHANGELOG.md index a6933cb..bb2750b 100644 --- a/skills/unity-cli/CHANGELOG.md +++ b/skills/unity-cli/CHANGELOG.md @@ -4,25 +4,92 @@ All notable changes to the `unity-cli` skill documentation are recorded here. Th skill documents the published [`unity` CLI](https://public-cdn.cloud.unity3d.com/hub/prod/cli/); each entry notes the CLI version the skill was aligned to. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), with one +deliberate departure: there is no `Unreleased` section. Sections are cut per CLI release, and +documentation for a CLI version that has not shipped publicly is not recorded here until that +release is out — so this file never names unreleased surface. Pending skill work is tracked +alongside the CLI change itself, not here. -## [Unreleased] — aligned to CLI `1.0.0-beta.4` (2026-08-06) +## CLI `1.0.0-beta.8` (2026-09-01) -Tracks the CLI's `1.0.0-beta.4` release. Coverage is the full `1.0.0-beta.3` surface plus the beta.4 additions an automation or CI caller reaches for first: `unity test --report-format`/`--coverage`, `unity build --profile` and the zero-code build strategies, `unity projects exec`, `unity bug --attachments`/`--share-project`, and the rule that a failure is readable from stdout. The rest of beta.4 lands in the next skill pass and is **not** documented here yet: `unity skill install`/`refresh`, `unity projects clean`, `unity editors prune`/`verify`, `unity templates pack`, the `unity command` listing-query flags, multi-account auth (`unity auth list`/`switch`/`default`), and the output pager. Documenting a subset of the shipped surface is safe; the stamp exists to stop the reverse (publishing surface that isn't in the shipped binary). +Aligned to the CLI's `1.0.0-beta.8` release, which supersedes the withdrawn `1.0.0-beta.7`. That release reached the production beta channel and was pulled the same day, so beta.8 is what actually carries its surface to users, and this stamp moves on from `1.0.0-beta.6`, which is what the channel served in between. Everything the skill already documents stays accurate. Two additions extend the `unity vcs` provider layer that the beta.7 note below recorded as public but not yet documented: repository creation and readiness reporting through Bitbucket's `bkt` and Azure DevOps' `az`. Both are deferred to that same alignment pass rather than documented piecemeal here. Also deferred, for the same reason: `unity skill install --local` now mirroring the agent skill a project's `com.unity.pipeline` package ships, and `unity install --format json` printing on success the same result envelope the NDJSON `result` frame already carried. The rest of the release is Windows elevation and install fixes that change no flag or exit code this skill documents. + +## CLI `1.0.0-beta.7` (2026-08-25) + +Aligned to the CLI's `1.0.0-beta.7` release. The surface this release ships that the skill already documents landed with the feature PRs themselves: `unity vcs uvcs locks` and `unity vcs uvcs changesets`, the per-organization default cloud project (`unity cloud project set-default` / `current` / `clear-default` and the project-resolution fallback), the long-output pager, and `unity plugin install` / `remove` / `upgrade`. The release's new `unity vcs` verb family (`setup`, `status`, `sync`, `doctor`, `merge-setup`, `conflicts` / `explain` / `resolve`, `diff`, `summarize`, `hooks`, `git worktree`, `git migrate-lfs`, `providers`, `affected`, `switch`) is now public but not yet documented by this skill — that coverage follows as its own alignment pass. + +## CLI `1.0.0-beta.6` (2026-08-19) + +Aligned to the CLI's `1.0.0-beta.6` release. Most of this release's surface — `unity doctor --ci`, `unity cache key`, `--format github`, `unity test --shard`, and `unity collaboration` — landed already documented in `1.0.0-beta.5`'s reference files ahead of that release's stamp bump. This pass adds the two pieces that were still outstanding: the `unity build` stall heartbeat and `--timeout`, and the `unity open` background identity server plus the new git-credential interactivity gating. + +### Added + +- **`unity build --timeout ` (env `UNITY_BUILD_TIMEOUT`) and the stall heartbeat** — documented in the build options table and as its own note: a long build now prints a periodic "Still building — Nm elapsed, last log output Nm ago" heartbeat (on stderr for the human path, as progress frames under `--format json`/`ndjson`), and `--timeout` aborts a build that runs past the given number of seconds, exit 6. +- **`unity open`'s background identity server** — a small helper that answers the Editor's account lookup from the stored `unity auth login` session, so a Hub-less machine gets a signed-in Editor. Documents that it steps aside for a real Hub, exits on its own, and can be disabled with `UNITY_NO_EDITOR_IDENTITY_SERVER`. +- **Git credential interactivity gating** — the CLI now decides up front whether anyone is there to answer a credential prompt. Documented alongside the existing token-resolution order: on a real terminal a configured helper (including Git Credential Manager's browser/device-code flows) runs and is relayed; without one — CI, machine `--format`, `--non-interactive` — the command fails immediately with exit 4 instead of hanging, naming the missing credential. + +### Changed + +- Refreshed the latest-version note to `1.0.0-beta.6`. +- **Corrected the pager documentation** to describe the one surface that actually pages. The `git log`-style external pager recorded under Added in the `1.0.0-beta.5` notes below was never ported to the shipped binary — nothing spawns `less`, `more.com`, `$PAGER`, or `$UNITY_PAGER` — so the resolution chain, the `TERM=dumb` and `unity shell` conditions, and the five paging listings (`unity command`, `releases`, `editors`, `changelog`, `logs`) never applied; those listings print in full every time. What does page is `unity projects list`, in-process and on a terminal only, ten projects per screen, and off for redirected stdout, `--format json`/`ndjson`, `--all`, `--watch`, and `--no-pager` / `UNITY_NO_PAGER` — but not `--format tsv` or `--format github`, which on a terminal fall through to the human table and page, so the tables no longer claim that every machine format bypasses the pager. The global flags and environment tables now say that, and `UNITY_PAGER` is documented as having no effect. + +## CLI `1.0.0-beta.5` (2026-08-13) + +Aligned to the CLI's `1.0.0-beta.5` release. That release is fixes only — it adds no command, flag, or exit code. This pass instead closes the documentation gap the previous one left open: every item the `1.0.0-beta.4` note listed as deferred is now documented, so the skill covers the full shipped surface of both releases. + +### Added + +- **`unity editors prune`** — find editors no registered project uses and optionally uninstall them. Report-only by default; `--remove` uninstalls, and `-y, --yes` is required to do so non-interactively. Notes that "unused" is judged against the project registry, so an unregistered project's editor counts as unused. +- **`unity editors verify `** — structurally verify an installed editor's files and modules, reporting each component as `ok` / `missing` / `skipped` with the exact repair command. Documented as a structural check (presence, not integrity or signing), and that `--architecture` is inherited from the `editors` parent. +- **`unity projects clean`** — delete a project's regenerable folders (`Library`, `Temp`, `Logs`, …). Documents `--dry-run`, that `--yes` is required non-interactively, and the two guardrails: it refuses while an editor has the project open (naming the PID) and rejects a path that isn't a Unity project. +- **`unity templates pack`** — pack a project into a portable `.tgz`, with the distinction from `templates create` stated up front (`pack` writes a standalone archive and registers nothing; `create` installs into the user templates directory). Covers the required `--output` file path, the `--template-version` spelling that avoids the global `--version` collision, and the rejection of an output path inside the project being packed. +- **`unity command` listing-query flags** — `--detail`, `--query`, `--tag`, `--group_by`, `--sort`, `--order`, `--offset`, `--limit` as a table with values and defaults, plus the two traps: `--group_by` is deliberately underscored, and the flags only mean "listing" when no command name is given (with a name they forward to that Pipeline command as parameters, which is why each takes an optional value). +- **Multi-account auth** — `unity auth list` (with its `ls` alias), `switch`, and `default` (`--project`, `--clear`), plus `auth logout `. Documents the precedence that makes these predictable: a project default beats the active account, and service-account credentials beat both; `switch` fails on an ambiguous match rather than guessing, carrying candidates in `data.candidates`. +- **The output pager** — documented in the global flags and environment tables (`--no-pager`, `UNITY_NO_PAGER`, `UNITY_PAGER`) with the `git log` model spelled out: which commands page, the `$UNITY_PAGER` → `$PAGER` → `less -RFX` → `more.com` resolution chain, and the conditions under which paging never happens (non-TTY, machine formats, `--quiet`, `TERM=dumb`, inside `unity shell`) so scripts need no special handling. +- **`unity skill install` / `refresh`** — install this skill into an AI client from the copy embedded in the binary, framed against `mcp configure` (tools vs. docs). Covers the client list, `--list` as the authority on which scopes each client supports, and that `refresh` should follow `unity upgrade` because installed copies otherwise go stale. + +### Changed + +- Refreshed the latest-version note to `1.0.0-beta.5`. +- Command index: added `editors prune`/`verify`, `projects clean`, `templates pack`, `auth list`/`switch`/`default`, and `skill install`/`refresh`. Also dropped one listed subcommand that is not part of the public surface. + +## CLI `1.0.0-beta.4` (2026-08-06) + +Tracks the CLI's `1.0.0-beta.4` release. Coverage is the full `1.0.0-beta.3` surface plus the beta.4 additions an automation or CI caller reaches for first: `unity test --report-format`/`--coverage`, `unity build --profile` and the zero-code build strategies, `unity projects exec`, `unity bug --attachments`/`--share-project`, and the rule that a failure is readable from stdout. The rest of beta.4 was deferred to a later pass and is documented under `1.0.0-beta.5` above: `unity skill install`/`refresh`, `unity projects clean`, `unity editors prune`/`verify`, `unity templates pack`, the `unity command` listing-query flags, multi-account auth (`unity auth list`/`switch`/`default`), and the output pager. Documenting a subset of the shipped surface is safe; the stamp exists to stop the reverse (publishing surface that isn't in the shipped binary). ### Added -- **`unity editors running`** — list running Editor instances and the project each has open (version + PID; cross-platform; an empty list is exit 0). -- **`unity projects size [project]`** — on-disk footprint by top-level folder (`-a, --all`; `--json` emits raw bytes). -- **`unity run --command `** — execute a registered `[CliCommand]` Editor command headlessly (arguments after `--` parsed against its `[CliArg]` schema; requires `com.unity.pipeline`). -- **`unity install --list-components`** — list an editor's available modules and exit (a drop-in alias for `unity modules list `). -- **`unity bug` non-interactive flags** — `--title`, `--description`, `--steps` (repeatable), `--reproducibility `, `--email`. - **`unity bug --attachments ` / `--share-project `** — attach extra files (each must be an existing readable file; a folder is rejected), or a stripped copy of the project using the same packaging the Editor's bug reporter uses. Interactively, omitting both flags makes the reporter ask about each. - **`unity test --report-format nunit|junit|nunit,junit`** — write a JUnit-schema report that GitHub Actions and GitLab ingest as native test results, with no converter step. `junit` alone makes `--output` the JUnit file; `nunit,junit` writes both from a single Editor run, the JUnit one landing beside the NUnit report. `--junit-output` chooses that second path and is valid **only** with `nunit,junit`; passing it with a single format is an option error. The report is written even when tests fail, and the NUnit default is unchanged. - **`unity test --coverage`** (with `--coverage-output`, `--coverage-options`) — collect coverage through the Unity Code Coverage package. A project without the package gets a warning and the tests still run. - **`unity build --profile ` and the zero-code build strategies** — documented the three ways to pick a build: a Unity 6+ Build Profile (a `.asset` path or a profile name under `Assets/Settings/Build Profiles`, which defines the target), a built-in desktop player build (`--target` plus a required `--output-path`), or a custom `--execute-method`. `--execute-method` is no longer required, and `--target` is not needed when `--profile` is used. Non-desktop targets still need `--profile` or `--execute-method`. - **`unity projects exec -- `** — run one command across every registered project, each in its own directory with `UNITY_PROJECT_PATH` and `UNITY_EDITOR_VERSION` set. Narrow the set with repeatable `--filter` terms (`name:`, `version:`, `pinned[:]`), raise concurrency with `--parallel `, and use `--continue-on-error` or `--dry-run`. Arguments are passed verbatim rather than through a shell, so pipes and `&&` are unavailable. - **`unity run --command` worked example** — a `[CliCommand]` source snippet with the human output it produces and the `--format json` envelope beside it (`data.result`, `data.parameters`, and `data.reusedRunningEditor`, which reports whether an already-open Editor was reused). + +### Changed + +- **Read failures from stdout, not stderr** — documented the machine-format failure contract. Under `--format json` a failed command still writes a full envelope (`success: false` and a populated `errors` array whose `errors[0].code` is the stable token to branch on); under `--format ndjson` it closes with the usual terminal `result` frame. `data` is usually `null` on a failure but not always, so branch on `success`, never on `data`: a partial `unity editors add` failure carries a row per path, and an ambiguous `unity auth switch` carries `data.candidates`. Empty stdout is not a failure signal, and the commands that still report only on stderr are called out as a known gap rather than a shape to code against. +- **Reserved forwarded flags** — matching is spelling-insensitive, so `-projectPath`, `--projectPath`, and `-projectPath=` are all rejected, on every command that forwards user arguments (`unity run`, `unity test`, `unity build --args`, `unity open --args`). Also clarified that `unity run` deliberately never passes `-useHub`/`-hubIPC`, because the CLI runs no Hub IPC server and those flags would make the Editor launch the Unity Hub. +- **`unity mcp configure --local`** — corrected the client list. The clients with a project-local config are `cursor`, `vscode`, `vscode-insiders`, `kiro`, and `codex`. Windsurf reads one global file and has no project-local variant. +- **`UNITY_NO_ELEVATE` / `--no-elevate`** — corrected to say it keeps the install service unelevated. The Editor's NSIS installer is manifested `highestAvailable`, so it still asks for elevation on demand under an administrator account and never does for a standard user; in CI, run the agent elevated instead. +- Refreshed the latest-version note to `1.0.0-beta.4`. + +### Security + +- **Install integrity stated, and scoped** — the install script verifies the downloaded binary against the SHA-256 published in the channel's release manifest and aborts on mismatch, or when no SHA-256 tool is available. Because the manifest is fetched from the same CDN origin as the binary, this is described as an integrity check against a corrupted, truncated, or substituted *download*, not a defense against a compromise of the origin; the trust assumption (TLS plus Unity's control of that CDN) is stated explicitly. +- **Linux install side effects split by package** — the CDN script installs a self-contained binary under `~/.local/bin` and touches no system package sources. The separately published packages do change system state, and differently: the `.deb` adds an apt repository entry and installs Unity's signing key into the system keyring, while the `.rpm` adds a yum repository entry with `gpgcheck` enabled pointing at the published key URL and imports no key at install time. + +## CLI `1.0.0-beta.3` (2026-07-24) + +Tracks the CLI's `1.0.0-beta.3` release. The CLI's own `[Unreleased]` changes at the time (detached command jobs — `unity command --detach`, `unity job wait/status/cancel` — and live in-terminal progress for `unity command`) were intentionally **not** documented in this pass: they weren't in the shipped `1.0.0-beta.3` binary. They shipped in `1.0.0-beta.4`, documented above. + +### Added + +- **`unity editors running`** — list running Editor instances and the project each has open (version + PID; cross-platform; an empty list is exit 0). +- **`unity projects size [project]`** — on-disk footprint by top-level folder (`-a, --all`; `--json` emits raw bytes). +- **`unity run --command `** — execute a registered `[CliCommand]` Editor command headlessly (arguments after `--` parsed against its `[CliArg]` schema; requires `com.unity.pipeline`). +- **`unity install --list-components`** — list an editor's available modules and exit (a drop-in alias for `unity modules list `). +- **`unity bug` non-interactive flags** — `--title`, `--description`, `--steps` (repeatable), `--reproducibility `, `--email`. - **`unity shell`** — command-history persistence (↑/↓; secret-bearing flag values masked on disk), tab completion, session context/defaults (`use project|org`, `set format|verbose|banner`, `unset`, `context`), and the `--protocol ndjson` machine/agent mode. - Environment variables **`UNITY_NO_CONSENT_PROMPT`** (suppress the first-run consent prompt without recording a choice) and **`UNITY_NO_CRASH_REPORT`** (disable anonymous crash/error reporting). - Global **`--json`** shorthand (accepted on every command) in the global-flags table. @@ -42,19 +109,13 @@ Tracks the CLI's `1.0.0-beta.4` release. Coverage is the full `1.0.0-beta.3` sur - **`unity language --set`** accepts BCP-47 / locale / bare-language / bare-region spellings (resolved case-insensitively when unambiguous); catalog shared with the Hub. - **`unity projects`** path resolution documented as tolerant of casing, separator direction, and trailing slash (verified against real filesystem identity). - Terminal-hardening note extended to Commander usage errors, the `bug` log-archive warning, and `projects add`/`remove` tsv output; noted that an invalid `--proxy` now fails with exit 2; `UNITY_PROJECT_PATH` now honored by `status` and the cloud commands. -- **Read failures from stdout, not stderr** — documented the machine-format failure contract. Under `--format json` a failed command still writes a full envelope (`success: false` and a populated `errors` array whose `errors[0].code` is the stable token to branch on); under `--format ndjson` it closes with the usual terminal `result` frame. `data` is usually `null` on a failure but not always, so branch on `success`, never on `data`: a partial `unity editors add` failure carries a row per path, and an ambiguous `unity auth switch` carries `data.candidates`. Empty stdout is not a failure signal, and the commands that still report only on stderr are called out as a known gap rather than a shape to code against. -- **Reserved forwarded flags** — matching is spelling-insensitive, so `-projectPath`, `--projectPath`, and `-projectPath=` are all rejected, on every command that forwards user arguments (`unity run`, `unity test`, `unity build --args`, `unity open --args`). Also clarified that `unity run` deliberately never passes `-useHub`/`-hubIPC`, because the CLI runs no Hub IPC server and those flags would make the Editor launch the Unity Hub. -- **`unity mcp configure --local`** — corrected the client list. The clients with a project-local config are `cursor`, `vscode`, `vscode-insiders`, `kiro`, and `codex`. Windsurf reads one global file and has no project-local variant. -- **`UNITY_NO_ELEVATE` / `--no-elevate`** — corrected to say it keeps the install service unelevated. The Editor's NSIS installer is manifested `highestAvailable`, so it still asks for elevation on demand under an administrator account and never does for a standard user; in CI, run the agent elevated instead. -- Refreshed the latest-version note to `1.0.0-beta.4`. +- Refreshed the latest-version note to `1.0.0-beta.3`. ### Security - Added `SECURITY.md` documenting the skill's powerful-by-design capabilities (local Editor control and C# evaluation, official-CDN install) and the safeguards around them (local-user-context execution, trusted-input-only machine mode, HTTPS official CDN). - Clarified that driving a live Editor and running C# happen entirely on the local machine in the user's own account — not remote access — and added a trusted-input warning to `unity shell --protocol ndjson` machine mode. - Removed internal development-only command documentation from the public skill; the production Editor-side C# evaluation via `unity command eval` remains documented. `SECURITY.md` now carries only the user-facing capability rationale and safeguards. -- **Install integrity stated, and scoped** — the install script verifies the downloaded binary against the SHA-256 published in the channel's release manifest and aborts on mismatch, or when no SHA-256 tool is available. Because the manifest is fetched from the same CDN origin as the binary, this is described as an integrity check against a corrupted, truncated, or substituted *download*, not a defense against a compromise of the origin; the trust assumption (TLS plus Unity's control of that CDN) is stated explicitly. -- **Linux install side effects split by package** — the CDN script installs a self-contained binary under `~/.local/bin` and touches no system package sources. The separately published packages do change system state, and differently: the `.deb` adds an apt repository entry and installs Unity's signing key into the system keyring, while the `.rpm` adds a yum repository entry with `gpgcheck` enabled pointing at the published key URL and imports no key at install time. ## CLI `1.0.0-beta.2` (2026-07-21) diff --git a/skills/unity-cli/SECURITY.md b/skills/unity-cli/SECURITY.md index 2e1c862..723a567 100644 --- a/skills/unity-cli/SECURITY.md +++ b/skills/unity-cli/SECURITY.md @@ -2,10 +2,22 @@ This skill documents the official first-party [`unity` CLI](https://public-cdn.cloud.unity3d.com/hub/prod/cli/). A few of its capabilities are powerful by design and are flagged by automated skill scanners. They are intentional, first-party functionality with the safeguards described below. - +## Accepted risks + +These capabilities are accepted by design. Each is documented in full in the sections below; this table is the explicit, human-readable acknowledgment. + +| Risk | Capability | Why it is accepted | +|---|---|---| +| `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 | 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` — plus symlink refusals and a warning before project-local installs from the home directory. | ## Accepted, by-design capabilities +### Installing skills into AI clients + +`unity skill install` and `unity skill refresh` write skill files — this skill, and the `unity-pipeline` skill a project's `com.unity.pipeline` package ships — into AI clients' configuration directories, which automated scanners flag as an agent-persistence pattern. The writes happen only when the user runs the command (nothing installs at load or in the background), the capability is the command's advertised purpose, and it is fenced: an install ledger records every write and a directory this CLI did not write is reported, never overwritten, without explicit `--force` consent; targets that resolve through a symlinked path component are refused; a package-shipped tree is read with file-count, per-file, and aggregate size bounds and never through symbolic links; and `--local` from the home directory warns first. + ### Local Editor control and C# evaluation `unity command`, `unity command eval`, and `unity shell --protocol ndjson` can drive a Unity Editor that is already open on the same machine and run C# through the project's `com.unity.pipeline` package. This executes **entirely on the local machine, in the current user's account, against the user's own Editor** — it is not remote access and grants no privilege the user does not already have at their own terminal. It is the CLI's core value for AI-assisted and automated Editor workflows. diff --git a/skills/unity-cli/SKILL.md b/skills/unity-cli/SKILL.md index 61d25cb..95b7ddc 100644 --- a/skills/unity-cli/SKILL.md +++ b/skills/unity-cli/SKILL.md @@ -19,11 +19,23 @@ unity command editor_play # run one — e.g. enter Play mode unity command eval 'new UnityEngine.GameObject("Joe");' ``` +### 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 +``` + +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). + 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`, and `unity list` can't connect at all. 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). -## Step 1: Install the CLI (if not already installed) +## Install the CLI (if not already installed) First check if the CLI is available: @@ -43,21 +55,7 @@ curl -fsSL https://public-cdn.cloud.unity3d.com/hub/prod/cli/install.sh | UNITY_ $env:UNITY_CLI_CHANNEL='beta'; irm https://public-cdn.cloud.unity3d.com/hub/prod/cli/install.ps1 | iex ``` -After installing, open a new shell so `unity` is on PATH, then verify: - -```bash -unity --version -``` - -If the install script fails or the binary is still not found, tell the user and stop. - -## Step 2: Verify it works - -```bash -unity --version -``` - -If this fails with a permissions error or crash, the CLI installation may be broken. Suggest re-running the install script. +After installing, open a new shell so `unity` is on PATH, then verify with `unity --version`. If the install script fails or the binary is still not found, tell the user and stop; if the command itself fails with a permissions error or crash, the installation may be broken — suggest re-running the install script. --- @@ -67,19 +65,26 @@ These work on every command: | Flag | Description | |---|---| -| `--format ` | Output format: `human` (default), `json`, `tsv`, `ndjson`. Also via `UNITY_FORMAT` env var. | +| `--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. | | `--no-banner` | Suppress the branded header — use in scripts | +| `--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 | | `--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). | -| `--log-proxy` | Log one redacted entry per outbound request (host-only URL, resolved proxy, auth source, status, duration) to `proxy-request.json` — for reproducing proxy issues for support. Also via `UNITY_LOG_PROXY=1` or the persisted `proxyRequestLogging` setting. | +| `--log-proxy` | Log one redacted entry per outbound request to `proxy-request.json` — for reproducing proxy issues. Also via `UNITY_LOG_PROXY=1` or the `proxyRequestLogging` setting. | | `--no-log-proxy` | Opt a single invocation out of proxy request logging when it's enabled globally. | **Always use `--format json` when you need to parse output programmatically.** +**`unity projects list` is the only command that pages IN-PROCESS.** It shows 10 projects per screen and waits for a keypress between screens, and only when stdout is a terminal. Paging is off for redirected stdout, under `--format json` and `--format ndjson`, and under `--all`, `--watch`, or `--no-pager` / `UNITY_NO_PAGER`. + +**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. + 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. ## Environment variables @@ -88,7 +93,7 @@ All CLI env vars use the `UNITY_` prefix. A CLI flag always overrides the corres | Variable | Mirrors flag | Description | |---|---|---| -| `UNITY_FORMAT` | `--format` | Output format (`human`, `json`, `tsv`, `ndjson`). `HUB_FORMAT` is a deprecated alias. | +| `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. | @@ -96,6 +101,10 @@ All CLI env vars use the `UNITY_` prefix. A CLI flag always overrides the corres | `UNITY_VERBOSE` | `--verbose` | Show full error details on failure. | | `UNITY_NON_INTERACTIVE` | `--non-interactive` | Disable interactive prompts. | | `UNITY_NO_BANNER` | `--no-banner` | Suppress the branded banner. | +| `UNITY_NO_PAGER` | `--no-pager` | Turn off paging — both the external pager over the long listings and `unity projects list`'s interactive one. Presence-based: any value counts, including `0`. | +| `UNITY_PAGER` | — | The pager to use for the long listings, overriding `$PAGER` and the `less -RFX` default. Runs through a shell, so flags work (`less -S`). A blank value is ignored, not an opt-out. | +| `PAGER` | — | Same as `UNITY_PAGER`, consulted only when that is unset or blank. | +| `LESS` / `LV` / `LESSCHARSET` / `MORE` | — | Passed to the pager only when you have not set them, defaulting to `FRX`, `-c`, `utf-8`, and `FRX`. `LESSCHARSET` keeps multi-byte glyphs readable where the locale does not declare UTF-8; `MORE` exists because `more` on macOS/BSD is `less` under another name and reads `$MORE`, so without it `PAGER=more` waits for a keypress even for one line. | | `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. | @@ -113,16 +122,7 @@ All CLI env vars use the `UNITY_` prefix. A CLI flag always overrides the corres ## Getting help -If a command fails or you're unsure of the available options, append `-h` or `--help` to any command or subcommand: - -```bash -unity --help -unity install --help -unity projects --help -unity projects create --help -``` - -This works at every level of the command hierarchy. +Append `-h` or `--help` to any command or subcommand, at any level: `unity --help`, `unity projects create --help`. ## Exit codes @@ -134,6 +134,7 @@ This works at every level of the command hierarchy. | 3 | Authentication failure | | 4 | Precondition not met (e.g. no license active, floating server not configured) | | 6 | Command-specific failure | +| 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. | | 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). | @@ -150,13 +151,14 @@ flags, environment variables, and exit codes above apply throughout. Every comma | Commands | Reference file | |---|---| -| `auth` (login / logout / status), `license` (activate / return / server), `cloud` (org / project) | [auth-license-cloud.md](references/auth-license-cloud.md) | -| `editors` (list / running / add / default / path / install-path / info / upgrade / 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 / exec / close), `releases`, `templates` | [projects-templates.md](references/projects-templates.md) | +| `auth` (login / logout / status / list / switch / default), `license` (activate / return / server), `cloud` (org / project) | [auth-license-cloud.md](references/auth-license-cloud.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), `releases`, `templates` (list / info / create / pack / delete) | [projects-templates.md](references/projects-templates.md) | | `config` (proxy / update-check), `hub install` | [config-hub.md](references/config-hub.md) | | `run`, `test`, `build` | [build-run-test.md](references/build-run-test.md) | -| `logs`, `doctor`, `env`, `cache`, `analytics`, `changelog`, `language`, `completion`, `bug`, `upgrade`, `self-uninstall`, `diagnose proxy` | [diagnostics-maintenance.md](references/diagnostics-maintenance.md) | -| `mcp` (+ `configure`), connected editors (`pipeline` / `command` / `status` / `list`), `shell` | [integration-advanced.md](references/integration-advanced.md) | +| `logs`, `doctor`, `env`, `cache`, `analytics`, `changelog`, `language`, `completion`, `bug`, `self-update`, `self-uninstall`, `diagnose proxy` | [diagnostics-maintenance.md](references/diagnostics-maintenance.md) | +| `mcp` (+ `configure`), `skill` (install / refresh), connected editors (`pipeline` / `command` / `status` / `list`), `shell` | [integration-advanced.md](references/integration-advanced.md) | +| `collaboration` (alias `collab`) — `annotations` / `attachments` / `thumbnail` / `reactions` / `read` / `subscribe` / `jira` | [collaboration.md](references/collaboration.md) | ## Common workflows @@ -247,7 +249,51 @@ unity projects create "MyGame" --path ~/UnityProjects \ 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 -(UVCS uses your Unity sign-in, so no token is needed). See +(UVCS uses your Unity sign-in, so no token is needed). + +**Working with a UVCS workspace day to day: two wrapped reads, everything else straight through +to `cm`.** The split is deliberate and worth teaching, because guessing wrong wastes a user's time: + +- `unity vcs uvcs locks [path]` — who holds a lock, **and which locks cover files you have already + changed**. That join is the only thing here `cm` cannot do for you: it knows the repository's + locks and it knows your workspace's changes, but nothing puts them side by side, so without this + you learn a teammate holds a scene when your check-in is refused. Read-only, stamped with the + time it was taken (locks are shared state, so never treat a reading as current), and it prints + the exact `unity uvcs lock` command for anything worth acting on. +- `unity vcs uvcs changesets [path] [--limit ]` — recent history in a stable envelope for CI and + agents. Use it when something parses the output; use `unity uvcs log` when a human reads it. +- **Everything else is `unity uvcs `**, which forwards the whole command line to `cm` + verbatim, `--help` and `--format` included. That is the supported route, not a workaround: `cm` + owns and versions this vocabulary, so wrapping it would pin a paraphrase that goes stale. Reach + for it for **partial checkout**, **shelves**, and **taking or releasing a lock**. + +```bash +# Partial checkout (Gluon): work on part of a huge repository. cm's own flags, unchanged. +unity uvcs partial configure +unity uvcs partial update /Assets/Levels + +# Shelve work in progress, then bring it back. Again, cm's own vocabulary. +unity uvcs shelve -c "wip: lighting pass" +unity uvcs shelve --apply sh:12 + +# Locks: read them through the wrapper (it adds the join), mutate them through cm. +unity vcs uvcs locks # who holds what, and what collides with your changes +unity uvcs lock list # the raw listing, cm's own flags and output +unity uvcs lock unlock itemid:42@my-game # release someone's lock, if you are entitled to +``` + +`unity cm ` is the same passthrough under cm's own name. Both need the `cm` client; install +it with `unity plugin install plastic` if a command says it is missing. + +**Git tokens belong to the user's credential manager, not the CLI.** When no token flag or env var +is given, the CLI asks `git credential fill` and uses whatever the configured helper returns; it +stores nothing it is passed or told. Don't suggest the CLI can save a Git token, and don't reach for +a token flag when the user already has a working credential helper. If they want a different token +per organization, that is `git config --global credential.useHttpPath true` plus a multi-account +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 [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: @@ -358,9 +404,15 @@ unity test /path/to/MyProject \ --output ./test-results.xml \ --allow-install \ --timeout 600 -echo "Exit code: $?" # 0 = pass, 6 = test failures +case $? in + 0) echo "All tests passed" ;; + 8) echo "Tests failed — report to developers, do not retry" ;; + *) echo "Run did not complete — infrastructure failure, safe to retry" ;; +esac ``` +Exit `8` means the run finished and reported failing tests; any other non-zero code means it never produced a verdict. Under `--format json` the same split is `errors[0].code`: `TESTS_FAILED` versus `TEST_RUN_ERROR` / `TEST_TIMED_OUT`. + `--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). ### Debug the CLI @@ -384,6 +436,6 @@ unity logs --follow --level info - 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. -- The CLI is currently in **beta** (latest: `1.0.0-beta.4`). It moved to 1.0 versioning at `1.0.0-beta.1`; it's still a beta, so keep `UNITY_CLI_CHANNEL=beta` in the install command until GA ships, after which that part can be dropped. +- The CLI is currently in **beta** (latest: `1.0.0-beta.8`). It moved to 1.0 versioning at `1.0.0-beta.1`; it's still a beta, so keep `UNITY_CLI_CHANNEL=beta` in the install command until GA ships, after which that part can be dropped. - 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. - 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 0d2cd1d..0ca4861 100644 --- a/skills/unity-cli/references/auth-license-cloud.md +++ b/skills/unity-cli/references/auth-license-cloud.md @@ -30,10 +30,47 @@ unity auth login --client-id --secret-from-stdin --no-store # Logout (clears both service-account and OAuth credential slots) unity auth logout +# Log a specific stored account out, rather than the active one +unity auth logout user@example.com + # Skip the confirmation prompt unity auth logout --yes ``` +#### Multiple accounts + +The CLI stores more than one signed-in account and keeps one of them *active*. `unity auth login` adds an account; these three manage the set. + +```bash +# List stored accounts; "*" marks the active one +unity auth list +unity auth ls # alias +unity auth list --format json + +# Make a stored account active (by email or id) — no browser round-trip +unity auth switch user@example.com + +# Show the account a project uses for cloud commands +unity auth default + +# Pin this project to an account, regardless of which one is active +unity auth default user@example.com + +# Target a project other than the current directory +unity auth default user@example.com --project ./MyGame + +# Remove the pin; the project follows the active account again +unity auth default --clear +``` + +Three behaviors worth knowing before scripting these: + +- **A project pin beats the active account.** If a project has a default set, commands run inside it keep using that account even after `unity auth switch` — the switch reports this rather than failing silently. Use `auth default --clear` to hand the project back to the active account. +- **Service-account credentials outrank both.** When `UNITY_SERVICE_ACCOUNT_ID` / `UNITY_SERVICE_ACCOUNT_SECRET` are set (or a service account is signed in), they take precedence over every stored account and `auth switch` says so instead of appearing to work. Unset them, or `unity auth logout`, before switching. +- **`auth switch` is ambiguity-aware.** Given a string matching several stored accounts it fails rather than guessing, and under `--format json` carries the candidates in `data.candidates` so a script can disambiguate. Pass the full email or the account id. + +`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. + **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.) **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. @@ -97,12 +134,30 @@ unity cloud org set-default # set active default org unity cloud org clear-default # revert to "All Organizations" # Projects in the active organization -unity cloud project list --format json +unity cloud project list --format json # * marks the active default project + +# Default project, stored per organization +unity cloud project current # print the active default project id +unity cloud project set-default # project UUID, Genesis id, or exact name +unity cloud project clear-default # drop this organization's default # Override the active organization for a single call unity cloud project list --cloud-org # also via UNITY_CLOUD_ORG env var ``` +**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. + +**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 +stored default, so inside a cloud-linked project the link still wins. `unity collaboration` and +the `cloud-pipeline` preview family both use 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. --- diff --git a/skills/unity-cli/references/build-run-test.md b/skills/unity-cli/references/build-run-test.md index 8e4d914..55e0445 100644 --- a/skills/unity-cli/references/build-run-test.md +++ b/skills/unity-cli/references/build-run-test.md @@ -131,9 +131,14 @@ unity test /path/to/MyProject --report-format nunit,junit --junit-output ./resul # Collect code coverage (requires com.unity.testtools.codecoverage in the project) unity test /path/to/MyProject --coverage --coverage-output ./coverage unity test /path/to/MyProject --coverage --coverage-options "generateHtmlReport" + +# Split the suite across parallel CI jobs (seed the inventory with one full run first) +unity test /path/to/MyProject # writes test-results.xml +unity test /path/to/MyProject --shard 2/5 # writes test-results.shard-2-of-5.xml +unity test /path/to/MyProject --shard 2/5 --shard-inventory ./ci/full-run.xml ``` -`unity test` launches the editor's built-in test runner in batch mode (`-runTests -testPlatform -testResults -testFilter `), waits for it to finish, and writes the report to `--output` (default `test-results.xml`). It exits 0 when the run succeeds and 6 (EXIT_COMMAND_FAILURE) when the editor exits non-zero — i.e. reports test failures or fails to run. It runs the tests **directly via the editor command line** — no pipeline package or server is involved. `--mode` is optional; when omitted, `-testPlatform` is not passed and the editor runs its default platform. +`unity test` launches the editor's built-in test runner in batch mode (`-runTests -testPlatform -testResults -testFilter `), waits for it to finish, and writes the report to `--output` (default `test-results.xml`). It exits 0 when every test passes, 8 (EXIT_TESTS_FAILED) when the run completed and reported failing tests, and 6 (EXIT_COMMAND_FAILURE) when the run never produced a verdict — a compile error, an unavailable license, an editor crash, an unknown test platform, or `--timeout`. That split is what lets CI retry an infrastructure failure without ever retrying a failing test; under `--format json` it also appears as `errors[0].code` (`TESTS_FAILED` versus `TEST_RUN_ERROR` / `TEST_TIMED_OUT`), so pipelines never have to match the localized message. It runs the tests **directly via the editor command line** — no pipeline package or server is involved. `--mode` is optional; when omitted, `-testPlatform` is not passed and the editor runs its default platform. 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). @@ -153,12 +158,57 @@ All of these flag-combination mistakes, and an unknown `--report-format` value, **The JUnit report is written even when tests fail**, before the non-zero exit is surfaced — that is exactly when a CI system needs it to annotate the failures. A run whose results cannot be converted (a truncated report from an editor that died mid-write, say) fails the command and names the file it could not read. +#### GitHub Actions annotations (`--format github`) + +`--format github` is a global format value — accepted on every command, and settable via `UNITY_FORMAT` — that emits [GitHub Actions workflow commands](https://docs.github.com/actions/reference/workflow-commands-for-github-actions) for failures instead of plain terminal text. It is listed here because CI is where it earns its keep: + +```bash +unity test /path/to/MyProject --format github +unity build --target StandaloneWindows64 --output-path ./Build/Game.exe --format github +``` + +- **A failing command annotates the job.** Its error message is emitted as `::error::`, and a warning as `::warning::`, so the runner surfaces it as an annotation on the run rather than leaving it buried in the log. +- **Annotations go to stdout**, which is where GitHub's own documented examples emit them. +- **Human progress output still renders**, so the run stays readable — only the error and warning channels change shape. +- **Annotations are not anchored to a file and line yet.** Harvesting compile diagnostics out of the editor log (`::error file=…,line=…,col=…`) and wrapping that log in a collapsible `::group::` are not part of this format today, so a compile error annotates with the command's message rather than landing on the diff. + +This is complementary to `--report-format junit`, not an alternative: a JUnit file needs an upload-and-report step and surfaces in a separate tab, while annotations need neither. Using both together is reasonable — and today the JUnit report is what gets you per-test detail. + +Outside GitHub Actions the format is inert — the `::`-prefixed lines are ordinary text to any other terminal or log collector, so a local `--format github` run is reproducible and harmless. + +> Every value is sanitized and percent-encoded before it reaches a workflow command. The protocol is line-oriented and the runner does not escape anything, so an unescaped newline in an editor-supplied message would end the command and let the rest be interpreted as a new one. Never hand-build `::` lines around the CLI's output. + #### Code coverage `--coverage` drives Unity's [Code Coverage package](https://docs.unity3d.com/Packages/com.unity.testtools.codecoverage@latest) by passing `-enableCodeCoverage -coverageResultsPath ` (plus `-coverageOptions` when `--coverage-options` is given). `--coverage-output` defaults to `CodeCoverage` relative to the working directory. Coverage **degrades gracefully**: if the project does not depend on `com.unity.testtools.codecoverage` (checked in `Packages/manifest.json`, then `Packages/packages-lock.json`), the CLI prints a warning naming the missing package, skips the coverage flags, and runs the tests normally. It never fails the test run for a missing coverage package — `-enableCodeCoverage` on a project without it silently produces nothing, which is the confusing outcome this replaces. `--coverage-output` / `--coverage-options` without `--coverage` is an error. +#### Sharding across parallel CI jobs + +`--shard N/M` runs one deterministic slice of the suite, so a matrix job can take `--shard 1/5` through `--shard 5/5` and finish in a fifth of the time. `N` is 1-based and must be between 1 and `M`; anything else is a usage error (exit **2**). + +**It needs an inventory first.** The editor has no way to list tests without running them, so the CLI reads the full test names out of an NUnit3 report an earlier full run already wrote. That report defaults to the unsharded `--output` path (`test-results.xml`), and `--shard-inventory ` points at one somewhere else — a cached CI artifact, say. Seed it by running `unity test` once without `--shard`. Without a readable inventory the command fails and says so rather than guessing; an inventory holding no test cases fails the same way. + +**The inventory must be NUnit, not JUnit.** The two formats spell the element differently (`test-case` against `testcase`), so a JUnit report reads as simply having no tests. Because `--report-format junit` makes `--output` *the JUnit report*, `--shard` without an explicit `--shard-inventory` is rejected up front in that combination — a usage error, exit **2** — rather than failing later as an empty inventory. Either point `--shard-inventory` at an NUnit report, or ask for both formats with `--report-format nunit,junit`, which keeps `--output` NUnit. Pointing `--shard-inventory` at a JUnit file by hand is caught too, and says so specifically. + +Assignment is a hash of each test's fully qualified name, which gives the properties a CI matrix depends on: + +- **Deterministic** — the same suite and shard count always produce the same assignment, so a failure reproduces on the shard that reported it. +- **Complete and disjoint** — every test in the inventory belongs to exactly one shard. Nothing is skipped and nothing runs twice. +- **Independent of outcome and timing** — nothing about a previous run's results or duration feeds the split, so it does not drift between runs. +- **Stable as the suite grows** — because the slice follows the name rather than a position in the file, adding a test leaves every other test on the shard it was already on. Changing `M` reshuffles everything, which is inherent to repartitioning. + +Precedence with the other options: + +| Option | When sharding | +|---|---| +| `--mode` | Unchanged — selects the platform, and is orthogonal to the split. | +| `--filter` | Applied **by the CLI**, to narrow the inventory before the shard is taken from what survives. It has to be: the editor accepts a single `-testFilter`, and on a sharded run the slice's explicit name list already occupies it, so a `--filter` passed through would be dropped. Filtering here also makes the `tests` reported in `--format json` the ones that will actually run. It accepts the editor's own syntax — a semicolon-separated list of full names or regular expressions, each optionally negated with `!`. Note this ordering does **not** rebalance anything: assignment hashes each name independently, so filtering before or after the partition gives the same membership, and a filter can still leave a shard empty. | +| `--output`, `--junit-output`, `--coverage-output` | Every artifact path gains a `.shard-N-of-M` suffix before its extension, so shards sharing a working directory never overwrite each other and a merge step can collect them by glob. | + +Two cases end the run early rather than producing something misleading. A slice with **no tests assigned** (more shards than tests) skips the editor entirely and exits 0 with a warning — launching with an empty filter would run the whole suite, turning `M` shards into `M` full runs. A slice whose assembled filter is **too long for a single editor argument** fails and tells you to raise the shard count, which shortens every slice. A test whose name contains a semicolon cannot be expressed in the editor's filter syntax at all, so it fails rather than silently not running. + With `--format json` the envelope reports every artifact, so a pipeline can locate them without guessing: ```json @@ -166,13 +216,54 @@ With `--format json` the envelope reports every artifact, so a pipeline can loca "projectPath": "/path/to/MyProject", "output": "/path/to/results.xml", "reports": { "nunit": "/path/to/results.xml", "junit": "/path/to/results.junit.xml" }, - "coverage": { "requested": true, "enabled": true, "output": "/path/to/coverage" } + "coverage": { "requested": true, "enabled": true, "output": "/path/to/coverage" }, + "shard": { "index": 2, "count": 5, "testCount": 2, "tests": ["Acme.Tests.A", "Acme.Tests.B"] } } ``` -`reports.junit` is `null` when JUnit was not requested, `reports.nunit` is `null` when only JUnit was. `coverage.requested` with `enabled: false` is the missing-package case. +`reports.junit` is `null` when JUnit was not requested, `reports.nunit` is `null` when only JUnit was. `coverage.requested` with `enabled: false` is the missing-package case. `shard` is **absent entirely** on an unsharded run, so output every existing script already parses is unchanged. -Options: `--mode EditMode|PlayMode`, `--filter `, `--output `, `--report-format nunit|junit|nunit,junit`, `--junit-output `, `--coverage`, `--coverage-output `, `--coverage-options `, `--editor-version ` (env `UNITY_EDITOR_VERSION`), `-e, --editor-path `, `-a, --architecture `, `--allow-install`, `--timeout ` (env `UNITY_TEST_TIMEOUT`). +#### Retrying failing tests and reporting flakes + +`--retries N` re-runs the tests that failed, up to `N` extra attempts, stopping as soon as they all pass. Retries are **off by default**; `N` must be a whole number, zero or more (anything else is a usage error, exit **2**). `--timeout` applies **per attempt**, not to the whole sequence. + +Only the failing tests are re-run — the CLI reads their names out of the NUnit report the run just wrote and hands them back to the editor as a single `-testFilter` — so a retry costs the failures, not another full pass. + +**The point is reporting flakes, not hiding them.** The final summary separates three outcomes: passed the first time, passed only on a retry (**flaky**), and failed every attempt. A flaky test does not silently turn the job green: the run exits **0**, because the tests do pass, and the flakiness is stated in the human output, in `--format json`, and in a file. Tests that never pass still exit **8**. + +**A test leaves the failing set only when a report says it passed.** The editor exits `0` for "ran them and they passed", for "matched none of them", and — it reads a filter entry as a name *or a regular expression* — for "matched a neighbouring test". A test that did not run, ran under a different name, was skipped, or was inconclusive is carried forward, because none of those contradict a failure; so is anything an attempt that failed again did not clear. When an attempt does not clear everything it was given, the survivors stay failing, the run exits **8**, and a warning says why. + +**A run that never produced a verdict is not retried.** A compile error, a crashed editor, an expired license or a `--timeout` all keep exit **6** and stop immediately, without spending the budget: there is no failing set to narrow to, and re-running the same broken configuration would only reproduce the same error. That is the split `unity test` already draws between exit `8` and exit `6`. + +Artifacts: + +| Path | Contents | +|---|---| +| `--output` (e.g. `test-results.xml`) | The **first** attempt's report — the full-suite record. Deliberately not rewritten to show a flaky test as passing, since that would hide what the retry found. | +| `test-results.attempt-2.xml`, `.attempt-3.xml`, … | One report per retry. Not kept under `--report-format junit` alone, where the NUnit reports live in a scratch directory discarded with the run. | +| `test-results.retries.json` | The machine-readable retry summary, written whenever a retry ran, passing or failing. It exists because a failing run's JSON envelope carries `data: null`, and the per-test attempt counts matter most when the run failed. | + +**A JUnit report is converted from the first attempt too**, so a run that went green on a retry still ships a JUnit file containing the original failure. Many CI systems gate on the ingested JUnit artifact rather than the exit code, and for those the flake still shows red — gate on the exit code, or on `retries.failed`, when you want a flake to pass. + +**Coverage is collected on the first attempt only.** A retry runs a handful of tests, so letting it write the same coverage path would replace the whole suite's coverage with that subset's. + +Under `--format json` the same summary appears as a `retries` block, **absent entirely** when `--retries` was not used or nothing failed. Only tests that failed at least once are listed; `passedFirstAttempt` counts the rest: + +```json +"retries": { + "requested": 2, + "attempts": 3, + "passedFirstAttempt": 41, + "flaky": [{ "test": "Maths.Calc.Adds", "attempts": 2 }], + "failed": [{ "test": "Maths.Calc.Divides", "attempts": 3 }] +} +``` + +`--rerun-failed` starts from the failures a **previous** run recorded in `--output` instead of running the whole suite, so a follow-up job can re-check just the failures of the first. It reads the same NUnit report `--shard` reads its inventory from, honours `--filter` (applied CLI-side, against the failing set), and can be combined with `--retries`. It writes to a derived path — `test-results.xml` becomes `test-results.rerun.xml` — rather than overwriting `--output`, which would replace the full-suite record with a partial one and shrink the inventory `--shard` reads from that same default path. + +Three cases end early rather than doing something misleading. **Nothing failed** → the editor is not started and the run exits 0 with a warning, because an empty test filter is no filter at all and launching would run everything. **`--report-format junit` alone** → rejected up front (exit **2**): `--output` is then the JUnit report, and the failing set is read from NUnit, so ask for `--report-format nunit,junit`. **`--shard`** → rejected (exit **2**): the editor accepts one test filter and each option needs it, so to retry a shard, run that shard again with `--retries`. + +Options: `--mode EditMode|PlayMode`, `--filter `, `--output `, `--report-format nunit|junit|nunit,junit`, `--junit-output `, `--shard `, `--shard-inventory `, `--retries ` (0-10), `--rerun-failed`, `--coverage`, `--coverage-output `, `--coverage-options `, `--editor-version ` (env `UNITY_EDITOR_VERSION`), `-e, --editor-path `, `-a, --architecture `, `--allow-install`, `--timeout ` (env `UNITY_TEST_TIMEOUT`). --- @@ -216,6 +307,9 @@ unity build /path/to/MyProject --profile "Windows Release" --output-path ./Build | `--versioning-strategy ` | `semantic`, `tag`, `custom`, or `none` (default: `none`). | | `--build-version ` | Explicit version string; only used with `--versioning-strategy custom`. | | `--allow-dirty-build` | Skip the uncommitted-changes guard (default: false). | +| `--timeout ` (env `UNITY_BUILD_TIMEOUT`) | Abort a build that runs longer than this many seconds, exit **6**. Disabled by default. | +| `--provenance-path ` | Where to write the provenance manifest. Default: beside the build output, or beside the log file when `--output-path` is not set. | +| `--no-provenance` | Do not write the provenance manifest. | **Android signing & export** (applied to Android targets only): @@ -234,8 +328,12 @@ Keystore flags are validated together. Secrets passed as command-line flags surf **Versioning** — `semantic` and `tag` derive the version from git tags/history; `custom` requires an explicit `--build-version`; a dirty working tree is rejected unless `--allow-dirty-build` is passed. +**Provenance manifest** — every build that reaches the editor writes a JSON manifest recording what produced it: editor version and changeset, resolved package set, target, profile, execute method, version stamp, git revision and dirty flag, CLI version, timestamps, and outcome. Failed builds get one too, with the exit code, so they stay diagnosable. It is redacted for publication — paths are project-relative, `--args`, the Android keystore flags, the editor's install location and the hostname are never written, a git package reference keeps its locator but loses any embedded credentials, and a `file:` dependency is recorded as `file:` — so it can be attached to a release next to the artifact. Under `--format json` / `--format ndjson` the path is reported as `data.provenance` (omitted when no manifest was written), on failed builds as well as successful ones. The git revision is captured before the build starts, so an artifact written into the project does not make the manifest claim the build came from a dirty tree. `--provenance-path` and a relative `--output-path` both resolve against the current directory, matching what the CLI hands Unity. A manifest that cannot be written warns instead of failing the build. Schema: `apps/cli/docs/build-provenance.md`. + **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. + ```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 --format json diff --git a/skills/unity-cli/references/collaboration.md b/skills/unity-cli/references/collaboration.md new file mode 100644 index 0000000..4060b0e --- /dev/null +++ b/skills/unity-cli/references/collaboration.md @@ -0,0 +1,477 @@ +# Collaboration — unity-cli command reference + +Part of the **`unity-cli`** skill. See that skill's `SKILL.md` for CLI install, global flags, +environment variables, exit codes, and common workflows. All global flags (`--format json`, +`--non-interactive`, `--proxy`, …) apply to every command below. **`--yes` is not a global flag** — +it is bound per-command elsewhere in the CLI and no collaboration command accepts it, so passing it +here is an unknown-option usage error (exit 2). Use `--non-interactive` to skip confirmations. + +--- + +`unity collaboration` (alias **`unity collab`** — both accepted everywhere; examples below use the +canonical name) manages Unity Collaboration resources: review **annotations** on project assets, +their **attachments** (files, sketches, spatial anchors), **Jira** integration, emoji +**reactions**, **thumbnails**, and per-thread read/notification state. + +**In-Editor counterpart.** These commands operate on the same annotation data as the +[`com.unity.cloud.collaboration.tools`](https://packages.unity.com/com.unity.cloud.collaboration.tools) +package, which lets users view, create, and reply to annotations from inside the Unity Editor — +including the 3D pins and sketch overlays whose payloads are described under +[Data model](#data-model). Install it through the Package Manager in a cloud-linked project; it is +**experimental** (latest `0.2.0-exp.1`), needs **Unity 6000.0+**, and pulls in +`com.unity.cloud.collaboration` — the service SDK, a separate package id — as a dependency. The CLI +needs no package: it talks to the collaboration service directly, so it works with or without the +Editor open. Annotations created either way are visible to both. + +### Shared behavior + +**Project scoping — `--project-id` 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 + +1. explicit `--project-id`, 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 +4. the stored default cloud project for your active organization + (`unity cloud project set-default`, see [auth-license-cloud.md](auth-license-cloud.md)). + +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 +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 +differently — see [Jira](#jira). + +**`--all` — auto-paginate.** `annotations list`, `annotations replies`, and `jira issues list` accept +`--all` to stream every page instead of one. It is **mutually exclusive with `--next` and +`--limit`** — passing either alongside it is an error. + +**`--full` and `--resolve-users`** (table output helpers): `--full` prints annotation/reply text +untruncated (whitespace still collapsed to one line); `--resolve-users` replaces user ids with +display names. `--resolve-users` is on `annotations list`/`replies`/`get`/`export` and +`attachments list`; `--full` only on `annotations list`/`replies`. + +**Delete confirmations.** `annotations delete`, `annotations delete-fields`, `attachments delete`, +`jira server delete`, and `jira project delete` prompt for confirmation (default **No**) only when +all three hold: output format is `human`, `--non-interactive` was not passed, and both stdin and +stdout are TTYs. In scripts/CI (piped output, `--format json`, or `--non-interactive`) they delete +immediately without prompting. + +**`key=value` flags — typed vs string.** Two repeatable pair collectors look identical but behave +differently: + +- `--metadata k=v` (typed): each value goes through `JSON.parse`, so `count=3` becomes the number + `3`, `done=true` a boolean, `tags=["a","b"]` an array. Unparseable values stay strings. To force + a numeric-looking string to stay a string, quote it as JSON: `--metadata 'k="2"'`. +- `--target-context k=v` (string-only): values are always kept as raw strings. + +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`, +`--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 | +| `--time` | `{"timeScale":1,"timeStamp":0}` | +| `--position` (attachments spatial) | `{"x":0,"y":0,"z":0}` | +| `--attachments` (annotations create) | JSON array of `{"type":"...", ...}` attachment objects | + +--- + +### Data model + +#### Root vs reply + +Every annotation — root or reply — is the same type. The distinguishing field is +`rootAnnotationId`: + +| Field | Root thread | Reply | +|---|---|---| +| `rootAnnotationId` | `null` | ID of the root annotation | +| `target` | asset or project path | more specific path, often includes `/files/` | +| `replyCount` | populated | `null` | +| `replyUserIds` | populated | `null` | +| `threadAttachmentsCount` | populated (thread-wide; see caveat below) | `null` | +| `hasDraftReply` | populated | `null` | +| `integrations` | `{}` or populated (Jira lives here) | `null` | +| `resolved` / `resolvedBy` | meaningful (thread-level op) | `null` | +| `camera` / `metadata` | minimal | rich — full viewer state snapshot | + +Thread-level operations (resolve/unresolve, subscribe/unsubscribe, Jira linking, thumbnails) act on +the root. Replies capture a richer viewport snapshot (`camera`, `metadata` with `materialOverride`, +lighting, grid state) because they usually represent a specific view at time of writing. Both root +and reply can independently hold `attachments`, `reactions`, and `hasThumbnail`. + +#### Target paths + +`target` always starts with `/projects//...`. The prefix sets the context: + +| Prefix | Context | Example | +|---|---|---| +| `assets/projects//...` | Asset Manager asset | `assets/projects//assets/` | +| `assets/projects//.../files/` | Specific file within an AM asset | `assets/projects//assets//files/mesh.fbx` | +| `unity/projects//...` | Unity Editor | `unity/projects//assets/` | + +`**` as a trailing segment matches all descendants (`annotations count` target arg). + +The two prefixes are **separate trees, and no glob spans both.** A project routinely holds +annotations under each, so any count or listing is scoped to whichever prefix you name — see the +`count` caveat under [Annotations](#annotations). + +#### Mention syntax in `--text` + +| Type | Syntax | Example | +|---|---|---| +| User | `:user[Display Name]{#userId}` | `:user[Alex Rivera]{#2475297437902}` | +| Asset | `:asset[Asset Name]{#assetId}` | `:asset[Unity Tower]{#68de9f5d476ac89c752cbf88}` | + +#### Attachment payload shapes + +`threadAttachmentsCount` on the root counts the **whole thread**, while `attachments list ` +returns only the attachments owned by that one annotation. A root reporting 5 can list just its own +single sketch, with the other four hanging off replies — that mismatch is expected, not a bug. To +reach them, list the replies (`annotations replies `) and call `attachments list` per reply id, +or read the `attachments` field directly via `annotations list --include-fields attachments`. + +**Don't index it.** `threadAttachmentsCount` is normally a number (or `null`), but a per-type object +(`{ "sketch": 4, "spatial-3d": 3, "file": 1 }`) also shows up in real payloads. Guard the type +before reading it rather than assuming either shape. + +Every attachment object also carries its type under **two** keys, `type` and `Type`, with the same +value — the API emits both and the CLI passes responses through verbatim. Key off lowercase `type`; +that is what the formatters use. + +**`sketch`** — 2D drawing overlay captured over a 3D viewport. `sketchData` is a JSON *string* +(stroke/arrow records with positions, colors, widths): +```json +{ + "type": "sketch", + "attachmentId": "689f4236496f6d50dcbd6e20", + "sketchData": "", + "camera": { "position": {}, "rotation": {}, "fieldOfView": 60, "target": {}, "projection": "perspective" }, + "preview": { "filePath": "..._preview.png", "fileSize": 42770, "contentType": "image/png", "status": "Uploaded" }, + "sketchImage": { "filePath": "..._sketch.png", "fileSize": 42770, "contentType": "image/png", "status": "Uploaded" }, + "metadata": { "materialOverride": "default", "wireframe": -1 }, + "created": "2025-08-15T14:20:38.806Z", + "createdBy": "2475297437902" +} +``` + +**`spatial-3d`** — numbered 3D pin on a mesh in world space. Multiple pins per annotation, each +with an incrementing `label`. `camera.target` points at the pin's `position`; no +`preview`/`sketchImage` (it's a point, not an image): +```json +{ + "type": "spatial-3d", + "attachmentId": "6a1effe63e164caf9cea8aee", + "label": "1", + "position": { "x": -0.403, "y": 2.763, "z": 0.066 }, + "camera": { "position": {}, "rotation": {}, "fieldOfView": 60, "target": { "x": -0.403, "y": 2.763, "z": 0.066 } }, + "metadata": { "materialOverride": "default", "wireframe": -1 }, + "created": "2026-06-02T16:08:06.935Z", + "createdBy": "2475297437902" +} +``` + +**`file`** — generic upload (image, document). The annotation's top-level `camera` is `null` for +these — not tied to a 3D viewport: +```json +{ + "type": "file", + "attachmentId": "6a1effcfe9693f7f4d84ad13", + "filePath": "qa_no_replies.jpg", + "fileSize": 348842, + "fileType": "image", + "contentType": "image/jpeg", + "status": "Uploaded", + "metadata": {}, + "created": "2026-06-02T16:07:43.657Z", + "createdBy": "2475297437902" +} +``` + +--- + +### Annotations + +An annotation is a review comment anchored to a target path (e.g. +`unity/projects//assets/`). A **reply** is an annotation whose +`rootAnnotationId` points at the thread root — `create --reply-to ` makes one, and +`replies ` lists a thread. Status lifecycle: `Draft` → `Sending` → `Active`. + +Annotation objects returned by `get`/`list`/`replies` (`--format json`) carry: `annotationId`, +`messageType`, `target`, `targetContext`, `rootAnnotationId`, `status`, `text`, `created`/ +`createdBy`, `updated`/`updatedBy`, `resolved`/`resolvedBy`, `metadata`, plus include-only fields +(below). + +| Command | Args | Key options | +|---|---|---| +| `count` | `[target]` (glob `**` at end OK) — **defaults to `unity/projects//**` only**, see below | `--grouped` (per-target breakdown), `--offset `, `--limit ` | +| `create` | `` | `--text`, `--reply-to `, `--status Active\|Draft`, `--metadata k=v`…, `--target-context k=v`…, `--camera`, `--local-space`, `--time`, `--attachments`, `--unresolve-root-annotation` | +| `delete` | `` | (confirmation — see Shared behavior) | +| `delete-fields` | ` ` | removes metadata fields; variadic; confirmation | +| `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 | +| `get` | `` | `--fields a,b,c` or `--fields all` (table output only), `--resolve-users` | +| `list` | — | `--query ` (optional — defaults to root threads only), `--next `, `--limit 1-100` (default 10), `--all`, `--sort Ascending\|Descending`, `--sort-field annotationId\|latestReply`, `--include-fields a,b`, `--fields a,b` or `--fields all` (table output only), `--full`, `--resolve-users` | +| `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` | +| `resolve` / `unresolve` | `` | — (echoes only `annotationId`, see below) | +| `status` | ` ` | — | +| `update` | `` | `--text`, `--metadata k=v`…, `--camera`, `--local-space`, `--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" \ + --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" + +# List root threads (default query) from inside a cloud-linked project — no --project-id 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 \ + --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 \ + --target "assets/projects/$PROJ/**" --out annotations-assets.json +unity collaboration annotations export --project-id $PROJ \ + --target "unity/projects/$PROJ/**" --out annotations-editor.json +``` + +**`export` is not a whole-project export.** With no `--target` the command defaults to +`assets/projects//**`, so annotations under the separate `unity/projects//**` +tree are silently absent from the archive — and nothing in the output says so. Always pass `--target` +explicitly, once per prefix, when completeness matters. (Note the default differs from +`annotations count`, which defaults to the `unity/**` tree instead.) The CLI's own `--target` help +text says "the whole project", which contradicts the actual default — trust the prefix above. + +**`--query` shape.** A JSON *array* of clauses (a bare object is rejected: +`The --query value must be a JSON array of query clauses.`). Clause vocabulary is the +collaboration API's — e.g. `{"type":"hasNot","field":"annotationParentId"}`. Omitting `--query` +applies exactly that root-threads-only clause. + +A supplied `--query` **replaces** that default rather than adding to it, so a lone filter clause +(e.g. `[{"type":"glob","field":"target","value":"assets/**"}]`) returns replies interleaved with +roots. Re-add `{"type":"hasNot","field":"annotationParentId"}` alongside your clause to keep +thread-roots-only results. + +**Include-only fields.** `replyCount`, `replyUserIds`, `latestReply`, `attachments`, +`threadAttachmentsCount`, `replyLastReadTimestamp`, and `replyUnreadCount` come back null/absent +unless named in `--include-fields` on `list` (server omits them by default). If `replyCount` is +unexpectedly null, that's why. + +**`delete-fields`** removes **`metadata` sub-keys**, not top-level annotation fields — the variadic +args are metadata key names (`delete-fields severity build`). A name that isn't a metadata key +is a silent no-op (it is echoed back in `data.fields` and the command still exits 0), including +`metadata` itself: passing it does **not** clear the object. + +**`count` with no target counts one prefix tree only.** It defaults to +`unity/projects//**`, so it reads like a project-wide total but omits everything under +`assets/projects//**` — in a project with annotations on both, the bare command can +report 16 while 34 more exist. Pass the target explicitly (once per prefix) when you want a real +total, and prefer `--grouped` to see which trees are populated. + +**The mutators echo ids, not the annotation.** `resolve` and `unresolve` return just +`{ "annotationId": … }`; `status` adds `status`, and `delete-fields` adds the `fields` it was asked to +remove. None return a `resolved` timestamp or the updated object. A read-after-write therefore needs a follow-up `get`; an id-only response is success, not a +silent failure. Re-resolving an already-resolved thread is an error (`HTTP 409 … is already +resolved`), which is one way to confirm the first call landed. + +--- + +### Attachments + +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`. + +| Command | Args | Key options | +|---|---|---| +| `list` | `` | `--resolve-users` | +| `delete` | ` ` | (confirmation — see Shared behavior) | +| `download` | ` ` | `--out ` (default: the attachment's original filename in CWD, falling back to `` when it has no file path), `--force` (overwrite), `--width ` (resize image) | +| `upload` | ` ` | `--name` (display name), `--content-type` (override inferred MIME) | +| `add file` | ` ` | same options and **same handler** as `upload`; only the reported command label, the success message, and the JSON error code (`COLLAB_ATTACHMENTS_ADD_ERROR`) differ — use either | +| `add sketch` | `` | `--sketch-data ` **(required)**, `--camera ` **(required)**, `--time `, `--preview `, `--sketch-image ` | +| `add spatial` | `` | `--label` **(required)**, `--position ` **(required)**, `--camera ` **(required)**, `--time `, `--local ` | +| `update [file]` | ` ` | `--content-type`, `--metadata k=v`… — **at least one required**; `file` is the **default variant**: `update ` without a subcommand means `update file` | +| `update sketch` | ` ` | `--sketch-data`, `--camera`, `--time`, `--metadata k=v`… — each individually optional, but **at least one required** | +| `update spatial` | ` ` | `--label`, `--position`, `--camera`, `--time`, `--local`, `--metadata k=v`… — each individually optional, but **at least one required** | + +```bash +# Attach a screenshot (upload and `add file` are interchangeable) +unity collaboration attachments upload $ANNOTATION_ID ./screenshot.png --project-id $PROJ + +# Add a labeled 3D anchor +unity collaboration attachments add spatial $ANNOTATION_ID --project-id $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 \ + --out ./shot.png --force +``` + +Notes: + +- `--sketch-data` is passed through as a raw string, not parsed as JSON — only `--camera`/`--time`/ + `--position`/`--local` get JSON validation at the CLI layer. +- 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). + +--- + +### Reactions, thumbnails, read state + +All use the same optional project resolution as `annotations` — `--project-id` or `--project-path`, +else inferred from the current project (see [Shared behavior](#shared-behavior)). + +| Command | Args | Key options | +|---|---|---| +| `reactions add` / `reactions remove` | ` ` | emoji is a single Unicode emoji, e.g. `👍` | +| `thumbnail upload` | ` ` | image file; MIME inferred from extension (jpg/png/gif/webp) | +| `thumbnail download` | `` | `--out ` (default `./thumbnail`), `--width `; **no `--force`** — errors if the file exists ("Delete it first") | +| `read` | `` | `--timestamp ` (default now) — marks the thread read up to that time (per-user read receipt) | +| `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 +``` + +--- + +### Jira + +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`). +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 + link lands in `annotation.integrations.jiraIssues[]` — see + [Jira integration payload](#jira-integration-payload) below. + +**Scoping — most of `jira` does not use the project resolver** (no `--project-path`, no inference): + +| 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) | + +**`--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 +distinction is written down. What *does* differ is where a missing option is caught, and therefore +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 | + +`jira project link` / `unlink` take positionals instead, so a missing id is always a parse-time +usage error. + +#### `jira server` — server configurations + +| Command | Args | Key options | +|---|---|---| +| `add` | — | `--organization-id`, `--url`, `--username`, `--key` (API token), `--name` — all required | +| `delete` | `` | `--organization-id` (required); confirmation | +| `update` | `` | `--organization-id` (required) + at least one of `--url`/`--username`/`--key`/`--name` | +| `test` | — | `--organization-id`, `--url`, `--username`, `--key` — all required; validates credentials **without persisting** | +| `users` | `` | `--organization-id` (required), `--query ` — search Jira users | +| `projects` | `` | `--organization-id` (required) — lists **Jira-side** projects on the server | +| `permissions` | `` | `--organization-id`, `--jira-project-id` — both required; checks required Jira permissions | + +#### `jira project` — project configurations + +| Command | Args | Key options | +|---|---|---| +| `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) | +| `delete` | `` | `--organization-id` (required, not marked in `--help`); confirmation | +| `link` / `unlink` | ` ` | — (Unity project id is positional here, not a flag) | +| `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 issues` + +| Command | Args | Key options | +|---|---|---| +| `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) | +| `get` | `` | `--jira-project-config-id` required; `--project-id`/`--project-path` optional (inferred) | +| `link` / `unlink` | ` ` | `--project-id`/`--project-path` optional (inferred); `link` also takes optional `--jira-project-config-id`. `unlink` does **not** delete the issue in Jira | +| `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` | +| `search` | — | `--jira-project-config-id` required; `--project-id`/`--project-path` optional (inferred); `--query ` (plain text, **not JQL**), `--include-subtasks` | +| `types` | — | `--jira-project-config-id` required; `--project-id`/`--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). + +```bash +# One-time setup: validate credentials, persist server, add a Jira project, link Unity project +unity collaboration jira server test --organization-id $ORG \ + --url https://jira.example.com --username bot@example.com --key $JIRA_TOKEN +unity collaboration jira server add --organization-id $ORG \ + --url https://jira.example.com --username bot@example.com --key $JIRA_TOKEN --name "Main Jira" +unity collaboration jira project add $SERVER_CONFIG_ID --organization-id $ORG \ + --jira-project-id 10042 --default-reporter-id $JIRA_ACCOUNT_ID +unity collaboration jira project link $PROJ $PROJECT_CONFIG_ID + +# File an issue from an annotation (get valid type ids from `issues types` first) +unity collaboration jira issues create $ANNOTATION_ID --project-id $PROJ \ + --jira-project-config-id $PROJECT_CONFIG_ID --summary "Texture seam" --type 10001 +``` + +#### Jira integration payload + +Lives in `annotation.integrations.jiraIssues[]` on the root (`integrations` is `null` on replies). +Multiple issues can be linked to one thread. + +```json +{ + "integrations": { + "jiraIssues": [ + { + "type": "Jira", + "jiraIssueId": "18955", + "jiraProjectConfigId": "6997204de238d85bc249625b", + "jiraIssueKey": "PROJ-1", + "jiraIssueUrl": "https://yourcompany.atlassian.net/browse/PROJ-1", + "sourceAnnotationId": "698e04cb85335d04c814ea67", + "createdBy": "2474131300352" + } + ] + } +} +``` + +- `jiraIssueKey` — human-readable key (e.g. `PROJ-1`); use for display. +- `jiraIssueUrl` — direct link to the issue. +- `sourceAnnotationId` — the annotation the issue was created/linked from; may differ from the + annotation carrying the integration when linked from a reply. + +--- diff --git a/skills/unity-cli/references/diagnostics-maintenance.md b/skills/unity-cli/references/diagnostics-maintenance.md index 54da1e4..3ce0b15 100644 --- a/skills/unity-cli/references/diagnostics-maintenance.md +++ b/skills/unity-cli/references/diagnostics-maintenance.md @@ -44,7 +44,38 @@ unity doctor --format json unity doctor --tail 50 ``` -`unity doctor` reports real session state (matching `unity auth status`) and surfaces the resolved proxy URL, its source, and auth source. It also runs environment health checks and reports pass/warn per check (in every output format): whether the `unity` binary's directory is actually on `PATH` (the top post-install pitfall on Windows, where a new terminal is needed), whether multiple `unity` binaries shadow each other on `PATH`, and whether Windows long-path support is enabled. +`unity doctor` reports real session state (matching `unity auth status`) and surfaces the resolved proxy URL, its source, and auth source. It also runs environment health checks and reports pass/warn per check (in every output format): whether the `unity` binary's directory is actually on `PATH` (the top post-install pitfall on Windows, where a new terminal is needed), whether multiple `unity` binaries shadow each other on `PATH`, whether Windows long-path support is enabled, and whether a git credential helper is configured (`git-credential-helper`: advisory for the git-token flows in `projects clone`/`create`/`link vcs`; the row is omitted on machines without git). + +--- + +### Doctor --ci — preflight before a long pipeline step + +```bash +# First step of a CI job: fail in seconds instead of after a long build +unity doctor --ci + +# Per-check results a workflow can branch on +unity doctor --ci --format json + +# Failed checks become inline annotations on the pull request +unity doctor --ci --format github +``` + +`--ci` replaces the diagnostic report with a **preflight**: it verifies the environment can actually finish a build or test run and exits non-zero when it cannot, so a pipeline fails fast rather than tens of minutes into a build. It checks an activatable license, the project's required editor, free disk space, and reachability of the Unity services endpoint, and folds the `PATH` / long-path / git-credential-helper checks in as advisory rows that never fail a job. + +Exit codes distinguish the two kinds of bad news, so a workflow can retry only what is worth retrying: + +| Exit | Meaning | +|---|---| +| `0` | Every blocking check passed. Warnings do not fail the preflight. | +| `6` | A definitive failure — no license, the required editor is not installed, disk below the floor. Retrying will not help. | +| `7` | The preflight could not reach a verdict because a required service was unreachable. Worth a retry. | + +A `6` outranks a `7` when both occur, so a real blocker is never reported as retryable. + +Every check carries a machine-readable `code` (`LICENSE_NONE`, `EDITOR_NOT_INSTALLED`, `DISK_SPACE_LOW`, `NETWORK_UNREACHABLE`, …) plus a remediation `hint`. In `--format json` the per-check results stay in `data` even on failure, with one coded entry per failure in `errors`. Output is redacted and carries no tokens and no absolute user paths, so it is safe to paste into a public CI log. + +`--ci` is always explicit — it is never inferred from `CI=true`, because a report that silently changed shape and exit code on a runner would be a trap. Note that the CLI already defaults to `--format tsv` whenever stdout is redirected, which in CI it usually is; that output leads with a `verdict` row. --- @@ -85,6 +116,57 @@ unity cache clean --yes --- +### Cache key — deterministic key for CI cache steps + +`unity cache key` prints one hash derived from the inputs that actually invalidate a project's build cache: the editor version, the resolved package set, and (optionally) the build target. Use it as the `key:` of a CI cache step instead of hand-rolling the hashing. + +```bash +# Print the key for the project in the current directory +unity cache key + +# Scope it to a build target — a Library/ folder is platform-specific +unity cache key --target Android + +# Any project path +unity cache key ./MyProject +``` + +Whenever stdout is not an interactive terminal — a pipe, a redirect, or any CI runner — the key is the only thing written to it, so it drops straight into a shell substitution or a workflow expression. (On an interactive colour terminal the CLI's usual one-line banner still prints above it, as it does for every command; `--quiet` suppresses that.) + +Pass the project path if the workflow's working directory isn't the project: + +```yaml +- id: cachekey + run: echo "key=$(unity cache key MyProject --target Android)" >> "$GITHUB_OUTPUT" +- uses: actions/cache@v4 + with: + path: MyProject/Library + key: Library-${{ steps.cachekey.outputs.key }} +``` + +What moves the key, and what doesn't: + +- **Changes** when the editor version (`ProjectSettings/ProjectVersion.txt`), the package set (`Packages/packages-lock.json`, falling back to `Packages/manifest.json`), or `--target` changes. +- **Does not change** for unrelated edits — scenes, scripts, assets, or project settings other than the version file. +- **Is identical across machines and operating systems** for the same inputs. Line endings and a UTF-8 BOM are normalized before hashing, so a Windows checkout with `core.autocrlf` and a Linux one agree. No paths, usernames, or timestamps enter the hash. + +`--format json` returns the key plus each component's raw value and hash, so one invocation can build a layered key with fallback levels: + +```bash +unity cache key --target Android --format json +``` + +`--component ` prints just one component's hash for the fallback levels themselves. Under `--format json` it also sets `data.key` to that component, so `jq -r .data.key` means the same thing either way: + +```bash +# Restore any cache built for this editor, whatever the packages were +unity cache key --component editor +``` + +Runs without an editor and without network access. Exit 2 on an unknown `--target` or `--component` (a silently-accepted typo would produce a key nothing else matches); exit 6 when the directory isn't a Unity project. A project with neither package file still emits a key, with a warning that it doesn't cover the package set. + +--- + ### Analytics — usage/telemetry consent The CLI defaults to **opt-out**. On the first interactive run a prompt is shown once before any data is collected; it now requires an explicit `y` or `n` — pressing Enter alone re-asks instead of silently recording the opt-out default, so an accidental keystroke can't lock in an answer. Ctrl-C skips the prompt and keeps the opt-out default. Non-interactive, CI, piped, and `--quiet` contexts silently keep the opt-out default. @@ -103,7 +185,7 @@ unity analytics opt-in unity analytics opt-out ``` -Consent is stored in the shared Hub privacy preferences, so opting out in the CLI also opts out in Hub, and vice versa. When opted **in**, the CLI records which commands run (registered command names only — never your arguments, paths, or project names), editor uninstalls, project open/create (editor version and template id only), CLI self-upgrade/uninstall outcomes, `unity shell` and `unity mcp` session usage, and `unity doctor` / `unity bug` results. When opted out (the default), no events are sent. +Consent is stored in the shared Hub privacy preferences, so opting out in the CLI also opts out in Hub, and vice versa. When opted **in**, the CLI records which commands run (registered command names only — never your arguments, paths, or project names), editor uninstalls, project open/create (editor version and template id only), CLI self-update/uninstall outcomes, `unity shell` and `unity mcp` session usage, and `unity doctor` / `unity bug` results. When opted out (the default), no events are sent. Separately from analytics, 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 so crash-free-user rates can be computed; opted-out users stay fully anonymous. Set `UNITY_NO_CRASH_REPORT` to disable crash reporting entirely. @@ -184,41 +266,41 @@ Interactively, when you don't pass `--attachments` or `--share-project`, the rep --- -### Upgrade — update the CLI itself +### Self-update — update the CLI itself ```bash # Check for available updates -unity upgrade --check --format json +unity self-update --check --format json # Show changelog for the new version -unity upgrade --changelog +unity self-update --changelog -# Upgrade (interactive confirmation) -unity upgrade +# Update (interactive confirmation) +unity self-update -# Upgrade without prompts -unity upgrade --yes +# Update without prompts +unity self-update --yes # Install a specific version -unity upgrade --target 0.2.0 +unity self-update --target 0.2.0 # Select update channel (stable or beta) -unity upgrade --channel beta +unity self-update --channel beta # Dry-run: show what would change -unity upgrade --dry-run +unity self-update --dry-run # Rollback to previous version -unity upgrade --rollback +unity self-update --rollback ``` -`unity upgrade` detects how the CLI was installed and upgrades accordingly: +`unity self-update` detects how the CLI was installed and updates accordingly (`unity upgrade` is still accepted as an alias): -- **`curl | sh` install** — keeps upgrading itself in place. +- **`curl | sh` install** — keeps updating itself in place. - **Linux AppImage** — updates in place: downloads the new `.AppImage` artifact, verifies its checksum against the release manifest, and atomically replaces the AppImage you launched (`--rollback` restores the previous one). The embedded zsync update info is preserved, so external updaters (AppImageUpdate, Gear Lever) keep working. - **Package-manager install** — points you at the owning manager instead of replacing the binary. The `.deb` and `.rpm` packages are published to Unity's apt and rpm repositories on every beta and GA release (rpm packages are GPG-signed), so a package-managed install stays current through the system package manager: `sudo apt update && sudo apt upgrade unity-cli` on Debian/Ubuntu, `sudo dnf upgrade unity-cli` on Fedora/RHEL. -`--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 upgrade`; installs whose manager doesn't carry the release yet stay quiet. +`--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. --- diff --git a/skills/unity-cli/references/editors-install.md b/skills/unity-cli/references/editors-install.md index 271e92a..fe55024 100644 --- a/skills/unity-cli/references/editors-install.md +++ b/skills/unity-cli/references/editors-install.md @@ -125,6 +125,43 @@ unity editors upgrade 2022.3.10f1 --no-modules unity editors upgrade 2022.3.10f1 --module android --module ios ``` +#### editors prune + +Finds installed editors that **no registered project uses** and, optionally, uninstalls them. Report-only by default — it never deletes anything unless you pass `--remove`. + +```bash +# Report only: which editors are unused, and how much they'd reclaim +unity editors prune + +# Uninstall the unused editors (prompts to confirm) +unity editors prune --remove + +# Non-interactive: --yes is REQUIRED alongside --remove in a script or CI +unity editors prune --remove --yes + +# Machine output +unity editors prune --format json +``` + +The report lists version, architecture, path, size, and status, then the total reclaimable size. With `--remove` in a non-interactive shell and no `-y, --yes`, it refuses rather than deleting unprompted. "Unused" is judged against the **project registry** (`unity projects list`), so an editor used only by a project you never registered counts as unused — register it first, or verify with `unity editors prune` before adding `--remove`. + +#### editors verify + +Structurally verifies an installed editor: checks that its files and modules are actually present on disk. It's the command to reach for when an editor launches oddly, a module seems half-installed, or a download was interrupted. + +```bash +# Verify an installed editor +unity editors verify 6000.1.0f1 + +# Disambiguate when the same version is installed for two architectures +unity editors verify 6000.1.0f1 --architecture arm64 + +# Machine output +unity editors verify 6000.1.0f1 --json +``` + +Reports each component as `ok`, `missing`, or `skipped`, and names the exact `unity install-modules` command to repair anything missing. A clean editor exits 0; missing or empty files fail the check. This is a **structural** check — it confirms files exist, not that they are uncorrupted or correctly signed. `--architecture` is inherited from the `editors` parent, so `unity editors --architecture arm64 verify ` works too. + #### editors module / editor module Module management is exposed under **both** `editors module` and the `editor` (singular) command group. Both share the same subcommands: diff --git a/skills/unity-cli/references/integration-advanced.md b/skills/unity-cli/references/integration-advanced.md index 7ca5864..40e6d6a 100644 --- a/skills/unity-cli/references/integration-advanced.md +++ b/skills/unity-cli/references/integration-advanced.md @@ -6,6 +6,47 @@ environment variables, exit codes, and common workflows. All global flags (`--fo --- +## Targeting one of several running Editors + +`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. + +**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. + +> `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: + +``` +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. +``` + +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: + +```json +{ + "success": false, + "data": { + "candidates": [ + { "project": "Alpha", "projectPath": "/path/to/Alpha", "port": 38412, "pid": 4242 }, + { "project": "Beta", "projectPath": "/path/to/Beta", "port": 38413, "pid": 4243 } + ] + }, + "errors": [{ "code": "AMBIGUOUS_EDITOR", "message": "Multiple Unity Editors are running…" }] +} +``` + +`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`. + ### MCP — Model Context Protocol server (AI agent integration) New in `0.1.0-beta.8`. `unity mcp` starts a Model Context Protocol server, built into the `unity` binary, that exposes the commands of a connected Unity Editor as MCP tools. AI agent clients connect over stdio, list those tools, and run them. The server starts even when no Editor is running and reports that it isn't connected; commands that a connected Editor adds show up as tools automatically. @@ -43,6 +84,48 @@ unity mcp configure vscode --dry-run --- +### Skill — install this skill into an AI client + +`unity mcp configure` gives a client the Unity **tools**; `unity skill install` gives it these **docs**. The skill tree is embedded in the CLI binary at build time, so it always matches the installed CLI and needs no network access. + +```bash +# See the supported clients, their install paths, and current install status +unity skill install --list + +# Install into a client's user-global skills directory +unity skill install claude-code + +# Install into the current project instead of the user-global location +unity skill install cursor --local + +# Overwrite an existing install without prompting; preview without writing +unity skill install claude-code --yes +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. + +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. + +```bash +# Re-render every tracked install against the embedded skill tree +unity skill refresh + +# Non-interactive / preview +unity skill refresh --yes +unity skill refresh --dry-run +``` + +Every install is tracked, so `unity skill refresh` re-renders all of them at once and drops tracking for any whose location has since disappeared. **Run it after `unity self-update`** — the embedded skill ships with the binary, so an updated CLI leaves previously-installed copies stale until they're refreshed. + +Two safety behaviors: writing through a symlink is refused rather than followed, and `--local` from your home directory warns first, since for most clients that either duplicates the global install or writes somewhere the client never reads. + +If you last installed the Codex skill with an older CLI, `unity skill refresh` migrates it: it writes the skill directory, strips the old `AGENTS.md` block, and replaces the tracking entry. + +--- + ### Connected Editors — pipeline / command / status > **Promoted to production in `0.1.0-beta.8`.** In earlier betas these were development-only (and the Pipeline package was Unity-internal). They now talk to any running Unity Editor over its Pipeline server, and the supporting Editor-side package (`com.unity.pipeline`) is resolved from the **Unity (UPM) registry** and added to the project's `Packages/manifest.json` — no internal access or manual setup required. The Editor defines each command's parameters, help, and error messages, so the commands a connected Editor exposes are usable without a CLI update. @@ -83,7 +166,8 @@ unity command eval "return Application.unityVersion;" --project-path /path/to/My **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 `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). +`--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 @@ -158,6 +242,47 @@ unity command --runtime-path /path/to/port-file unity command editor_play --timeout 60 ``` +#### Querying the command list + +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 +unity command --query screenshot + +# Filter to a tag subtree +unity command --tag assets +unity command --tag assets/import + +# Compact rows instead of full detail +unity command --detail compact + +# Group the results +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 +``` + +| Flag | Values | Default | +|---|---|---| +| `--detail [level]` | `compact`, `full` | `full` | +| `--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` | +| `--sort [key]` | `name`, `package` | `name` | +| `--order [direction]` | `asc`, `desc` | `asc` | +| `--offset [n]` / `--limit [n]` | integers | — | + +Two traps worth knowing: + +- **`--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`. +- **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. + #### Available in production — the common live commands Everything reached through **`unity command `** is part of the project's `com.unity.pipeline` package and works against a normal, **production** Editor (or a Player runtime via `--runtime`) — it is *not* development-gated. Don't refuse a live-Editor task on the assumption that driving the Editor requires a development build — it doesn't. diff --git a/skills/unity-cli/references/projects-templates.md b/skills/unity-cli/references/projects-templates.md index 447450f..6f1676e 100644 --- a/skills/unity-cli/references/projects-templates.md +++ b/skills/unity-cli/references/projects-templates.md @@ -40,6 +40,8 @@ unity 6000.0.47f1 /path/to/MyProject The project argument is matched against the Hub registry first (exact name or path opens immediately; a glob like `"My Game*"` prompts when multiple match); with no registry match it falls back to treating the argument as a filesystem path. Path matching is tolerant of casing, separator direction, and a trailing slash — resolved against real filesystem path identity — so a registered project is found even when the path is spelled differently, while two genuinely distinct case-variant folders on a case-sensitive volume stay distinct. `unity open` forwards `--args` to the Editor correctly on all platforms (including Windows). +**Signed-in Editor, no Hub required.** `unity open` starts a small background identity helper that answers the Editor's account lookup with the session `unity auth login` stored — your account, organization list (so Package Manager entitlements resolve), and the service addresses for your resolved `--cloudEnvironment` — so a Hub-less machine gets a signed-in Editor instead of an anonymous one. It steps aside whenever a real Hub is running or starting, exits on its own a few minutes after the Editor stops using it, and can be disabled with `UNITY_NO_EDITOR_IDENTITY_SERVER`. Signed out, the Editor just starts anonymous, as before. + **Reserved flags — do NOT pass these via `--args`.** `-projectPath` is managed by the command (Unity's parser is last-wins, so forwarding it would silently redirect the open to a different project), and `-useHub`/`-hubIPC` are deliberately never passed — they tell the Editor a Unity Hub manages its session, which the CLI is not. Passing any of them fails fast, before launch, with exit code 6: ``` @@ -50,7 +52,7 @@ All three spellings Unity accepts are rejected (`-useHub`, `--useHub`, `-useHub= #### projects create -Create a project. On a TTY, prompts for any missing options (parent directory, editor version, template). In CI, pass `--non-interactive` or pipe stdin to suppress prompts and rely on stored defaults. The first positional argument is the project **name**; `--path` sets the parent directory: +Create a project. On a TTY, prompts for any missing options (parent directory, editor version, template) and then asks whether to link the project to a Unity Cloud project — that last question defaults to **No**, so pressing Enter creates an unlinked project. In CI, pass `--non-interactive` or pipe stdin to suppress prompts and rely on stored defaults. The first positional argument is the project **name**; `--path` sets the parent directory: ```bash unity projects create MyGame --editor-version 6000.0.47f1 --template com.unity.template.3d @@ -72,6 +74,14 @@ unity projects create MyGame --cloud --cloud-org unity projects create MyGame --cloud-project ``` +Passing any of `--cloud`, `--cloud-project`, or `--cloud-org` answers the cloud question, so it is not asked again. The question is also skipped in every machine output mode (`--json`, `--format tsv|ndjson`, `--quiet`), under `--non-interactive` (or `UNITY_NON_INTERACTIVE`), when stdout is not a TTY, and when the current credentials cannot create a cloud project (signed out, or service-account auth) — those keep today's flag-only, default-off behaviour. Unlike the other three questions, this one can fire even when every option was supplied on the command line, so `--non-interactive` is what keeps a fully-specified scripted run from stopping on it. Be aware that the global currently gates **only** this question: the parent-directory, editor-version, and template questions still gate on terminal interactivity alone, so `--non-interactive` on a TTY does not make them fall back to stored defaults. For a fully unattended run on a terminal, pass `--path`, `--editor-version`, and `--template` as well — or use `projects new`, which never prompts at all. + +Answering Yes never costs you the project: if the link cannot be set up (expired session, no resolvable organization), the project is still created unlinked and the reason is reported as a warning, exit 0. `--cloud` behaves differently and still fails outright — an explicit flag is a contract, not a suggestion. + +When a project is created without a cloud link, human output ends with a line pointing at `unity projects link cloud`. It is human-format only: `json`, `ndjson`, and `tsv` output is unchanged. + +For machine consumers, cloud state is reported by the presence of the `cloudLinked`, `cloudProject`, and `cloudOrgSource` fields on the result payload — they are emitted **only when a cloud link was requested**. Their absence is itself the signal that the project is unlinked; do not read `cloudLinked` expecting a `false`. + **Source-control during creation** — publish the new project to a fresh repository: ```bash @@ -84,10 +94,28 @@ unity projects create MyGame \ --git-token-stdin ``` -Source-control flags (shared with `projects link vcs`): `--vcs github|gitlab|uvcs`, `--git-namespace `, `--git-repo `, `--git-visibility private|public|internal` (default private), `--git-default-branch `, `--git-token ` / `--git-token-stdin`, `--no-initial-commit`, `--git-lfs`, and `--vcs-region ` for Unity Version Control. +Source-control flags (shared with `projects link vcs`): `--vcs github|gitlab|uvcs|`, `--git-namespace `, `--git-repo `, `--git-visibility private|public|internal` (default private), `--git-default-branch `, `--git-remote-protocol https|ssh` (default https), `--git-description `, `--git-token ` / `--git-token-stdin`, `--no-initial-commit`, `--git-lfs`, and `--vcs-region ` for Unity Version Control. **Flag names differ by subcommand:** `projects create` and `projects link vcs` use `--git-namespace` / `--git-repo`, while `projects clone` (below) uses `--vcs-namespace` / `--vcs-repo`. Copy the names for the exact command you're running, and confirm with `--help` if unsure. +**`--git-remote-protocol ssh` attaches the created repository's SSH remote instead of its HTTPS one.** The provider REST call that creates the repository still needs the resolved PAT — SSH has no equivalent for that API call — but the local `origin` remote and the initial push then use the repository's `git@:/.git` form with pure ambient SSH auth (the running ssh-agent, a repository-local `core.sshCommand`, or an `~/.ssh/config` host alias — whichever the machine already has configured; the CLI never handles keys itself). Passing `--git-token`/`--git-token-stdin` alongside `--git-remote-protocol ssh` is fine: the token still authenticates the repository-creation API call, it's only the git transport that switches. + +**Self-hosted hosts create through a provider CLI, not REST.** `--vcs` also accepts a bare host (e.g. `--vcs gitea.example.com`) for anything other than github.com/gitlab.com — there is no built-in REST client for those, so the repository is created through `gh`, `glab`, or `tea`, whichever is installed and already signed in for that host (checked in that order). The same fallback covers github/gitlab themselves when no REST token can be resolved: the matching CLI (`gh` for github, `glab` for gitlab) steps in if it is signed in, before the command gives up. No PAT is ever read, stored, or forwarded on a provider-CLI path. When a provider CLI created the repository, the machine-readable result carries `vcs.mechanism` (`gh` | `glab` | `tea`) naming which one — omitted for the REST path and for a `[url]`-form link, so existing scripted consumers see no change on github.com/gitlab.com. If nothing can create it — no REST token and no signed-in provider CLI for that host — the error explains creating the repository yourself and linking it with `unity projects link vcs `. + +**Where the Git token comes from.** Resolution order, first hit wins: `--git-token-stdin` → `--git-token` → `UNITY_GITHUB_TOKEN` / `UNITY_GITLAB_TOKEN` → `git credential fill` (the user's credential helper) → an interactive masked prompt. The first three are explicit per-command overrides and always beat the helper. **The CLI stores no Git token** at any tier, including one typed at the prompt. + +**Whether anyone is there to answer is decided up front.** On a real terminal, a configured credential helper is free to run its own sign-in — including Git Credential Manager's browser and device-code flows — and its instructions are relayed to you rather than swallowed. Without a terminal, in CI, under a machine-readable `--format`, or with `--non-interactive`, nothing prompts at all: the command fails immediately with **exit 4**, naming the credential it needed and how to supply it out of band (`--git-token[-stdin]` or the env vars above). Interaction is judged on all three standard streams, so redirecting stderr alone can no longer leave a password prompt writing into a file while waiting on a keystroke you were never shown. + +The credential lookup passes the full repository URL, so a helper that keeps one account per URL can return a different token per organization, but only when `credential.useHttpPath` is set, since git otherwise withholds the path from helpers: + +```bash +git config --global credential.useHttpPath true +``` + +Per-project identity instead lives in the repository's own config (`credential.useHttpPath`, `credential.username` in its `.git/config`); `projects link vcs` runs the lookup inside the project, so the Hub, the CLI, and plain `git` all resolve the same credential. For a CI pipeline spanning several organizations, pass `--git-token-stdin` per invocation: the env vars hold one token per provider, and there is no per-org variant. + +Per-organization scoping needs the organization to be known before the credential is looked up. `projects clone` always requires `--vcs-namespace`, so it is always scoped. `projects create` and `projects link vcs` take `--git-namespace`, and when it is omitted the lookup stays host-scoped, because the namespace cannot be resolved until you are authenticated and you cannot authenticate without a credential. Pass `--git-namespace` to target a specific organization's credential. + #### projects new Create a project without any interactive prompts — resolves missing options from stored defaults, never asks the user. The first positional argument is the project **name**; `--path` sets the parent directory: @@ -103,12 +131,14 @@ unity projects new MyGame --path /path/to/projects --editor-version 6000.0.47f1 unity projects new MyGame --open ``` +`new` never links to Unity Cloud and never asks. Its human output ends with the same pointer at `unity projects link cloud`; machine output is unchanged. To link during creation, use `projects create --cloud`, or link afterwards with `projects link cloud`. + #### projects clone Clone a remote repository and register the Unity project it contains. Works across providers: ```bash -# Clone by full repo URL / shorthand +# Clone by provider + namespace + repo unity projects clone --vcs github --vcs-namespace my-org --vcs-repo my-game --path ./MyGame # Check out a specific ref (branch, sha, or UVCS changeset) @@ -120,10 +150,138 @@ unity projects clone --vcs gitlab --vcs-namespace my-org --vcs-repo my-game --gi # Project lives in a subdirectory of the repo unity projects clone --vcs github --vcs-namespace my-org --vcs-repo monorepo \ --path ./repo --project-path packages/MyGame + +# Clone an arbitrary git URL instead (HTTPS, or SSH via the standard +# git user@host:path shorthand) — no --vcs/--vcs-namespace/--vcs-repo needed +unity projects clone https://github.com/my-org/my-game.git +unity projects clone --ref develop ``` Options: `--vcs github|gitlab|uvcs`, `--vcs-namespace `, `--vcs-repo `, `--ref ` (an all-digit ref is treated as a Unity Version Control changeset, anything else as a branch), `--path ` (clone destination), `--project-path ` (project subdirectory), `--git-token ` / `--git-token-stdin`, `--json`. Git LFS assets are fetched as pointer files only. +**The `[url]` form is an alternative to `--vcs`/`--vcs-namespace`/`--vcs-repo`, not an addition to them** — passing a URL alongside any of those three flags is a bad-arguments error. `--ref`, `--path`, `--project-path`, `--no-lfs` always apply. `--git-token`/`--git-token-stdin` only apply when the URL is an **explicit HTTPS** URL whose host is github.com or gitlab.com — passing a token for any other host (an SSH-form or SCP-style URL, or a host that isn't github.com/gitlab.com) is a bad-arguments error, since there's nowhere for that credential to go. No token is required: with none supplied, the clone runs with whatever git auth is already set up on the machine (SSH agent, a configured credential helper, `.netrc`, or userinfo embedded in the URL itself) — this includes every SSH-form clone, even against github.com/gitlab.com, since the provider credential helper is HTTPS-only. Unity-project detection is **always** a post-clone scan of the downloaded tree for the URL form (never a provider API call, regardless of host or token) — only Git LFS credentials differ by tier: an HTTPS URL to github.com/gitlab.com with a token uses the provider-scoped LFS credential helper, everything else uses the machine's own git auth for the LFS pull too. A malformed URL, an unreachable host, a rejected/unknown SSH host key, and an authentication failure are reported as distinct errors (exit 2, 7, 3, and 3 respectively). + +**SSH transport policy.** No SSH URL is ever rewritten to HTTPS, and no key handling happens in the CLI — a key held by a running ssh-agent is used automatically, and a repository-local `core.sshCommand` or an `~/.ssh/config` host alias behaves identically to plain `git`, because the CLI only supplies its own SSH defaults when none of those (nor `GIT_SSH_COMMAND`/`GIT_SSH`) are already set — and it supplies none of them at all on an interactive terminal, deferring entirely to ssh's own prompts (a real fingerprint prompt for an unfamiliar host, a real passphrase prompt for a protected key with no agent), since a person at the terminal can answer them. Only in a non-interactive invocation (no TTY, or `--non-interactive`/`UNITY_NON_INTERACTIVE`), where no prompt could ever be answered, does the CLI supply its own defaults: an unknown host is trusted on first connect and pinned (so a *later* change to that host's key still fails loudly — this is not silenced), and a passphrase-protected key with no agent fails fast with an actionable error instead of hanging. + +#### Working in a Unity Version Control workspace + +Two reads are wrapped, because Unity-aware wrapping adds something, and code reviews get their own +subgroup because `cm` cannot reach them at all. Everything else forwards to `cm` unchanged, because +wrapping it would add nothing. + +```bash +# Wrapped: the lock listing joined to YOUR pending changes. This is the one thing cm cannot +# do for you, since it has no single command that knows both. +unity vcs uvcs locks # who holds what, and what collides with your work +unity vcs uvcs locks --format json # stable envelope: asOf, inLocalChanges, unlockCommand + +# Wrapped: recent history in a shape a script or an agent can rely on. +unity vcs uvcs changesets --limit 50 +unity vcs uvcs changesets --format json + +# Straight through to cm: partial checkout, shelves, and lock mutations. cm's own flags, +# cm's own output, cm's own --help. This is the supported route, not a workaround. +unity uvcs partial configure +unity uvcs partial update /Assets/Levels +unity uvcs shelve -c "wip: lighting pass" +unity uvcs shelve --apply sh:12 +unity uvcs lock list +unity uvcs lock unlock itemid:42@my-game +unity uvcs --help # cm's own help, forwarded verbatim +``` + +`unity vcs uvcs --help` says the same thing at the point of confusion: anything it does not list +forwards straight to `cm`. `unity cm ` is the identical passthrough under cm's own name. + +**Lock state is shared and racy.** `unity vcs uvcs locks` reports the state as of the moment it +read it, and says so in both the human output and the `asOf` field. Do not cache a reading or treat +one as authoritative; read again before you act. The wrapper never mutates a lock for the same +reason it never paraphrases cm's flags: releasing someone else's lock is a decision about shared +team state, so it stays an explicit `unity uvcs lock unlock` the user types. + +#### Code review comments + +`unity vcs uvcs review` reads the review comments a person left in the Unity Version Control GUI, +and answers them. This is the one UVCS surface with no `cm` route at all — `cm codereview` manages +review objects but has no comment verbs — so without these commands a reviewer's feedback is +invisible to anything outside the GUI, and a human has to restate every comment in the prompt. + +```bash +# Find a review. Any of --changeset, --branch or --status narrows it server-side. +unity vcs uvcs review list --branch /main --format json + +# Read its comments, with the file and line each one is anchored to. +unity vcs uvcs review comments --review 42 --format json +unity vcs uvcs review comments --changeset 118 # the review attached to a changeset +unity vcs uvcs review comments --review 42 --all-activity + +# Answer one, then record which change addressed it. +unity vcs uvcs review reply --review 42 --comment 7 --body "Fixed in the next changeset." +unity vcs uvcs review resolve --review 42 --comment 7 --changeset 118 +``` + +All four take the usual optional `[path]` operand; everything else is an option. `--limit` defaults +to 50 and caps at 500, and the envelope's `truncated` tells you when there was more. + +**Unity Cloud only.** The reviews service resolves a per-organization cloud region, so a +self-hosted workspace has no reviews API — those commands refuse with +`VCS_UVCS_REVIEW_SELF_HOSTED` and point at the GUI rather than failing obscurely. + +**`line` is one-based, and may be absent.** The service anchors a comment with a zero-based line in +a string field whose `-1` means "not anchored to a line". The CLI does that arithmetic once: `line` +in the envelope matches what the dashboard shows, and is `null` — never `0` — for a comment that +is not tied to a line. Do not add one yourself. + +**The default is file comments, not the whole feed.** The service's comments route is an activity +feed of fourteen types, only four of which (`Comment`, `Change`, `Question`, `Conversation`) are +things a person wrote against a file. `comments` returns those four; `--all-activity` adds the +status changes, reviewer requests and timeline entries. Read `activityRead` alongside `returned` to +tell "no comments" from "activity, but nothing to act on". + +**Resolving means naming a changeset, and is not idempotent.** There is no resolved flag on the +wire — the state IS the changeset the comment was applied in — so `--changeset` is required and is +never inferred from whatever the workspace happens to be sitting on. A comment that is already +resolved is refused (`VCS_UVCS_REVIEW_ALREADY_RESOLVED`) rather than silently re-pointed, because +overwriting it would lose which change actually addressed the comment. The service performs no +already-resolved check of its own, so that guard is entirely client-side: on a review with more +activity than one page, where the comment falls outside the window, `resolve` **refuses** +(`VCS_UVCS_REVIEW_STATE_UNKNOWN`) rather than writing blind — resolve those from the GUI. The guard +is not atomic either: another writer can resolve between the read and the write, and the service +offers no conditional write to close that window. + +**Writes name both ids explicitly.** `reply` and `resolve` take `--review` and `--comment` and +accept no `--changeset`/`--branch` selector, unlike the read leaves: they write a durable record +other people read, and an indirect selector resolving to the wrong review would post into a +conversation nobody looked at. Both ids come straight out of `review comments`. A reply body +carrying control characters is refused rather than stripped, so what lands in the review is exactly +what was written. + +The `cm` client is needed for all of this. `unity plugin install plastic` installs it; a command +that needs it and cannot find it says so and names that command. + +#### Connecting to a self-hosted or enterprise host + +GitHub Enterprise Server, self-managed GitLab, and self-hosted Gitea/Forgejo all work with the URL form of `projects clone` and `projects link vcs` (not `projects create --vcs`, which accepts only `github`, `gitlab`, and `uvcs`), but **each host signs in separately**: being signed in to github.com grants nothing on `ghe.example.com`. The first attempt against a new host fails to authenticate (exit 3) until you sign in to that host specifically; the CLI then prints the exact command for whichever mechanism your machine has. + +Three ways in, any one is enough: + +```bash +# 1. The provider's own CLI, scoped to the host (enables repo browsing/creation) +gh auth login --hostname ghe.example.com +glab auth login --hostname gitlab.example.com +tea login add --name work --url https://gitea.example.com + +# 2. Git Credential Manager: one credential per host, no per-host setup, +# picked up once `git credential approve` has stored one for that host + +# 3. SSH: needs neither of the above; uses your SSH agent +unity projects clone +``` + +`gh` and `glab` hold one session per host and can be signed in to several simultaneously, which is why the sign-in commands are host-scoped rather than bare. `tea` has no default host at all: every login is a named entry for one instance URL. + +The CLI reads and stores no token on any of these paths. It asks each installed provider CLI whether it holds a session for that specific host, with credential-bearing environment variables stripped from the child process: `gh` otherwise applies `GH_ENTERPRISE_TOKEN` to any non-cloud host and dials it to validate, which would leak the token to a host that merely appeared in a failing URL. Stripped, `gh` answers from its own config: an unconfigured host is never contacted. Side effect: authenticating purely via `GH_ENTERPRISE_TOKEN` (no `gh auth login` entry) reads as signed out, so you may be offered a sign-in you do not need. A host with no provider CLI available is not a dead end: plain git still clones and pushes, with credentials from the credential manager or the SSH agent. Note `--git-token`/`--git-token-stdin` still only apply to github.com and gitlab.com over HTTPS; for any other host, rely on one of the three paths above. + #### projects pin / unpin ```bash @@ -151,6 +309,81 @@ unity projects size --all --json Human output uses readable units; `--json` (and `--format ndjson`) emit raw byte counts. +#### projects clean + +The counterpart to `projects size`: deletes the **regenerable** folders (`Library`, `Temp`, `Logs`, …) to reclaim disk space. Unity rebuilds them on the next open — at the cost of a slow first import. + +```bash +# Preview: what would be deleted, with sizes — deletes nothing +unity projects clean --dry-run + +# Clean the current project (prompts to confirm) +unity projects clean + +# Clean a project by path or registered name +unity projects clean ./MyGame + +# Non-interactive: --yes is REQUIRED in a script or CI +unity projects clean MyGame --yes +``` + +The project argument defaults to the current directory and accepts a path or a registered project name. Guardrails worth relying on: + +- **It refuses while the project is open in a running editor**, naming the PID — cleaning `Library` under a live editor corrupts the session. If the CLI cannot determine whether an editor has it open, it warns and proceeds, so close editors first in automation. +- **It refuses to delete unprompted.** In a non-interactive shell without `-y, --yes` it stops rather than deleting. +- A path that isn't a Unity project (no `ProjectVersion.txt`) is rejected outright, so a mistyped path can't delete anything. + +`--dry-run` is the safe way to size the win first; it reports what it *would* reclaim and exits without touching the filesystem. + +#### projects verify + +An Editor-free integrity check on the **project**, meant as the first step of a CI job. `unity doctor` answers "can this machine build?"; this answers "is this project sound?" — the version-control damage that otherwise surfaces after the expensive build step, as a confusing import error or an artifact that is wrong rather than missing: + +```bash +# Verify the current project +unity projects verify + +# Verify a project by path or registered name +unity projects verify ./MyGame + +# CI gate: warnings fail the job too +unity projects verify --strict + +# Only the checks you care about (either spelling works) +unity projects verify --check meta-missing,guid-duplicate + +# Confirm the project targets the version the pipeline pins +unity projects verify --expect-editor 6000.0.30f1 + +# Inline annotations on the job, anchored to the offending file and line +unity projects verify --format github +``` + +Exits `0` when nothing error-severity is found, `6` otherwise. Warnings alone still exit `0` — `--strict` promotes them. Every finding carries a stable code, a severity, a project-relative path, and a remediation hint: + +| Code | Severity | Detects | +|---|---|---| +| `META_MISSING` | error | An asset under `Assets/` with no sibling `.meta`. Unity assigns a fresh guid, silently breaking every reference to it. | +| `META_ORPHAN` | warning | A `.meta` whose asset no longer exists. | +| `GUID_DUPLICATE` | error | Two `.meta` files claiming the same `guid` — typically two branches that each added one. | +| `CONFLICT_MARKERS` | error | Unresolved merge markers in a `.meta`, `ProjectSettings/*.asset`, `Packages/manifest.json`, or `packages-lock.json`. | +| `MANIFEST_INVALID` | error | `Packages/manifest.json` does not parse, or a dependency version is not a string. | +| `EDITOR_VERSION_DRIFT` | warning | `ProjectVersion.txt` disagrees with `--expect-editor`. | +| `PATH_UNVERIFIABLE` | warning | A path the scan did not inspect, named so you know which subtree went unchecked. Always on — not selectable via `--check`. | + +Worth knowing: + +- **Editor-version drift is opt-in.** Nothing in the CLI stores a pinned version, so the check only runs when you pass `--expect-editor ` — the version your pipeline pins. +- **`--format json` returns the full report** (findings plus an errors/warnings/filesScanned summary); **`--format ndjson` emits one `type: "finding"` record per finding** as it is found, then a terminal result frame. Piped stdout defaults to `tsv`, like the rest of the CLI. +- **Safe to paste into a public log.** Finding paths are project-relative, terminal-escape-stripped, and the project root has its home directory masked. +- **It is built to scan an untrusted repository** — a fork, an unreviewed pull-request branch, a third-party template. So it refuses to follow a symlinked `Assets/`, `ProjectSettings/`, or `Packages/` (exit 6, `PROJECTS_VERIFY_UNSCANNABLE_DIR`), which would otherwise make it enumerate or read a tree outside the project into your CI log, and it bounds every file read at 16 MiB. A symlink **deeper** in the tree is not refused — it is skipped and reported (next bullet), since one link inside `Assets/` should not fail the whole scan. +- **`summary.unverifiable` tells you whether the pass is complete, and each skipped path is named.** Anything the scan could not inspect — a symlinked directory or file at any depth, an unreadable subtree, a walk past the depth cap, an over-budget file, a missing `Assets/` — is counted there and reported as a `PATH_UNVERIFIABLE` warning carrying the path, so you can see which subtree went unchecked instead of only how many did. Under `--strict` those warnings promote to errors like any other, so a project cannot satisfy the gate by making verification impossible rather than by being sound. +- **`--expect-editor` must be a real Unity version.** A typo like `6000.x` exits 2 rather than becoming a drift warning that passes — otherwise a misconfigured pipeline would silently satisfy its own version gate. +- **`data.checks` lists what actually ran.** `EDITOR_VERSION_DRIFT` is absent unless you passed `--expect-editor`, since without a version to compare against there is nothing to check. +- **No Editor, no license, no network, no installed editor** — and it does not read asset bodies, so it stays fast on a large project. +- **Detection only.** It does not repair anything; fixing meta/guid divergence needs the Editor's own asset database. +- Names Unity's importer ignores (dot-prefixed, `~`-suffixed, `.tmp`, `cvs`) are skipped, so a `.gitignore` or a `Documentation~` folder never reports a missing `.meta`. + #### projects require Ensure the editor version required by a project is installed, installing it if needed: @@ -239,6 +472,8 @@ unity projects unlink cloud /path/to/MyProject # Publish a local project to a NEW GitHub / GitLab / Unity Version Control repository unity projects link vcs /path/to/MyProject \ --vcs github --git-namespace my-org --git-repo my-game --git-token-stdin +# Attach to an ALREADY-EXISTING remote instead of creating one — pass its URL +unity projects link vcs /path/to/MyProject https://github.com/my-org/my-game.git # Remove a project's git remotes (the remote repositories are NOT deleted) unity projects unlink vcs /path/to/MyProject # Also detach the Unity Version Control workspace @@ -247,6 +482,8 @@ unity projects unlink vcs /path/to/MyProject --unlink-workspace `link vcs` shares the source-control flag set documented under `projects create`. `link cloud` / `link vcs` accept `--cloud-org ` (env `UNITY_CLOUD_ORG`). +The `[url]` second operand attaches to a remote that already exists, instead of creating one — the one thing the flag form of `link vcs` cannot do. It is mutually exclusive with `--vcs`, `--git-namespace`, `--git-repo`, `--git-visibility`, `--git-default-branch`, `--git-remote-protocol`, `--git-description`, `--cloud-org`, and `--cloud-project` (all meaningless without a repository to create — the URL's own scheme already says which transport to use). `--git-token[-stdin]`, `--no-initial-commit`, and `--git-lfs` still apply, and the same ambient-auth / Tier A rules as `projects clone [url]` govern whether the push uses a supplied token or the machine's own git auth. + --- ### Releases — browse Unity versions @@ -335,6 +572,31 @@ unity templates create /path/to/MyProject \ - On success, prints the path to the created `.tgz` archive - Created templates appear in `unity templates list --editor --custom` +**`templates pack` — portable archive, not a registered template.** `create` installs into the Hub-configured user templates directory so the template shows up in `templates list --custom`; `pack` writes a standalone `.tgz` to a file path you choose and registers nothing. Reach for `pack` when the archive is an artifact to check in, attach to a release, or hand to someone else. + +```bash +# Pack a project into a portable template archive (--output is REQUIRED) +unity templates pack ./MyProject \ + --output ./my-template.tgz \ + --name com.myorg.template.mytemplate \ + --display-name "My Template" + +# Minimal form — prompts for name and display name on a TTY +unity templates pack ./MyProject --output ./my-template.tgz + +# Replace an existing archive, with machine output +unity templates pack ./MyProject --output ./my-template.tgz --overwrite --json +``` + +**`templates pack` key notes:** +- `--output ` is a **file path**, not a directory, and is required +- `--name` and `--display-name` are required; on a TTY they're prompted for when omitted, so pass both in CI +- Use `--template-version`, **not** `--version` — the latter collides with the global `-V, --version` flag +- The output path may not be **inside** the project being packed; that's rejected, so the archive can't include itself +- An existing output file is an error unless `--overwrite` is passed +- `--keep-embedded-packages` and `--keep-project-settings` retain content that is otherwise stripped +- Consumable directly by project creation: `unity projects create MyGame --template ./my-template.tgz` + ```bash # Delete a user-generated custom template (prompts for confirmation) unity templates delete com.myorg.template.mytemplate --editor 6000.0.47f1