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
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ Select Window > Unity CLI Loop > Settings. A dedicated window will open. If the

The installer places the global `uloop` dispatcher on PATH. Project-specific `uloop-project-runner` binaries are downloaded into the user cache automatically from each project's `.uloop/project-runner-pin.json`.

To return to the v2 line, press **Uninstall CLI** in Settings, downgrade the U-LOOP package to a v2 version such as `2.1.1`, then press **Install CLI** again from Settings.
Keep the v3 dispatcher installed when working with both v2 and v3 projects. If Unity resolves a project to a v2 `io.github.hatayama.uloopmcp` package, the dispatcher automatically installs the matching v2 `uloop-cli` release into its versioned user cache and delegates the command to it. The resolved package version takes precedence over a stale v3 project-runner pin left after a downgrade. The initial npm installation and the v2-mode notice are written to stderr so stdout remains the delegated command's output. V3 projects continue to use the project runner selected by their pin.

The global `install`, `update`, `uninstall`, `completion`, and `launch` commands remain owned by the v3 dispatcher in every project. Other project commands, help, and the project-scoped version request are delegated for detected v2 projects.

V2 delegation requires Node.js 22 or later, including npm for the first command that populates the cache. Do not press **Update CLI** or **Downgrade CLI** in a v2 project's Settings window. These buttons are normally hidden because the delegated CLI reports the matching v2 version, but using one can restore a global npm CLI that hides the v3 dispatcher depending on PATH order.

<details>
<summary>CLI-only terminal install</summary>
Expand Down Expand Up @@ -128,20 +132,20 @@ If npm is unavailable or the old command belongs to a different Node prefix, the
npm uninstall -g uloop-cli
```

If the Unity UI path is not available or your terminal still resolves `uloop` to the v3 CLI, remove that command first, then install the v2 version you want:
Do not install a v2 CLI globally to switch projects. If your terminal resolves `uloop` to an old npm installation instead of the native dispatcher, remove the npm installation and reinstall the native dispatcher:

```bash
rm -f "$HOME/.local/bin/uloop"
npm install -g uloop-cli@2.1.1
npm uninstall -g uloop-cli
# Run the verified native installer above again.
which uloop
uloop --version
```

On Windows PowerShell:

```powershell
Remove-Item "$env:LOCALAPPDATA\Programs\uloop\bin\uloop.exe" -Force -ErrorAction SilentlyContinue
npm install -g uloop-cli@2.1.1
npm uninstall -g uloop-cli
# Run the verified native installer above again.
Get-Command uloop
uloop --version
```
Expand Down
18 changes: 12 additions & 6 deletions README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,13 @@ Scope(s): io.github.hatayama.uloopmcp

Window > Unity CLI Loop > Settingsを選択します。専用ウィンドウが開くので、**CLI** ボタンが青くなっていなければ **Install CLI** を押してください。

v2系に戻したい場合は、Settings で **Uninstall CLI** を押し、Unity Package Manager か `manifest.json` で U-LOOP package を `2.1.1` などのv2系のバージョンへ下げてから、もう一度 Settings で **Install CLI** を押してください。
installerはグローバルな`uloop` dispatcherをPATH上に配置します。プロジェクト固有の`uloop-project-runner` binaryは、各プロジェクトの`.uloop/project-runner-pin.json`に従ってuser cacheへ自動的にdownloadされます。

v2とv3のプロジェクトを併用するときも、v3 dispatcherをインストールしたままにしてください。Unityがプロジェクトをv2系の`io.github.hatayama.uloopmcp` packageへ解決している場合、dispatcherは同じバージョンのv2 `uloop-cli` releaseをバージョン別user cacheへ自動的にインストールし、コマンドを委譲します。解決済みpackageのバージョンは、downgrade後に残った古いv3 project-runner pinより優先されます。初回のnpmインストールとv2モードの注記はstderrへ出力されるため、stdoutには委譲先コマンドの出力だけが残ります。v3プロジェクトはpinで選ばれたproject runnerを引き続き使用します。

グローバルな`install`、`update`、`uninstall`、`completion`、`launch`コマンドは、どのプロジェクトでもv3 dispatcherが処理します。検出されたv2プロジェクトでは、それ以外のプロジェクトコマンド、help、プロジェクトスコープのversion表示が委譲されます。

v2への委譲には、初回コマンドでcacheを作成するnpmを含むNode.js 22以降が必要です。v2プロジェクトのSettingsウィンドウでは、**Update CLI**または**Downgrade CLI**を押さないでください。委譲先CLIが同じv2バージョンを返すため通常はボタン自体が表示されませんが、使用するとグローバルnpm版CLIが復活し、PATHの順序によってv3 dispatcherが隠れる可能性があります。

<details>
<summary>CLIだけをterminalからinstallする場合はこちら</summary>
Expand Down Expand Up @@ -125,20 +131,20 @@ npm が見つからない場合や、古い command が別の Node prefix に属
npm uninstall -g uloop-cli
```

