@@ -10,14 +10,15 @@ import (
1010
1111 "github.com/NodeOps-app/createos-cli/internal/config"
1212 internaloauth "github.com/NodeOps-app/createos-cli/internal/oauth"
13+ "github.com/NodeOps-app/createos-cli/internal/terminal"
1314)
1415
1516const (
1617 oauthCallbackPort = 65341
1718 oauthCallbackURI = "http://localhost:65341/callback"
1819)
1920
20- // NewLoginCommand creates the login command
21+ // NewLoginCommand creates the login command.
2122func NewLoginCommand () * cli.Command {
2223 return & cli.Command {
2324 Name : "login" ,
@@ -30,7 +31,7 @@ func NewLoginCommand() *cli.Command {
3031 },
3132 },
3233 Action : func (c * cli.Context ) error {
33- // --token flag: API key flow
34+ // --token flag: API key flow (works in both TTY and non-TTY)
3435 if token := c .String ("token" ); token != "" {
3536 if err := config .SaveToken (token ); err != nil {
3637 return fmt .Errorf ("could not save your token: %w" , err )
@@ -39,6 +40,11 @@ func NewLoginCommand() *cli.Command {
3940 return nil
4041 }
4142
43+ // Non-interactive (CI/script): require --token flag
44+ if ! terminal .IsInteractive () {
45+ return fmt .Errorf ("non-interactive mode: use --token flag to sign in\n \n Example:\n createos login --token <your-api-token>" )
46+ }
47+
4248 // Interactive: let user choose auth method
4349 options := []string {
4450 "Sign in with browser (recommended)" ,
@@ -79,26 +85,22 @@ func loginWithBrowser() error {
7985 port := oauthCallbackPort
8086 redirectURI := oauthCallbackURI
8187
82- // 2. Fetch OAuth server metadata
8388 pterm .Info .Println ("Fetching authorization server info..." )
8489 meta , err := internaloauth .FetchServerMetadata (config .OAuthIssuerURL )
8590 if err != nil {
8691 return fmt .Errorf ("could not reach authorization server: %w" , err )
8792 }
8893
89- // 3. Generate PKCE pair
9094 pkce , err := internaloauth .GeneratePKCE ()
9195 if err != nil {
9296 return fmt .Errorf ("could not generate security parameters: %w" , err )
9397 }
9498
95- // 4. Generate state for CSRF protection
9699 state , err := internaloauth .GenerateState ()
97100 if err != nil {
98101 return fmt .Errorf ("could not generate state: %w" , err )
99102 }
100103
101- // 5. Build authorization URL
102104 authURL := internaloauth .BuildAuthURL (
103105 meta .AuthorizationEndpoint ,
104106 config .OAuthClientID ,
@@ -107,31 +109,26 @@ func loginWithBrowser() error {
107109 pkce .Challenge ,
108110 )
109111
110- // 6. Print fallback URL before opening browser
111112 fmt .Println ()
112113 pterm .Println (pterm .Gray (" If your browser doesn't open, visit this URL:" ))
113114 pterm .Println (pterm .Gray (" " + authURL ))
114115 fmt .Println ()
115116
116- // 7. Open browser
117117 if err := internaloauth .OpenBrowser (authURL ); err != nil {
118118 pterm .Warning .Println ("Could not open browser automatically. Please open the URL above." )
119119 } else {
120120 pterm .Info .Println ("Waiting for you to complete login in your browser..." )
121121 }
122122
123- // 8. Wait for callback
124123 code , returnedState , err := internaloauth .StartCallbackServer (port )
125124 if err != nil {
126125 return fmt .Errorf ("login was not completed: %w" , err )
127126 }
128127
129- // 9. Verify state
130128 if returnedState != state {
131129 return fmt .Errorf ("invalid state parameter — possible CSRF attack, login aborted" )
132130 }
133131
134- // 10. Exchange code for tokens
135132 pterm .Info .Println ("Completing sign in..." )
136133 tokenResp , err := internaloauth .ExchangeCode (
137134 meta .TokenEndpoint ,
@@ -144,10 +141,9 @@ func loginWithBrowser() error {
144141 return fmt .Errorf ("could not complete sign in: %w" , err )
145142 }
146143
147- // 11. Save OAuth session
148144 expiresAt := time .Now ().Unix () + int64 (tokenResp .ExpiresIn )
149145 if tokenResp .ExpiresIn <= 0 {
150- expiresAt = time .Now ().Unix () + 3600 // default 1 hour
146+ expiresAt = time .Now ().Unix () + 3600
151147 }
152148 session := config.OAuthSession {
153149 AccessToken : tokenResp .AccessToken ,
0 commit comments