Skip to content
Merged
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
71 changes: 41 additions & 30 deletions cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type rootFlags struct {
logFile io.Closer
}

func isCliPLugin() bool {
func isDockerAgent() bool {
cliPluginBinary := "docker-agent"
if runtime.GOOS == "windows" {
cliPluginBinary += ".exe"
Expand Down Expand Up @@ -110,7 +110,7 @@ func NewRootCmd() *cobra.Command {
cmd.AddGroup(&cobra.Group{ID: "core", Title: "Core Commands:"})
cmd.AddGroup(&cobra.Group{ID: "advanced", Title: "Advanced Commands:"})

if isCliPLugin() {
if isDockerAgent() {
cmd.Use = "agent"
cmd.Short = "create or run AI agents"
cmd.Long = "create or run AI agents"
Expand Down Expand Up @@ -147,44 +147,55 @@ We collect anonymous usage data to help improve cagent. To disable:
}

rootCmd := NewRootCmd()

// When no subcommand is given, default to "run" (which runs the default agent).
// Users can use "cagent --help" to see available commands.
args = defaultToRun(rootCmd, args)

rootCmd.SetArgs(args)
rootCmd.SetIn(stdin)
rootCmd.SetOut(stdout)
rootCmd.SetErr(stderr)
setContextRecursive(ctx, rootCmd)

if isCliPLugin() {
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
originalPreRun := rootCmd.PersistentPreRunE
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
return err
}
if originalPreRun != nil {
if err := originalPreRun(cmd, args); err != nil {
return processErr(ctx, err, stderr, rootCmd)
}
if plugin.RunningStandalone() {
// When no subcommand is given, default to "run".
rootCmd.SetArgs(defaultToRun(rootCmd, args))

if err := rootCmd.Execute(); err != nil {
return processErr(ctx, err, stderr, rootCmd)
}
return nil
}

// When no subcommand is given, default to "run".
rootCmd.SetArgs(append(args[0:1], defaultToRun(rootCmd, args[1:])...))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asymmetric Argument Handling

There's an asymmetry in how arguments are processed for rootCmd.SetArgs() vs os.Args:

  • Line 166: Preserves args[0:1] (1 element) and processes args[1:]
  • Line 167: Preserves os.Args[0:2] (2 elements) and processes os.Args[2:]

This creates different slicing offsets. For example:

  • If args = ["docker-agent", "myfile.yaml"] and os.Args = ["docker", "agent", "myfile.yaml"]
  • After processing:
    • rootCmd gets: ["docker-agent", "run", "myfile.yaml"]
    • os.Args becomes: ["docker", "agent", "run", "myfile.yaml"]

This divergence could cause issues if the Docker plugin framework or other code reads os.Args directly after this point. Could you confirm this asymmetry is intentional? If so, consider adding a comment explaining why args and os.Args need different handling.

os.Args = append(os.Args[0:2], defaultToRun(rootCmd, os.Args[2:])...)

plugin.Run(func(dockerCli command.Cli) *cobra.Command {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing Error Handling

The plugin.Run() function returns an error (signature: func Run(fn func(Cli) *cobra.Command, meta Metadata) error), but this return value is not being captured or handled. This means any errors that occur during plugin execution will be silently swallowed, and the function will unconditionally return nil on line 189.

In contrast, the standalone mode path (lines 155-159) properly checks and handles errors:

if err := rootCmd.Execute(); err != nil {
    return processErr(ctx, err, stderr, rootCmd)
}

Consider wrapping the plugin.Run() call:

return plugin.Run(func(dockerCli command.Cli) *cobra.Command {
    // ... existing callback code ...
}, metadata.Metadata{
    // ... existing metadata ...
})

This ensures error conditions are properly propagated to the caller.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plugin.Run() does not return an error, it directly os.exit(x) after displaying errors on stderr.

originalPreRun := rootCmd.PersistentPreRunE
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
return err
}
if originalPreRun != nil {
if err := originalPreRun(cmd, args); err != nil {
return processErr(cmd.Context(), err, stderr, rootCmd)
}
return nil
}
rootCmd.SetContext(ctx)
return rootCmd
}, metadata.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: version.Version,
})
} else if err := rootCmd.ExecuteContext(ctx); err != nil {
return processErr(ctx, err, stderr, rootCmd)
}
return nil
}
return rootCmd
}, metadata.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: version.Version,
})

return nil
}

func setContextRecursive(ctx context.Context, cmd *cobra.Command) {
cmd.SetContext(ctx)
for _, child := range cmd.Commands() {
setContextRecursive(ctx, child)
}
}

// defaultToRun prepends "run" to the argument list when no subcommand is
// specified so that bare "cagent" (or "cagent --debug", etc.) launches the
// default agent. Help flags (--help / -h) are left alone.
Expand Down