Skip to content

Commit 4d35964

Browse files
fix: added missing pkg
1 parent 82fd1c2 commit 4d35964

5 files changed

Lines changed: 53 additions & 33 deletions

File tree

cmd/auth/login.go

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1516
const (
1617
oauthCallbackPort = 65341
1718
oauthCallbackURI = "http://localhost:65341/callback"
1819
)
1920

20-
// NewLoginCommand creates the login command
21+
// NewLoginCommand creates the login command.
2122
func 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,

cmd/projects/delete.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/urfave/cli/v2"
99

1010
"github.com/NodeOps-app/createos-cli/internal/api"
11+
"github.com/NodeOps-app/createos-cli/internal/terminal"
1112
)
1213

1314
func newDeleteCommand() *cli.Command {
@@ -18,6 +19,12 @@ func newDeleteCommand() *cli.Command {
1819
Description: "Permanently deletes a project. This action cannot be undone.\n\n" +
1920
" To find your project ID, run:\n" +
2021
" createos projects list",
22+
Flags: []cli.Flag{
23+
&cli.BoolFlag{
24+
Name: "force",
25+
Usage: "Skip confirmation prompt (required in non-interactive mode)",
26+
},
27+
},
2128
Action: func(c *cli.Context) error {
2229
if c.NArg() == 0 {
2330
return fmt.Errorf("please provide a project ID\n\n To see your projects and their IDs, run:\n createos projects list")
@@ -30,17 +37,22 @@ func newDeleteCommand() *cli.Command {
3037

3138
id := c.Args().First()
3239

33-
confirm, err := pterm.DefaultInteractiveConfirm.
34-
WithDefaultText(fmt.Sprintf("Are you sure you want to permanently delete project %q? This cannot be undone", id)).
35-
WithDefaultValue(false).
36-
Show()
37-
if err != nil {
38-
return fmt.Errorf("could not read confirmation: %w", err)
40+
if !terminal.IsInteractive() && !c.Bool("force") {
41+
return fmt.Errorf("non-interactive mode: use --force flag to confirm deletion\n\n Example:\n createos projects delete %s --force", id)
3942
}
4043

41-
if !confirm {
42-
fmt.Println("Cancelled. Your project was not deleted.")
43-
return nil
44+
if terminal.IsInteractive() && !c.Bool("force") {
45+
confirm, err := pterm.DefaultInteractiveConfirm.
46+
WithDefaultText(fmt.Sprintf("Are you sure you want to permanently delete project %q? This cannot be undone", id)).
47+
WithDefaultValue(false).
48+
Show()
49+
if err != nil {
50+
return fmt.Errorf("could not read confirmation: %w", err)
51+
}
52+
if !confirm {
53+
fmt.Println("Cancelled. Your project was not deleted.")
54+
return nil
55+
}
4456
}
4557

4658
if err := client.DeleteProject(id); err != nil {

go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
module github.com/NodeOps-app/createos-cli
22

3-
go 1.24.0
3+
go 1.25.0
44

55
require (
6+
github.com/charmbracelet/bubbletea v1.3.10
7+
github.com/charmbracelet/lipgloss v1.1.0
68
github.com/go-resty/resty/v2 v2.17.2
79
github.com/pterm/pterm v0.12.82
810
github.com/urfave/cli/v2 v2.27.7
11+
golang.org/x/term v0.41.0
912
)
1013

1114
require (
1215
atomicgo.dev/cursor v0.2.0 // indirect
1316
atomicgo.dev/keyboard v0.2.9 // indirect
1417
atomicgo.dev/schedule v0.1.0 // indirect
1518
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
16-
github.com/charmbracelet/bubbletea v1.3.10 // indirect
1719
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
18-
github.com/charmbracelet/lipgloss v1.1.0 // indirect
1920
github.com/charmbracelet/x/ansi v0.10.1 // indirect
2021
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
2122
github.com/charmbracelet/x/term v0.2.1 // indirect
@@ -36,7 +37,6 @@ require (
3637
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
3738
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
3839
golang.org/x/net v0.43.0 // indirect
39-
golang.org/x/sys v0.36.0 // indirect
40-
golang.org/x/term v0.34.0 // indirect
40+
golang.org/x/sys v0.42.0 // indirect
4141
golang.org/x/text v0.28.0 // indirect
4242
)

go.sum

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,17 +131,15 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
131131
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
132132
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
133133
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
134-
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
135-
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
136-
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
137-
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
134+
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
135+
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
138136
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
139137
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
140138
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
141139
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
142140
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
143-
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
144-
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
141+
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
142+
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
145143
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
146144
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
147145
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

internal/terminal/tty.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Package terminal provides helpers for detecting the terminal environment.
2+
package terminal
3+
4+
import (
5+
"os"
6+
7+
"golang.org/x/term"
8+
)
9+
10+
// IsInteractive returns true when stdout is a real TTY (i.e. a human is
11+
// watching). Returns false in CI pipelines, scripts, or when output is piped.
12+
func IsInteractive() bool {
13+
return term.IsTerminal(int(os.Stdout.Fd()))
14+
}

0 commit comments

Comments
 (0)