refactor(cli): improve help UX — add examples support, show help on arg errors

- Add EXAMPLES section to leaf and sub help templates (gh CLI style)
- Add example to attachment download command
- Simplify attachment download description
- Show help output when required args are missing (error first, then help)
- Replace cobra.ExactArgs with custom exactArgs that prints help on failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yushen 2026-04-05 07:00:19 +08:00
parent d6a5ba4d5e
commit 4c0dbbf1c8
10 changed files with 70 additions and 33 deletions

View file

@ -29,7 +29,7 @@ var agentListCmd = &cobra.Command{
var agentGetCmd = &cobra.Command{ var agentGetCmd = &cobra.Command{
Use: "get <id>", Use: "get <id>",
Short: "Get agent details", Short: "Get agent details",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runAgentGet, RunE: runAgentGet,
} }
@ -42,28 +42,28 @@ var agentCreateCmd = &cobra.Command{
var agentUpdateCmd = &cobra.Command{ var agentUpdateCmd = &cobra.Command{
Use: "update <id>", Use: "update <id>",
Short: "Update an agent", Short: "Update an agent",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runAgentUpdate, RunE: runAgentUpdate,
} }
var agentArchiveCmd = &cobra.Command{ var agentArchiveCmd = &cobra.Command{
Use: "archive <id>", Use: "archive <id>",
Short: "Archive an agent", Short: "Archive an agent",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runAgentArchive, RunE: runAgentArchive,
} }
var agentRestoreCmd = &cobra.Command{ var agentRestoreCmd = &cobra.Command{
Use: "restore <id>", Use: "restore <id>",
Short: "Restore an archived agent", Short: "Restore an archived agent",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runAgentRestore, RunE: runAgentRestore,
} }
var agentTasksCmd = &cobra.Command{ var agentTasksCmd = &cobra.Command{
Use: "tasks <id>", Use: "tasks <id>",
Short: "List tasks for an agent", Short: "List tasks for an agent",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runAgentTasks, RunE: runAgentTasks,
} }
@ -77,14 +77,14 @@ var agentSkillsCmd = &cobra.Command{
var agentSkillsListCmd = &cobra.Command{ var agentSkillsListCmd = &cobra.Command{
Use: "list <agent-id>", Use: "list <agent-id>",
Short: "List skills assigned to an agent", Short: "List skills assigned to an agent",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runAgentSkillsList, RunE: runAgentSkillsList,
} }
var agentSkillsSetCmd = &cobra.Command{ var agentSkillsSetCmd = &cobra.Command{
Use: "set <agent-id>", Use: "set <agent-id>",
Short: "Set skills for an agent (replaces all current assignments)", Short: "Set skills for an agent (replaces all current assignments)",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runAgentSkillsSet, RunE: runAgentSkillsSet,
} }

View file

@ -20,8 +20,13 @@ var attachmentCmd = &cobra.Command{
var attachmentDownloadCmd = &cobra.Command{ var attachmentDownloadCmd = &cobra.Command{
Use: "download <attachment-id>", Use: "download <attachment-id>",
Short: "Download an attachment to a local file", Short: "Download an attachment to a local file",
Long: "Fetches the attachment metadata from the API, then downloads the file using its signed URL. Prints the local file path on success.", Long: "Download an attachment by its ID to a local file.",
Args: cobra.ExactArgs(1), Example: ` # Download an image attachment to the current directory
$ multica attachment download abc123
# Download to a specific directory
$ multica attachment download abc123 -o /tmp/images`,
Args: exactArgs(1),
RunE: runAttachmentDownload, RunE: runAttachmentDownload,
} }

View file

@ -25,7 +25,7 @@ var configSetCmd = &cobra.Command{
Use: "set <key> <value>", Use: "set <key> <value>",
Short: "Set a CLI configuration value", Short: "Set a CLI configuration value",
Long: "Supported keys: server_url, app_url, workspace_id", Long: "Supported keys: server_url, app_url, workspace_id",
Args: cobra.ExactArgs(2), Args: exactArgs(2),
RunE: runConfigSet, RunE: runConfigSet,
} }

View file

@ -28,7 +28,7 @@ var issueListCmd = &cobra.Command{
var issueGetCmd = &cobra.Command{ var issueGetCmd = &cobra.Command{
Use: "get <id>", Use: "get <id>",
Short: "Get issue details", Short: "Get issue details",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runIssueGet, RunE: runIssueGet,
} }
@ -41,21 +41,21 @@ var issueCreateCmd = &cobra.Command{
var issueUpdateCmd = &cobra.Command{ var issueUpdateCmd = &cobra.Command{
Use: "update <id>", Use: "update <id>",
Short: "Update an issue", Short: "Update an issue",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runIssueUpdate, RunE: runIssueUpdate,
} }
var issueAssignCmd = &cobra.Command{ var issueAssignCmd = &cobra.Command{
Use: "assign <id>", Use: "assign <id>",
Short: "Assign an issue to a member or agent", Short: "Assign an issue to a member or agent",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runIssueAssign, RunE: runIssueAssign,
} }
var issueStatusCmd = &cobra.Command{ var issueStatusCmd = &cobra.Command{
Use: "status <id> <status>", Use: "status <id> <status>",
Short: "Change issue status", Short: "Change issue status",
Args: cobra.ExactArgs(2), Args: exactArgs(2),
RunE: runIssueStatus, RunE: runIssueStatus,
} }
@ -69,21 +69,21 @@ var issueCommentCmd = &cobra.Command{
var issueCommentListCmd = &cobra.Command{ var issueCommentListCmd = &cobra.Command{
Use: "list <issue-id>", Use: "list <issue-id>",
Short: "List comments on an issue", Short: "List comments on an issue",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runIssueCommentList, RunE: runIssueCommentList,
} }
var issueCommentAddCmd = &cobra.Command{ var issueCommentAddCmd = &cobra.Command{
Use: "add <issue-id>", Use: "add <issue-id>",
Short: "Add a comment to an issue", Short: "Add a comment to an issue",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runIssueCommentAdd, RunE: runIssueCommentAdd,
} }
var issueCommentDeleteCmd = &cobra.Command{ var issueCommentDeleteCmd = &cobra.Command{
Use: "delete <comment-id>", Use: "delete <comment-id>",
Short: "Delete a comment", Short: "Delete a comment",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runIssueCommentDelete, RunE: runIssueCommentDelete,
} }
@ -92,14 +92,14 @@ var issueCommentDeleteCmd = &cobra.Command{
var issueRunsCmd = &cobra.Command{ var issueRunsCmd = &cobra.Command{
Use: "runs <issue-id>", Use: "runs <issue-id>",
Short: "List execution history for an issue", Short: "List execution history for an issue",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runIssueRuns, RunE: runIssueRuns,
} }
var issueRunMessagesCmd = &cobra.Command{ var issueRunMessagesCmd = &cobra.Command{
Use: "run-messages <task-id>", Use: "run-messages <task-id>",
Short: "List messages for an execution", Short: "List messages for an execution",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runIssueRunMessages, RunE: runIssueRunMessages,
} }

View file

@ -21,7 +21,7 @@ var repoCheckoutCmd = &cobra.Command{
Use: "checkout <url>", Use: "checkout <url>",
Short: "Check out a repository into the working directory", Short: "Check out a repository into the working directory",
Long: "Creates a git worktree from the daemon's bare clone cache. Used by agents to check out repos on demand.", Long: "Creates a git worktree from the daemon's bare clone cache. Used by agents to check out repos on demand.",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runRepoCheckout, RunE: runRepoCheckout,
} }

View file

@ -25,28 +25,28 @@ var runtimeListCmd = &cobra.Command{
var runtimeUsageCmd = &cobra.Command{ var runtimeUsageCmd = &cobra.Command{
Use: "usage <runtime-id>", Use: "usage <runtime-id>",
Short: "Get token usage for a runtime", Short: "Get token usage for a runtime",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runRuntimeUsage, RunE: runRuntimeUsage,
} }
var runtimeActivityCmd = &cobra.Command{ var runtimeActivityCmd = &cobra.Command{
Use: "activity <runtime-id>", Use: "activity <runtime-id>",
Short: "Get hourly task activity for a runtime", Short: "Get hourly task activity for a runtime",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runRuntimeActivity, RunE: runRuntimeActivity,
} }
var runtimePingCmd = &cobra.Command{ var runtimePingCmd = &cobra.Command{
Use: "ping <runtime-id>", Use: "ping <runtime-id>",
Short: "Ping a runtime to check connectivity", Short: "Ping a runtime to check connectivity",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runRuntimePing, RunE: runRuntimePing,
} }
var runtimeUpdateCmd = &cobra.Command{ var runtimeUpdateCmd = &cobra.Command{
Use: "update <runtime-id>", Use: "update <runtime-id>",
Short: "Initiate a CLI update on a runtime", Short: "Initiate a CLI update on a runtime",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runRuntimeUpdate, RunE: runRuntimeUpdate,
} }

View file

@ -28,7 +28,7 @@ var skillListCmd = &cobra.Command{
var skillGetCmd = &cobra.Command{ var skillGetCmd = &cobra.Command{
Use: "get <id>", Use: "get <id>",
Short: "Get skill details (includes files)", Short: "Get skill details (includes files)",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runSkillGet, RunE: runSkillGet,
} }
@ -41,14 +41,14 @@ var skillCreateCmd = &cobra.Command{
var skillUpdateCmd = &cobra.Command{ var skillUpdateCmd = &cobra.Command{
Use: "update <id>", Use: "update <id>",
Short: "Update a skill", Short: "Update a skill",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runSkillUpdate, RunE: runSkillUpdate,
} }
var skillDeleteCmd = &cobra.Command{ var skillDeleteCmd = &cobra.Command{
Use: "delete <id>", Use: "delete <id>",
Short: "Delete a skill", Short: "Delete a skill",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runSkillDelete, RunE: runSkillDelete,
} }
@ -68,21 +68,21 @@ var skillFilesCmd = &cobra.Command{
var skillFilesListCmd = &cobra.Command{ var skillFilesListCmd = &cobra.Command{
Use: "list <skill-id>", Use: "list <skill-id>",
Short: "List files for a skill", Short: "List files for a skill",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runSkillFilesList, RunE: runSkillFilesList,
} }
var skillFilesUpsertCmd = &cobra.Command{ var skillFilesUpsertCmd = &cobra.Command{
Use: "upsert <skill-id>", Use: "upsert <skill-id>",
Short: "Create or update a skill file", Short: "Create or update a skill file",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runSkillFilesUpsert, RunE: runSkillFilesUpsert,
} }
var skillFilesDeleteCmd = &cobra.Command{ var skillFilesDeleteCmd = &cobra.Command{
Use: "delete <skill-id> <file-id>", Use: "delete <skill-id> <file-id>",
Short: "Delete a skill file", Short: "Delete a skill file",
Args: cobra.ExactArgs(2), Args: exactArgs(2),
RunE: runSkillFilesDelete, RunE: runSkillFilesDelete,
} }

View file

@ -41,14 +41,14 @@ var workspaceMembersCmd = &cobra.Command{
var workspaceWatchCmd = &cobra.Command{ var workspaceWatchCmd = &cobra.Command{
Use: "watch <workspace-id>", Use: "watch <workspace-id>",
Short: "Add a workspace to the daemon watch list", Short: "Add a workspace to the daemon watch list",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runWatch, RunE: runWatch,
} }
var workspaceUnwatchCmd = &cobra.Command{ var workspaceUnwatchCmd = &cobra.Command{
Use: "unwatch <workspace-id>", Use: "unwatch <workspace-id>",
Short: "Remove a workspace from the daemon watch list", Short: "Remove a workspace from the daemon watch list",
Args: cobra.ExactArgs(1), Args: exactArgs(1),
RunE: runUnwatch, RunE: runUnwatch,
} }

View file

@ -15,6 +15,26 @@ const (
groupAdditional = "additional" groupAdditional = "additional"
) )
// errSilent is returned when the error message has already been printed.
var errSilent = fmt.Errorf("")
// exactArgs returns a cobra.PositionalArgs that validates the arg count
// and prints help on failure, so users see usage context with the error.
func exactArgs(n int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) != n {
if n == 1 {
fmt.Fprintf(cmd.ErrOrStderr(), "Error: accepts 1 arg, received %d\n\n", len(args))
} else {
fmt.Fprintf(cmd.ErrOrStderr(), "Error: accepts %d args, received %d\n\n", n, len(args))
}
cmd.Help()
return errSilent
}
return nil
}
}
// initHelp configures the root command to use gh-style help output. // initHelp configures the root command to use gh-style help output.
func initHelp(root *cobra.Command) { func initHelp(root *cobra.Command) {
root.SetHelpTemplate(rootHelpTemplate) root.SetHelpTemplate(rootHelpTemplate)
@ -120,6 +140,11 @@ COMMANDS
{{formatCommandList .Commands}} {{formatCommandList .Commands}}
INHERITED FLAGS INHERITED FLAGS
--help Show help for command --help Show help for command
{{- if .Example}}
EXAMPLES
{{.Example}}
{{- end}}
LEARN MORE LEARN MORE
Use ` + "`{{.CommandPath}} <command> --help`" + ` for more information about a command. Use ` + "`{{.CommandPath}} <command> --help`" + ` for more information about a command.
@ -136,6 +161,11 @@ FLAGS
{{- end}} {{- end}}
INHERITED FLAGS INHERITED FLAGS
--help Show help for command --help Show help for command
{{- if .Example}}
EXAMPLES
{{.Example}}
{{- end}}
LEARN MORE LEARN MORE
Use ` + "`multica <command> <subcommand> --help`" + ` for more information about a command. Use ` + "`multica <command> <subcommand> --help`" + ` for more information about a command.

View file

@ -63,7 +63,9 @@ func init() {
func main() { func main() {
if err := rootCmd.Execute(); err != nil { if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, "Error:", err) if err != errSilent {
fmt.Fprintln(os.Stderr, "Error:", err)
}
os.Exit(1) os.Exit(1)
} }
} }