Skip to content

Fix project list pagination - #220

Merged
rgarcia merged 3 commits into
mainfrom
hypeship/fix-projects-pagination
Aug 9, 2026
Merged

Fix project list pagination#220
rgarcia merged 3 commits into
mainfrom
hypeship/fix-projects-pagination

Conversation

@rgarcia

@rgarcia rgarcia commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • request 100 projects by default instead of relying on the API's 20-item default
  • add --limit and --offset flags and show absolute zero-based indexes in table output
  • use the API's next-offset header to print an exact continuation command when output is truncated

Replays

Testing

  • go vet ./...
  • go test ./...
  • live smoke test: kernel projects list --limit 1

Note

Low Risk
Read-only CLI listing changes with validation and tests; no auth or mutating API behavior.

Overview
kernel projects list now sends explicit limit (default 100, max 100) and offset to the API instead of inheriting a smaller server default, and reads X-Has-More / X-Next-Offset from the HTTP response to drive continuation.

Table output adds a zero-based idx column aligned with --offset, and when more pages exist the CLI prints a ready-to-run kernel projects list --limit … --offset … command. --output json returns { "projects": [...], "next_offset": <n> } (omitting next_offset on the last page). Invalid flag ranges and inconsistent pagination headers fail fast with clear errors.

README documents the new flags and JSON shape; tests cover integration, header parsing, and JSON marshaling for empty pages.

Reviewed by Cursor Bugbot for commit 17ae47b. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Missing JSON pagination cursor
    • Added --output json support to projects list that returns a {projects, next_offset} envelope (omitting next_offset when absent) and wired the output flag through command input handling.

Create PR

Or push these changes by commenting:

@cursor push 8b7d2640e7
Preview (8b7d2640e7)
diff --git a/cmd/projects.go b/cmd/projects.go
--- a/cmd/projects.go
+++ b/cmd/projects.go
@@ -2,6 +2,7 @@
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"strconv"
@@ -42,6 +43,7 @@
 type ProjectsListInput struct {
 	Limit  int
 	Offset int
+	Output string
 }
 
 type ProjectsCreateInput struct {
@@ -92,6 +94,9 @@
 }
 
 func (c ProjectsCmd) List(ctx context.Context, in ProjectsListInput) error {
+	if err := validateJSONOutput(in.Output); err != nil {
+		return err
+	}
 	if in.Limit < 1 || in.Limit > 100 {
 		return fmt.Errorf("--limit must be between 1 and 100")
 	}
@@ -108,13 +113,37 @@
 		return util.CleanedUpSdkError{Err: err}
 	}
 
