Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions internal/cmd/configcmd/clear.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package configcmd

import (
"fmt"
"os"

"github.com/fatih/color"
"github.com/spf13/cobra"

"github.com/open-cli-collective/confluence-cli/internal/config"
)

// NewCmdClear creates the config clear command.
func NewCmdClear() *cobra.Command {
cmd := &cobra.Command{
Use: "clear",
Short: "Remove stored configuration",
Long: `Delete the cfl configuration file. Environment variables will still be used if set.`,
Example: ` # Clear config
cfl config clear`,
RunE: func(cmd *cobra.Command, _ []string) error {
noColor, _ := cmd.Flags().GetBool("no-color")
return runClear(noColor)
},
}

return cmd
}

func runClear(noColor bool) error {
if noColor {
color.NoColor = true
}

configPath := config.DefaultConfigPath()

err := os.Remove(configPath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove config file: %w", err)
}

green := color.New(color.FgGreen)
dim := color.New(color.Faint)

if os.IsNotExist(err) {
_, _ = green.Printf("✓ No config file to remove\n")
} else {
_, _ = green.Printf("✓ Configuration cleared from %s\n", configPath)
}

// Check if env vars are set
envVars := []string{"CFL_URL", "CFL_EMAIL", "CFL_API_TOKEN", "CFL_DEFAULT_SPACE",
"ATLASSIAN_URL", "ATLASSIAN_EMAIL", "ATLASSIAN_API_TOKEN"}
var activeVars []string
for _, v := range envVars {
if os.Getenv(v) != "" {
activeVars = append(activeVars, v)
}
}

if len(activeVars) > 0 {
_, _ = dim.Printf("\nNote: Environment variables will still be used: %s\n", fmt.Sprintf("%v", activeVars))
}

return nil
}
57 changes: 57 additions & 0 deletions internal/cmd/configcmd/clear_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package configcmd

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/open-cli-collective/confluence-cli/internal/config"
)

func TestRunClear_WithExistingConfig(t *testing.T) {
tmpDir := t.TempDir()
xdgDir := filepath.Join(tmpDir, "cfl")
os.MkdirAll(xdgDir, 0755)

origXDG := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", tmpDir)
defer os.Setenv("XDG_CONFIG_HOME", origXDG)

cfg := &config.Config{
URL: "https://test.atlassian.net/wiki",
Email: "test@example.com",
APIToken: "test-token",
}
configPath := filepath.Join(xdgDir, "config.yml")
require.NoError(t, cfg.Save(configPath))

err := runClear(true)
require.NoError(t, err)

// Verify file is deleted
_, err = os.Stat(configPath)
assert.True(t, os.IsNotExist(err))
}

func TestRunClear_NoConfigFile(t *testing.T) {
origXDG := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", t.TempDir())
defer os.Setenv("XDG_CONFIG_HOME", origXDG)

// Should not error even if file doesn't exist
err := runClear(true)
require.NoError(t, err)
}

func TestRunClear_Idempotent(t *testing.T) {
origXDG := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", t.TempDir())
defer os.Setenv("XDG_CONFIG_HOME", origXDG)

// Running twice should succeed
require.NoError(t, runClear(true))
require.NoError(t, runClear(true))
}
21 changes: 21 additions & 0 deletions internal/cmd/configcmd/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Package configcmd provides config management commands.
package configcmd

import (
"github.com/spf13/cobra"
)

// NewCmdConfig creates the config command.
func NewCmdConfig() *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: "Manage cfl configuration",
Long: `Commands for viewing, testing, and clearing cfl configuration.`,
}

cmd.AddCommand(NewCmdShow())
cmd.AddCommand(NewCmdTest())
cmd.AddCommand(NewCmdClear())

return cmd
}
95 changes: 95 additions & 0 deletions internal/cmd/configcmd/show.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package configcmd

import (
"fmt"
"os"
"strings"

"github.com/fatih/color"
"github.com/spf13/cobra"

"github.com/open-cli-collective/confluence-cli/internal/config"
)

// NewCmdShow creates the config show command.
func NewCmdShow() *cobra.Command {
cmd := &cobra.Command{
Use: "show",
Short: "Display current configuration",
Long: `Display the current cfl configuration with credential source indicators.`,
Example: ` # Show current config
cfl config show`,
RunE: func(cmd *cobra.Command, _ []string) error {
noColor, _ := cmd.Flags().GetBool("no-color")
return runShow(noColor)
},
}

return cmd
}

