리뷰에서 확인된 Milestone 식별자와 컴파일 바이너리 상태 검증의 빈틈을 보완하고, 활성 agent-task에서는 dispatcher만 실행 경로로 사용하도록 고정한다.
736 lines
22 KiB
Go
736 lines
22 KiB
Go
// Package command owns the narrow Cobra surface that exposes the agent runtime
|
|
// to a headless operator. It declares request/response DTOs and a service port
|
|
// so the root command can dispatch without depending on concrete task manager,
|
|
// config, or provider implementations.
|
|
//
|
|
// Every command builds a request DTO, hands it to a CommandService, and prints
|
|
// the response DTO. The service port is the only mutation boundary; preview
|
|
// deliberately cannot invoke mutation methods.
|
|
package command
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
// Service is the narrow application-facing port for the CLI command surface.
|
|
// Implementations supply configuration loading, provider discovery, project
|
|
// workflow observation, and task lifecycle control. Construction never starts
|
|
// or stops anything.
|
|
type Service interface {
|
|
// Validate returns a non-nil error when the supplied configuration paths
|
|
// cannot be loaded into a usable snapshot. It is side-effect free.
|
|
Validate(ctx context.Context, req ValidateRequest) (ValidateResponse, error)
|
|
|
|
// ProviderList returns the catalog entries registered under the supplied
|
|
// provider catalog path. It is side-effect free.
|
|
ProviderList(ctx context.Context, req ProviderListRequest) (ProviderListResponse, error)
|
|
|
|
// ProjectList returns registered projects and their Milestone metadata
|
|
// from the loaded runtime snapshot. It is side-effect free.
|
|
ProjectList(ctx context.Context, req ProjectListRequest) (ProjectListResponse, error)
|
|
|
|
// MilestoneList returns the workflow-backed Milestones discovered for the
|
|
// supplied project. It is side-effect free.
|
|
MilestoneList(ctx context.Context, req MilestoneListRequest) (MilestoneListResponse, error)
|
|
|
|
// MilestoneSelect records the user's explicit Milestone choice for the
|
|
// supplied project. Start rejects a project whose selected milestone is
|
|
// empty.
|
|
MilestoneSelect(ctx context.Context, req MilestoneSelectRequest) (MilestoneSelectResponse, error)
|
|
|
|
// Preview runs the same selection and dependency logic as start but
|
|
// returns a PreviewResponse without mutating durable state, starting
|
|
// processes, or acquiring leases.
|
|
Preview(ctx context.Context, req PreviewRequest) (PreviewResponse, error)
|
|
|
|
// Serve blocks until ctx is cancelled and drives the runtime lifecycle.
|
|
// It is the only command that performs sustained mutation.
|
|
Serve(ctx context.Context, req ServeRequest) error
|
|
|
|
// Start records a manual start intent for the supplied project. It
|
|
// rejects projects whose selected milestone is empty.
|
|
Start(ctx context.Context, req StartRequest) (StartResponse, error)
|
|
|
|
// Stop records a manual stop intent for the supplied project.
|
|
Stop(ctx context.Context, req StopRequest) (StopResponse, error)
|
|
|
|
// Resume records a manual resume intent for the supplied project.
|
|
Resume(ctx context.Context, req ResumeRequest) (ResumeResponse, error)
|
|
|
|
// Status returns the live project status including ordered per-work state,
|
|
// durable dispatch ordinals, overlay/integration state, and blockers.
|
|
Status(ctx context.Context, req StatusRequest) (StatusResponse, error)
|
|
|
|
// TaskLoop performs one bounded operator pass over the authoritative
|
|
// standalone runtime. Dry-run only reads projections; live operation may
|
|
// resume an explicitly requested blocked project and reconcile it once.
|
|
TaskLoop(ctx context.Context, req TaskLoopRequest) (TaskLoopResponse, error)
|
|
|
|
// TaskLoopParity validates and renders the S13 disposal manifest.
|
|
TaskLoopParity(ctx context.Context, output io.Writer) error
|
|
|
|
// ValidatePlan validates one candidate PLAN write set against a workspace.
|
|
// It is read-only and never constructs a runtime or provider.
|
|
ValidatePlan(ctx context.Context, req PlanValidationRequest) error
|
|
}
|
|
|
|
// RuntimeConfigPaths carries the repo-global and user-local configuration paths.
|
|
type RuntimeConfigPaths struct {
|
|
RepoGlobalPath string
|
|
UserLocalPath string
|
|
ProviderCatalog string
|
|
}
|
|
|
|
// ValidateRequest carries the configuration paths that the operator has
|
|
// supplied on the command line.
|
|
type ValidateRequest struct {
|
|
RepoGlobalPath string
|
|
UserLocalPath string
|
|
ProviderCatalog string
|
|
}
|
|
|
|
// ValidateResponse reports whether the supplied configuration loads into a
|
|
// usable snapshot.
|
|
type ValidateResponse struct {
|
|
Valid bool
|
|
Revision string
|
|
Projects int
|
|
Providers int
|
|
Profiles int
|
|
Errors []string
|
|
}
|
|
|
|
// ProviderListRequest carries the catalog path used to enumerate providers.
|
|
type ProviderListRequest struct {
|
|
CatalogPath string
|
|
}
|
|
|
|
// ProviderListResponse reports every provider declared in the catalog.
|
|
type ProviderListResponse struct {
|
|
Providers []ProviderEntry
|
|
}
|
|
|
|
// ProviderEntry is one catalog-declared provider identity.
|
|
type ProviderEntry struct {
|
|
ID string
|
|
Command string
|
|
Capabilities []string
|
|
}
|
|
|
|
// ProjectListRequest carries the runtime configuration paths used to enumerate projects.
|
|
type ProjectListRequest struct {
|
|
Config RuntimeConfigPaths
|
|
}
|
|
|
|
// ProjectListResponse reports every registered project and its Milestone
|
|
// selection state.
|
|
type ProjectListResponse struct {
|
|
Projects []ProjectEntry
|
|
}
|
|
|
|
// ProjectEntry is one registered project identity with its Milestone state.
|
|
type ProjectEntry struct {
|
|
ID string
|
|
Workspace string
|
|
Enabled bool
|
|
SelectedMilestone string
|
|
StartedMilestone string
|
|
AutoResumeInterrupt bool
|
|
}
|
|
|
|
// MilestoneListRequest carries the project identity used to list its
|
|
// workflow-backed Milestones.
|
|
type MilestoneListRequest struct {
|
|
Config RuntimeConfigPaths
|
|
Project string
|
|
}
|
|
|
|
// MilestoneListResponse reports every Milestone declared in the workspace for
|
|
// the supplied project.
|
|
type MilestoneListResponse struct {
|
|
Milestones []MilestoneEntry
|
|
}
|
|
|
|
// MilestoneEntry is one Milestone summary.
|
|
type MilestoneEntry struct {
|
|
ID string
|
|
Selected bool
|
|
WorkUnits int
|
|
CompletedWorkUnits int
|
|
}
|
|
|
|
// MilestoneSelectRequest records the operator's explicit Milestone choice.
|
|
type MilestoneSelectRequest struct {
|
|
Config RuntimeConfigPaths
|
|
Project string
|
|
MilestoneID string
|
|
}
|
|
|
|
// MilestoneSelectResponse reports the recorded selection.
|
|
type MilestoneSelectResponse struct {
|
|
Project string
|
|
MilestoneID string
|
|
}
|
|
|
|
// PreviewRequest carries the project identity used to run selection and
|
|
// dependency logic without mutation.
|
|
type PreviewRequest struct {
|
|
Config RuntimeConfigPaths
|
|
Project string
|
|
}
|
|
|
|
// PreviewResponse reports the selection and dependency verdict that start
|
|
// would apply. It must not call any mutation method on the backing service.
|
|
type PreviewResponse struct {
|
|
Project string
|
|
Selected bool
|
|
MilestoneID string
|
|
NextWork string
|
|
Blockers []BlockerEntry
|
|
}
|
|
|
|
// BlockerEntry is one active blocker code and human-readable message.
|
|
type BlockerEntry struct {
|
|
Code string
|
|
Message string
|
|
Retryable bool
|
|
}
|
|
|
|
// ServeRequest carries the configuration paths and runtime hooks used to
|
|
// drive the full lifecycle.
|
|
type ServeRequest struct {
|
|
RepoGlobalPath string
|
|
UserLocalPath string
|
|
ProviderCatalog string
|
|
}
|
|
|
|
// StartRequest carries the project identity used to record a manual start
|
|
// intent.
|
|
type StartRequest struct {
|
|
Config RuntimeConfigPaths
|
|
Project string
|
|
}
|
|
|
|
// StartResponse reports the recorded start intent.
|
|
type StartResponse struct {
|
|
Project string
|
|
Status string
|
|
}
|
|
|
|
// StopRequest carries the project identity used to record a manual stop
|
|
// intent.
|
|
type StopRequest struct {
|
|
Config RuntimeConfigPaths
|
|
Project string
|
|
}
|
|
|
|
// StopResponse reports the recorded stop intent.
|
|
type StopResponse struct {
|
|
Project string
|
|
Status string
|
|
}
|
|
|
|
// ResumeRequest carries the project identity used to record a manual resume
|
|
// intent.
|
|
type ResumeRequest struct {
|
|
Config RuntimeConfigPaths
|
|
Project string
|
|
}
|
|
|
|
// ResumeResponse reports the recorded resume intent.
|
|
type ResumeResponse struct {
|
|
Project string
|
|
Status string
|
|
}
|
|
|
|
// StatusRequest carries the project identity used to query live state.
|
|
type StatusRequest struct {
|
|
Config RuntimeConfigPaths
|
|
Project string
|
|
}
|
|
|
|
// StatusResponse reports the live project status including ordered per-work status
|
|
// entries and blockers.
|
|
type StatusResponse struct {
|
|
Project string
|
|
Status string
|
|
SelectedMilestone string
|
|
StartedMilestone string
|
|
Works []WorkStatusEntry
|
|
Blockers []BlockerEntry
|
|
}
|
|
|
|
// WorkStatusEntry is the status projection for one work unit.
|
|
type WorkStatusEntry struct {
|
|
ID string
|
|
State string
|
|
Overlay string
|
|
Integration string
|
|
DispatchOrdinal uint64
|
|
Blocker *BlockerEntry
|
|
}
|
|
|
|
// TaskLoopRequest is the bounded operator request used by the production
|
|
// orchestration skill. TaskGroup, when supplied, must be an exact m- prefixed
|
|
// selected-Milestone group; it never infers a dependency from numeric order.
|
|
type TaskLoopRequest struct {
|
|
Config RuntimeConfigPaths
|
|
TaskGroup string
|
|
DryRun bool
|
|
RetryBlocked bool
|
|
}
|
|
|
|
// PlanValidationRequest carries a candidate PLAN path and the workspace that
|
|
// bounds every declared write target. The candidate itself may be outside the
|
|
// workspace so plan/review rendering can use a temporary file.
|
|
type PlanValidationRequest struct {
|
|
Workspace string
|
|
PlanPath string
|
|
}
|
|
|
|
// TaskLoopResponse reports a single runtime pass. ExitCode is zero for a
|
|
// successful or dry-run pass and two only when the final reconciled selected
|
|
// scope is blocked with no started or running project.
|
|
type TaskLoopResponse struct {
|
|
DryRun bool
|
|
Projects []TaskLoopProject
|
|
ExitCode int
|
|
}
|
|
|
|
type TaskLoopProject struct {
|
|
Project string
|
|
Group string
|
|
Status string
|
|
NextWork string
|
|
Blockers []BlockerEntry
|
|
}
|
|
|
|
// FormatText renders a ValidateResponse as stable plain text.
|
|
func (r ValidateResponse) FormatText() string {
|
|
var b strings.Builder
|
|
if r.Valid {
|
|
b.WriteString("validate: ok\n")
|
|
} else {
|
|
b.WriteString("validate: FAIL\n")
|
|
}
|
|
b.WriteString(fmt.Sprintf(" revision: %s\n", r.Revision))
|
|
b.WriteString(fmt.Sprintf(" projects: %d\n", r.Projects))
|
|
b.WriteString(fmt.Sprintf(" providers: %d\n", r.Providers))
|
|
b.WriteString(fmt.Sprintf(" profiles: %d\n", r.Profiles))
|
|
for _, err := range r.Errors {
|
|
b.WriteString(fmt.Sprintf(" error: %s\n", err))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// FormatText renders a ProviderListResponse as stable plain text.
|
|
func (r ProviderListResponse) FormatText() string {
|
|
var b strings.Builder
|
|
b.WriteString(fmt.Sprintf("providers: %d\n", len(r.Providers)))
|
|
for _, p := range r.Providers {
|
|
b.WriteString(fmt.Sprintf(" - %s command=%s caps=%s\n", p.ID, p.Command, strings.Join(p.Capabilities, ",")))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// FormatText renders a ProjectListResponse as stable plain text.
|
|
func (r ProjectListResponse) FormatText() string {
|
|
var b strings.Builder
|
|
b.WriteString(fmt.Sprintf("projects: %d\n", len(r.Projects)))
|
|
for _, p := range r.Projects {
|
|
flag := "enabled"
|
|
if !p.Enabled {
|
|
flag = "disabled"
|
|
}
|
|
autoResume := "no"
|
|
if p.AutoResumeInterrupt {
|
|
autoResume = "yes"
|
|
}
|
|
b.WriteString(fmt.Sprintf(" - %s ws=%s %s selected=%q started=%q auto_resume=%s\n",
|
|
p.ID, p.Workspace, flag, p.SelectedMilestone, p.StartedMilestone, autoResume))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// FormatText renders a MilestoneListResponse as stable plain text.
|
|
func (r MilestoneListResponse) FormatText() string {
|
|
var b strings.Builder
|
|
b.WriteString(fmt.Sprintf("milestones: %d\n", len(r.Milestones)))
|
|
for _, m := range r.Milestones {
|
|
selected := "no"
|
|
if m.Selected {
|
|
selected = "yes"
|
|
}
|
|
b.WriteString(fmt.Sprintf(" - %s selected=%s work_units=%d completed=%d\n",
|
|
m.ID, selected, m.WorkUnits, m.CompletedWorkUnits))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// FormatText renders a MilestoneSelectResponse as stable plain text.
|
|
func (r MilestoneSelectResponse) FormatText() string {
|
|
return fmt.Sprintf("milestone select project=%s milestone=%s\n", r.Project, r.MilestoneID)
|
|
}
|
|
|
|
// FormatText renders a PreviewResponse as stable plain text.
|
|
func (r PreviewResponse) FormatText() string {
|
|
var b strings.Builder
|
|
b.WriteString(fmt.Sprintf("preview project=%s\n", r.Project))
|
|
if r.Selected {
|
|
b.WriteString(fmt.Sprintf(" selected: %s\n", r.MilestoneID))
|
|
b.WriteString(fmt.Sprintf(" next: %s\n", r.NextWork))
|
|
} else {
|
|
b.WriteString(" selected: (none)\n")
|
|
}
|
|
for _, bl := range r.Blockers {
|
|
retry := "no"
|
|
if bl.Retryable {
|
|
retry = "yes"
|
|
}
|
|
b.WriteString(fmt.Sprintf(" blocker: %s %s retryable=%s\n", bl.Code, bl.Message, retry))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// FormatText renders a StartResponse as stable plain text.
|
|
func (r StartResponse) FormatText() string {
|
|
return fmt.Sprintf("start project=%s status=%s\n", r.Project, r.Status)
|
|
}
|
|
|
|
// FormatText renders a StopResponse as stable plain text.
|
|
func (r StopResponse) FormatText() string {
|
|
return fmt.Sprintf("stop project=%s status=%s\n", r.Project, r.Status)
|
|
}
|
|
|
|
// FormatText renders a ResumeResponse as stable plain text.
|
|
func (r ResumeResponse) FormatText() string {
|
|
return fmt.Sprintf("resume project=%s status=%s\n", r.Project, r.Status)
|
|
}
|
|
|
|
// FormatText renders a StatusResponse as stable plain text.
|
|
func (r StatusResponse) FormatText() string {
|
|
var b strings.Builder
|
|
b.WriteString(fmt.Sprintf("status project=%s\n", r.Project))
|
|
b.WriteString(fmt.Sprintf(" state: %s\n", r.Status))
|
|
b.WriteString(fmt.Sprintf(" selected_milestone: %s\n", r.SelectedMilestone))
|
|
b.WriteString(fmt.Sprintf(" started_milestone: %s\n", r.StartedMilestone))
|
|
b.WriteString(fmt.Sprintf(" works: %d\n", len(r.Works)))
|
|
for _, w := range r.Works {
|
|
b.WriteString(fmt.Sprintf(" - %s state=%s overlay=%s integration=%s ordinal=%d\n",
|
|
w.ID, w.State, w.Overlay, w.Integration, w.DispatchOrdinal))
|
|
if w.Blocker != nil {
|
|
retry := "no"
|
|
if w.Blocker.Retryable {
|
|
retry = "yes"
|
|
}
|
|
b.WriteString(fmt.Sprintf(" blocker: %s %s retryable=%s\n",
|
|
w.Blocker.Code, w.Blocker.Message, retry))
|
|
}
|
|
}
|
|
for _, bl := range r.Blockers {
|
|
retry := "no"
|
|
if bl.Retryable {
|
|
retry = "yes"
|
|
}
|
|
b.WriteString(fmt.Sprintf(" blocker: %s %s retryable=%s\n", bl.Code, bl.Message, retry))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (r TaskLoopResponse) FormatText() string {
|
|
var b strings.Builder
|
|
b.WriteString(fmt.Sprintf("task-loop dry_run=%t projects=%d\n", r.DryRun, len(r.Projects)))
|
|
for _, project := range r.Projects {
|
|
b.WriteString(fmt.Sprintf(" - project=%s group=%s status=%s next=%s\n", project.Project, project.Group, project.Status, project.NextWork))
|
|
for _, blocker := range project.Blockers {
|
|
b.WriteString(fmt.Sprintf(" blocker=%s retryable=%t %s\n", blocker.Code, blocker.Retryable, blocker.Message))
|
|
}
|
|
}
|
|
b.WriteString(fmt.Sprintf(" exit_code: %d\n", r.ExitCode))
|
|
return b.String()
|
|
}
|
|
|
|
// FormatJSON renders a ValidateResponse as stable JSON.
|
|
func (r ValidateResponse) FormatJSON() string {
|
|
type dto struct {
|
|
Valid bool `json:"valid"`
|
|
Revision string `json:"revision"`
|
|
Projects int `json:"projects"`
|
|
Providers int `json:"providers"`
|
|
Profiles int `json:"profiles"`
|
|
Errors []string `json:"errors"`
|
|
}
|
|
errs := r.Errors
|
|
if errs == nil {
|
|
errs = []string{}
|
|
}
|
|
data, _ := json.MarshalIndent(dto{
|
|
Valid: r.Valid,
|
|
Revision: r.Revision,
|
|
Projects: r.Projects,
|
|
Providers: r.Providers,
|
|
Profiles: r.Profiles,
|
|
Errors: errs,
|
|
}, "", " ")
|
|
return string(data)
|
|
}
|
|
|
|
// FormatJSON renders a ProviderListResponse as stable JSON.
|
|
func (r ProviderListResponse) FormatJSON() string {
|
|
type providerDTO struct {
|
|
ID string `json:"id"`
|
|
Command string `json:"command"`
|
|
Capabilities []string `json:"capabilities"`
|
|
}
|
|
type dto struct {
|
|
Providers []providerDTO `json:"providers"`
|
|
}
|
|
list := make([]providerDTO, 0, len(r.Providers))
|
|
for _, p := range r.Providers {
|
|
caps := p.Capabilities
|
|
if caps == nil {
|
|
caps = []string{}
|
|
}
|
|
list = append(list, providerDTO{
|
|
ID: p.ID,
|
|
Command: p.Command,
|
|
Capabilities: caps,
|
|
})
|
|
}
|
|
data, _ := json.MarshalIndent(dto{Providers: list}, "", " ")
|
|
return string(data)
|
|
}
|
|
|
|
// FormatJSON renders a ProjectListResponse as stable JSON.
|
|
func (r ProjectListResponse) FormatJSON() string {
|
|
type projectDTO struct {
|
|
ID string `json:"id"`
|
|
Workspace string `json:"workspace"`
|
|
Enabled bool `json:"enabled"`
|
|
SelectedMilestone string `json:"selected_milestone"`
|
|
StartedMilestone string `json:"started_milestone"`
|
|
AutoResumeInterrupt bool `json:"auto_resume_interrupted"`
|
|
}
|
|
type dto struct {
|
|
Projects []projectDTO `json:"projects"`
|
|
}
|
|
list := make([]projectDTO, 0, len(r.Projects))
|
|
for _, p := range r.Projects {
|
|
list = append(list, projectDTO{
|
|
ID: p.ID,
|
|
Workspace: p.Workspace,
|
|
Enabled: p.Enabled,
|
|
SelectedMilestone: p.SelectedMilestone,
|
|
StartedMilestone: p.StartedMilestone,
|
|
AutoResumeInterrupt: p.AutoResumeInterrupt,
|
|
})
|
|
}
|
|
data, _ := json.MarshalIndent(dto{Projects: list}, "", " ")
|
|
return string(data)
|
|
}
|
|
|
|
// FormatJSON renders a MilestoneListResponse as stable JSON.
|
|
func (r MilestoneListResponse) FormatJSON() string {
|
|
type milestoneDTO struct {
|
|
ID string `json:"id"`
|
|
Selected bool `json:"selected"`
|
|
WorkUnits int `json:"work_units"`
|
|
CompletedWorkUnits int `json:"completed_work_units"`
|
|
}
|
|
type dto struct {
|
|
Milestones []milestoneDTO `json:"milestones"`
|
|
}
|
|
list := make([]milestoneDTO, 0, len(r.Milestones))
|
|
for _, m := range r.Milestones {
|
|
list = append(list, milestoneDTO{
|
|
ID: m.ID,
|
|
Selected: m.Selected,
|
|
WorkUnits: m.WorkUnits,
|
|
CompletedWorkUnits: m.CompletedWorkUnits,
|
|
})
|
|
}
|
|
data, _ := json.MarshalIndent(dto{Milestones: list}, "", " ")
|
|
return string(data)
|
|
}
|
|
|
|
// FormatJSON renders a MilestoneSelectResponse as stable JSON.
|
|
func (r MilestoneSelectResponse) FormatJSON() string {
|
|
type dto struct {
|
|
Project string `json:"project"`
|
|
MilestoneID string `json:"milestone_id"`
|
|
}
|
|
data, _ := json.Marshal(dto{
|
|
Project: r.Project,
|
|
MilestoneID: r.MilestoneID,
|
|
})
|
|
return string(data)
|
|
}
|
|
|
|
// FormatJSON renders a PreviewResponse as stable JSON.
|
|
func (r PreviewResponse) FormatJSON() string {
|
|
type blockerDTO struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Retryable bool `json:"retryable"`
|
|
}
|
|
type dto struct {
|
|
Project string `json:"project"`
|
|
Selected bool `json:"selected"`
|
|
MilestoneID string `json:"milestone_id"`
|
|
NextWork string `json:"next_work"`
|
|
Blockers []blockerDTO `json:"blockers"`
|
|
}
|
|
blockers := make([]blockerDTO, 0, len(r.Blockers))
|
|
for _, bl := range r.Blockers {
|
|
blockers = append(blockers, blockerDTO{
|
|
Code: bl.Code,
|
|
Message: bl.Message,
|
|
Retryable: bl.Retryable,
|
|
})
|
|
}
|
|
data, _ := json.MarshalIndent(dto{
|
|
Project: r.Project,
|
|
Selected: r.Selected,
|
|
MilestoneID: r.MilestoneID,
|
|
NextWork: r.NextWork,
|
|
Blockers: blockers,
|
|
}, "", " ")
|
|
return string(data)
|
|
}
|
|
|
|
// FormatJSON renders a StartResponse as stable JSON.
|
|
func (r StartResponse) FormatJSON() string {
|
|
type dto struct {
|
|
Project string `json:"project"`
|
|
Status string `json:"status"`
|
|
}
|
|
data, _ := json.Marshal(dto{
|
|
Project: r.Project,
|
|
Status: r.Status,
|
|
})
|
|
return string(data)
|
|
}
|
|
|
|
// FormatJSON renders a StopResponse as stable JSON.
|
|
func (r StopResponse) FormatJSON() string {
|
|
type dto struct {
|
|
Project string `json:"project"`
|
|
Status string `json:"status"`
|
|
}
|
|
data, _ := json.Marshal(dto{
|
|
Project: r.Project,
|
|
Status: r.Status,
|
|
})
|
|
return string(data)
|
|
}
|
|
|
|
// FormatJSON renders a ResumeResponse as stable JSON.
|
|
func (r ResumeResponse) FormatJSON() string {
|
|
type dto struct {
|
|
Project string `json:"project"`
|
|
Status string `json:"status"`
|
|
}
|
|
data, _ := json.Marshal(dto{
|
|
Project: r.Project,
|
|
Status: r.Status,
|
|
})
|
|
return string(data)
|
|
}
|
|
|
|
// FormatJSON renders a StatusResponse as stable JSON.
|
|
func (r StatusResponse) FormatJSON() string {
|
|
type blockerDTO struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Retryable bool `json:"retryable"`
|
|
}
|
|
type workStatusDTO struct {
|
|
ID string `json:"id"`
|
|
State string `json:"state"`
|
|
Overlay string `json:"overlay"`
|
|
Integration string `json:"integration"`
|
|
DispatchOrdinal uint64 `json:"dispatch_ordinal"`
|
|
Blocker *blockerDTO `json:"blocker,omitempty"`
|
|
}
|
|
type dto struct {
|
|
Project string `json:"project"`
|
|
Status string `json:"status"`
|
|
SelectedMilestone string `json:"selected_milestone"`
|
|
StartedMilestone string `json:"started_milestone"`
|
|
Works []workStatusDTO `json:"works"`
|
|
Blockers []blockerDTO `json:"blockers"`
|
|
}
|
|
works := make([]workStatusDTO, 0, len(r.Works))
|
|
for _, w := range r.Works {
|
|
var b *blockerDTO
|
|
if w.Blocker != nil {
|
|
b = &blockerDTO{
|
|
Code: w.Blocker.Code,
|
|
Message: w.Blocker.Message,
|
|
Retryable: w.Blocker.Retryable,
|
|
}
|
|
}
|
|
works = append(works, workStatusDTO{
|
|
ID: w.ID,
|
|
State: w.State,
|
|
Overlay: w.Overlay,
|
|
Integration: w.Integration,
|
|
DispatchOrdinal: w.DispatchOrdinal,
|
|
Blocker: b,
|
|
})
|
|
}
|
|
blockers := make([]blockerDTO, 0, len(r.Blockers))
|
|
for _, bl := range r.Blockers {
|
|
blockers = append(blockers, blockerDTO{
|
|
Code: bl.Code,
|
|
Message: bl.Message,
|
|
Retryable: bl.Retryable,
|
|
})
|
|
}
|
|
data, _ := json.MarshalIndent(dto{
|
|
Project: r.Project,
|
|
Status: r.Status,
|
|
SelectedMilestone: r.SelectedMilestone,
|
|
StartedMilestone: r.StartedMilestone,
|
|
Works: works,
|
|
Blockers: blockers,
|
|
}, "", " ")
|
|
return string(data)
|
|
}
|
|
|
|
func (r TaskLoopResponse) FormatJSON() string {
|
|
type blockerDTO struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
Retryable bool `json:"retryable"`
|
|
}
|
|
type projectDTO struct {
|
|
Project string `json:"project"`
|
|
Group string `json:"group"`
|
|
Status string `json:"status"`
|
|
NextWork string `json:"next_work"`
|
|
Blockers []blockerDTO `json:"blockers"`
|
|
}
|
|
projects := make([]projectDTO, 0, len(r.Projects))
|
|
for _, project := range r.Projects {
|
|
blockers := make([]blockerDTO, 0, len(project.Blockers))
|
|
for _, blocker := range project.Blockers {
|
|
blockers = append(blockers, blockerDTO{Code: blocker.Code, Message: blocker.Message, Retryable: blocker.Retryable})
|
|
}
|
|
projects = append(projects, projectDTO{Project: project.Project, Group: project.Group, Status: project.Status, NextWork: project.NextWork, Blockers: blockers})
|
|
}
|
|
data, _ := json.MarshalIndent(struct {
|
|
DryRun bool `json:"dry_run"`
|
|
Projects []projectDTO `json:"projects"`
|
|
ExitCode int `json:"exit_code"`
|
|
}{DryRun: r.DryRun, Projects: projects, ExitCode: r.ExitCode}, "", " ")
|
|
return string(data)
|
|
}
|
|
|
|
func joinStrings(items []string) string {
|
|
return strings.Join(items, ", ")
|
|
}
|