224 lines
5.6 KiB
Go
224 lines
5.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var validName = regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
|
|
|
|
func SpaceCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "space",
|
|
Short: "Manage space connections",
|
|
}
|
|
cmd.AddCommand(spaceAddCommand(), spaceListCommand(), spaceRemoveCommand())
|
|
return cmd
|
|
}
|
|
|
|
func spaceAddCommand() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "add",
|
|
Short: "Add a new space",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return SpaceAddInteractive(nil)
|
|
},
|
|
}
|
|
}
|
|
|
|
// SpaceAddInteractive runs the interactive space-add flow.
|
|
// If presetURL is non-nil, it skips the URL prompt (used by App to extend this).
|
|
func SpaceAddInteractive(presetURL *string) error {
|
|
cfg, err := LoadConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
// Name
|
|
fmt.Print("Space name: ")
|
|
name, _ := reader.ReadString('\n')
|
|
name = strings.TrimSpace(name)
|
|
if !validName.MatchString(name) {
|
|
return fmt.Errorf("name must be alphanumeric with hyphens only")
|
|
}
|
|
for _, s := range cfg.Spaces {
|
|
if s.Name == name {
|
|
return fmt.Errorf("space %q already exists", name)
|
|
}
|
|
}
|
|
|
|
// URL
|
|
var spaceURL string
|
|
if presetURL != nil {
|
|
spaceURL = *presetURL
|
|
} else {
|
|
fmt.Print("URL (e.g. http://localhost:3000): ")
|
|
spaceURL, _ = reader.ReadString('\n')
|
|
spaceURL = strings.TrimSpace(spaceURL)
|
|
}
|
|
if _, err := url.ParseRequestURI(spaceURL); err != nil {
|
|
return fmt.Errorf("invalid URL format")
|
|
}
|
|
spaceURL = strings.TrimRight(spaceURL, "/")
|
|
|
|
// Probe the server to check reachability and whether auth is required
|
|
authType := "none"
|
|
conn, _ := NewSpaceConnection(ConnectionOpts{URL: spaceURL})
|
|
if conn != nil {
|
|
reachable, needsAuth := conn.Probe()
|
|
if !reachable {
|
|
fmt.Fprintln(os.Stderr, "Warning: could not reach server at that URL (saving anyway)")
|
|
fmt.Print("Auth type (token / password / none) [none]: ")
|
|
authInput, _ := reader.ReadString('\n')
|
|
authType = strings.TrimSpace(authInput)
|
|
if authType == "" {
|
|
authType = "none"
|
|
}
|
|
} else if needsAuth {
|
|
fmt.Println("Server requires authentication.")
|
|
fmt.Print("Auth type (password / token) [password]:")
|
|
authInput, _ := reader.ReadString('\n')
|
|
authType = strings.TrimSpace(authInput)
|
|
if authType == "" {
|
|
authType = "password"
|
|
}
|
|
} else {
|
|
fmt.Println("Server is reachable (no authentication required).")
|
|
}
|
|
}
|
|
if authType != "token" && authType != "password" && authType != "none" {
|
|
return fmt.Errorf("auth type must be token, password, or none")
|
|
}
|
|
|
|
space := SpaceConfig{
|
|
ID: NewUUID(),
|
|
Name: name,
|
|
URL: spaceURL,
|
|
Auth: AuthConfig{Method: authType},
|
|
}
|
|
|
|
for authType != "none" {
|
|
if authType == "token" {
|
|
fmt.Print("Token: ")
|
|
token, _ := reader.ReadString('\n')
|
|
token = strings.TrimSpace(token)
|
|
enc, err := Encrypt(token)
|
|
if err != nil {
|
|
return fmt.Errorf("encrypting token: %w", err)
|
|
}
|
|
space.Auth.EncryptedToken = enc
|
|
space.Auth.EncryptedPassword = ""
|
|
space.Auth.Username = ""
|
|
} else if authType == "password" {
|
|
fmt.Print("Username: ")
|
|
username, _ := reader.ReadString('\n')
|
|
space.Auth.Username = strings.TrimSpace(username)
|
|
fmt.Print("Password: ")
|
|
password, _ := reader.ReadString('\n')
|
|
password = strings.TrimSpace(password)
|
|
enc, err := Encrypt(password)
|
|
if err != nil {
|
|
return fmt.Errorf("encrypting password: %w", err)
|
|
}
|
|
space.Auth.EncryptedPassword = enc
|
|
space.Auth.EncryptedToken = ""
|
|
}
|
|
space.Auth.Method = authType
|
|
|
|
verifyConn, err := NewSpaceConnection(ConnectionOpts{Space: &space})
|
|
if err != nil {
|
|
return fmt.Errorf("creating connection: %w", err)
|
|
}
|
|
if verifyConn.AuthCheck() {
|
|
fmt.Println("Authentication verified.")
|
|
break
|
|
}
|
|
fmt.Println("Authentication failed. Try again.")
|
|
fmt.Print("Auth type (password / token) [password]:")
|
|
authInput, _ := reader.ReadString('\n')
|
|
authType = strings.TrimSpace(authInput)
|
|
if authType == "" {
|
|
authType = "password"
|
|
}
|
|
if authType != "token" && authType != "password" {
|
|
return fmt.Errorf("auth type must be token or password")
|
|
}
|
|
}
|
|
|
|
cfg.Spaces = append(cfg.Spaces, space)
|
|
if err := SaveConfig(cfg); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Space %q added.\n", name)
|
|
return nil
|
|
}
|
|
|
|
func spaceListCommand() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "ls",
|
|
Aliases: []string{"list"},
|
|
Short: "List configured spaces",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
cfg, err := LoadConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(cfg.Spaces) == 0 {
|
|
fmt.Println("No spaces configured. Use 'space add' to add one.")
|
|
return nil
|
|
}
|
|
fmt.Println()
|
|
fmt.Printf("%-20s%-40s%s\n", "NAME", "URL", "AUTH")
|
|
fmt.Println(strings.Repeat("-", 70))
|
|
for _, s := range cfg.Spaces {
|
|
u := s.URL
|
|
if u == "" && s.FolderPath != "" {
|
|
u = s.FolderPath
|
|
}
|
|
fmt.Printf("%-20s%-40s%s\n", s.Name, u, s.Auth.Method)
|
|
}
|
|
fmt.Println()
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func spaceRemoveCommand() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "rm <name>",
|
|
Aliases: []string{"remove"},
|
|
Short: "Remove a space",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
name := args[0]
|
|
cfg, err := LoadConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
idx := -1
|
|
for i, s := range cfg.Spaces {
|
|
if s.Name == name {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
if idx == -1 {
|
|
return fmt.Errorf("space %q not found", name)
|
|
}
|
|
cfg.Spaces = append(cfg.Spaces[:idx], cfg.Spaces[idx+1:]...)
|
|
if err := SaveConfig(cfg); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("Space %q removed.\n", name)
|
|
return nil
|
|
},
|
|
}
|
|
}
|