Skip to main content

Adding CLI Commands

Runix uses cobra for CLI commands. This guide shows how to add a new command.

Step 1: Create the Command File​

Create cmd/runix/<command>.go:

package main

import (
"fmt"
"os"

"github.com/runixio/runix/internal/daemon"
"github.com/spf13/cobra"
)

func newMyCommand() *cobra.Command {
var format string

cmd := &cobra.Command{
Use: "mycommand <id|name>",
Short: "Short description of my command",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
target := args[0]

// Support dry-run
if dryRun {
fmt.Fprintf(os.Stdout, "[Runix] Dry run: would mycommand %q\n", target)
return nil
}

// Try daemon IPC first
if daemonIsRunning() {
resp, err := sendIPC(daemon.ActionMyCommand, daemon.MyPayload{
Target: target,
})
if err != nil {
fmt.Fprintf(os.Stderr, "[Runix] Daemon IPC failed, using direct mode: %v\n", err)
} else if !resp.Success {
return fmt.Errorf("daemon error: %s", resp.Error)
} else {
fmt.Fprintf(os.Stdout, "[Runix] Done\n")
return nil
}
}

// Direct mode fallback
sup, err := getSupervisor()
if err != nil {
return err
}

// ... implement direct mode logic ...

return nil
},
}

cmd.Flags().StringVarP(&format, "format", "f", "text", "output format: text, json")

return cmd
}

Step 2: Register in root.go​

Open cmd/runix/root.go and add the command to the init() function:

rootCmd.AddCommand(newMyCommand())

Step 3: Add Daemon IPC Action (if needed)​

If the command should work through the daemon:

protocol.go — Add action constant and payload​

const ActionMyCommand = "mycommand"

type MyPayload struct {
Target string `json:"target"`
}

server.go — Add handler​

case daemon.ActionMyCommand:
// Handle the action

client.go — Add client method (if needed)​

No changes needed — sendIPC() handles all actions generically.

Patterns to Follow​

Dual-Mode Execution​

Every command must follow the dual-mode pattern:

// 1. Try daemon IPC
if daemonIsRunning() {
resp, err := sendIPC(action, payload)
if err != nil {
// Log and fall through to direct mode
} else if !resp.Success {
return fmt.Errorf("daemon error: %s", resp.Error)
} else {
// Success via daemon
return nil
}
}

// 2. Direct mode (in-process supervisor)
sup, err := getSupervisor()
if err != nil {
return err
}
// ... execute logic ...

Output Formatting​

Use the outputResult() helper for dual text/JSON output:

outputResult(format, resultData, func() {
fmt.Fprintf(os.Stdout, "[Runix] Human-readable message\n")
})

Target Resolution​

Use sup.Get(target) which handles:

  • Exact numeric ID match
  • Exact name match
  • Unique UUID prefix match

Global Flags​

These are available automatically via root.go:

VariableFlagDescription
dryRun--dry-runPrint what would happen
cfgFile--configConfig file path
verbose--verboseVerbose output

What's Next​