Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plenty-onions-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@e2b/cli': patch
---

Add implicit limit for sandbox list query to CLI
48 changes: 27 additions & 21 deletions packages/cli/src/commands/sandbox/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { components, Sandbox, SandboxInfo } from 'e2b'
import { ensureAPIKey } from 'src/api'
import { parseMetadata } from './utils'

const DEFAULT_LIMIT = 1000
const PAGE_LIMIT = 100

function getStateTitle(state?: components['schemas']['SandboxState'][]) {
if (state?.length === 1) {
if (state?.includes('running')) return 'Running sandboxes'
Expand All @@ -24,22 +27,29 @@ export const listCommand = new commander.Command('list')
.option('-m, --metadata <metadata>', 'filter by metadata, eg. key1=value1')
.option(
'-l, --limit <limit>',
'limit the number of sandboxes returned',
`limit the number of sandboxes returned (default: ${DEFAULT_LIMIT}, 0 for no limit)`,
(value) => parseInt(value)
)
.option('-f, --format <format>', 'output format, eg. json, pretty')
.action(async (options) => {
try {
const state = options.state || ['running']
const format = options.format || 'pretty'
const sandboxes = await listSandboxes({
limit: options.limit,
const limit =
options.limit === 0 ? undefined : (options.limit ?? DEFAULT_LIMIT)

Choose a reason for hiding this comment

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

P2 Badge Guard against NaN limit bypassing implicit cap

When --limit is passed a non-numeric value (for example --limit foo), parseInt yields NaN, and this expression keeps that NaN instead of falling back to DEFAULT_LIMIT. In listSandboxes, NaN is then treated as falsy by !limit, which makes the loop behave like “no limit” and can fetch the full sandbox list, defeating the new implicit cap and potentially causing unexpectedly large/unbounded queries from a simple typo.

Useful? React with 👍 / 👎.

const { sandboxes, hasMore } = await listSandboxes({
limit,
state,
metadataRaw: options.metadata,
})

if (format === 'pretty') {
renderTable(sandboxes, state)
if (hasMore) {
console.log(
`Showing first ${limit} sandboxes. Use --limit to change.`
)
}
} else if (format === 'json') {
console.log(JSON.stringify(sandboxes, null, 2))
} else {
Expand Down Expand Up @@ -130,23 +140,22 @@ type ListSandboxesOptions = {
metadataRaw?: string
}

type ListSandboxesResult = {
sandboxes: SandboxInfo[]
hasMore: boolean
}

export async function listSandboxes({
limit,
state,
metadataRaw,
}: ListSandboxesOptions = {}): Promise<SandboxInfo[]> {
}: ListSandboxesOptions = {}): Promise<ListSandboxesResult> {
const apiKey = ensureAPIKey()
const metadata = parseMetadata(metadataRaw)

let pageLimit = limit
if (!limit || limit > 100) {
pageLimit = 100
}

let remainingLimit = limit
//backwards compatibility
if (limit === 0) {
remainingLimit = undefined
if (!limit || limit > PAGE_LIMIT) {
pageLimit = PAGE_LIMIT
}

const sandboxes: SandboxInfo[] = []
Expand All @@ -156,17 +165,14 @@ export async function listSandboxes({
query: { state, metadata },
})

while (
iterator.hasNext &&
(remainingLimit === undefined || remainingLimit > 0)
) {
while (iterator.hasNext && (!limit || sandboxes.length < limit)) {
const batch = await iterator.nextItems()
sandboxes.push(...batch)

if (limit && remainingLimit) {
remainingLimit -= batch.length
}
}

return sandboxes
return {
sandboxes: limit ? sandboxes.slice(0, limit) : sandboxes,
// We can't change the page size during iteration, so we may have to check if we have more sandboxes than the limit
hasMore: iterator.hasNext || (limit ? sandboxes.length > limit : false),
}
}