// Package command owns the narrow Cobra surface that exposes the agent runtime // to a headless operator. This file constructs the root command, registers // flags, and wires every subcommand to a CommandService. // // The root command accepts --repo-config, --local-config, and --provider-catalog // flags. Every subcommand builds a request DTO, dispatches it through the // Service port, and prints the response DTO in text or JSON form. package command import ( "fmt" "io" "github.com/spf13/cobra" ) // NewRoot constructs the Cobra root command with all subcommands and flags. // It accepts a Service implementation so the command tree can be driven by // either a live runtime adapter or a recording fake in tests. func NewRoot(svc Service, stdout, stderr io.Writer) *cobra.Command { root := &cobra.Command{ Use: "iop-agent", Short: "IOP agent headless runtime CLI", Long: "Headless CLI for the IOP agent runtime. Provides validate, list, preview, serve, start, stop, resume, and status commands over narrow host ports.", SilenceErrors: true, SilenceUsage: true, TraverseChildren: true, } var ( repoConfig string localConfig string providerCat string outputFormat string ) root.PersistentFlags().StringVar(&repoConfig, "repo-config", "", "path to the repo-global runtime config (required for mutating commands)") root.PersistentFlags().StringVar(&localConfig, "local-config", "", "path to the user-local runtime config (required for mutating commands)") root.PersistentFlags().StringVar(&providerCat, "provider-catalog", "", "path to the provider catalog YAML (required for mutating commands)") root.PersistentFlags().StringVar(&outputFormat, "output", "text", "output format: text or json") root.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { return validateOutputFormat(outputFormat) } root.SetOut(stdout) root.SetErr(stderr) // validate root.AddCommand(newValidateCmd(svc, &repoConfig, &localConfig, &providerCat, &outputFormat)) // provider group providerCmd := &cobra.Command{ Use: "provider", Short: "Provider catalog commands", } providerCmd.AddCommand(newProviderListCmd(svc, &providerCat, &outputFormat)) root.AddCommand(providerCmd) // project group projectCmd := &cobra.Command{ Use: "project", Short: "Project commands", } projectCmd.AddCommand(newProjectListCmd(svc, &repoConfig, &localConfig, &providerCat, &outputFormat)) root.AddCommand(projectCmd) // milestone group milestoneCmd := &cobra.Command{ Use: "milestone", Short: "Milestone commands", } milestoneCmd.AddCommand(newMilestoneListCmd(svc, &repoConfig, &localConfig, &providerCat, &outputFormat)) milestoneCmd.AddCommand(newMilestoneSelectCmd(svc, &repoConfig, &localConfig, &providerCat, &outputFormat)) root.AddCommand(milestoneCmd) // preview root.AddCommand(newPreviewCmd(svc, &repoConfig, &localConfig, &providerCat, &outputFormat)) // serve root.AddCommand(newServeCmd(svc, &repoConfig, &localConfig, &providerCat)) // start root.AddCommand(newStartCmd(svc, &repoConfig, &localConfig, &providerCat, &outputFormat)) // stop root.AddCommand(newStopCmd(svc, &repoConfig, &localConfig, &providerCat, &outputFormat)) // resume root.AddCommand(newResumeCmd(svc, &repoConfig, &localConfig, &providerCat, &outputFormat)) // status root.AddCommand(newStatusCmd(svc, &repoConfig, &localConfig, &providerCat, &outputFormat)) // task-loop root.AddCommand(newTaskLoopCmd(svc, &repoConfig, &localConfig, &providerCat, &outputFormat)) return root } func validateOutputFormat(format string) error { if format != "text" && format != "json" { return fmt.Errorf("unsupported output format: %s", format) } return nil } func newValidateCmd(svc Service, repoConfig, localConfig, providerCat, outputFormat *string) *cobra.Command { return &cobra.Command{ Use: "validate", Short: "Validate configuration paths", Long: "Load and validate the repo-global, user-local, and provider catalog configurations. Does not mutate any state.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { req := ValidateRequest{ RepoGlobalPath: *repoConfig, UserLocalPath: *localConfig, ProviderCatalog: *providerCat, } resp, err := svc.Validate(cmd.Context(), req) if err != nil { return fmt.Errorf("validate: %w", err) } return printOutput(cmd, resp, *outputFormat) }, } } func newProviderListCmd(svc Service, providerCat, outputFormat *string) *cobra.Command { return &cobra.Command{ Use: "list", Short: "List providers from the catalog", Long: "Enumerate providers declared in the provider catalog. Does not mutate any state.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { req := ProviderListRequest{ CatalogPath: *providerCat, } resp, err := svc.ProviderList(cmd.Context(), req) if err != nil { return fmt.Errorf("provider list: %w", err) } return printOutput(cmd, resp, *outputFormat) }, } } func newProjectListCmd(svc Service, repoConfig, localConfig, providerCatalog, outputFormat *string) *cobra.Command { return &cobra.Command{ Use: "list", Short: "List registered projects", Long: "Enumerate registered projects and their Milestone selection state from the loaded runtime snapshot.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { req := ProjectListRequest{ Config: RuntimeConfigPaths{ RepoGlobalPath: *repoConfig, UserLocalPath: *localConfig, }, } resp, err := svc.ProjectList(cmd.Context(), req) if err != nil { return fmt.Errorf("project list: %w", err) } return printOutput(cmd, resp, *outputFormat) }, } } func newMilestoneListCmd(svc Service, repoConfig, localConfig, providerCatalog, outputFormat *string) *cobra.Command { return &cobra.Command{ Use: "list [project]", Short: "List milestones for a project", Long: "List selectable milestones for the supplied project.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { req := MilestoneListRequest{ Config: RuntimeConfigPaths{ RepoGlobalPath: *repoConfig, UserLocalPath: *localConfig, }, Project: args[0], } resp, err := svc.MilestoneList(cmd.Context(), req) if err != nil { return fmt.Errorf("milestone list: %w", err) } return printOutput(cmd, resp, *outputFormat) }, } } func newMilestoneSelectCmd(svc Service, repoConfig, localConfig, providerCatalog, outputFormat *string) *cobra.Command { return &cobra.Command{ Use: "select [project] [milestone]", Short: "Select a Milestone for a project", Long: "Record the operator's explicit Milestone choice for the supplied project. Start rejects a project without an explicitly selected Milestone.", Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { req := MilestoneSelectRequest{ Config: RuntimeConfigPaths{ RepoGlobalPath: *repoConfig, UserLocalPath: *localConfig, }, Project: args[0], MilestoneID: args[1], } resp, err := svc.MilestoneSelect(cmd.Context(), req) if err != nil { return fmt.Errorf("milestone select: %w", err) } return printOutput(cmd, resp, *outputFormat) }, } } func newPreviewCmd(svc Service, repoConfig, localConfig, providerCatalog, outputFormat *string) *cobra.Command { return &cobra.Command{ Use: "preview [project]", Short: "Preview selection and dependency verdict", Long: "Run the same selection and dependency logic as start but do not mutate durable state, start processes, or acquire leases. Does not call any mutation method.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { req := PreviewRequest{ Config: RuntimeConfigPaths{ RepoGlobalPath: *repoConfig, UserLocalPath: *localConfig, }, Project: args[0], } resp, err := svc.Preview(cmd.Context(), req) if err != nil { return fmt.Errorf("preview: %w", err) } return printOutput(cmd, resp, *outputFormat) }, } } func newServeCmd(svc Service, repoConfig, localConfig, providerCat *string) *cobra.Command { return &cobra.Command{ Use: "serve", Short: "Run the agent runtime lifecycle", Long: "Block until interrupted and drive the runtime lifecycle. This is the only command that performs sustained mutation.", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { req := ServeRequest{ RepoGlobalPath: *repoConfig, UserLocalPath: *localConfig, ProviderCatalog: *providerCat, } return svc.Serve(cmd.Context(), req) }, } } func newStartCmd(svc Service, repoConfig, localConfig, providerCatalog, outputFormat *string) *cobra.Command { return &cobra.Command{ Use: "start [project]", Short: "Start a project manually", Long: "Record a manual start intent for the supplied project. Rejects projects whose selected milestone is empty.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { req := StartRequest{ Config: RuntimeConfigPaths{ RepoGlobalPath: *repoConfig, UserLocalPath: *localConfig, ProviderCatalog: *providerCatalog, }, Project: args[0], } resp, err := svc.Start(cmd.Context(), req) if err != nil { return fmt.Errorf("start: %w", err) } return printOutput(cmd, resp, *outputFormat) }, } } func newStopCmd(svc Service, repoConfig, localConfig, providerCatalog, outputFormat *string) *cobra.Command { return &cobra.Command{ Use: "stop [project]", Short: "Stop a project manually", Long: "Record a manual stop intent for the supplied project.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { req := StopRequest{ Config: RuntimeConfigPaths{ RepoGlobalPath: *repoConfig, UserLocalPath: *localConfig, ProviderCatalog: *providerCatalog, }, Project: args[0], } resp, err := svc.Stop(cmd.Context(), req) if err != nil { return fmt.Errorf("stop: %w", err) } return printOutput(cmd, resp, *outputFormat) }, } } func newResumeCmd(svc Service, repoConfig, localConfig, providerCatalog, outputFormat *string) *cobra.Command { return &cobra.Command{ Use: "resume [project]", Short: "Resume a stopped project", Long: "Record a manual resume intent for the supplied project.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { req := ResumeRequest{ Config: RuntimeConfigPaths{ RepoGlobalPath: *repoConfig, UserLocalPath: *localConfig, ProviderCatalog: *providerCatalog, }, Project: args[0], } resp, err := svc.Resume(cmd.Context(), req) if err != nil { return fmt.Errorf("resume: %w", err) } return printOutput(cmd, resp, *outputFormat) }, } } func newStatusCmd(svc Service, repoConfig, localConfig, providerCatalog, outputFormat *string) *cobra.Command { return &cobra.Command{ Use: "status [project]", Short: "Show project status", Long: "Show the live project status including ordered per-work state, dispatch ordinals, overlay/integration state, and blockers.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { req := StatusRequest{ Config: RuntimeConfigPaths{ RepoGlobalPath: *repoConfig, UserLocalPath: *localConfig, }, Project: args[0], } resp, err := svc.Status(cmd.Context(), req) if err != nil { return fmt.Errorf("status: %w", err) } return printOutput(cmd, resp, *outputFormat) }, } } // printOutput dispatches the response DTO to stdout in the requested format. func printOutput(cmd *cobra.Command, resp interface{}, format string) error { if err := validateOutputFormat(format); err != nil { return err } out := cmd.OutOrStdout() if format == "json" { if j, ok := resp.(interface{ FormatJSON() string }); ok { _, err := fmt.Fprintln(out, j.FormatJSON()) return err } } if v, ok := resp.(interface{ FormatText() string }); ok { _, err := fmt.Fprint(out, v.FormatText()) return err } return fmt.Errorf("unsupported response type: %T", resp) }