Repository navigation
feat(cli): add workspace and project commands #2105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mishushakov
merged 6 commits into
mishushakov/cli-workspace-api-login
from
mishushakov/cli-project-commands
Nov 12, 2026
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
1f278bd
feat(cli): add workspace and project commands, --project everywhere
mishushakov 4655a4d
fix(cli): name project sources and statuses, reject ambiguous workspa…
mishushakov 15631cb
refactor(cli): drop --project from the project commands
mishushakov ee1bb1d
refactor(cli): drop the project source from currentProject and projec…
mishushakov 1e09c96
feat(cli): let `project use` pick a project, scope it with --workspace
mishushakov f075e5e
test(cli): project use always prompts for the project
mishushakov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| '@e2b/cli': minor | ||
| --- | ||
|
|
||
| Add commands for workspaces and projects: | ||
|
|
||
| ```sh | ||
| e2b workspace list # workspaces you can access | ||
| e2b project list [--workspace <ws>] # projects, the current one marked with * | ||
| e2b project use [project] [-w <ws>] # set the project commands run in, or pick one | ||
| e2b project info # the current project | ||
| ``` | ||
|
|
||
| A project can be given by its ID, `prj_` ID, slug or name. `project use` without a project asks for the workspace and then the project; `--workspace` picks from that workspace only and scopes a name to it. Commands run in the project from `E2B_PROJECT_ID`, then the one chosen with `e2b project use` (or `e2b auth configure`, which picks it interactively). With `E2B_API_KEY` set, the API key determines the project. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| import * as commander from '@commander-js/extra-typings' | ||
|
|
||
| import { currentProject, projectId, requireMe } from 'src/api' | ||
| import { Format, parseFormat } from 'src/options' | ||
| import { asBold, asFormattedError } from 'src/utils/format' | ||
| import { renderTable } from 'src/utils/table' | ||
| import { updateUserConfig } from 'src/utils/token-refresh' | ||
| import { | ||
| allProjects, | ||
| blockedWarning, | ||
| findProject, | ||
| findWorkspace, | ||
| Me, | ||
| pickProject, | ||
| Project, | ||
| ProjectEntry, | ||
| ProjectStatus, | ||
| } from 'src/workspaces' | ||
|
|
||
| const workspaceOption = (purpose: string) => | ||
| new commander.Option( | ||
| '-w, --workspace <workspace>', | ||
| `${purpose}: its ID, wrk_ ID or name` | ||
| ) | ||
|
|
||
| const formatOption = new commander.Option( | ||
| '-f, --format <format>', | ||
| 'output format, eg. json, pretty' | ||
| ) | ||
| .argParser(parseFormat) | ||
| .default(Format.PRETTY) | ||
|
|
||
| function fail(err: unknown): never { | ||
| console.error( | ||
| asFormattedError(err instanceof Error ? err.message : String(err)) | ||
| ) | ||
| process.exit(1) | ||
| } | ||
|
|
||
| /** `active`, `provisioning`, `deleting`, or `blocked` with its reasons. */ | ||
| function projectStatus({ status, blocked, block_reasons }: Project) { | ||
| if (!blocked) return status | ||
| return block_reasons.length | ||
| ? `blocked (${block_reasons.join(', ')})` | ||
| : 'blocked' | ||
| } | ||
|
|
||
| /** The current project, if it is one `me` lists. */ | ||
| function findCurrent(me: Me): ProjectEntry | undefined { | ||
| const current = currentProject() | ||
| if (!current) return undefined | ||
| try { | ||
| return findProject(me, current) | ||
| } catch { | ||
| return undefined | ||
| } | ||
| } | ||
|
|
||
| const listCommand = new commander.Command('list') | ||
| .description('list the projects you can access') | ||
| .alias('ls') | ||
| .addOption(workspaceOption('only list projects in this workspace')) | ||
| .addOption(formatOption) | ||
| .action(async (opts) => { | ||
| try { | ||
| const me = await requireMe() | ||
|
|
||
| let entries = allProjects(me) | ||
| if (opts.workspace) { | ||
| const workspace = findWorkspace(me, opts.workspace) | ||
| entries = entries.filter((e) => e.workspace.id === workspace.id) | ||
| } | ||
| const currentId = findCurrent(me)?.project.id | ||
|
|
||
| if (opts.format === Format.JSON) { | ||
| console.log( | ||
| JSON.stringify( | ||
| entries.map(({ project, workspace }) => ({ | ||
| ...project, | ||
| workspace_name: workspace.name, | ||
| current: project.id === currentId, | ||
| })), | ||
| null, | ||
| 2 | ||
| ) | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| if (!entries.length) { | ||
| console.log('No projects found') | ||
| return | ||
| } | ||
| renderTable(entries, [ | ||
| { | ||
| header: 'Current', | ||
| value: ({ project }) => (project.id === currentId ? '*' : ''), | ||
| }, | ||
| { header: 'Project ID', value: ({ project }) => project.public_id }, | ||
| { header: 'Name', value: ({ project }) => project.name }, | ||
| { header: 'Workspace', value: ({ workspace }) => workspace.name }, | ||
| { header: 'Region', value: ({ project }) => project.region }, | ||
| { header: 'Status', value: ({ project }) => projectStatus(project) }, | ||
| ]) | ||
| } catch (err) { | ||
| fail(err) | ||
| } | ||
| }) | ||
|
|
||
| const useCommand = new commander.Command('use') | ||
| .description( | ||
| 'set the project commands run in by default; without a project, pick one' | ||
| ) | ||
| .argument('[project]', 'the project: its ID, prj_ ID, slug or name') | ||
| .addOption(workspaceOption('the workspace to pick from or look in')) | ||
| .action(async (ref, opts) => { | ||
| try { | ||
| const me = await requireMe() | ||
| const workspace = opts.workspace | ||
| ? findWorkspace(me, opts.workspace) | ||
| : undefined | ||
| let entry: ProjectEntry | ||
| if (ref) { | ||
| entry = findProject(me, ref, workspace) | ||
| } else { | ||
| if (!process.stdin.isTTY) { | ||
| fail( | ||
| `Pass the project to use: ${asBold('e2b project use <project>')}.` | ||
| ) | ||
| } | ||
| entry = await pickProject(me, { | ||
| message: 'Select project', | ||
| selectedId: findCurrent(me)?.project.id, | ||
| workspace, | ||
| }) | ||
| } | ||
| const { project, workspace: owner } = entry | ||
| if (project.status !== ProjectStatus.Active) { | ||
| fail( | ||
| `Project ${project.name} is ${project.status} and can't be used. Pick an active project from ${asBold( | ||
| 'e2b project list' | ||
| )}, or try again once it is active.` | ||
| ) | ||
| } | ||
| const warning = blockedWarning(entry) | ||
| if (warning) console.warn(warning) | ||
|
|
||
| await updateUserConfig({ | ||
| projectId: project.id, | ||
| projectName: project.name, | ||
| projectDomain: project.control_plane_domain, | ||
| }) | ||
| console.log( | ||
| `Now using project ${asBold(project.name)} (${project.public_id}) in workspace ${asBold(owner.name)}.` | ||
| ) | ||
|
|
||
| if (projectId) { | ||
| console.warn( | ||
| `${asBold('E2B_PROJECT_ID')} is set and takes precedence over the selected project.` | ||
| ) | ||
| } | ||
| if (process.env.E2B_API_KEY) { | ||
| console.warn( | ||
| `${asBold('E2B_API_KEY')} is set: commands use the API key's project instead.` | ||
| ) | ||
| } | ||
| } catch (err) { | ||
| fail(err) | ||
| } | ||
| }) | ||
|
|
||
| const infoCommand = new commander.Command('info') | ||
| .description('show the project commands run in') | ||
| .addOption(formatOption) | ||
| .action(async (opts) => { | ||
| try { | ||
| const current = currentProject() | ||
| if (!current) { | ||
| fail(`No project selected. Run ${asBold('e2b project use <project>')}.`) | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
Resolved
|
||
| const { project, workspace } = findProject(await requireMe(), current) | ||
|
|
||
| if (opts.format === Format.JSON) { | ||
| console.log( | ||
| JSON.stringify( | ||
| { | ||
| ...project, | ||
| workspace_name: workspace.name, | ||
| }, | ||
| null, | ||
| 2 | ||
| ) | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| const rows: [string, string][] = [ | ||
| ['Name', project.name], | ||
| ['Project ID', project.public_id], | ||
| ['UUID', project.id], | ||
| ['Slug', project.slug], | ||
| ['Workspace', `${workspace.name} (${workspace.public_id})`], | ||
| ['Region', project.region], | ||
| ['Control plane', project.control_plane_domain], | ||
| ['Status', projectStatus(project)], | ||
| ['Your access', project.capabilities.join(', ') || 'none'], | ||
| ] | ||
| const width = Math.max(...rows.map(([label]) => label.length)) + 2 | ||
| console.log(`\nProject info for ${asBold(project.name)}:`) | ||
| for (const [label, value] of rows) { | ||
| console.log(` ${`${label}:`.padEnd(width)}${value}`) | ||
| } | ||
| } catch (err) { | ||
| fail(err) | ||
| } | ||
| }) | ||
|
|
||
| export const projectCommand = new commander.Command('project') | ||
| .description('work with projects') | ||
| .alias('prj') | ||
| .addCommand(listCommand) | ||
| .addCommand(useCommand) | ||
| .addCommand(infoCommand) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import * as commander from '@commander-js/extra-typings' | ||
|
|
||
| import { requireMe } from 'src/api' | ||
| import { Format, parseFormat } from 'src/options' | ||
| import { asFormattedError } from 'src/utils/format' | ||
| import { renderTable } from 'src/utils/table' | ||
| import { Workspace } from 'src/workspaces' | ||
|
|
||
| /** `active`, or `blocked` with its reasons. */ | ||
| function workspaceStatus({ blocked, block_reasons }: Workspace) { | ||
| if (!blocked) return 'active' | ||
| return block_reasons.length | ||
| ? `blocked (${block_reasons.join(', ')})` | ||
| : 'blocked' | ||
| } | ||
|
|
||
| const listCommand = new commander.Command('list') | ||
| .description('list the workspaces you can access') | ||
| .alias('ls') | ||
| .option( | ||
| '-f, --format <format>', | ||
| 'output format, eg. json, pretty', | ||
| parseFormat, | ||
| Format.PRETTY | ||
| ) | ||
| .action(async (opts) => { | ||
| try { | ||
| const { workspaces } = await requireMe() | ||
|
|
||
| if (opts.format === Format.JSON) { | ||
| console.log( | ||
| JSON.stringify( | ||
| workspaces.map(({ projects, ...workspace }) => ({ | ||
| ...workspace, | ||
| project_count: projects.length, | ||
| })), | ||
| null, | ||
| 2 | ||
| ) | ||
| ) | ||
| return | ||
| } | ||
|
|
||
| if (!workspaces.length) { | ||
| console.log('No workspaces found') | ||
| return | ||
| } | ||
| renderTable(workspaces, [ | ||
| { header: 'Workspace ID', value: (w) => w.public_id }, | ||
| { header: 'Name', value: (w) => w.name }, | ||
| { header: 'Kind', value: (w) => w.kind }, | ||
| { header: 'Projects', value: (w) => String(w.projects.length) }, | ||
| { header: 'Status', value: workspaceStatus }, | ||
| ]) | ||
| } catch (err) { | ||
| console.error( | ||
| asFormattedError(err instanceof Error ? err.message : String(err)) | ||
| ) | ||
| process.exit(1) | ||
| } | ||
| }) | ||
|
|
||
| export const workspaceCommand = new commander.Command('workspace') | ||
| .description('work with workspaces') | ||
| .alias('ws') | ||
| .addCommand(listCommand) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.