From 1b69722ac1699d6b0a285fb375cb9106ed391c6a Mon Sep 17 00:00:00 2001 From: Masamichi Hatayama Date: Mon, 20 Jul 2026 11:30:08 +0900 Subject: [PATCH 1/2] feat: project runner commands can show their own detailed --help (#1860) --- .../projectrunner/native_command_help.go | 70 +++++++++++++++++ .../projectrunner/native_command_help_test.go | 76 +++++++++++++++++++ .../internal/projectrunner/run.go | 3 + 3 files changed, 149 insertions(+) create mode 100644 cli/project-runner/internal/projectrunner/native_command_help.go create mode 100644 cli/project-runner/internal/projectrunner/native_command_help_test.go 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..9a554b1df2 --- /dev/null +++ b/cli/project-runner/internal/projectrunner/native_command_help.go @@ -0,0 +1,70 @@ +package projectrunner + +import ( + "io" + "sort" + + "github.com/hatayama/unity-cli-loop/common/clicore" + "github.com/hatayama/unity-cli-loop/common/tooldocs" +) + +// 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: { + "--" + clicore.PausePointIDFlagName, + "--" + clicore.PausePointTimeoutFlagName, + "--" + clicore.PausePointLogsMaxCountFlagName, + "--" + clicore.PausePointCapturedVariablesFlagName, + }, + clicore.PausePointStatusUserCommandName: { + "--" + clicore.PausePointIDFlagName, + "--" + clicore.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/run.go b/cli/project-runner/internal/projectrunner/run.go index 88947a8588..d27e9ed0a1 100644 --- a/cli/project-runner/internal/projectrunner/run.go +++ b/cli/project-runner/internal/projectrunner/run.go @@ -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 } From 3d73e558428dde1d2f388aae8f58d091f2999a91 Mon Sep 17 00:00:00 2001 From: Masamichi Hatayama Date: Mon, 20 Jul 2026 12:04:30 +0900 Subject: [PATCH 2/2] fix: Runner-owned command flags no longer force a dispatcher release (#1861) --- README.md | 21 +- README_ja.md | 21 +- cli/common/clicore/command_registry.go | 30 +- cli/common/clicore/global_args.go | 16 - .../internal/dispatcher/command_help.go | 8 +- .../internal/dispatcher/completion.go | 491 +------------- .../internal/dispatcher/completion_options.go | 33 - .../internal/dispatcher/completion_test.go | 611 +----------------- .../dispatcher_bootstrap_routing_test.go | 25 +- .../internal/dispatcher/dispatcher_process.go | 7 +- .../internal/dispatcher/dispatcher_test.go | 30 + .../internal/dispatcher/help_test.go | 78 +-- .../dispatcher/native_command_options.go | 25 + .../internal/dispatcher/run_dispatcher.go | 15 +- .../internal/dispatcher/run_help.go | 15 +- cli/dispatcher/shared-inputs-stamp.json | 2 +- .../projectrunner/native_command_help.go | 19 +- .../pause_point_captured_variables_mode.go | 4 +- .../projectrunner/pause_point_wait.go | 20 +- .../internal/projectrunner/run.go | 2 +- .../projectrunner/runner_usage_test.go | 2 - cli/project-runner/shared-inputs-stamp.json | 2 +- 22 files changed, 187 insertions(+), 1290 deletions(-) delete mode 100644 cli/dispatcher/internal/dispatcher/completion_options.go create mode 100644 cli/dispatcher/internal/dispatcher/native_command_options.go 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 index 9a554b1df2..ccdf186527 100644 --- a/cli/project-runner/internal/projectrunner/native_command_help.go +++ b/cli/project-runner/internal/projectrunner/native_command_help.go @@ -8,20 +8,27 @@ import ( "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: { - "--" + clicore.PausePointIDFlagName, - "--" + clicore.PausePointTimeoutFlagName, - "--" + clicore.PausePointLogsMaxCountFlagName, - "--" + clicore.PausePointCapturedVariablesFlagName, + "--" + PausePointIDFlagName, + "--" + PausePointTimeoutFlagName, + "--" + PausePointLogsMaxCountFlagName, + "--" + PausePointCapturedVariablesFlagName, }, clicore.PausePointStatusUserCommandName: { - "--" + clicore.PausePointIDFlagName, - "--" + clicore.PausePointCapturedVariablesFlagName, + "--" + PausePointIDFlagName, + "--" + PausePointCapturedVariablesFlagName, }, } 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 d27e9ed0a1..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 } 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" }