Skip to content

Commit 4fb8da8

Browse files
committed
feat: add clawmachine version --all to print the full version list
1 parent 6c870da commit 4fb8da8

File tree

5 files changed

+306
-10
lines changed

5 files changed

+306
-10
lines changed

.config/mise.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ description = "Build the ClawMachine binary"
2828
depends = ["charts"]
2929
run = "cd control-plane && go build -o clawmachine ./cmd/clawmachine/ && echo '✓ Binary built'"
3030

31+
[tasks.clawmachine]
32+
description = "Run the ClawMachine CLI via go run (pass args after --)"
33+
depends = ["charts"]
34+
run = "cd control-plane && go run ./cmd/clawmachine"
35+
3136
[tasks.test]
3237
description = "Run Go tests"
3338
depends = ["charts"]

control-plane/cmd/clawmachine/main.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,6 @@ func newRootCmd() *cobra.Command {
5757
return root
5858
}
5959

60-
func newVersionCmd() *cobra.Command {
61-
return &cobra.Command{
62-
Use: "version",
63-
Short: "Print the ClawMachine version",
64-
Run: func(cmd *cobra.Command, args []string) {
65-
fmt.Println(titleStyle.Render("clawmachine") + " " + accentStyle.Render("v"+version))
66-
},
67-
}
68-
}
69-
7060
func newCompletionCmd() *cobra.Command {
7161
cmd := &cobra.Command{
7262
Use: "completion [bash|zsh|fish|powershell]",
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"crypto/sha256"
6+
"fmt"
7+
"io"
8+
"strings"
9+
10+
"github.com/spf13/cobra"
11+
"github.com/zackerydev/clawmachine/control-plane/internal/service"
12+
"helm.sh/helm/v4/pkg/chart/loader"
13+
chartv2 "helm.sh/helm/v4/pkg/chart/v2"
14+
)
15+
16+
type botChartSpec struct {
17+
name string
18+
kind service.BotType
19+
}
20+
21+
type vendoredChartSpec struct {
22+
getter func() []byte
23+
}
24+
25+
var botChartSpecs = []botChartSpec{
26+
{name: "openclaw", kind: service.BotTypeOpenClaw},
27+
{name: "picoclaw", kind: service.BotTypePicoClaw},
28+
{name: "ironclaw", kind: service.BotTypeIronClaw},
29+
{name: "busybox", kind: service.BotTypeBusyBox},
30+
}
31+
32+
var vendoredChartSpecs = []vendoredChartSpec{
33+
{getter: service.GetESOChart},
34+
{getter: service.GetCiliumChart},
35+
{getter: service.GetConnectChart},
36+
}
37+
38+
func newVersionCmd() *cobra.Command {
39+
var showAll bool
40+
cmd := &cobra.Command{
41+
Use: "version",
42+
Short: "Print the ClawMachine version",
43+
RunE: func(cmd *cobra.Command, args []string) error {
44+
return runVersion(cmd.OutOrStdout(), showAll)
45+
},
46+
}
47+
cmd.Flags().BoolVar(&showAll, "all", false, "Print CLI version plus canonical bot image refs and vendored chart checksums")
48+
return cmd
49+
}
50+
51+
func runVersion(w io.Writer, showAll bool) error {
52+
if _, err := fmt.Fprintln(w, titleStyle.Render("clawmachine")+" "+accentStyle.Render("v"+version)); err != nil {
53+
return fmt.Errorf("writing version output: %w", err)
54+
}
55+
if !showAll {
56+
return nil
57+
}
58+
59+
botImages, err := resolveBotImageRefs()
60+
if err != nil {
61+
return err
62+
}
63+
vendoredCharts, err := resolveVendoredChartChecksums()
64+
if err != nil {
65+
return err
66+
}
67+
68+
if _, err := fmt.Fprintln(w); err != nil {
69+
return fmt.Errorf("writing bot image section spacing: %w", err)
70+
}
71+
if _, err := fmt.Fprintln(w, "bot images (canonical repo:tag):"); err != nil {
72+
return fmt.Errorf("writing bot image section header: %w", err)
73+
}
74+
for _, bot := range botImages {
75+
if _, err := fmt.Fprintf(w, " - %s: %s:%s\n", bot.name, bot.repository, bot.tag); err != nil {
76+
return fmt.Errorf("writing bot image ref for %s: %w", bot.name, err)
77+
}
78+
}
79+
80+
if _, err := fmt.Fprintln(w); err != nil {
81+
return fmt.Errorf("writing vendored chart section spacing: %w", err)
82+
}
83+
if _, err := fmt.Fprintln(w, "vendored charts (sha256):"); err != nil {
84+
return fmt.Errorf("writing vendored chart section header: %w", err)
85+
}
86+
for _, chart := range vendoredCharts {
87+
if _, err := fmt.Fprintf(w, " - %s@%s: %s\n", chart.name, chart.version, chart.sha256); err != nil {
88+
return fmt.Errorf("writing vendored chart checksum for %s: %w", chart.name, err)
89+
}
90+
}
91+
92+
return nil
93+
}
94+
95+
type botImageRef struct {
96+
name string
97+
repository string
98+
tag string
99+
}
100+
101+
func resolveBotImageRefs() ([]botImageRef, error) {
102+
refs := make([]botImageRef, 0, len(botChartSpecs))
103+
for _, spec := range botChartSpecs {
104+
archive, err := service.GetEmbeddedChart(spec.kind)
105+
if err != nil {
106+
return nil, fmt.Errorf("resolving embedded %s chart: %w", spec.name, err)
107+
}
108+
109+
chrt, err := loader.LoadArchive(bytes.NewReader(archive))
110+
if err != nil {
111+
return nil, fmt.Errorf("loading embedded %s chart: %w", spec.name, err)
112+
}
113+
chrtV2, err := asV2Chart(chrt)
114+
if err != nil {
115+
return nil, fmt.Errorf("accessing embedded %s chart: %w", spec.name, err)
116+
}
117+
118+
repo, tag, err := imageRefFromValues(chrtV2.Values)
119+
if err != nil {
120+
return nil, fmt.Errorf("resolving image ref for %s: %w", spec.name, err)
121+
}
122+
refs = append(refs, botImageRef{name: spec.name, repository: repo, tag: tag})
123+
}
124+
return refs, nil
125+
}
126+
127+
type vendoredChartChecksum struct {
128+
name string
129+
version string
130+
sha256 string
131+
}
132+
133+
func resolveVendoredChartChecksums() ([]vendoredChartChecksum, error) {
134+
checksums := make([]vendoredChartChecksum, 0, len(vendoredChartSpecs))
135+
for _, spec := range vendoredChartSpecs {
136+
archive := spec.getter()
137+
chrt, err := loader.LoadArchive(bytes.NewReader(archive))
138+
if err != nil {
139+
return nil, fmt.Errorf("loading embedded vendored chart: %w", err)
140+
}
141+
chrtV2, err := asV2Chart(chrt)
142+
if err != nil {
143+
return nil, fmt.Errorf("accessing embedded vendored chart: %w", err)
144+
}
145+
146+
name := strings.TrimSpace(chrtV2.Name())
147+
version := ""
148+
if chrtV2.Metadata != nil {
149+
version = strings.TrimSpace(chrtV2.Metadata.Version)
150+
}
151+
if name == "" || version == "" {
152+
return nil, fmt.Errorf("vendored chart metadata is incomplete")
153+
}
154+
155+
sum := sha256.Sum256(archive)
156+
checksums = append(checksums, vendoredChartChecksum{
157+
name: name,
158+
version: version,
159+
sha256: "sha256:" + fmt.Sprintf("%x", sum[:]),
160+
})
161+
}
162+
return checksums, nil
163+
}
164+
165+
func imageRefFromValues(values map[string]any) (repository, tag string, err error) {
166+
if values == nil {
167+
return "", "", fmt.Errorf("chart values are empty")
168+
}
169+
imageAny, ok := values["image"]
170+
if !ok {
171+
return "", "", fmt.Errorf("image values are missing")
172+
}
173+
imageMap, ok := imageAny.(map[string]any)
174+
if !ok {
175+
return "", "", fmt.Errorf("image values have unexpected type %T", imageAny)
176+
}
177+
178+
repoAny, ok := imageMap["repository"]
179+
if !ok {
180+
return "", "", fmt.Errorf("image.repository is missing")
181+
}
182+
tagAny, ok := imageMap["tag"]
183+
if !ok {
184+
return "", "", fmt.Errorf("image.tag is missing")
185+
}
186+
187+
repository = strings.TrimSpace(fmt.Sprintf("%v", repoAny))
188+
tag = strings.TrimSpace(fmt.Sprintf("%v", tagAny))
189+
if repository == "" || tag == "" {
190+
return "", "", fmt.Errorf("image.repository or image.tag is empty")
191+
}
192+
return repository, tag, nil
193+
}
194+
195+
func asV2Chart(chrt any) (*chartv2.Chart, error) {
196+
chrtV2, ok := chrt.(*chartv2.Chart)
197+
if !ok || chrtV2 == nil {
198+
return nil, fmt.Errorf("unexpected chart type %T", chrt)
199+
}
200+
return chrtV2, nil
201+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"regexp"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestVersionCommand_Default(t *testing.T) {
11+
origVersion := version
12+
version = "1.2.3"
13+
defer func() { version = origVersion }()
14+
15+
cmd := newVersionCmd()
16+
var out bytes.Buffer
17+
cmd.SetOut(&out)
18+
cmd.SetErr(&out)
19+
20+
if err := cmd.Execute(); err != nil {
21+
t.Fatalf("execute version command: %v", err)
22+
}
23+
24+
got := out.String()
25+
if !strings.Contains(got, "clawmachine") {
26+
t.Fatalf("output missing clawmachine name: %q", got)
27+
}
28+
if !strings.Contains(got, "v1.2.3") {
29+
t.Fatalf("output missing version: %q", got)
30+
}
31+
if strings.Contains(got, "bot images (canonical repo:tag):") {
32+
t.Fatalf("unexpected --all output for default version command: %q", got)
33+
}
34+
}
35+
36+
func TestVersionCommand_All(t *testing.T) {
37+
origVersion := version
38+
version = "0.1.0"
39+
defer func() { version = origVersion }()
40+
41+
cmd := newVersionCmd()
42+
var out bytes.Buffer
43+
cmd.SetOut(&out)
44+
cmd.SetErr(&out)
45+
cmd.SetArgs([]string{"--all"})
46+
47+
if err := cmd.Execute(); err != nil {
48+
t.Fatalf("execute version --all command: %v", err)
49+
}
50+
51+
got := out.String()
52+
for _, want := range []string{
53+
"clawmachine",
54+
"v0.1.0",
55+
"bot images (canonical repo:tag):",
56+
"openclaw:",
57+
"picoclaw:",
58+
"ironclaw:",
59+
"busybox:",
60+
"vendored charts (sha256):",
61+
62+
63+
64+
} {
65+
if !strings.Contains(got, want) {
66+
t.Fatalf("output missing %q:\n%s", want, got)
67+
}
68+
}
69+
70+
shaPattern := regexp.MustCompile(`sha256:[a-f0-9]{64}`)
71+
matches := shaPattern.FindAllString(got, -1)
72+
if len(matches) < 3 {
73+
t.Fatalf("expected at least 3 sha256 checksums, got %d:\n%s", len(matches), got)
74+
}
75+
}

docs/content/docs/cli.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ clawmachine backup
2020
clawmachine restore
2121
clawmachine completion [bash|zsh|fish|powershell]
2222
clawmachine version
23+
clawmachine version --all
2324
```
2425

2526
## Global Flags
@@ -119,6 +120,30 @@ clawmachine completion bash
119120

120121
```bash
121122
clawmachine version
123+
clawmachine version --all
124+
```
125+
126+
`clawmachine version` prints the CLI version.
127+
128+
`clawmachine version --all` also prints:
129+
- Canonical bot image refs (`repo:tag`) from embedded bot charts
130+
- SHA256 checksums for vendored embedded charts (External Secrets, Cilium, Connect)
131+
132+
Example:
133+
134+
```text
135+
clawmachine v0.1.0
136+
137+
bot images (canonical repo:tag):
138+
- openclaw: ghcr.io/zackerydev/openclaw:0.1.0
139+
- picoclaw: ghcr.io/zackerydev/picoclaw:0.1.0
140+
- ironclaw: ghcr.io/zackerydev/ironclaw:0.1.0
141+
- busybox: ghcr.io/zackerydev/theclawmachine-toolbox:0.1.0
142+
143+
vendored charts (sha256):
144+
- [email protected]: sha256:<digest>
145+
- [email protected]: sha256:<digest>
146+
- [email protected]: sha256:<digest>
122147
```
123148

124149
## HTTP API Endpoints (Serve Mode)

0 commit comments

Comments
 (0)