Skip to content

Commit 6055909

Browse files
authored
Merge pull request #502 from docker/please
Add shortcut management for agent configurations
2 parents 4148d8c + 4b720df commit 6055909

6 files changed

Lines changed: 384 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: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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(_ *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(*cobra.Command, []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(_ *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+
absAgentPath, err := expandTilde(agentPath)
93+
if err != nil {
94+
return err
95+
}
96+
97+
// Store the alias
98+
s.Set(name, absAgentPath)
99+
100+
// Save to file
101+
if err := s.Save(); err != nil {
102+
return fmt.Errorf("failed to save aliases: %w", err)
103+
}
104+
105+
fmt.Printf("Alias '%s' created successfully\n", name)
106+
fmt.Printf(" Alias: %s\n", name)
107+
fmt.Printf(" Agent: %s\n", absAgentPath)
108+
fmt.Printf("\nYou can now run: cagent run %s\n", name)
109+
110+
return nil
111+
}
112+
113+
// removeAlias removes an alias
114+
func removeAlias(name string) error {
115+
s, err := aliases.Load()
116+
if err != nil {
117+
return fmt.Errorf("failed to load aliases: %w", err)
118+
}
119+
120+
if !s.Delete(name) {
121+
return fmt.Errorf("alias '%s' not found", name)
122+
}
123+
124+
if err := s.Save(); err != nil {
125+
return fmt.Errorf("failed to save aliases: %w", err)
126+
}
127+
128+
fmt.Printf("Alias '%s' removed successfully\n", name)
129+
return nil
130+
}
131+
132+
func listAliases() error {
133+
s, err := aliases.Load()
134+
if err != nil {
135+
return fmt.Errorf("failed to load aliases: %w", err)
136+
}
137+
138+
allAliases := s.List()
139+
if len(allAliases) == 0 {
140+
fmt.Println("No aliases registered.")
141+
fmt.Println("\nCreate an alias with: cagent alias add <name> <agent-path>")
142+
return nil
143+
}
144+
145+
fmt.Printf("Registered aliases (%d):\n\n", len(allAliases))
146+
147+
// Sort aliases by name for consistent output
148+
names := make([]string, 0, len(allAliases))
149+
for name := range allAliases {
150+
names = append(names, name)
151+
}
152+
sort.Strings(names)
153+
154+
// Find max name length for alignment
155+
maxLen := 0
156+
for _, name := range names {
157+
if len(name) > maxLen {
158+
maxLen = len(name)
159+
}
160+
}
161+
162+
for _, name := range names {
163+
path := allAliases[name]
164+
padding := strings.Repeat(" ", maxLen-len(name))
165+
fmt.Printf(" %s%s → %s\n", name, padding, path)
166+
}
167+
168+
fmt.Printf("\nRun an alias with: cagent run <alias>\n")
169+
170+
return nil
171+
}
172+
173+
// expandTilde expands the tilde in a path to the user's home directory
174+
func expandTilde(path string) (string, error) {
175+
if !strings.HasPrefix(path, "~/") {
176+
return path, nil
177+
}
178+
179+
homeDir, err := os.UserHomeDir()
180+
if err != nil {
181+
return "", err
182+
}
183+
184+
return filepath.Join(homeDir, strings.TrimPrefix(path, "~/")), nil
185+
}

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: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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 map[string]string
15+
16+
func aliasesFilePath() string {
17+
return filepath.Join(paths.GetConfigDir(), "aliases.yaml")
18+
}
19+
20+
// Load loads aliases from the configuration file
21+
func Load() (*Aliases, error) {
22+
return loadFrom(aliasesFilePath())
23+
}
24+
25+
// loadFrom loads aliases from a specific file path
26+
func loadFrom(path string) (*Aliases, error) {
27+
data, err := os.ReadFile(path)
28+
if err != nil {
29+
if os.IsNotExist(err) {
30+
return &Aliases{}, nil
31+
}
32+
return nil, fmt.Errorf("failed to read aliases file: %w", err)
33+
}
34+
35+
s := Aliases{}
36+
if err := yaml.Unmarshal(data, &s); err != nil {
37+
return nil, fmt.Errorf("failed to parse aliases file: %w", err)
38+
}
39+
40+
return &s, nil
41+
}
42+
43+
// Save saves aliases to the configuration file
44+
func (s *Aliases) Save() error {
45+
return s.saveTo(aliasesFilePath())
46+
}
47+
48+
// saveTo saves aliases to a specific file path
49+
func (s *Aliases) saveTo(path string) error {
50+
// Ensure directory exists
51+
dir := filepath.Dir(path)
52+
if err := os.MkdirAll(dir, 0o755); err != nil {
53+
return fmt.Errorf("failed to create directory: %w", err)
54+
}
55+
56+
data, err := yaml.Marshal(s)
57+
if err != nil {
58+
return fmt.Errorf("failed to marshal aliases: %w", err)
59+
}
60+
61+
if err := os.WriteFile(path, data, 0o644); err != nil {
62+
return fmt.Errorf("failed to write aliases file: %w", err)
63+
}
64+
65+
return nil
66+
}
67+
68+
// Get retrieves the agent path for an alias
69+
func (s *Aliases) Get(name string) (string, bool) {
70+
path, ok := (*s)[name]
71+
return path, ok
72+
}
73+
74+
// Set creates or updates an alias
75+
func (s *Aliases) Set(name, agentPath string) {
76+
(*s)[name] = agentPath
77+
}
78+
79+
// Delete removes an alias
80+
func (s *Aliases) Delete(name string) bool {
81+
if _, exists := (*s)[name]; exists {
82+
delete(*s, name)
83+
return true
84+
}
85+
return false
86+
}
87+
88+
// List returns all aliases
89+
func (s *Aliases) List() map[string]string {
90+
return *s
91+
}

0 commit comments

Comments
 (0)