Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- Long-running commands now finish faster. Test runs, package operations, profiler sampling, recording and `qa run-sequence` all advance one step per Editor update tick, and an idle Editor runs that loop slowly — measured at roughly six ticks per second, whether or not its window is in front. While one of those commands is in flight the bridge now keeps the Editor ticking at full rate and lets it settle back the moment the work finishes, which measured about 1.5× faster end to end (60 ticks: 9.4–10.4 s → 6.0–6.1 s on Unity 6000.3 / macOS). Nothing to turn on, and on an Editor version that does not expose the internal API this relies on, commands simply run at the old speed.
- Component values may now be written as JSON arrays: `"m_Center": [1, 2, 3]` alongside the existing `{"x": 1, "y": 2, "z": 3}`. Vector2/3/4, Vector2Int/Vector3Int, Quaternion, Rect, RectInt and Color all accept the short form, and a Color array may leave off alpha.
- Structured values that arrive quoted — `"[1,2,3]"` or `"{\"x\":1,\"y\":2,\"z\":3}"` instead of the JSON value itself — are now parsed instead of rejected. AI agents produce this shape often, and the previous error read as though the value were wrong rather than the quoting. Only strings that open a JSON object or array are re-read, so asset paths, object-reference handles, enum names and plain text are untouched, and a string that fails to parse still produces the original validation error.
- The bridge can now be kept from starting, for build machines that have no use for it. Set `UNITY_CLI_BRIDGE_DISABLE=1` or pass `-noUnityCliBridge` on the Editor command line and the bridge builds nothing at all — no socket, no instance registration, no token file, no console warnings if any of that would have failed. A CI job that only opens the Editor to produce a player build gets a quiet log and no registry lock to contend over with parallel builds.

### Changed
- Unity Recorder is no longer installed alongside the package. Only the `record` commands and `qa run-sequence --record` ever used it, so projects that do not record no longer carry the dependency. If you do record, add `com.unity.recorder` to your project — `record start` now fails with that exact instruction when it is missing, and `record stop` / `record status` keep working on recordings you already have. `com.unity.test-framework`, which the test commands genuinely require, is now declared outright instead of arriving as a side effect of the Recorder dependency.

### Fixed
- The package no longer compiles its editor-side plumbing into your game. Everything the bridge uses to talk to the CLI — the instance registry, file-backup transactions, the command catalog and its help text — was being built into player builds as unreachable code, roughly 150 KB of it, along with the strings it carries. Player builds now receive only the QA marker types (`[QaTarget]`, `IQaTappable`, `IQaQueryable`, `QaTappable`) that a project references on purpose, about 7 KB. Nothing changes for editor use, and no code needs updating.
- An Editor whose IPC listener died is no longer advertised as reachable. The listener could stop accepting connections for good — an unexpected socket failure, or a first bind that never succeeded — while the instance kept publishing itself, so the CLI would route to it and fail to connect over and over. The bridge now watches its own listener and re-binds it in place within a few seconds; if it cannot be revived, the instance removes itself from the registry with an Editor console message telling you to restart, instead of staying on the list as a target that never answers.
- The IPC auth token is now compared in fixed time, so the check cannot leak the expected token through timing to other processes on the same machine. Authentication behavior is otherwise unchanged.

Expand Down
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The repo is a single solution (`UnityCliBridge.sln`) split across four projects:

**CLI (`cli/UnityCli.Cli/`)** — `.NET 9`, published self-contained for `osx-arm64` and `win-x64`. `CliApp.RunAsync` is the dispatcher: local-only flows (`status`, `instances`, `doctor`) are answered without IPC; everything else goes through `Services/CliArgumentParser` → `Models/ParsedCommand` → `Services/LocalIpcClient` to the running Editor. `Services/InstanceRegistryStore` reads `InstanceRegistryFile` (see Protocol below) to find the right Editor for a given project root. `Services/EditorLauncher` implements the local `editor launch` flow: pre-flight (live-instance reuse, stray-process detection), editor-binary resolution, spawn with detached stdio, and registry-readiness polling.

