diff --git a/README.md b/README.md
index a812ecb6e7..f0e1da9acd 100644
--- a/README.md
+++ b/README.md
@@ -77,7 +77,7 @@ The installer places the global `uloop` dispatcher on PATH. Project-specific `ul
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.
+The global `install`, `update`, `uninstall`, `completion` (now a no-op stub; shell completion has been removed), 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.
@@ -257,25 +257,6 @@ uloop execute-dynamic-code --code 'using UnityEngine; Debug.Log("Hello from CLI!
-
-Shell Completion (Optional)
-
-You can install Bash/Zsh/PowerShell completion:
-
-```bash
-# Add completion script to shell config (auto-detects shell)
-uloop completion --install
-
-# Explicitly specify shell (when auto-detection fails on Windows)
-uloop completion --shell bash --install # Git Bash / MINGW64
-uloop completion --shell powershell --install # PowerShell
-
-# Check completion script
-uloop completion
-```
-
-
-
## Project Path Specification
If `--project-path` is omitted, the port is automatically selected from the Unity project detected in the current directory.
diff --git a/README_ja.md b/README_ja.md
index 893eccc905..c506323a9a 100644
--- a/README_ja.md
+++ b/README_ja.md
@@ -78,7 +78,7 @@ installerはグローバルな`uloop` dispatcherをPATH上に配置します。
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表示が委譲されます。
+グローバルな`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が隠れる可能性があります。
@@ -255,25 +255,6 @@ uloop execute-dynamic-code --code 'using UnityEngine; Debug.Log("Hello from CLI!
-
-シェル補完(オプション)
-
-Bash/Zsh/PowerShell の補完機能をインストールできます:
-
-```bash
-# 補完スクリプトをシェル設定に追加(シェル自動検出)
-uloop completion --install
-
-# シェルを明示的に指定(Windows環境で自動検出が失敗する場合)
-uloop completion --shell bash --install # Git Bash / MINGW64
-uloop completion --shell powershell --install # PowerShell
-
-# 補完スクリプトを確認
-uloop completion
-```
-
-
-
## プロジェクトパス指定
`--project-path` を省略した場合は、カレントディレクトリの Unity プロジェクトで設定されたポートが自動選択されます。
diff --git a/cli/common/clicore/command_registry.go b/cli/common/clicore/command_registry.go
index a1b531c6e9..094f8832f8 100644
--- a/cli/common/clicore/command_registry.go
+++ b/cli/common/clicore/command_registry.go
@@ -1,23 +1,17 @@
package clicore
const (
- LaunchCommandName = "launch"
- InstallCommandName = "install"
- UpdateCommandName = "update"
- UninstallCommandName = "uninstall"
- SkillsCommandName = "skills"
- CompileCommandName = "compile"
- ExecuteDynamicCodeCommandName = "execute-dynamic-code"
- PausePointAwaitCommandName = "await-pause-point"
- PausePointStatusUserCommandName = "pause-point-status"
- RunTestsCommandName = "run-tests"
- CompletionCommand = "completion"
- ListCommandsFlag = "--list-commands"
- ListOptionsFlag = "--list-options"
- PausePointIDFlagName = "id"
- PausePointTimeoutFlagName = "timeout-seconds"
- PausePointLogsMaxCountFlagName = "matching-logs-max-count"
- PausePointCapturedVariablesFlagName = "captured-variables"
+ LaunchCommandName = "launch"
+ InstallCommandName = "install"
+ UpdateCommandName = "update"
+ UninstallCommandName = "uninstall"
+ SkillsCommandName = "skills"
+ CompileCommandName = "compile"
+ ExecuteDynamicCodeCommandName = "execute-dynamic-code"
+ PausePointAwaitCommandName = "await-pause-point"
+ PausePointStatusUserCommandName = "pause-point-status"
+ RunTestsCommandName = "run-tests"
+ CompletionCommand = "completion"
)
type NativeCommandEntry struct {
@@ -41,7 +35,7 @@ var NativeCommands = []NativeCommandEntry{
{Name: PausePointAwaitCommandName, Description: "Wait until a named UloopPausePoint.Pause marker pauses Unity", Owner: RunnerOwned},
{Name: PausePointStatusUserCommandName, Description: "Show the state of a named UloopPausePoint.Pause marker", Owner: RunnerOwned},
{Name: SkillsCommandName, Description: "List, install, or uninstall agent skills", Owner: DispatcherOwned},
- {Name: CompletionCommand, Description: "Print or install shell completion", Owner: DispatcherOwned},
+ {Name: CompletionCommand, Description: "Deprecated: shell completion has been removed; this command is now a no-op", Owner: DispatcherOwned},
{Name: InstallCommandName, Description: "Configure the global uloop launcher binary", Owner: DispatcherOwned},
{Name: UpdateCommandName, Description: "Update the global uloop launcher binary", Owner: DispatcherOwned},
{Name: UninstallCommandName, Description: "Remove the global uloop launcher binary", Owner: DispatcherOwned},
diff --git a/cli/common/clicore/global_args.go b/cli/common/clicore/global_args.go
index 299da4bf73..8426729a67 100644
--- a/cli/common/clicore/global_args.go
+++ b/cli/common/clicore/global_args.go
@@ -95,19 +95,3 @@ func ContainsHelpRequest(args []string) bool {
}
return false
}
-
-// ShouldHandleCompletionRequest reports whether args target the native completion
-// command or its list-commands/list-options helper flags, so run.go can route them
-// before falling back to the project-connected Unity tool dispatch path.
-func ShouldHandleCompletionRequest(args []string) bool {
- if len(args) == 0 {
- return false
- }
-
- switch args[0] {
- case ListCommandsFlag, ListOptionsFlag, CompletionCommand:
- return true
- default:
- return false
- }
-}
diff --git a/cli/dispatcher/internal/dispatcher/command_help.go b/cli/dispatcher/internal/dispatcher/command_help.go
index 60dad22dc6..8d6367b938 100644
--- a/cli/dispatcher/internal/dispatcher/command_help.go
+++ b/cli/dispatcher/internal/dispatcher/command_help.go
@@ -12,7 +12,11 @@ import (
)
func tryHandleCommandHelp(command string, startPath string, projectPath string, stdout io.Writer, stderr io.Writer) (bool, int) {
- if _, ok := clicore.NativeCommand(command); ok {
+ // Runner-owned commands are intentionally excluded here: their --help must
+ // go through the normal dispatch path to the pinned runner (see
+ // shouldRunInDispatcherProcess), which answers with its own up-to-date
+ // flag list instead of a dispatcher-side table that can drift.
+ if clicore.IsDispatcherOwnedCommandName(command) {
printNativeSingleCommandHelp(command, stdout)
return true, 0
}
@@ -114,7 +118,7 @@ func nativeCommandDescription(command string) (string, bool) {
func nativeCommandUsesProject(command string) bool {
switch command {
- case clicore.LaunchCommandName, "list", "sync", "focus-window", clicore.SkillsCommandName, clicore.PausePointAwaitCommandName, clicore.PausePointStatusUserCommandName:
+ case clicore.LaunchCommandName, clicore.SkillsCommandName:
return true
default:
return false
diff --git a/cli/dispatcher/internal/dispatcher/completion.go b/cli/dispatcher/internal/dispatcher/completion.go
index 88a8228904..3b49bf2697 100644
--- a/cli/dispatcher/internal/dispatcher/completion.go
+++ b/cli/dispatcher/internal/dispatcher/completion.go
@@ -1,492 +1,17 @@
package dispatcher
import (
- "fmt"
- "io"
- "os"
- "os/exec"
- "path/filepath"
- "regexp"
- "runtime"
- "sort"
- "strings"
-
- clierrors "github.com/hatayama/unity-cli-loop/common/errors"
- "github.com/hatayama/unity-cli-loop/common/tooldocs"
-
"github.com/hatayama/unity-cli-loop/common/clicore"
)
-const (
- installCompletionFlag = "--install"
- shellFlag = "--shell"
- completionStartMarker = "# >>> uloop completion >>>"
- completionEndMarker = "# <<< uloop completion <<<"
- powerShellProfileSubpath = "Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1"
- pwshProfileSubpath = "Documents/PowerShell/Microsoft.PowerShell_profile.ps1"
-)
-
-var completionBlockPattern = regexp.MustCompile(`(?s)\n?# >>> uloop completion >>>.*?# <<< uloop completion <<<\n?`)
-
-func tryHandleCompletionRequest(args []string, cache clicore.ToolsCache, stdout io.Writer, stderr io.Writer) (bool, int) {
- if len(args) == 0 {
- return false, 0
- }
-
- if handled, code := tryHandleCompletionListRequest(args, cache, stdout, stderr); handled {
- return true, code
- }
-
- if args[0] != clicore.CompletionCommand {
- return false, 0
- }
- return true, runCompletionCommand(args[1:], cache, stdout, stderr)
-}
-
-func tryHandleCompletionListRequest(args []string, cache clicore.ToolsCache, stdout io.Writer, stderr io.Writer) (bool, int) {
- if len(args) == 0 {
- return false, 0
- }
-
- switch args[0] {
- case clicore.ListCommandsFlag:
- printCommandNames(cache, stdout)
- return true, 0
- case clicore.ListOptionsFlag:
- if len(args) < 2 {
- writeMissingCompletionCommandName(stderr)
- return true, 1
- }
- printOptionsForCommand(args[1], cache, stdout)
- return true, 0
- default:
+// tryHandleCompletionRequest keeps `completion` as a harmless no-op: it used
+// to generate and install shell completion scripts, but that feature has been
+// removed. Shells that ran `uloop completion --install` before this change
+// still run `eval "$(uloop completion --shell zsh)"` on every shell startup,
+// so this must stay silent and exit 0 instead of erroring on unknown args.
+func tryHandleCompletionRequest(args []string) (bool, int) {
+ if len(args) == 0 || args[0] != clicore.CompletionCommand {
return false, 0
}
-}
-
-func runCompletionCommand(args []string, cache clicore.ToolsCache, stdout io.Writer, stderr io.Writer) int {
- if handled, code := tryHandleCompletionListRequest(args, cache, stdout, stderr); handled {
- return code
- }
-
- if clicore.ContainsHelpRequest(args) {
- printCompletionHelp(stdout)
- return 0
- }
-
- request, err := parseCompletionRequest(args)
- if err != nil {
- clierrors.WriteClassifiedError(stderr, err, clierrors.ErrorContext{Command: clicore.CompletionCommand})
- return 1
- }
- return runCompletionRequest(request, stdout, stderr)
-}
-
-func runCompletionRequest(request completionRequest, stdout io.Writer, stderr io.Writer) int {
- shellName := request.shell
- if shellName == "" {
- shellName = detectShell()
- }
- if shellName == "" {
- writeCompletionShellDetectionError(stderr)
- return 1
- }
-
- script := getCompletionScript(shellName)
- if !request.install {
- clicore.WriteLine(stdout, script)
- return 0
- }
-
- configPath, err := getShellConfigPath(shellName)
- if err != nil {
- clierrors.WriteClassifiedError(stderr, err, clierrors.ErrorContext{Command: clicore.CompletionCommand})
- return 1
- }
- if err := installCompletionScript(configPath, shellName, script); err != nil {
- clierrors.WriteClassifiedError(stderr, err, clierrors.ErrorContext{Command: clicore.CompletionCommand})
- return 1
- }
-
- writeCompletionInstallResult(stdout, shellName, configPath)
- return 0
-}
-
-func writeMissingCompletionCommandName(stderr io.Writer) {
- clierrors.WriteErrorEnvelope(stderr, (&clierrors.ArgumentError{
- Message: "--list-options requires a command name",
- Option: clicore.ListOptionsFlag,
- Command: clicore.CompletionCommand,
- NextActions: []string{"Pass the command name after `--list-options`."},
- }).ToCLIError(clierrors.ErrorContext{Command: clicore.CompletionCommand}))
-}
-
-func writeCompletionShellDetectionError(stderr io.Writer) {
- clierrors.WriteErrorEnvelope(stderr, clierrors.CLIError{
- ErrorCode: clierrors.ErrorCodeInvalidArgument,
- Phase: clierrors.ErrorPhaseArgumentParsing,
- Message: "Could not detect shell.",
- Retryable: false,
- SafeToRetry: false,
- Command: clicore.CompletionCommand,
- NextActions: []string{"Pass `--shell bash`, `--shell zsh`, `--shell powershell`, or `--shell pwsh`."},
- })
-}
-
-func writeCompletionInstallResult(stdout io.Writer, shellName string, configPath string) {
- clicore.WriteFormat(stdout, "Completion installed to %s\n", configPath)
- if isPowerShellShell(shellName) {
- clicore.WriteLine(stdout, "Restart PowerShell to enable completion.")
- return
- }
- clicore.WriteFormat(stdout, "Run 'source %s' or restart your shell to enable completion.\n", configPath)
-}
-
-type completionRequest struct {
- install bool
- shell string
-}
-
-func parseCompletionRequest(args []string) (completionRequest, error) {
- request := completionRequest{}
- for index := 0; index < len(args); index++ {
- arg := args[index]
- if arg == installCompletionFlag {
- request.install = true
- continue
- }
-
- if strings.HasPrefix(arg, shellFlag+"=") {
- normalized, err := normalizeShell(strings.TrimPrefix(arg, shellFlag+"="))
- if err != nil {
- return completionRequest{}, err
- }
- request.shell = normalized
- continue
- }
-
- if arg == shellFlag {
- if index+1 >= len(args) {
- return completionRequest{}, clierrors.MissingValueArgumentError(shellFlag)
- }
- normalized, err := normalizeShell(args[index+1])
- if err != nil {
- return completionRequest{}, err
- }
- request.shell = normalized
- index++
- continue
- }
-
- return completionRequest{}, &clierrors.ArgumentError{
- Message: "Unknown completion option: " + arg,
- Option: arg,
- Command: clicore.CompletionCommand,
- NextActions: []string{"Run `uloop completion --help` to inspect supported completion options."},
- }
- }
- return request, nil
-}
-
-func normalizeShell(value string) (string, error) {
- normalized := strings.ToLower(value)
- if normalized == "bash" || normalized == "zsh" || normalized == "powershell" || normalized == "pwsh" {
- return normalized, nil
- }
- if normalized == "powershell-core" {
- return "pwsh", nil
- }
- return "", &clierrors.ArgumentError{
- Message: "Unknown shell: " + value,
- Option: shellFlag,
- Received: value,
- ExpectedType: "bash|zsh|powershell|pwsh",
- Command: clicore.CompletionCommand,
- NextActions: []string{"Use one of: bash, zsh, powershell, pwsh."},
- }
-}
-
-func printCommandNames(cache clicore.ToolsCache, stdout io.Writer) {
- seen := map[string]bool{}
- nativeCommandNames := clicore.NativeCommandNamesForCompletion()
- commands := make([]string, 0, len(nativeCommandNames)+len(cache.Tools))
- for _, command := range nativeCommandNames {
- if seen[command] {
- continue
- }
- seen[command] = true
- commands = append(commands, command)
- }
- for _, tool := range cache.Tools {
- if seen[tool.Name] {
- continue
- }
- seen[tool.Name] = true
- commands = append(commands, tool.Name)
- }
- sort.Strings(commands)
- clicore.WriteLine(stdout, strings.Join(commands, "\n"))
-}
-
-func printOptionsForCommand(command string, cache clicore.ToolsCache, stdout io.Writer) {
- nativeOptions, ok := nativeCommandOptions[command]
- if ok {
- options := append([]string{}, nativeOptions...)
- sort.Strings(options)
- clicore.WriteLine(stdout, strings.Join(options, "\n"))
- return
- }
- if _, ok := clicore.NativeCommand(command); ok {
- clicore.WriteLine(stdout, "")
- return
- }
- if command == clicore.ExecuteDynamicCodeCommandName {
- tool, ok := clicore.FindDefaultTool(command)
- if !ok {
- return
- }
- printOptionsForTool(tool, stdout)
- return
- }
-
- tool, ok := clicore.FindTool(cache, command)
- if !ok {
- return
- }
-
- printOptionsForTool(tool, stdout)
-}
-
-func printOptionsForTool(tool clicore.ToolDefinition, stdout io.Writer) {
- clicore.WriteLine(stdout, strings.Join(tooldocs.VisibleOptionNamesForTool(tool), "\n"))
-}
-
-func detectShell() string {
- return detectShellForPlatform(runtime.GOOS, os.Getenv("SHELL"), os.Getenv("MSYSTEM"), exec.LookPath)
-}
-
-func detectShellFromEnvironment(goos string, shellPath string, msystem string) string {
- return detectShellForPlatform(goos, shellPath, msystem, exec.LookPath)
-}
-
-func detectShellForPlatform(goos string, shellPath string, msystem string, lookPath func(string) (string, error)) string {
- shellName := detectShellName(shellPath)
- if goos == "windows" {
- if isPosixShell(shellName) && msystem != "" {
- return shellName
- }
- if shellName == "pwsh" || shellName == "powershell" {
- return shellName
- }
- if _, err := lookPath("pwsh"); err == nil {
- return "pwsh"
- }
- if _, err := lookPath("powershell"); err == nil {
- return "powershell"
- }
- return ""
- }
-
- return shellName
-}
-
-func detectShellName(shellPath string) string {
- shellPath = strings.ToLower(shellPath)
- if strings.Contains(shellPath, "pwsh") {
- return "pwsh"
- }
- if strings.Contains(shellPath, "powershell") {
- return "powershell"
- }
- if strings.Contains(shellPath, "zsh") {
- return "zsh"
- }
- if strings.Contains(shellPath, "bash") {
- return "bash"
- }
- return ""
-}
-
-func getShellConfigPath(shellName string) (string, error) {
- home, err := getHomeDirectoryForShell(shellName, runtime.GOOS, getHomeDirectory, os.UserHomeDir)
- if err != nil {
- return "", err
- }
-
- switch shellName {
- case "zsh":
- return filepath.Join(home, ".zshrc"), nil
- case "bash":
- return filepath.Join(home, ".bashrc"), nil
- case "powershell":
- return filepath.Join(home, filepath.FromSlash(powerShellProfileSubpath)), nil
- case "pwsh":
- return getPwshProfilePath(home, runtime.GOOS), nil
- default:
- return "", fmt.Errorf("unknown shell: %s", shellName)
- }
-}
-
-func getHomeDirectory() (string, error) {
- home := os.Getenv("HOME")
- if home != "" {
- return home, nil
- }
-
- return os.UserHomeDir()
-}
-
-func getHomeDirectoryForShell(
- shellName string,
- goos string,
- environmentHomeDirectory func() (string, error),
- userHomeDirectory func() (string, error),
-) (string, error) {
- if goos == "windows" && isPowerShellShell(shellName) {
- return userHomeDirectory()
- }
-
- home, err := environmentHomeDirectory()
- if err != nil {
- return "", err
- }
- if goos == "windows" && isPosixShell(shellName) {
- return normalizeWindowsPosixHomeDirectory(home), nil
- }
- return home, nil
-}
-
-func getPwshProfilePath(home string, goos string) string {
- if goos == "windows" {
- return filepath.Join(home, filepath.FromSlash(pwshProfileSubpath))
- }
- return filepath.Join(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1")
-}
-
-func isPosixShell(shellName string) bool {
- return shellName == "bash" || shellName == "zsh"
-}
-
-func normalizeWindowsPosixHomeDirectory(home string) string {
- if home == "" {
- return home
- }
- if len(home) >= 3 && home[0] == '/' && isASCIIAlpha(home[1]) && home[2] == '/' {
- return windowsDrivePath(home[1], home[3:])
- }
- if len(home) >= 7 && strings.HasPrefix(home, "/mnt/") && isASCIIAlpha(home[5]) && home[6] == '/' {
- return windowsDrivePath(home[5], home[7:])
- }
- return home
-}
-
-func windowsDrivePath(driveLetter byte, rest string) string {
- drive := string(toUpperASCIILetter(driveLetter)) + `:\`
- if rest == "" {
- return drive
- }
- return drive + strings.ReplaceAll(rest, "/", `\`)
-}
-
-func isASCIIAlpha(value byte) bool {
- return (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z')
-}
-
-func toUpperASCIILetter(value byte) byte {
- if value >= 'a' && value <= 'z' {
- return value - ('a' - 'A')
- }
- return value
-}
-
-func installCompletionScript(configPath string, shellName string, script string) error {
- if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
- return err
- }
-
- content := ""
- if existing, err := os.ReadFile(configPath); err == nil {
- content = string(existing)
- }
-
- content = removeExistingCompletionBlock(content)
- lineToAdd := "\n" + completionStartMarker + "\n"
- if isPowerShellShell(shellName) {
- lineToAdd += script + "\n"
- } else {
- lineToAdd += fmt.Sprintf("eval \"$(uloop completion --shell %s)\"\n", shellName)
- }
- lineToAdd += completionEndMarker + "\n"
- return os.WriteFile(configPath, []byte(content+lineToAdd), 0o644)
-}
-
-func removeExistingCompletionBlock(content string) string {
- return completionBlockPattern.ReplaceAllString(content, "")
-}
-
-func getCompletionScript(shellName string) string {
- switch shellName {
- case "bash":
- return `# uloop bash completion
-_uloop_completions() {
- local cur="${COMP_WORDS[COMP_CWORD]}"
- local cmd="${COMP_WORDS[1]}"
-
- if [[ ${COMP_CWORD} -eq 1 ]]; then
- COMPREPLY=($(compgen -W "$(uloop --list-commands 2>/dev/null)" -- "${cur}"))
- elif [[ ${COMP_CWORD} -ge 2 ]]; then
- COMPREPLY=($(compgen -W "$(uloop --list-options ${cmd} 2>/dev/null)" -- "${cur}"))
- fi
-}
-complete -F _uloop_completions uloop`
- case "powershell", "pwsh":
- return `# uloop PowerShell completion
-Register-ArgumentCompleter -Native -CommandName uloop -ScriptBlock {
- param($wordToComplete, $commandAst, $cursorPosition)
- $commands = $commandAst.CommandElements
- if ($commands.Count -eq 1) {
- uloop --list-commands 2>$null | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
- [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
- }
- } elseif ($commands.Count -ge 2) {
- $cmd = $commands[1].ToString()
- uloop --list-options $cmd 2>$null | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
- [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
- }
- }
-}`
- default:
- return `# uloop zsh completion
-_uloop() {
- local -a commands
- local -a options
- local -a used_options
-
- if (( CURRENT == 2 )); then
- commands=(${(f)"$(uloop --list-commands 2>/dev/null)"})
- _describe 'command' commands
- elif (( CURRENT >= 3 )); then
- options=(${(f)"$(uloop --list-options ${words[2]} 2>/dev/null)"})
- used_options=(${words:2})
- for opt in ${used_options}; do
- options=(${options:#$opt})
- done
- _describe 'option' options
- fi
-}
-compdef _uloop uloop`
- }
-}
-
-func isPowerShellShell(shellName string) bool {
- return shellName == "powershell" || shellName == "pwsh"
-}
-
-func printCompletionHelp(stdout io.Writer) {
- clicore.WriteLine(stdout, "Usage:")
- clicore.WriteLine(stdout, " uloop completion [--shell bash|zsh|powershell|pwsh] [--install]")
- clicore.WriteLine(stdout, "")
- clicore.WriteLine(stdout, "Completion helpers:")
- clicore.WriteLine(stdout, " uloop --list-commands Print command names for completion")
- clicore.WriteLine(stdout, " uloop --list-options Print options for a command")
+ return true, 0
}
diff --git a/cli/dispatcher/internal/dispatcher/completion_options.go b/cli/dispatcher/internal/dispatcher/completion_options.go
deleted file mode 100644
index f5f74feb11..0000000000
--- a/cli/dispatcher/internal/dispatcher/completion_options.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package dispatcher
-
-import (
- "github.com/hatayama/unity-cli-loop/common/clicore"
- "github.com/hatayama/unity-cli-loop/common/tooldocs"
-)
-
-var nativeCommandOptions = map[string][]string{
- clicore.CompletionCommand: {installCompletionFlag, shellFlag},
- clicore.LaunchCommandName: {
- "--" + tooldocs.ProjectPathFlagName,
- "--delete-recovery",
- "--editor-version",
- "--max-depth",
- "--platform",
- "--quit",
- "--restart",
- },
- clicore.InstallCommandName: {"--" + installDirFlagName},
- clicore.UpdateCommandName: {"--" + updateToVersionFlagName},
- clicore.PausePointAwaitCommandName: {
- "--" + clicore.PausePointIDFlagName,
- "--" + clicore.PausePointTimeoutFlagName,
- "--" + clicore.PausePointLogsMaxCountFlagName,
- "--" + clicore.PausePointCapturedVariablesFlagName,
- "--" + tooldocs.ProjectPathFlagName,
- },
- clicore.PausePointStatusUserCommandName: {
- "--" + clicore.PausePointIDFlagName,
- "--" + clicore.PausePointCapturedVariablesFlagName,
- "--" + tooldocs.ProjectPathFlagName,
- },
-}
diff --git a/cli/dispatcher/internal/dispatcher/completion_test.go b/cli/dispatcher/internal/dispatcher/completion_test.go
index 2eea80c0ed..02b4b2add3 100644
--- a/cli/dispatcher/internal/dispatcher/completion_test.go
+++ b/cli/dispatcher/internal/dispatcher/completion_test.go
@@ -1,610 +1,37 @@
package dispatcher
import (
- "bytes"
- "os"
- "path/filepath"
- "runtime"
- "slices"
- "strings"
"testing"
"github.com/hatayama/unity-cli-loop/common/clicore"
)
-func TestCompletionListCommandsIncludesNativeCommandsAndDefaultTools(t *testing.T) {
- var stdout bytes.Buffer
- handled, code := tryHandleCompletionRequest(
- []string{"--list-commands"},
- clicore.LoadDefaultTools(),
- &stdout,
- &bytes.Buffer{},
- )
-
- if !handled {
- t.Fatal("completion request was not handled")
- }
- if code != 0 {
- t.Fatalf("exit code mismatch: %d", code)
- }
-
- output := stdout.String()
- for _, command := range []string{"completion", "focus-window", "sync", "uninstall"} {
- if !strings.Contains(output, command) {
- t.Fatalf("command %s was not listed: %s", command, output)
- }
- }
-}
-
-func TestCompletionListOptionsUsesToolSchema(t *testing.T) {
- var stdout bytes.Buffer
- handled, code := tryHandleCompletionRequest(
- []string{"--list-options", "compile"},
- clicore.LoadDefaultTools(),
- &stdout,
- &bytes.Buffer{},
- )
-
- if !handled {
- t.Fatal("completion request was not handled")
- }
- if code != 0 {
- t.Fatalf("exit code mismatch: %d", code)
- }
-
- output := stdout.String()
- for _, option := range []string{"--force-recompile", "--no-wait-for-domain-reload", "--stop-on-external-scene-changes"} {
- if !strings.Contains(output, option) {
- t.Fatalf("option %s was not listed: %s", option, output)
- }
- }
-}
-
-func TestCompletionListOptionsPrefersProjectCacheForDefaultToolNames(t *testing.T) {
- // Verifies synced project tool metadata overrides embedded defaults for regular commands.
- var stdout bytes.Buffer
- cache := clicore.ToolsCache{
- Tools: []clicore.ToolDefinition{
- {
- Name: "compile",
- InputSchema: clicore.InputSchema{
- Type: "object",
- Properties: map[string]clicore.ToolProperty{
- "CachedOnly": {Type: "boolean"},
- },
- },
- },
- },
- }
-
- handled, code := tryHandleCompletionRequest(
- []string{"--list-options", "compile"},
- cache,
- &stdout,
- &bytes.Buffer{},
- )
-
- if !handled {
- t.Fatal("completion request was not handled")
- }
- if code != 0 {
- t.Fatalf("exit code mismatch: %d", code)
- }
-
- output := stdout.String()
- if !strings.Contains(output, "--cached-only") {
- t.Fatalf("cached compile options were not used: %s", output)
- }
- if strings.Contains(output, "--stop-on-external-scene-changes") {
- t.Fatalf("embedded compile option should not be listed when cache exists: %s", output)
- }
-}
-
-func TestCompletionListOptionsUsesExecuteDynamicCodeWaitFlag(t *testing.T) {
- // Verifies shell completion exposes reload waiting as an explicit opt-in flag.
- var stdout bytes.Buffer
- handled, code := tryHandleCompletionRequest(
- []string{"--list-options", clicore.ExecuteDynamicCodeCommandName},
- clicore.LoadDefaultTools(),
- &stdout,
- &bytes.Buffer{},
- )
-
- if !handled {
- t.Fatal("completion request was not handled")
- }
- if code != 0 {
- t.Fatalf("exit code mismatch: %d", code)
- }
-
- output := stdout.String()
- if !strings.Contains(output, "--wait-for-domain-reload") {
- t.Fatalf("execute-dynamic-code wait option was not listed: %s", output)
- }
- if !strings.Contains(output, "--code-file") {
- t.Fatalf("execute-dynamic-code code-file option was not listed: %s", output)
- }
- if strings.Contains(output, "--no-wait-for-domain-reload") {
- t.Fatalf("execute-dynamic-code no-wait option should not be listed: %s", output)
- }
- if strings.Contains(output, "--compile-only") {
- t.Fatalf("execute-dynamic-code internal compile-only option should stay hidden: %s", output)
- }
-}
-
-func TestCompletionListOptionsUsesEmbeddedExecuteDynamicCodeDefinition(t *testing.T) {
- // Verifies stale project caches do not hide hot-path execute-dynamic-code options.
- var stdout bytes.Buffer
- cache := clicore.ToolsCache{
- Tools: []clicore.ToolDefinition{
- {
- Name: "execute-dynamic-code",
- InputSchema: clicore.InputSchema{
- Properties: map[string]clicore.ToolProperty{
- "Code": {Type: "string"},
- },
- },
- },
- },
- }
-
- handled, code := tryHandleCompletionRequest(
- []string{"--list-options", clicore.ExecuteDynamicCodeCommandName},
- cache,
- &stdout,
- &bytes.Buffer{},
- )
-
- if !handled {
- t.Fatal("completion request was not handled")
- }
- if code != 0 {
- t.Fatalf("exit code mismatch: %d", code)
- }
-
- output := stdout.String()
- if !strings.Contains(output, "--wait-for-domain-reload") {
- t.Fatalf("embedded execute-dynamic-code options were not used: %s", output)
- }
-}
-
-func TestCompletionListOptionsUsesNativeLaunchOptions(t *testing.T) {
- // Verifies shell completion still suggests native launch flags after CLI unification.
- var stdout bytes.Buffer
- handled, code := tryHandleCompletionRequest(
- []string{"--list-options", clicore.LaunchCommandName},
- clicore.LoadDefaultTools(),
- &stdout,
- &bytes.Buffer{},
- )
-
- if !handled {
- t.Fatal("completion request was not handled")
- }
- if code != 0 {
- t.Fatalf("exit code mismatch: %d", code)
- }
-
- output := stdout.String()
- listedOptions := strings.Split(strings.TrimSpace(output), "\n")
- for _, option := range []string{"--project-path", "--restart", "--quit", "--delete-recovery", "--platform", "--max-depth", "--editor-version"} {
- if !slices.Contains(listedOptions, option) {
- t.Fatalf("launch option %s was not listed: %s", option, output)
- }
- }
- for _, option := range []string{"-i", "--ignore-compiler-errors"} {
- if slices.Contains(listedOptions, option) {
- t.Fatalf("removed launch option %s was listed: %s", option, output)
- }
- }
-}
-
-func TestCompletionListOptionsUsesNativeUpdateOptions(t *testing.T) {
- // Verifies shell completion suggests exact update target flags.
- var stdout bytes.Buffer
- handled, code := tryHandleCompletionRequest(
- []string{"--list-options", clicore.UpdateCommandName},
- clicore.LoadDefaultTools(),
- &stdout,
- &bytes.Buffer{},
- )
-
- if !handled {
- t.Fatal("completion request was not handled")
- }
- if code != 0 {
- t.Fatalf("exit code mismatch: %d", code)
- }
- if !strings.Contains(stdout.String(), "--to-version") {
- t.Fatalf("update option was not listed: %s", stdout.String())
- }
-}
-
-func TestCompletionCommandListOptionsUsesNativeCompletionOptions(t *testing.T) {
- // Verifies nested completion option probes preserve dispatcher-compatible behavior.
- var stdout bytes.Buffer
- handled, code := tryHandleCompletionRequest(
- []string{clicore.CompletionCommand, "--list-options", clicore.CompletionCommand},
- clicore.LoadDefaultTools(),
- &stdout,
- &bytes.Buffer{},
- )
-
- if !handled {
- t.Fatal("completion request was not handled")
- }
- if code != 0 {
- t.Fatalf("exit code mismatch: %d", code)
- }
-
- output := stdout.String()
- for _, option := range []string{"--install", "--shell"} {
- if !strings.Contains(output, option) {
- t.Fatalf("completion option %s was not listed: %s", option, output)
- }
- }
-}
-
-func TestCompletionHelpDocumentsMachineReadableHelpers(t *testing.T) {
- // Verifies completion-specific probes are documented outside the main help surface.
- var stdout bytes.Buffer
- handled, code := tryHandleCompletionRequest(
- []string{clicore.CompletionCommand, "--help"},
- clicore.LoadDefaultTools(),
- &stdout,
- &bytes.Buffer{},
- )
-
- if !handled {
- t.Fatal("completion request was not handled")
- }
- if code != 0 {
- t.Fatalf("exit code mismatch: %d", code)
- }
-
- output := stdout.String()
- for _, expected := range []string{"uloop --list-commands", "uloop --list-options "} {
- if !strings.Contains(output, expected) {
- t.Fatalf("completion help missing %q:\n%s", expected, output)
- }
- }
-}
-
-func TestCompletionListOptionsIgnoresCachedToolSchemaForNativeCommand(t *testing.T) {
- // Verifies native commands keep priority when a cached Unity tool has the same name.
- var stdout bytes.Buffer
- cache := clicore.ToolsCache{
- Tools: []clicore.ToolDefinition{
- {
- Name: "focus-window",
- InputSchema: clicore.InputSchema{
- Type: "object",
- Properties: map[string]clicore.ToolProperty{
- "ProjectPath": {Type: "string"},
- },
- },
- },
- },
- }
-
- handled, code := tryHandleCompletionRequest(
- []string{"--list-options", "focus-window"},
- cache,
- &stdout,
- &bytes.Buffer{},
- )
-
- if !handled {
- t.Fatal("completion request was not handled")
- }
- if code != 0 {
- t.Fatalf("exit code mismatch: %d", code)
- }
- if stdout.String() != "\n" {
- t.Fatalf("native command should not use cached tool options: %s", stdout.String())
- }
-}
-
-// Tests that completion lists default-enabled boolean arguments as --no-* flags.
-func TestCompletionListOptionsUsesNegatedDefaultTrueBooleanFlags(t *testing.T) {
- var stdout bytes.Buffer
- handled, code := tryHandleCompletionRequest(
- []string{"--list-options", "get-hierarchy"},
- clicore.LoadDefaultTools(),
- &stdout,
- &bytes.Buffer{},
- )
-
- if !handled {
- t.Fatal("completion request was not handled")
- }
- if code != 0 {
- t.Fatalf("exit code mismatch: %d", code)
- }
-
- output := stdout.String()
- options := strings.Split(strings.TrimSpace(output), "\n")
- for _, option := range []string{"--no-include-components", "--no-include-inactive"} {
- if !slices.Contains(options, option) {
- t.Fatalf("option %s was not listed: %s", option, output)
- }
- }
- for _, option := range []string{"--include-components", "--include-inactive"} {
- if slices.Contains(options, option) {
- t.Fatalf("default-enabled option %s should not be listed: %s", option, output)
- }
- }
-}
-
-func TestCompletionPrintsShellScriptWithoutProject(t *testing.T) {
- var stdout bytes.Buffer
- handled, code := tryHandleCompletionRequest(
- []string{"completion", "--shell", "bash"},
- clicore.LoadDefaultTools(),
- &stdout,
- &bytes.Buffer{},
- )
-
- if !handled {
- t.Fatal("completion request was not handled")
- }
- if code != 0 {
- t.Fatalf("exit code mismatch: %d", code)
- }
-
- output := stdout.String()
- if !strings.Contains(output, "complete -F _uloop_completions uloop") {
- t.Fatalf("bash completion script mismatch: %s", output)
- }
-}
-
-func TestCompletionDetectionSkipsRegularToolCommands(t *testing.T) {
- // Verifies that normal tool execution avoids completion cache loading.
- if clicore.ShouldHandleCompletionRequest([]string{clicore.ExecuteDynamicCodeCommandName, "--code", "return 1;"}) {
- t.Fatal("execute-dynamic-code should not enter completion handling")
- }
-}
-
-func TestCompletionDetectionHandlesCompletionCommands(t *testing.T) {
- // Verifies that completion-specific commands still load completion metadata.
+// Verifies the completion stub silently handles any arguments so shell rc
+// files with a stale `eval "$(uloop completion --shell zsh)"` block keep
+// starting up without an unknown-command error.
+func TestTryHandleCompletionRequestIsSilentNoOpForAnyArgs(t *testing.T) {
for _, args := range [][]string{
- {clicore.CompletionCommand, "--shell", "bash"},
- {clicore.ListCommandsFlag},
- {clicore.ListOptionsFlag, "compile"},
+ {clicore.CompletionCommand},
+ {clicore.CompletionCommand, "--shell", "zsh"},
+ {clicore.CompletionCommand, "--install"},
+ {clicore.CompletionCommand, "--help"},
+ {clicore.CompletionCommand, "--anything", "unexpected"},
} {
- if !clicore.ShouldHandleCompletionRequest(args) {
- t.Fatalf("completion request was not detected: %#v", args)
- }
- }
-}
+ handled, code := tryHandleCompletionRequest(args)
-// Tests that Git Bash auto-install writes bash completion instead of PowerShell completion.
-func TestDetectShellOnWindowsGitBashUsesBash(t *testing.T) {
- shellName := detectShellFromEnvironment("windows", "/usr/bin/bash", "MINGW64")
-
- if shellName != "bash" {
- t.Fatalf("windows Git Bash shell mismatch: %s", shellName)
- }
-}
-
-func TestDetectShellOnWindowsPrefersPwshWhenAvailable(t *testing.T) {
- // Verifies Windows completion install targets PowerShell 7 when it is available.
- shellName := detectShellForPlatform("windows", "", "", func(name string) (string, error) {
- if name == "pwsh" {
- return filepath.Join("bin", "pwsh"), nil
+ if !handled {
+ t.Fatalf("%v: completion command must be handled", args)
}
- return "", os.ErrNotExist
- })
-
- if shellName != "pwsh" {
- t.Fatalf("windows shell mismatch: %s", shellName)
- }
-}
-
-// Tests that regular Windows terminals still get the native PowerShell completion default.
-func TestDetectShellOnWindowsPowerShellDefaultsToPowerShell(t *testing.T) {
- shellName := detectShellForPlatform("windows", "", "", func(name string) (string, error) {
- if name == "powershell" {
- return filepath.Join("bin", "powershell"), nil
+ if code != 0 {
+ t.Fatalf("%v: expected exit code 0, got %d", args, code)
}
- return "", os.ErrNotExist
- })
-
- if shellName != "powershell" {
- t.Fatalf("windows default shell mismatch: %s", shellName)
}
}
-func TestCompletionInstallReplacesExistingBlock(t *testing.T) {
- temporaryHome := t.TempDir()
- t.Setenv("HOME", temporaryHome)
-
- configPath := filepath.Join(temporaryHome, ".zshrc")
- existing := "before\n" + completionStartMarker + "\nstale\n" + completionEndMarker + "\nafter\n"
- if err := os.WriteFile(configPath, []byte(existing), 0o644); err != nil {
- t.Fatalf("failed to seed shell config: %v", err)
- }
-
- var stdout bytes.Buffer
- handled, code := tryHandleCompletionRequest(
- []string{"completion", "--shell", "zsh", "--install"},
- clicore.LoadDefaultTools(),
- &stdout,
- &bytes.Buffer{},
- )
-
- if !handled {
- t.Fatal("completion request was not handled")
- }
- if code != 0 {
- t.Fatalf("exit code mismatch: %d", code)
- }
-
- content, err := os.ReadFile(configPath)
- if err != nil {
- t.Fatalf("failed to read shell config: %v", err)
- }
-
- result := string(content)
- if strings.Contains(result, "stale") {
- t.Fatalf("stale completion block was not removed: %s", result)
- }
- if !strings.Contains(result, `eval "$(uloop completion --shell zsh)"`) {
- t.Fatalf("new completion eval line missing: %s", result)
- }
- if !strings.Contains(stdout.String(), "Completion installed") {
- t.Fatalf("install output mismatch: %s", stdout.String())
- }
-}
-
-func TestCompletionSupportsPwshProfile(t *testing.T) {
- temporaryHome := t.TempDir()
- t.Setenv("HOME", temporaryHome)
-
- configPath, err := getShellConfigPath("pwsh")
- if err != nil {
- t.Fatalf("getShellConfigPath failed: %v", err)
- }
-
- expectedHome := temporaryHome
- if runtime.GOOS == "windows" {
- userHome, userHomeErr := os.UserHomeDir()
- if userHomeErr != nil {
- t.Fatalf("os.UserHomeDir failed: %v", userHomeErr)
- }
- expectedHome = userHome
- }
- expectedPath := getPwshProfilePath(expectedHome, runtime.GOOS)
- if configPath != expectedPath {
- t.Fatalf("pwsh profile path mismatch: %s", configPath)
- }
-
- script := getCompletionScript("pwsh")
- if !strings.Contains(script, "Register-ArgumentCompleter") {
- t.Fatalf("pwsh completion script mismatch: %s", script)
- }
-}
-
-func TestGetPwshProfilePathUsesPlatformSpecificLocation(t *testing.T) {
- home := filepath.Join("home", "user")
-
- windowsPath := getPwshProfilePath(home, "windows")
- expectedWindowsPath := filepath.Join(home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1")
- if windowsPath != expectedWindowsPath {
- t.Fatalf("windows pwsh profile path mismatch: %s", windowsPath)
- }
-
- posixPath := getPwshProfilePath(home, "darwin")
- expectedPosixPath := filepath.Join(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1")
- if posixPath != expectedPosixPath {
- t.Fatalf("posix pwsh profile path mismatch: %s", posixPath)
- }
-}
-
-func TestGetHomeDirectoryForShellOnWindowsPowerShellIgnoresHomeOverride(t *testing.T) {
- // Tests that Windows PowerShell profiles use the Windows user profile instead of MSYS-style HOME.
- environmentHomeCalls := 0
- userHomeCalls := 0
-
- for _, shellName := range []string{"powershell", "pwsh"} {
- home, err := getHomeDirectoryForShell(
- shellName,
- "windows",
- func() (string, error) {
- environmentHomeCalls++
- return "/c/Users/ExampleUser", nil
- },
- func() (string, error) {
- userHomeCalls++
- return `C:\Users\ExampleUser`, nil
- },
- )
- if err != nil {
- t.Fatalf("getHomeDirectoryForShell failed: %v", err)
- }
-
- if home != `C:\Users\ExampleUser` {
- t.Fatalf("windows %s home mismatch: %s", shellName, home)
- }
- }
- if environmentHomeCalls != 0 {
- t.Fatalf("environment HOME should not be used for Windows PowerShell")
- }
- if userHomeCalls != 2 {
- t.Fatalf("user home resolver call count mismatch: %d", userHomeCalls)
- }
-}
-
-func TestGetHomeDirectoryForShellOnWindowsBashNormalizesMsysHome(t *testing.T) {
- // Tests that Windows POSIX shells convert Git Bash HOME to a Win32 path before file writes.
- environmentHomeCalls := 0
- userHomeCalls := 0
-
- home, err := getHomeDirectoryForShell(
- "bash",
- "windows",
- func() (string, error) {
- environmentHomeCalls++
- return "/c/Users/ExampleUser", nil
- },
- func() (string, error) {
- userHomeCalls++
- return `C:\Users\ExampleUser`, nil
- },
- )
- if err != nil {
- t.Fatalf("getHomeDirectoryForShell failed: %v", err)
- }
-
- if home != `C:\Users\ExampleUser` {
- t.Fatalf("windows bash home mismatch: %s", home)
- }
- if environmentHomeCalls != 1 {
- t.Fatalf("environment HOME resolver call count mismatch: %d", environmentHomeCalls)
- }
- if userHomeCalls != 0 {
- t.Fatalf("user home should not be used for Windows bash")
- }
-}
-
-func TestGetHomeDirectoryForShellOnWindowsZshNormalizesWslHome(t *testing.T) {
- // Tests that Windows POSIX shell HOME values from /mnt/c are safe for Win32 file APIs.
- home, err := getHomeDirectoryForShell(
- "zsh",
- "windows",
- func() (string, error) {
- return "/mnt/c/Users/ExampleUser", nil
- },
- func() (string, error) {
- return `C:\Users\IgnoredUser`, nil
- },
- )
- if err != nil {
- t.Fatalf("getHomeDirectoryForShell failed: %v", err)
- }
-
- if home != `C:\Users\ExampleUser` {
- t.Fatalf("windows zsh home mismatch: %s", home)
- }
-}
-
-func TestGetHomeDirectoryForShellOnWindowsBashNormalizesWslDriveRoot(t *testing.T) {
- // Tests that a WSL drive-root HOME is converted before Win32 file APIs receive it.
- home, err := getHomeDirectoryForShell(
- "bash",
- "windows",
- func() (string, error) {
- return "/mnt/c/", nil
- },
- func() (string, error) {
- return `C:\Users\IgnoredUser`, nil
- },
- )
- if err != nil {
- t.Fatalf("getHomeDirectoryForShell failed: %v", err)
- }
-
- if home != `C:\` {
- t.Fatalf("windows bash drive-root home mismatch: %s", home)
+// Verifies non-completion commands are left unhandled so normal dispatch continues.
+func TestTryHandleCompletionRequestIgnoresOtherCommands(t *testing.T) {
+ handled, _ := tryHandleCompletionRequest([]string{clicore.CompileCommandName})
+ if handled {
+ t.Fatal("non-completion command must not be handled by the completion stub")
}
}
diff --git a/cli/dispatcher/internal/dispatcher/dispatcher_bootstrap_routing_test.go b/cli/dispatcher/internal/dispatcher/dispatcher_bootstrap_routing_test.go
index b7c7edd229..8838ab2e01 100644
--- a/cli/dispatcher/internal/dispatcher/dispatcher_bootstrap_routing_test.go
+++ b/cli/dispatcher/internal/dispatcher/dispatcher_bootstrap_routing_test.go
@@ -123,24 +123,9 @@ func TestRunDispatcherSkillsHelpDoesNotRequireProjectPin(t *testing.T) {
}
}
-// Verifies command name completion works in the dispatcher process from the embedded tool catalog.
-func TestRunDispatcherListCommandsDoesNotRequireProjectPin(t *testing.T) {
- t.Chdir(t.TempDir())
-
- var stdout bytes.Buffer
- var stderr bytes.Buffer
- code := RunDispatcher(context.Background(), []string{"--list-commands"}, &stdout, &stderr)
-
- if code != 0 {
- t.Fatalf("dispatcher list-commands failed: code=%d stderr=%s", code, stderr.String())
- }
- if !strings.Contains(stdout.String(), "compile") {
- t.Fatalf("list-commands output mismatch: %s", stdout.String())
- }
-}
-
-// Verifies completion script generation works in the dispatcher process without a project pin.
-func TestRunDispatcherCompletionScriptDoesNotRequireProjectPin(t *testing.T) {
+// Verifies the completion stub runs in the dispatcher process without a project pin, since
+// shells with a stale `eval "$(uloop completion --shell zsh)"` block must keep starting up.
+func TestRunDispatcherCompletionStubDoesNotRequireProjectPin(t *testing.T) {
t.Chdir(t.TempDir())
var stdout bytes.Buffer
@@ -150,7 +135,7 @@ func TestRunDispatcherCompletionScriptDoesNotRequireProjectPin(t *testing.T) {
if code != 0 {
t.Fatalf("dispatcher completion failed: code=%d stderr=%s", code, stderr.String())
}
- if !strings.Contains(stdout.String(), "complete -F _uloop_completions uloop") {
- t.Fatalf("completion script output mismatch: %s", stdout.String())
+ if stdout.String() != "" {
+ t.Fatalf("completion stub must print nothing: %s", stdout.String())
}
}
diff --git a/cli/dispatcher/internal/dispatcher/dispatcher_process.go b/cli/dispatcher/internal/dispatcher/dispatcher_process.go
index f345608c96..ab22255783 100644
--- a/cli/dispatcher/internal/dispatcher/dispatcher_process.go
+++ b/cli/dispatcher/internal/dispatcher/dispatcher_process.go
@@ -47,11 +47,8 @@ func tryHandlePreConnectionRequestWithDeps(
stderr io.Writer,
deps dispatcherRunDeps,
) (bool, int) {
- if clicore.ShouldHandleCompletionRequest(remainingArgs) {
- completionTools := loadCompletionTools(startPath, projectPath)
- if handled, code := tryHandleCompletionRequest(remainingArgs, completionTools, stdout, stderr); handled {
- return true, code
- }
+ if handled, code := tryHandleCompletionRequest(remainingArgs); handled {
+ return true, code
}
if clicore.IsUnknownLeadingOption(command) {
clierrors.WriteClassifiedError(stderr, &clierrors.ArgumentError{
diff --git a/cli/dispatcher/internal/dispatcher/dispatcher_test.go b/cli/dispatcher/internal/dispatcher/dispatcher_test.go
index ec605ee861..559c991a44 100644
--- a/cli/dispatcher/internal/dispatcher/dispatcher_test.go
+++ b/cli/dispatcher/internal/dispatcher/dispatcher_test.go
@@ -159,6 +159,36 @@ func TestRunDispatcherForwardsProjectScopedVersionJSONToPinnedRunner(t *testing.
}
}
+func TestRunDispatcherForwardsRunnerOwnedCommandHelpToPinnedRunner(t *testing.T) {
+ // Verifies await-pause-point --help is forwarded to the pinned runner instead of being answered locally.
+ projectRoot := createDispatcherUnityProject(t)
+ cacheRoot := t.TempDir()
+ writeDispatcherProjectPin(t, projectRoot, clicontract.ProjectRunnerVersion())
+ writeCachedDispatcherRealCLI(t, cacheRoot, clicontract.ProjectRunnerVersion())
+ t.Setenv(nativepath.CacheDirEnvName, cacheRoot)
+ t.Setenv(dispatcherDisableSelfUpdateEnvName, "1")
+ t.Chdir(projectRoot)
+
+ deps := defaultDispatcherRunDeps()
+ var actualArgs []string
+ deps.runRealCLI = func(ctx context.Context, realCLIPath string, args []string, stdout io.Writer, stderr io.Writer) int {
+ actualArgs = append([]string{}, args...)
+ return 0
+ }
+
+ var stdout bytes.Buffer
+ var stderr bytes.Buffer
+ code := runDispatcherWithDeps(context.Background(), []string{clicore.PausePointAwaitCommandName, "--help"}, &stdout, &stderr, deps)
+
+ if code != 0 {
+ t.Fatalf("dispatcher failed: code=%d stderr=%s", code, stderr.String())
+ }
+ assertStringSliceEqual(t, actualArgs, []string{clicore.PausePointAwaitCommandName, "--help"})
+ if stdout.String() != "" {
+ t.Fatalf("dispatcher must not answer runner-owned command help locally: %s", stdout.String())
+ }
+}
+
func TestRunDispatcherCommandHelpDoesNotRequireProjectPin(t *testing.T) {
// Verifies dispatcher handles command help before project and pin resolution.
t.Chdir(t.TempDir())
diff --git a/cli/dispatcher/internal/dispatcher/help_test.go b/cli/dispatcher/internal/dispatcher/help_test.go
index ee119d8043..baada23a83 100644
--- a/cli/dispatcher/internal/dispatcher/help_test.go
+++ b/cli/dispatcher/internal/dispatcher/help_test.go
@@ -29,12 +29,13 @@ func TestPrintLauncherHelpListsNativeCommandsAndLiveToolGuidance(t *testing.T) {
" skills",
" uninstall",
"Unity tool commands are project-specific.",
+ "does not include the full command list",
+ "uloop --project-path /path/to/project --help",
"uloop list",
"--project-path ",
"uloop --project-path /path/to/project list",
"uloop --help",
"Show help for native and Unity tool commands",
- "uloop completion --help",
} {
if !strings.Contains(output, expected) {
t.Fatalf("help output missing %q:\n%s", expected, output)
@@ -68,10 +69,11 @@ func TestPrintProjectLocalHelpListsNativeCommandsAndLiveToolGuidance(t *testing.
" sync",
" uninstall",
"Unity tool commands are project-specific.",
+ "does not include the full command list",
+ "uloop --project-path /path/to/project --help",
"--project-path ",
"uloop --project-path /path/to/project list",
"uloop list",
- "uloop completion --help",
} {
if !strings.Contains(output, expected) {
t.Fatalf("help output missing %q:\n%s", expected, output)
@@ -151,30 +153,6 @@ func TestRunDispatcherHelpShowsConciseProjectToolDescriptions(t *testing.T) {
}
}
-// Tests that native project commands show the shared project path option.
-func TestRunDispatcherListHelpShowsGlobalOptions(t *testing.T) {
- t.Chdir(t.TempDir())
- var stdout bytes.Buffer
- var stderr bytes.Buffer
-
- code := RunDispatcher(context.Background(), []string{"list", "--help"}, &stdout, &stderr)
-
- if code != 0 {
- t.Fatalf("list help failed: code=%d stderr=%s", code, stderr.String())
- }
- output := stdout.String()
- for _, expected := range []string{
- "Usage:",
- "uloop list",
- "Global options:",
- "--project-path ",
- } {
- if !strings.Contains(output, expected) {
- t.Fatalf("list help missing %q:\n%s", expected, output)
- }
- }
-}
-
// Tests that launch help documents both positional and global project selection.
func TestRunDispatcherLaunchHelpShowsGlobalOptions(t *testing.T) {
t.Chdir(t.TempDir())
@@ -409,28 +387,52 @@ func TestRunDispatcherSkillsSubcommandHelpDoesNotRequireUnityProject(t *testing.
}
}
-// Tests that native command help keeps the command description visible even
-// when the command has registered options.
-func TestRunDispatcherPausePointWaitHelpShowsDescriptionAndOptions(t *testing.T) {
+// Tests that a runner-owned command's --help outside a Unity project reports a
+// friendly project-resolution error instead of a raw forwarding failure, since
+// its help is only available from the pinned runner once a project resolves.
+func TestRunDispatcherRunnerOwnedCommandHelpOutsideProjectGivesProjectGuidance(t *testing.T) {
t.Chdir(t.TempDir())
var stdout bytes.Buffer
var stderr bytes.Buffer
code := RunDispatcher(context.Background(), []string{clicore.PausePointAwaitCommandName, "--help"}, &stdout, &stderr)
- if code != 0 {
- t.Fatalf("pause-point-wait help failed: code=%d stderr=%s", code, stderr.String())
+ if code != 1 {
+ t.Fatalf("expected failure outside a Unity project: code=%d stdout=%s", code, stdout.String())
}
- output := stdout.String()
+ output := stderr.String()
for _, expected := range []string{
- "Usage:",
- "uloop " + clicore.PausePointAwaitCommandName + " [options]",
- "Wait until a named UloopPausePoint.Pause marker pauses Unity",
- "Options:",
- "--" + clicore.PausePointIDFlagName,
+ "PROJECT_NOT_FOUND",
+ "Run the command from inside a Unity project.",
+ "when targeting another Unity project.",
+ } {
+ if !strings.Contains(output, expected) {
+ t.Fatalf("runner-owned command help guidance missing %q:\n%s", expected, output)
+ }
+ }
+}
+
+// Tests that a Unity tool command without an embedded default definition gives
+// the same project-resolution guidance as runner-owned commands when run
+// outside a Unity project, since both depend on resolving a project.
+func TestRunDispatcherUnityToolCommandHelpOutsideProjectGivesProjectGuidance(t *testing.T) {
+ t.Chdir(t.TempDir())
+ var stdout bytes.Buffer
+ var stderr bytes.Buffer
+
+ code := RunDispatcher(context.Background(), []string{"some-project-only-tool", "--help"}, &stdout, &stderr)
+
+ if code != 1 {
+ t.Fatalf("expected failure outside a Unity project: code=%d stdout=%s", code, stdout.String())
+ }
+ output := stderr.String()
+ for _, expected := range []string{
+ "PROJECT_NOT_FOUND",
+ "Run the command from inside a Unity project.",
+ "when targeting another Unity project.",
} {
if !strings.Contains(output, expected) {
- t.Fatalf("pause-point-wait help missing %q:\n%s", expected, output)
+ t.Fatalf("Unity tool command help guidance missing %q:\n%s", expected, output)
}
}
}
diff --git a/cli/dispatcher/internal/dispatcher/native_command_options.go b/cli/dispatcher/internal/dispatcher/native_command_options.go
new file mode 100644
index 0000000000..58afad825f
--- /dev/null
+++ b/cli/dispatcher/internal/dispatcher/native_command_options.go
@@ -0,0 +1,25 @@
+package dispatcher
+
+import (
+ "github.com/hatayama/unity-cli-loop/common/clicore"
+ "github.com/hatayama/unity-cli-loop/common/tooldocs"
+)
+
+// nativeCommandOptions lists the static --help options for native commands
+// whose help the dispatcher answers itself. Shell completion used to read
+// this table too, but that feature has been removed; only dispatcher-owned
+// commands remain here, since runner-owned commands forward --help to the
+// pinned runner instead (see command_help.go).
+var nativeCommandOptions = map[string][]string{
+ clicore.LaunchCommandName: {
+ "--" + tooldocs.ProjectPathFlagName,
+ "--delete-recovery",
+ "--editor-version",
+ "--max-depth",
+ "--platform",
+ "--quit",
+ "--restart",
+ },
+ clicore.InstallCommandName: {"--" + installDirFlagName},
+ clicore.UpdateCommandName: {"--" + updateToVersionFlagName},
+}
diff --git a/cli/dispatcher/internal/dispatcher/run_dispatcher.go b/cli/dispatcher/internal/dispatcher/run_dispatcher.go
index edbce5dcef..6a949a65a9 100644
--- a/cli/dispatcher/internal/dispatcher/run_dispatcher.go
+++ b/cli/dispatcher/internal/dispatcher/run_dispatcher.go
@@ -173,11 +173,11 @@ func runDispatcherProcessCommandWithDeps(
}
func shouldKeepDispatcherProcessCommand(args []string) bool {
- if len(args) == 0 || clicore.ShouldHandleCompletionRequest(args) {
+ if len(args) == 0 {
return true
}
switch args[0] {
- case clicore.InstallCommandName, clicore.UpdateCommandName, clicore.UninstallCommandName, clicore.LaunchCommandName:
+ case clicore.InstallCommandName, clicore.UpdateCommandName, clicore.UninstallCommandName, clicore.LaunchCommandName, clicore.CompletionCommand:
return true
default:
return false
@@ -185,9 +185,15 @@ func shouldKeepDispatcherProcessCommand(args []string) bool {
}
func shouldRunInDispatcherProcess(args []string) bool {
- if len(args) == 0 || clicore.IsHelpRequest(args) || clicore.ContainsHelpRequest(args) {
+ if len(args) == 0 || clicore.IsHelpRequest(args) {
return true
}
+ if clicore.ContainsHelpRequest(args) {
+ // Runner-owned command help is answered by the pinned runner itself
+ // (see command_help.go), not by a dispatcher-side table, so it must be
+ // forwarded through the normal dispatch path instead of staying here.
+ return !clicore.IsRunnerOwnedCommandName(args[0])
+ }
// Why version requests must be forwarded: bare --version is answered by
// tryHandleDispatcherInfoRequest before global-argument parsing, so a
// version request seen here is project-scoped and the pinned runner must
@@ -198,9 +204,6 @@ func shouldRunInDispatcherProcess(args []string) bool {
if clicore.IsUnknownLeadingOption(args[0]) {
return true
}
- if clicore.ShouldHandleCompletionRequest(args) {
- return true
- }
return clicore.IsDispatcherOwnedCommandName(args[0])
}
diff --git a/cli/dispatcher/internal/dispatcher/run_help.go b/cli/dispatcher/internal/dispatcher/run_help.go
index 56921848a0..73de6cf8e7 100644
--- a/cli/dispatcher/internal/dispatcher/run_help.go
+++ b/cli/dispatcher/internal/dispatcher/run_help.go
@@ -69,7 +69,6 @@ func printMainHelp(stdout io.Writer, displayVersion string, description string,
clicore.WriteLine(stdout, " uloop list Show the live Unity tool list")
clicore.WriteLine(stdout, " uloop --project-path /path/to/project list Show tools for another Unity project")
clicore.WriteLine(stdout, " uloop --help Show help for native and Unity tool commands")
- clicore.WriteLine(stdout, " uloop completion --help Show shell completion setup and helpers")
}
func printNativeCommandHelp(stdout io.Writer) {
@@ -87,7 +86,9 @@ func printGlobalOptionsHelp(stdout io.Writer) {
func printUnityToolCommandHelp(stdout io.Writer, cache clicore.ToolsCache, hasProjectToolCache bool) {
if !hasProjectToolCache {
clicore.WriteLine(stdout, "Unity tool commands are project-specific.")
+ clicore.WriteLine(stdout, " This output does not include the full command list; it depends on a resolved Unity project.")
clicore.WriteLine(stdout, " Run `uloop list` inside a Unity project to show the live tool list.")
+ clicore.WriteLine(stdout, " Run `uloop --project-path /path/to/project --help` to see the full command list and help for that project.")
clicore.WriteLine(stdout, " Run `uloop sync` after the Editor tool set changes to refresh cached commands.")
return
}
@@ -121,15 +122,3 @@ func commandListDescription(description string) string {
}
return strings.TrimSpace(string(runes[:maxCommandListDescriptionLength-3])) + "..."
}
-
-func loadCompletionTools(startPath string, projectPath string) clicore.ToolsCache {
- connection, err := project.ResolveConnection(startPath, projectPath)
- if err != nil {
- return clicore.LoadDefaultTools()
- }
- cache, err := clicore.LoadTools(connection.ProjectRoot)
- if err != nil {
- return clicore.LoadDefaultTools()
- }
- return cache
-}
diff --git a/cli/dispatcher/shared-inputs-stamp.json b/cli/dispatcher/shared-inputs-stamp.json
index 017eab2081..121363ffd8 100644
--- a/cli/dispatcher/shared-inputs-stamp.json
+++ b/cli/dispatcher/shared-inputs-stamp.json
@@ -1,4 +1,4 @@
{
"schemaVersion": 1,
- "sharedInputsHash": "1177f4566ab96dda739ca71da17255030aa7d7c0"
+ "sharedInputsHash": "9eed1894ac125f75cdbb2f5145977ecfa92095bf"
}
diff --git a/cli/project-runner/internal/projectrunner/native_command_help.go b/cli/project-runner/internal/projectrunner/native_command_help.go
new file mode 100644
index 0000000000..ccdf186527
--- /dev/null
+++ b/cli/project-runner/internal/projectrunner/native_command_help.go
@@ -0,0 +1,77 @@
+package projectrunner
+
+import (
+ "io"
+ "sort"
+
+ "github.com/hatayama/unity-cli-loop/common/clicore"
+ "github.com/hatayama/unity-cli-loop/common/tooldocs"
+)
+
+const (
+ PausePointIDFlagName = "id"
+ PausePointTimeoutFlagName = "timeout-seconds"
+ PausePointLogsMaxCountFlagName = "matching-logs-max-count"
+ PausePointCapturedVariablesFlagName = "captured-variables"
+)
+
+// runnerNativeCommandOptions lists the flags accepted by each runner-owned
+// native command. It lives with the runner binary (rather than the
+// dispatcher) so that adding a flag to a runner-owned command never requires
+// a dispatcher code change or release.
+var runnerNativeCommandOptions = map[string][]string{
+ clicore.PausePointAwaitCommandName: {
+ "--" + PausePointIDFlagName,
+ "--" + PausePointTimeoutFlagName,
+ "--" + PausePointLogsMaxCountFlagName,
+ "--" + PausePointCapturedVariablesFlagName,
+ },
+ clicore.PausePointStatusUserCommandName: {
+ "--" + PausePointIDFlagName,
+ "--" + PausePointCapturedVariablesFlagName,
+ },
+}
+
+// tryPrintNativeCommandHelp prints command-specific help for a runner-owned
+// native command and reports whether it did. The dispatcher forwards `--help`
+// for these commands to the pinned runner instead of keeping a hardcoded
+// options table, so this output must stay in sync with the runner's actual
+// flags by construction.
+func tryPrintNativeCommandHelp(command string, stdout io.Writer) bool {
+ if !clicore.IsRunnerOwnedCommandName(command) {
+ return false
+ }
+ printNativeCommandHelp(command, stdout)
+ return true
+}
+
+func printNativeCommandHelp(command string, stdout io.Writer) {
+ entry, _ := clicore.NativeCommand(command)
+ options := sortedNativeCommandOptions(runnerNativeCommandOptions[command])
+
+ clicore.WriteLine(stdout, "Usage:")
+ if len(options) > 0 {
+ clicore.WriteFormat(stdout, " uloop %s [options]\n", command)
+ clicore.WriteLine(stdout, "")
+ clicore.WriteLine(stdout, entry.Description)
+ clicore.WriteLine(stdout, "")
+ clicore.WriteLine(stdout, "Options:")
+ for _, option := range options {
+ clicore.WriteFormat(stdout, " %s\n", option)
+ }
+ } else {
+ clicore.WriteFormat(stdout, " uloop %s\n", command)
+ clicore.WriteLine(stdout, "")
+ clicore.WriteLine(stdout, entry.Description)
+ }
+
+ clicore.WriteLine(stdout, "")
+ clicore.WriteLine(stdout, "Global options:")
+ clicore.WriteFormat(stdout, " --%s Run against a Unity project outside the current directory\n", tooldocs.ProjectPathFlagName)
+}
+
+func sortedNativeCommandOptions(options []string) []string {
+ result := append([]string{}, options...)
+ sort.Strings(result)
+ return result
+}
diff --git a/cli/project-runner/internal/projectrunner/native_command_help_test.go b/cli/project-runner/internal/projectrunner/native_command_help_test.go
new file mode 100644
index 0000000000..9661c5508c
--- /dev/null
+++ b/cli/project-runner/internal/projectrunner/native_command_help_test.go
@@ -0,0 +1,76 @@
+package projectrunner
+
+import (
+ "bytes"
+ "context"
+ "strings"
+ "testing"
+)
+
+// Verifies await-pause-point --help lists every flag the runner accepts for
+// that command, since the dispatcher forwards this help request here instead
+// of keeping its own hardcoded options table.
+func TestRunProjectLocalAwaitPausePointHelpListsExpectedFlags(t *testing.T) {
+ t.Chdir(t.TempDir())
+
+ var stdout bytes.Buffer
+ var stderr bytes.Buffer
+ code := RunProjectLocal(context.Background(), []string{"await-pause-point", "--help"}, &stdout, &stderr)
+
+ if code != 0 {
+ t.Fatalf("await-pause-point --help failed: code=%d stderr=%s", code, stderr.String())
+ }
+ for _, flag := range []string{"--id", "--timeout-seconds", "--matching-logs-max-count", "--captured-variables"} {
+ if !strings.Contains(stdout.String(), flag) {
+ t.Fatalf("await-pause-point --help must list %s: %s", flag, stdout.String())
+ }
+ }
+ if !strings.Contains(stdout.String(), "--project-path") {
+ t.Fatalf("await-pause-point --help must list the global --project-path option: %s", stdout.String())
+ }
+}
+
+// Verifies pause-point-status --help lists the flags it accepts.
+func TestRunProjectLocalPausePointStatusHelpListsExpectedFlags(t *testing.T) {
+ t.Chdir(t.TempDir())
+
+ var stdout bytes.Buffer
+ var stderr bytes.Buffer
+ code := RunProjectLocal(context.Background(), []string{"pause-point-status", "--help"}, &stdout, &stderr)
+
+ if code != 0 {
+ t.Fatalf("pause-point-status --help failed: code=%d stderr=%s", code, stderr.String())
+ }
+ for _, flag := range []string{"--id", "--captured-variables"} {
+ if !strings.Contains(stdout.String(), flag) {
+ t.Fatalf("pause-point-status --help must list %s: %s", flag, stdout.String())
+ }
+ }
+ if strings.Contains(stdout.String(), "--timeout-seconds") {
+ t.Fatalf("pause-point-status --help must not list await-pause-point-only flags: %s", stdout.String())
+ }
+}
+
+// Verifies list/sync/focus-window --help print only the global --project-path
+// option, since these commands take no command-specific flags.
+func TestRunProjectLocalNoOptionCommandsHelpListsOnlyGlobalOption(t *testing.T) {
+ for _, command := range []string{"list", "sync", "focus-window"} {
+ t.Run(command, func(t *testing.T) {
+ t.Chdir(t.TempDir())
+
+ var stdout bytes.Buffer
+ var stderr bytes.Buffer
+ code := RunProjectLocal(context.Background(), []string{command, "--help"}, &stdout, &stderr)
+
+ if code != 0 {
+ t.Fatalf("%s --help failed: code=%d stderr=%s", command, code, stderr.String())
+ }
+ if !strings.Contains(stdout.String(), "--project-path") {
+ t.Fatalf("%s --help must list the global --project-path option: %s", command, stdout.String())
+ }
+ if strings.Contains(stdout.String(), "Options:") {
+ t.Fatalf("%s --help must not print a command-specific Options section: %s", command, stdout.String())
+ }
+ })
+ }
+}
diff --git a/cli/project-runner/internal/projectrunner/pause_point_captured_variables_mode.go b/cli/project-runner/internal/projectrunner/pause_point_captured_variables_mode.go
index 4ed695862f..3212cb0639 100644
--- a/cli/project-runner/internal/projectrunner/pause_point_captured_variables_mode.go
+++ b/cli/project-runner/internal/projectrunner/pause_point_captured_variables_mode.go
@@ -2,8 +2,6 @@ package projectrunner
import (
clierrors "github.com/hatayama/unity-cli-loop/common/errors"
-
- "github.com/hatayama/unity-cli-loop/common/clicore"
)
// pausePointCapturedVariablesMode controls how much of each captured variable's data the CLI
@@ -25,7 +23,7 @@ func parsePausePointCapturedVariablesMode(value string) (pausePointCapturedVaria
return pausePointCapturedVariablesModeNames, nil
default:
return "", clierrors.InvalidValueArgumentError(
- "--"+clicore.PausePointCapturedVariablesFlagName, value, "full or names")
+ "--"+PausePointCapturedVariablesFlagName, value, "full or names")
}
}
diff --git a/cli/project-runner/internal/projectrunner/pause_point_wait.go b/cli/project-runner/internal/projectrunner/pause_point_wait.go
index 619ac1c6e8..0e1fef7e04 100644
--- a/cli/project-runner/internal/projectrunner/pause_point_wait.go
+++ b/cli/project-runner/internal/projectrunner/pause_point_wait.go
@@ -240,23 +240,23 @@ func parseWaitForPausePointOptions(args []string) (waitForPausePointOptions, err
}
switch name {
- case clicore.PausePointIDFlagName:
+ case PausePointIDFlagName:
options.id = value
- case clicore.PausePointTimeoutFlagName:
+ case PausePointTimeoutFlagName:
timeoutSeconds, parseErr := parsePausePointTimeoutSeconds(value)
if parseErr != nil {
return waitForPausePointOptions{}, parseErr
}
options.timeoutSeconds = timeoutSeconds
options.timeout = time.Duration(timeoutSeconds) * time.Second
- case clicore.PausePointLogsMaxCountFlagName:
+ case PausePointLogsMaxCountFlagName:
maxCount, parseErr := strconv.Atoi(value)
if parseErr != nil || maxCount <= 0 {
return waitForPausePointOptions{}, clierrors.InvalidValueArgumentError(
- "--"+clicore.PausePointLogsMaxCountFlagName, value, "positive integer")
+ "--"+PausePointLogsMaxCountFlagName, value, "positive integer")
}
options.matchingLogsMaxCount = maxCount
- case clicore.PausePointCapturedVariablesFlagName:
+ case PausePointCapturedVariablesFlagName:
mode, parseErr := parsePausePointCapturedVariablesMode(value)
if parseErr != nil {
return waitForPausePointOptions{}, parseErr
@@ -279,7 +279,7 @@ func parseWaitForPausePointOptions(args []string) (waitForPausePointOptions, err
if options.id == "" {
return waitForPausePointOptions{}, &clierrors.ArgumentError{
Message: "Missing required option: --id",
- Option: "--" + clicore.PausePointIDFlagName,
+ Option: "--" + PausePointIDFlagName,
ExpectedType: "value",
Command: clicore.PausePointAwaitCommandName,
NextActions: []string{"Pass `--id ` matching UloopPausePoint.Pause(\"\")."},
@@ -300,9 +300,9 @@ func parsePausePointStatusOptions(args []string) (pausePointStatusOptions, error
}
switch name {
- case clicore.PausePointIDFlagName:
+ case PausePointIDFlagName:
options.id = value
- case clicore.PausePointCapturedVariablesFlagName:
+ case PausePointCapturedVariablesFlagName:
mode, parseErr := parsePausePointCapturedVariablesMode(value)
if parseErr != nil {
return pausePointStatusOptions{}, parseErr
@@ -325,7 +325,7 @@ func parsePausePointStatusOptions(args []string) (pausePointStatusOptions, error
if options.id == "" {
return pausePointStatusOptions{}, &clierrors.ArgumentError{
Message: "Missing required option: --id",
- Option: "--" + clicore.PausePointIDFlagName,
+ Option: "--" + PausePointIDFlagName,
ExpectedType: "value",
Command: clicore.PausePointStatusUserCommandName,
NextActions: []string{"Pass `--id ` matching UloopPausePoint.Pause(\"\")."},
@@ -338,7 +338,7 @@ func parsePausePointStatusOptions(args []string) (pausePointStatusOptions, error
func parsePausePointTimeoutSeconds(value string) (int, error) {
timeoutSeconds, err := strconv.Atoi(value)
if err != nil || timeoutSeconds <= 0 {
- return 0, clierrors.InvalidValueArgumentError("--"+clicore.PausePointTimeoutFlagName, value, "positive integer")
+ return 0, clierrors.InvalidValueArgumentError("--"+PausePointTimeoutFlagName, value, "positive integer")
}
return timeoutSeconds, nil
}
diff --git a/cli/project-runner/internal/projectrunner/run.go b/cli/project-runner/internal/projectrunner/run.go
index 88947a8588..245bd4f786 100644
--- a/cli/project-runner/internal/projectrunner/run.go
+++ b/cli/project-runner/internal/projectrunner/run.go
@@ -31,7 +31,7 @@ func RunProjectLocal(ctx context.Context, args []string, stdout io.Writer, stder
command := remainingArgs[0]
commandArgs := remainingArgs[1:]
- if clicore.IsDispatcherOwnedCommandName(command) || clicore.ShouldHandleCompletionRequest(remainingArgs) {
+ if clicore.IsDispatcherOwnedCommandName(command) {
clierrors.WriteErrorEnvelope(stderr, dispatcherOwnedCommandError(command))
return 1
}
@@ -44,6 +44,9 @@ func RunProjectLocal(ctx context.Context, args []string, stdout io.Writer, stder
return 1
}
if clicore.ContainsHelpRequest(commandArgs) {
+ if tryPrintNativeCommandHelp(command, stdout) {
+ return 0
+ }
printRunnerUsage(stdout)
return 0
}
diff --git a/cli/project-runner/internal/projectrunner/runner_usage_test.go b/cli/project-runner/internal/projectrunner/runner_usage_test.go
index 4466acbe86..238d60b490 100644
--- a/cli/project-runner/internal/projectrunner/runner_usage_test.go
+++ b/cli/project-runner/internal/projectrunner/runner_usage_test.go
@@ -19,8 +19,6 @@ func TestRunProjectLocalRejectsDispatcherOwnedCommands(t *testing.T) {
"uninstall",
"skills",
"completion",
- "--list-commands",
- "--list-options",
} {
var stdout bytes.Buffer
var stderr bytes.Buffer
diff --git a/cli/project-runner/shared-inputs-stamp.json b/cli/project-runner/shared-inputs-stamp.json
index b6826b745a..cbc2dd113d 100644
--- a/cli/project-runner/shared-inputs-stamp.json
+++ b/cli/project-runner/shared-inputs-stamp.json
@@ -1,4 +1,4 @@
{
"schemaVersion": 1,
- "sharedInputsHash": "b3533cd7138808911984abbee8c3fbc5c99d3765"
+ "sharedInputsHash": "5e974fe74190b43814743342f870ff27ded3b83c"
}