func runShow(noColor bool) error {
if noColor {
color.NoColor = true
}

configPath := config.DefaultConfigPath()

// Load file config (may not exist)
fileCfg, fileErr := config.Load(configPath)
if fileErr != nil {
fileCfg = &config.Config{}
}

// Load full config with env overrides
cfg, _ := config.LoadWithEnv(configPath)

bold := color.New(color.Bold)
dim := color.New(color.Faint)

printField := func(label, value, fileValue string, envVars ...string) {
_, _ = bold.Printf("%-12s", label+":")
if value == "" {
_, _ = dim.Println("-")
return
}

// Mask tokens
display := value
if strings.Contains(strings.ToLower(label), "token") && len(value) > 8 {
display = value[:4] + strings.Repeat("*", len(value)-8) + value[len(value)-4:]
}

fmt.Print(display)

// Determine source
source := "config"
if fileErr != nil {
source = "-"
}
for _, envVar := range envVars {
if v := os.Getenv(envVar); v != "" && v == value {
source = envVar
break
}
}
if fileValue != value && source == "config" {
source = "-"
}

_, _ = dim.Printf(" (source: %s)\n", source)
}

printField("URL", cfg.URL, fileCfg.URL, "CFL_URL", "ATLASSIAN_URL")
printField("Email", cfg.Email, fileCfg.Email, "CFL_EMAIL", "ATLASSIAN_EMAIL")
printField("API Token", cfg.APIToken, fileCfg.APIToken, "CFL_API_TOKEN", "ATLASSIAN_API_TOKEN")
printField("Space", cfg.DefaultSpace, fileCfg.DefaultSpace, "CFL_DEFAULT_SPACE")

fmt.Println()
_, _ = dim.Printf("Config file: %s\n", configPath)
if fileErr != nil {
_, _ = dim.Println("(file not found)")
}

return nil
}
55 changes: 55 additions & 0 deletions internal/cmd/configcmd/show_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package configcmd

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"

"github.com/open-cli-collective/confluence-cli/internal/config"
)

func TestRunShow_WithConfigFile(t *testing.T) {
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "config.yml")

cfg := &config.Config{
URL: "https://test.atlassian.net/wiki",
Email: "test@example.com",
APIToken: "test-token-value",
DefaultSpace: "DEV",
}
require.NoError(t, cfg.Save(configPath))

// Override default config path for test
origXDG := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", tmpDir)
defer os.Setenv("XDG_CONFIG_HOME", origXDG)

// Ensure XDG path matches
xdgDir := filepath.Join(tmpDir, "cfl")
os.MkdirAll(xdgDir, 0755)
xdgPath := filepath.Join(xdgDir, "config.yml")
require.NoError(t, cfg.Save(xdgPath))

err := runShow(true)
require.NoError(t, err)
}

func TestRunShow_NoConfigFile(t *testing.T) {
// Clear env vars
for _, v := range []string{"CFL_URL", "CFL_EMAIL", "CFL_API_TOKEN", "CFL_DEFAULT_SPACE",
"ATLASSIAN_URL", "ATLASSIAN_EMAIL", "ATLASSIAN_API_TOKEN"} {
orig := os.Getenv(v)
os.Unsetenv(v)
defer os.Setenv(v, orig)
}

origXDG := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", t.TempDir())
defer os.Setenv("XDG_CONFIG_HOME", origXDG)

err := runShow(true)
require.NoError(t, err)
}
98 changes: 98 additions & 0 deletions internal/cmd/configcmd/test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package configcmd

import (
"fmt"
"net/http"
"time"

"github.com/fatih/color"
"github.com/spf13/cobra"

"github.com/open-cli-collective/confluence-cli/internal/config"
)

// NewCmdTest creates the config test command.
func NewCmdTest() *cobra.Command {
cmd := &cobra.Command{
Use: "test",
Short: "Test connectivity with configured credentials",
Long: `Test that cfl can connect to your Confluence instance with the current configuration.`,
Example: ` # Test connection
cfl config test`,
RunE: func(cmd *cobra.Command, _ []string) error {
noColor, _ := cmd.Flags().GetBool("no-color")
return runTest(noColor, nil)
},
}

return cmd
}

func runTest(noColor bool, httpClient *http.Client, cfgs ...*config.Config) error {
if noColor {
color.NoColor = true
}

var cfg *config.Config
if len(cfgs) > 0 && cfgs[0] != nil {
cfg = cfgs[0]
} else {
var err error
cfg, err = config.LoadWithEnv(config.DefaultConfigPath())
if err != nil {
return fmt.Errorf("failed to load config: %w (run 'cfl init' to configure)", err)
}

if err := cfg.Validate(); err != nil {
return fmt.Errorf("invalid config: %w (run 'cfl init' to configure)", err)
}
}

green := color.New(color.FgGreen)
red := color.New(color.FgRed)

fmt.Printf("Testing connection to %s...\n", cfg.URL)

if httpClient == nil {
httpClient = &http.Client{Timeout: 10 * time.Second}
}

req, err := http.NewRequest("GET", cfg.URL+"/api/v2/spaces?limit=1", nil)
if err != nil {
return err
}

req.SetBasicAuth(cfg.Email, cfg.APIToken)
req.Header.Set("Accept", "application/json")

resp, err := httpClient.Do(req)
if err != nil {
_, _ = red.Println("✗ Connection failed:", err)
fmt.Println("\nCheck your URL with: cfl config show")
fmt.Println("Reconfigure with: cfl init")
return fmt.Errorf("connection failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()

if resp.StatusCode == 401 {
_, _ = red.Println("✗ Authentication failed: 401 Unauthorized")
fmt.Println("\nCheck your credentials with: cfl config show")
fmt.Println("Reconfigure with: cfl init")
return fmt.Errorf("authentication failed")
}
if resp.StatusCode == 403 {
_, _ = red.Println("✗ Access denied: 403 Forbidden")
fmt.Println("\nCheck your permissions.")
return fmt.Errorf("access denied")
}
if resp.StatusCode != 200 {
_, _ = red.Printf("✗ Unexpected response: %d\n", resp.StatusCode)
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

_, _ = green.Println("✓ Authentication successful")
_, _ = green.Println("✓ API access verified")
fmt.Printf("\nAuthenticated as: %s\n", cfg.Email)

return nil
}
Loading
Loading