feat: validate + vendor list/inspect with --json
- validate: offline hard rules (all+include, exclude-without-all, orphaned include entries vs pinned inventory) and lockfile/external-area divergence - vendor list: name/url/ref/rev/mode/item counts, human + json - vendor inspect: per-type inventory with frontmatter metadata and recorded warnings read from the external area, human + json - exit code 1 on invalid state for headless drift checks
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/m3tam3re/agent-lib/internal/lockfile"
|
||||
"github.com/m3tam3re/agent-lib/internal/vendor"
|
||||
)
|
||||
|
||||
func loadLockfileOrFail(cmd *cobra.Command) *lockfile.Lockfile {
|
||||
lf, err := lockfile.Load(filepath.Join(".", lockfile.FileName))
|
||||
if err != nil {
|
||||
if err == lockfile.ErrNotExist {
|
||||
fmt.Fprintln(os.Stderr, "error: no lockfile found — run `agent-lib vendor add` first")
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
return lf
|
||||
}
|
||||
|
||||
func emitJSON(cmd *cobra.Command, v any) error {
|
||||
enc := json.NewEncoder(cmd.OutOrStdout())
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(v)
|
||||
}
|
||||
|
||||
func newValidateCmd() *cobra.Command {
|
||||
var asJSON bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "validate",
|
||||
Short: "Check lockfile rules and external-area consistency offline",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
lf := loadLockfileOrFail(cmd)
|
||||
result := vendor.Validate(".", lf)
|
||||
if asJSON {
|
||||
return emitJSON(cmd, result)
|
||||
}
|
||||
w := cmd.OutOrStdout()
|
||||
if result.Valid {
|
||||
fmt.Fprintf(w, "lockfile valid (%d sources)\n", len(lf.Sources))
|
||||
return nil
|
||||
}
|
||||
for _, e := range result.Errors {
|
||||
fmt.Fprintf(w, "%s: %s\n", e.Rule, e.Message)
|
||||
}
|
||||
cmd.SilenceErrors = true
|
||||
os.Exit(1)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&asJSON, "json", false, "machine-readable output")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newVendorListCmd() *cobra.Command {
|
||||
var asJSON bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all vendored sources with pin, mode and item counts",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
lf := loadLockfileOrFail(cmd)
|
||||
rows := vendor.List(lf)
|
||||
if asJSON {
|
||||
return emitJSON(cmd, map[string]any{"sources": rows})
|
||||
}
|
||||
w := cmd.OutOrStdout()
|
||||
if len(rows) == 0 {
|
||||
fmt.Fprintln(w, "no sources vendored yet")
|
||||
return nil
|
||||
}
|
||||
fmt.Fprintf(w, "%-16s %-6s %-9s %-8s %s\n", "SOURCE", "REF", "REV", "MODE", "URL")
|
||||
for _, r := range rows {
|
||||
rev := r.Rev
|
||||
if len(rev) > 8 {
|
||||
rev = rev[:8]
|
||||
}
|
||||
fmt.Fprintf(w, "%-16s %-6s %-9s %-8s %s\n", r.Name, r.Ref, rev, r.Mode, r.URL)
|
||||
fmt.Fprintf(w, "%16s %s\n", "", countsLine(r.Counts))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&asJSON, "json", false, "machine-readable output")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func countsLine(counts map[string]int) string {
|
||||
line := ""
|
||||
total := 0
|
||||
for _, t := range lockfile.Types {
|
||||
if n, ok := counts[t]; ok {
|
||||
if line != "" {
|
||||
line += ", "
|
||||
}
|
||||
line += fmt.Sprintf("%d %s", n, t)
|
||||
total += n
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%d items: %s", total, line)
|
||||
}
|
||||
|
||||
func newVendorInspectCmd() *cobra.Command {
|
||||
var asJSON bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect <name>",
|
||||
Short: "Show one source's full item inventory with metadata and warnings",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
lf := loadLockfileOrFail(cmd)
|
||||
report, err := vendor.Inspect(".", args[0], lf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if asJSON {
|
||||
return emitJSON(cmd, report)
|
||||
}
|
||||
w := cmd.OutOrStdout()
|
||||
fmt.Fprintf(w, "source %s\n", report.Name)
|
||||
fmt.Fprintf(w, " url: %s\n", report.URL)
|
||||
fmt.Fprintf(w, " ref: %s\n", report.Ref)
|
||||
fmt.Fprintf(w, " rev: %s\n", report.Rev)
|
||||
fmt.Fprintf(w, " mode: %s\n", report.Mode)
|
||||
for _, t := range lockfile.Types {
|
||||
items := report.Items[t]
|
||||
if len(items) == 0 {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, " %s:\n", t)
|
||||
for _, it := range items {
|
||||
fmt.Fprintf(w, " %s", it.DeployedID)
|
||||
if it.DeployedID != it.UpstreamID {
|
||||
fmt.Fprintf(w, " (upstream: %s)", it.UpstreamID)
|
||||
}
|
||||
if it.Name != "" {
|
||||
fmt.Fprintf(w, " name=%s", it.Name)
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
for _, warn := range it.Warnings {
|
||||
fmt.Fprintf(w, " warning: %s\n", warn)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVar(&asJSON, "json", false, "machine-readable output")
|
||||
return cmd
|
||||
}
|
||||
Reference in New Issue
Block a user