@@ -8,10 +8,12 @@ import (
88 "io"
99 "net/url"
1010 "os"
11+ "path/filepath"
1112 "runtime"
1213 "strings"
1314
1415 "github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName | caseDash }}/internal/app"
16+ "github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName | caseDash }}/internal/config"
1517 "github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName | caseDash }}/internal/output"
1618 "github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName | caseDash }}/internal/prompt"
1719 "github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName | caseDash }}/internal/sdk"
@@ -43,6 +45,64 @@ type apiError interface {
4345// rendered error page.
4446const maximumMessageLength = 400
4547
48+ // actionableError carries a known recovery path without baking terminal layout
49+ // into Error(). Logs and reports retain the ordinary message; Report can render
50+ // the title, facts and command as a readable block.
51+ type actionableError struct {
52+ cause error
53+ title string
54+ details []output.FailureDetail
55+ action string
56+ command string
57+ }
58+
59+ func (e * actionableError ) Error () string { return e .cause .Error () }
60+ func (e * actionableError ) Unwrap () error { return e .cause }
61+
62+ func endpointMismatchError (projectEndpoint , sessionEndpoint string ) error {
63+ switchEndpoint := config .NormalizeCloudConsoleEndpoint (projectEndpoint )
64+ environment := "the project environment"
65+ if base , cloud := config .CloudBaseHost (projectEndpoint ); cloud {
66+ environment = "Appwrite Cloud"
67+ if strings .Contains (base , "staging" ) {
68+ environment = "Appwrite Cloud Staging"
69+ }
70+ }
71+
72+ cause := fmt .Errorf ("project endpoint %s does not match active session endpoint %s" ,
73+ projectEndpoint , sessionEndpoint )
74+ return & actionableError {
75+ cause : cause ,
76+ title : "Active session doesn’t match this project" ,
77+ details : []output.FailureDetail {
78+ {Label : "Project endpoint" , Value : projectEndpoint },
79+ {Label : "Active session" , Value : sessionEndpoint },
80+ },
81+ action : "Switch to " + environment + ":" ,
82+ command : fmt .Sprintf ("%s login --switch --endpoint %s" ,
83+ app .ExecutableName , switchEndpoint ),
84+ }
85+ }
86+
87+ func missingProjectConfigError (err error ) * actionableError {
88+ var pathError * os.PathError
89+ if ! errors .As (err , & pathError ) || ! errors .Is (err , os .ErrNotExist ) ||
90+ filepath .Base (pathError .Path ) != config .LocalFileName {
91+ return nil
92+ }
93+
94+ return & actionableError {
95+ cause : err ,
96+ title : "Appwrite project configuration not found" ,
97+ details : []output.FailureDetail {
98+ {Label : "Expected file" , Value : pathError .Path },
99+ },
100+ action : "Run this command from a directory containing " + config .LocalFileName +
101+ ", or initialize a project:" ,
102+ command : app .ExecutableName + " init project" ,
103+ }
104+ }
105+
46106// FormatError renders a command failure for the terminal.
47107func FormatError (err error ) string {
48108 if err == nil {
@@ -196,7 +256,16 @@ func Report(writer io.Writer, executed *cobra.Command, err error) int {
196256 output .Log (writer , "For detailed error pass the --verbose or --report flag" )
197257 }
198258
199- output .Failure (writer , "%s" , FormatError (err ))
259+ var actionable * actionableError
260+ if errors .As (err , & actionable ) {
261+ output .ActionableFailure (writer , actionable .title , actionable .details ,
262+ actionable .action , actionable .command )
263+ } else if missing := missingProjectConfigError (err ); missing != nil {
264+ output .ActionableFailure (writer , missing .title , missing .details ,
265+ missing .action , missing .command )
266+ } else {
267+ output .Failure (writer , "%s" , FormatError (err ))
268+ }
200269
201270 // --verbose has to add the response body: on the failure it matters most for
202271 // -- a response the SDK could not decode -- a message naming a field and a
0 commit comments