Overview
CLI commands use Cobra and are defined inpkg/cli/. Each command file typically contains one main command with optional subcommands.
Command Structure
Copy
// pkg/cli/mycommand.go
package cli
import (
"fmt"
"github.com/spf13/cobra"
)
var (
// Command flags
myFlag string
myBoolFlag bool
)
var myCmd = &cobra.Command{
Use: "mycommand",
Short: "Short description",
Long: `Long description with details.`,
RunE: func(cmd *cobra.Command, args []string) error {
return runMyCommand(args)
},
}
func init() {
// Register with root command
rootCmd.AddCommand(myCmd)
// Add flags
myCmd.Flags().StringVarP(&myFlag, "flag", "f", "", "Flag description")
myCmd.Flags().BoolVarP(&myBoolFlag, "verbose", "v", false, "Verbose output")
// Add subcommands
myCmd.AddCommand(mySubCmd)
}
func runMyCommand(args []string) error {
// Load configuration
cfg, err := loadConfig()
if err != nil {
return err
}
// Command logic
fmt.Printf("Running mycommand with flag: %s\n", myFlag)
return nil
}
Steps to Add a Command
1. Create Command File
Createpkg/cli/mycommand.go:
Copy
package cli
import (
"fmt"
"github.com/osmedeus/osmedeus-ng/internal/config"
"github.com/osmedeus/osmedeus-ng/internal/terminal"
"github.com/spf13/cobra"
)
var (
targetFlag string
outputFlag string
verboseFlag bool
)
var myCmd = &cobra.Command{
Use: "mycommand [subcommand]",
Aliases: []string{"my", "mc"},
Short: "My custom command",
Long: `My custom command does something useful.
Examples:
osmedeus mycommand do-something -t example.com
osmedeus mycommand list`,
RunE: func(cmd *cobra.Command, args []string) error {
// Show help if no subcommand
return cmd.Help()
},
}
var myDoCmd = &cobra.Command{
Use: "do-something",
Short: "Do something specific",
RunE: func(cmd *cobra.Command, args []string) error {
return runDoSomething()
},
}
var myListCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List items",
RunE: func(cmd *cobra.Command, args []string) error {
return runList()
},
}
func init() {
// Register main command
rootCmd.AddCommand(myCmd)
// Add flags to main command (inherited by subcommands)
myCmd.PersistentFlags().StringVarP(&targetFlag, "target", "t", "", "Target")
myCmd.PersistentFlags().BoolVarP(&verboseFlag, "verbose", "v", false, "Verbose")
// Add subcommand-specific flags
myDoCmd.Flags().StringVarP(&outputFlag, "output", "o", "", "Output path")
// Register subcommands
myCmd.AddCommand(myDoCmd)
myCmd.AddCommand(myListCmd)
}
func runDoSomething() error {
cfg, err := config.Load(settingsFile)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}
if targetFlag == "" {
return fmt.Errorf("target is required")
}
terminal.PrintInfo("Processing target: %s", targetFlag)
// Your logic here...
terminal.PrintSuccess("Done!")
return nil
}
func runList() error {
cfg, err := config.Load(settingsFile)
if err != nil {
return err
}
items := []string{"item1", "item2", "item3"}
terminal.PrintTable([]string{"Name", "Status"}, [][]string{
{"item1", "active"},
{"item2", "pending"},
{"item3", "complete"},
})
return nil
}
2. Use Terminal Helpers
Theinternal/terminal package provides output formatting:
Copy
import "github.com/osmedeus/osmedeus-ng/internal/terminal"
// Print messages
terminal.PrintInfo("Information message")
terminal.PrintSuccess("Success message")
terminal.PrintWarning("Warning message")
terminal.PrintError("Error message")
// Print formatted
terminal.PrintInfo("Processing %s with %d threads", target, threads)
// Print tables
terminal.PrintTable(
[]string{"Column1", "Column2"},
[][]string{
{"row1-col1", "row1-col2"},
{"row2-col1", "row2-col2"},
},
)
// Print with colors
terminal.PrintColored(terminal.Green, "Success!")
// Spinner for long operations
spinner := terminal.NewSpinner("Loading...")
spinner.Start()
// ... do work ...
spinner.Stop()
3. Access Configuration
Copy
import "github.com/osmedeus/osmedeus-ng/internal/config"
func runMyCommand() error {
// Load config (uses global settingsFile from root.go)
cfg, err := config.Load(settingsFile)
if err != nil {
return err
}
// Use config values
baseFolder := cfg.BaseFolder
dbPath := cfg.Database.DBPath
// Access environment paths
workflowsPath := cfg.Environments.Workflows
binariesPath := cfg.Environments.Binaries
return nil
}
4. Handle Errors
Copy
func runMyCommand() error {
// Return errors - Cobra handles display
if targetFlag == "" {
return fmt.Errorf("--target is required")
}
result, err := doSomething(targetFlag)
if err != nil {
return fmt.Errorf("operation failed: %w", err)
}
return nil
}
5. Add Command Help
Copy
var myCmd = &cobra.Command{
Use: "mycommand <action>",
Short: "Brief one-line description",
Long: `Detailed description of the command.
This command does X, Y, and Z. Use it when you need to...
Examples:
# Basic usage
osmedeus mycommand action -t target
# With options
osmedeus mycommand action -t target -o output.txt --verbose
# Multiple targets
osmedeus mycommand action -t target1 -t target2`,
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
Example: Export Command
Copy
// pkg/cli/export.go
package cli
import (
"fmt"
"os"
"github.com/osmedeus/osmedeus-ng/internal/config"
"github.com/osmedeus/osmedeus-ng/internal/database"
"github.com/osmedeus/osmedeus-ng/internal/terminal"
"github.com/spf13/cobra"
)
var (
exportWorkspace string
exportFormat string
exportOutput string
)
var exportCmd = &cobra.Command{
Use: "export",
Short: "Export data from workspace",
Long: `Export assets, vulnerabilities, or other data from a workspace.
Examples:
osmedeus export assets -w example.com -f json -o assets.json
osmedeus export vulns -w example.com -f csv`,
}
var exportAssetsCmd = &cobra.Command{
Use: "assets",
Short: "Export assets",
RunE: func(cmd *cobra.Command, args []string) error {
return runExportAssets()
},
}
var exportVulnsCmd = &cobra.Command{
Use: "vulns",
Short: "Export vulnerabilities",
RunE: func(cmd *cobra.Command, args []string) error {
return runExportVulns()
},
}
func init() {
rootCmd.AddCommand(exportCmd)
exportCmd.PersistentFlags().StringVarP(&exportWorkspace, "workspace", "w", "", "Workspace name (required)")
exportCmd.PersistentFlags().StringVarP(&exportFormat, "format", "f", "json", "Output format (json, csv, jsonl)")
exportCmd.PersistentFlags().StringVarP(&exportOutput, "output", "o", "", "Output file (stdout if empty)")
exportCmd.MarkPersistentFlagRequired("workspace")
exportCmd.AddCommand(exportAssetsCmd)
exportCmd.AddCommand(exportVulnsCmd)
}
func runExportAssets() error {
cfg, err := config.Load(settingsFile)
if err != nil {
return err
}
db, err := database.Connect(cfg)
if err != nil {
return fmt.Errorf("database connection failed: %w", err)
}
defer db.Close()
assets, err := db.GetAssetsByWorkspace(exportWorkspace)
if err != nil {
return err
}
output, err := formatOutput(assets, exportFormat)
if err != nil {
return err
}
if exportOutput != "" {
return os.WriteFile(exportOutput, []byte(output), 0644)
}
fmt.Println(output)
return nil
}
Flag Types
Copy
// String flag
cmd.Flags().StringVarP(&myString, "string", "s", "default", "description")
// Bool flag
cmd.Flags().BoolVarP(&myBool, "bool", "b", false, "description")
// Int flag
cmd.Flags().IntVarP(&myInt, "count", "c", 10, "description")
// String slice (repeatable)
cmd.Flags().StringSliceVarP(&mySlice, "item", "i", []string{}, "description")
// Persistent flag (inherited by subcommands)
cmd.PersistentFlags().StringVarP(&myPersistent, "global", "g", "", "description")
// Required flag
cmd.Flags().StringVarP(&required, "required", "r", "", "description")
cmd.MarkFlagRequired("required")
Best Practices
- Use subcommands for related actions
- Provide aliases for common commands
- Write helpful examples in Long description
- Validate input early before processing
- Use terminal helpers for consistent output
- Handle interrupts with context
Next Steps
- Adding API Endpoints - REST endpoints
- Adding Step Types - Custom steps
- CLI Reference - Existing commands