Waveterm add-wshcmd
Guide for adding new wsh commands to Wave Terminal. Use when implementing new CLI commands, adding command-line functionality, or extending the wsh command interface.
git clone https://github.com/wavetermdev/waveterm
T=$(mktemp -d) && git clone --depth=1 https://github.com/wavetermdev/waveterm "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.kilocode/skills/add-wshcmd" ~/.claude/skills/wavetermdev-waveterm-add-wshcmd && rm -rf "$T"
.kilocode/skills/add-wshcmd/SKILL.mdAdding a New wsh Command to Wave Terminal
This guide explains how to add a new command to the
wsh CLI tool.
wsh Command System Overview
Wave Terminal's
wsh command provides CLI access to Wave Terminal features. The system uses:
- Cobra Framework - CLI command structure and parsing
- Command Files - Individual command implementations in
cmd/wsh/cmd/wshcmd-*.go - RPC Client - Communication with Wave Terminal backend via
RpcClient - Activity Tracking - Telemetry for command usage analytics
- Documentation - User-facing docs in
docs/docs/wsh-reference.mdx
Commands are registered in their
init() functions and execute through the Cobra framework.
Step-by-Step Guide
Step 1: Create Command File
Create a new file in
cmd/wsh/cmd/ named wshcmd-[commandname].go:
// Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 package cmd import ( "fmt" "github.com/spf13/cobra" "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" ) var myCommandCmd = &cobra.Command{ Use: "mycommand [args]", Short: "Brief description of what this command does", Long: `Detailed description of the command. Can include multiple lines and examples of usage.`, RunE: myCommandRun, PreRunE: preRunSetupRpcClient, // Include if command needs RPC DisableFlagsInUseLine: true, } // Flag variables var ( myCommandFlagExample string myCommandFlagVerbose bool ) func init() { // Add command to root rootCmd.AddCommand(myCommandCmd) // Define flags myCommandCmd.Flags().StringVarP(&myCommandFlagExample, "example", "e", "", "example flag description") myCommandCmd.Flags().BoolVarP(&myCommandFlagVerbose, "verbose", "v", false, "enable verbose output") } func myCommandRun(cmd *cobra.Command, args []string) (rtnErr error) { // Always track activity for telemetry defer func() { sendActivity("mycommand", rtnErr == nil) }() // Validate arguments if len(args) == 0 { OutputHelpMessage(cmd) return fmt.Errorf("requires at least one argument") } // Command implementation fmt.Printf("Command executed successfully\n") return nil }
File Naming Convention:
- Use
formatwshcmd-[commandname].go - Use lowercase, hyphenated names for multi-word commands
- Examples:
,wshcmd-getvar.go
,wshcmd-setmeta.gowshcmd-ai.go
Step 2: Command Structure
Basic Command Structure
var myCommandCmd = &cobra.Command{ Use: "mycommand [required] [optional...]", Short: "One-line description (shown in help)", Long: `Detailed multi-line description`, // Argument validation Args: cobra.MinimumNArgs(1), // Or cobra.ExactArgs(1), cobra.NoArgs, etc. // Execution function RunE: myCommandRun, // Pre-execution setup (if needed) PreRunE: preRunSetupRpcClient, // Sets up RPC client for backend communication // Example usage (optional) Example: " wsh mycommand foo\n wsh mycommand --flag bar", // Disable flag notation in usage line DisableFlagsInUseLine: true, }
Key Fields:
: Command name and argument patternUse
: Brief description for command listShort
: Detailed description shown in helpLong
: Argument validator (optional)Args
: Main execution function (returns error)RunE
: Setup function that runs beforePreRunERunE
: Usage examples (optional)Example
: Clean up help displayDisableFlagsInUseLine
When to Use PreRunE
Include
PreRunE: preRunSetupRpcClient if your command:
- Communicates with the Wave Terminal backend
- Needs access to
RpcClient - Requires JWT authentication (WAVETERM_JWT env var)
- Makes RPC calls via
functionswshclient.*Command()
Don't include PreRunE for commands that:
- Only manipulate local state
- Don't need backend communication
- Are purely informational/local operations
Step 3: Implement Command Logic
Command Function Pattern
func myCommandRun(cmd *cobra.Command, args []string) (rtnErr error) { // Step 1: Always track activity (for telemetry) defer func() { sendActivity("mycommand", rtnErr == nil) }() // Step 2: Validate arguments and flags if len(args) != 1 { OutputHelpMessage(cmd) return fmt.Errorf("requires exactly one argument") } // Step 3: Parse/prepare data targetArg := args[0] // Step 4: Make RPC call if needed result, err := wshclient.SomeCommand(RpcClient, wshrpc.CommandSomeData{ Field: targetArg, }, &wshrpc.RpcOpts{Timeout: 2000}) if err != nil { return fmt.Errorf("executing command: %w", err) } // Step 5: Output results fmt.Printf("Result: %s\n", result) return nil }
Important Patterns:
-
Activity Tracking: Always include deferred
callsendActivity()defer func() { sendActivity("commandname", rtnErr == nil) }() -
Error Handling: Return errors, don't call
os.Exit()if err != nil { return fmt.Errorf("context: %w", err) } -
Output: Use standard
package for outputfmtfmt.Printf("Success message\n") fmt.Fprintf(os.Stderr, "Error message\n") -
Help Messages: Show help when arguments are invalid
if len(args) == 0 { OutputHelpMessage(cmd) return fmt.Errorf("requires arguments") } -
Exit Codes: Set custom exit code via
WshExitCodeif notFound { WshExitCode = 1 return nil // Don't return error, just set exit code }
Step 4: Define Flags
Add flags in the
init() function:
var ( // Declare flag variables at package level myCommandFlagString string myCommandFlagBool bool myCommandFlagInt int ) func init() { rootCmd.AddCommand(myCommandCmd) // String flag with short version myCommandCmd.Flags().StringVarP(&myCommandFlagString, "name", "n", "default", "description") // Boolean flag myCommandCmd.Flags().BoolVarP(&myCommandFlagBool, "verbose", "v", false, "enable verbose") // Integer flag myCommandCmd.Flags().IntVar(&myCommandFlagInt, "count", 10, "set count") // Flag without short version myCommandCmd.Flags().StringVar(&myCommandFlagString, "longname", "", "description") }
Flag Types:
- String valuesStringVar/StringVarP
- Boolean flagsBoolVar/BoolVarP
- Integer valuesIntVar/IntVarP- The
suffix versions include a short flag nameP
Flag Naming:
- Use camelCase for variable names:
myCommandFlagName - Use kebab-case for flag names:
--flag-name - Prefix variable names with command name for clarity
Step 5: Working with Block Arguments
Many commands operate on blocks. Use the standard block resolution pattern:
func myCommandRun(cmd *cobra.Command, args []string) (rtnErr error) { defer func() { sendActivity("mycommand", rtnErr == nil) }() // Resolve block using the -b/--block flag fullORef, err := resolveBlockArg() if err != nil { return err } // Use the blockid in RPC call err = wshclient.SomeCommand(RpcClient, wshrpc.CommandSomeData{ BlockId: fullORef.OID, }, &wshrpc.RpcOpts{Timeout: 2000}) if err != nil { return fmt.Errorf("command failed: %w", err) } return nil }
Block Resolution:
- The
flag is defined globally in-b/--blockwshcmd-root.go
resolves the block argument to a full ORefresolveBlockArg()- Supports:
,this
, full UUIDs, 8-char prefixes, block numberstab - Default is
(current block)"this"
Alternative: Manual Block Resolution
// Get tab ID from environment tabId := os.Getenv("WAVETERM_TABID") if tabId == "" { return fmt.Errorf("WAVETERM_TABID not set") } // Create route for tab-level operations route := wshutil.MakeTabRouteId(tabId) // Use route in RPC call err := wshclient.SomeCommand(RpcClient, commandData, &wshrpc.RpcOpts{ Route: route, Timeout: 2000, })
Step 6: Making RPC Calls
Use the
wshclient package to make RPC calls:
import ( "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" ) // Simple RPC call result, err := wshclient.GetMetaCommand(RpcClient, wshrpc.CommandGetMetaData{ ORef: *fullORef, }, &wshrpc.RpcOpts{Timeout: 2000}) if err != nil { return fmt.Errorf("getting metadata: %w", err) } // RPC call with routing err := wshclient.SetMetaCommand(RpcClient, wshrpc.CommandSetMetaData{ ORef: *fullORef, Meta: metaMap, }, &wshrpc.RpcOpts{ Route: route, Timeout: 5000, }) if err != nil { return fmt.Errorf("setting metadata: %w", err) }
RPC Options:
: Request timeout in milliseconds (typically 2000-5000)Timeout
: Route ID for targeting specific componentsRoute- Available routes:
,wshutil.ControlRoutewshutil.MakeTabRouteId(tabId)
Step 7: Add Documentation
Add your command to
docs/docs/wsh-reference.mdx:
## mycommand Brief description of what the command does. ```sh wsh mycommand [args] [flags] ``` Detailed explanation of the command's purpose and behavior. Flags: - `-n, --name <value>` - description of this flag - `-v, --verbose` - enable verbose output - `-b, --block <blockid>` - specify target block (default: current block) Examples: ```sh # Basic usage wsh mycommand arg1 # With flags wsh mycommand --name value arg1 # With block targeting wsh mycommand -b 2 arg1 # Complex example wsh mycommand -v --name "example" arg1 arg2 ``` Additional notes, tips, or warnings about the command. ---
Documentation Guidelines:
- Place in alphabetical order with other commands
- Include command signature with argument pattern
- List all flags with short and long versions
- Provide practical examples (at least 3-5)
- Explain common use cases and patterns
- Add tips or warnings if relevant
- Use
separator between commands---
Step 8: Test Your Command
Build and test the command:
# Build wsh task build:wsh # Or build everything task build # Test the command ./bin/wsh/wsh mycommand --help ./bin/wsh/wsh mycommand arg1 arg2
Testing Checklist:
- Help message displays correctly
- Required arguments validated
- Flags work as expected
- Error messages are clear
- Success cases work correctly
- RPC calls complete successfully
- Output is formatted correctly
Complete Examples
Example 1: Simple Command with No RPC
Use case: A command that prints Wave Terminal version info
Command File (cmd/wsh/cmd/wshcmd-version.go
)
cmd/wsh/cmd/wshcmd-version.go// Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 package cmd import ( "github.com/spf13/cobra" "github.com/wavetermdev/waveterm/pkg/wavebase" ) var versionCmd = &cobra.Command{ Use: "version", Short: "Print Wave Terminal version", RunE: versionRun, } func init() { rootCmd.AddCommand(versionCmd) } func versionRun(cmd *cobra.Command, args []string) (rtnErr error) { defer func() { sendActivity("version", rtnErr == nil) }() fmt.Printf("Wave Terminal %s\n", wavebase.WaveVersion) return nil }
Documentation
## version Print the current Wave Terminal version. ```sh wsh version ``` Examples: ```sh # Print version wsh version ```
Example 2: Command with Flags and RPC
Use case: A command to update block title
Command File (cmd/wsh/cmd/wshcmd-settitle.go
)
cmd/wsh/cmd/wshcmd-settitle.go// Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 package cmd import ( "fmt" "github.com/spf13/cobra" "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" ) var setTitleCmd = &cobra.Command{ Use: "settitle [title]", Short: "Set block title", Long: `Set the title for the current or specified block.`, Args: cobra.ExactArgs(1), RunE: setTitleRun, PreRunE: preRunSetupRpcClient, DisableFlagsInUseLine: true, } var setTitleIcon string func init() { rootCmd.AddCommand(setTitleCmd) setTitleCmd.Flags().StringVarP(&setTitleIcon, "icon", "i", "", "set block icon") } func setTitleRun(cmd *cobra.Command, args []string) (rtnErr error) { defer func() { sendActivity("settitle", rtnErr == nil) }() title := args[0] // Resolve block fullORef, err := resolveBlockArg() if err != nil { return err } // Build metadata map meta := make(map[string]interface{}) meta["title"] = title if setTitleIcon != "" { meta["icon"] = setTitleIcon } // Make RPC call err = wshclient.SetMetaCommand(RpcClient, wshrpc.CommandSetMetaData{ ORef: *fullORef, Meta: meta, }, &wshrpc.RpcOpts{Timeout: 2000}) if err != nil { return fmt.Errorf("setting title: %w", err) } fmt.Printf("title updated\n") return nil }
Documentation
## settitle Set the title for a block. ```sh wsh settitle [title] ``` Update the display title for the current or specified block. Optionally set an icon as well. Flags: - `-i, --icon <icon>` - set block icon along with title - `-b, --block <blockid>` - specify target block (default: current block) Examples: ```sh # Set title for current block wsh settitle "My Terminal" # Set title and icon wsh settitle --icon "terminal" "Development Shell" # Set title for specific block wsh settitle -b 2 "Build Output" ```
Example 3: Subcommands
Use case: Command with multiple subcommands (like
wsh conn)
Command File (cmd/wsh/cmd/wshcmd-mygroup.go
)
cmd/wsh/cmd/wshcmd-mygroup.go// Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 package cmd import ( "fmt" "github.com/spf13/cobra" "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" ) var myGroupCmd = &cobra.Command{ Use: "mygroup", Short: "Manage something", } var myGroupListCmd = &cobra.Command{ Use: "list", Short: "List items", RunE: myGroupListRun, PreRunE: preRunSetupRpcClient, } var myGroupAddCmd = &cobra.Command{ Use: "add [name]", Short: "Add an item", Args: cobra.ExactArgs(1), RunE: myGroupAddRun, PreRunE: preRunSetupRpcClient, } func init() { // Add parent command rootCmd.AddCommand(myGroupCmd) // Add subcommands myGroupCmd.AddCommand(myGroupListCmd) myGroupCmd.AddCommand(myGroupAddCmd) } func myGroupListRun(cmd *cobra.Command, args []string) (rtnErr error) { defer func() { sendActivity("mygroup:list", rtnErr == nil) }() // Implementation fmt.Printf("Listing items...\n") return nil } func myGroupAddRun(cmd *cobra.Command, args []string) (rtnErr error) { defer func() { sendActivity("mygroup:add", rtnErr == nil) }() name := args[0] fmt.Printf("Adding item: %s\n", name) return nil }
Documentation
## mygroup Manage something with subcommands. ### list List all items. ```sh wsh mygroup list ``` ### add Add a new item. ```sh wsh mygroup add [name] ``` Examples: ```sh # List items wsh mygroup list # Add an item wsh mygroup add "new-item" ```
Common Patterns
Reading from Stdin
import "io" func myCommandRun(cmd *cobra.Command, args []string) (rtnErr error) { defer func() { sendActivity("mycommand", rtnErr == nil) }() // Check if reading from stdin (using "-" convention) var data []byte var err error if len(args) > 0 && args[0] == "-" { data, err = io.ReadAll(os.Stdin) if err != nil { return fmt.Errorf("reading stdin: %w", err) } } else { // Read from file or other source data, err = os.ReadFile(args[0]) if err != nil { return fmt.Errorf("reading file: %w", err) } } // Process data fmt.Printf("Read %d bytes\n", len(data)) return nil }
JSON File Input
import ( "encoding/json" "io" ) func loadJSONFile(filepath string) (map[string]interface{}, error) { var data []byte var err error if filepath == "-" { data, err = io.ReadAll(os.Stdin) if err != nil { return nil, fmt.Errorf("reading stdin: %w", err) } } else { data, err = os.ReadFile(filepath) if err != nil { return nil, fmt.Errorf("reading file: %w", err) } } var result map[string]interface{} if err := json.Unmarshal(data, &result); err != nil { return nil, fmt.Errorf("parsing JSON: %w", err) } return result, nil }
Conditional Output (TTY Detection)
func myCommandRun(cmd *cobra.Command, args []string) (rtnErr error) { defer func() { sendActivity("mycommand", rtnErr == nil) }() isTty := getIsTty() // Output value fmt.Printf("%s", value) // Add newline only if TTY (for better piping experience) if isTty { fmt.Printf("\n") } return nil }
Environment Variable Access
func myCommandRun(cmd *cobra.Command, args []string) (rtnErr error) { defer func() { sendActivity("mycommand", rtnErr == nil) }() // Get block ID from environment blockId := os.Getenv("WAVETERM_BLOCKID") if blockId == "" { return fmt.Errorf("WAVETERM_BLOCKID not set") } // Get tab ID from environment tabId := os.Getenv("WAVETERM_TABID") if tabId == "" { return fmt.Errorf("WAVETERM_TABID not set") } fmt.Printf("Block: %s, Tab: %s\n", blockId, tabId) return nil }
Best Practices
Command Design
- Single Responsibility: Each command should do one thing well
- Composable: Design commands to work with pipes and other commands
- Consistent: Follow existing wsh command patterns and conventions
- Documented: Provide clear help text and examples
Error Handling
- Context: Wrap errors with context using
fmt.Errorf("context: %w", err) - User-Friendly: Make error messages clear and actionable
- No Panics: Return errors instead of calling
oros.Exit()panic() - Exit Codes: Use
for custom exit codesWshExitCode
Output
- Structured: Use consistent formatting for output
- Quiet by Default: Only output what's necessary
- Verbose Flag: Optionally provide
for detailed output-v - Stderr for Errors: Use
for error messagesfmt.Fprintf(os.Stderr, ...)
Flags
- Short Versions: Provide
short versions for common flags-x - Sensible Defaults: Choose defaults that work for most users
- Boolean Flags: Use for on/off options
- String Flags: Use for values that need user input
RPC Calls
- Timeouts: Always specify reasonable timeouts
- Error Context: Wrap RPC errors with operation context
- Retries: Don't retry automatically; let user retry command
- Routes: Use appropriate routes for different operations
Common Pitfalls
1. Forgetting Activity Tracking
Problem: Command usage not tracked in telemetry
Solution: Always include deferred
sendActivity() call:
defer func() { sendActivity("commandname", rtnErr == nil) }()
2. Using os.Exit() Instead of Returning Error
Problem: Breaks defer statements and cleanup
Solution: Return errors from RunE function:
// Bad if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } // Good if err != nil { return fmt.Errorf("operation failed: %w", err) }
3. Not Validating Arguments
Problem: Command crashes with nil pointer or index out of range
Solution: Validate arguments early and show help:
if len(args) == 0 { OutputHelpMessage(cmd) return fmt.Errorf("requires at least one argument") }
4. Forgetting to Add to init()
Problem: Command not available when running wsh
Solution: Always add command in
init() function:
func init() { rootCmd.AddCommand(myCommandCmd) }
5. Inconsistent Output
Problem: Inconsistent use of output methods
Solution: Use standard
fmt package functions:
// For stdout fmt.Printf("output\n") // For stderr fmt.Fprintf(os.Stderr, "error message\n")
Quick Reference Checklist
When adding a new wsh command:
- Create
cmd/wsh/cmd/wshcmd-[commandname].go - Define command struct with Use, Short, Long descriptions
- Add
if using RPCPreRunE: preRunSetupRpcClient - Implement command function with activity tracking
- Add command to
inrootCmd
functioninit() - Define flags in
function if neededinit() - Add documentation to
docs/docs/wsh-reference.mdx - Build and test:
task build:wsh - Test help:
wsh [commandname] --help - Test all flag combinations
- Test error cases
Related Files
- Root Command:
- Main command setup and utilitiescmd/wsh/cmd/wshcmd-root.go - RPC Client:
- Client functions for RPC callspkg/wshrpc/wshclient/ - RPC Types:
- RPC request/response data structurespkg/wshrpc/wshrpctypes.go - Documentation:
- User-facing command referencedocs/docs/wsh-reference.mdx - Examples:
- Existing command implementationscmd/wsh/cmd/wshcmd-*.go