Skip to content

Commit 1ccf879

Browse files
Profiles in CLI
1 parent 5248d10 commit 1ccf879

6 files changed

Lines changed: 543 additions & 42 deletions

File tree

README.md

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,32 +26,50 @@ sudo mv cipi-cli-* /usr/local/bin/cipi-cli
2626

2727
### 1. Configure
2828

29-
Set up the connection to your Cipi API server:
29+
Set up the connection to your Cipi API server. Each profile maps to one server:
3030

3131
```bash
32-
cipi-cli configure
32+
cipi-cli configure --profile prod
33+
cipi-cli configure --profile staging
3334
```
3435

3536
You will be prompted for:
3637

3738
- **API endpoint** — the URL of your Cipi API (e.g. `https://api.example.com`)
3839
- **Token** — a Sanctum token created with `cipi api token create` on your server
3940

40-
Credentials are stored in `~/.cipi/config.json` (permissions `0600`).
41+
Credentials are stored per profile in `~/.cipi/config.json` (permissions `0600`).
4142

4243
You can also pass values directly:
4344

4445
```bash
45-
cipi-cli configure --endpoint https://api.example.com --token "1|yourtoken..."
46+
cipi-cli configure --profile prod --endpoint https://api.example.com --token "1|yourtoken..."
47+
```
48+
49+
Manage profiles:
50+
51+
```bash
52+
cipi-cli profiles
53+
cipi-cli profiles list
54+
cipi-cli profiles default prod
55+
cipi-cli profiles show prod
4656
```
4757

4858
### 2. Use
4959

60+
Prefix commands with a profile name to target that server:
61+
62+
```bash
63+
cipi-cli prod apps list
64+
cipi-cli staging apps show myapp
65+
cipi-cli prod deploy myapp
66+
cipi-cli prod ssl install myapp
67+
```
68+
69+
If you set a default profile (`cipi-cli configure default prod`), you can omit the prefix:
70+
5071
```bash
5172
cipi-cli apps list
52-
cipi-cli apps show myapp
53-
cipi-cli deploy myapp
54-
cipi-cli ssl install myapp
5573
```
5674

5775
## Commands
@@ -115,8 +133,17 @@ cipi-cli jobs wait <id> Wait for a job to complete
115133
### Configuration
116134

117135
```
118-
cipi-cli configure Set up API endpoint and token
119-
cipi-cli configure show Show current configuration
136+
cipi-cli configure [--profile NAME] Set up API endpoint and token for a profile
137+
```
138+
139+
### Profiles
140+
141+
```
142+
cipi-cli profiles List configured profiles
143+
cipi-cli profiles list List configured profiles
144+
cipi-cli profiles show [profile] Show one or all profiles
145+
cipi-cli profiles default <profile> Set the default profile
146+
cipi-cli profiles delete <profile> [-y] Delete a profile
120147
```
121148

122149
### Update

cmd/configure.go

