Skip to content
Closed
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/cli-list-project-ids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@e2b/cli': patch
---

`e2b sandbox list`, `e2b sandbox snapshot list` and `e2b template list` show the `prj_` ID of the project they list, as a `Project ID` column and as `projectId` (`projectID` for templates) in `--format json`. With `E2B_API_KEY`, where the key decides the project, it is left out.
13 changes: 12 additions & 1 deletion packages/cli/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as packageJSON from '../package.json'
import { getUserConfig, UserConfig } from './user'
import { asBold, asPrimary } from './utils/format'
import { ensureValidAccessToken, updateUserConfig } from './utils/token-refresh'
import { fetchMe, findProject, Me } from './workspaces'
import { fetchMe, findProject, Me, publicProjectId } from './workspaces'

// Must run before any ConnectionConfig is constructed (including the
// module-level one below) — configs read the integration at construction time.
Expand Down Expand Up @@ -167,6 +167,17 @@ export async function ensureAuth(): Promise<Auth> {
}
}

/**
* The `prj_` ID of the project `auth` sends requests to, or `undefined` with
* `E2B_API_KEY`, where the key selects the project and the CLI can't tell
* which one it is.
*/
export function projectIdOf(auth: Auth): string | undefined {
return 'apiHeaders' in auth
? publicProjectId(auth.apiHeaders['X-Team-ID'])
: undefined
}

/** An E2B API client authenticated with {@link ensureAuth}. */
export async function apiClient() {
return new e2b.ApiClient(new e2b.ConnectionConfig(await ensureAuth()))
Expand Down
42 changes: 27 additions & 15 deletions packages/cli/src/commands/sandbox/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Sandbox, SandboxInfo, SandboxListOrder, SandboxState } from 'e2b'

import { z } from 'zod'