**Shared protocol (`cli/UnityCli.Protocol/` ↔ `unity-package/.../Runtime/Protocol/`)** — The `.csproj` uses `<Compile Include>` links to compile the same `.cs` files from the Unity package. **A change to any protocol file is a change to both sides; keep them buildable for both `.NET 9` and Unity's runtime.** Shared protocol source files must live in `unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/`; the CLI project enforces this via a build-time guard. Hot spots:
**Shared protocol (`cli/UnityCli.Protocol/` ↔ `unity-package/.../Runtime/Protocol/`)** — The `.csproj` uses `<Compile Include>` links to compile the same `.cs` files from the Unity package. **A change to any protocol file is a change to both sides; keep them buildable for both `.NET 9` and Unity's runtime.** Shared protocol source files must live in `unity-package/com.yhc509.unity-cli-bridge/Runtime/Protocol/`; the CLI project enforces this via a build-time guard. That folder sits under `Runtime/` only so the CLI can link it — its own `UnityCliBridge.Bridge.Protocol.asmdef` is `includePlatforms: ["Editor"]`, so none of it reaches a player build (see the release-build convention below). Hot spots:
- `CliCommandCatalog.cs` is the single source of truth for command metadata, including `ForceRule` (None / OnOverwrite / OnDestructiveOp / Always) — every force-gating decision must trace back here.
- `FileBackupTransaction.cs` is `.NET`-testable and powers all backup/restore flows.
- `InstanceRegistryFile.cs` owns the atomic registry-lock protocol (atomic `FileMode.CreateNew`, PID + UTC timestamp content, stale-reclaim via open-then-rename-then-delete) used by both `BridgeHost` and the CLI's `InstanceRegistryStore`. It also owns per-instance 0600 auth-token sidecars (`<registryDir>/tokens/<hash>.token`) so mixed-version registry rewrites cannot strip live tokens.
Expand All @@ -50,7 +50,7 @@ The repo is a single solution (`UnityCliBridge.sln`) split across four projects:
- `ExecuteValueSerializer.cs` serializes values returned from `execute` and custom commands with a safe whitelist and round-trip float/double formatting.
- `AssetBackupTransaction.cs` wraps `FileBackupTransaction` with an `AssetDatabase.Refresh` discipline (always in `finally`) and is the rollback core for scene patch / prefab patch / asset overwrite. Backups land in `Library/com.yhc509.unity-cli-bridge/backups/` to stay outside `Assets/` and `AssetDatabase` scanning.
- `PackageCommandHandler.cs` polls Unity Package Manager from `EditorApplication.update` (deferred dispatch) with a single-flight guard and a 300 s `PACKAGE_TIMEOUT` — never re-introduce blocking polls here.
- `RecordCommandHandler.cs` starts/stops Unity Recorder mp4 capture for Play Mode and writes finalized result sidecars under `Library/com.yhc509.unity-cli-bridge/recordings/`.
- `RecordCommandHandler.cs` starts/stops Unity Recorder mp4 capture for Play Mode and writes finalized result sidecars under `Library/com.yhc509.unity-cli-bridge/recordings/`. Every Recorder API touch is behind `#if UNITY_CLI_BRIDGE_RECORDER` — see the optional-dependency convention below.
- `ProfileCommandHandler.cs` + `.Capture.cs` (partial) sample `ProfilerRecorder` counters for `profile stats` and drive `profile capture`/`status` via `ProfilerDriver`/`HierarchyFrameDataView`, writing capture summaries to `Library/com.yhc509.unity-cli-bridge/profiles/<captureId>.json` sidecars that `profile analyze` reads locally without an Editor round-trip.
- `TestCommandHandler.cs` + `.EditMode.cs` + `.PlayMode.cs` (partial)와 `TestRunnerCallbacks.cs` (ScriptableObject)는 `TestRunnerApi`를 래핑한다. EditMode는 동기 응답, PlayMode는 비동기(즉시 `STARTED+runId`, 결과는 `Library/com.yhc509.unity-cli-bridge/test-runs/<runId>.json` atomic write). `DomainReloadDisableScope`는 `--no-domain-reload` 옵션을 구현하지만 디폴트는 Unity 정상 동작.
- `CliInstallerWindow.cs` / `CliInstallerState.cs` / `CliDownloader.cs` / `SkillInstaller.cs` form the `Window > Unity CLI Manager` flow that fetches the matching CLI binary from GitHub Releases and installs the AI-agent skill.
Expand All @@ -77,6 +77,9 @@ Tests live in `tests/UnityCli.Cli.Tests/` (xUnit, `.NET`-testable surface only).
- **Auth token comparison:** `AuthTokenComparison.FixedTimeEquals` (no early exit, length folded into the same accumulator) is the only comparison allowed on the token path. Hand-rolled rather than `CryptographicOperations.FixedTimeEquals` so the shared source compiles on Unity's runtime profile and .NET 9.
- **Auth token storage:** Live IPC auth tokens are stored in per-instance 0600 sidecars (`<registryDir>/tokens/<hash>.token`), not in the shared registry. The CLI reads the sidecar during resolve/load, and `InstanceRecord.token` is a non-serialized in-memory field. The token sidecar is written as soon as the listener key is acquired, before a client can connect; the registry entry is only written on the first editor tick after the listener is ready. A live socket with no registry entry is therefore a real (if brief) state, and the CLI must treat it as a retryable `LIVE_UNAVAILABLE` rather than connecting with an empty token — do not remove that guard. Both the entry and the sidecar survive domain reloads and are removed only when the Editor exits; during a reload only the socket/listener is closed.
- **Token sidecar is overwrite-if-different:** `EnsureTokenSidecar` replaces a stale sidecar left by a killed/crashed session; never restore the old "skip when file exists" behavior — it makes every CLI call fail `UNAUTHORIZED` for the whole next session.
- **Player-build surface is three files:** the package ships two assemblies to a consuming project. `UnityCliBridge.Bridge.Runtime` (`includePlatforms: []`) is the only one that reaches a player, and it must contain nothing but the four QA marker sources a game references on purpose (`IQaTappable`, `IQaQueryable`, `QaTappable`, `QaTargetAttribute` — ~7 KB compiled). Everything else — the whole `Runtime/Protocol/` tree plus `Editor/` — is Editor-only. `ReleaseBuildSurfaceTests` pins this: the asmdefs and `package.json` are plain JSON that no compiler validates, and a one-character edit there silently pushes registry file I/O, `Process.Start`, and the full command catalog into a shipped game. Verify a change with `CompilationPipeline.GetAssemblies(AssembliesType.Player)`, not by reading the asmdef.
- **Bridge disable switch:** `UNITY_CLI_BRIDGE_DISABLE` (any value but `0`/`false`/empty) or the `-noUnityCliBridge` editor flag keeps `BridgeBootstrap` from constructing the host at all — no session-lock restore, no registry entry, no token sidecar, no update hook. It exists for CI/release build jobs, where a bridge is pointless and its watchdog errors are log noise. The check must stay *before* `new BridgeHost()`; `BridgeDisableSwitch` (`Runtime/Protocol/`) holds the parsing so it is unit-tested, and a source-ordering test guards the call site.
- **Optional Unity packages are versionDefine-gated, never dependencies:** a missing asmdef reference is silently dropped by Unity (it is not a compile error), but any code using its types then fails `CS0246`. So an optional package needs both halves — keep the asmdef `references` entry *and* wrap every API touch in the matching `versionDefines` symbol. `com.unity.inputsystem` → `ENABLE_INPUT_SYSTEM` and `com.unity.recorder` → `UNITY_CLI_BRIDGE_RECORDER` follow this; `record start` degrades to a `RECORD_FAILED` install hint (same shape as the memory-profiler gate) while `record stop`/`status` keep serving existing sidecars. `com.unity.test-framework` is a real dependency by contrast — the test handlers use `TestRunnerApi` types unguarded — and must stay declared in `package.json`; it used to arrive transitively through Recorder, which is exactly the trap this convention exists to avoid.
- **Headless editors are first-class:** the bridge starts in any main editor process (GUI or `-batchmode`); only secondary Unity processes (MPE / `-adb2` AssetImportWorker) are excluded. Recommended headless launch is `-batchmode` *without* `-nographics` so the GPU stays available. Commands whose catalog entry sets `requiresGraphics` fail with `HEADLESS_NO_GRAPHICS` when `SystemInfo.graphicsDeviceType == Null`.
- **`editor launch` / `editor stop`:** `editor launch` is a local command — pre-flight (live registry match → idempotent reuse; stray editor process → `EDITOR_ALREADY_RUNNING_CONFLICT`) then spawn + registry-readiness polling (default 300 s). The spawned editor's stdio is detached (Unix: `sh -c 'exec …'` wrapper redirecting to the null device, so `Process.Id` stays the editor PID) — never let it inherit the CLI's streams, or `editor launch | grep …` pipelines hang forever after the CLI exits. `editor stop` is the `editor-quit` wire command (ForceRule OnDestructiveOp): the bridge replies first and schedules `EditorApplication.Exit` via an `EditorApplication.update` one-shot callback (+0.5 s grace), then the CLI waits for PID exit (default 30 s). The deferred quit must stay `update`-based, not `delayCall` — `delayCall` rides the inspector-update cycle and starves in an unfocused GUI editor, timing out every stop. A graceful quit removes the registry entry and token sidecar. The CLI is installed per version under `~/.unity-cli-bridge/versions/<version>/` (binary + `meta.json` = `{"cliVersion","protocolVersion"}`), and `~/.unity-cli-bridge/unity-cli/` stays the PATH target — a symlink to the newest version on macOS/Linux, a copy on Windows, plus a `meta.json` marker naming the version it resolves to. `CliInstallLayout` (`Runtime/Protocol/`) owns that layout for both sides. On a `PROTOCOL_MISMATCH` the CLI reads the bridge's protocol off `response.protocolVersion`, finds the newest installed version speaking it, and re-execs with the original argv (`execve` on macOS/Linux, child process on Windows) with `UNITY_CLI_DISPATCHED=1` set; if that guard is already set it reports the mismatch instead of dispatching again. This works because the bridge checks the protocol before auth and before dispatch and `return`s, so nothing ran and re-sending cannot double-execute — do not move that check. `LocalIpcClient.EnsureCompatibleResponse` must keep the *peer's* `protocolVersion` on the envelope it synthesizes; that field is what routing depends on. Local-only commands (`status`, `instances`, `doctor`) never dispatch. Happy path pays nothing: the decision short-circuits before touching disk.
- **CLI install target = package version:** The Manager downloads the CLI release matching *its own package version*, never the newest release. Only the same-version CLI is guaranteed to speak the package's protocol, and the Manager writes that protocol into `meta.json` from `ProtocolConstants.ProtocolVersion`. Old versions are never garbage-collected; removal is a per-version button in the Manager.
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ Add the following to your `Packages/manifest.json`:

