From 0fb61bd887e619b51547ec9ef02e5b42d1d22d17 Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:30:00 -0400 Subject: [PATCH 1/4] Show extension checksums in CLI output Expose the API-provided SHA-256 checksum in list, get, and upload tables so operators can verify extension artifacts without switching to JSON output. --- README.md | 2 +- cmd/extensions.go | 23 +++++++---------------- cmd/extensions_test.go | 12 +++++++++--- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index c54f458c..1a35d1f2 100644 --- a/README.md +++ b/README.md @@ -481,7 +481,7 @@ Per-category updates are partial — only categories you name are changed; other - `kernel extensions list` - List all uploaded extensions - `--output json`, `-o json` - Output raw JSON array -- `kernel extensions get ` - Show extension metadata (id, name, created, size, last used) +- `kernel extensions get ` - Show extension metadata (id, name, checksum, created, size, last used) - `--output json`, `-o json` - Output raw JSON object - `kernel extensions upload ` - Upload an unpacked browser extension directory - `--name ` - Optional unique extension name diff --git a/cmd/extensions.go b/cmd/extensions.go index 80f15d74..69aa33fa 100644 --- a/cmd/extensions.go +++ b/cmd/extensions.go @@ -129,15 +129,12 @@ func (e ExtensionsCmd) List(ctx context.Context, in ExtensionsListInput) error { pterm.Info.Println("No extensions found") return nil } - rows := pterm.TableData{{"Extension ID", "Name", "Created At", "Size (bytes)", "Last Used At"}} + rows := pterm.TableData{{"Extension ID", "Name", "Checksum", "Created At", "Size (bytes)", "Last Used At"}} for _, it := range items { - name := it.Name - if name == "" { - name = "-" - } rows = append(rows, []string{ it.ID, - name, + util.FirstOrDash(it.Name), + util.FirstOrDash(it.Checksum), util.FormatLocal(it.CreatedAt), fmt.Sprintf("%d", it.SizeBytes), util.FormatLocal(it.LastUsedAt), @@ -165,17 +162,14 @@ func (e ExtensionsCmd) Get(ctx context.Context, in ExtensionsGetInput) error { return util.PrintPrettyJSON(item) } - name := item.Name - if name == "" { - name = "-" - } rows := pterm.TableData{{"Property", "Value"}} rows = append(rows, []string{"ID", item.ID}) - rows = append(rows, []string{"Name", name}) + rows = append(rows, []string{"Name", util.FirstOrDash(item.Name)}) rows = append(rows, []string{"Created At", util.FormatLocal(item.CreatedAt)}) rows = append(rows, []string{"Size (bytes)", fmt.Sprintf("%d", item.SizeBytes)}) rows = append(rows, []string{"Last Used At", util.FormatLocal(item.LastUsedAt)}) PrintTableNoPad(rows, true) + pterm.Printf("Checksum: %s\n", util.FirstOrDash(item.Checksum)) return nil } @@ -432,16 +426,13 @@ func (e ExtensionsCmd) Upload(ctx context.Context, in ExtensionsUploadInput) err return util.PrintPrettyJSON(item) } - name := item.Name - if name == "" { - name = "-" - } rows := pterm.TableData{{"Property", "Value"}} rows = append(rows, []string{"ID", item.ID}) - rows = append(rows, []string{"Name", name}) + rows = append(rows, []string{"Name", util.FirstOrDash(item.Name)}) rows = append(rows, []string{"Created At", util.FormatLocal(item.CreatedAt)}) rows = append(rows, []string{"Size (bytes)", fmt.Sprintf("%d", item.SizeBytes)}) PrintTableNoPad(rows, true) + pterm.Printf("Checksum: %s\n", util.FirstOrDash(item.Checksum)) return nil } diff --git a/cmd/extensions_test.go b/cmd/extensions_test.go index 83626810..b9fc0f97 100644 --- a/cmd/extensions_test.go +++ b/cmd/extensions_test.go @@ -67,10 +67,11 @@ func (f *FakeExtensionsService) Upload(ctx context.Context, body kernel.Extensio func TestExtensionsGet_Table(t *testing.T) { buf := capturePtermOutput(t) + const checksum = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" fake := &FakeExtensionsService{ GetFunc: func(ctx context.Context, idOrName string, opts ...option.RequestOption) (*kernel.ExtensionGetResponse, error) { assert.Equal(t, "my-ext", idOrName) - return &kernel.ExtensionGetResponse{ID: "e-123", Name: "my-ext", CreatedAt: time.Unix(0, 0), SizeBytes: 42}, nil + return &kernel.ExtensionGetResponse{ID: "e-123", Name: "my-ext", Checksum: checksum, CreatedAt: time.Unix(0, 0), SizeBytes: 42}, nil }, } e := ExtensionsCmd{extensions: fake} @@ -79,6 +80,7 @@ func TestExtensionsGet_Table(t *testing.T) { out := buf.String() assert.Contains(t, out, "e-123") assert.Contains(t, out, "my-ext") + assert.Contains(t, out, "Checksum: "+checksum) assert.Contains(t, out, "42") } @@ -93,15 +95,17 @@ func TestExtensionsList_Empty(t *testing.T) { func TestExtensionsList_WithRows(t *testing.T) { buf := capturePtermOutput(t) created := time.Unix(0, 0) - rows := []kernel.ExtensionListResponse{{ID: "e1", Name: "alpha", CreatedAt: created, SizeBytes: 10}, {ID: "e2", Name: "", CreatedAt: created, SizeBytes: 20}} + rows := []kernel.ExtensionListResponse{{ID: "e1", Name: "alpha", Checksum: "abc123", CreatedAt: created, SizeBytes: 10}, {ID: "e2", Name: "", CreatedAt: created, SizeBytes: 20}} fake := &FakeExtensionsService{ListFunc: func(ctx context.Context, query kernel.ExtensionListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.ExtensionListResponse], error) { return &pagination.OffsetPagination[kernel.ExtensionListResponse]{Items: rows}, nil }} e := ExtensionsCmd{extensions: fake} _ = e.List(context.Background(), ExtensionsListInput{}) out := buf.String() + assert.Contains(t, out, "Checksum") assert.Contains(t, out, "e1") assert.Contains(t, out, "alpha") + assert.Contains(t, out, "abc123") assert.Contains(t, out, "e2") } @@ -205,13 +209,14 @@ func TestExtensionsDownloadWebStore_InvalidOS(t *testing.T) { func TestExtensionsUpload_Success(t *testing.T) { buf := capturePtermOutput(t) + const checksum = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" dir := t.TempDir() // create a sample file inside dir err := os.WriteFile(filepath.Join(dir, "manifest.json"), []byte("{}"), 0644) assert.NoError(t, err) fake := &FakeExtensionsService{UploadFunc: func(ctx context.Context, body kernel.ExtensionUploadParams, opts ...option.RequestOption) (*kernel.ExtensionUploadResponse, error) { - return &kernel.ExtensionUploadResponse{ID: "e1", Name: "myext", CreatedAt: time.Unix(0, 0), SizeBytes: 10}, nil + return &kernel.ExtensionUploadResponse{ID: "e1", Name: "myext", Checksum: checksum, CreatedAt: time.Unix(0, 0), SizeBytes: 10}, nil }} e := ExtensionsCmd{extensions: fake} _ = e.Upload(context.Background(), ExtensionsUploadInput{Dir: dir, Name: "myext"}) @@ -220,6 +225,7 @@ func TestExtensionsUpload_Success(t *testing.T) { assert.Contains(t, out, "e1") assert.Contains(t, out, "Name") assert.Contains(t, out, "myext") + assert.Contains(t, out, "Checksum: "+checksum) } func TestExtensionsUpload_InvalidDir(t *testing.T) { From d0660eb050b1448fe0b9073bffcc2d38f1ca8ce6 Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:04:35 -0400 Subject: [PATCH 2/4] test(cli): cover extension checksum output contracts --- README.md | 3 ++- cmd/extensions_test.go | 26 ++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1a35d1f2..2ac7c65f 100644 --- a/README.md +++ b/README.md @@ -479,13 +479,14 @@ Per-category updates are partial — only categories you name are changed; other ### Extension Management -- `kernel extensions list` - List all uploaded extensions +- `kernel extensions list` - List all uploaded extensions with checksums - `--output json`, `-o json` - Output raw JSON array - `kernel extensions get ` - Show extension metadata (id, name, checksum, created, size, last used) - `--output json`, `-o json` - Output raw JSON object - `kernel extensions upload ` - Upload an unpacked browser extension directory - `--name ` - Optional unique extension name - `--output json`, `-o json` - Output raw JSON object + - Successful uploads show the stored archive checksum - `kernel extensions download ` - Download an extension archive - `--to ` - Output directory (required) - `kernel extensions download-web-store ` - Download an extension from the Chrome Web Store diff --git a/cmd/extensions_test.go b/cmd/extensions_test.go index b9fc0f97..65f89095 100644 --- a/cmd/extensions_test.go +++ b/cmd/extensions_test.go @@ -4,6 +4,7 @@ import ( "archive/zip" "bytes" "context" + "encoding/json" "io" "net/http" "os" @@ -84,6 +85,26 @@ func TestExtensionsGet_Table(t *testing.T) { assert.Contains(t, out, "42") } +func TestExtensionsGet_JSONOutput(t *testing.T) { + ptermOutput := capturePtermOutput(t) + fake := &FakeExtensionsService{ + GetFunc: func(ctx context.Context, idOrName string, opts ...option.RequestOption) (*kernel.ExtensionGetResponse, error) { + var response kernel.ExtensionGetResponse + err := json.Unmarshal([]byte(`{"id":"e-123","name":"my-ext","checksum":"abc123","created_at":"1970-01-01T00:00:00Z","size_bytes":42}`), &response) + assert.NoError(t, err) + return &response, nil + }, + } + e := ExtensionsCmd{extensions: fake} + var commandErr error + stdout := captureStdout(t, func() { + commandErr = e.Get(context.Background(), ExtensionsGetInput{Identifier: "my-ext", Output: "json"}) + }) + assert.NoError(t, commandErr) + assert.JSONEq(t, `{"id":"e-123","name":"my-ext","checksum":"abc123","created_at":"1970-01-01T00:00:00Z","size_bytes":42}`, stdout) + assert.Empty(t, ptermOutput.String()) +} + func TestExtensionsList_Empty(t *testing.T) { buf := capturePtermOutput(t) fake := &FakeExtensionsService{} @@ -95,7 +116,7 @@ func TestExtensionsList_Empty(t *testing.T) { func TestExtensionsList_WithRows(t *testing.T) { buf := capturePtermOutput(t) created := time.Unix(0, 0) - rows := []kernel.ExtensionListResponse{{ID: "e1", Name: "alpha", Checksum: "abc123", CreatedAt: created, SizeBytes: 10}, {ID: "e2", Name: "", CreatedAt: created, SizeBytes: 20}} + rows := []kernel.ExtensionListResponse{{ID: "e1", Name: "alpha", Checksum: "abc123", CreatedAt: created, SizeBytes: 10}, {ID: "e2", Name: "legacy", CreatedAt: created, SizeBytes: 20}} fake := &FakeExtensionsService{ListFunc: func(ctx context.Context, query kernel.ExtensionListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.ExtensionListResponse], error) { return &pagination.OffsetPagination[kernel.ExtensionListResponse]{Items: rows}, nil }} @@ -107,6 +128,7 @@ func TestExtensionsList_WithRows(t *testing.T) { assert.Contains(t, out, "alpha") assert.Contains(t, out, "abc123") assert.Contains(t, out, "e2") + assert.Regexp(t, `legacy\s+\|\s+-`, out) } func TestExtensionsList_ForwardsLimitOffset(t *testing.T) { @@ -219,7 +241,7 @@ func TestExtensionsUpload_Success(t *testing.T) { return &kernel.ExtensionUploadResponse{ID: "e1", Name: "myext", Checksum: checksum, CreatedAt: time.Unix(0, 0), SizeBytes: 10}, nil }} e := ExtensionsCmd{extensions: fake} - _ = e.Upload(context.Background(), ExtensionsUploadInput{Dir: dir, Name: "myext"}) + assert.NoError(t, e.Upload(context.Background(), ExtensionsUploadInput{Dir: dir, Name: "myext"})) out := buf.String() assert.Contains(t, out, "ID") assert.Contains(t, out, "e1") From c20585f65810e07ed3802b3f27a9445a09def721 Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:06:27 -0400 Subject: [PATCH 3/4] test(cli): normalize colored extension output --- cmd/extensions_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/extensions_test.go b/cmd/extensions_test.go index 65f89095..d11e0f0f 100644 --- a/cmd/extensions_test.go +++ b/cmd/extensions_test.go @@ -16,6 +16,7 @@ import ( "github.com/kernel/kernel-go-sdk" "github.com/kernel/kernel-go-sdk/option" "github.com/kernel/kernel-go-sdk/packages/pagination" + "github.com/pterm/pterm" "github.com/stretchr/testify/assert" ) @@ -128,7 +129,7 @@ func TestExtensionsList_WithRows(t *testing.T) { assert.Contains(t, out, "alpha") assert.Contains(t, out, "abc123") assert.Contains(t, out, "e2") - assert.Regexp(t, `legacy\s+\|\s+-`, out) + assert.Regexp(t, `legacy\s+\|\s+-`, pterm.RemoveColorFromString(out)) } func TestExtensionsList_ForwardsLimitOffset(t *testing.T) { From db9cf98408e24687b4c726dd2b8a6c8f15d8a10b Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:14:06 -0400 Subject: [PATCH 4/4] docs(cli): clarify extension checksum availability --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ac7c65f..76dc643c 100644 --- a/README.md +++ b/README.md @@ -479,7 +479,7 @@ Per-category updates are partial — only categories you name are changed; other ### Extension Management -- `kernel extensions list` - List all uploaded extensions with checksums +- `kernel extensions list` - List all uploaded extensions, including available checksums - `--output json`, `-o json` - Output raw JSON array - `kernel extensions get ` - Show extension metadata (id, name, checksum, created, size, last used) - `--output json`, `-o json` - Output raw JSON object