import { ensureAuth } from 'src/api'
import { Auth, ensureAuth, projectIdOf } from 'src/api'
import { Format, parseFormat } from 'src/options'
import { renderTable } from 'src/utils/table'
import { integer, zodParser } from 'src/validation'
Expand Down Expand Up @@ -69,7 +69,9 @@ export const listCommand = new commander.Command('list')
const state = options.state || ['running']
const limit =
options.limit === 0 ? undefined : (options.limit ?? DEFAULT_LIMIT)
const { sandboxes, hasMore } = await listSandboxes({
const auth = await ensureAuth()
const projectId = projectIdOf(auth)
const { sandboxes, hasMore } = await listSandboxes(auth, {
limit,
state,
metadata: options.metadata,
Expand All @@ -79,15 +81,21 @@ export const listCommand = new commander.Command('list')
})

if (options.format === Format.PRETTY) {
renderSandboxTable(sandboxes, options.order)
renderSandboxTable(sandboxes, options.order, projectId)
if (hasMore) {
console.log(
`Showing first ${limit} sandboxes. Use --limit to change.`
)
}
} else {
console.log(
JSON.stringify(sortSandboxes(sandboxes, options.order), null, 2)
JSON.stringify(
sortSandboxes(sandboxes, options.order).map((sandbox) =>
projectId ? { ...sandbox, projectId } : sandbox

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.

T-12 — abbreviations keep their casing in identifiers: ID, not Id. This PR adds the same new JSON key in three places and spells it two ways — projectId here and in sandbox snapshot list, but projectID in template list. So jq '.[].projectID' works on one list and silently returns null on the other two. Use the T-12 spelling, which template list already uses, everywhere. It's a new key, so it doesn't matter that the neighbouring sandboxId predates the rule:

Suggested change
projectId ? { ...sandbox, projectId } : sandbox
projectId ? { ...sandbox, projectID: projectId } : sandbox

),
null,
2
)
)
}
} catch (err: any) {
Expand Down Expand Up @@ -126,7 +134,8 @@ export function buildTableRows(

function renderSandboxTable(
sandboxes: SandboxInfo[],
order?: SandboxListOrder
order?: SandboxListOrder,
projectId?: string
) {
if (!sandboxes?.length) {
console.log('No sandboxes found')
Expand All @@ -144,6 +153,8 @@ function renderSandboxTable(
{ header: 'RAM MiB', value: (row) => String(row.memoryMB) },
{ header: 'Envd version', value: (row) => row.envdVersion },
{ header: 'Metadata', value: (row) => row.metadata },
// Omitted with E2B_API_KEY, where the CLI doesn't know the key's project.
...(projectId ? [{ header: 'Project ID', value: () => projectId }] : []),
])
}

Expand All @@ -161,16 +172,17 @@ type ListSandboxesResult = {
hasMore: boolean
}

async function listSandboxes({
limit,
state,
metadata,
template,
startedAfter,
order,
}: ListSandboxesOptions = {}): Promise<ListSandboxesResult> {
const auth = await ensureAuth()

async function listSandboxes(
auth: Auth,
{
limit,
state,
metadata,
template,
startedAfter,
order,
}: ListSandboxesOptions = {}
): Promise<ListSandboxesResult> {
let pageLimit = limit
if (!limit || limit > PAGE_LIMIT) {
pageLimit = PAGE_LIMIT
Expand Down
19 changes: 15 additions & 4 deletions packages/cli/src/commands/sandbox/snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as commander from '@commander-js/extra-typings'
import { NotFoundError, Sandbox, SnapshotInfo } from 'e2b'

import { ensureAuth } from 'src/api'
import { ensureAuth, projectIdOf } from 'src/api'
import { Format, parseFormat } from 'src/options'
import { integer, zodParser } from 'src/validation'
import { asBold } from 'src/utils/format'
Expand Down Expand Up @@ -67,6 +67,7 @@ const listSnapshotsCommand = new commander.Command('list')
.action(async (sandboxID, options) => {
try {
const auth = await ensureAuth()
const projectId = projectIdOf(auth)
const limit =
options.limit === 0 ? undefined : (options.limit ?? DEFAULT_LIMIT)

Expand All @@ -93,14 +94,22 @@ const listSnapshotsCommand = new commander.Command('list')
paginator.hasNext || (limit ? allSnapshots.length > limit : false)

if (options.format === Format.PRETTY) {
renderSnapshotTable(snapshots)
renderSnapshotTable(snapshots, projectId)
if (hasMore) {
console.log(
`Showing first ${limit} snapshots. Use --limit to change.`
)
}
} else {
console.log(JSON.stringify(snapshots, null, 2))
console.log(
JSON.stringify(
snapshots.map((snapshot) =>
projectId ? { ...snapshot, projectId } : snapshot

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.

T-12 — same issue as in sandbox list: the new key should be projectID, matching template list and the ID casing rule:

Suggested change
projectId ? { ...snapshot, projectId } : snapshot
projectId ? { ...snapshot, projectID: projectId } : snapshot

),
null,
2
)
)
}
} catch (err: any) {
console.error(err)
Expand Down Expand Up @@ -153,7 +162,7 @@ export const snapshotCommand = new commander.Command('snapshot')
.addCommand(listSnapshotsCommand)
.addCommand(deleteSnapshotCommand)

function renderSnapshotTable(snapshots: SnapshotInfo[]) {
function renderSnapshotTable(snapshots: SnapshotInfo[], projectId?: string) {
if (!snapshots?.length) {
console.log('No snapshots found')
return
Expand All @@ -165,6 +174,8 @@ function renderSnapshotTable(snapshots: SnapshotInfo[]) {
header: 'Names',
value: (snapshot) => snapshot.names.map(stripControlChars).join(', '),
},
// Omitted with E2B_API_KEY, where the CLI doesn't know the key's project.
...(projectId ? [{ header: 'Project ID', value: () => projectId }] : []),
])
}

Expand Down
46 changes: 31 additions & 15 deletions packages/cli/src/commands/template/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as e2b from 'e2b'
import { listAliases } from '../../utils/format'
import { renderTable } from 'src/utils/table'
import { sortTemplatesAliases } from 'src/utils/templateSort'
import { apiClient, resolveProjectId } from 'src/api'
import { Auth, ensureAuth, projectIdOf, resolveProjectId } from 'src/api'
import {
deprecatedTeamOption,
Format,
Expand Down Expand Up @@ -40,8 +40,10 @@ export const listCommand = new commander.Command('list')
const limit = opts.limit === 0 ? undefined : (opts.limit ?? defaultLimit)
process.stdout.write('\n')

const { templates, hasMore } = await listSandboxTemplates({
projectId: resolveProjectId(),
const auth = await ensureAuth()
const projectId = projectIdOf(auth)
const { templates, hasMore } = await listSandboxTemplates(auth, {
teamId: resolveProjectId(),
limit,
})

Expand All @@ -50,14 +52,22 @@ export const listCommand = new commander.Command('list')
}

if (opts.format === Format.PRETTY) {
renderTemplateTable(templates)
renderTemplateTable(templates, projectId)
if (hasMore) {
console.log(
`Showing first ${limit} templates. Use --limit to change.`
)
}
} else {
console.log(JSON.stringify(templates, null, 2))
console.log(
JSON.stringify(
templates.map((template) =>
projectId ? { ...template, projectID: projectId } : template
),
null,
2
)
)
}
} catch (err: any) {
console.error(err)
Expand All @@ -66,7 +76,8 @@ export const listCommand = new commander.Command('list')
})

function renderTemplateTable(
templates: e2b.components['schemas']['Template'][]
templates: e2b.components['schemas']['Template'][],
projectId?: string
) {
if (!templates?.length) {
console.log('No templates found.')
Expand Down Expand Up @@ -98,6 +109,8 @@ function renderTemplateTable(
value: (template) => String(template.diskSizeMB),
},
{ header: 'Envd version', value: (template) => template.envdVersion },
// Omitted with E2B_API_KEY, where the CLI doesn't know the key's project.
...(projectId ? [{ header: 'Project ID', value: () => projectId }] : []),
])
}

Expand All @@ -106,13 +119,16 @@ type ListSandboxTemplatesResult = {
hasMore: boolean
}

async function listSandboxTemplates({
projectId,
limit,
}: {
projectId?: string
limit?: number
}): Promise<ListSandboxTemplatesResult> {
async function listSandboxTemplates(
auth: Auth,
{
teamId,
limit,
}: {
teamId?: string
limit?: number
}
): Promise<ListSandboxTemplatesResult> {
let pageLimit = limit
if (!limit || limit > PAGE_LIMIT) {
pageLimit = PAGE_LIMIT
Expand All @@ -121,15 +137,15 @@ async function listSandboxTemplates({
// With an access token, X-Team-ID (from the same project ID) selects the
// project; with an API key, the key does and the teamID query param is
// ignored.
const client = await apiClient()
const client = new e2b.ApiClient(new e2b.ConnectionConfig(auth))
const templates: e2b.components['schemas']['Template'][] = []
let nextToken: string | undefined

do {
const res = await client.api.GET('/v2/templates', {
params: {
// the backend API still calls the project ID "teamID"
query: { teamID: projectId, limit: pageLimit, nextToken },
query: { teamID: teamId, limit: pageLimit, nextToken },
},
})

Expand Down
19 changes: 19 additions & 0 deletions packages/cli/src/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,22 @@ export async function pickProject(
default: entries.find(({ project }) => project.id === selectedId),
})
}

const TYPEID_ALPHABET = '0123456789abcdefghjkmnpqrstvwxyz'

/**
* The `prj_` public ID of a project UUID: the TypeID encoding workspace-api
* uses for `public_id`, computed locally so lists don't need `/v1/me`.
* Anything that isn't a UUID is returned as is.
*/
export function publicProjectId(id: string): string {
const hex = id.replace(/-/g, '')
if (!/^[0-9a-f]{32}$/i.test(hex)) return id
Comment on lines +225 to +231

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.

T-52 — the backend is the source of truth, and client code must not copy backend business rules ("a duplicated rule drifts from the server's the moment the API changes"). This re-implements workspace-api's public_id TypeID encoding in the CLI, even though the server already returns public_id (see Project.public_id, used by findProject above). If workspace-api changes the prefix, alphabet or ID scheme, the CLI keeps printing well-formed prj_ IDs that match nothing, and no test fails. Only the unit test's single fixture ties the two together.

The fallback makes this worse. Any X-Team-ID that isn't a UUID comes back unchanged and is still shown under the Project ID header.

Compliant form: take public_id from the server. When --project / the saved project is resolved through findProject / fetchMe, keep the matched project.public_id (or cache it next to the selected project in the user config) and pass that through. When the CLI doesn't have the server's value, leave the column out, the same way the E2B_API_KEY case already does, rather than computing it.


const value = BigInt(`0x${hex}`)
let suffix = ''
for (let i = 25; i >= 0; i--) {
suffix += TYPEID_ALPHABET[Number((value >> BigInt(5 * i)) & 31n)]
}
return `prj_${suffix}`
}
Loading
Loading