Skip to content

Commit cebeb4f

Browse files
committed
Add the please command
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
1 parent f93dca4 commit cebeb4f

5 files changed

Lines changed: 447 additions & 0 deletions

File tree

cmd/root/alias.go

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

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
@@ -28,6 +28,7 @@ import (
2828
"github.com/docker/cagent/pkg/remote"
2929
"github.com/docker/cagent/pkg/runtime"
3030
"github.com/docker/cagent/pkg/session"
31+
"github.com/docker/cagent/pkg/shortcuts"
3132
"github.com/docker/cagent/pkg/team"
3233
"github.com/docker/cagent/pkg/teamloader"
3334
"github.com/docker/cagent/pkg/telemetry"
@@ -94,6 +95,15 @@ func doRunCommand(ctx context.Context, args []string, exec bool) error {
9495
slog.Debug("Starting agent", "agent", agentName, "debug_mode", debugMode)
9596

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

pkg/shortcuts/shortcuts.go

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

0 commit comments

Comments
 (0)