Unity UIから戻せない場合や、ターミナルの `uloop` がまだv3系のCLIを指している場合は、先にその `uloop` コマンドを削除してから、戻したいv2系のバージョンをインストールしてください
プロジェクトを切り替えるためにv2 CLIをグローバルインストールしないでください。ターミナルの`uloop`がnative dispatcherではなく古いnpm版を指している場合は、npm版を削除してnative dispatcherを再インストールしてください

```bash
rm -f "$HOME/.local/bin/uloop"
npm install -g uloop-cli@2.1.1
npm uninstall -g uloop-cli
# 上記の検証済みnative installerをもう一度実行します。
which uloop
uloop --version
```

Windows PowerShell の場合:

```powershell
Remove-Item "$env:LOCALAPPDATA\Programs\uloop\bin\uloop.exe" -Force -ErrorAction SilentlyContinue
npm install -g uloop-cli@2.1.1
npm uninstall -g uloop-cli
# 上記の検証済みnative installerをもう一度実行します。
Get-Command uloop
uloop --version
```
Expand Down
1 change: 1 addition & 0 deletions cli/common/errors/error_envelope.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
errorCodeUnityRPCError = "UNITY_RPC_ERROR"
errorCodeUnityServerBusy = "UNITY_SERVER_BUSY"
ErrorCodeCLIUpdateRequired = "CLI_UPDATE_REQUIRED"
ErrorCodeV2ProjectDetected = "V2_PROJECT_DETECTED"
ErrorCodeToolDisabled = "TOOL_DISABLED"
ErrorCodeCompileWaitTimeout = "COMPILE_WAIT_TIMEOUT"
ErrorCodeControlPlayModeWaitTimeout = "CONTROL_PLAY_MODE_WAIT_TIMEOUT"
Expand Down
62 changes: 62 additions & 0 deletions cli/dispatcher/internal/dispatcher/dispatcher_v2_delegate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package dispatcher

import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
)

const dispatcherNodeCommandName = "node"

type dispatcherV2CLIPackageJSON struct {
Bin json.RawMessage `json:"bin"`
}

// resolveDispatcherV2CLIEntrypoint resolves the JavaScript file declared by the installed V2 CLI package.
// Why: executing the package entrypoint directly avoids platform-dependent node_modules/.bin shims.
func resolveDispatcherV2CLIEntrypoint(installPath string) (string, error) {
packageDirectory := filepath.Join(installPath, "node_modules", dispatcherV2CLIPackageName)
packagePath := filepath.Join(packageDirectory, dispatcherPackageJSONFileName)
content, err := os.ReadFile(packagePath)
if err != nil {
return "", err
}
packageInfo := dispatcherV2CLIPackageJSON{}
if err := json.Unmarshal(content, &packageInfo); err != nil {
return "", fmt.Errorf("parse %s: %w", packagePath, err)
}
entrypoint, err := dispatcherV2BinEntrypoint(packageInfo.Bin)
if err != nil {
return "", err
}
if filepath.IsAbs(entrypoint) {
return "", fmt.Errorf("%s bin entrypoint must be relative", dispatcherV2CLIPackageName)
}
return filepath.Join(packageDirectory, entrypoint), nil
}

