Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.
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
2 changes: 2 additions & 0 deletions cmd/jtk/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/comments"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/completion"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/configcmd"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/initcmd"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/issues"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/me"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/root"
Expand All @@ -29,6 +30,7 @@ func run() error {
rootCmd, opts := root.NewCmd()

// Register all commands
initcmd.Register(rootCmd, opts)
configcmd.Register(rootCmd, opts)
issues.Register(rootCmd, opts)
transitions.Register(rootCmd, opts)
Expand Down
55 changes: 55 additions & 0 deletions internal/cmd/configcmd/configcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func Register(parent *cobra.Command, opts *root.Options) {
cmd.AddCommand(newSetCmd(opts))
cmd.AddCommand(newShowCmd(opts))
cmd.AddCommand(newClearCmd(opts))
cmd.AddCommand(newTestCmd(opts))

parent.AddCommand(cmd)
}
Expand Down Expand Up @@ -197,3 +198,57 @@ func getAPITokenSource() string {
}
return "-"
}

func newTestCmd(opts *root.Options) *cobra.Command {
return &cobra.Command{
Use: "test",
Short: "Test connection to Jira",
Long: `Verify that jtk can connect to Jira with the current configuration.

This command tests authentication and API access, providing clear
pass/fail status and troubleshooting suggestions on failure.`,
Example: ` # Test connection
jtk config test`,
RunE: func(cmd *cobra.Command, args []string) error {
v := opts.View()

url := config.GetURL()
if url == "" {
v.Error("No Jira URL configured")
v.Println("")
v.Info("Configure with: jtk init")
v.Info("Or set environment variable: JIRA_URL")
return nil
}

v.Println("Testing connection to %s...", url)
v.Println("")

client, err := opts.APIClient()
if err != nil {
v.Error("Failed to create client: %v", err)
v.Println("")
v.Info("Check your configuration with: jtk config show")
v.Info("Reconfigure with: jtk init")
return nil
}

user, err := client.GetCurrentUser()
if err != nil {
v.Error("Authentication failed: %v", err)
v.Println("")
v.Info("Check your credentials with: jtk config show")
v.Info("Reconfigure with: jtk init")
return nil
}

v.Success("Authentication successful")
v.Success("API access verified")
v.Println("")
v.Println("Authenticated as: %s (%s)", user.DisplayName, user.EmailAddress)
v.Println("Account ID: %s", user.AccountID)

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

import (
"bufio"
"fmt"
"strings"

"github.com/spf13/cobra"

"github.com/open-cli-collective/jira-ticket-cli/api"
"github.com/open-cli-collective/jira-ticket-cli/internal/cmd/root"
"github.com/open-cli-collective/jira-ticket-cli/internal/config"
)

// Register registers the init command
func Register(parent *cobra.Command, opts *root.Options) {
var url, email, token string
var noVerify bool

cmd := &cobra.Command{
Use: "init",
Short: "Initialize jtk with guided setup",
Long: `Interactive setup wizard for configuring jtk.

Prompts for your Jira URL, email, and API token, then verifies
the connection before saving the configuration.

Get your API token from: https://id.atlassian.com/manage-profile/security/api-tokens`,
Example: ` # Interactive setup
jtk init

# Non-interactive setup
jtk init --url https://mycompany.atlassian.net --email user@example.com --token YOUR_TOKEN

# Skip connection verification
jtk init --no-verify`,
RunE: func(cmd *cobra.Command, args []string) error {
return runInit(opts, url, email, token, noVerify)
},
}

cmd.Flags().StringVar(&url, "url", "", "Jira URL (e.g., https://mycompany.atlassian.net)")
cmd.Flags().StringVar(&email, "email", "", "Email address for authentication")
cmd.Flags().StringVar(&token, "token", "", "API token")
cmd.Flags().BoolVar(&noVerify, "no-verify", false, "Skip connection verification")

parent.AddCommand(cmd)
}

func runInit(opts *root.Options, url, email, token string, noVerify bool) error {
v := opts.View()
reader := bufio.NewReader(opts.Stdin)

v.Println("Jira CLI Setup")
v.Println("")

// Check for existing config
existingCfg, _ := config.Load()
if existingCfg.URL != "" || existingCfg.Email != "" || existingCfg.APIToken != "" {
v.Warning("Existing configuration found at %s", config.Path())
v.Println("")

overwrite, err := promptYesNo(reader, "Overwrite existing configuration?", false)
if err != nil {
return err
}
if !overwrite {
v.Info("Setup cancelled")
return nil
}
v.Println("")
}

// Prompt for URL if not provided
if url == "" {
v.Println("Enter your Jira URL")
v.Println(" Examples: https://mycompany.atlassian.net")
v.Println(" https://jira.internal.corp.com")
v.Println("")

var err error
url, err = promptRequired(reader, "URL")
if err != nil {
return err
}
}
url = config.NormalizeURL(url)

// Prompt for email if not provided
if email == "" {
v.Println("")
var err error
email, err = promptRequired(reader, "Email")
if err != nil {
return err
}
}

// Prompt for token if not provided
if token == "" {
v.Println("")
v.Println("Get your API token from:")
v.Println(" https://id.atlassian.com/manage-profile/security/api-tokens")
v.Println("")

var err error
token, err = promptRequired(reader, "API Token")
if err != nil {
return err
}
}

v.Println("")

// Verify connection unless --no-verify
if !noVerify {
v.Println("Testing connection...")

client, err := api.New(api.ClientConfig{
URL: url,
Email: email,
APIToken: token,
})
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}

user, err := client.GetCurrentUser()
if err != nil {
v.Error("Connection failed: %v", err)
v.Println("")
v.Info("Check your credentials and try again")
return fmt.Errorf("authentication failed")
}

v.Success("Connected to %s", url)
v.Success("Authenticated as %s (%s)", user.DisplayName, user.EmailAddress)
v.Println("")
}

// Save configuration
cfg := &config.Config{
URL: url,
Email: email,
APIToken: token,
}

if err := config.Save(cfg); err != nil {
return fmt.Errorf("failed to save configuration: %w", err)
}

v.Success("Configuration saved to %s", config.Path())
v.Println("")
v.Println("Try it out:")
v.Println(" jtk me")
v.Println(" jtk issues list --project <PROJECT>")

return nil
}

func promptRequired(reader *bufio.Reader, label string) (string, error) {
for {
fmt.Printf("%s: ", label)
input, err := reader.ReadString('\n')
if err != nil {
return "", err
}
input = strings.TrimSpace(input)
if input != "" {
return input, nil
}
fmt.Printf(" %s is required\n", label)
}
}

func promptYesNo(reader *bufio.Reader, question string, defaultYes bool) (bool, error) {
suffix := " [y/N]: "
if defaultYes {
suffix = " [Y/n]: "
}

fmt.Print(question + suffix)
input, err := reader.ReadString('\n')
if err != nil {
return false, err
}
input = strings.TrimSpace(strings.ToLower(input))

if input == "" {
return defaultYes, nil
}
return input == "y" || input == "yes", nil
}