The bridge starts automatically when the Editor opens. No configuration needed.

`#main` always tracks the latest release. For a build you need to reproduce later, pin a release tag instead — `...#v0.5.2` — and commit `Packages/packages-lock.json`.

Unity Recorder is optional and is not installed for you. Add `com.unity.recorder` if you want the `record` commands or `qa run-sequence --record`; without it those commands fail with an install hint and everything else works normally.

> **Upgrading from 0.3.x?** `0.4.0` bumps the wire protocol to `5`, so a `0.3.x` CLI cannot talk to a `0.4.0` package. From `0.4.1` on you no longer have to choose: install the CLI from each project's **Window > Unity CLI Manager**, and the `unity-cli` on your PATH hands off to the version that matches whichever project you are pointing at.

### 2. Install the CLI
Expand Down Expand Up @@ -409,6 +413,22 @@ docs/ Generated CLI reference, specs
- **Component value shapes:** Vector2/3/4, Vector2Int/Vector3Int, Quaternion, Rect, RectInt, and Color accept both the member object (`{"x":1,"y":2,"z":3}`) and the array shorthand (`[1,2,3]`); a Color array may omit alpha. A structured value that arrives quoted (`"[1,2,3]"`) is re-parsed, but send real JSON values — the fallback never applies to genuine string fields such as asset paths or enum names.
- **set-node warnings:** Unrecognized keys now return warnings instead of silent success.

## Release Builds & CI

The bridge is editor-only. The single assembly that reaches a shipped player is `UnityCliBridge.Bridge.Runtime`, which holds nothing but the QA marker types you reference on purpose (`[QaTarget]`, `IQaTappable`, `IQaQueryable`, `QaTappable`). No listener is bound, no registry entry is written, and no bridge code runs in a build.

The bridge *does* start in any main editor process, including the headless editor a build job launches. That is usually harmless but pointless on a build machine — it binds a socket, publishes a registry entry, and contends for the registry lock when several builds share a host. Turn it off for those jobs:

```bash
# Environment variable — anything but 0/false/empty counts as "disable"
UNITY_CLI_BRIDGE_DISABLE=1 Unity -batchmode -quit -projectPath . -executeMethod Build.Run

# Or the editor command-line flag, for jobs that cannot set an environment variable
Unity -batchmode -quit -projectPath . -noUnityCliBridge -executeMethod Build.Run
```

A disabled bridge constructs nothing and logs one line saying so.

## Documentation

- [CLI Reference (generated)](docs/cli-reference.md)
Expand Down
Loading