리뷰에서 확인된 Milestone 식별자와 컴파일 바이너리 상태 검증의 빈틈을 보완하고, 활성 agent-task에서는 dispatcher만 실행 경로로 사용하도록 고정한다.
369 lines
12 KiB
Go
369 lines
12 KiB
Go
// 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)
|
|
}
|