diff --git a/src/commands/blueprint/list.tsx b/src/commands/blueprint/list.tsx
index 7a50e62..00222c5 100644
--- a/src/commands/blueprint/list.tsx
+++ b/src/commands/blueprint/list.tsx
@@ -15,7 +15,7 @@ import { Operation } from "../../components/OperationsMenu.js";
import { ActionsPopup } from "../../components/ActionsPopup.js";
import { formatTimeAgo } from "../../components/ResourceListView.js";
import { SearchBar } from "../../components/SearchBar.js";
-import { output, outputError } from "../../utils/output.js";
+import { output, outputError, parseLimit } from "../../utils/output.js";
import { getBlueprintUrl } from "../../utils/url.js";
import { colors } from "../../utils/theme.js";
import { getStatusDisplay } from "../../components/StatusBadge.js";
@@ -148,7 +148,7 @@ const ListBlueprintsUI = ({
const result = {
items: pageBlueprints,
hasMore: page.has_more || false,
- totalCount: page.total_count || pageBlueprints.length,
+ totalCount: pageBlueprints.length,
};
return result;
@@ -179,7 +179,7 @@ const ListBlueprintsUI = ({
!executingOperation &&
!showDeleteConfirm &&
!search.searchMode,
- deps: [PAGE_SIZE, search.submittedSearchQuery],
+ deps: [search.submittedSearchQuery],
});
// Memoize columns array
@@ -624,10 +624,18 @@ const ListBlueprintsUI = ({
bindings: {
up: () => {
if (selectedIndex > 0) setSelectedIndex(selectedIndex - 1);
+ else if (!loading && !navigating && hasPrev) {
+ prevPage();
+ setSelectedIndex(blueprints.length - 1);
+ }
},
down: () => {
if (selectedIndex < blueprints.length - 1)
setSelectedIndex(selectedIndex + 1);
+ else if (!loading && !navigating && hasMore) {
+ nextPage();
+ setSelectedIndex(0);
+ }
},
n: goToNextPage,
right: goToNextPage,
@@ -875,7 +883,7 @@ const ListBlueprintsUI = ({
data={blueprints}
keyExtractor={(blueprint: BlueprintListItem) => blueprint.id}
selectedIndex={selectedIndex}
- title={`blueprints[${totalCount}]`}
+ title={`blueprints[${hasMore ? `${totalCount}+` : totalCount}]`}
columns={blueprintColumns}
emptyState={
@@ -889,7 +897,7 @@ const ListBlueprintsUI = ({
{!showPopup && (
- {figures.hamburger} {totalCount}
+ {figures.hamburger} {hasMore ? `${totalCount}+` : totalCount}
{" "}
@@ -907,7 +915,8 @@ const ListBlueprintsUI = ({
) : (
- Page {currentPage + 1} of {totalPages}
+ Page {currentPage + 1} of{" "}
+ {hasMore ? `${totalPages}+` : totalPages}
)}
>
@@ -917,7 +926,8 @@ const ListBlueprintsUI = ({
•{" "}
- Showing {startIndex + 1}-{endIndex} of {totalCount}
+ Showing {startIndex + 1}-{endIndex} of{" "}
+ {hasMore ? `${totalCount}+` : totalCount}
{search.submittedSearchQuery && (
<>
@@ -982,6 +992,7 @@ const ListBlueprintsUI = ({
interface ListBlueprintsOptions {
name?: string;
+ limit?: string;
output?: string;
}
@@ -992,23 +1003,45 @@ export async function listBlueprints(options: ListBlueprintsOptions = {}) {
try {
const client = getClient();
- // Build query params
- const queryParams: Record = {
- limit: DEFAULT_PAGE_SIZE,
- };
- if (options.name) {
- queryParams.name = options.name;
- }
+ const maxResults = parseLimit(options.limit);
+ const allBlueprints: unknown[] = [];
+ let startingAfter: string | undefined;
- // Fetch blueprints
- const page = (await client.blueprints.list(
- queryParams,
- )) as BlueprintsCursorIDPage<{ id: string }>;
+ do {
+ const remaining = maxResults - allBlueprints.length;
+ // Build query params
+ const queryParams: Record = {
+ limit: Math.min(DEFAULT_PAGE_SIZE, remaining),
+ };
+ if (options.name) {
+ queryParams.name = options.name;
+ }
+ if (startingAfter) {
+ queryParams.starting_after = startingAfter;
+ }
- // Extract blueprints array
- const blueprints = page.blueprints || [];
+ // Fetch one page
+ const page = (await client.blueprints.list(
+ queryParams,
+ )) as BlueprintsCursorIDPage<{ id: string }>;
+
+ const pageBlueprints = page.blueprints || [];
+ allBlueprints.push(...pageBlueprints);
+
+ if (
+ page.has_more &&
+ pageBlueprints.length > 0 &&
+ allBlueprints.length < maxResults
+ ) {
+ startingAfter = (
+ pageBlueprints[pageBlueprints.length - 1] as { id: string }
+ ).id;
+ } else {
+ startingAfter = undefined;
+ }
+ } while (startingAfter !== undefined);
- output(blueprints, { format: options.output, defaultFormat: "json" });
+ output(allBlueprints, { format: options.output, defaultFormat: "json" });
} catch (error) {
outputError("Failed to list blueprints", error);
}
diff --git a/src/commands/devbox/list.tsx b/src/commands/devbox/list.tsx
index 7abe0b1..e09c409 100644
--- a/src/commands/devbox/list.tsx
+++ b/src/commands/devbox/list.tsx
@@ -12,7 +12,7 @@ import type { Column } from "../../components/Table.js";
import { Table, createTextColumn } from "../../components/Table.js";
import { formatTimeAgo } from "../../components/ResourceListView.js";
import { SearchBar } from "../../components/SearchBar.js";
-import { output, outputError } from "../../utils/output.js";
+import { output, outputError, parseLimit } from "../../utils/output.js";
import { DevboxDetailPage } from "../../components/DevboxDetailPage.js";
import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
import { ResourceActionsMenu } from "../../components/ResourceActionsMenu.js";
@@ -117,7 +117,7 @@ const ListDevboxesUI = ({
const result = {
items: pageDevboxes,
hasMore: page.has_more || false,
- totalCount: page.total_count || pageDevboxes.length,
+ totalCount: pageDevboxes.length,
};
return result;
@@ -148,7 +148,7 @@ const ListDevboxesUI = ({
!showActions &&
!showPopup &&
!search.searchMode,
- deps: [status, search.submittedSearchQuery, PAGE_SIZE],
+ deps: [status, search.submittedSearchQuery],
});
// Sync devboxes to store for detail screen
@@ -559,10 +559,18 @@ const ListDevboxesUI = ({
bindings: {
up: () => {
if (selectedIndex > 0) setSelectedIndex(selectedIndex - 1);
+ else if (!loading && !navigating && hasPrev) {
+ prevPage();
+ setSelectedIndex(devboxes.length - 1);
+ }
},
down: () => {
if (selectedIndex < devboxes.length - 1)
setSelectedIndex(selectedIndex + 1);
+ else if (!loading && !navigating && hasMore) {
+ nextPage();
+ setSelectedIndex(0);
+ }
},
n: goToNextPage,
right: goToNextPage,
@@ -712,7 +720,7 @@ const ListDevboxesUI = ({
{!showPopup && (
- {figures.hamburger} {totalCount}
+ {figures.hamburger} {hasMore ? `${totalCount}+` : totalCount}
{" "}
@@ -730,7 +738,8 @@ const ListDevboxesUI = ({
) : (
- Page {currentPage + 1} of {totalPages}
+ Page {currentPage + 1} of{" "}
+ {hasMore ? `${totalPages}+` : totalPages}
)}
>
@@ -740,7 +749,8 @@ const ListDevboxesUI = ({
•{" "}
- Showing {startIndex + 1}-{endIndex} of {totalCount}
+ Showing {startIndex + 1}-{endIndex} of{" "}
+ {hasMore ? `${totalCount}+` : totalCount}
{search.submittedSearchQuery && (
<>
@@ -796,23 +806,45 @@ export async function listDevboxes(options: ListOptions) {
try {
const client = getClient();
- // Build query params
- const queryParams: Record = {
- limit: options.limit ? parseInt(options.limit, 10) : DEFAULT_PAGE_SIZE,
- };
- if (options.status) {
- queryParams.status = options.status;
- }
+ const maxResults = parseLimit(options.limit);
+ const allDevboxes: unknown[] = [];
+ let startingAfter: string | undefined;
- // Fetch devboxes
- const page = (await client.devboxes.list(
- queryParams,
- )) as DevboxesCursorIDPage<{ id: string }>;
+ do {
+ const remaining = maxResults - allDevboxes.length;
+ // Build query params
+ const queryParams: Record = {
+ limit: Math.min(DEFAULT_PAGE_SIZE, remaining),
+ };
+ if (options.status) {
+ queryParams.status = options.status;
+ }
+ if (startingAfter) {
+ queryParams.starting_after = startingAfter;
+ }
- // Extract devboxes array
- const devboxes = page.devboxes || [];
+ // Fetch one page
+ const page = (await client.devboxes.list(
+ queryParams,
+ )) as DevboxesCursorIDPage<{ id: string }>;
+
+ const pageDevboxes = page.devboxes || [];
+ allDevboxes.push(...pageDevboxes);
+
+ if (
+ page.has_more &&
+ pageDevboxes.length > 0 &&
+ allDevboxes.length < maxResults
+ ) {
+ startingAfter = (
+ pageDevboxes[pageDevboxes.length - 1] as { id: string }
+ ).id;
+ } else {
+ startingAfter = undefined;
+ }
+ } while (startingAfter !== undefined);
- output(devboxes, { format: options.output, defaultFormat: "json" });
+ output(allDevboxes, { format: options.output, defaultFormat: "json" });
} catch (error) {
outputError("Failed to list devboxes", error);
}
diff --git a/src/commands/gateway-config/list.tsx b/src/commands/gateway-config/list.tsx
index 0726366..91b0a4b 100644
--- a/src/commands/gateway-config/list.tsx
+++ b/src/commands/gateway-config/list.tsx
@@ -14,7 +14,7 @@ import { ActionsPopup } from "../../components/ActionsPopup.js";
import { Operation } from "../../components/OperationsMenu.js";
import { formatTimeAgo } from "../../components/ResourceListView.js";
import { SearchBar } from "../../components/SearchBar.js";
-import { output, outputError } from "../../utils/output.js";
+import { output, outputError, parseLimit } from "../../utils/output.js";
import { colors } from "../../utils/theme.js";
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
@@ -26,6 +26,7 @@ import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
interface ListOptions {
name?: string;
+ limit?: string;
output?: string;
}
@@ -162,7 +163,7 @@ const ListGatewayConfigsUI = ({
const result = {
items: pageConfigs,
hasMore: page.has_more || false,
- totalCount: page.total_count || pageConfigs.length,
+ totalCount: pageConfigs.length,
};
return result;
@@ -195,7 +196,7 @@ const ListGatewayConfigsUI = ({
!showEditConfig &&
!showDeleteConfirm &&
!search.searchMode,
- deps: [PAGE_SIZE, search.submittedSearchQuery],
+ deps: [search.submittedSearchQuery],
});
// Operations for a specific gateway config (shown in popup)
@@ -428,8 +429,26 @@ const ListGatewayConfigsUI = ({
// Handle list view navigation
if (key.upArrow && selectedIndex > 0) {
setSelectedIndex(selectedIndex - 1);
+ } else if (
+ key.upArrow &&
+ selectedIndex === 0 &&
+ !loading &&
+ !navigating &&
+ hasPrev
+ ) {
+ prevPage();
+ setSelectedIndex(pageConfigs - 1);
} else if (key.downArrow && selectedIndex < pageConfigs - 1) {
setSelectedIndex(selectedIndex + 1);
+ } else if (
+ key.downArrow &&
+ selectedIndex === pageConfigs - 1 &&
+ !loading &&
+ !navigating &&
+ hasMore
+ ) {
+ nextPage();
+ setSelectedIndex(0);
} else if (
(input === "n" || key.rightArrow) &&
!loading &&
@@ -630,7 +649,7 @@ const ListGatewayConfigsUI = ({
data={configs}
keyExtractor={(config: GatewayConfigListItem) => config.id}
selectedIndex={selectedIndex}
- title={`gateway_configs[${totalCount}]`}
+ title={`gateway_configs[${hasMore ? `${totalCount}+` : totalCount}]`}
columns={columns}
emptyState={
@@ -645,7 +664,7 @@ const ListGatewayConfigsUI = ({
{!showPopup && (
- {figures.hamburger} {totalCount}
+ {figures.hamburger} {hasMore ? `${totalCount}+` : totalCount}
{" "}
@@ -663,7 +682,8 @@ const ListGatewayConfigsUI = ({
) : (
- Page {currentPage + 1} of {totalPages}
+ Page {currentPage + 1} of{" "}
+ {hasMore ? `${totalPages}+` : totalPages}
)}
>
@@ -673,7 +693,8 @@ const ListGatewayConfigsUI = ({
•{" "}
- Showing {startIndex + 1}-{endIndex} of {totalCount}
+ Showing {startIndex + 1}-{endIndex} of{" "}
+ {hasMore ? `${totalCount}+` : totalCount}
{search.submittedSearchQuery && (
<>
@@ -744,23 +765,44 @@ export async function listGatewayConfigs(options: ListOptions = {}) {
try {
const client = getClient();
- // Build query params
- const queryParams: Record = {
- limit: DEFAULT_PAGE_SIZE,
- };
- if (options.name) {
- queryParams.name = options.name;
- }
+ const maxResults = parseLimit(options.limit);
+ const allConfigs: unknown[] = [];
+ let startingAfter: string | undefined;
- // Fetch gateway configs
- const page = (await client.gatewayConfigs.list(
- queryParams,
- )) as GatewayConfigsCursorIDPage<{ id: string }>;
+ do {
+ const remaining = maxResults - allConfigs.length;
+ // Build query params
+ const queryParams: Record = {
+ limit: Math.min(DEFAULT_PAGE_SIZE, remaining),
+ };
+ if (options.name) {
+ queryParams.name = options.name;
+ }
+ if (startingAfter) {
+ queryParams.starting_after = startingAfter;
+ }
- // Extract gateway configs array
- const gatewayConfigs = page.gateway_configs || [];
+ // Fetch one page
+ const page = (await client.gatewayConfigs.list(
+ queryParams,
+ )) as GatewayConfigsCursorIDPage<{ id: string }>;
+
+ const pageConfigs = page.gateway_configs || [];
+ allConfigs.push(...pageConfigs);
+
+ if (
+ page.has_more &&
+ pageConfigs.length > 0 &&
+ allConfigs.length < maxResults
+ ) {
+ startingAfter = (pageConfigs[pageConfigs.length - 1] as { id: string })
+ .id;
+ } else {
+ startingAfter = undefined;
+ }
+ } while (startingAfter !== undefined);
- output(gatewayConfigs, { format: options.output, defaultFormat: "json" });
+ output(allConfigs, { format: options.output, defaultFormat: "json" });
} catch (error) {
outputError("Failed to list gateway configs", error);
}
diff --git a/src/commands/network-policy/list.tsx b/src/commands/network-policy/list.tsx
index fa416c0..e6863e4 100644
--- a/src/commands/network-policy/list.tsx
+++ b/src/commands/network-policy/list.tsx
@@ -14,7 +14,7 @@ import { ActionsPopup } from "../../components/ActionsPopup.js";
import { Operation } from "../../components/OperationsMenu.js";
import { formatTimeAgo } from "../../components/ResourceListView.js";
import { SearchBar } from "../../components/SearchBar.js";
-import { output, outputError } from "../../utils/output.js";
+import { output, outputError, parseLimit } from "../../utils/output.js";
import { colors } from "../../utils/theme.js";
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
@@ -26,6 +26,7 @@ import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
interface ListOptions {
name?: string;
+ limit?: string;
output?: string;
}
@@ -173,7 +174,7 @@ const ListNetworkPoliciesUI = ({
const result = {
items: pagePolicies,
hasMore: page.has_more || false,
- totalCount: page.total_count || pagePolicies.length,
+ totalCount: pagePolicies.length,
};
return result;
@@ -206,7 +207,7 @@ const ListNetworkPoliciesUI = ({
!showEditPolicy &&
!showDeleteConfirm &&
!search.searchMode,
- deps: [PAGE_SIZE, search.submittedSearchQuery],
+ deps: [search.submittedSearchQuery],
});
// Operations for a specific network policy (shown in popup)
@@ -461,8 +462,26 @@ const ListNetworkPoliciesUI = ({
// Handle list view navigation
if (key.upArrow && selectedIndex > 0) {
setSelectedIndex(selectedIndex - 1);
+ } else if (
+ key.upArrow &&
+ selectedIndex === 0 &&
+ !loading &&
+ !navigating &&
+ hasPrev
+ ) {
+ prevPage();
+ setSelectedIndex(pagePolicies - 1);
} else if (key.downArrow && selectedIndex < pagePolicies - 1) {
setSelectedIndex(selectedIndex + 1);
+ } else if (
+ key.downArrow &&
+ selectedIndex === pagePolicies - 1 &&
+ !loading &&
+ !navigating &&
+ hasMore
+ ) {
+ nextPage();
+ setSelectedIndex(0);
} else if (
(input === "n" || key.rightArrow) &&
!loading &&
@@ -660,7 +679,7 @@ const ListNetworkPoliciesUI = ({
data={policies}
keyExtractor={(policy: NetworkPolicyListItem) => policy.id}
selectedIndex={selectedIndex}
- title={`network_policies[${totalCount}]`}
+ title={`network_policies[${hasMore ? `${totalCount}+` : totalCount}]`}
columns={columns}
emptyState={
@@ -674,7 +693,7 @@ const ListNetworkPoliciesUI = ({
{!showPopup && (
- {figures.hamburger} {totalCount}
+ {figures.hamburger} {hasMore ? `${totalCount}+` : totalCount}
{" "}
@@ -692,7 +711,8 @@ const ListNetworkPoliciesUI = ({
) : (
- Page {currentPage + 1} of {totalPages}
+ Page {currentPage + 1} of{" "}
+ {hasMore ? `${totalPages}+` : totalPages}
)}
>
@@ -702,7 +722,8 @@ const ListNetworkPoliciesUI = ({
•{" "}
- Showing {startIndex + 1}-{endIndex} of {totalCount}
+ Showing {startIndex + 1}-{endIndex} of{" "}
+ {hasMore ? `${totalCount}+` : totalCount}
{search.submittedSearchQuery && (
<>
@@ -773,23 +794,45 @@ export async function listNetworkPolicies(options: ListOptions = {}) {
try {
const client = getClient();
- // Build query params
- const queryParams: Record = {
- limit: DEFAULT_PAGE_SIZE,
- };
- if (options.name) {
- queryParams.name = options.name;
- }
+ const maxResults = parseLimit(options.limit);
+ const allPolicies: unknown[] = [];
+ let startingAfter: string | undefined;
- // Fetch network policies
- const page = (await client.networkPolicies.list(
- queryParams,
- )) as NetworkPoliciesCursorIDPage<{ id: string }>;
+ do {
+ const remaining = maxResults - allPolicies.length;
+ // Build query params
+ const queryParams: Record = {
+ limit: Math.min(DEFAULT_PAGE_SIZE, remaining),
+ };
+ if (options.name) {
+ queryParams.name = options.name;
+ }
+ if (startingAfter) {
+ queryParams.starting_after = startingAfter;
+ }
- // Extract network policies array
- const networkPolicies = page.network_policies || [];
+ // Fetch one page
+ const page = (await client.networkPolicies.list(
+ queryParams,
+ )) as NetworkPoliciesCursorIDPage<{ id: string }>;
+
+ const pagePolicies = page.network_policies || [];
+ allPolicies.push(...pagePolicies);
+
+ if (
+ page.has_more &&
+ pagePolicies.length > 0 &&
+ allPolicies.length < maxResults
+ ) {
+ startingAfter = (
+ pagePolicies[pagePolicies.length - 1] as { id: string }
+ ).id;
+ } else {
+ startingAfter = undefined;
+ }
+ } while (startingAfter !== undefined);
- output(networkPolicies, { format: options.output, defaultFormat: "json" });
+ output(allPolicies, { format: options.output, defaultFormat: "json" });
} catch (error) {
outputError("Failed to list network policies", error);
}
diff --git a/src/commands/object/list.tsx b/src/commands/object/list.tsx
index c4e76cd..d757929 100644
--- a/src/commands/object/list.tsx
+++ b/src/commands/object/list.tsx
@@ -15,7 +15,7 @@ import { ActionsPopup } from "../../components/ActionsPopup.js";
import { Operation } from "../../components/OperationsMenu.js";
import { formatTimeAgo } from "../../components/ResourceListView.js";
import { SearchBar } from "../../components/SearchBar.js";
-import { output, outputError } from "../../utils/output.js";
+import { output, outputError, parseLimit } from "../../utils/output.js";
import { colors } from "../../utils/theme.js";
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
@@ -30,6 +30,7 @@ interface ListOptions {
contentType?: string;
state?: string;
public?: boolean;
+ limit?: string;
output?: string;
}
@@ -172,14 +173,13 @@ const ListObjectsUI = ({
// Access pagination properties from the result
const pageResult = result as unknown as {
objects: unknown[];
- total_count?: number;
has_more?: boolean;
};
return {
items: pageObjects,
hasMore: pageResult.has_more || false,
- totalCount: pageResult.total_count || pageObjects.length,
+ totalCount: pageObjects.length,
};
},
[search.submittedSearchQuery],
@@ -209,7 +209,7 @@ const ListObjectsUI = ({
!showDownloadPrompt &&
!showDeleteConfirm &&
!search.searchMode,
- deps: [PAGE_SIZE, search.submittedSearchQuery],
+ deps: [search.submittedSearchQuery],
});
// Operations for objects
@@ -499,8 +499,26 @@ const ListObjectsUI = ({
// Handle list view navigation
if (key.upArrow && selectedIndex > 0) {
setSelectedIndex(selectedIndex - 1);
+ } else if (
+ key.upArrow &&
+ selectedIndex === 0 &&
+ !loading &&
+ !navigating &&
+ hasPrev
+ ) {
+ prevPage();
+ setSelectedIndex(pageObjects - 1);
} else if (key.downArrow && selectedIndex < pageObjects - 1) {
setSelectedIndex(selectedIndex + 1);
+ } else if (
+ key.downArrow &&
+ selectedIndex === pageObjects - 1 &&
+ !loading &&
+ !navigating &&
+ hasMore
+ ) {
+ nextPage();
+ setSelectedIndex(0);
} else if (
(input === "n" || key.rightArrow) &&
!loading &&
@@ -706,7 +724,7 @@ const ListObjectsUI = ({
data={objects}
keyExtractor={(obj: ObjectListItem) => obj.id}
selectedIndex={selectedIndex}
- title={`storage_objects[${totalCount}]`}
+ title={`storage_objects[${hasMore ? `${totalCount}+` : totalCount}]`}
columns={columns}
emptyState={
@@ -721,7 +739,7 @@ const ListObjectsUI = ({
{!showPopup && (
- {figures.hamburger} {totalCount}
+ {figures.hamburger} {hasMore ? `${totalCount}+` : totalCount}
{" "}
@@ -739,7 +757,8 @@ const ListObjectsUI = ({
) : (
- Page {currentPage + 1} of {totalPages}
+ Page {currentPage + 1} of{" "}
+ {hasMore ? `${totalPages}+` : totalPages}
)}
>
@@ -749,7 +768,8 @@ const ListObjectsUI = ({
•{" "}
- Showing {startIndex + 1}-{endIndex} of {totalCount}
+ Showing {startIndex + 1}-{endIndex} of{" "}
+ {hasMore ? `${totalCount}+` : totalCount}
{search.submittedSearchQuery && (
<>
@@ -816,30 +836,53 @@ export async function listObjects(options: ListOptions) {
try {
const client = getClient();
- // Build query params
- const queryParams: Record = {
- limit: DEFAULT_PAGE_SIZE,
- };
- if (options.name) {
- queryParams.name = options.name;
- }
- if (options.contentType) {
- queryParams.content_type = options.contentType;
- }
- if (options.state) {
- queryParams.state = options.state;
- }
- if (options.public !== undefined) {
- queryParams.is_public = options.public;
- }
+ const maxResults = parseLimit(options.limit);
+ const allObjects: unknown[] = [];
+ let startingAfter: string | undefined;
- // Fetch objects
- const result = await client.objects.list(queryParams);
+ do {
+ const remaining = maxResults - allObjects.length;
+ // Build query params
+ const queryParams: Record = {
+ limit: Math.min(DEFAULT_PAGE_SIZE, remaining),
+ };
+ if (options.name) {
+ queryParams.name = options.name;
+ }
+ if (options.contentType) {
+ queryParams.content_type = options.contentType;
+ }
+ if (options.state) {
+ queryParams.state = options.state;
+ }
+ if (options.public !== undefined) {
+ queryParams.is_public = options.public;
+ }
+ if (startingAfter) {
+ queryParams.starting_after = startingAfter;
+ }
- // Extract objects array
- const objects = result.objects || [];
+ // Fetch one page
+ const result = await client.objects.list(queryParams);
+ const pageResult = result as unknown as {
+ objects?: { id: string }[];
+ has_more?: boolean;
+ };
+ const pageObjects = pageResult.objects || [];
+ allObjects.push(...pageObjects);
+
+ if (
+ pageResult.has_more &&
+ pageObjects.length > 0 &&
+ allObjects.length < maxResults
+ ) {
+ startingAfter = pageObjects[pageObjects.length - 1].id;
+ } else {
+ startingAfter = undefined;
+ }
+ } while (startingAfter !== undefined);
- output(objects, { format: options.output, defaultFormat: "json" });
+ output(allObjects, { format: options.output, defaultFormat: "json" });
} catch (error) {
outputError("Failed to list storage objects", error);
}
diff --git a/src/commands/secret/list.tsx b/src/commands/secret/list.tsx
index 48fe7ec..2f721c6 100644
--- a/src/commands/secret/list.tsx
+++ b/src/commands/secret/list.tsx
@@ -13,7 +13,7 @@ import { ActionsPopup } from "../../components/ActionsPopup.js";
import { Operation } from "../../components/OperationsMenu.js";
import { formatTimeAgo } from "../../components/ResourceListView.js";
import { SearchBar } from "../../components/SearchBar.js";
-import { output, outputError } from "../../utils/output.js";
+import { output, outputError, parseLimit } from "../../utils/output.js";
import { colors } from "../../utils/theme.js";
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
@@ -167,7 +167,7 @@ const ListSecretsUI = ({
!executingOperation &&
!showDeleteConfirm &&
!search.searchMode,
- deps: [PAGE_SIZE, search.submittedSearchQuery],
+ deps: [search.submittedSearchQuery],
});
// Operations for a specific secret (shown in popup)
@@ -339,8 +339,26 @@ const ListSecretsUI = ({
// Handle list view navigation
if (key.upArrow && selectedIndex > 0) {
setSelectedIndex(selectedIndex - 1);
+ } else if (
+ key.upArrow &&
+ selectedIndex === 0 &&
+ !loading &&
+ !navigating &&
+ hasPrev
+ ) {
+ prevPage();
+ setSelectedIndex(pageSecrets - 1);
} else if (key.downArrow && selectedIndex < pageSecrets - 1) {
setSelectedIndex(selectedIndex + 1);
+ } else if (
+ key.downArrow &&
+ selectedIndex === pageSecrets - 1 &&
+ !loading &&
+ !navigating &&
+ hasMore
+ ) {
+ nextPage();
+ setSelectedIndex(0);
} else if (
(input === "n" || key.rightArrow) &&
!loading &&
@@ -525,7 +543,7 @@ const ListSecretsUI = ({
{!showPopup && (
- {figures.hamburger} {totalCount}
+ {figures.hamburger} {hasMore ? `${totalCount}+` : totalCount}
{" "}
@@ -543,7 +561,8 @@ const ListSecretsUI = ({
) : (
- Page {currentPage + 1} of {totalPages}
+ Page {currentPage + 1} of{" "}
+ {hasMore ? `${totalPages}+` : totalPages}
)}
>
@@ -553,7 +572,8 @@ const ListSecretsUI = ({
•{" "}
- Showing {startIndex + 1}-{endIndex} of {totalCount}
+ Showing {startIndex + 1}-{endIndex} of{" "}
+ {hasMore ? `${totalCount}+` : totalCount}
{search.submittedSearchQuery && (
<>
@@ -619,18 +639,39 @@ export async function listSecrets(options: ListOptions = {}) {
try {
const client = getClient();
- const limit = options.limit
- ? parseInt(options.limit, 10)
- : DEFAULT_PAGE_SIZE;
+ const maxResults = parseLimit(options.limit);
+ const allSecrets: unknown[] = [];
+ let startingAfter: string | undefined;
- // Fetch secrets
- const result = await client.secrets.list({ limit });
+ do {
+ const remaining = maxResults - allSecrets.length;
+ const queryParams: Record = {
+ limit: Math.min(DEFAULT_PAGE_SIZE, remaining),
+ };
+ if (startingAfter) {
+ queryParams.starting_after = startingAfter;
+ }
- // Extract secrets array
- const secrets = result.secrets || [];
+ const result = await client.secrets.list(queryParams);
+ const pageResult = result as unknown as {
+ secrets?: { id: string }[];
+ has_more?: boolean;
+ };
+ const pageSecrets = pageResult.secrets || [];
+ allSecrets.push(...pageSecrets);
+
+ if (
+ pageResult.has_more &&
+ pageSecrets.length > 0 &&
+ allSecrets.length < maxResults
+ ) {
+ startingAfter = pageSecrets[pageSecrets.length - 1].id;
+ } else {
+ startingAfter = undefined;
+ }
+ } while (startingAfter !== undefined);
- // Default: output JSON for lists
- output(secrets, { format: options.output, defaultFormat: "json" });
+ output(allSecrets, { format: options.output, defaultFormat: "json" });
} catch (error) {
outputError("Failed to list secrets", error);
}
diff --git a/src/commands/snapshot/list.tsx b/src/commands/snapshot/list.tsx
index ff523a1..bf357a1 100644
--- a/src/commands/snapshot/list.tsx
+++ b/src/commands/snapshot/list.tsx
@@ -15,7 +15,7 @@ import { ActionsPopup } from "../../components/ActionsPopup.js";
import { Operation } from "../../components/OperationsMenu.js";
import { formatTimeAgo } from "../../components/ResourceListView.js";
import { SearchBar } from "../../components/SearchBar.js";
-import { output, outputError } from "../../utils/output.js";
+import { output, outputError, parseLimit } from "../../utils/output.js";
import { colors } from "../../utils/theme.js";
import { useViewportHeight } from "../../hooks/useViewportHeight.js";
import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
@@ -27,6 +27,7 @@ import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
interface ListOptions {
devbox?: string;
+ limit?: string;
output?: string;
}
@@ -142,7 +143,7 @@ const ListSnapshotsUI = ({
const result = {
items: pageSnapshots,
hasMore: page.has_more || false,
- totalCount: page.total_count || pageSnapshots.length,
+ totalCount: pageSnapshots.length,
};
return result;
@@ -174,7 +175,7 @@ const ListSnapshotsUI = ({
!showCreateDevbox &&
!showDeleteConfirm &&
!search.searchMode,
- deps: [devboxId, PAGE_SIZE, search.submittedSearchQuery],
+ deps: [devboxId, search.submittedSearchQuery],
});
// Operations for snapshots
@@ -378,8 +379,26 @@ const ListSnapshotsUI = ({
// Handle list view navigation
if (key.upArrow && selectedIndex > 0) {
setSelectedIndex(selectedIndex - 1);
+ } else if (
+ key.upArrow &&
+ selectedIndex === 0 &&
+ !loading &&
+ !navigating &&
+ hasPrev
+ ) {
+ prevPage();
+ setSelectedIndex(pageSnapshots - 1);
} else if (key.downArrow && selectedIndex < pageSnapshots - 1) {
setSelectedIndex(selectedIndex + 1);
+ } else if (
+ key.downArrow &&
+ selectedIndex === pageSnapshots - 1 &&
+ !loading &&
+ !navigating &&
+ hasMore
+ ) {
+ nextPage();
+ setSelectedIndex(0);
} else if (
(input === "n" || key.rightArrow) &&
!loading &&
@@ -576,7 +595,7 @@ const ListSnapshotsUI = ({
data={snapshots}
keyExtractor={(snapshot: SnapshotListItem) => snapshot.id}
selectedIndex={selectedIndex}
- title={`snapshots[${totalCount}]`}
+ title={`snapshots[${hasMore ? `${totalCount}+` : totalCount}]`}
columns={columns}
emptyState={
@@ -591,7 +610,7 @@ const ListSnapshotsUI = ({
{!showPopup && (
- {figures.hamburger} {totalCount}
+ {figures.hamburger} {hasMore ? `${totalCount}+` : totalCount}
{" "}
@@ -609,7 +628,8 @@ const ListSnapshotsUI = ({
) : (
- Page {currentPage + 1} of {totalPages}
+ Page {currentPage + 1} of{" "}
+ {hasMore ? `${totalPages}+` : totalPages}
)}
>
@@ -619,7 +639,8 @@ const ListSnapshotsUI = ({
•{" "}
- Showing {startIndex + 1}-{endIndex} of {totalCount}
+ Showing {startIndex + 1}-{endIndex} of{" "}
+ {hasMore ? `${totalCount}+` : totalCount}
{search.submittedSearchQuery && (
<>
@@ -686,33 +707,56 @@ export async function listSnapshots(options: ListOptions) {
try {
const client = getClient();
- // Build query params
- const queryParams: Record = {
- limit: DEFAULT_PAGE_SIZE,
- };
- if (options.devbox) {
- queryParams.devbox_id = options.devbox;
- }
+ const maxResults = parseLimit(options.limit);
+ const allSnapshots: ReturnType[] = [];
+ let startingAfter: string | undefined;
- // Fetch snapshots
- const page = (await client.devboxes.listDiskSnapshots(
- queryParams,
- )) as DiskSnapshotsCursorIDPage;
-
- // Extract snapshots array and strip to plain objects to avoid
- // camelCase aliases added by the API client library
- const snapshots = (page.snapshots || []).map((s) => ({
- id: s.id,
- name: s.name ?? undefined,
- create_time_ms: s.create_time_ms,
- metadata: s.metadata,
- source_devbox_id: s.source_devbox_id,
- source_blueprint_id: s.source_blueprint_id ?? undefined,
- commit_message: s.commit_message ?? undefined,
- }));
-
- output(snapshots, { format: options.output, defaultFormat: "json" });
+ do {
+ const remaining = maxResults - allSnapshots.length;
+ // Build query params
+ const queryParams: Record = {
+ limit: Math.min(DEFAULT_PAGE_SIZE, remaining),
+ };
+ if (options.devbox) {
+ queryParams.devbox_id = options.devbox;
+ }
+ if (startingAfter) {
+ queryParams.starting_after = startingAfter;
+ }
+
+ // Fetch one page
+ const page = (await client.devboxes.listDiskSnapshots(
+ queryParams,
+ )) as DiskSnapshotsCursorIDPage;
+
+ const pageSnapshots = page.snapshots || [];
+ allSnapshots.push(...pageSnapshots.map(mapSnapshot));
+
+ if (
+ page.has_more &&
+ pageSnapshots.length > 0 &&
+ allSnapshots.length < maxResults
+ ) {
+ startingAfter = pageSnapshots[pageSnapshots.length - 1].id;
+ } else {
+ startingAfter = undefined;
+ }
+ } while (startingAfter !== undefined);
+
+ output(allSnapshots, { format: options.output, defaultFormat: "json" });
} catch (error) {
outputError("Failed to list snapshots", error);
}
}
+
+function mapSnapshot(s: DevboxSnapshotView) {
+ return {
+ id: s.id,
+ name: s.name ?? undefined,
+ create_time_ms: s.create_time_ms,
+ metadata: s.metadata,
+ source_devbox_id: s.source_devbox_id,
+ source_blueprint_id: s.source_blueprint_id ?? undefined,
+ commit_message: s.commit_message ?? undefined,
+ };
+}
diff --git a/src/hooks/useCursorPagination.ts b/src/hooks/useCursorPagination.ts
index 6c8a132..5aae8c3 100644
--- a/src/hooks/useCursorPagination.ts
+++ b/src/hooks/useCursorPagination.ts
@@ -101,6 +101,7 @@ export function useCursorPagination(
const [currentPage, setCurrentPage] = React.useState(0);
const [hasMore, setHasMore] = React.useState(false);
const [totalCount, setTotalCount] = React.useState(0);
+ const maxTotalCountRef = React.useRef(0);
// Cursor history: cursorHistory[N] = last item ID of page N
// Used to determine startingAt for page N+1
@@ -190,9 +191,12 @@ export function useCursorPagination(
// Update pagination state
setHasMore(result.hasMore);
- if (result.totalCount !== undefined) {
- setTotalCount(result.totalCount);
- }
+ // Compute cumulative total: items seen on all previous pages + current page.
+ // Use a high-water mark so the count never decreases when navigating back.
+ const computed = page * pageSizeRef.current + result.items.length;
+ const newTotal = Math.max(computed, maxTotalCountRef.current);
+ maxTotalCountRef.current = newTotal;
+ setTotalCount(newTotal);
} catch (err) {
if (!isMountedRef.current) return;
setError(err as Error);
@@ -212,6 +216,7 @@ export function useCursorPagination(
React.useEffect(() => {
// Clear cursor history when deps change
cursorHistoryRef.current = [];
+ maxTotalCountRef.current = 0;
setCurrentPage(0);
setItems([]);
setHasMore(false);
diff --git a/src/services/benchmarkJobService.ts b/src/services/benchmarkJobService.ts
index 7cfacb5..43b2bd7 100644
--- a/src/services/benchmarkJobService.ts
+++ b/src/services/benchmarkJobService.ts
@@ -79,7 +79,7 @@ export async function listBenchmarkJobs(
return {
jobs,
- totalCount: page.total_count || jobs.length,
+ totalCount: jobs.length,
hasMore: page.has_more || false,
};
}
diff --git a/src/services/benchmarkService.ts b/src/services/benchmarkService.ts
index c4d265d..17a164c 100644
--- a/src/services/benchmarkService.ts
+++ b/src/services/benchmarkService.ts
@@ -66,7 +66,7 @@ export async function listBenchmarkRuns(
return {
benchmarkRuns,
- totalCount: page.total_count || benchmarkRuns.length,
+ totalCount: benchmarkRuns.length,
hasMore: page.has_more || false,
};
}
@@ -105,7 +105,7 @@ export async function listScenarioRuns(
return {
scenarioRuns,
- totalCount: page.total_count || scenarioRuns.length,
+ totalCount: scenarioRuns.length,
hasMore: page.has_more || false,
};
}
@@ -124,7 +124,7 @@ export async function listScenarioRuns(
return {
scenarioRuns,
- totalCount: page.total_count || scenarioRuns.length,
+ totalCount: scenarioRuns.length,
hasMore: page.has_more || false,
};
}
@@ -166,7 +166,7 @@ export async function listBenchmarks(
return {
benchmarks,
- totalCount: page.total_count || benchmarks.length,
+ totalCount: benchmarks.length,
hasMore: page.has_more || false,
};
}
diff --git a/src/services/blueprintService.ts b/src/services/blueprintService.ts
index 7e1b8f1..d0be6f6 100644
--- a/src/services/blueprintService.ts
+++ b/src/services/blueprintService.ts
@@ -79,7 +79,7 @@ export async function listBlueprints(
const result = {
blueprints,
- totalCount: page.total_count || blueprints.length,
+ totalCount: blueprints.length,
hasMore: page.has_more || false,
};
diff --git a/src/services/devboxService.ts b/src/services/devboxService.ts
index 4f0ce8c..8c93b5d 100644
--- a/src/services/devboxService.ts
+++ b/src/services/devboxService.ts
@@ -130,7 +130,7 @@ export async function listDevboxes(
const result = {
devboxes,
- totalCount: page.total_count || devboxes.length,
+ totalCount: devboxes.length,
hasMore: page.has_more || false,
};
diff --git a/src/services/gatewayConfigService.ts b/src/services/gatewayConfigService.ts
index b8849ca..e72a932 100644
--- a/src/services/gatewayConfigService.ts
+++ b/src/services/gatewayConfigService.ts
@@ -73,7 +73,7 @@ export async function listGatewayConfigs(
const result = {
gatewayConfigs,
- totalCount: page.total_count || gatewayConfigs.length,
+ totalCount: gatewayConfigs.length,
hasMore: page.has_more || false,
};
diff --git a/src/services/networkPolicyService.ts b/src/services/networkPolicyService.ts
index 6727581..1a12ee7 100644
--- a/src/services/networkPolicyService.ts
+++ b/src/services/networkPolicyService.ts
@@ -72,7 +72,7 @@ export async function listNetworkPolicies(
const result = {
networkPolicies,
- totalCount: page.total_count || networkPolicies.length,
+ totalCount: networkPolicies.length,
hasMore: page.has_more || false,
};
diff --git a/src/services/objectService.ts b/src/services/objectService.ts
index 2195cb2..b4c2647 100644
--- a/src/services/objectService.ts
+++ b/src/services/objectService.ts
@@ -77,13 +77,12 @@ export async function listObjects(
// Access pagination properties from the result
const pageResult = result as unknown as {
objects: unknown[];
- total_count?: number;
has_more?: boolean;
};
return {
objects,
- totalCount: pageResult.total_count || objects.length,
+ totalCount: objects.length,
hasMore: pageResult.has_more || false,
};
}
diff --git a/src/services/scenarioService.ts b/src/services/scenarioService.ts
index 6a41326..0b32209 100644
--- a/src/services/scenarioService.ts
+++ b/src/services/scenarioService.ts
@@ -47,7 +47,7 @@ export async function listScenarios(
return {
scenarios,
- totalCount: page.total_count || scenarios.length,
+ totalCount: scenarios.length,
hasMore: page.has_more || false,
};
}
diff --git a/src/services/snapshotService.ts b/src/services/snapshotService.ts
index bc8e8f0..5bb93b0 100644
--- a/src/services/snapshotService.ts
+++ b/src/services/snapshotService.ts
@@ -90,7 +90,7 @@ export async function listSnapshots(
const result = {
snapshots,
- totalCount: page.total_count || snapshots.length,
+ totalCount: snapshots.length,
hasMore: page.has_more || false,
};
diff --git a/src/utils/commands.ts b/src/utils/commands.ts
index 1f196c9..dbf5e3d 100644
--- a/src/utils/commands.ts
+++ b/src/utils/commands.ts
@@ -359,6 +359,7 @@ export function createProgram(): Command {
.command("list")
.description("List all snapshots")
.option("-d, --devbox ", "Filter by devbox ID")
+ .option("-l, --limit ", "Max results", "20")
.option(
"-o, --output [format]",
"Output format: text|json|yaml (default: json)",
@@ -446,6 +447,7 @@ export function createProgram(): Command {
.command("list")
.description("List all blueprints")
.option("-n, --name ", "Filter by blueprint name")
+ .option("-l, --limit ", "Max results", "20")
.option(
"-o, --output [format]",
"Output format: text|json|yaml (default: json)",
diff --git a/src/utils/output.ts b/src/utils/output.ts
index 8b973aa..9502601 100644
--- a/src/utils/output.ts
+++ b/src/utils/output.ts
@@ -11,6 +11,24 @@ import { processUtils } from "./processUtils.js";
export type OutputFormat = "text" | "json" | "yaml";
+/**
+ * Parse a --limit option string into a max results number.
+ * - undefined or "0" → Infinity (unlimited)
+ * - positive integer string → that number
+ * - negative integer → Infinity (treat as unlimited)
+ * - non-numeric non-empty string → throws an error
+ */
+export function parseLimit(limit: string | undefined): number {
+ if (!limit || limit.trim() === "") return Infinity;
+ const trimmed = limit.trim();
+ if (!/^-?\d+$/.test(trimmed)) {
+ throw new Error(`Invalid --limit value: "${limit}". Must be an integer.`);
+ }
+ const n = parseInt(trimmed, 10);
+ if (n <= 0) return Infinity;
+ return n;
+}
+
export interface OutputOptions {
output?: string;
}