with one click
add-command-with-json
Add a new CLI command with JSON output support
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Add a new CLI command with JSON output support
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
Add a new runtime implementation to the kdn runtime system
Guide to using the instances manager API for workspace management and project detection
Guide to understanding and working with the kdn runtime system architecture
Guide to configuring the Podman runtime including image setup, agent configuration, and containerfile generation
Conventional Commit Message Generator
Guide to the autoconf package — how secret detection, filtering, and the runner are wired together, and how to extend the system with new detector types
| name | add-command-with-json |
| description | Add a new CLI command with JSON output support |
| argument-hint | <command-name> |
This skill helps you add a new CLI command that supports both text and JSON output formats.
Create pkg/cmd/<command>.go with the following structure:
package cmd
import (
"encoding/json"
"fmt"
"io"
"path/filepath"
"github.com/spf13/cobra"
"github.com/openkaiden/kdn/pkg/instances"
// "github.com/openkaiden/kdn/pkg/runtimesetup" // Uncomment if registering runtimes
// "github.com/openkaiden/kdn/pkg/steplogger" // Uncomment if calling runtime methods
// Add other imports as needed
)
type <command>Cmd struct {
output string // Bound to --output flag
// Add other fields for flags and dependencies
manager instances.Manager
}
func New<Command>Cmd() *cobra.Command {
c := &<command>Cmd{}
cmd := &cobra.Command{
Use: "<command> [args]",
Short: "Short description of the command",
Long: `Long description of what the command does.`,
Example: `# Basic usage
kdn <command> arg1
# With JSON output
kdn <command> arg1 --output json
# With other flags
kdn <command> arg1 --flag value`,
Args: cobra.ExactArgs(1), // Or NoArgs, MinimumNArgs(1), MaximumNArgs(1), etc.
PreRunE: c.preRun,
RunE: c.run,
}
// Bind flags to struct fields using *Var variants
cmd.Flags().StringVarP(&c.output, "output", "o", "", "Output format (supported: json)")
return cmd
}
func (c *<command>Cmd) preRun(cmd *cobra.Command, args []string) error {
// 1. FIRST: Validate output format
if c.output != "" && c.output != "json" {
return fmt.Errorf("unsupported output format: %s (supported: json)", c.output)
}
// 2. EARLY: Silence Cobra's error output in JSON mode
if c.output == "json" {
cmd.SilenceErrors = true
}
// 3. ALL subsequent errors use outputErrorIfJSON
storageDir, err := cmd.Flags().GetString("storage")
if err != nil {
return outputErrorIfJSON(cmd, c.output, fmt.Errorf("failed to read --storage flag: %w", err))
}
// Validate arguments
// Convert paths to absolute if needed
// Create dependencies (manager, etc.)
// Example: Create manager
manager, err := instances.NewManager(storageDir)
if err != nil {
return outputErrorIfJSON(cmd, c.output, fmt.Errorf("failed to create manager: %w", err))
}
c.manager = manager
// Register runtimes if your command interacts with workspaces
// (e.g., Start, Stop, Remove, Create operations)
// Commands that only list or query workspaces don't need this
//
// if err := runtimesetup.RegisterAll(manager); err != nil {
// return outputErrorIfJSON(cmd, c.output, fmt.Errorf("failed to register runtimes: %w", err))
// }
return nil
}
// createStepLogger creates the appropriate StepLogger based on output mode.
// Call this in your run() method if your command calls runtime operations.
func (c *<command>Cmd) createStepLogger(cmd *cobra.Command) steplogger.StepLogger {
if c.output == "json" {
// No step logging in JSON mode - silent
return steplogger.NewNoOpLogger()
}
// Use text logger with spinners for text output
return steplogger.NewTextLogger(cmd.ErrOrStderr())
}
func (c *<command>Cmd) run(cmd *cobra.Command, args []string) error {
// If your command calls runtime methods (Create, Start, Stop, Remove),
// inject StepLogger into context for user progress feedback:
//
// logger := c.createStepLogger(cmd)
// defer logger.Complete()
// ctx := steplogger.WithLogger(cmd.Context(), logger)
//
// Then pass ctx to runtime methods:
// info, err := runtime.Create(ctx, params)
// Perform the command logic
// ALL errors use outputErrorIfJSON
data, err := c.manager.GetData()
if err != nil {
return outputErrorIfJSON(cmd, c.output, fmt.Errorf("failed to get data: %w", err))
}
if c.output == "json" {
return c.outputJSON(cmd, data)
}
// Text output
cmd.Println("Success message")
return nil
}
func (c *<command>Cmd) outputJSON(cmd *cobra.Command, data interface{}) error {
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return outputErrorIfJSON(cmd, c.output, fmt.Errorf("failed to marshal to JSON: %w", err))
}
fmt.Fprintln(cmd.OutOrStdout(), string(jsonData))
return nil
}
In pkg/cmd/root.go, add to the NewRootCmd() function:
rootCmd.AddCommand(New<Command>Cmd())
Optional: Assign to a command group
If you want the command to appear in a specific group in the help output:
// Define a group if it doesn't exist yet
rootCmd.AddGroup(&cobra.Group{
ID: "mygroup",
Title: "My Command Group:",
})
// Create and assign command to the group
myCmd := New<Command>Cmd()
myCmd.GroupID = "mygroup"
rootCmd.AddCommand(myCmd)
Existing groups:
main - Main Commands (for commonly used workspace operations)workspace - Workspace Commands (for the workspace parent command)Create pkg/cmd/<command>_test.go:
package cmd
import (
"encoding/json"
"path/filepath"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/openkaiden/kdn/pkg/cmd/testutil"
)
func Test<Command>Cmd_PreRun(t *testing.T) {
t.Parallel()
t.Run("validates output format", func(t *testing.T) {
t.Parallel()
storageDir := t.TempDir()
c := &<command>Cmd{
output: "xml", // Invalid format
}
cmd := &cobra.Command{}
cmd.Flags().String("storage", storageDir, "test storage flag")
err := c.preRun(cmd, []string{})
if err == nil {
t.Fatal("Expected error for invalid output format")
}
if !strings.Contains(err.Error(), "unsupported output format") {
t.Errorf("Expected 'unsupported output format' error, got: %v", err)
}
})
t.Run("sets fields correctly", func(t *testing.T) {
t.Parallel()
storageDir := t.TempDir()
c := &<command>Cmd{
output: "json",
}
cmd := &cobra.Command{}
cmd.Flags().String("storage", storageDir, "test storage flag")
err := c.preRun(cmd, []string{})
if err != nil {
t.Fatalf("preRun() failed: %v", err)
}
if c.manager == nil {
t.Error("Expected manager to be created")
}
})
}
func Test<Command>Cmd_E2E(t *testing.T) {
t.Parallel()
t.Run("executes with text output", func(t *testing.T) {
t.Parallel()
storageDir := t.TempDir()
rootCmd := NewRootCmd()
rootCmd.SetArgs([]string{"<command>", "--storage", storageDir})
err := rootCmd.Execute()
if err != nil {
t.Fatalf("Execute() failed: %v", err)
}
})
t.Run("executes with JSON output", func(t *testing.T) {
t.Parallel()
storageDir := t.TempDir()
rootCmd := NewRootCmd()
var output strings.Builder
rootCmd.SetOut(&output)
rootCmd.SetArgs([]string{"<command>", "--storage", storageDir, "--output", "json"})
err := rootCmd.Execute()
if err != nil {
t.Fatalf("Execute() failed: %v", err)
}
// Verify JSON structure
var result map[string]interface{}
if err := json.Unmarshal([]byte(output.String()), &result); err != nil {
t.Fatalf("Failed to parse JSON output: %v", err)
}
})
}
func Test<Command>Cmd_Examples(t *testing.T) {
t.Parallel()
cmd := New<Command>Cmd()
if cmd.Example == "" {
t.Fatal("Example field should not be empty")
}
commands, err := testutil.ParseExampleCommands(cmd.Example)
if err != nil {
t.Fatalf("Failed to parse examples: %v", err)
}
expectedCount := 3 // Adjust based on your examples
if len(commands) != expectedCount {
t.Errorf("Expected %d example commands, got %d", expectedCount, len(commands))
}
rootCmd := NewRootCmd()
err = testutil.ValidateCommandExamples(rootCmd, cmd.Example)
if err != nil {
t.Errorf("Example validation failed: %v", err)
}
}
# Run tests for the new command
go test ./pkg/cmd -run Test<Command>
# Run all tests
make test
If the command warrants user-facing documentation, update relevant docs.
If your command calls runtime methods (Create, Start, Stop, Remove), you MUST inject a StepLogger into the context to provide user progress feedback.
When to use:
runtime.Create(), runtime.Start(), runtime.Stop(), or runtime.Remove()How to integrate:
"github.com/openkaiden/kdn/pkg/steplogger"
func (c *<command>Cmd) createStepLogger(cmd *cobra.Command) steplogger.StepLogger {
if c.output == "json" {
return steplogger.NewNoOpLogger() // Silent in JSON mode
}
return steplogger.NewTextLogger(cmd.ErrOrStderr()) // Spinners in text mode
}
func (c *<command>Cmd) run(cmd *cobra.Command, args []string) error {
// Create and attach logger
logger := c.createStepLogger(cmd)
defer logger.Complete()
ctx := steplogger.WithLogger(cmd.Context(), logger)
// Call runtime methods with the context
info, err := runtime.Start(ctx, workspaceID)
if err != nil {
return outputErrorIfJSON(cmd, c.output, err)
}
// ... rest of implementation
}
Benefits:
See also:
pkg/cmd/init.go - Example with Create operationpkg/cmd/workspace_start.go - Example with Start operation*Var variants (StringVarP, BoolVarP, etc.) to bind flags to struct fieldsoutputErrorIfJSON() for ALL errors after setting up JSON modecmd.SilenceErrors = true early in preRunruntimesetup.RegisterAll(manager) in preRun. Commands that only query or list workspaces don't need this.pkg/cmd/init.go - Complete implementation with JSON outputpkg/cmd/workspace_list.go - List command with JSON outputpkg/cmd/workspace_remove.go - Remove command with JSON outputpkg/cmd/conversion.go - Helper functions for JSON error handling