func dispatcherV2BinEntrypoint(bin json.RawMessage) (string, error) {
entrypoint := ""
if err := json.Unmarshal(bin, &entrypoint); err == nil {
return entrypoint, nil
}
entries := map[string]string{}
if err := json.Unmarshal(bin, &entries); err != nil {
return "", fmt.Errorf("%s package bin must be a string or object: %w", dispatcherV2CLIPackageName, err)
}
entrypoint, found := entries["uloop"]
if !found || entrypoint == "" {
return "", fmt.Errorf("%s package bin does not define uloop", dispatcherV2CLIPackageName)
}
return entrypoint, nil
}

func resolveDispatcherV2Node(lookPath func(string) (string, error)) (string, error) {
return lookPath(dispatcherNodeCommandName)
}

func defaultDispatcherV2NodePath() (string, error) {
return resolveDispatcherV2Node(exec.LookPath)
}
59 changes: 59 additions & 0 deletions cli/dispatcher/internal/dispatcher/dispatcher_v2_delegate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package dispatcher

import (
"os"
"path/filepath"
"testing"
)

func TestResolveDispatcherV2CLIEntrypointReadsObjectBin(t *testing.T) {
// Verifies the V2 CLI entrypoint is resolved from the published object-form bin declaration.
installPath := t.TempDir()
writeDispatcherV2PackageBin(t, installPath, `{"uloop":"dist/cli.bundle.cjs"}`)

entrypoint, err := resolveDispatcherV2CLIEntrypoint(installPath)
if err != nil {
t.Fatalf("resolve V2 CLI entrypoint: %v", err)
}
want := filepath.Join(installPath, "node_modules", dispatcherV2CLIPackageName, "dist", "cli.bundle.cjs")
if entrypoint != want {
t.Fatalf("entrypoint = %q, want %q", entrypoint, want)
}
}

func TestResolveDispatcherV2CLIEntrypointReadsStringBin(t *testing.T) {
// Verifies the V2 CLI entrypoint also supports a string-form bin declaration.
installPath := t.TempDir()
writeDispatcherV2PackageBin(t, installPath, `"dist/cli.bundle.cjs"`)

entrypoint, err := resolveDispatcherV2CLIEntrypoint(installPath)
if err != nil {
t.Fatalf("resolve V2 CLI entrypoint: %v", err)
}
want := filepath.Join(installPath, "node_modules", dispatcherV2CLIPackageName, "dist", "cli.bundle.cjs")
if entrypoint != want {
t.Fatalf("entrypoint = %q, want %q", entrypoint, want)
}
}

func TestResolveDispatcherV2NodeReportsMissingNode(t *testing.T) {
// Verifies a missing Node executable is returned to the caller as an error.
_, err := resolveDispatcherV2Node(func(string) (string, error) {
return "", os.ErrNotExist
})
if err == nil {
t.Fatal("expected missing Node error")
}
}

func writeDispatcherV2PackageBin(t *testing.T, installPath string, bin string) {
t.Helper()
packagePath := filepath.Join(installPath, "node_modules", dispatcherV2CLIPackageName, dispatcherPackageJSONFileName)
if err := os.MkdirAll(filepath.Dir(packagePath), 0o755); err != nil {
t.Fatalf("create V2 package directory: %v", err)
}
content := "{\n \"bin\": " + bin + "\n}\n"
if err := os.WriteFile(packagePath, []byte(content), 0o644); err != nil {
t.Fatalf("write V2 package.json: %v", err)
}
}
Loading
Loading