Skip to content

Commit e5c8b26

Browse files
jeanlaurentdgageot
authored andcommitted
Adding alias command
1 parent 661ad44 commit e5c8b26

6 files changed

Lines changed: 395 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ models:
125125
- Tests located alongside source files (`*_test.go`)
126126
- Run `task test` to execute full test suite
127127

128+
#### Testing Best Practices
129+
130+
This project uses `github.com/stretchr/testify` for assertions.
131+
132+
In Go tests, always prefer `require` and `assert` from the `testify` package over manual error handling.
133+
128134
### Configuration Validation
129135

130136
- All agent references must exist in config

cmd/root/alias.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package root
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"sort"
8+
"strings"
9+
10+
"github.com/spf13/cobra"
11+
12+
"github.com/docker/cagent/pkg/aliases"
13+
"github.com/docker/cagent/pkg/telemetry"
14+
)
15+
16+
// NewAliasCmd creates a new alias command for managing aliases
17+
func NewAliasCmd() *cobra.Command {
18+
cmd := &cobra.Command{
19+
Use: "alias",
20+
Short: "Manage aliases for agents",
21+
Long: `Create and manage aliases for agent configurations or catalog references.`,
22+
Example: ` # Create an alias for a catalog agent
23+
cagent alias add code agentcatalog/notion-expert
24+
25+
# Create an alias for a local agent file
26+
cagent alias add myagent ~/myagent.yaml
27+
28+
# List all registered aliases
29+
cagent alias list
30+
31+
# Remove an alias
32+
cagent alias remove code`,
33+
}
34+
35+
cmd.AddCommand(newAliasAddCmd())
36+
cmd.AddCommand(newAliasListCmd())
37+
cmd.AddCommand(newAliasRemoveCmd())
38+
39+
return cmd
40+
}
41+
42+
// newAliasAddCmd creates the add subcommand
43+
func newAliasAddCmd() *cobra.Command {
44+
return &cobra.Command{
45+
Use: "add <alias-name> <agent-path>",
46+
Short: "Add a new alias",
47+
Args: cobra.ExactArgs(2),
48+
RunE: func(cmd *cobra.Command, args []string) error {
49+
telemetry.TrackCommand("alias", append([]string{"add"}, args...))
50+
return createAlias(args[0], args[1])
51+
},
52+
}
53+
}
54+
55+
// newAliasListCmd creates the list subcommand
56+
func newAliasListCmd() *cobra.Command {
57+
return &cobra.Command{
58+
Use: "list",
59+
Aliases: []string{"ls"},
60+
Short: "List all registered aliases",
61+
Args: cobra.NoArgs,
62+
RunE: func(cmd *cobra.Command, args []string) error {
63+
telemetry.TrackCommand("alias", []string{"list"})
64+
return listAliases()
65+
},
66+
}
67+
}
68+
69+
// newAliasRemoveCmd creates the remove subcommand
70+
func newAliasRemoveCmd() *cobra.Command {
71+
return &cobra.Command{
72+
Use: "remove <alias-name>",
73+
Aliases: []string{"rm"},
74+
Short: "Remove a registered alias",
75+
Args: cobra.ExactArgs(1),
76+
RunE: func(cmd *cobra.Command, args []string) error {
77+
telemetry.TrackCommand("alias", append([]string{"remove"}, args...))
78+
return removeAlias(args[0])
79+
},
80+
}
81+
}
82+
83+
// createAlias creates a new alias
84+
func createAlias(name, agentPath string) error {
85+
// Load existing aliases
86+
s, err := aliases.Load()
87+
if err != nil {
88+
return fmt.Errorf("failed to load aliases: %w", err)
89+
}
90+
91+
// Expand tilde in path if it's a local file path
92+
if strings.HasPrefix(agentPath, "~/") {
93+
if abs, err := expandTilde(agentPath); err == nil {
94+
agentPath = abs
95+
}
96+
}
97+
98+
// Store the alias
99+
s.Set(name, agentPath)
100+
101+
// Save to file
102+
if err := s.Save(); err != nil {
103+
return fmt.Errorf("failed to save aliases: %w", err)
104+
}
105+
106+
fmt.Printf("Alias '%s' created successfully\n", name)
107+
fmt.Printf(" Alias: %s\n", name)
108+
fmt.Printf(" Agent: %s\n", agentPath)
109+
fmt.Printf("\nYou can now run: cagent run %s\n", name)
110+
111+
return nil
112+
}
113+
114+
// removeAlias removes an alias
115+
func removeAlias(name string) error {
116+
s, err := aliases.Load()
117+
if err != nil {
118+
return fmt.Errorf("failed to load aliases: %w", err)
119+
}
120+
121+
if !s.Delete(name) {
122+
return fmt.Errorf("alias '%s' not found", name)
123+
}
124+
125+
if err := s.Save(); err != nil {
126+
return fmt.Errorf("failed to save aliases: %w", err)
127+
}
128+
129+
fmt.Printf("Alias '%s' removed successfully\n", name)
130+
return nil
131+
}
132+
133+
func listAliases() error {
134+
s, err := aliases.Load()
135+
if err != nil {
136+
return fmt.Errorf("failed to load aliases: %w", err)
137+
}
138+
139+
allAliases := s.List()
140+
if len(allAliases) == 0 {
141+
fmt.Println("No aliases registered.")
142+
fmt.Println("\nCreate an alias with: cagent alias add <name> <agent-path>")
143+
return nil
144+
}
145+
146+
fmt.Printf("Registered aliases (%d):\n\n", len(allAliases))
147+
148+
// Sort aliases by name for consistent output
149+
names := make([]string, 0, len(allAliases))
150+
for name := range allAliases {
151+
names = append(names, name)
152+
}
153+
sort.Strings(names)
154+
155+
// Find max name length for alignment
156+
maxLen := 0
157+
for _, name := range names {
158+
if len(name) > maxLen {
159+
maxLen = len(name)
160+
}
161+
}
162+
163+
for _, name := range names {
164+
path := allAliases[name]
165+
padding := strings.Repeat(" ", maxLen-len(name))
166+
fmt.Printf(" %s%s → %s\n", name, padding, path)
167+
}
168+
169+
fmt.Printf("\nRun an alias with: cagent run <alias>\n")
170+
171+
return nil
172+
}
173+
174+
// expandTilde expands the tilde in a path to the user's home directory
175+
func expandTilde(path string) (string, error) {
176+
if !strings.HasPrefix(path, "~/") {
177+
return path, nil
178+
}
179+
180+
homeDir, err := os.UserHomeDir()
181+
if err != nil {
182+
return "", err
183+
}
184+
185+
return filepath.Join(homeDir, strings.TrimPrefix(path, "~/")), nil
186+
}

