diff --git a/go.mod b/go.mod index 21870a94..9358d345 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/signadot/cli -go 1.25 +go 1.25.0 require ( github.com/Masterminds/semver v1.5.0 @@ -10,8 +10,8 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/docker/go-units v0.5.0 github.com/go-git/go-git/v5 v5.16.5 - github.com/go-openapi/runtime v0.29.2 - github.com/go-openapi/strfmt v0.25.0 + github.com/go-openapi/runtime v0.29.3 + github.com/go-openapi/strfmt v0.26.1 github.com/goccy/go-yaml v1.10.0 github.com/golang/protobuf v1.5.4 github.com/google/gops v0.3.28 @@ -21,14 +21,14 @@ require ( github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 github.com/oklog/run v1.1.0 github.com/panta/machineid v1.0.2 - github.com/signadot/go-sdk v0.3.8-0.20260105152858-7f85937470f8 + github.com/signadot/go-sdk v0.3.8-0.20260402222445-b8b5bc1f40c0 github.com/signadot/libconnect v0.1.1-0.20260224205539-f894c2d0a57e github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.11.0 github.com/theckman/yacspin v0.13.12 github.com/xeonx/timeago v1.0.0-rc5 github.com/zalando/go-keyring v0.2.6 - golang.org/x/term v0.38.0 + golang.org/x/term v0.41.0 google.golang.org/grpc v1.78.0 google.golang.org/protobuf v1.36.11 k8s.io/client-go v0.33.0 @@ -58,18 +58,18 @@ require ( github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect - github.com/go-openapi/swag/cmdutils v0.25.4 // indirect - github.com/go-openapi/swag/conv v0.25.4 // indirect - github.com/go-openapi/swag/fileutils v0.25.4 // indirect - github.com/go-openapi/swag/jsonname v0.25.4 // indirect - github.com/go-openapi/swag/jsonutils v0.25.4 // indirect - github.com/go-openapi/swag/loading v0.25.4 // indirect - github.com/go-openapi/swag/mangling v0.25.4 // indirect - github.com/go-openapi/swag/netutils v0.25.4 // indirect - github.com/go-openapi/swag/stringutils v0.25.4 // indirect - github.com/go-openapi/swag/typeutils v0.25.4 // indirect - github.com/go-openapi/swag/yamlutils v0.25.4 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-openapi/swag/cmdutils v0.25.5 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/fileutils v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/mangling v0.25.5 // indirect + github.com/go-openapi/swag/netutils v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/gnostic-models v0.6.9 // indirect @@ -84,6 +84,7 @@ require ( github.com/muesli/termenv v0.16.0 // indirect github.com/mwitkow/grpc-proxy v0.0.0-20230212185441-f345521cb9c9 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prataprc/goparsec v0.0.0-20211219142520-daac0e635e7e // indirect @@ -96,8 +97,8 @@ require ( github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sync v0.19.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect @@ -113,14 +114,14 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/analysis v0.24.1 // indirect - github.com/go-openapi/errors v0.22.5 // indirect - github.com/go-openapi/jsonpointer v0.22.4 // indirect - github.com/go-openapi/jsonreference v0.21.4 // indirect - github.com/go-openapi/loads v0.23.2 // indirect - github.com/go-openapi/spec v0.22.2 // indirect - github.com/go-openapi/swag v0.25.4 // indirect - github.com/go-openapi/validate v0.25.1 // indirect + github.com/go-openapi/analysis v0.25.0 // indirect + github.com/go-openapi/errors v0.22.7 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/loads v0.23.3 // indirect + github.com/go-openapi/spec v0.22.4 // indirect + github.com/go-openapi/swag v0.25.5 // indirect + github.com/go-openapi/validate v0.25.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -137,7 +138,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/oklog/ulid v1.3.1 // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/rivo/uniseg v0.4.7 // indirect @@ -146,14 +146,13 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect - go.mongodb.org/mongo-driver v1.17.6 // indirect - go.opentelemetry.io/otel v1.39.0 // indirect - go.opentelemetry.io/otel/metric v1.39.0 // indirect - go.opentelemetry.io/otel/trace v1.39.0 // indirect - golang.org/x/crypto v0.46.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect + golang.org/x/crypto v0.49.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index c022ce97..585bebe1 100644 --- a/go.sum +++ b/go.sum @@ -154,54 +154,54 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= -github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM= -github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84= -github.com/go-openapi/errors v0.22.5 h1:Yfv4O/PRYpNF3BNmVkEizcHb3uLVVsrDt3LNdgAKRY4= -github.com/go-openapi/errors v0.22.5/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= -github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= -github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= -github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= -github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= -github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4= -github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY= -github.com/go-openapi/runtime v0.29.2 h1:UmwSGWNmWQqKm1c2MGgXVpC2FTGwPDQeUsBMufc5Yj0= -github.com/go-openapi/runtime v0.29.2/go.mod h1:biq5kJXRJKBJxTDJXAa00DOTa/anflQPhT0/wmjuy+0= -github.com/go-openapi/spec v0.22.2 h1:KEU4Fb+Lp1qg0V4MxrSCPv403ZjBl8Lx1a83gIPU8Qc= -github.com/go-openapi/spec v0.22.2/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= -github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= -github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= -github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= -github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= -github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= -github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= -github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= -github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= -github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= -github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= -github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= -github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= -github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= -github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= -github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= -github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= -github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= -github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= -github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= -github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= -github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= -github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= -github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= -github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= -github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= -github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= -github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= -github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= -github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= -github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw= -github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc= +github.com/go-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvCB7bCs= +github.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE= +github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA= +github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ= +github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA= +github.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y= +github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c= +github.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y= +github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= +github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= +github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= +github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= +github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= +github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= +github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= +github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q= +github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw= +github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0= +github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= @@ -211,8 +211,8 @@ github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7a github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-yaml v1.10.0 h1:rBi+5HGuznOxx0JZ+60LDY85gc0dyIJCIMvsMJTKSKQ= github.com/goccy/go-yaml v1.10.0/go.mod h1:h/18Lr6oSQ3mvmqFoWmQ47KChOgpfHpTyIHl3yVmpiY= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= @@ -379,14 +379,15 @@ github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1a github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/panta/machineid v1.0.2 h1:LVYeEq1hZ+FwcM+/H6eB8KfXM2R5b2h1SWdnWwZ0OQw= github.com/panta/machineid v1.0.2/go.mod h1:AROj156fsca3R3rNw3q9h8xFkos25W9P0ZG9gu+3Uf0= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= @@ -421,8 +422,8 @@ github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/signadot/go-sdk v0.3.8-0.20260105152858-7f85937470f8 h1:MQ6IebgvaT8KaclHmdO18D7r+zQj6FlR8949shHU4Vs= -github.com/signadot/go-sdk v0.3.8-0.20260105152858-7f85937470f8/go.mod h1:eSr+sGcPoeQvBH5v4Yy0+t2PlBHYohLDW+nxFQUj3L0= +github.com/signadot/go-sdk v0.3.8-0.20260402222445-b8b5bc1f40c0 h1:CpkTVrKonGpInT2p5p1cWihob4Nh1rYM52CrTqQS+sI= +github.com/signadot/go-sdk v0.3.8-0.20260402222445-b8b5bc1f40c0/go.mod h1:2+pvoCGoDDO+iPUmUuU2BjYHByHp3IDQ9k3o0liibb4= github.com/signadot/libconnect v0.1.1-0.20260224205539-f894c2d0a57e h1:NiYn5S3cMIhsGh3RzBgRg9NzLDG5qEP7uhSJKtwW7oc= github.com/signadot/libconnect v0.1.1-0.20260224205539-f894c2d0a57e/go.mod h1:cAsgAummH9Q9DrLQ7+S3mqrBv/+ZYKVSEXjR/WfoUJM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -475,8 +476,6 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= -go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= -go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -485,16 +484,16 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= @@ -509,8 +508,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -580,8 +579,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -604,8 +603,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -653,11 +652,11 @@ golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -667,8 +666,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -723,8 +722,8 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/command/command.go b/internal/command/command.go index 18a05a5d..46557035 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -16,6 +16,7 @@ import ( "github.com/signadot/cli/internal/command/locald" "github.com/signadot/cli/internal/command/logs" "github.com/signadot/cli/internal/command/mcp" + "github.com/signadot/cli/internal/command/plan" "github.com/signadot/cli/internal/command/resourceplugin" "github.com/signadot/cli/internal/command/routegroup" "github.com/signadot/cli/internal/command/sandbox" @@ -58,6 +59,7 @@ func New() *cobra.Command { mcp.New(cfg), smarttest.New(cfg), traffic.New(cfg), + plan.New(cfg), // hidden commands hostedtest.New(cfg), diff --git a/internal/command/logs/command.go b/internal/command/logs/command.go index 5c6831ba..768874f3 100644 --- a/internal/command/logs/command.go +++ b/internal/command/logs/command.go @@ -2,15 +2,16 @@ package logs import ( "context" - "encoding/json" "errors" + "fmt" "io" "github.com/go-openapi/runtime" - "github.com/jclem/sseparser" "github.com/signadot/cli/internal/config" + sdkprint "github.com/signadot/cli/internal/print" "github.com/signadot/go-sdk/client" "github.com/signadot/go-sdk/client/job_logs" + planexeclogs "github.com/signadot/go-sdk/client/plan_execution_logs" "github.com/signadot/go-sdk/utils" "github.com/spf13/cobra" ) @@ -20,7 +21,7 @@ func New(api *config.API) *cobra.Command { cmd := &cobra.Command{ Use: "logs", - Short: "Display job logs", + Short: "Display job or plan execution logs", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return showLogs(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), cfg) @@ -32,10 +33,21 @@ func New(api *config.API) *cobra.Command { } func showLogs(ctx context.Context, outW, errW io.Writer, cfg *config.Logs) error { + if cfg.Job == "" && cfg.Plan == "" { + return fmt.Errorf("must specify --job or --plan") + } + if cfg.Job != "" && cfg.Plan != "" { + return fmt.Errorf("--job and --plan are mutually exclusive") + } + if err := cfg.InitAPIConfig(); err != nil { return err } + if cfg.Plan != "" { + return showPlanLogs(ctx, outW, cfg) + } + var w io.Writer switch cfg.Stream { case utils.LogTypeStderr: @@ -48,14 +60,63 @@ func showLogs(ctx context.Context, outW, errW io.Writer, cfg *config.Logs) error return err } -type event struct { - Event string `sse:"event"` - Data string `sse:"data"` -} +func showPlanLogs(ctx context.Context, out io.Writer, cfg *config.Logs) error { + transportCfg := cfg.GetBaseTransport() + transportCfg.Consumers = map[string]runtime.Consumer{ + "text/event-stream": runtime.ByteStreamConsumer(), + } + + return cfg.APIClientWithCustomTransport(transportCfg, + func(c *client.SignadotAPI) error { + reader, writer := io.Pipe() + + errch := make(chan error, 2) + + go func() { + _, err := sdkprint.ParseSSEStream(reader, out) + if errors.Is(err, io.ErrClosedPipe) { + err = nil + } + reader.Close() + errch <- err + }() -type message struct { - Message string `json:"message"` - Cursor string `json:"cursor"` + go func() { + var err error + if cfg.Step != "" { + params := planexeclogs.NewStreamPlanExecutionStepLogsParams(). + WithContext(ctx). + WithTimeout(0). + WithOrgName(cfg.Org). + WithExecutionID(cfg.Plan). + WithStepID(cfg.Step). + WithStream(cfg.Stream) + if cfg.TailLines > 0 { + tl := int64(cfg.TailLines) + params.WithTailLines(&tl) + } + _, err = c.PlanExecutionLogs.StreamPlanExecutionStepLogs(params, nil, writer) + } else { + params := planexeclogs.NewStreamPlanExecutionLogsParams(). + WithContext(ctx). + WithTimeout(0). + WithOrgName(cfg.Org). + WithExecutionID(cfg.Plan) + if cfg.TailLines > 0 { + tl := int64(cfg.TailLines) + params.WithTailLines(&tl) + } + _, err = c.PlanExecutionLogs.StreamPlanExecutionLogs(params, nil, writer) + } + if errors.Is(err, io.ErrClosedPipe) { + err = nil + } + writer.Close() + errch <- err + }() + + return errors.Join(<-errch, <-errch) + }) } func ShowLogs(ctx context.Context, cfg *config.API, out io.Writer, jobName, stream, cursor string, tailLines int) (string, error) { @@ -95,7 +156,7 @@ func ShowLogs(ctx context.Context, cfg *config.API, out io.Writer, jobName, stre go func() { // parse the SSE stream - lastCursor, err = parseSSEStream(reader, out) + lastCursor, err = sdkprint.ParseSSEStream(reader, out) if errors.Is(err, io.ErrClosedPipe) { err = nil // ignore ErrClosedPipe error } @@ -119,45 +180,3 @@ func ShowLogs(ctx context.Context, cfg *config.API, out io.Writer, jobName, stre return lastCursor, err } -func parseSSEStream(reader io.Reader, out io.Writer) (string, error) { - scanner := sseparser.NewStreamScanner(reader) - var lastCursor string - - for { - // Then, we call `UnmarshalNext`, and log each completion chunk, until we - // encounter an error or reach the end of the stream. - var e event - _, err := scanner.UnmarshalNext(&e) - if err != nil { - if errors.Is(err, sseparser.ErrStreamEOF) { - err = nil - } - return lastCursor, err - } - - switch e.Event { - case "message": - var m message - err = json.Unmarshal([]byte(e.Data), &m) - if err != nil { - return lastCursor, err - } - if m.Message == "" { - continue - } - out.Write([]byte(m.Message)) - - lastCursor = m.Cursor - case "error": - return lastCursor, errors.New(string(e.Data)) - case "signal": - switch e.Data { - case "EOF": - return lastCursor, nil - case "RESTART": - out.Write([]byte("\n\n-------------------------------------------------------------------------------\n")) - out.Write([]byte("WARNING: The job execution has been restarted...\n\n")) - } - } - } -} diff --git a/internal/command/plan/command.go b/internal/command/plan/command.go new file mode 100644 index 00000000..58529f80 --- /dev/null +++ b/internal/command/plan/command.go @@ -0,0 +1,31 @@ +package plan + +import ( + "github.com/signadot/cli/internal/command/planexec" + "github.com/signadot/cli/internal/command/plantag" + "github.com/signadot/cli/internal/config" + "github.com/spf13/cobra" +) + +func New(api *config.API) *cobra.Command { + cfg := &config.Plan{API: api} + + cmd := &cobra.Command{ + Use: "plan", + Short: "Manage plans (compiled prompts that define runnable workflows)", + } + + // Subcommands + cmd.AddCommand( + newCompile(cfg), + newCreate(cfg), + newGet(cfg), + newDelete(cfg), + newRecompile(cfg), + plantag.New(cfg), + planexec.New(cfg), + newRun(cfg), + ) + + return cmd +} diff --git a/internal/command/plan/compile.go b/internal/command/plan/compile.go new file mode 100644 index 00000000..d0acf475 --- /dev/null +++ b/internal/command/plan/compile.go @@ -0,0 +1,82 @@ +package plan + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/signadot/cli/internal/command/plantag" + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + sdkplans "github.com/signadot/go-sdk/client/plans" + "github.com/signadot/go-sdk/models" + "github.com/spf13/cobra" +) + +func newCompile(plan *config.Plan) *cobra.Command { + cfg := &config.PlanCompile{Plan: plan} + + cmd := &cobra.Command{ + Use: "compile -f PROMPT_FILE", + Short: "Compile a natural-language prompt into a runnable plan", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return compile(cfg, cmd.OutOrStdout(), cmd.ErrOrStderr()) + }, + } + + cfg.AddFlags(cmd) + return cmd +} + +func compile(cfg *config.PlanCompile, out, log io.Writer) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + // Read the prompt from file or stdin. + var raw []byte + var err error + if cfg.Filename == "-" { + raw, err = io.ReadAll(os.Stdin) + } else { + raw, err = os.ReadFile(cfg.Filename) + } + if err != nil { + return fmt.Errorf("reading prompt: %w", err) + } + prompt := strings.TrimSpace(string(raw)) + if prompt == "" { + return fmt.Errorf("prompt file %q is empty", cfg.Filename) + } + + params := sdkplans.NewCompilePlanParams(). + WithOrgName(cfg.Org). + WithData(&models.PlanCompileInput{ + Prompt: prompt, + }) + resp, err := cfg.Client.Plans.CompilePlan(params, nil) + if err != nil { + return err + } + + // If --tag was provided, tag the compiled plan. + if cfg.Tag != "" { + if _, err := plantag.ApplyTag(cfg.Plan, resp.Payload.ID, cfg.Tag); err != nil { + return fmt.Errorf("plan compiled (id=%s) but tagging failed: %w", resp.Payload.ID, err) + } + fmt.Fprintf(log, "Tagged plan %s as %q\n", resp.Payload.ID, cfg.Tag) + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printPlanDetails(out, resp.Payload) + case config.OutputFormatJSON: + return print.RawJSON(out, resp.Payload) + case config.OutputFormatYAML: + return print.RawYAML(out, resp.Payload) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/plan/create.go b/internal/command/plan/create.go new file mode 100644 index 00000000..56fa17d6 --- /dev/null +++ b/internal/command/plan/create.go @@ -0,0 +1,98 @@ +package plan + +import ( + "encoding/json" + "fmt" + "io" + "strings" + + "github.com/signadot/cli/internal/command/plantag" + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/jsonexact" + "github.com/signadot/cli/internal/print" + "github.com/signadot/cli/internal/utils" + sdkplans "github.com/signadot/go-sdk/client/plans" + "github.com/signadot/go-sdk/models" + "github.com/spf13/cobra" +) + +func newCreate(plan *config.Plan) *cobra.Command { + cfg := &config.PlanCreate{Plan: plan} + + cmd := &cobra.Command{ + Use: "create -f SPEC_FILE", + Short: "Create a plan from a hand-authored spec file", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return create(cfg, cmd.OutOrStdout(), cmd.ErrOrStderr()) + }, + } + + cfg.AddFlags(cmd) + return cmd +} + +func create(cfg *config.PlanCreate, out, log io.Writer) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + spec, err := loadPlanSpec(cfg.Filename, cfg.TemplateVals) + if err != nil { + return err + } + + params := sdkplans.NewCreatePlanParams(). + WithOrgName(cfg.Org). + WithData(spec) + resp, err := cfg.Client.Plans.CreatePlan(params, nil) + if err != nil { + return err + } + + if cfg.Tag != "" { + if _, err := plantag.ApplyTag(cfg.Plan, resp.Payload.ID, cfg.Tag); err != nil { + return fmt.Errorf("plan created (id=%s) but tagging failed: %w", resp.Payload.ID, err) + } + fmt.Fprintf(log, "Tagged plan %s as %q\n", resp.Payload.ID, cfg.Tag) + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printPlanDetails(out, resp.Payload) + case config.OutputFormatJSON: + return print.RawJSON(out, resp.Payload) + case config.OutputFormatYAML: + return print.RawYAML(out, resp.Payload) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} + +func loadPlanSpec(file string, tplVals config.TemplateVals) (*models.PlanSpec, error) { + template, err := utils.LoadUnstructuredTemplate(file, tplVals, false) + if err != nil { + return nil, err + } + + // Extract the spec field if present, otherwise treat the whole thing as spec. + m, ok := template.(map[string]any) + if !ok { + return nil, fmt.Errorf("plan file must be a YAML/JSON object") + } + specVal, hasSpec := m["spec"] + if hasSpec { + template = specVal + } + + d, err := json.Marshal(template) + if err != nil { + return nil, err + } + spec := &models.PlanSpec{} + if err := jsonexact.Unmarshal(d, spec); err != nil { + return nil, fmt.Errorf("couldn't parse plan spec - %s", + strings.TrimPrefix(err.Error(), "json: ")) + } + return spec, nil +} diff --git a/internal/command/plan/delete.go b/internal/command/plan/delete.go new file mode 100644 index 00000000..bd71a0fe --- /dev/null +++ b/internal/command/plan/delete.go @@ -0,0 +1,40 @@ +package plan + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + sdkplans "github.com/signadot/go-sdk/client/plans" + "github.com/spf13/cobra" +) + +func newDelete(plan *config.Plan) *cobra.Command { + cfg := &config.PlanDelete{Plan: plan} + + cmd := &cobra.Command{ + Use: "delete PLAN_ID", + Short: "Delete a plan", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return deletePlan(cfg, cmd.ErrOrStderr(), args[0]) + }, + } + + return cmd +} + +func deletePlan(cfg *config.PlanDelete, log io.Writer, planID string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + params := sdkplans.NewDeletePlanParams(). + WithOrgName(cfg.Org). + WithPlanID(planID) + _, err := cfg.Client.Plans.DeletePlan(params, nil) + if err != nil { + return err + } + fmt.Fprintf(log, "Deleted plan %q.\n", planID) + return nil +} diff --git a/internal/command/plan/get.go b/internal/command/plan/get.go new file mode 100644 index 00000000..276ca81f --- /dev/null +++ b/internal/command/plan/get.go @@ -0,0 +1,50 @@ +package plan + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + sdkplans "github.com/signadot/go-sdk/client/plans" + "github.com/spf13/cobra" +) + +func newGet(plan *config.Plan) *cobra.Command { + cfg := &config.PlanGet{Plan: plan} + + cmd := &cobra.Command{ + Use: "get PLAN_ID", + Short: "Get plan details", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return getPlan(cfg, cmd.OutOrStdout(), args[0]) + }, + } + + return cmd +} + +func getPlan(cfg *config.PlanGet, out io.Writer, planID string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + params := sdkplans.NewGetPlanParams(). + WithOrgName(cfg.Org). + WithPlanID(planID) + resp, err := cfg.Client.Plans.GetPlan(params, nil) + if err != nil { + return err + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printPlanDetails(out, resp.Payload) + case config.OutputFormatJSON: + return print.RawJSON(out, resp.Payload) + case config.OutputFormatYAML: + return print.RawYAML(out, resp.Payload) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/plan/printers.go b/internal/command/plan/printers.go new file mode 100644 index 00000000..1d37c427 --- /dev/null +++ b/internal/command/plan/printers.go @@ -0,0 +1,69 @@ +package plan + +import ( + "fmt" + "io" + "slices" + "strings" + "text/tabwriter" + + "github.com/signadot/cli/internal/print" + "github.com/signadot/cli/internal/utils" + "github.com/signadot/go-sdk/models" +) + +func printPlanDetails(out io.Writer, p *models.RunnablePlan) error { + tw := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0) + + fmt.Fprintf(tw, "ID:\t%s\n", p.ID) + if p.Spec != nil { + if p.Spec.Prompt != "" { + fmt.Fprintf(tw, "Prompt:\t%s\n", print.FirstLine(p.Spec.Prompt)) + } + if p.Spec.Runner != "" { + fmt.Fprintf(tw, "Runner:\t%s\n", p.Spec.Runner) + } + if c := p.Spec.Cluster; c != nil { + switch { + case c.FromCluster != "": + fmt.Fprintf(tw, "Cluster:\tfrom param %q\n", c.FromCluster) + case c.FromSandbox != "": + fmt.Fprintf(tw, "Cluster:\tfrom sandbox param %q\n", c.FromSandbox) + case c.FromRouteGroup != "": + fmt.Fprintf(tw, "Cluster:\tfrom route group param %q\n", c.FromRouteGroup) + case c.Pattern != "": + fmt.Fprintf(tw, "Cluster:\tpattern %q\n", c.Pattern) + } + } + fmt.Fprintf(tw, "Steps:\t%d\n", len(p.Spec.Steps)) + if len(p.Spec.Params) > 0 { + names := make([]string, len(p.Spec.Params)) + for i, param := range p.Spec.Params { + names[i] = param.Name + } + fmt.Fprintf(tw, "Params:\t%s\n", strings.Join(names, ", ")) + } + if len(p.Spec.Output) > 0 { + outputNames := make([]string, 0, len(p.Spec.Output)) + for k := range p.Spec.Output { + outputNames = append(outputNames, k) + } + slices.Sort(outputNames) + fmt.Fprintf(tw, "Outputs:\t%s\n", strings.Join(outputNames, ", ")) + } + if len(p.Spec.Requires) > 0 { + fmt.Fprintf(tw, "Requires:\t%s\n", strings.Join(p.Spec.Requires, ", ")) + } + } + if p.Status != nil { + fmt.Fprintf(tw, "Created:\t%s\n", utils.FormatTimestamp(p.Status.CreatedAt)) + if p.Status.CompiledFrom != "" { + fmt.Fprintf(tw, "Compiled From:\t%s\n", p.Status.CompiledFrom) + } + if p.Status.Executions > 0 { + fmt.Fprintf(tw, "Executions:\t%d\n", p.Status.Executions) + } + } + + return tw.Flush() +} diff --git a/internal/command/plan/recompile.go b/internal/command/plan/recompile.go new file mode 100644 index 00000000..3131da6b --- /dev/null +++ b/internal/command/plan/recompile.go @@ -0,0 +1,60 @@ +package plan + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/command/plantag" + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + sdkplans "github.com/signadot/go-sdk/client/plans" + "github.com/spf13/cobra" +) + +func newRecompile(plan *config.Plan) *cobra.Command { + cfg := &config.PlanRecompile{Plan: plan} + + cmd := &cobra.Command{ + Use: "recompile PLAN_ID", + Short: "Recompile a plan from its original prompt", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return recompile(cfg, cmd.OutOrStdout(), cmd.ErrOrStderr(), args[0]) + }, + } + + cfg.AddFlags(cmd) + return cmd +} + +func recompile(cfg *config.PlanRecompile, out, log io.Writer, planID string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + params := sdkplans.NewRecompilePlanParams(). + WithOrgName(cfg.Org). + WithPlanID(planID) + resp, err := cfg.Client.Plans.RecompilePlan(params, nil) + if err != nil { + return err + } + + if cfg.Tag != "" { + if _, err := plantag.ApplyTag(cfg.Plan, resp.Payload.ID, cfg.Tag); err != nil { + return fmt.Errorf("plan recompiled (id=%s) but tagging failed: %w", resp.Payload.ID, err) + } + fmt.Fprintf(log, "Tagged plan %s as %q\n", resp.Payload.ID, cfg.Tag) + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printPlanDetails(out, resp.Payload) + case config.OutputFormatJSON: + return print.RawJSON(out, resp.Payload) + case config.OutputFormatYAML: + return print.RawYAML(out, resp.Payload) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/plan/run.go b/internal/command/plan/run.go new file mode 100644 index 00000000..cf15bbcb --- /dev/null +++ b/internal/command/plan/run.go @@ -0,0 +1,433 @@ +package plan + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "github.com/go-openapi/runtime" + "github.com/signadot/cli/internal/command/planexec" + "github.com/signadot/cli/internal/config" + sdkprint "github.com/signadot/cli/internal/print" + "github.com/signadot/cli/internal/spinner" + sdkclient "github.com/signadot/go-sdk/client" + planlogs "github.com/signadot/go-sdk/client/plan_execution_logs" + planexecs "github.com/signadot/go-sdk/client/plan_executions" + plantags "github.com/signadot/go-sdk/client/plan_tags" + "github.com/signadot/go-sdk/models" + "github.com/spf13/cobra" +) + +func newRun(plan *config.Plan) *cobra.Command { + cfg := &config.PlanRun{Plan: plan} + + cmd := &cobra.Command{ + Use: "run [PLAN_ID] [--tag TAG_NAME] [--param key=value ...]", + Short: "Run a plan: create execution, wait for completion, print results", + Long: `Creates an execution of a compiled plan and polls until completion. + +Resolve the plan by ID (positional argument) or by tag name (--tag). +Use --attach to stream structured events (logs, outputs, result) to stdout. +Exit codes: 0 = completed, 1 = failed, 2 = cancelled.`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runPlan(cfg, cmd.OutOrStdout(), cmd.ErrOrStderr(), args) + }, + } + + cfg.AddFlags(cmd) + return cmd +} + +func runPlan(cfg *config.PlanRun, out, log io.Writer, args []string) error { + ctx, cancel := signal.NotifyContext(context.Background(), + os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) + defer cancel() + + if cfg.Attach && cfg.OutputFormat == config.OutputFormatYAML { + return fmt.Errorf("--attach does not support -o yaml; use -o json for structured output") + } + + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + // Resolve plan ID. + planID, err := resolvePlanID(cfg, args) + if err != nil { + return err + } + + // Build params. + params := buildParams(cfg.Params) + + // Create execution. + spec := &models.PlanExecutionSpec{ + PlanID: planID, + Params: params, + } + createParams := planexecs.NewCreatePlanExecutionParams(). + WithContext(ctx). + WithOrgName(cfg.Org). + WithData(spec) + createResp, err := cfg.Client.PlanExecutions.CreatePlanExecution(createParams, nil) + if err != nil { + return fmt.Errorf("creating execution: %w", err) + } + execID := createResp.Payload.ID + fmt.Fprintf(log, "Created execution %s for plan %s\n", execID, planID) + + // Fire-and-forget mode. + if !cfg.Wait { + return writeRunOutput(cfg, out, createResp.Payload) + } + + // Wait for completion: attach streams structured events, otherwise poll with spinner. + var exec *models.PlanExecution + if cfg.Attach { + exec, err = attachExecution(ctx, cfg, out, log, execID) + } else { + exec, err = pollExecution(ctx, cfg, log, execID) + } + if err != nil { + // On interrupt or timeout, try to cancel the execution. + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + fmt.Fprintf(log, "\nCancelling execution %s...\n", execID) + cancelCtx, cancelCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelCancel() + cancelParams := planexecs.NewCancelPlanExecutionParams(). + WithContext(cancelCtx). + WithOrgName(cfg.Org). + WithExecutionID(execID) + cfg.Client.PlanExecutions.CancelPlanExecution(cancelParams, nil) + os.Exit(2) + } + return err + } + + // Export outputs if --output-dir specified. + if cfg.OutputDir != "" { + if err := exportOutputs(cfg, log, exec); err != nil { + fmt.Fprintf(log, "Warning: output export failed: %v\n", err) + } + } + + // In attach mode, events were already emitted to stdout. Just exit. + if cfg.Attach { + switch exec.Status.Phase { + case models.PlansExecutionPhaseFailed: + os.Exit(1) + case models.PlansExecutionPhaseCancelled: + os.Exit(2) + } + return nil + } + + // Print result and exit with appropriate code. + // On failure/cancellation, write details to stderr so stdout stays clean. + switch exec.Status.Phase { + case models.PlansExecutionPhaseFailed: + if err := writeRunOutput(cfg, log, exec); err != nil { + fmt.Fprintf(log, "error rendering output: %v\n", err) + } + os.Exit(1) + case models.PlansExecutionPhaseCancelled: + if err := writeRunOutput(cfg, log, exec); err != nil { + fmt.Fprintf(log, "error rendering output: %v\n", err) + } + os.Exit(2) + default: + return writeRunOutput(cfg, out, exec) + } + return nil +} + +func resolvePlanID(cfg *config.PlanRun, args []string) (string, error) { + if cfg.Tag != "" && len(args) > 0 { + return "", fmt.Errorf("specify either a plan ID argument or --tag, not both") + } + if cfg.Tag == "" && len(args) == 0 { + return "", fmt.Errorf("specify a plan ID argument or --tag") + } + if cfg.Tag != "" { + params := plantags.NewGetPlanTagParams(). + WithOrgName(cfg.Org). + WithPlanTagName(cfg.Tag) + resp, err := cfg.Client.PlanTags.GetPlanTag(params, nil) + if err != nil { + return "", fmt.Errorf("resolving tag %q: %w", cfg.Tag, err) + } + if resp.Payload.Spec == nil || resp.Payload.Spec.PlanID == "" { + return "", fmt.Errorf("tag %q has no plan ID", cfg.Tag) + } + return resp.Payload.Spec.PlanID, nil + } + return args[0], nil +} + +func buildParams(tplVals config.TemplateVals) map[string]any { + if len(tplVals) == 0 { + return nil + } + params := make(map[string]any, len(tplVals)) + for _, tv := range tplVals { + // If value looks like JSON, pass through as-is. + v := tv.Val + if looksLikeJSON(v) { + var raw json.RawMessage + if json.Unmarshal([]byte(v), &raw) == nil { + params[tv.Var] = raw + continue + } + } + params[tv.Var] = v + } + return params +} + +func looksLikeJSON(s string) bool { + if len(s) == 0 { + return false + } + switch s[0] { + case '{', '[', '"': + return true + } + switch s { + case "true", "false", "null": + return true + } + // Check if it's a number. + if s[0] == '-' || (s[0] >= '0' && s[0] <= '9') { + var v json.RawMessage + return json.Unmarshal([]byte(s), &v) == nil + } + return false +} + +func isTerminal(phase models.PlansExecutionPhase) bool { + switch phase { + case models.PlansExecutionPhaseCompleted, + models.PlansExecutionPhaseFailed, + models.PlansExecutionPhaseCancelled: + return true + } + return false +} + +func pollExecution(ctx context.Context, cfg *config.PlanRun, log io.Writer, execID string) (*models.PlanExecution, error) { + if cfg.Timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, cfg.Timeout) + defer cancel() + } + + spin := spinner.Start(log, "Execution") + defer spin.Stop() + + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + params := planexecs.NewGetPlanExecutionParams(). + WithContext(ctx). + WithOrgName(cfg.Org). + WithExecutionID(execID) + resp, err := cfg.Client.PlanExecutions.GetPlanExecution(params, nil) + if err != nil { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + spin.StopFail() + return nil, err + } + spin.Messagef("error: %v", err) + } else { + ex := resp.Payload + if isTerminal(ex.Status.Phase) { + switch ex.Status.Phase { + case models.PlansExecutionPhaseCompleted: + spin.StopMessage(string(ex.Status.Phase)) + default: + spin.StopFail() + } + return ex, nil + } + msg := string(ex.Status.Phase) + if sc := ex.Status.StepCounts; sc != nil { + total := sc.Init + sc.Waiting + sc.Running + sc.Completed + sc.Failed + sc.Skipped + msg = fmt.Sprintf("%s (%d/%d steps completed)", msg, sc.Completed, total) + } + spin.Message(msg) + } + + select { + case <-ticker.C: + case <-ctx.Done(): + spin.StopFail() + return nil, ctx.Err() + } + } +} + +func attachExecution(ctx context.Context, cfg *config.PlanRun, out, log io.Writer, execID string) (*models.PlanExecution, error) { + if cfg.Timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, cfg.Timeout) + defer cancel() + } + + jsonMode := cfg.OutputFormat == config.OutputFormatJSON + aw := sdkprint.NewAttachWriter(out, jsonMode) + + // Stream aggregated logs in background, emitting structured events. + logCtx, logCancel := context.WithCancel(ctx) + defer logCancel() + + logDone := make(chan error, 1) + go func() { + transportCfg := cfg.GetBaseTransport() + transportCfg.Consumers = map[string]runtime.Consumer{ + "text/event-stream": runtime.ByteStreamConsumer(), + } + err := cfg.APIClientWithCustomTransport(transportCfg, + func(c *sdkclient.SignadotAPI) error { + reader, writer := io.Pipe() + errch := make(chan error, 2) + + go func() { + _, err := sdkprint.ParseSSEAttach(reader, aw) + if errors.Is(err, io.ErrClosedPipe) { + err = nil + } + reader.Close() + errch <- err + }() + + go func() { + params := planlogs.NewStreamPlanExecutionLogsParams(). + WithContext(logCtx). + WithTimeout(0). + WithOrgName(cfg.Org). + WithExecutionID(execID) + _, err := c.PlanExecutionLogs.StreamPlanExecutionLogs(params, nil, writer) + if errors.Is(err, io.ErrClosedPipe) || errors.Is(err, context.Canceled) { + err = nil + } + writer.Close() + errch <- err + }() + + return errors.Join(<-errch, <-errch) + }) + logDone <- err + }() + + // Poll for terminal phase. + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + + for { + params := planexecs.NewGetPlanExecutionParams(). + WithContext(ctx). + WithOrgName(cfg.Org). + WithExecutionID(execID) + resp, err := cfg.Client.PlanExecutions.GetPlanExecution(params, nil) + if err != nil { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + logCancel() + <-logDone + return nil, err + } + } else if isTerminal(resp.Payload.Status.Phase) { + logCancel() + <-logDone + + ex := resp.Payload + // Emit output events for resolved plan-level outputs. + if ex.Status != nil { + for _, o := range ex.Status.Outputs { + aw.Emit(sdkprint.AttachEvent{ + Type: "output", + Name: o.Name, + Value: o.Value, + }) + } + } + // Emit result event. + resultEvent := sdkprint.AttachEvent{ + Type: "result", + ID: ex.ID, + Phase: string(ex.Status.Phase), + } + if ex.Status.Error != "" { + resultEvent.Error = ex.Status.Error + } + aw.Emit(resultEvent) + + return ex, nil + } + + select { + case <-ticker.C: + case <-ctx.Done(): + logCancel() + <-logDone + return nil, ctx.Err() + } + } +} + +func writeRunOutput(cfg *config.PlanRun, out io.Writer, exec *models.PlanExecution) error { + switch cfg.OutputFormat { + case config.OutputFormatJSON: + return sdkprint.RawJSON(out, exec) + case config.OutputFormatYAML: + return sdkprint.RawYAML(out, exec) + default: + return planexec.PrintRunResult(out, exec) + } +} + +func exportOutputs(cfg *config.PlanRun, log io.Writer, exec *models.PlanExecution) error { + if exec.Status == nil || len(exec.Status.Outputs) == 0 { + return nil + } + + if err := os.MkdirAll(cfg.OutputDir, 0o755); err != nil { + return err + } + + transportCfg := cfg.GetBaseTransport() + transportCfg.OverrideConsumers = true + transportCfg.Consumers = map[string]runtime.Consumer{ + "*/*": runtime.ByteStreamConsumer(), + } + + return cfg.APIClientWithCustomTransport(transportCfg, + func(c *sdkclient.SignadotAPI) error { + for _, o := range exec.Status.Outputs { + outPath := filepath.Join(cfg.OutputDir, o.Name) + f, err := os.Create(outPath) + if err != nil { + return fmt.Errorf("creating %s: %w", outPath, err) + } + params := planexecs.NewGetPlanExecutionOutputParams(). + WithOrgName(cfg.Org). + WithExecutionID(exec.ID). + WithOutputName(o.Name) + _, _, err = c.PlanExecutions.GetPlanExecutionOutput(params, nil, f) + f.Close() + if err != nil { + return fmt.Errorf("downloading %q: %w", o.Name, err) + } + fmt.Fprintf(log, "Exported %s\n", outPath) + } + return nil + }) +} diff --git a/internal/command/planexec/cancel.go b/internal/command/planexec/cancel.go new file mode 100644 index 00000000..993cb390 --- /dev/null +++ b/internal/command/planexec/cancel.go @@ -0,0 +1,51 @@ +package planexec + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + planexecs "github.com/signadot/go-sdk/client/plan_executions" + "github.com/spf13/cobra" +) + +func newCancel(exec *config.PlanExecution) *cobra.Command { + cfg := &config.PlanExecCancel{PlanExecution: exec} + + cmd := &cobra.Command{ + Use: "cancel EXECUTION_ID", + Short: "Cancel a running plan execution", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return cancelExec(cfg, cmd.OutOrStdout(), cmd.ErrOrStderr(), args[0]) + }, + } + + return cmd +} + +func cancelExec(cfg *config.PlanExecCancel, out, log io.Writer, execID string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + params := planexecs.NewCancelPlanExecutionParams(). + WithOrgName(cfg.Org). + WithExecutionID(execID) + resp, err := cfg.Client.PlanExecutions.CancelPlanExecution(params, nil) + if err != nil { + return err + } + fmt.Fprintf(log, "Cancelled execution %q.\n", execID) + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printExecDetails(out, resp.Payload) + case config.OutputFormatJSON: + return print.RawJSON(out, resp.Payload) + case config.OutputFormatYAML: + return print.RawYAML(out, resp.Payload) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/planexec/command.go b/internal/command/planexec/command.go new file mode 100644 index 00000000..72ea5963 --- /dev/null +++ b/internal/command/planexec/command.go @@ -0,0 +1,27 @@ +package planexec + +import ( + "github.com/signadot/cli/internal/config" + "github.com/spf13/cobra" +) + +func New(plan *config.Plan) *cobra.Command { + cfg := &config.PlanExecution{Plan: plan} + + cmd := &cobra.Command{ + Use: "execution", + Short: "Manage plan executions (runs of compiled plans)", + Aliases: []string{"x"}, + } + + cmd.AddCommand( + newList(cfg), + newGet(cfg), + newCancel(cfg), + newOutputs(cfg), + newGetOutput(cfg), + newLogs(cfg), + ) + + return cmd +} diff --git a/internal/command/planexec/get.go b/internal/command/planexec/get.go new file mode 100644 index 00000000..2654f092 --- /dev/null +++ b/internal/command/planexec/get.go @@ -0,0 +1,50 @@ +package planexec + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + planexecs "github.com/signadot/go-sdk/client/plan_executions" + "github.com/spf13/cobra" +) + +func newGet(exec *config.PlanExecution) *cobra.Command { + cfg := &config.PlanExecGet{PlanExecution: exec} + + cmd := &cobra.Command{ + Use: "get EXECUTION_ID", + Short: "Get plan execution details", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return getExec(cfg, cmd.OutOrStdout(), args[0]) + }, + } + + return cmd +} + +func getExec(cfg *config.PlanExecGet, out io.Writer, execID string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + params := planexecs.NewGetPlanExecutionParams(). + WithOrgName(cfg.Org). + WithExecutionID(execID) + resp, err := cfg.Client.PlanExecutions.GetPlanExecution(params, nil) + if err != nil { + return err + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printExecDetails(out, resp.Payload) + case config.OutputFormatJSON: + return print.RawJSON(out, resp.Payload) + case config.OutputFormatYAML: + return print.RawYAML(out, resp.Payload) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/planexec/get_output.go b/internal/command/planexec/get_output.go new file mode 100644 index 00000000..f910bf24 --- /dev/null +++ b/internal/command/planexec/get_output.go @@ -0,0 +1,219 @@ +package planexec + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/go-openapi/runtime" + "github.com/signadot/cli/internal/config" + "github.com/signadot/go-sdk/client" + planexecs "github.com/signadot/go-sdk/client/plan_executions" + "github.com/signadot/go-sdk/models" + "github.com/spf13/cobra" +) + +func newGetOutput(exec *config.PlanExecution) *cobra.Command { + cfg := &config.PlanExecGetOutput{PlanExecution: exec} + + cmd := &cobra.Command{ + Use: "get-output EXECUTION_ID [NAME]", + Short: "Download a plan execution output", + Long: `Download an output by name, or export all outputs to a directory. + +Single output: + signadot plan x get-output # plan-level output + signadot plan x get-output / # step-level output + +Bulk export: + signadot plan x get-output --all --dir ./outputs/ + signadot plan x get-output --all --dir ./outputs/ --metadata`, + Args: cobra.RangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + if cfg.All { + if len(args) != 1 { + return fmt.Errorf("--all expects exactly one argument (execution ID)") + } + if cfg.Dir == "" { + return fmt.Errorf("--all requires --dir") + } + return getAllOutputs(cfg, cmd.ErrOrStderr(), args[0]) + } + if len(args) != 2 { + return fmt.Errorf("expected EXECUTION_ID and NAME arguments") + } + return getOutput(cfg, os.Stdout, args[0], args[1]) + }, + } + + cmd.Flags().BoolVar(&cfg.All, "all", false, "export all outputs") + cmd.Flags().StringVar(&cfg.Dir, "dir", "", "directory to export outputs to (requires --all)") + cmd.Flags().BoolVar(&cfg.Metadata, "metadata", false, "write metadata sidecar JSON files (requires --all)") + + return cmd +} + +func getOutput(cfg *config.PlanExecGetOutput, out io.Writer, execID, name string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + transportCfg := cfg.GetBaseTransport() + transportCfg.OverrideConsumers = true + transportCfg.Consumers = map[string]runtime.Consumer{ + "*/*": runtime.ByteStreamConsumer(), + } + + return cfg.APIClientWithCustomTransport(transportCfg, + func(c *client.SignadotAPI) error { + // If name contains '/', treat as step_id/output_name. + if stepID, outputName, ok := strings.Cut(name, "/"); ok { + params := planexecs.NewGetStepOutputParams(). + WithTimeout(4*time.Minute). + WithOrgName(cfg.Org). + WithExecutionID(execID). + WithStepID(stepID). + WithOutputName(outputName) + _, _, err := c.PlanExecutions.GetStepOutput(params, nil, out) + if err != nil { + return fmt.Errorf("downloading output %q: %w", name, err) + } + return nil + } + + params := planexecs.NewGetPlanExecutionOutputParams(). + WithTimeout(4*time.Minute). + WithOrgName(cfg.Org). + WithExecutionID(execID). + WithOutputName(name) + _, _, err := c.PlanExecutions.GetPlanExecutionOutput(params, nil, out) + if err != nil { + return fmt.Errorf("downloading output %q: %w", name, err) + } + return nil + }) +} + +func getAllOutputs(cfg *config.PlanExecGetOutput, log io.Writer, execID string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + // Fetch execution to get output list. + getParams := planexecs.NewGetPlanExecutionParams(). + WithOrgName(cfg.Org). + WithExecutionID(execID) + resp, err := cfg.Client.PlanExecutions.GetPlanExecution(getParams, nil) + if err != nil { + return err + } + + // Reuse collectAllOutputs to gather plan-level and step-level outputs. + all := collectAllOutputs(resp.Payload) + if len(all) == 0 { + fmt.Fprintln(log, "No outputs.") + return nil + } + + // Build a metadata lookup from the raw response for sidecar export. + metadataMap := buildMetadataMap(resp.Payload) + + if err := os.MkdirAll(cfg.Dir, 0o755); err != nil { + return err + } + + transportCfg := cfg.GetBaseTransport() + transportCfg.OverrideConsumers = true + transportCfg.Consumers = map[string]runtime.Consumer{ + "*/*": runtime.ByteStreamConsumer(), + } + + return cfg.APIClientWithCustomTransport(transportCfg, + func(c *client.SignadotAPI) error { + for _, o := range all { + // Determine file path: plan-level → /, step-level → //. + var outPath string + if o.Step != "" { + stepDir := filepath.Join(cfg.Dir, o.Step) + if err := os.MkdirAll(stepDir, 0o755); err != nil { + return fmt.Errorf("creating %s: %w", stepDir, err) + } + outPath = filepath.Join(stepDir, o.Name) + } else { + outPath = filepath.Join(cfg.Dir, o.Name) + } + + f, err := os.Create(outPath) + if err != nil { + return fmt.Errorf("creating %s: %w", outPath, err) + } + + // Download using the appropriate API based on scope. + qualName := o.Name + if o.Scope == "step" { + qualName = o.Step + "/" + o.Name + params := planexecs.NewGetStepOutputParams(). + WithTimeout(4*time.Minute). + WithOrgName(cfg.Org). + WithExecutionID(execID). + WithStepID(o.Step). + WithOutputName(o.Name) + _, _, err = c.PlanExecutions.GetStepOutput(params, nil, f) + } else { + params := planexecs.NewGetPlanExecutionOutputParams(). + WithTimeout(4*time.Minute). + WithOrgName(cfg.Org). + WithExecutionID(execID). + WithOutputName(o.Name) + _, _, err = c.PlanExecutions.GetPlanExecutionOutput(params, nil, f) + } + f.Close() + if err != nil { + return fmt.Errorf("downloading %q: %w", qualName, err) + } + fmt.Fprintf(log, "Exported %s\n", outPath) + + // Write metadata sidecar if requested. + if cfg.Metadata { + if meta := metadataMap[qualName]; meta != nil { + metaPath := outPath + ".meta.json" + metaJSON, err := json.MarshalIndent(meta, "", " ") + if err != nil { + return fmt.Errorf("marshaling metadata for %q: %w", qualName, err) + } + if err := os.WriteFile(metaPath, metaJSON, 0o644); err != nil { + return fmt.Errorf("writing %s: %w", metaPath, err) + } + fmt.Fprintf(log, "Exported %s\n", metaPath) + } + } + } + return nil + }) +} + +// buildMetadataMap extracts metadata from plan-level and step-level outputs, +// keyed by "name" (plan-level) or "step/name" (step-level). +func buildMetadataMap(ex *models.PlanExecution) map[string]any { + m := map[string]any{} + if ex.Status == nil { + return m + } + for _, o := range ex.Status.Outputs { + if o.Metadata != nil { + m[o.Name] = o.Metadata + } + } + for _, s := range ex.Status.Steps { + for _, o := range s.Outputs { + if o.Metadata != nil { + m[s.ID+"/"+o.Name] = o.Metadata + } + } + } + return m +} diff --git a/internal/command/planexec/list.go b/internal/command/planexec/list.go new file mode 100644 index 00000000..4e51414c --- /dev/null +++ b/internal/command/planexec/list.go @@ -0,0 +1,81 @@ +package planexec + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + planexecs "github.com/signadot/go-sdk/client/plan_executions" + "github.com/signadot/go-sdk/models" + "github.com/spf13/cobra" +) + +func newList(exec *config.PlanExecution) *cobra.Command { + cfg := &config.PlanExecList{PlanExecution: exec} + + cmd := &cobra.Command{ + Use: "list", + Short: "List plan executions", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return listExecs(cfg, cmd.OutOrStdout()) + }, + } + + cfg.AddFlags(cmd) + return cmd +} + +func listExecs(cfg *config.PlanExecList, out io.Writer) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + var results []*models.PlanExecutionQueryResult + var cursor *string + + for { + params := planexecs.NewListPlanExecutionsParams(). + WithOrgName(cfg.Org) + if cfg.PlanID != "" { + params.WithPlanID(&cfg.PlanID) + } + if cfg.Tag != "" { + params.WithTag(&cfg.Tag) + } + if cfg.Phase != "" { + params.WithPhase(&cfg.Phase) + } + if cursor != nil { + params.WithCursor(cursor) + } + + resp, err := cfg.Client.PlanExecutions.ListPlanExecutions(params, nil) + if err != nil { + return err + } + results = append(results, resp.Payload...) + + // If fewer results than default page size, we're done. + if len(resp.Payload) == 0 { + break + } + last := resp.Payload[len(resp.Payload)-1] + if last.Cursor == "" { + break + } + cursor = &last.Cursor + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printExecTable(out, results) + case config.OutputFormatJSON: + return print.RawJSON(out, results) + case config.OutputFormatYAML: + return print.RawYAML(out, results) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/planexec/logs.go b/internal/command/planexec/logs.go new file mode 100644 index 00000000..da67ed6b --- /dev/null +++ b/internal/command/planexec/logs.go @@ -0,0 +1,114 @@ +package planexec + +import ( + "context" + "errors" + "io" + "os" + "os/signal" + "syscall" + + "github.com/go-openapi/runtime" + "github.com/signadot/cli/internal/config" + sdkprint "github.com/signadot/cli/internal/print" + "github.com/signadot/go-sdk/client" + planlogs "github.com/signadot/go-sdk/client/plan_execution_logs" + "github.com/spf13/cobra" +) + +func newLogs(exec *config.PlanExecution) *cobra.Command { + cfg := &config.PlanExecLogs{PlanExecution: exec} + + cmd := &cobra.Command{ + Use: "logs EXECUTION_ID [STEP_ID]", + Short: "Stream plan execution logs", + Long: `Stream logs for a plan execution. + +Without a step ID, streams aggregated logs for all steps. +With a step ID, streams logs for that specific step.`, + Args: cobra.RangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + return streamLogs(cfg, cmd.OutOrStdout(), args) + }, + } + + cfg.AddFlags(cmd) + return cmd +} + +func showPlanLogs(ctx context.Context, cfg *config.API, out io.Writer, execID, stepID, stream string, tailLines int) error { + transportCfg := cfg.GetBaseTransport() + transportCfg.Consumers = map[string]runtime.Consumer{ + "text/event-stream": runtime.ByteStreamConsumer(), + } + + return cfg.APIClientWithCustomTransport(transportCfg, + func(c *client.SignadotAPI) error { + reader, writer := io.Pipe() + + errch := make(chan error, 2) + + go func() { + _, err := sdkprint.ParseSSEStream(reader, out) + if errors.Is(err, io.ErrClosedPipe) { + err = nil + } + reader.Close() + errch <- err + }() + + go func() { + var err error + if stepID != "" { + params := planlogs.NewStreamPlanExecutionStepLogsParams(). + WithContext(ctx). + WithTimeout(0). + WithOrgName(cfg.Org). + WithExecutionID(execID). + WithStepID(stepID). + WithStream(stream) + if tailLines > 0 { + tl := int64(tailLines) + params.WithTailLines(&tl) + } + _, err = c.PlanExecutionLogs.StreamPlanExecutionStepLogs(params, nil, writer) + } else { + params := planlogs.NewStreamPlanExecutionLogsParams(). + WithContext(ctx). + WithTimeout(0). + WithOrgName(cfg.Org). + WithExecutionID(execID) + if tailLines > 0 { + tl := int64(tailLines) + params.WithTailLines(&tl) + } + _, err = c.PlanExecutionLogs.StreamPlanExecutionLogs(params, nil, writer) + } + if errors.Is(err, io.ErrClosedPipe) { + err = nil + } + writer.Close() + errch <- err + }() + + return errors.Join(<-errch, <-errch) + }) +} + +func streamLogs(cfg *config.PlanExecLogs, out io.Writer, args []string) error { + ctx, cancel := signal.NotifyContext(context.Background(), + os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) + defer cancel() + + if err := cfg.InitAPIConfig(); err != nil { + return err + } + + execID := args[0] + stepID := "" + if len(args) > 1 { + stepID = args[1] + } + + return showPlanLogs(ctx, cfg.API, out, execID, stepID, cfg.Stream, int(cfg.TailLines)) +} diff --git a/internal/command/planexec/outputs.go b/internal/command/planexec/outputs.go new file mode 100644 index 00000000..68737b47 --- /dev/null +++ b/internal/command/planexec/outputs.go @@ -0,0 +1,142 @@ +package planexec + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + planexecs "github.com/signadot/go-sdk/client/plan_executions" + "github.com/signadot/go-sdk/models" + "github.com/spf13/cobra" +) + +func newOutputs(exec *config.PlanExecution) *cobra.Command { + cfg := &config.PlanExecOutputs{PlanExecution: exec} + + cmd := &cobra.Command{ + Use: "outputs EXECUTION_ID", + Short: "List all outputs of a plan execution (plan-level and step-level)", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return listOutputs(cfg, cmd.OutOrStdout(), args[0]) + }, + } + + return cmd +} + +// allOutput unifies plan-level and step-level outputs for display. +type allOutput struct { + Name string `json:"name"` + Step string `json:"step"` + Scope string `json:"scope"` // "plan" or "step" + Type string `json:"type"` // "inline" or "artifact" + Size int64 `json:"size,omitempty"` + Ready *bool `json:"ready,omitempty"` +} + +func listOutputs(cfg *config.PlanExecOutputs, out io.Writer, execID string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + params := planexecs.NewGetPlanExecutionParams(). + WithOrgName(cfg.Org). + WithExecutionID(execID) + resp, err := cfg.Client.PlanExecutions.GetPlanExecution(params, nil) + if err != nil { + return err + } + + all := collectAllOutputs(resp.Payload) + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printAllOutputsTable(out, all) + case config.OutputFormatJSON: + return print.RawJSON(out, all) + case config.OutputFormatYAML: + return print.RawYAML(out, all) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} + +func collectAllOutputs(ex *models.PlanExecution) []allOutput { + if ex.Status == nil { + return nil + } + + // Track plan-level output names to avoid duplicating them in step section. + planOutputNames := map[string]bool{} + var all []allOutput + + // Plan-level outputs. + for _, o := range ex.Status.Outputs { + step := "" + if o.StepRef != nil { + step = o.StepRef.StepID + } + planOutputNames[step+"/"+o.Name] = true + all = append(all, allOutput{ + Name: o.Name, + Step: step, + Scope: "plan", + Type: outputType(o.Artifact), + Size: outputSize(o.Artifact, o.Value), + Ready: outputReady(o.Artifact), + }) + } + + // Step-level outputs (skip those already shown as plan-level). + for _, s := range ex.Status.Steps { + for _, o := range s.Outputs { + key := s.ID + "/" + o.Name + if planOutputNames[key] { + continue + } + all = append(all, allOutput{ + Name: o.Name, + Step: s.ID, + Scope: "step", + Type: outputType(o.Artifact), + Size: outputSize(o.Artifact, o.Value), + Ready: outputReady(o.Artifact), + }) + } + } + + return all +} + +func outputType(a *models.PlanArtifactRef) string { + if a != nil { + return "artifact" + } + return "inline" +} + +func outputSize(a *models.PlanArtifactRef, value any) int64 { + if a != nil { + return a.Size + } + if value != nil { + if s, ok := value.(string); ok { + return int64(len(s)) + } + b, err := json.Marshal(value) + if err == nil { + return int64(len(b)) + } + } + return 0 +} + +func outputReady(a *models.PlanArtifactRef) *bool { + t := true + if a != nil { + return &a.Ready + } + return &t +} diff --git a/internal/command/planexec/printers.go b/internal/command/planexec/printers.go new file mode 100644 index 00000000..025ca6a6 --- /dev/null +++ b/internal/command/planexec/printers.go @@ -0,0 +1,232 @@ +package planexec + +import ( + "fmt" + "io" + "text/tabwriter" + "time" + + "github.com/docker/go-units" + "github.com/signadot/cli/internal/sdtab" + "github.com/signadot/cli/internal/utils" + "github.com/signadot/go-sdk/models" + "github.com/xeonx/timeago" +) + +// PrintRunResult prints execution details with output summary for plan run. +// Inline values are printed to stdout; artifact outputs are listed by reference. +func PrintRunResult(out io.Writer, ex *models.PlanExecution) error { + if err := printExecDetails(out, ex); err != nil { + return err + } + + if ex.Status == nil || len(ex.Status.Outputs) == 0 { + return nil + } + + // Print inline values directly. + for _, o := range ex.Status.Outputs { + if o.Value != nil { + fmt.Fprintf(out, "\n--- %s ---\n%v\n", o.Name, o.Value) + } + } + + // List artifact outputs by reference. + var artifacts []*models.PlanOutputStatus + for _, o := range ex.Status.Outputs { + if o.Artifact != nil { + artifacts = append(artifacts, o) + } + } + if len(artifacts) > 0 { + fmt.Fprintln(out) + fmt.Fprintln(out, "Artifact outputs:") + return printOutputsTable(out, artifacts) + } + return nil +} + +func printExecDetails(out io.Writer, ex *models.PlanExecution) error { + tw := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0) + + fmt.Fprintf(tw, "ID:\t%s\n", ex.ID) + if ex.Spec != nil { + fmt.Fprintf(tw, "Plan:\t%s\n", ex.Spec.PlanID) + if ex.Spec.Cluster != "" { + fmt.Fprintf(tw, "Cluster:\t%s\n", ex.Spec.Cluster) + } + if ex.Spec.Runner != "" { + fmt.Fprintf(tw, "Runner:\t%s\n", ex.Spec.Runner) + } + } + if ex.Status != nil { + fmt.Fprintf(tw, "Phase:\t%s\n", ex.Status.Phase) + fmt.Fprintf(tw, "Created:\t%s\n", utils.FormatTimestamp(ex.Status.CreatedAt)) + if ex.Status.UpdatedAt != "" { + fmt.Fprintf(tw, "Updated:\t%s\n", utils.FormatTimestamp(ex.Status.UpdatedAt)) + } + if ex.Status.CompletedAt != "" { + fmt.Fprintf(tw, "Completed:\t%s\n", utils.FormatTimestamp(ex.Status.CompletedAt)) + } + if sc := ex.Status.StepCounts; sc != nil { + total := sc.Init + sc.Waiting + sc.Running + sc.Completed + sc.Failed + sc.Skipped + fmt.Fprintf(tw, "Steps:\t%d/%d completed", sc.Completed, total) + if sc.Failed > 0 { + fmt.Fprintf(tw, ", %d failed", sc.Failed) + } + if sc.Running > 0 { + fmt.Fprintf(tw, ", %d running", sc.Running) + } + fmt.Fprintln(tw) + } + if ex.Status.Error != "" { + fmt.Fprintf(tw, "Error:\t%s\n", ex.Status.Error) + } + } + + if err := tw.Flush(); err != nil { + return err + } + + // Print step status table if steps are present. + if ex.Status != nil && len(ex.Status.Steps) > 0 { + fmt.Fprintln(out) + return printStepTable(out, ex.Status.Steps) + } + + return nil +} + +type stepRow struct { + ID string `sdtab:"STEP"` + Phase string `sdtab:"PHASE"` + Error string `sdtab:"ERROR,trunc"` +} + +func printStepTable(out io.Writer, steps []*models.PlanStepStatus) error { + t := sdtab.New[stepRow](out) + t.AddHeader() + for _, s := range steps { + t.AddRow(stepRow{ + ID: s.ID, + Phase: string(s.Phase), + Error: s.Error, + }) + } + return t.Flush() +} + +type outputRow struct { + Name string `sdtab:"NAME"` + Step string `sdtab:"STEP"` + Type string `sdtab:"TYPE"` + Size string `sdtab:"SIZE"` + Ready string `sdtab:"READY"` +} + +func printOutputsTable(out io.Writer, outputs []*models.PlanOutputStatus) error { + t := sdtab.New[outputRow](out) + t.AddHeader() + for _, o := range outputs { + step := "" + if o.StepRef != nil { + step = o.StepRef.StepID + } + typ := "inline" + size := "" + ready := "-" + if o.Artifact != nil { + typ = "artifact" + size = units.HumanSize(float64(o.Artifact.Size)) + if o.Artifact.Ready { + ready = "true" + } else { + ready = "false" + } + } + t.AddRow(outputRow{ + Name: o.Name, + Step: step, + Type: typ, + Size: size, + Ready: ready, + }) + } + return t.Flush() +} + +type allOutputRow struct { + Name string `sdtab:"NAME"` + Step string `sdtab:"STEP"` + Scope string `sdtab:"SCOPE"` + Storage string `sdtab:"STORAGE"` + Size string `sdtab:"SIZE"` + Ready string `sdtab:"READY"` +} + +func printAllOutputsTable(out io.Writer, outputs []allOutput) error { + t := sdtab.New[allOutputRow](out) + t.AddHeader() + for _, o := range outputs { + size := "" + ready := "-" + if o.Size > 0 { + size = units.HumanSize(float64(o.Size)) + } + if o.Ready != nil { + if *o.Ready { + ready = "true" + } else { + ready = "false" + } + } + t.AddRow(allOutputRow{ + Name: o.Name, + Step: o.Step, + Scope: o.Scope, + Storage: o.Type, + Size: size, + Ready: ready, + }) + } + return t.Flush() +} + +type execRow struct { + ID string `sdtab:"ID"` + Plan string `sdtab:"PLAN"` + Phase string `sdtab:"PHASE"` + Steps string `sdtab:"STEPS"` + Created string `sdtab:"CREATED"` +} + +func printExecTable(out io.Writer, results []*models.PlanExecutionQueryResult) error { + t := sdtab.New[execRow](out) + t.AddHeader() + for _, r := range results { + var plan, phase, steps, created string + if r.Spec != nil { + plan = r.Spec.PlanID + } + if r.Status != nil { + phase = string(r.Status.Phase) + if sc := r.Status.StepCounts; sc != nil { + total := sc.Init + sc.Waiting + sc.Running + sc.Completed + sc.Failed + sc.Skipped + steps = fmt.Sprintf("%d/%d", sc.Completed, total) + } + if r.Status.CreatedAt != "" { + if ts, err := time.Parse(time.RFC3339, r.Status.CreatedAt); err == nil { + created = timeago.NoMax(timeago.English).Format(ts) + } + } + } + t.AddRow(execRow{ + ID: r.ID, + Plan: plan, + Phase: phase, + Steps: steps, + Created: created, + }) + } + return t.Flush() +} diff --git a/internal/command/plantag/apply.go b/internal/command/plantag/apply.go new file mode 100644 index 00000000..6b772900 --- /dev/null +++ b/internal/command/plantag/apply.go @@ -0,0 +1,64 @@ +package plantag + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + plantags "github.com/signadot/go-sdk/client/plan_tags" + "github.com/signadot/go-sdk/models" + "github.com/spf13/cobra" +) + +func newApply(tag *config.PlanTag) *cobra.Command { + cfg := &config.PlanTagApply{PlanTag: tag} + + cmd := &cobra.Command{ + Use: "apply TAG_NAME --plan PLAN_ID", + Short: "Create or move a plan tag", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return applyTag(cfg, cmd.OutOrStdout(), args[0]) + }, + } + + cfg.AddFlags(cmd) + return cmd +} + +// ApplyTag creates or moves a plan tag. Caller must have already called InitAPIConfig. +func ApplyTag(cfg *config.Plan, planID, tagName string) (*models.PlanTag, error) { + params := plantags.NewPutPlanTagParams(). + WithOrgName(cfg.Org). + WithPlanTagName(tagName). + WithData(&models.PlanTagSpec{ + PlanID: planID, + }) + resp, err := cfg.Client.PlanTags.PutPlanTag(params, nil) + if err != nil { + return nil, err + } + return resp.Payload, nil +} + +func applyTag(cfg *config.PlanTagApply, out io.Writer, tagName string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + tag, err := ApplyTag(cfg.Plan, cfg.PlanID, tagName) + if err != nil { + return err + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printTagDetails(out, tag) + case config.OutputFormatJSON: + return print.RawJSON(out, tag) + case config.OutputFormatYAML: + return print.RawYAML(out, tag) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/plantag/command.go b/internal/command/plantag/command.go new file mode 100644 index 00000000..50a2f3f3 --- /dev/null +++ b/internal/command/plantag/command.go @@ -0,0 +1,25 @@ +package plantag + +import ( + "github.com/signadot/cli/internal/config" + "github.com/spf13/cobra" +) + +func New(plan *config.Plan) *cobra.Command { + cfg := &config.PlanTag{Plan: plan} + + cmd := &cobra.Command{ + Use: "tag", + Short: "Manage plan tags (named references to plans, like Docker tags)", + Aliases: []string{"t"}, + } + + cmd.AddCommand( + newList(cfg), + newGet(cfg), + newApply(cfg), + newDelete(cfg), + ) + + return cmd +} diff --git a/internal/command/plantag/delete.go b/internal/command/plantag/delete.go new file mode 100644 index 00000000..664f10ca --- /dev/null +++ b/internal/command/plantag/delete.go @@ -0,0 +1,40 @@ +package plantag + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + plantags "github.com/signadot/go-sdk/client/plan_tags" + "github.com/spf13/cobra" +) + +func newDelete(tag *config.PlanTag) *cobra.Command { + cfg := &config.PlanTagDelete{PlanTag: tag} + + cmd := &cobra.Command{ + Use: "delete TAG_NAME", + Short: "Delete a plan tag", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return deleteTag(cfg, cmd.ErrOrStderr(), args[0]) + }, + } + + return cmd +} + +func deleteTag(cfg *config.PlanTagDelete, log io.Writer, tagName string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + params := plantags.NewDeletePlanTagParams(). + WithOrgName(cfg.Org). + WithPlanTagName(tagName) + _, err := cfg.Client.PlanTags.DeletePlanTag(params, nil) + if err != nil { + return err + } + fmt.Fprintf(log, "Deleted plan tag %q.\n", tagName) + return nil +} diff --git a/internal/command/plantag/get.go b/internal/command/plantag/get.go new file mode 100644 index 00000000..beb57f1b --- /dev/null +++ b/internal/command/plantag/get.go @@ -0,0 +1,50 @@ +package plantag + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + plantags "github.com/signadot/go-sdk/client/plan_tags" + "github.com/spf13/cobra" +) + +func newGet(tag *config.PlanTag) *cobra.Command { + cfg := &config.PlanTagGet{PlanTag: tag} + + cmd := &cobra.Command{ + Use: "get TAG_NAME", + Short: "Get a plan tag", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return getTag(cfg, cmd.OutOrStdout(), args[0]) + }, + } + + return cmd +} + +func getTag(cfg *config.PlanTagGet, out io.Writer, tagName string) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + params := plantags.NewGetPlanTagParams(). + WithOrgName(cfg.Org). + WithPlanTagName(tagName) + resp, err := cfg.Client.PlanTags.GetPlanTag(params, nil) + if err != nil { + return err + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printTagDetails(out, resp.Payload) + case config.OutputFormatJSON: + return print.RawJSON(out, resp.Payload) + case config.OutputFormatYAML: + return print.RawYAML(out, resp.Payload) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/plantag/list.go b/internal/command/plantag/list.go new file mode 100644 index 00000000..d87e31dd --- /dev/null +++ b/internal/command/plantag/list.go @@ -0,0 +1,48 @@ +package plantag + +import ( + "fmt" + "io" + + "github.com/signadot/cli/internal/config" + "github.com/signadot/cli/internal/print" + plantags "github.com/signadot/go-sdk/client/plan_tags" + "github.com/spf13/cobra" +) + +func newList(tag *config.PlanTag) *cobra.Command { + cfg := &config.PlanTagList{PlanTag: tag} + + cmd := &cobra.Command{ + Use: "list", + Short: "List plan tags", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return listTags(cfg, cmd.OutOrStdout()) + }, + } + + return cmd +} + +func listTags(cfg *config.PlanTagList, out io.Writer) error { + if err := cfg.InitAPIConfig(); err != nil { + return err + } + resp, err := cfg.Client.PlanTags.ListPlanTags( + plantags.NewListPlanTagsParams().WithOrgName(cfg.Org), nil) + if err != nil { + return err + } + + switch cfg.OutputFormat { + case config.OutputFormatDefault: + return printTagTable(out, resp.Payload) + case config.OutputFormatJSON: + return print.RawJSON(out, resp.Payload) + case config.OutputFormatYAML: + return print.RawYAML(out, resp.Payload) + default: + return fmt.Errorf("unsupported output format: %q", cfg.OutputFormat) + } +} diff --git a/internal/command/plantag/printers.go b/internal/command/plantag/printers.go new file mode 100644 index 00000000..015dd242 --- /dev/null +++ b/internal/command/plantag/printers.go @@ -0,0 +1,129 @@ +package plantag + +import ( + "fmt" + "io" + "text/tabwriter" + "time" + + "github.com/signadot/cli/internal/print" + "github.com/signadot/cli/internal/sdtab" + "github.com/signadot/cli/internal/utils" + "github.com/signadot/go-sdk/models" + "github.com/xeonx/timeago" +) + +type tagRow struct { + Name string `sdtab:"NAME"` + PlanID string `sdtab:"PLAN ID"` + Created string `sdtab:"CREATED"` + Updated string `sdtab:"UPDATED"` +} + +func printTagTable(out io.Writer, tags []*models.PlanTag) error { + t := sdtab.New[tagRow](out) + t.AddHeader() + for _, tag := range tags { + var planID, created, updated string + if tag.Spec != nil { + planID = tag.Spec.PlanID + } + if tag.Status != nil { + if tag.Status.CreatedAt != "" { + if ts, err := time.Parse(time.RFC3339, tag.Status.CreatedAt); err == nil { + created = timeago.NoMax(timeago.English).Format(ts) + } + } + if tag.Status.UpdatedAt != "" { + if ts, err := time.Parse(time.RFC3339, tag.Status.UpdatedAt); err == nil { + updated = timeago.NoMax(timeago.English).Format(ts) + } + } + } + t.AddRow(tagRow{ + Name: tag.Name, + PlanID: planID, + Created: created, + Updated: updated, + }) + } + return t.Flush() +} + +func printTagDetails(out io.Writer, tag *models.PlanTag) error { + tw := tabwriter.NewWriter(out, 0, 0, 3, ' ', 0) + + fmt.Fprintf(tw, "Name:\t%s\n", tag.Name) + if tag.Spec != nil { + fmt.Fprintf(tw, "Plan ID:\t%s\n", tag.Spec.PlanID) + } + if tag.Status != nil { + fmt.Fprintf(tw, "Created:\t%s\n", utils.FormatTimestamp(tag.Status.CreatedAt)) + fmt.Fprintf(tw, "Updated:\t%s\n", utils.FormatTimestamp(tag.Status.UpdatedAt)) + } + + if err := tw.Flush(); err != nil { + return err + } + + // If the tag has an inlined plan, show its details. + if tag.Plan != nil { + fmt.Fprintln(out) + fmt.Fprintln(out, "Plan:") + tw = tabwriter.NewWriter(out, 0, 0, 3, ' ', 0) + fmt.Fprintf(tw, " ID:\t%s\n", tag.Plan.ID) + if tag.Plan.Spec != nil { + fmt.Fprintf(tw, " Steps:\t%d\n", len(tag.Plan.Spec.Steps)) + if tag.Plan.Spec.Prompt != "" { + fmt.Fprintf(tw, " Prompt:\t%s\n", print.FirstLine(tag.Plan.Spec.Prompt)) + } + } + if tag.Plan.Status != nil { + fmt.Fprintf(tw, " Created:\t%s\n", utils.FormatTimestamp(tag.Plan.Status.CreatedAt)) + } + if err := tw.Flush(); err != nil { + return err + } + } + + // Print tag history if present. + if len(tag.History) > 1 { + fmt.Fprintln(out) + fmt.Fprintln(out, "History:") + return printHistoryTable(out, tag.History) + } + + return nil +} + +type historyRow struct { + PlanID string `sdtab:"PLAN ID"` + TaggedAt string `sdtab:"TAGGED"` + UntaggedAt string `sdtab:"UNTAGGED"` +} + +func printHistoryTable(out io.Writer, history []*models.TagMapping) error { + t := sdtab.New[historyRow](out) + t.AddHeader() + for _, h := range history { + tagged := "" + if h.TaggedAt != "" { + if ts, err := time.Parse(time.RFC3339, h.TaggedAt); err == nil { + tagged = timeago.NoMax(timeago.English).Format(ts) + } + } + untagged := "(current)" + if h.UntaggedAt != "" { + if ts, err := time.Parse(time.RFC3339, h.UntaggedAt); err == nil { + untagged = timeago.NoMax(timeago.English).Format(ts) + } + } + t.AddRow(historyRow{ + PlanID: h.PlanID, + TaggedAt: tagged, + UntaggedAt: untagged, + }) + } + return t.Flush() +} + diff --git a/internal/config/logs.go b/internal/config/logs.go index efc17ef5..f5ce6931 100644 --- a/internal/config/logs.go +++ b/internal/config/logs.go @@ -8,13 +8,16 @@ type Logs struct { *API Job string + Plan string + Step string Stream string TailLines uint } func (c *Logs) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVarP(&c.Job, "job", "j", "", "job name whose log lines will be displayed") - cmd.MarkFlagRequired("job") + cmd.Flags().StringVar(&c.Plan, "plan", "", "plan execution ID whose log lines will be displayed") + cmd.Flags().StringVar(&c.Step, "step", "", "step ID (used with --plan for step-level logs)") cmd.Flags().StringVarP(&c.Stream, "stream", "s", "stdout", "stream from where to display log lines (stdout or stderr)") cmd.Flags().UintVarP(&c.TailLines, "tail", "t", 0, "lines of recent log file to display, defaults to 0, showing all log lines") diff --git a/internal/config/plan.go b/internal/config/plan.go new file mode 100644 index 00000000..4e9b726e --- /dev/null +++ b/internal/config/plan.go @@ -0,0 +1,56 @@ +package config + +import "github.com/spf13/cobra" + +type Plan struct { + *API +} + +type PlanCompile struct { + *Plan + + // Flags + Filename string + Tag string +} + +func (c *PlanCompile) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&c.Filename, "filename", "f", "", "file containing the prompt to compile") + cmd.MarkFlagRequired("filename") + cmd.Flags().StringVar(&c.Tag, "tag", "", "tag the compiled plan with this name") +} + +type PlanCreate struct { + *Plan + + // Flags + Filename string + Tag string + TemplateVals TemplateVals +} + +func (c *PlanCreate) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&c.Filename, "filename", "f", "", "YAML or JSON file containing the plan spec") + cmd.MarkFlagRequired("filename") + cmd.Flags().StringVar(&c.Tag, "tag", "", "tag the created plan with this name") + cmd.Flags().Var(&c.TemplateVals, "set", "--set var=val") +} + +type PlanGet struct { + *Plan +} + +type PlanRecompile struct { + *Plan + + // Flags + Tag string +} + +func (c *PlanRecompile) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&c.Tag, "tag", "", "tag the recompiled plan with this name") +} + +type PlanDelete struct { + *Plan +} diff --git a/internal/config/planexec.go b/internal/config/planexec.go new file mode 100644 index 00000000..94f97544 --- /dev/null +++ b/internal/config/planexec.go @@ -0,0 +1,56 @@ +package config + +import "github.com/spf13/cobra" + +type PlanExecution struct { + *Plan +} + +type PlanExecGet struct { + *PlanExecution +} + +type PlanExecCancel struct { + *PlanExecution +} + +type PlanExecOutputs struct { + *PlanExecution +} + +type PlanExecGetOutput struct { + *PlanExecution + + // Flags + All bool + Dir string + Metadata bool +} + +type PlanExecList struct { + *PlanExecution + + // Flags + PlanID string + Tag string + Phase string +} + +func (c *PlanExecList) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&c.PlanID, "plan", "", "filter by plan ID") + cmd.Flags().StringVar(&c.Tag, "tag", "", "filter by plan tag name") + cmd.Flags().StringVar(&c.Phase, "phase", "", "filter by execution phase") +} + +type PlanExecLogs struct { + *PlanExecution + + // Flags + Stream string + TailLines uint +} + +func (c *PlanExecLogs) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&c.Stream, "stream", "s", "stdout", "stream type (stdout or stderr), only used with a step ID") + cmd.Flags().UintVarP(&c.TailLines, "tail", "t", 0, "number of lines from the end to show (0 = all)") +} diff --git a/internal/config/planrun.go b/internal/config/planrun.go new file mode 100644 index 00000000..50f1bae8 --- /dev/null +++ b/internal/config/planrun.go @@ -0,0 +1,28 @@ +package config + +import ( + "time" + + "github.com/spf13/cobra" +) + +type PlanRun struct { + *Plan + + // Flags + Tag string + Params TemplateVals + Wait bool + Attach bool + Timeout time.Duration + OutputDir string +} + +func (c *PlanRun) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&c.Tag, "tag", "", "run the plan referenced by this tag (alternative to plan ID argument)") + cmd.Flags().Var(&c.Params, "param", "parameter in key=value form (can be repeated)") + cmd.Flags().BoolVar(&c.Wait, "wait", true, "wait for execution to complete") + cmd.Flags().BoolVar(&c.Attach, "attach", false, "stream structured events (logs, outputs, result) to stdout") + cmd.Flags().DurationVar(&c.Timeout, "timeout", 0, "timeout for waiting (0 means no timeout)") + cmd.Flags().StringVar(&c.OutputDir, "output-dir", "", "directory to export all outputs to on completion") +} diff --git a/internal/config/plantag.go b/internal/config/plantag.go new file mode 100644 index 00000000..35e7cdbf --- /dev/null +++ b/internal/config/plantag.go @@ -0,0 +1,31 @@ +package config + +import "github.com/spf13/cobra" + +type PlanTag struct { + *Plan +} + +type PlanTagApply struct { + *PlanTag + + // Flags + PlanID string +} + +func (c *PlanTagApply) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&c.PlanID, "plan", "", "plan ID to tag") + cmd.MarkFlagRequired("plan") +} + +type PlanTagGet struct { + *PlanTag +} + +type PlanTagList struct { + *PlanTag +} + +type PlanTagDelete struct { + *PlanTag +} diff --git a/internal/print/attach.go b/internal/print/attach.go new file mode 100644 index 00000000..e3d31be3 --- /dev/null +++ b/internal/print/attach.go @@ -0,0 +1,91 @@ +package print + +import ( + "encoding/json" + "fmt" + "io" + "strings" + "sync" + "time" +) + +// AttachEvent represents a structured event emitted during --attach mode. +type AttachEvent struct { + Time time.Time `json:"time"` + Type string `json:"type"` // "log", "output", "result" + Step string `json:"step,omitempty"` // for log events + Stream string `json:"stream,omitempty"` // "stdout" or "stderr", for log events + Msg string `json:"msg,omitempty"` // for log events + Name string `json:"name,omitempty"` // for output events + Value any `json:"value,omitempty"` // for output events + ID string `json:"id,omitempty"` // for result events + Phase string `json:"phase,omitempty"` // for result events + Error string `json:"error,omitempty"` // for result events (if failed) +} + +// AttachWriter writes structured events to an io.Writer in either +// JSON (one object per line) or slog-style text format. +type AttachWriter struct { + mu sync.Mutex + out io.Writer + json bool +} + +// NewAttachWriter creates an AttachWriter. If jsonMode is true, events +// are written as JSON lines; otherwise as slog-style text. +func NewAttachWriter(out io.Writer, jsonMode bool) *AttachWriter { + return &AttachWriter{out: out, json: jsonMode} +} + +// Emit writes an event. +func (w *AttachWriter) Emit(e AttachEvent) { + if e.Time.IsZero() { + e.Time = time.Now().UTC() + } + w.mu.Lock() + defer w.mu.Unlock() + + if w.json { + data, _ := json.Marshal(e) + w.out.Write(data) + w.out.Write([]byte("\n")) + } else { + w.out.Write([]byte(formatText(e))) + w.out.Write([]byte("\n")) + } +} + +func formatText(e AttachEvent) string { + var b strings.Builder + fmt.Fprintf(&b, "time=%s", e.Time.Format(time.TimeOnly)) + fmt.Fprintf(&b, " type=%s", e.Type) + + switch e.Type { + case "log": + if e.Step != "" { + fmt.Fprintf(&b, " step=%s", e.Step) + } + if e.Stream != "" { + fmt.Fprintf(&b, " stream=%s", e.Stream) + } + fmt.Fprintf(&b, " msg=%s", quoteIfNeeded(strings.TrimRight(e.Msg, "\n"))) + case "output": + fmt.Fprintf(&b, " name=%s", e.Name) + fmt.Fprintf(&b, " value=%s", quoteIfNeeded(fmt.Sprint(e.Value))) + case "result": + fmt.Fprintf(&b, " id=%s", e.ID) + fmt.Fprintf(&b, " phase=%s", e.Phase) + if e.Error != "" { + fmt.Fprintf(&b, " error=%s", quoteIfNeeded(e.Error)) + } + } + + return b.String() +} + +func quoteIfNeeded(s string) string { + if s == "" || strings.ContainsAny(s, " \t\n\"=") { + return fmt.Sprintf("%q", s) + } + return s +} diff --git a/internal/print/sse.go b/internal/print/sse.go new file mode 100644 index 00000000..20224dc0 --- /dev/null +++ b/internal/print/sse.go @@ -0,0 +1,105 @@ +package print + +import ( + "encoding/json" + "errors" + "io" + + "github.com/jclem/sseparser" +) + +type sseEvent struct { + Event string `sse:"event"` + Data string `sse:"data"` +} + +type sseMessage struct { + Message string `json:"message"` + Cursor string `json:"cursor"` + Step string `json:"step,omitempty"` + Stream string `json:"stream,omitempty"` +} + +// ParseSSEAttach reads SSE events and emits structured AttachEvents. +// Used by plan run --attach to produce structured output. +func ParseSSEAttach(reader io.Reader, w *AttachWriter) (string, error) { + scanner := sseparser.NewStreamScanner(reader) + var lastCursor string + + for { + var e sseEvent + _, err := scanner.UnmarshalNext(&e) + if err != nil { + if errors.Is(err, sseparser.ErrStreamEOF) { + err = nil + } + return lastCursor, err + } + + switch e.Event { + case "message": + var m sseMessage + if err := json.Unmarshal([]byte(e.Data), &m); err != nil { + return lastCursor, err + } + if m.Message == "" { + continue + } + w.Emit(AttachEvent{ + Type: "log", + Step: m.Step, + Stream: m.Stream, + Msg: m.Message, + }) + lastCursor = m.Cursor + case "error": + return lastCursor, errors.New(e.Data) + case "signal": + if e.Data == "EOF" { + return lastCursor, nil + } + } + } +} + +// ParseSSEStream reads SSE events and writes message content to out. +// Returns the last cursor and any error. +func ParseSSEStream(reader io.Reader, out io.Writer) (string, error) { + scanner := sseparser.NewStreamScanner(reader) + var lastCursor string + + for { + var e sseEvent + _, err := scanner.UnmarshalNext(&e) + if err != nil { + if errors.Is(err, sseparser.ErrStreamEOF) { + err = nil + } + return lastCursor, err + } + + switch e.Event { + case "message": + var m sseMessage + err = json.Unmarshal([]byte(e.Data), &m) + if err != nil { + return lastCursor, err + } + if m.Message == "" { + continue + } + out.Write([]byte(m.Message)) + lastCursor = m.Cursor + case "error": + return lastCursor, errors.New(string(e.Data)) + case "signal": + switch e.Data { + case "EOF": + return lastCursor, nil + case "RESTART": + out.Write([]byte("\n\n-------------------------------------------------------------------------------\n")) + out.Write([]byte("WARNING: The execution has been restarted...\n\n")) + } + } + } +} diff --git a/internal/print/text.go b/internal/print/text.go new file mode 100644 index 00000000..c45b8ab2 --- /dev/null +++ b/internal/print/text.go @@ -0,0 +1,15 @@ +package print + +import "strings" + +// FirstLine returns the first line of s, trimmed and truncated to 80 chars. +func FirstLine(s string) string { + s = strings.TrimSpace(s) + if i := strings.IndexByte(s, '\n'); i >= 0 { + s = s[:i] + } + if len(s) > 80 { + s = s[:77] + "..." + } + return s +}