bevor-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. bevor_cli/__init__.py +0 -0
  2. bevor_cli/annotations/__init__.py +0 -0
  3. bevor_cli/annotations/confirmations.py +16 -0
  4. bevor_cli/annotations/query.py +25 -0
  5. bevor_cli/apps/agent.py +10 -0
  6. bevor_cli/apps/analyses.py +47 -0
  7. bevor_cli/apps/chat.py +16 -0
  8. bevor_cli/apps/codes.py +18 -0
  9. bevor_cli/apps/findings.py +22 -0
  10. bevor_cli/apps/main.py +61 -0
  11. bevor_cli/apps/nodes.py +17 -0
  12. bevor_cli/apps/projects.py +14 -0
  13. bevor_cli/apps/sessions.py +13 -0
  14. bevor_cli/apps/workspaces.py +16 -0
  15. bevor_cli/client.py +157 -0
  16. bevor_cli/commands/__init__.py +0 -0
  17. bevor_cli/commands/agent/__init__.py +0 -0
  18. bevor_cli/commands/agent/materialze_cmd.py +65 -0
  19. bevor_cli/commands/agent/setup_cmd.py +18 -0
  20. bevor_cli/commands/analyses/__init__.py +0 -0
  21. bevor_cli/commands/analyses/active.py +42 -0
  22. bevor_cli/commands/analyses/chats/__init__.py +0 -0
  23. bevor_cli/commands/analyses/chats/get.py +39 -0
  24. bevor_cli/commands/analyses/chats/list.py +64 -0
  25. bevor_cli/commands/analyses/chats/messages.py +41 -0
  26. bevor_cli/commands/analyses/chats/new.py +44 -0
  27. bevor_cli/commands/analyses/chats/resume.py +43 -0
  28. bevor_cli/commands/analyses/chats/session.py +295 -0
  29. bevor_cli/commands/analyses/child.py +81 -0
  30. bevor_cli/commands/analyses/commit.py +49 -0
  31. bevor_cli/commands/analyses/create.py +105 -0
  32. bevor_cli/commands/analyses/delete.py +65 -0
  33. bevor_cli/commands/analyses/diff.py +87 -0
  34. bevor_cli/commands/analyses/entrypoints.py +56 -0
  35. bevor_cli/commands/analyses/findings.py +129 -0
  36. bevor_cli/commands/analyses/fork.py +47 -0
  37. bevor_cli/commands/analyses/list.py +85 -0
  38. bevor_cli/commands/analyses/merge.py +46 -0
  39. bevor_cli/commands/analyses/retry.py +77 -0
  40. bevor_cli/commands/analyses/run.py +87 -0
  41. bevor_cli/commands/analyses/select.py +85 -0
  42. bevor_cli/commands/analyses/set_head.py +45 -0
  43. bevor_cli/commands/analyses/transitions.py +90 -0
  44. bevor_cli/commands/analyses/view.py +94 -0
  45. bevor_cli/commands/auth/__init__.py +7 -0
  46. bevor_cli/commands/auth/login.py +135 -0
  47. bevor_cli/commands/auth/logout.py +23 -0
  48. bevor_cli/commands/auth/whoami.py +21 -0
  49. bevor_cli/commands/codes/__init__.py +0 -0
  50. bevor_cli/commands/codes/list.py +90 -0
  51. bevor_cli/commands/codes/push.py +76 -0
  52. bevor_cli/commands/codes/select.py +93 -0
  53. bevor_cli/commands/codes/view.py +80 -0
  54. bevor_cli/commands/context/__init__.py +92 -0
  55. bevor_cli/commands/findings/__init__.py +0 -0
  56. bevor_cli/commands/findings/acknowledge.py +28 -0
  57. bevor_cli/commands/findings/add.py +236 -0
  58. bevor_cli/commands/findings/bulk.py +110 -0
  59. bevor_cli/commands/findings/delete.py +28 -0
  60. bevor_cli/commands/findings/edit.py +109 -0
  61. bevor_cli/commands/findings/get.py +62 -0
  62. bevor_cli/commands/findings/interactive.py +329 -0
  63. bevor_cli/commands/findings/list.py +108 -0
  64. bevor_cli/commands/findings/revert.py +28 -0
  65. bevor_cli/commands/graph/__init__.py +0 -0
  66. bevor_cli/commands/graph/call_chain.py +48 -0
  67. bevor_cli/commands/graph/edges.py +59 -0
  68. bevor_cli/commands/graph/interactive.py +207 -0
  69. bevor_cli/commands/graph/renderer.py +135 -0
  70. bevor_cli/commands/link/__init__.py +52 -0
  71. bevor_cli/commands/link/helpers.py +26 -0
  72. bevor_cli/commands/nodes/__init__.py +0 -0
  73. bevor_cli/commands/nodes/get.py +52 -0
  74. bevor_cli/commands/nodes/list.py +75 -0
  75. bevor_cli/commands/nodes/source.py +40 -0
  76. bevor_cli/commands/projects/__init__.py +0 -0
  77. bevor_cli/commands/projects/create.py +54 -0
  78. bevor_cli/commands/projects/list.py +57 -0
  79. bevor_cli/commands/projects/select.py +75 -0
  80. bevor_cli/commands/projects/view.py +49 -0
  81. bevor_cli/commands/sessions/__init__.py +0 -0
  82. bevor_cli/commands/sessions/list.py +36 -0
  83. bevor_cli/commands/sessions/revoke.py +35 -0
  84. bevor_cli/commands/workspaces/__init__.py +0 -0
  85. bevor_cli/commands/workspaces/list.py +50 -0
  86. bevor_cli/commands/workspaces/pick.py +20 -0
  87. bevor_cli/commands/workspaces/select.py +68 -0
  88. bevor_cli/commands/workspaces/view.py +48 -0
  89. bevor_cli/configuration/creds.py +52 -0
  90. bevor_cli/configuration/settings.py +54 -0
  91. bevor_cli/configuration/state.py +113 -0
  92. bevor_cli/findings_yaml.py +291 -0
  93. bevor_cli/main.py +33 -0
  94. bevor_cli/output.py +69 -0
  95. bevor_cli/select.py +64 -0
  96. bevor_cli/settings.py +4 -0
  97. bevor_cli-0.1.0.dist-info/METADATA +14 -0
  98. bevor_cli-0.1.0.dist-info/RECORD +100 -0
  99. bevor_cli-0.1.0.dist-info/WHEEL +4 -0
  100. bevor_cli-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,52 @@