Lines changed: 182 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,23 @@ import (
1212
var configureCmd = &cobra.Command{
1313
Use: "configure",
1414
Short: "Configure API endpoint and authentication token",
15-
Long: "Set up the connection to your Cipi server API. Credentials are stored in ~/.cipi/config.json",
15+
Long: "Set up the connection to your Cipi server API. Credentials are stored per profile in ~/.cipi/config.json",
1616
RunE: func(cmd *cobra.Command, args []string) error {
17+
profile, _ := cmd.Flags().GetString("profile")
1718
endpoint, _ := cmd.Flags().GetString("endpoint")
1819
token, _ := cmd.Flags().GetString("token")
1920

21+
if profile == "" {
22+
profile = "default"
23+
}
24+
2025
fmt.Println()
2126

27+
if err := config.ValidateProfileName(profile); err != nil {
28+
output.Error("%s", err)
29+
return err
30+
}
31+
2232
if endpoint == "" {
2333
endpoint = output.ReadInput("Cipi API endpoint (e.g. https://api.example.com)")
2434
}
@@ -37,48 +47,195 @@ var configureCmd = &cobra.Command{
3747
endpoint = "https://" + endpoint
3848
}
3949

40-
cfg := &config.Config{
50+
cfg := &config.Profile{
4151
Endpoint: endpoint,
4252
Token: token,
4353
}
4454

45-
if err := config.Save(cfg); err != nil {
55+
if err := config.SaveProfile(profile, cfg); err != nil {
4656
output.Error("Failed to save configuration: %s", err)
4757
return err
4858
}
4959

50-
output.Success("Configuration saved to %s", config.Path())
60+
output.Success("Profile %q saved to %s", profile, config.Path())
5161
fmt.Println()
5262
return nil
5363
},
5464
}
5565

5666
var configureShowCmd = &cobra.Command{
57-
Use: "show",
58-
Short: "Show current configuration",
67+
Use: "show [profile]",
68+
Short: "Show configuration for one or all profiles",
69+
Args: cobra.MaximumNArgs(1),
5970
RunE: func(cmd *cobra.Command, args []string) error {
60-
cfg, err := config.Load()
61-
if err != nil {
62-
output.Error("%s", err)
63-
return err
71+
if len(args) == 1 {
72+
return showProfile(args[0])
73+
}
74+
return showAllProfiles()
75+
},
76+
}
77+
78+
var configureListCmd = &cobra.Command{
79+
Use: "list",
80+
Short: "List configured profiles",
81+
RunE: func(cmd *cobra.Command, args []string) error {
82+
return listProfiles()
83+
},
84+
}
85+
86+
var configureDeleteCmd = &cobra.Command{
87+
Use: "delete <profile>",
88+
Short: "Delete a profile",
89+
Args: cobra.ExactArgs(1),
90+
RunE: func(cmd *cobra.Command, args []string) error {
91+
yes, _ := cmd.Flags().GetBool("yes")
92+
return deleteProfile(args[0], yes)
93+
},
94+
}
95+
96+
var configureDefaultCmd = &cobra.Command{
97+
Use: "default <profile>",
98+
Short: "Set the default profile for commands without a profile prefix",
99+
Args: cobra.ExactArgs(1),
100+
RunE: func(cmd *cobra.Command, args []string) error {
101+
return setDefaultProfile(args[0])
102+
},
103+
}
104+
105+
func listProfiles() error {
106+
names, defaultProfile, err := config.ListProfiles()
107+
if err != nil {
108+
output.Error("%s", err)
109+
return err
110+
}
111+
112+
if jsonFlag {
113+
output.PrintJSON(map[string]interface{}{
114+
"profiles": names,
115+
"default": defaultProfile,
116+
"path": config.Path(),
117+
})
118+
return nil
119+
}
120+
121+
output.Header("Profiles")
122+
output.KeyValue(nil, "Config file", config.Path())
123+
if defaultProfile != "" {
124+
output.KeyValue(nil, "Default", defaultProfile)
125+
}
126+
fmt.Println()
127+
if len(names) == 0 {
128+
output.Warn("No profiles configured")
129+
return nil
130+
}
131+
for _, name := range names {
132+
marker := ""
133+
if name == defaultProfile {
134+
marker = " (default)"
64135
}
136+
fmt.Printf(" %s%s\n", name, marker)
137+
}
138+
fmt.Println()
139+
return nil
140+
}
65141

66-
if jsonFlag {
67-
output.PrintJSON(map[string]string{
68-
"endpoint": cfg.Endpoint,
69-
"token": maskToken(cfg.Token),
70-
"path": config.Path(),
71-
})
142+
func deleteProfile(name string, yes bool) error {
143+
if !yes {
144+
if !output.Confirm(fmt.Sprintf("Delete profile %q?", name)) {
145+
output.Warn("Cancelled")
72146
return nil
73147
}
148+
}
74149

75-
output.Header("Configuration")
76-
output.KeyValue(nil, "Config file", config.Path())
77-
output.KeyValue(nil, "Endpoint", cfg.Endpoint)
78-
output.KeyValue(nil, "Token", maskToken(cfg.Token))
79-
fmt.Println()
150+
if err := config.DeleteProfile(name); err != nil {
151+
output.Error("%s", err)
152+
return err
153+
}
154+
155+
output.Success("Profile %q deleted", name)
156+
return nil
157+
}
158+
159+
func setDefaultProfile(name string) error {
160+
if err := config.SetDefaultProfile(name); err != nil {
161+
output.Error("%s", err)
162+
return err
163+
}
164+
output.Success("Default profile set to %q", name)
165+
return nil
166+
}
167+
168+
func showProfile(name string) error {
169+
profile, err := config.GetProfile(name)
170+
if err != nil {
171+
output.Error("%s", err)
172+
return err
173+
}
174+
175+
if jsonFlag {
176+
output.PrintJSON(map[string]string{
177+
"profile": name,
178+
"endpoint": profile.Endpoint,
179+
"token": maskToken(profile.Token),
180+
"path": config.Path(),
181+
})
80182
return nil
81-
},
183+
}
184+
185+
output.Header("Configuration")
186+
output.KeyValue(nil, "Profile", name)
187+
output.KeyValue(nil, "Config file", config.Path())
188+
output.KeyValue(nil, "Endpoint", profile.Endpoint)
189+
output.KeyValue(nil, "Token", maskToken(profile.Token))
190+
fmt.Println()
191+
return nil
192+
}
193+
194+
func showAllProfiles() error {
195+
names, defaultProfile, err := config.ListProfiles()
196+
if err != nil {
197+
output.Error("%s", err)
198+
return err
199+
}
200+
201+
if jsonFlag {
202+
profiles := make(map[string]map[string]string, len(names))
203+
for _, name := range names {
204+
profile, err := config.GetProfile(name)
205+
if err != nil {
206+
return err
207+
}
208+
profiles[name] = map[string]string{
209+
"endpoint": profile.Endpoint,
210+
"token": maskToken(profile.Token),
211+
}
212+
}
213+
output.PrintJSON(map[string]interface{}{
214+
"profiles": profiles,
215+
"default": defaultProfile,
216+
"path": config.Path(),
217+
})
218+
return nil
219+
}
220+
221+
output.Header("Configuration")
222+
output.KeyValue(nil, "Config file", config.Path())
223+
if defaultProfile != "" {
224+
output.KeyValue(nil, "Default", defaultProfile)
225+
}
226+
fmt.Println()
227+
228+
for _, name := range names {
229+
profile, err := config.GetProfile(name)
230+
if err != nil {
231+
return err
232+
}
233+
output.Header(name)
234+
output.KeyValue(nil, "Endpoint", profile.Endpoint)
235+
output.KeyValue(nil, "Token", maskToken(profile.Token))
236+
fmt.Println()
237+
}
238+
return nil
82239
}
83240

84241
func maskToken(token string) string {
@@ -89,8 +246,10 @@ func maskToken(token string) string {
89246
}
90247

91248
func init() {
249+
configureCmd.Flags().String("profile", "default", "Profile name (e.g. prod, staging)")
92250
configureCmd.Flags().String("endpoint", "", "Cipi API endpoint URL")
93251
configureCmd.Flags().String("token", "", "API authentication token")
94-
configureCmd.AddCommand(configureShowCmd)
252+
configureDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation")
253+
configureCmd.AddCommand(configureShowCmd, configureListCmd, configureDeleteCmd, configureDefaultCmd)
95254
rootCmd.AddCommand(configureCmd)
96255
}

cmd/profiles.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
"strings"
6+
7+
"github.com/cipi-sh/cli/internal/config"
8+
)
9+
10+
var reservedRootCommands = map[string]struct{}{
11+
"configure": {},
12+
"profiles": {},
13+
"profile": {},
14+
"version": {},
15+
"update": {},
16+
"help": {},
17+
"completion": {},
18+
}
19+
20+
func stripProfileArg() {
21+
if len(os.Args) < 2 {
22+
return
23+
}
24+
25+
arg := os.Args[1]
26+
if strings.HasPrefix(arg, "-") {
27+
return
28+
}
29+
if _, reserved := reservedRootCommands[arg]; reserved {
30+
return
31+
}
32+
if isKnownRootCommand(arg) {
33+
return
34+
}
35+
if !config.ProfileExists(arg) {
36+
return
37+
}
38+
39+
config.SetActiveProfile(arg)
40+
os.Args = append([]string{os.Args[0]}, os.Args[2:]...)
41+
}
42+
43+
func isKnownRootCommand(name string) bool {
44+
for _, cmd := range rootCmd.Commands() {
45+
if cmd.Name() == name {
46+
return true
47+
}
48+
for _, alias := range cmd.Aliases {
49+
if alias == name {
50+
return true
51+
}
52+
}
53+
}
54+
return false
55+
}

0 commit comments

Comments
 (0)