- name
- add-wshcmd
- description
- 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.
# Adding 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:
1. **Cobra Framework** - CLI command structure and parsing
2. **Command Files** - Individual command implementations in `cmd/wsh/cmd/wshcmd-*.go`
3. **RPC Client** - Communication with Wave Terminal backend via `RpcClient`
4. **Activity Tracking** - Telemetry for command usage analytics
5. **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`:
```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 `wshcmd-[commandname].go` format
- Use lowercase, hyphenated names for multi-word commands
- Examples: `wshcmd-getvar.go`, `wshcmd-setmeta.go`, `wshcmd-ai.go`
### Step 2: Command Structure
#### Basic Command Structure
```go
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:**
- `Use`: Command name and argument pattern
- `Short`: Brief description for command list
- `Long`: Detailed description shown in help
- `Args`: Argument validator (optional)
- `RunE`: Main execution function (returns error)
- `PreRunE`: Setup function that runs before `RunE`
- `Example`: Usage examples (optional)
- `DisableFlagsInUseLine`: Clean up help display
#### 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 `wshclient.*Command()` functions
**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
```go
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:**
1. **Activity Tracking**: Always include deferred `sendActivity()` call
```go
defer func() {
sendActivity("commandname", rtnErr == nil)
}()
```
2. **Error Handling**: Return errors, don't call `os.Exit()`
```go
if err != nil {
return fmt.Errorf("context: %w", err)
}
```
3. **Output**: Use standard `fmt` package for output
```go
fmt.Printf("Success message\n")
fmt.Fprintf(os.Stderr, "Error message\n")
```
4. **Help Messages**: Show help when arguments are invalid
```go
if len(args) == 0 {
OutputHelpMessage(cmd)
return fmt.Errorf("requires arguments")
}
```
5. **Exit Codes**: Set custom exit code via `WshExitCode`
```go
if notFound {
WshExitCode = 1
return nil // Don't return error, just set exit code
}
```
### Step 4: Define Flags
Add flags in the `init()` function:
```go
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:**
- `StringVar/StringVarP` - String values
- `BoolVar/BoolVarP` - Boolean flags
- `IntVar/IntVarP` - Integer values
- The `P` suffix versions include a short flag name
**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:
```go
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 `-b/--block` flag is defined globally in `wshcmd-root.go`
- `resolveBlockArg()` resolves the block argument to a full ORef
- Supports: `this`, `tab`, full UUIDs, 8-char prefixes, block numbers
- Default is `"this"` (current block)
**Alternative: Manual Block Resolution**
```go
// 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:
```go
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:**
- `Timeout`: Request timeout in milliseconds (typically 2000-5000)
- `Route`: Route ID for targeting specific components
- Available routes: `wshutil.ControlRoute`, `wshutil.MakeTabRouteId(tabId)`
### Step 7: Add Documentation
Add your command to `docs/docs/wsh-reference.mdx`:
````markdown
## 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:
```bash
# 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`)
```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
````markdown
## 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`)
```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"
)
View on GitHub