1
+ """``bevor code node get`` — fetch a single graph node."""
2
+
3
+ import json
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from bevor_sdk.types import ID
8
+ from rich.table import Table
9
+
10
+ from bevor_cli.annotations.confirmations import JSON_OUT
11
+ from bevor_cli.client import BevorInternalClient
12
+ from bevor_cli.configuration.creds import Credentials
13
+ from bevor_cli.configuration.state import State
14
+ from bevor_cli.output import console
15
+
16
+
17
+ def get_node(
18
+ node_id: str = typer.Argument(..., help="Node short id (or full wire id)."),
19
+ code_id: Optional[str] = typer.Option(
20
+ None, "--code", help="Code id to list for (defaults to current configuration)"
21
+ ),
22
+ json_out: JSON_OUT = False,
23
+ ) -> None:
24
+ """Get the details of a node. Uses the currently configured code version, or you can pass --code."""
25
+ creds = Credentials.load()
26
+ creds.require_token()
27
+
28
+ state = State.load()
29
+ workspace_id = state.validate_workspace()
30
+ code_id = state.validate_code(priority=code_id)
31
+
32
+ client = BevorInternalClient(
33
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
34
+ )
35
+
36
+ node = client.codes.nodes.get(code_id, ID(node_id))
37
+
38
+ if json_out:
39
+ typer.echo(json.dumps(node, indent=2))
40
+ return
41
+
42
+ table = Table(show_header=False, box=None, padding=(0, 1))
43
+ table.add_column(style="bold")
44
+ table.add_column()
45
+
46
+ table.add_row("ID", str(node.id))
47
+ table.add_row("Short id", str(node.short_id))
48
+ table.add_row("Name", node.name)
49
+ table.add_row("Type", str(node.node_type))
50
+ table.add_row("File", f"{node.path} ({node.file_id})")
51
+
52
+ console.print(table)
@@ -0,0 +1,75 @@
1
+ """``bevor code node list`` — list graph nodes for a code version."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from bevor_sdk.types import ID
7
+ from bevor_sdk.types.requests.graph import NodeQueryParams
8
+ from rich.table import Table
9
+
10
+ from bevor_cli import output
11
+ from bevor_cli.annotations.confirmations import JSON_OUT
12
+ from bevor_cli.client import BevorInternalClient
13
+ from bevor_cli.configuration.creds import Credentials
14
+ from bevor_cli.configuration.state import State
15
+ from bevor_cli.output import console
16
+
17
+
18
+ def list_nodes(
19
+ code_id: Optional[str] = typer.Option(
20
+ None, "--code", help="Code id to list for (defaults to current configuration)"
21
+ ),
22
+ entrypoints_only: bool = typer.Option(
23
+ False,
24
+ "--entrypoints",
25
+ help="Only show auditable entry-point functions.",
26
+ ),
27
+ file_id: Optional[str] = typer.Option(None, "--file", help="Filter to nodes in this file."),
28
+ no_deps: Optional[bool] = typer.Option(
29
+ None, "--no-deps", help="Filter to nodes under this directory prefix."
30
+ ),
31
+ name: Optional[str] = typer.Option(None, "--name", help="Filter to nodes by name"),
32
+ node_type: Optional[str] = typer.Option(None, "--node-type", help="Filter to nodes by type"),
33
+ json_out: JSON_OUT = False,
34
+ ) -> None:
35
+ """List all nodes. Uses the currently configured code version, or you can pass --code."""
36
+ creds = Credentials.load()
37
+ creds.require_token()
38
+
39
+ state = State.load()
40
+ workspace_id = state.validate_workspace()
41
+ code_id = state.validate_code(priority=code_id)
42
+
43
+ client = BevorInternalClient(
44
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
45
+ )
46
+
47
+ nodes = client.codes.nodes.list(
48
+ code_id,
49
+ NodeQueryParams(
50
+ file_id=ID(file_id) if file_id else None,
51
+ entrypoint_only=entrypoints_only,
52
+ no_deps=no_deps,
53
+ name=name,
54
+ node_type=node_type,
55
+ ),
56
+ )
57
+
58
+ if json_out:
59
+ console.print_json(nodes.model_dump_json(indent=2))
60
+ return
61
+
62
+ if not nodes.results:
63
+ output.error("No nodes found")
64
+ raise typer.Exit(1)
65
+
66
+ table = Table(title=f"Nodes — version {state.code_id}", show_header=True, header_style="bold")
67
+ table.add_column("Short id", style="dim", no_wrap=True)
68
+ table.add_column("Name")
69
+ table.add_column("Type", style="cyan")
70
+ table.add_column("File", style="dim")
71
+ table.add_column("EP", justify="center")
72
+
73
+ for n in nodes.results:
74
+ table.add_row(n.short_id, n.name, n.node_type, n.path, "⚡" if n.is_entrypoint else "")
75
+ console.print(table)
@@ -0,0 +1,40 @@
1
+ """``bevor code node content`` — show source for a graph node."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from bevor_sdk.types import ID
7
+ from rich.syntax import Syntax
8
+
9
+ from bevor_cli import output
10
+ from bevor_cli.client import BevorInternalClient
11
+ from bevor_cli.configuration.creds import Credentials
12
+ from bevor_cli.configuration.state import State
13
+ from bevor_cli.output import console
14
+
15
+
16
+ def show_source(
17
+ node_id: str = typer.Argument(..., help="Node short id (or full wire id)."),
18
+ code_id: Optional[str] = typer.Option(
19
+ None, "--code", help="Code id to list for (defaults to current configuration)"
20
+ ),
21
+ ) -> None:
22
+ """Show the source code of a node. Uses the currently configured code version, or you can pass --code."""
23
+ creds = Credentials.load()
24
+ creds.require_token()
25
+
26
+ state = State.load()
27
+ workspace_id = state.validate_workspace()
28
+ code_id = state.validate_code(priority=code_id)
29
+
30
+ client = BevorInternalClient(
31
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
32
+ )
33
+
34
+ content = client.codes.nodes.content(code_id, ID(node_id))
35
+
36
+ if not content:
37
+ output.error("No source content available.")
38
+ raise typer.Exit(1)
39
+
40
+ console.print(Syntax(content.strip(), "solidity", theme="monokai", line_numbers=False, padding=1))
File without changes
@@ -0,0 +1,54 @@
1
+ from typing import Optional, get_args
2
+
3
+ import questionary
4
+ import typer
5
+ from bevor_sdk.types.literals import ProjectKind
6
+ from bevor_sdk.types.requests.manage import CreateProjectBodyDict
7
+ from bevor_sdk.types.responses.manage import ProjectSchema
8
+ from rich.prompt import Confirm, Prompt
9
+
10
+ from bevor_cli import output
11
+ from bevor_cli.annotations.confirmations import YES
12
+ from bevor_cli.client import BevorInternalBusinessClient
13
+ from bevor_cli.configuration.creds import Credentials
14
+ from bevor_cli.configuration.state import State
15
+ from bevor_cli.select import _select
16
+
17
+
18
+ def create_project(
19
+ name: Optional[str] = typer.Option(None, "--name", help="project name"),
20
+ description: Optional[str] = typer.Option(None, "--description", help="project description"),
21
+ kind: Optional[ProjectKind] = typer.Option(None, "--kind", help="project kind"),
22
+ yes: YES = False,
23
+ ) -> ProjectSchema:
24
+ """Create a project. Can pass arguments, or do so interactively."""
25
+ creds = Credentials.load()
26
+ creds.require_token()
27
+
28
+ state = State.load()
29
+ workspace_id = state.validate_workspace()
30
+
31
+ client = BevorInternalBusinessClient(creds=creds, workspace_id=workspace_id)
32
+
33
+ if name is None:
34
+ name = Prompt.ask("Project name")
35
+ if description is None:
36
+ description = Prompt.ask("Project description (optional)", default="")
37
+ if kind is None:
38
+ choices = [questionary.Choice(title=p_kind, value=p_kind) for p_kind in get_args(ProjectKind)]
39
+ kind = _select("Project Kind", choices=choices)
40
+
41
+ if not yes:
42
+ # Add this so that it's obvious which workspace they're creating the project under. Can override.
43
+ workspace = client.get_workspace()
44
+ if not Confirm.ask(f"Create project '{name}' under workspace '{workspace.name}'?"):
45
+ raise typer.Exit(0)
46
+
47
+ project = client.create_project(
48
+ CreateProjectBodyDict(name=name, description=description, kind=kind)
49
+ )
50
+
51
+ state.set_project(project.id)
52
+ state.save()
53
+ output.success(f"Created project: {project.id} {project.name}")
54
+ return project
@@ -0,0 +1,57 @@
1
+ """``bevor projects list``."""
2
+
3
+ import json
4
+ from typing import Any, List
5
+
6
+ from rich.table import Table
7
+ from rich.text import Text
8
+
9
+ from bevor_cli.annotations.confirmations import JSON_OUT
10
+ from bevor_cli.client import BevorInternalBusinessClient
11
+ from bevor_cli.configuration.creds import Credentials
12
+ from bevor_cli.configuration.state import State
13
+ from bevor_cli.output import console
14
+
15
+
16
+ def list_projects(
17
+ json_out: JSON_OUT = False,
18
+ ) -> None:
19
+ """List projects in the configured workspace."""
20
+ creds = Credentials.load()
21
+ creds.require_token()
22
+
23
+ state = State.load()
24
+ workspace_id = state.validate_workspace()
25
+
26
+ client = BevorInternalBusinessClient(creds=creds, workspace_id=workspace_id)
27
+
28
+ projects = client.list_projects()
29
+
30
+ if json_out:
31
+ console.print_json(json.dumps(projects.model_dump(mode="json"), indent=2))
32
+ return
33
+
34
+ if not projects.results:
35
+ console.print("No projects found.")
36
+ return
37
+
38
+ table = Table("NAME", "ID", "CODES", "ANALYSES", "CREATED")
39
+ table.columns[2].justify = "right"
40
+ table.columns[3].justify = "right"
41
+
42
+ for p in projects.results:
43
+ is_active = p.id == state.project_id
44
+
45
+ row: List[Any] = [
46
+ p.name,
47
+ p.id,
48
+ str(p.n_codes),
49
+ str(p.n_analyses),
50
+ str(p.created_at.date()),
51
+ ]
52
+ if is_active:
53
+ row.append(Text("●", style="bold green"))
54
+
55
+ table.add_row(*row, style="bold green" if is_active else None)
56
+
57
+ console.print(table)
@@ -0,0 +1,75 @@
1
+ """``bevor projects select`` — set the selected project."""
2
+
3
+ from typing import Optional
4
+
5
+ import questionary
6
+ import typer
7
+ from bevor_sdk.types.responses.manage import ProjectSchema
8
+
9
+ from bevor_cli import output
10
+ from bevor_cli.client import BevorInternalBusinessClient
11
+ from bevor_cli.configuration.creds import Credentials
12
+ from bevor_cli.configuration.state import State
13
+ from bevor_cli.output import console
14
+ from bevor_cli.select import _CANCEL, _select
15
+
16
+
17
+ def select_project(
18
+ project_id: Optional[str] = typer.Argument(
19
+ None, help="Project id to select. Prompts with a list when omitted."
20
+ ),
21
+ ) -> None:
22
+ """Select a project by id, or pick one from a list."""
23
+ creds = Credentials.load()
24
+ creds.require_token()
25
+
26
+ state = State.load()
27
+ workspace_id = state.validate_workspace()
28
+
29
+ client = BevorInternalBusinessClient(creds=creds, workspace_id=workspace_id)
30
+
31
+ projects = client.list_projects()
32
+
33
+ if not projects.results:
34
+ console.print("No projects found.")
35
+ return
36
+
37
+ if project_id:
38
+ if not any(p.id == project_id for p in projects.results):
39
+ output.error("project_id does not exist")
40
+ raise typer.Exit(1)
41
+
42
+ if state.project_id != project_id:
43
+ state.set_project(project_id)
44
+ state.save()
45
+ output.info("Updated bevor configuration")
46
+ return
47
+
48
+ if len(projects.results) == 1:
49
+ if projects.results[0].id == state.project_id:
50
+ output.info("only 1 project exists, skipping")
51
+ return
52
+
53
+ state.set_project(projects.results[0].id)
54
+ state.save()
55
+ output.info("only 1 project exists, updating your bevor context automatically")
56
+ return
57
+
58
+ choices = [
59
+ questionary.Choice(
60
+ title=f"{p.name} {p.id}" + (" ●" if p.id == state.project_id else ""),
61
+ value=p,
62
+ )
63
+ for p in projects.results
64
+ ] + [questionary.Choice(title="↵ Cancel", value=_CANCEL)]
65
+
66
+ project: ProjectSchema | None = _select("Select a project:", choices)
67
+ if project is None:
68
+ raise typer.Exit(0)
69
+
70
+ if project.id != state.project_id:
71
+ output.info("Clearing active code version and analysis (different project).")
72
+ state.set_project(project.id)
73
+
74
+ state.save()
75
+ console.print(f"Active project: {project.name} ({project.id})")
@@ -0,0 +1,49 @@
1
+ """``bevor projects view`` — show a project (the selected one by default)."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.table import Table
7
+
8
+ from bevor_cli.annotations.confirmations import JSON_OUT
9
+ from bevor_cli.client import BevorInternalClient
10
+ from bevor_cli.configuration.creds import Credentials
11
+ from bevor_cli.configuration.state import State
12
+ from bevor_cli.output import console
13
+
14
+
15
+ def view_project(
16
+ project_id: Optional[str] = typer.Argument(
17
+ None, help="Project id. Defaults to the selected project."
18
+ ),
19
+ json_out: JSON_OUT = False,
20
+ ) -> None:
21
+ """Print details for a project in the selected workspace, or the selected project."""
22
+ creds = Credentials.load()
23
+ creds.require_token()
24
+
25
+ state = State.load()
26
+ workspace_id = state.validate_workspace()
27
+ project_id = state.validate_project(project_id)
28
+
29
+ client = BevorInternalClient(creds=creds, workspace_id=workspace_id, project_id=project_id)
30
+
31
+ project = client.projects.get()
32
+
33
+ if json_out:
34
+ console.print_json(project.model_dump_json(indent=2))
35
+ return
36
+
37
+ table = Table(show_header=False, box=None, padding=(0, 1))
38
+ table.add_column(style="bold")
39
+ table.add_column()
40
+
41
+ table.add_row("Name", project.name)
42
+ table.add_row("ID", project.id)
43
+ table.add_row("Description", project.description or "")
44
+ table.add_row("Codes", str(project.n_codes))
45
+ table.add_row("Analyses", str(project.n_analyses))
46
+ table.add_row("Created by", project.created_by_user.username)
47
+ table.add_row("Created", str(project.created_at.date()))
48
+
49
+ console.print(table)
File without changes
@@ -0,0 +1,36 @@
1
+ """``bevor sessions list``."""
2
+
3
+ from rich.table import Table
4
+
5
+ from bevor_cli.client import BevorInternalBusinessClient
6
+ from bevor_cli.configuration.creds import Credentials
7
+ from bevor_cli.output import console
8
+
9
+
10
+ def sessions_list() -> None:
11
+ """List active CLI sessions."""
12
+ creds = Credentials.load()
13
+ creds.require_token()
14
+
15
+ client = BevorInternalBusinessClient(creds=creds)
16
+
17
+ data = client.get("/v1/cli-tokens").json()
18
+ sessions = data.get("results", [])
19
+
20
+ if not sessions:
21
+ console.print("No active CLI sessions.")
22
+ return
23
+
24
+ table = Table("ID", "Device", "IP", "Last Used", "Expires", "Current Device")
25
+
26
+ for s in sessions:
27
+ args = [
28
+ s["id"],
29
+ s["device_name"],
30
+ s["ip_address"],
31
+ s["last_used_at"],
32
+ s["expires_at"],
33
+ str(s["is_current"]),
34
+ ]
35
+ table.add_row(*args)
36
+ console.print(table)
@@ -0,0 +1,35 @@
1
+ """``bevor sessions revoke``."""
2
+
3
+ import typer
4
+
5
+ from bevor_cli import output
6
+ from bevor_cli.client import BevorInternalBusinessClient
7
+ from bevor_cli.configuration.creds import Credentials
8
+
9
+
10
+ def sessions_revoke(
11
+ token_id: str = typer.Argument(
12
+ ...,
13
+ help="CLI session id to revoke (from `bevor sessions`).",
14
+ ),
15
+ ) -> None:
16
+ """Revoke a CLI session by id."""
17
+
18
+ creds = Credentials.load()
19
+ creds.require_token()
20
+
21
+ if token_id == creds.token:
22
+ output.error(
23
+ "Cannot revoke this session since it's your current one.",
24
+ hint="Run `bevor logout` instead.",
25
+ )
26
+ raise typer.Exit(1)
27
+
28
+ client = BevorInternalBusinessClient(creds=creds)
29
+
30
+ res = client.delete(f"/v1/cli-tokens/revoke/{token_id}").json()
31
+ output.success(f"Revoked CLI session {token_id}.")
32
+
33
+ if res["is_current"]:
34
+ creds.delete()
35
+ output.success("Logged out.")
File without changes
@@ -0,0 +1,50 @@
1
+ """``bevor workspace list`` — list workspaces."""
2
+
3
+ import json
4
+ from typing import Any, List
5
+
6
+ from rich.table import Table
7
+ from rich.text import Text
8
+
9
+ from bevor_cli.annotations.confirmations import JSON_OUT
10
+ from bevor_cli.client import BevorInternalBusinessClient
11
+ from bevor_cli.configuration.creds import Credentials
12
+ from bevor_cli.configuration.state import State
13
+ from bevor_cli.output import console
14
+
15
+
16
+ def list_workspaces(
17
+ json_out: JSON_OUT = False,
18
+ ) -> None:
19
+ """List workspaces you belong to."""
20
+ creds = Credentials.load()
21
+ creds.require_token()
22
+
23
+ state = State.load()
24
+
25
+ workspaces = BevorInternalBusinessClient(creds=creds).list_workspaces()
26
+
27
+ if json_out:
28
+ console.print_json(json.dumps(workspaces.model_dump(mode="json"), indent=2))
29
+ return
30
+
31
+ if not workspaces.results:
32
+ console.print("No workspaces found.")
33
+ return
34
+
35
+ table = Table("ID", "Name", "N Projects", "Role", "Created")
36
+
37
+ for workspace in workspaces.results:
38
+ args: List[Any] = [
39
+ workspace.id,
40
+ workspace.name,
41
+ str(workspace.n_projects),
42
+ workspace.role,
43
+ str(workspace.created_at.date()),
44
+ ]
45
+
46
+ if workspace.id == state.workspace_id:
47
+ args.append(Text("●", style="bold green"))
48
+ table.add_row(*args)
49
+
50
+ console.print(table)
@@ -0,0 +1,20 @@
1
+ """Interactive workspace picker (shared with ``bevor link``)."""
2
+
3
+ from typing import List
4
+
5
+ import questionary
6
+ import typer
7
+ from bevor_sdk.types.responses.manage import WorkspaceDetailedSchema
8
+
9
+ from bevor_cli.select import _select
10
+
11
+
12
+ def pick_workspace(workspaces: List[WorkspaceDetailedSchema]) -> WorkspaceDetailedSchema:
13
+ choices = [
14
+ questionary.Choice(title=f"{workspace.name} {workspace.id}", value=workspace)
15
+ for workspace in workspaces
16
+ ]
17
+ chosen: WorkspaceDetailedSchema | None = _select("Select a workspace:", choices)
18
+ if chosen is None:
19
+ raise typer.Exit(0)
20
+ return chosen
@@ -0,0 +1,68 @@
1
+ """``bevor workspaces select`` — set the selected workspace."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+
7
+ from bevor_cli import output
8
+ from bevor_cli.client import BevorInternalBusinessClient
9
+ from bevor_cli.configuration.creds import Credentials
10
+ from bevor_cli.configuration.state import State
11
+ from bevor_cli.output import console
12
+
13
+ from .pick import pick_workspace
14
+
15
+
16
+ def select_workspace(
17
+ workspace_id: Optional[str] = typer.Argument(
18
+ None, help="Workspace id to select. Prompts with a list when omitted."
19
+ ),
20
+ ) -> None:
21
+ """Select a workspace by id, or pick one from a list."""
22
+
23
+ creds = Credentials.load()
24
+ creds.require_token()
25
+
26
+ state = State.load()
27
+
28
+ if workspace_id and state.workspace_id == workspace_id:
29
+ return
30
+
31
+ workspaces = BevorInternalBusinessClient(creds=creds).list_workspaces()
32
+
33
+ if not workspaces.results:
34
+ console.print("No workspaces found.")
35
+ return
36
+
37
+ if workspace_id:
38
+ if not any(workspace.id == workspace_id for workspace in workspaces.results):
39
+ output.error(
40
+ "passed workspace does not exist",
41
+ hint="run `bevor workspaces list` to see available ids.",
42
+ )
43
+ raise typer.Exit(1)
44
+
45
+ state.set_workspace(workspace_id)
46
+ state.save()
47
+ return
48
+
49
+ if len(workspaces.results) == 1:
50
+ if workspaces.results[0].id == state.workspace_id:
51
+ output.info("only 1 workspace exists, skipping")
52
+ return
53
+
54
+ state.set_workspace(workspaces.results[0].id)
55
+ state.save()
56
+ output.info("only 1 workspace exists, updating your bevor context automatically")
57
+ return
58
+
59
+ workspace = pick_workspace(workspaces.results)
60
+
61
+ if state.workspace_id == workspace.id:
62
+ output.info("no changes to your current bevor context.")
63
+ return
64
+
65
+ state.set_workspace(workspace.id)
66
+ state.save()
67
+
68
+ output.success(f"Active workspace: {workspace.name} ({workspace.id})")
@@ -0,0 +1,48 @@
1
+ """``bevor workspaces view`` — show a workspace (the selected one by default)."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from rich.table import Table
7
+
8
+ from bevor_cli.annotations.confirmations import JSON_OUT
9
+ from bevor_cli.client import BevorInternalBusinessClient
10
+ from bevor_cli.configuration.creds import Credentials
11
+ from bevor_cli.configuration.state import State
12
+ from bevor_cli.output import console
13
+
14
+
15
+ def view_workspace(
16
+ workspace_id: Optional[str] = typer.Argument(
17
+ None, help="Workspace id. Defaults to the selected workspace."
18
+ ),
19
+ json_out: JSON_OUT = False,
20
+ ) -> None:
21
+ """Print details for a workspace, or the selected one."""
22
+ creds = Credentials.load()
23
+ creds.require_token()
24
+
25
+ client = BevorInternalBusinessClient(
26
+ creds=creds, workspace_id=workspace_id or State.load().validate_workspace()
27
+ )
28
+
29
+ workspace = client.get_workspace()
30
+
31
+ if json_out:
32
+ console.print_json(workspace.model_dump_json(indent=2))
33
+ return
34
+
35
+ table = Table(show_header=False, box=None, padding=(0, 1))
36
+ table.add_column(style="bold")
37
+ table.add_column()
38
+
39
+ table.add_row("Name", workspace.name)
40
+ table.add_row("ID", workspace.id)
41
+ table.add_row("My Role", workspace.role)
42
+ table.add_row("Projects", str(workspace.n_projects))
43
+ table.add_row("Members", str(workspace.n_members))
44
+ table.add_row("Default workspace", str(workspace.is_default))
45
+ table.add_row("Created", str(workspace.created_at))
46
+ table.add_row("Created by", workspace.created_by_user.username)
47
+
48
+ console.print(table)