cmd/root/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ func NewRootCmd() *cobra.Command {
123123
cmd.AddCommand(NewCatalogCmd())
124124
cmd.AddCommand(NewBuildCmd())
125125
cmd.AddCommand(NewPrintCmd())
126+
cmd.AddCommand(NewAliasCmd())
126127

127128
return cmd
128129
}

cmd/root/run.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/spf13/cobra"
1919
"go.opentelemetry.io/otel"
2020

21+
"github.com/docker/cagent/pkg/aliases"
2122
"github.com/docker/cagent/pkg/app"
2223
"github.com/docker/cagent/pkg/chat"
2324
"github.com/docker/cagent/pkg/config"
@@ -92,6 +93,15 @@ func doRunCommand(ctx context.Context, args []string, exec bool) error {
9293
slog.Debug("Starting agent", "agent", agentName, "debug_mode", debugMode)
9394

9495
agentFilename := args[0]
96+
97+
// Try to resolve as an alias first
98+
if aliasStore, err := aliases.Load(); err == nil {
99+
if resolvedPath, ok := aliasStore.Get(agentFilename); ok {
100+
slog.Debug("Resolved alias", "alias", agentFilename, "path", resolvedPath)
101+
agentFilename = resolvedPath
102+
}
103+
}
104+
95105
if !strings.Contains(agentFilename, "\n") && (strings.Contains(agentFilename, ".yaml") || strings.Contains(agentFilename, ".yml")) {
96106
if abs, err := filepath.Abs(agentFilename); err == nil {
97107
agentFilename = abs

pkg/aliases/aliases.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package aliases
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/goccy/go-yaml"
9+
10+
"github.com/docker/cagent/pkg/paths"
11+
)
12+
13+
// Aliases represents the aliases configuration
14+
type Aliases struct {
15+
Aliases map[string]string `yaml:"aliases"`
16+
}
17+
18+
// GetAliasesFilePath returns the path to the aliases file
19+
func GetAliasesFilePath() string {
20+
return filepath.Join(paths.GetConfigDir(), "aliases.yaml")
21+
}
22+
23+
// Load loads aliases from the configuration file
24+
func Load() (*Aliases, error) {
25+
return LoadFrom(GetAliasesFilePath())
26+
}
27+
28+
// LoadFrom loads aliases from a specific file path
29+
func LoadFrom(path string) (*Aliases, error) {
30+
data, err := os.ReadFile(path)
31+
if err != nil {
32+
if os.IsNotExist(err) {
33+
return &Aliases{Aliases: make(map[string]string)}, nil
34+
}
35+
return nil, fmt.Errorf("failed to read aliases file: %w", err)
36+
}
37+
38+
var s Aliases
39+
if err := yaml.Unmarshal(data, &s); err != nil {
40+
return nil, fmt.Errorf("failed to parse aliases file: %w", err)
41+
}
42+
43+
if s.Aliases == nil {
44+
s.Aliases = make(map[string]string)
45+
}
46+
47+
return &s, nil
48+
}
49+
50+
// Save saves aliases to the configuration file
51+
func (s *Aliases) Save() error {
52+
return s.SaveTo(GetAliasesFilePath())
53+
}
54+
55+
// SaveTo saves aliases to a specific file path
56+
func (s *Aliases) SaveTo(path string) error {
57+
// Ensure directory exists
58+
dir := filepath.Dir(path)
59+
if err := os.MkdirAll(dir, 0o755); err != nil {
60+
return fmt.Errorf("failed to create directory: %w", err)
61+
}
62+
63+
data, err := yaml.Marshal(s)
64+
if err != nil {
65+
return fmt.Errorf("failed to marshal aliases: %w", err)
66+
}
67+
68+
if err := os.WriteFile(path, data, 0o644); err != nil {
69+
return fmt.Errorf("failed to write aliases file: %w", err)
70+
}
71+
72+
return nil
73+
}
74+
75+
// Get retrieves the agent path for an alias
76+
func (s *Aliases) Get(name string) (string, bool) {
77+
path, ok := s.Aliases[name]
78+
return path, ok
79+
}
80+
81+
// Set creates or updates an alias
82+
func (s *Aliases) Set(name, agentPath string) {
83+
s.Aliases[name] = agentPath
84+
}
85+
86+
// Delete removes an alias
87+
func (s *Aliases) Delete(name string) bool {
88+
if _, exists := s.Aliases[name]; exists {
89+
delete(s.Aliases, name)
90+
return true
91+
}
92+
return false
93+
}
94+
95+
// List returns all aliases
96+
func (s *Aliases) List() map[string]string {
97+
return s.Aliases
98+
}

0 commit comments

Comments
 (0)