-	if projects == nil || len(projects.Items) == 0 {
+	items := []kernel.Project{}
+	if projects != nil {
+		items = projects.Items
+	}
+
+	nextOffset := projectListNextOffsetRaw(response)
+	if in.Output == "json" {
+		payload := struct {
+			Projects   []kernel.Project `json:"projects"`
+			NextOffset string           `json:"next_offset,omitempty"`
+		}{
+			Projects: items,
+		}
+		if nextOffset != "" {
+			payload.NextOffset = nextOffset
+		}
+		data, err := json.MarshalIndent(payload, "", "  ")
+		if err != nil {
+			return err
+		}
+		fmt.Println(string(data))
+		return nil
+	}
+
+	if len(items) == 0 {
 		pterm.Info.Println("No projects found")
 		return nil
 	}
 
 	table := pterm.TableData{{"ID", "Name", "Status", "Created At", "idx"}}
-	for i, p := range projects.Items {
+	for i, p := range items {
 		table = append(table, []string{
 			p.ID,
 			p.Name,
@@ -125,20 +154,24 @@
 	}
 	PrintTableNoPad(table, true)
 
-	if nextOffset, ok := projectListNextOffset(response); ok {
+	if nextOffset, ok := projectListNextOffset(nextOffset); ok {
 		pterm.Warning.Printfln(
 			"Output truncated after index %d. Continue with: kernel projects list --limit %d --offset %d",
-			in.Offset+len(projects.Items)-1, in.Limit, nextOffset,
+			in.Offset+len(items)-1, in.Limit, nextOffset,
 		)
 	}
 	return nil
 }
 
-func projectListNextOffset(response *http.Response) (int, bool) {
+func projectListNextOffsetRaw(response *http.Response) string {
 	if response == nil {
-		return 0, false
+		return ""
 	}
-	nextOffset, err := strconv.Atoi(response.Header.Get("X-Next-Offset"))
+	return strings.TrimSpace(response.Header.Get("X-Next-Offset"))
+}
+
+func projectListNextOffset(nextOffsetRaw string) (int, bool) {
+	nextOffset, err := strconv.Atoi(nextOffsetRaw)
 	return nextOffset, err == nil && nextOffset > 0
 }
 
@@ -362,7 +395,8 @@
 	c := getProjectsHandler(cmd)
 	limit, _ := cmd.Flags().GetInt("limit")
 	offset, _ := cmd.Flags().GetInt("offset")
-	return c.List(cmd.Context(), ProjectsListInput{Limit: limit, Offset: offset})
+	output, _ := cmd.Flags().GetString("output")
+	return c.List(cmd.Context(), ProjectsListInput{Limit: limit, Offset: offset, Output: output})
 }
 
 func runProjectsCreate(cmd *cobra.Command, args []string) error {
@@ -518,6 +552,7 @@
 func init() {
 	projectsListCmd.Flags().Int("limit", 100, "Maximum number of projects to return (1-100)")
 	projectsListCmd.Flags().Int("offset", 0, "Number of projects to skip (for pagination)")
+	addJSONOutputFlag(projectsListCmd)
 
 	projectsUpdateCmd.Flags().String("name", "", "New project name (1-255 characters)")
 	projectsUpdateCmd.Flags().String("status", "", "New project status: active or archived")

diff --git a/cmd/projects_test.go b/cmd/projects_test.go
--- a/cmd/projects_test.go
+++ b/cmd/projects_test.go
@@ -2,6 +2,7 @@
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"net/http"
 	"testing"
@@ -103,6 +104,36 @@
 	assert.Contains(t, out, "21")
 }
 
+func TestProjectsList_JSONOutputEnvelope(t *testing.T) {
+	fakeProjects := &FakeProjectsService{
+		ListFunc: func(ctx context.Context, query kernel.ProjectListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.Project], error) {
+			return &pagination.OffsetPagination[kernel.Project]{
+				Items: []kernel.Project{
+					{ID: "proj_1", Name: "one", Status: kernel.ProjectStatusActive},
+					{ID: "proj_2", Name: "two", Status: kernel.ProjectStatusArchived},
+				},
+			}, nil
+		},
+	}
+	c := ProjectsCmd{projects: fakeProjects, limits: &FakeProjectLimitsService{}}
+
+	out := captureStdout(t, func() {
+		err := c.List(context.Background(), ProjectsListInput{Limit: 2, Offset: 0, Output: "json"})
+		assert.NoError(t, err)
+	})
+
+	var payload struct {
+		Projects   []kernel.Project `json:"projects"`
+		NextOffset string           `json:"next_offset"`
+	}
+	if !assert.NoError(t, json.Unmarshal([]byte(out), &payload)) {
+		return
+	}
+	assert.Len(t, payload.Projects, 2)
+	assert.Equal(t, "proj_1", payload.Projects[0].ID)
+	assert.Empty(t, payload.NextOffset)
+}
+
 func TestProjectsList_RejectsInvalidPagination(t *testing.T) {
 	fakeProjects := &FakeProjectsService{
 		ListFunc: func(ctx context.Context, query kernel.ProjectListParams, opts ...option.RequestOption) (*pagination.OffsetPagination[kernel.Project], error) {
@@ -123,13 +154,16 @@
 
 func TestProjectListNextOffset(t *testing.T) {
 	response := &http.Response{Header: http.Header{"X-Next-Offset": []string{"120"}}}
-	nextOffset, ok := projectListNextOffset(response)
+	nextOffsetRaw := projectListNextOffsetRaw(response)
+	assert.Equal(t, "120", nextOffsetRaw)
+
+	nextOffset, ok := projectListNextOffset(nextOffsetRaw)
 	assert.True(t, ok)
 	assert.Equal(t, 120, nextOffset)
 
-	_, ok = projectListNextOffset(&http.Response{Header: http.Header{}})
+	_, ok = projectListNextOffset(projectListNextOffsetRaw(&http.Response{Header: http.Header{}}))
 	assert.False(t, ok)
-	_, ok = projectListNextOffset(nil)
+	_, ok = projectListNextOffset(projectListNextOffsetRaw(nil))
 	assert.False(t, ok)
 }

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 29d5d69. Configure here.

Comment thread cmd/projects.go
@rgarcia
rgarcia requested a review from masnwilliams August 9, 2026 12:52

@masnwilliams masnwilliams left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requesting changes on three maintainability issues:

  1. projectListNextOffset collapses a missing response, malformed/missing cursor, and the valid terminal cursor into the same "no more results" state. it also infers hasMore from X-Next-Offset instead of consuming X-Has-More. please parse both headers into one explicit pagination result and return an error when has_more=true lacks a positive valid cursor. otherwise incomplete output can be presented as complete.

  2. marshalProjectsListJSON decomposes the page into per-item RawJSON values and reconstructs an array that the SDK page already retains in projects.RawJSON(). please preserve the page payload directly as a single json.RawMessage (with [] only for an absent/empty page), or put shared paginated-envelope handling in pkg/util/json.go. this deletes the loop and the silent {} fallback policy.

  3. the tests do not exercise the central integration path. the fake ignores option.WithResponseInto, so ProjectsCmd.List never receives pagination headers; removing the response option, warning, or JSON cursor wiring would leave the suite green. the index assertions are also vacuous because 20 and 21 already appear in the project IDs. please add an httptest path through the real SDK or make pagination metadata explicit in the service seam, and assert indexes using IDs/names that do not contain the expected values.

go test ./..., go vet ./..., and the targeted projects race tests pass. the file remains well below the 1k-line threshold; these are focused boundary, simplification, and coverage concerns rather than a broad file-size issue.

@rgarcia

rgarcia commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

addressed all three review points in 17ae47b:

  • pagination now parses both X-Has-More and X-Next-Offset into an explicit result, rejects missing/malformed/inconsistent metadata, and treats false + 0 as the terminal page
  • JSON output now embeds the SDK page payload as one json.RawMessage; only absent/empty pages use []
  • list coverage now runs through an httptest server and the real SDK, asserting query passthrough, response-header wiring, exact row indexes, the continuation command, and the JSON cursor

verified with go vet ./..., go test ./..., targeted race tests, and live first/terminal/JSON page smoke tests.

@rgarcia
rgarcia requested a review from masnwilliams August 9, 2026 21:34

@masnwilliams masnwilliams left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-checked 17ae47b. all three requested changes are addressed: pagination metadata is validated explicitly, JSON preserves the SDK page payload directly, and the real-SDK httptest coverage exercises headers, continuation output, JSON cursors, and exact indexes. go test ./..., go vet ./..., and the targeted projects race tests pass.

@rgarcia
rgarcia merged commit 5c50d0d into main Aug 9, 2026
7 checks passed
@rgarcia
rgarcia deleted the hypeship/fix-projects-pagination branch August 9, 2026 21:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants