diff --git a/cli/common/clicore/error_aliases.go b/cli/common/clicore/error_aliases.go new file mode 100644 index 0000000000..110a46a3c8 --- /dev/null +++ b/cli/common/clicore/error_aliases.go @@ -0,0 +1,102 @@ +package clicore + +import ( + "io" + + clierrors "github.com/hatayama/unity-cli-loop/common/errors" + "github.com/hatayama/unity-cli-loop/common/unityipc" +) + +const ( + ErrorCodeInvalidArgument = clierrors.ErrorCodeInvalidArgument + ErrorCodeUnityNotReachable = clierrors.ErrorCodeUnityNotReachable + ErrorCodeUnityStartupTimeout = clierrors.ErrorCodeUnityStartupTimeout + ErrorCodeUnityProcessExitTimeout = clierrors.ErrorCodeUnityProcessExitTimeout + ErrorCodeCLIUpdateRequired = clierrors.ErrorCodeCLIUpdateRequired + ErrorCodeToolDisabled = clierrors.ErrorCodeToolDisabled + ErrorCodeCompileWaitTimeout = clierrors.ErrorCodeCompileWaitTimeout + ErrorCodeControlPlayModeWaitTimeout = clierrors.ErrorCodeControlPlayModeWaitTimeout + ErrorCodeControlPlayModeCompileErrors = clierrors.ErrorCodeControlPlayModeCompileErrors + ErrorCodePausePointNotEnabled = clierrors.ErrorCodePausePointNotEnabled + ErrorCodePausePointWaitTimeout = clierrors.ErrorCodePausePointWaitTimeout + ErrorCodePausePointExpired = clierrors.ErrorCodePausePointExpired + ErrorCodePausePointCleared = clierrors.ErrorCodePausePointCleared + ErrorCodeInternalError = clierrors.ErrorCodeInternalError + + ErrorPhaseArgumentParsing = clierrors.ErrorPhaseArgumentParsing + ErrorPhaseProjectResolve = clierrors.ErrorPhaseProjectResolve + ErrorPhaseDispatch = clierrors.ErrorPhaseDispatch + ErrorPhaseConnection = clierrors.ErrorPhaseConnection + ErrorPhaseResponseWaiting = clierrors.ErrorPhaseResponseWaiting + ErrorPhaseCompileWaiting = clierrors.ErrorPhaseCompileWaiting + ErrorPhaseExecution = clierrors.ErrorPhaseExecution +) + +type ( + CLIError = clierrors.CLIError + CLIErrorEnvelope = clierrors.CLIErrorEnvelope + ErrorContext = clierrors.ErrorContext + ArgumentError = clierrors.ArgumentError + UnityServerNotRespondingError = clierrors.UnityServerNotRespondingError +) + +func WriteErrorEnvelope(writer io.Writer, err CLIError) { + clierrors.WriteErrorEnvelope(writer, err) +} + +func WriteClassifiedError(writer io.Writer, err error, context ErrorContext) { + clierrors.WriteClassifiedError(writer, err, context) +} + +func WriteToolFailure(writer io.Writer, err error, outcome unityipc.UnitySendOutcome, context ErrorContext) { + clierrors.WriteToolFailure(writer, err, outcome, context) +} + +func ClassifyError(err error, context ErrorContext) CLIError { + return clierrors.ClassifyError(err, context) +} + +func MissingValueArgumentError(option string) *ArgumentError { + return clierrors.MissingValueArgumentError(option) +} + +func InvalidValueArgumentError(option string, received string, expectedType string) *ArgumentError { + return clierrors.InvalidValueArgumentError(option, received, expectedType) +} + +func InternalCLIError(message string, context ErrorContext) CLIError { + return clierrors.InternalCLIError(message, context) +} + +func IsTransportDisconnectError(err error) bool { + return clierrors.IsTransportDisconnectError(err) +} + +func IsFinalResponseTimeoutError(err error) bool { + return clierrors.IsFinalResponseTimeoutError(err) +} + +func RPCDataType(data map[string]any) string { + return clierrors.RPCDataType(data) +} + +func UnknownCommandError(command string, cache ToolsCache, context ErrorContext) CLIError { + return clierrors.UnknownCommandError(command, availableCommandNames(cache), context) +} + +func availableCommandNames(cache ToolsCache) []string { + seen := map[string]bool{} + names := []string{} + for _, name := range NativeCommandNamesForCompletion() { + seen[name] = true + names = append(names, name) + } + for _, tool := range cache.Tools { + if seen[tool.Name] { + continue + } + seen[tool.Name] = true + names = append(names, tool.Name) + } + return names +} diff --git a/cli/common/clicore/error_aliases_test.go b/cli/common/clicore/error_aliases_test.go new file mode 100644 index 0000000000..6c0c7a5865 --- /dev/null +++ b/cli/common/clicore/error_aliases_test.go @@ -0,0 +1,14 @@ +package clicore + +import "testing" + +func TestAvailableCommandNamesIncludesBuiltIns(t *testing.T) { + // Verifies the clicore compatibility wrapper still contributes built-in commands. + names := availableCommandNames(ToolsCache{}) + expectedBuiltIns := []string{"launch", "list", "sync", "focus-window", "wait-for-pause-point", "pause-point-status", "skills", "completion", "install", "update", "uninstall"} + for index, expected := range expectedBuiltIns { + if names[index] != expected { + t.Fatalf("built-in command mismatch: %#v", names) + } + } +} diff --git a/cli/common/clicore/server_not_responding.go b/cli/common/clicore/server_not_responding.go index e3b68d52ff..7baefd83e4 100644 --- a/cli/common/clicore/server_not_responding.go +++ b/cli/common/clicore/server_not_responding.go @@ -1,38 +1,6 @@ package clicore -import ( - "fmt" - - "github.com/hatayama/unity-cli-loop/common/project" -) - -// UnityServerNotRespondingError reports that a running Unity Editor accepted a -// connection but its Unity CLI Loop server did not respond in time. Both the -// dispatcher's launch-readiness wait and the runner's connection retry loop -// construct this error, and the shared CORE classifiers turn it into a CLIError. -type UnityServerNotRespondingError struct { - ProjectRoot string - Endpoint string - Cause error -} - -func (err UnityServerNotRespondingError) Error() string { - if err.Cause != nil { - return fmt.Sprintf("Unity is running but the Unity CLI Loop server is not responding: %s", err.Cause) - } - return "Unity is running but the Unity CLI Loop server is not responding" -} - -func (err UnityServerNotRespondingError) Unwrap() error { - return err.Cause -} - -func (err UnityServerNotRespondingError) causeText() string { - if err.Cause == nil { - return "" - } - return err.Cause.Error() -} +import "github.com/hatayama/unity-cli-loop/common/project" func resolveProjectEndpointAddress(projectRoot string) string { connection, err := project.ResolveConnection(projectRoot, projectRoot) diff --git a/cli/common/clicore/argument_error.go b/cli/common/errors/argument_error.go similarity index 95% rename from cli/common/clicore/argument_error.go rename to cli/common/errors/argument_error.go index dfddcd1b4a..1765516cb8 100644 --- a/cli/common/clicore/argument_error.go +++ b/cli/common/errors/argument_error.go @@ -1,4 +1,4 @@ -package clicore +package clierrors import "fmt" @@ -16,7 +16,7 @@ func (err *ArgumentError) Error() string { } func (err *ArgumentError) ToCLIError(context ErrorContext) CLIError { - command := FirstNonEmpty(err.Command, context.Command) + command := firstNonEmpty(err.Command, context.Command) details := map[string]any{} if err.Option != "" { details["Option"] = err.Option diff --git a/cli/common/clicore/busy_status.go b/cli/common/errors/busy_status.go similarity index 95% rename from cli/common/clicore/busy_status.go rename to cli/common/errors/busy_status.go index c127aaddac..041e0ce759 100644 --- a/cli/common/clicore/busy_status.go +++ b/cli/common/errors/busy_status.go @@ -1,4 +1,4 @@ -package clicore +package clierrors import ( "encoding/json" @@ -51,7 +51,7 @@ func serverBusyStatusDetailsFromError(err CLIError) serverBusyStatusDetails { isPaused, hasIsPaused := rpcBoolData(data, "isPaused") return serverBusyStatusDetails{ runningToolName: rpcStringData(data, "runningToolName"), - requestedToolName: FirstNonEmpty(rpcStringData(data, "requestedToolName"), err.Command), + requestedToolName: firstNonEmpty(rpcStringData(data, "requestedToolName"), err.Command), isPlaying: isPlaying, hasIsPlaying: hasIsPlaying, isPaused: isPaused, @@ -77,7 +77,7 @@ func optionalBool(hasValue bool, value bool) *bool { func unityServerBusyMessage(fallback string, data map[string]any, requestedCommand string) string { runningToolName := rpcStringData(data, "runningToolName") - requestedToolName := FirstNonEmpty(rpcStringData(data, "requestedToolName"), requestedCommand) + requestedToolName := firstNonEmpty(rpcStringData(data, "requestedToolName"), requestedCommand) if runningToolName == "" || requestedToolName == "" { return fallback } diff --git a/cli/common/clicore/error_editor_unresponsive.go b/cli/common/errors/error_editor_unresponsive.go similarity index 98% rename from cli/common/clicore/error_editor_unresponsive.go rename to cli/common/errors/error_editor_unresponsive.go index 9dadb6e0b3..e776534159 100644 --- a/cli/common/clicore/error_editor_unresponsive.go +++ b/cli/common/errors/error_editor_unresponsive.go @@ -1,4 +1,4 @@ -package clicore +package clierrors import "github.com/hatayama/unity-cli-loop/common/unityipc" diff --git a/cli/common/clicore/error_envelope.go b/cli/common/errors/error_envelope.go similarity index 94% rename from cli/common/clicore/error_envelope.go rename to cli/common/errors/error_envelope.go index 783e02a240..1e3ae3a85b 100644 --- a/cli/common/clicore/error_envelope.go +++ b/cli/common/errors/error_envelope.go @@ -1,4 +1,4 @@ -package clicore +package clierrors import ( "encoding/json" @@ -244,7 +244,7 @@ func unityServerNotRespondingAfterDispatchError(err UnityServerNotRespondingErro Message: "Unity is running for this project, but the Unity CLI Loop server did not acknowledge the dispatched request.", Retryable: true, SafeToRetry: false, - ProjectRoot: FirstNonEmpty(context.ProjectRoot, err.ProjectRoot), + ProjectRoot: firstNonEmpty(context.ProjectRoot, err.ProjectRoot), Command: context.Command, NextActions: []string{ "Check Unity Console logs and project state because Unity may have received the request.", @@ -258,7 +258,7 @@ func unityServerNotRespondingAfterDispatchError(err UnityServerNotRespondingErro } } -func UnknownCommandError(command string, cache ToolsCache, context ErrorContext) CLIError { +func UnknownCommandError(command string, availableCommands []string, context ErrorContext) CLIError { return CLIError{ ErrorCode: errorCodeUnknownCommand, Phase: ErrorPhaseDispatch, @@ -272,28 +272,11 @@ func UnknownCommandError(command string, cache ToolsCache, context ErrorContext) "Run `uloop sync` if the local tool cache may be stale.", }, Details: map[string]any{ - "AvailableCommands": availableCommandNames(cache), + "AvailableCommands": availableCommands, }, } } -func availableCommandNames(cache ToolsCache) []string { - seen := map[string]bool{} - names := []string{} - for _, name := range NativeCommandNamesForCompletion() { - seen[name] = true - names = append(names, name) - } - for _, tool := range cache.Tools { - if seen[tool.Name] { - continue - } - seen[tool.Name] = true - names = append(names, tool.Name) - } - return names -} - func isSafeRetryCommand(command string) bool { switch command { case "list", "sync", "get-version", "get-logs", "get-tool-details": diff --git a/cli/common/clicore/error_envelope_classification.go b/cli/common/errors/error_envelope_classification.go similarity index 97% rename from cli/common/clicore/error_envelope_classification.go rename to cli/common/errors/error_envelope_classification.go index f327de9219..d6e6e07a6f 100644 --- a/cli/common/clicore/error_envelope_classification.go +++ b/cli/common/errors/error_envelope_classification.go @@ -1,4 +1,4 @@ -package clicore +package clierrors import ( "encoding/json" @@ -70,7 +70,7 @@ func unityServerNotRespondingCLIError(err UnityServerNotRespondingError, context Message: "Unity is running for this project, but the Unity CLI Loop server is not responding.", Retryable: true, SafeToRetry: true, - ProjectRoot: FirstNonEmpty(context.ProjectRoot, err.ProjectRoot), + ProjectRoot: firstNonEmpty(context.ProjectRoot, err.ProjectRoot), Command: context.Command, NextActions: []string{ "Wait and retry; Unity may be starting, importing assets, compiling, or reloading scripts.", @@ -91,7 +91,7 @@ func connectionAttemptCLIError(err *unityipc.ConnectionAttemptError, context Err Message: "The Unity CLI Loop server is not reachable for this project.", Retryable: true, SafeToRetry: true, - ProjectRoot: FirstNonEmpty(context.ProjectRoot, err.ProjectRoot), + ProjectRoot: firstNonEmpty(context.ProjectRoot, err.ProjectRoot), Command: context.Command, NextActions: []string{ "If Unity is closed, run `uloop launch`.", diff --git a/cli/common/clicore/error_envelope_test.go b/cli/common/errors/error_envelope_test.go similarity index 97% rename from cli/common/clicore/error_envelope_test.go rename to cli/common/errors/error_envelope_test.go index 1591d2a09a..e5c946045f 100644 --- a/cli/common/clicore/error_envelope_test.go +++ b/cli/common/errors/error_envelope_test.go @@ -1,4 +1,4 @@ -package clicore +package clierrors import ( "bytes" @@ -417,7 +417,7 @@ func TestWriteToolFailureClassifiesAcceptedResponseTimeout(t *testing.T) { func TestUnknownCommandErrorIncludesAvailableCommands(t *testing.T) { cliErr := UnknownCommandError( "missing", - ToolsCache{Tools: []ToolDefinition{{Name: "compile"}}}, + []string{"launch", "compile"}, ErrorContext{ProjectRoot: "/tmp/MyProject"}, ) @@ -470,16 +470,6 @@ func TestClassifyConnectionAttemptUsesContextProjectRootFallback(t *testing.T) { } } -func TestAvailableCommandNamesIncludesBuiltIns(t *testing.T) { - names := availableCommandNames(ToolsCache{}) - expectedBuiltIns := []string{"launch", "list", "sync", "focus-window", "wait-for-pause-point", "pause-point-status", "skills", "completion", "install", "update"} - for index, expected := range expectedBuiltIns { - if names[index] != expected { - t.Fatalf("built-in command mismatch: %#v", names) - } - } -} - func TestSafeRetryCommand(t *testing.T) { if !isSafeRetryCommand("get-logs") { t.Fatal("get-logs should be safe to retry") diff --git a/cli/common/errors/server_not_responding.go b/cli/common/errors/server_not_responding.go new file mode 100644 index 0000000000..ac6fe58201 --- /dev/null +++ b/cli/common/errors/server_not_responding.go @@ -0,0 +1,33 @@ +package clierrors + +import ( + "fmt" +) + +// UnityServerNotRespondingError reports that a running Unity Editor accepted a +// connection but its Unity CLI Loop server did not respond in time. Both the +// dispatcher's launch-readiness wait and the runner's connection retry loop +// construct this error, and the shared CORE classifiers turn it into a CLIError. +type UnityServerNotRespondingError struct { + ProjectRoot string + Endpoint string + Cause error +} + +func (err UnityServerNotRespondingError) Error() string { + if err.Cause != nil { + return fmt.Sprintf("Unity is running but the Unity CLI Loop server is not responding: %s", err.Cause) + } + return "Unity is running but the Unity CLI Loop server is not responding" +} + +func (err UnityServerNotRespondingError) Unwrap() error { + return err.Cause +} + +func (err UnityServerNotRespondingError) causeText() string { + if err.Cause == nil { + return "" + } + return err.Cause.Error() +} diff --git a/cli/common/errors/string_helpers.go b/cli/common/errors/string_helpers.go new file mode 100644 index 0000000000..29ce2b629a --- /dev/null +++ b/cli/common/errors/string_helpers.go @@ -0,0 +1,10 @@ +package clierrors + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if value != "" { + return value + } + } + return "" +} diff --git a/cli/common/clicore/transport_errors.go b/cli/common/errors/transport_errors.go similarity index 98% rename from cli/common/clicore/transport_errors.go rename to cli/common/errors/transport_errors.go index 35e6f21384..099b1c3ea1 100644 --- a/cli/common/clicore/transport_errors.go +++ b/cli/common/errors/transport_errors.go @@ -1,4 +1,4 @@ -package clicore +package clierrors import ( "errors" diff --git a/cli/common/clicore/transport_errors_test.go b/cli/common/errors/transport_errors_test.go similarity index 99% rename from cli/common/clicore/transport_errors_test.go rename to cli/common/errors/transport_errors_test.go index 53fd075488..35815c95eb 100644 --- a/cli/common/clicore/transport_errors_test.go +++ b/cli/common/errors/transport_errors_test.go @@ -1,4 +1,4 @@ -package clicore +package clierrors import ( "fmt" diff --git a/cli/dispatcher/shared-inputs-stamp.json b/cli/dispatcher/shared-inputs-stamp.json index 3b8ee2e91e..6767c332f2 100644 --- a/cli/dispatcher/shared-inputs-stamp.json +++ b/cli/dispatcher/shared-inputs-stamp.json @@ -1,4 +1,4 @@ { "schemaVersion": 1, - "sharedInputsHash": "b58452f9c0f12c73da40ccdda7b87ee77e64b99a" + "sharedInputsHash": "4ffd3b6d8156471cab11096b0f52f3696937bdba" } diff --git a/cli/project-runner/shared-inputs-stamp.json b/cli/project-runner/shared-inputs-stamp.json index 3ed0cde964..2f71facd18 100644 --- a/cli/project-runner/shared-inputs-stamp.json +++ b/cli/project-runner/shared-inputs-stamp.json @@ -1,4 +1,4 @@ { "schemaVersion": 1, - "sharedInputsHash": "bdaecfb852b88de8634ae849eddd5e1b0c0f064e" + "sharedInputsHash": "552a3bbd8757b2357b7fca535453fdf2e416577c" } diff --git a/cli/release-automation/internal/automation/release_trigger_guard.go b/cli/release-automation/internal/automation/release_trigger_guard.go index 3d701cd1d0..163f9a6f37 100644 --- a/cli/release-automation/internal/automation/release_trigger_guard.go +++ b/cli/release-automation/internal/automation/release_trigger_guard.go @@ -152,6 +152,7 @@ var sharedCommonPackageRoots = []string{ // intentionally excluded because they never ship in release binaries. "cli/common/clicontract/", "cli/common/clicore/", + "cli/common/errors/", "cli/common/project/", "cli/common/skills/", "cli/common/tools/", diff --git a/scripts/stamp-release-inputs.sh b/scripts/stamp-release-inputs.sh index c97f3224dd..a466097a25 100755 --- a/scripts/stamp-release-inputs.sh +++ b/scripts/stamp-release-inputs.sh @@ -19,6 +19,7 @@ list_shared_common_inputs() { cli/common/go.sum \ 'cli/common/clicontract/' \ 'cli/common/clicore/' \ + 'cli/common/errors/' \ 'cli/common/project/' \ 'cli/common/skills/' \ 'cli/common/tools/' \