Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,13 @@ Skills are reusable capabilities that can be discovered and executed by AI agent
2. In the `New<Command>Cmd()` function:
- Create and configure the `cobra.Command`
- **IMPORTANT**: Always define the `Args` field to specify argument validation
- **IMPORTANT**: Always add an `Example` field with usage examples
- Set up any flags or subcommands
- Return the configured command
3. Register the command in `pkg/cmd/root.go` by adding `rootCmd.AddCommand(New<Command>Cmd())` in the `NewRootCmd()` function
4. Create corresponding test file `pkg/cmd/<command>_test.go`
5. In tests, create command instances using `NewRootCmd()` or `New<Command>Cmd()` as needed
6. **IMPORTANT**: Add a `Test<Command>Cmd_Examples` function to validate the examples

**Command Argument Validation:**

Expand All @@ -227,6 +229,8 @@ func NewExampleCmd() *cobra.Command {
return &cobra.Command{
Use: "example",
Short: "An example command",
Example: `# Run the example command
kortex-cli example`,
Args: cobra.NoArgs, // Always declare Args field
Run: func(cmd *cobra.Command, args []string) {
// Command logic here
Expand All @@ -238,6 +242,142 @@ func NewExampleCmd() *cobra.Command {
rootCmd.AddCommand(NewExampleCmd())
```

**Command Examples:**

All commands **MUST** include an `Example` field with usage examples. Examples improve help documentation and are automatically validated to ensure they stay accurate as the code evolves.

**Example Format:**
```go
func NewExampleCmd() *cobra.Command {
return &cobra.Command{
Use: "example [arg]",
Short: "An example command",
Example: `# Basic usage with comment
kortex-cli example

# With argument
kortex-cli example value

# With flag
kortex-cli example --flag value`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
// Command logic here
},
}
}
```

**Example Guidelines:**
- Use comments (lines starting with `#`) to describe what each example does
- Show the most common use cases (typically 3-5 examples)
- Include examples for all important flags
- Examples must use the actual binary name (`kortex-cli`)
- All commands and flags in examples must exist
- Keep examples concise and realistic

**Validating Examples:**

Every command with an `Example` field **MUST** have a corresponding validation test:

```go
func Test<Command>Cmd_Examples(t *testing.T) {
t.Parallel()

// Get the command
cmd := New<Command>Cmd()

// Verify Example field is not empty
if cmd.Example == "" {
t.Fatal("Example field should not be empty")
}

// Parse the examples
commands, err := testutil.ParseExampleCommands(cmd.Example)
if err != nil {
t.Fatalf("Failed to parse examples: %v", err)
}

// Verify we have the expected number of examples
expectedCount := 3 // Adjust based on your examples
if len(commands) != expectedCount {
t.Errorf("Expected %d example commands, got %d", expectedCount, len(commands))
}

// Validate all examples against the root command
rootCmd := NewRootCmd()
err = testutil.ValidateCommandExamples(rootCmd, cmd.Example)
if err != nil {
t.Errorf("Example validation failed: %v", err)
}
}
```

**What the validator checks:**
- Binary name is `kortex-cli`
- All commands exist in the command tree
- All flags (both long and short) are valid for the command
- No invalid subcommands are used

**Reference:** See `pkg/cmd/init.go` and `pkg/cmd/init_test.go` for complete examples.

**Alias Commands:**

Alias commands are shortcuts that delegate to existing commands (e.g., `list` as an alias for `workspace list`). For alias commands:

1. **Inherit the Example field** from the original command
2. **Adapt the examples** to show the alias syntax instead of the full command
3. **Do NOT create validation tests** for aliases (they use the same validation as the original command)

Use the `AdaptExampleForAlias()` helper function (from `pkg/cmd/helpers.go`) to automatically replace the command name in examples while preserving comments:

```go
func NewListCmd() *cobra.Command {
// Create the workspace list command
workspaceListCmd := NewWorkspaceListCmd()

// Create an alias command that delegates to workspace list
cmd := &cobra.Command{
Use: "list",
Short: workspaceListCmd.Short,
Long: workspaceListCmd.Long,
Example: AdaptExampleForAlias(workspaceListCmd.Example, "workspace list", "list"),
Args: workspaceListCmd.Args,
PreRunE: workspaceListCmd.PreRunE,
RunE: workspaceListCmd.RunE,
}

// Copy flags from workspace list command
cmd.Flags().AddFlagSet(workspaceListCmd.Flags())

return cmd
}
```

The `AdaptExampleForAlias()` function:
- Replaces the original command with the alias **only in command lines** (lines starting with `kortex-cli`)
- **Preserves comments unchanged** (lines starting with `#`)
- Maintains formatting and indentation

**Example transformation:**
```bash
# Original (from workspace list):
# List all workspaces
kortex-cli workspace list

# List in JSON format
kortex-cli workspace list --output json

# After AdaptExampleForAlias(..., "workspace list", "list"):
# List all workspaces
kortex-cli list

# List in JSON format
kortex-cli list --output json
```

**Reference:** See `pkg/cmd/list.go` and `pkg/cmd/remove.go` for complete alias examples.

### Command Implementation Pattern

Commands should follow a consistent structure for maintainability and testability:
Expand Down
53 changes: 53 additions & 0 deletions pkg/cmd/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2026 Red Hat, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import "strings"

// AdaptExampleForAlias replaces the original command with the alias command
// in the example string, but only in command lines (not in comments).
// This is useful for alias commands that want to inherit examples from
// their original commands while showing the alias syntax.
//
// Example:
//
// original := `# List all workspaces
// kortex-cli workspace list
//
// # List in JSON format
// kortex-cli workspace list --output json`
//
// adapted := AdaptExampleForAlias(original, "workspace list", "list")
// // Result:
// // `# List all workspaces
// // kortex-cli list
// //
// // # List in JSON format
// // kortex-cli list --output json`
func AdaptExampleForAlias(example, originalCmd, aliasCmd string) string {
lines := strings.Split(example, "\n")
var result []string

for _, line := range lines {
trimmed := strings.TrimSpace(line)
// Only replace in command lines (starting with kortex-cli), not in comments
if strings.HasPrefix(trimmed, "kortex-cli ") {
line = strings.Replace(line, originalCmd, aliasCmd, 1)
}
result = append(result, line)
}

return strings.Join(result, "\n")
}
120 changes: 120 additions & 0 deletions pkg/cmd/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright 2026 Red Hat, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import "testing"

func TestAdaptExampleForAlias(t *testing.T) {
t.Parallel()

tests := []struct {
name string
example string
originalCmd string
aliasCmd string
want string
}{
{
name: "replaces command in simple example",
example: `# List all workspaces
kortex-cli workspace list`,
originalCmd: "workspace list",
aliasCmd: "list",
want: `# List all workspaces
kortex-cli list`,
},
{
name: "replaces command with flags",
example: `# List workspaces in JSON format
kortex-cli workspace list --output json`,
originalCmd: "workspace list",
aliasCmd: "list",
want: `# List workspaces in JSON format
kortex-cli list --output json`,
},
{
name: "replaces multiple occurrences",
example: `# List all workspaces
kortex-cli workspace list

# List in JSON format
kortex-cli workspace list --output json

# List using short flag
kortex-cli workspace list -o json`,
originalCmd: "workspace list",
aliasCmd: "list",
want: `# List all workspaces
kortex-cli list

# List in JSON format
kortex-cli list --output json

# List using short flag
kortex-cli list -o json`,
},
{
name: "does not replace in comments",
example: `# Use workspace list to see all workspaces
kortex-cli workspace list`,
originalCmd: "workspace list",
aliasCmd: "list",
want: `# Use workspace list to see all workspaces
kortex-cli list`,
},
{
name: "replaces remove command",
example: `# Remove workspace by ID
kortex-cli workspace remove abc123`,
originalCmd: "workspace remove",
aliasCmd: "remove",
want: `# Remove workspace by ID
kortex-cli remove abc123`,
},
{
name: "handles empty example",
example: ``,
originalCmd: "workspace list",
aliasCmd: "list",
want: ``,
},
{
name: "preserves indentation",
example: `# List all workspaces
kortex-cli workspace list

# Another example
kortex-cli workspace list --output json`,
originalCmd: "workspace list",
aliasCmd: "list",
want: `# List all workspaces
kortex-cli list

# Another example
kortex-cli list --output json`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

got := AdaptExampleForAlias(tt.example, tt.originalCmd, tt.aliasCmd)
if got != tt.want {
t.Errorf("AdaptExampleForAlias() mismatch:\nGot:\n%s\n\nWant:\n%s", got, tt.want)
}
})
}
}
11 changes: 11 additions & 0 deletions pkg/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ func NewInitCmd() *cobra.Command {

The sources directory defaults to the current directory (.) if not specified.
The workspace configuration directory defaults to .kortex/ inside the sources directory if not specified.`,
Example: `# Register current directory as workspace
kortex-cli init

# Register specific directory as workspace
kortex-cli init /path/to/project

# Register with custom workspace name
kortex-cli init --name my-project

# Show detailed output
kortex-cli init --verbose`,
Args: cobra.MaximumNArgs(1),
PreRunE: c.preRun,
RunE: c.run,
Expand Down
Loading
Loading