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.
- bevor_cli/__init__.py +0 -0
- bevor_cli/annotations/__init__.py +0 -0
- bevor_cli/annotations/confirmations.py +16 -0
- bevor_cli/annotations/query.py +25 -0
- bevor_cli/apps/agent.py +10 -0
- bevor_cli/apps/analyses.py +47 -0
- bevor_cli/apps/chat.py +16 -0
- bevor_cli/apps/codes.py +18 -0
- bevor_cli/apps/findings.py +22 -0
- bevor_cli/apps/main.py +61 -0
- bevor_cli/apps/nodes.py +17 -0
- bevor_cli/apps/projects.py +14 -0
- bevor_cli/apps/sessions.py +13 -0
- bevor_cli/apps/workspaces.py +16 -0
- bevor_cli/client.py +157 -0
- bevor_cli/commands/__init__.py +0 -0
- bevor_cli/commands/agent/__init__.py +0 -0
- bevor_cli/commands/agent/materialze_cmd.py +65 -0
- bevor_cli/commands/agent/setup_cmd.py +18 -0
- bevor_cli/commands/analyses/__init__.py +0 -0
- bevor_cli/commands/analyses/active.py +42 -0
- bevor_cli/commands/analyses/chats/__init__.py +0 -0
- bevor_cli/commands/analyses/chats/get.py +39 -0
- bevor_cli/commands/analyses/chats/list.py +64 -0
- bevor_cli/commands/analyses/chats/messages.py +41 -0
- bevor_cli/commands/analyses/chats/new.py +44 -0
- bevor_cli/commands/analyses/chats/resume.py +43 -0
- bevor_cli/commands/analyses/chats/session.py +295 -0
- bevor_cli/commands/analyses/child.py +81 -0
- bevor_cli/commands/analyses/commit.py +49 -0
- bevor_cli/commands/analyses/create.py +105 -0
- bevor_cli/commands/analyses/delete.py +65 -0
- bevor_cli/commands/analyses/diff.py +87 -0
- bevor_cli/commands/analyses/entrypoints.py +56 -0
- bevor_cli/commands/analyses/findings.py +129 -0
- bevor_cli/commands/analyses/fork.py +47 -0
- bevor_cli/commands/analyses/list.py +85 -0
- bevor_cli/commands/analyses/merge.py +46 -0
- bevor_cli/commands/analyses/retry.py +77 -0
- bevor_cli/commands/analyses/run.py +87 -0
- bevor_cli/commands/analyses/select.py +85 -0
- bevor_cli/commands/analyses/set_head.py +45 -0
- bevor_cli/commands/analyses/transitions.py +90 -0
- bevor_cli/commands/analyses/view.py +94 -0
- bevor_cli/commands/auth/__init__.py +7 -0
- bevor_cli/commands/auth/login.py +135 -0
- bevor_cli/commands/auth/logout.py +23 -0
- bevor_cli/commands/auth/whoami.py +21 -0
- bevor_cli/commands/codes/__init__.py +0 -0
- bevor_cli/commands/codes/list.py +90 -0
- bevor_cli/commands/codes/push.py +76 -0
- bevor_cli/commands/codes/select.py +93 -0
- bevor_cli/commands/codes/view.py +80 -0
- bevor_cli/commands/context/__init__.py +92 -0
- bevor_cli/commands/findings/__init__.py +0 -0
- bevor_cli/commands/findings/acknowledge.py +28 -0
- bevor_cli/commands/findings/add.py +236 -0
- bevor_cli/commands/findings/bulk.py +110 -0
- bevor_cli/commands/findings/delete.py +28 -0
- bevor_cli/commands/findings/edit.py +109 -0
- bevor_cli/commands/findings/get.py +62 -0
- bevor_cli/commands/findings/interactive.py +329 -0
- bevor_cli/commands/findings/list.py +108 -0
- bevor_cli/commands/findings/revert.py +28 -0
- bevor_cli/commands/graph/__init__.py +0 -0
- bevor_cli/commands/graph/call_chain.py +48 -0
- bevor_cli/commands/graph/edges.py +59 -0
- bevor_cli/commands/graph/interactive.py +207 -0
- bevor_cli/commands/graph/renderer.py +135 -0
- bevor_cli/commands/link/__init__.py +52 -0
- bevor_cli/commands/link/helpers.py +26 -0
- bevor_cli/commands/nodes/__init__.py +0 -0
- bevor_cli/commands/nodes/get.py +52 -0
- bevor_cli/commands/nodes/list.py +75 -0
- bevor_cli/commands/nodes/source.py +40 -0
- bevor_cli/commands/projects/__init__.py +0 -0
- bevor_cli/commands/projects/create.py +54 -0
- bevor_cli/commands/projects/list.py +57 -0
- bevor_cli/commands/projects/select.py +75 -0
- bevor_cli/commands/projects/view.py +49 -0
- bevor_cli/commands/sessions/__init__.py +0 -0
- bevor_cli/commands/sessions/list.py +36 -0
- bevor_cli/commands/sessions/revoke.py +35 -0
- bevor_cli/commands/workspaces/__init__.py +0 -0
- bevor_cli/commands/workspaces/list.py +50 -0
- bevor_cli/commands/workspaces/pick.py +20 -0
- bevor_cli/commands/workspaces/select.py +68 -0
- bevor_cli/commands/workspaces/view.py +48 -0
- bevor_cli/configuration/creds.py +52 -0
- bevor_cli/configuration/settings.py +54 -0
- bevor_cli/configuration/state.py +113 -0
- bevor_cli/findings_yaml.py +291 -0
- bevor_cli/main.py +33 -0
- bevor_cli/output.py +69 -0
- bevor_cli/select.py +64 -0
- bevor_cli/settings.py +4 -0
- bevor_cli-0.1.0.dist-info/METADATA +14 -0
- bevor_cli-0.1.0.dist-info/RECORD +100 -0
- bevor_cli-0.1.0.dist-info/WHEEL +4 -0
- bevor_cli-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""``bevor finding list``."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Literal, Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from bevor_sdk.types import ID
|
|
7
|
+
from bevor_sdk.types.literals import FindingDraftOperation, FindingLevel, FindingType
|
|
8
|
+
from bevor_sdk.types.requests.security import FindingsQueryParams
|
|
9
|
+
|
|
10
|
+
from bevor_cli import output
|
|
11
|
+
from bevor_cli.annotations.confirmations import JSON_OUT
|
|
12
|
+
from bevor_cli.annotations.query import ORDER, PAGE, PAGE_SIZE
|
|
13
|
+
from bevor_cli.client import BevorInternalClient
|
|
14
|
+
from bevor_cli.commands.findings.interactive import (
|
|
15
|
+
print_findings_table_with_ops,
|
|
16
|
+
)
|
|
17
|
+
from bevor_cli.configuration.creds import Credentials
|
|
18
|
+
from bevor_cli.configuration.settings import Settings
|
|
19
|
+
from bevor_cli.configuration.state import State
|
|
20
|
+
from bevor_cli.output import console
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def list_findings(
|
|
24
|
+
page: PAGE = None,
|
|
25
|
+
page_size: PAGE_SIZE = None,
|
|
26
|
+
order: ORDER = None,
|
|
27
|
+
order_by: Annotated[
|
|
28
|
+
Literal["created_at", "updated_at", "level"] | None,
|
|
29
|
+
typer.Option(
|
|
30
|
+
"--order-by", help="What field to order results by when a resource supports pagination"
|
|
31
|
+
),
|
|
32
|
+
] = None,
|
|
33
|
+
project_id: Optional[str] = typer.Option(None, "--project", help="Project id to search (defaults to configured project)."),
|
|
34
|
+
is_self: Optional[bool] = typer.Option(
|
|
35
|
+
None, "--self", help="Filter by findings in analyses the caller owns."
|
|
36
|
+
),
|
|
37
|
+
user_id: Optional[str] = typer.Option(
|
|
38
|
+
None, "--user", help="Filter by user id (user who owns the analysis the finding is in)"
|
|
39
|
+
),
|
|
40
|
+
code_id: Optional[str] = typer.Option(None, "--code", help="Code Id to filter by."),
|
|
41
|
+
analysis_ids: Optional[str] = typer.Option(
|
|
42
|
+
None, "--analyses", help="Analysis ids to filter by (comma separated)"
|
|
43
|
+
),
|
|
44
|
+
finding_type: Optional[FindingType] = typer.Option(None, "--type", help="Finding type."),
|
|
45
|
+
level: Optional[FindingLevel] = typer.Option(None, "--level", help="Finding level."),
|
|
46
|
+
is_acknowledged: Optional[bool] = typer.Option(
|
|
47
|
+
None, "--acknowledged", help="Filter by acknowledgement status."
|
|
48
|
+
),
|
|
49
|
+
operation: Optional[FindingDraftOperation] = typer.Option(
|
|
50
|
+
None, "--operation", help="Staged draft operation."
|
|
51
|
+
),
|
|
52
|
+
created_by_self: Optional[bool] = typer.Option(
|
|
53
|
+
None, "--created-by-self", help="Filter by findings the caller is original creator of"
|
|
54
|
+
),
|
|
55
|
+
created_by_user_id: Optional[str] = typer.Option(
|
|
56
|
+
None, "--created-by", help="Filter by original creator of a finding."
|
|
57
|
+
),
|
|
58
|
+
json_out: JSON_OUT = False,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Search findings across the currently configured project, or the --project."""
|
|
61
|
+
creds = Credentials.load()
|
|
62
|
+
creds.require_token()
|
|
63
|
+
|
|
64
|
+
state = State.load()
|
|
65
|
+
workspace_id = state.validate_workspace()
|
|
66
|
+
settings = Settings.load()
|
|
67
|
+
|
|
68
|
+
client = BevorInternalClient(
|
|
69
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project(priority=project_id)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
query_params = FindingsQueryParams(
|
|
73
|
+
page=page,
|
|
74
|
+
page_size=page_size or settings.default_page_size,
|
|
75
|
+
order=order,
|
|
76
|
+
order_by=order_by,
|
|
77
|
+
type=finding_type,
|
|
78
|
+
level=level,
|
|
79
|
+
is_acknowledged=is_acknowledged,
|
|
80
|
+
operation=operation,
|
|
81
|
+
)
|
|
82
|
+
if is_self is not None:
|
|
83
|
+
query_params["is_self"] = is_self
|
|
84
|
+
elif user_id:
|
|
85
|
+
query_params["user_id"] = ID(user_id)
|
|
86
|
+
if code_id:
|
|
87
|
+
query_params["code_id"] = ID(code_id)
|
|
88
|
+
if analysis_ids:
|
|
89
|
+
query_params["analysis_ids"] = [ID(aid.strip()) for aid in analysis_ids.split(",")]
|
|
90
|
+
if created_by_self is not None:
|
|
91
|
+
query_params["created_by_self"] = created_by_self
|
|
92
|
+
elif created_by_user_id:
|
|
93
|
+
query_params["created_by_user_id"] = ID(created_by_user_id)
|
|
94
|
+
|
|
95
|
+
found = client.findings.list(query_params)
|
|
96
|
+
|
|
97
|
+
if json_out:
|
|
98
|
+
console.print_json(found.model_dump_json(indent=2))
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
if not found.results:
|
|
102
|
+
console.print("No findings.")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
print_findings_table_with_ops(found.results)
|
|
106
|
+
|
|
107
|
+
if found.more:
|
|
108
|
+
output.info(f"\nShowing {found.page} of {found.total_pages}. Increase with --page-size/-n.")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""``bevor findings revert``."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from bevor_sdk.types import ID
|
|
5
|
+
|
|
6
|
+
from bevor_cli import output
|
|
7
|
+
from bevor_cli.client import BevorInternalClient
|
|
8
|
+
from bevor_cli.configuration.creds import Credentials
|
|
9
|
+
from bevor_cli.configuration.state import State
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def revert_finding(
|
|
13
|
+
finding_id: str = typer.Argument(..., help="Finding id to revert."),
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Revert a staged finding change."""
|
|
16
|
+
creds = Credentials.load()
|
|
17
|
+
creds.require_token()
|
|
18
|
+
|
|
19
|
+
state = State.load()
|
|
20
|
+
workspace_id = state.validate_workspace()
|
|
21
|
+
|
|
22
|
+
client = BevorInternalClient(
|
|
23
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
client.findings.revert(ID(finding_id))
|
|
27
|
+
|
|
28
|
+
output.success("Finding reverted.")
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Call-chain command (mounted at ``bevor code node call-chain``)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from bevor_sdk.types import ID
|
|
8
|
+
from bevor_sdk.types.requests.graph import CallChainBodyDict
|
|
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
|
+
from .renderer import build_call_chain_rich_tree
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def call_chain_cmd(
|
|
20
|
+
node_id: str = typer.Argument(..., help="Node id"),
|
|
21
|
+
code_id: Optional[str] = typer.Option(
|
|
22
|
+
None, "--code", help="Code id to list for (defaults to current configuration)"
|
|
23
|
+
),
|
|
24
|
+
json_out: JSON_OUT = False,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Get the call chain for a given node. Uses the currently configured code version, or you can pass --code."""
|
|
27
|
+
creds = Credentials.load()
|
|
28
|
+
creds.require_token()
|
|
29
|
+
|
|
30
|
+
state = State.load()
|
|
31
|
+
workspace_id = state.validate_workspace()
|
|
32
|
+
code_id = state.validate_code(priority=code_id)
|
|
33
|
+
|
|
34
|
+
client = BevorInternalClient(
|
|
35
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
call_chain = client.codes.nodes.call_chain(
|
|
39
|
+
code_id,
|
|
40
|
+
CallChainBodyDict(node_ids=[ID(node_id)], response_type="json"),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if not call_chain.results:
|
|
44
|
+
console.print("[dim]Empty call chain.[/dim]")
|
|
45
|
+
elif json_out:
|
|
46
|
+
console.print_json(json.dumps(call_chain.results[0].call_chain, indent=2))
|
|
47
|
+
else:
|
|
48
|
+
console.print(build_call_chain_rich_tree(call_chain.results[0], label=node_id))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Edges list (mounted at ``bevor code node edges``)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from bevor_sdk.types import ID
|
|
8
|
+
from bevor_sdk.types.literals import EdgeDirection, EdgeType
|
|
9
|
+
from bevor_sdk.types.requests.graph import EdgeQueryParams
|
|
10
|
+
|
|
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_node_edges(
|
|
19
|
+
node_id: str = typer.Argument(..., help="Node short id or full wire id."),
|
|
20
|
+
code_id: Optional[str] = typer.Option(
|
|
21
|
+
None, "--code", help="Code id to list for (defaults to current configuration)"
|
|
22
|
+
),
|
|
23
|
+
direction: Optional[EdgeDirection] = typer.Option(
|
|
24
|
+
None,
|
|
25
|
+
"--direction",
|
|
26
|
+
help="Edge direction filter: in, out.",
|
|
27
|
+
),
|
|
28
|
+
edge_type: Optional[EdgeType] = typer.Option(
|
|
29
|
+
None,
|
|
30
|
+
"--type",
|
|
31
|
+
help="Edge type (e.g. calls, defines).",
|
|
32
|
+
),
|
|
33
|
+
json_out: JSON_OUT = False,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""List the edges for a given node. 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
|
+
res = client.codes.nodes.edges(
|
|
48
|
+
code_id, ID(node_id), EdgeQueryParams(edge_type=edge_type, direction=direction)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if not res.results:
|
|
52
|
+
console.print("No edges.")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
if json_out:
|
|
56
|
+
console.print_json(res.model_dump_json(indent=2))
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
console.print_json(json.dumps([e.model_dump(mode="json") for e in res.results], indent=2))
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""``bevor code node interactive`` — browse from a node with get / source / call-chain / edges."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional, cast
|
|
6
|
+
|
|
7
|
+
import questionary
|
|
8
|
+
import typer
|
|
9
|
+
from bevor_sdk._shared import CallNode
|
|
10
|
+
from bevor_sdk.types import ID
|
|
11
|
+
from bevor_sdk.types.requests.graph import CallChainBodyDict
|
|
12
|
+
from bevor_sdk.types.responses.graph import GraphSnapshotEdge, GraphSnapshotNode
|
|
13
|
+
from rich.syntax import Syntax
|
|
14
|
+
|
|
15
|
+
from bevor_cli import output
|
|
16
|
+
from bevor_cli.client import BevorInternalClient
|
|
17
|
+
from bevor_cli.configuration.creds import Credentials
|
|
18
|
+
from bevor_cli.configuration.state import State
|
|
19
|
+
from bevor_cli.output import console
|
|
20
|
+
from bevor_cli.select import _CANCEL
|
|
21
|
+
|
|
22
|
+
from .renderer import build_call_chain_rich_tree, call_chain_nav_choices
|
|
23
|
+
|
|
24
|
+
_MENU_STYLE = questionary.Style(
|
|
25
|
+
[
|
|
26
|
+
("highlighted", "bold"),
|
|
27
|
+
("selected", "bold fg:ansibrightyellow"),
|
|
28
|
+
("instruction", "fg:ansibrightblack italic"),
|
|
29
|
+
("pointer", "bold fg:ansibrightyellow"),
|
|
30
|
+
]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _other_endpoint(self_full_id: str, e: GraphSnapshotEdge) -> str:
|
|
35
|
+
s, d = str(e.src_id), str(e.dst_id)
|
|
36
|
+
if s == self_full_id:
|
|
37
|
+
return d
|
|
38
|
+
if d == self_full_id:
|
|
39
|
+
return s
|
|
40
|
+
return d
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _select_jump(
|
|
44
|
+
message: str,
|
|
45
|
+
pairs: list[tuple[str, str]],
|
|
46
|
+
) -> str | None:
|
|
47
|
+
"""Return chosen node ref, or None for cancel / stay."""
|
|
48
|
+
if not pairs:
|
|
49
|
+
return None
|
|
50
|
+
choices: list = [questionary.Choice(title=label, value=ref) for ref, label in pairs]
|
|
51
|
+
choices.append(questionary.Choice(title="↵ Stay on current node", value=_CANCEL))
|
|
52
|
+
r = questionary.select(
|
|
53
|
+
message,
|
|
54
|
+
choices=choices,
|
|
55
|
+
use_jk_keys=True,
|
|
56
|
+
style=_MENU_STYLE,
|
|
57
|
+
instruction="(↑↓/j/k · Enter)",
|
|
58
|
+
).ask()
|
|
59
|
+
if r is None or r is _CANCEL:
|
|
60
|
+
return None
|
|
61
|
+
return str(r)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _main_action(
|
|
65
|
+
node: GraphSnapshotNode,
|
|
66
|
+
*,
|
|
67
|
+
can_back: bool,
|
|
68
|
+
) -> str | None:
|
|
69
|
+
choices: list[questionary.Choice] = [
|
|
70
|
+
questionary.Choice("Get — show metadata", value="g"),
|
|
71
|
+
questionary.Choice("Source — show code content", value="s"),
|
|
72
|
+
]
|
|
73
|
+
if node.is_callable:
|
|
74
|
+
choices.append(
|
|
75
|
+
questionary.Choice("Call chain — tree, then pick a node to jump to", value="c")
|
|
76
|
+
)
|
|
77
|
+
choices.append(
|
|
78
|
+
questionary.Choice("Edges — list edges, then pick a neighbor to jump to", value="e")
|
|
79
|
+
)
|
|
80
|
+
if can_back:
|
|
81
|
+
choices.append(questionary.Choice("Back — previous node", value="b"))
|
|
82
|
+
choices.append(questionary.Choice("Quit", value="q"))
|
|
83
|
+
r = questionary.select(
|
|
84
|
+
f"{node.short_id} {node.name} — action",
|
|
85
|
+
choices=choices,
|
|
86
|
+
style=_MENU_STYLE,
|
|
87
|
+
instruction="(↑↓/j/k · Enter)",
|
|
88
|
+
).ask()
|
|
89
|
+
if r is None:
|
|
90
|
+
return "q"
|
|
91
|
+
return cast(str, r)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def interactive_cmd(
|
|
95
|
+
node_id: str = typer.Argument(..., help="Node to start from (short id or full id)."),
|
|
96
|
+
code_id: Optional[str] = typer.Option(
|
|
97
|
+
None, "--code", help="Code id to list for (defaults to current configuration)"
|
|
98
|
+
),
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Explore the graph from a node: get, source, optional call-chain (callable nodes only), and edges.
|
|
101
|
+
Uses the currently configured code version, or you can pass --code.
|
|
102
|
+
"""
|
|
103
|
+
creds = Credentials.load()
|
|
104
|
+
creds.require_token()
|
|
105
|
+
|
|
106
|
+
state = State.load()
|
|
107
|
+
workspace_id = state.validate_workspace()
|
|
108
|
+
code_id = state.validate_code(priority=code_id)
|
|
109
|
+
|
|
110
|
+
client = BevorInternalClient(
|
|
111
|
+
creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
current = node_id
|
|
115
|
+
back_stack: list[str] = []
|
|
116
|
+
|
|
117
|
+
while True:
|
|
118
|
+
with output.spinner(f"Loading node {current}…"):
|
|
119
|
+
try:
|
|
120
|
+
node = client.codes.nodes.get(code_id, ID(current))
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
output.error(f"Could not load node {current!r}: {exc}")
|
|
123
|
+
raise typer.Exit(1) from exc
|
|
124
|
+
|
|
125
|
+
action = _main_action(node, can_back=bool(back_stack))
|
|
126
|
+
if action is None or action == "q":
|
|
127
|
+
return
|
|
128
|
+
if action == "b":
|
|
129
|
+
if not back_stack:
|
|
130
|
+
continue
|
|
131
|
+
current = back_stack.pop()
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
if action == "g":
|
|
135
|
+
typer.echo(f"ID: {node.id}")
|
|
136
|
+
typer.echo(f"Short id: {node.short_id}")
|
|
137
|
+
typer.echo(f"Name: {node.name}")
|
|
138
|
+
typer.echo(f"Type: {node.node_type}")
|
|
139
|
+
typer.echo(f"File: {node.path} ({node.file_id})")
|
|
140
|
+
elif action == "s":
|
|
141
|
+
with output.spinner(f"Fetching source for {node.short_id}…"):
|
|
142
|
+
content = client.codes.nodes.content(code_id, ID(node.short_id))
|
|
143
|
+
if not content:
|
|
144
|
+
output.error("No source content available.")
|
|
145
|
+
else:
|
|
146
|
+
console.print(
|
|
147
|
+
Syntax(
|
|
148
|
+
content.strip(), "solidity", theme="monokai", line_numbers=False, padding=1
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
elif action == "c":
|
|
152
|
+
if not node.is_callable:
|
|
153
|
+
output.error("Call chain is only available for callable nodes.")
|
|
154
|
+
continue
|
|
155
|
+
data = client.codes.nodes.call_chain(
|
|
156
|
+
code_id,
|
|
157
|
+
CallChainBodyDict(node_ids=[ID(node.short_id)], response_type="json"),
|
|
158
|
+
)
|
|
159
|
+
if not data.results:
|
|
160
|
+
console.print("[dim]Empty call chain.[/dim]")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
console.print(build_call_chain_rich_tree(data.results[0], label=node_id))
|
|
164
|
+
root = CallNode.from_dict(cast(dict, data.results[0].call_chain))
|
|
165
|
+
pairs = (
|
|
166
|
+
call_chain_nav_choices(
|
|
167
|
+
root,
|
|
168
|
+
skip_short_id=str(node.short_id),
|
|
169
|
+
skip_full_id=str(node.id),
|
|
170
|
+
)
|
|
171
|
+
if root
|
|
172
|
+
else []
|
|
173
|
+
)
|
|
174
|
+
if pairs:
|
|
175
|
+
picked = _select_jump("Jump to which node? (or stay)", pairs)
|
|
176
|
+
if picked:
|
|
177
|
+
back_stack.append(current)
|
|
178
|
+
current = picked
|
|
179
|
+
else:
|
|
180
|
+
typer.echo("No other nodes in this call chain to jump to.")
|
|
181
|
+
elif action == "e":
|
|
182
|
+
with output.spinner("Fetching edges…"):
|
|
183
|
+
res = client.codes.nodes.edges(code_id, ID(node.short_id))
|
|
184
|
+
self_id = str(node.id)
|
|
185
|
+
rows: list[GraphSnapshotEdge] = list(res.results)
|
|
186
|
+
if not rows:
|
|
187
|
+
typer.echo("No edges.")
|
|
188
|
+
else:
|
|
189
|
+
labels: list[tuple[str, str]] = []
|
|
190
|
+
with output.spinner("Resolving edge endpoints…"):
|
|
191
|
+
for e in rows:
|
|
192
|
+
other = _other_endpoint(self_id, e)
|
|
193
|
+
nbr = client.codes.nodes.get(code_id, ID(other))
|
|
194
|
+
way = e.direction if e.direction else "—"
|
|
195
|
+
labels.append(
|
|
196
|
+
(
|
|
197
|
+
nbr.short_id,
|
|
198
|
+
f"{e.edge_type} {way} {nbr.name} [{nbr.short_id}]",
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
picked = _select_jump("Jump to which neighbor? (or stay)", labels)
|
|
202
|
+
if picked:
|
|
203
|
+
back_stack.append(current)
|
|
204
|
+
current = picked
|
|
205
|
+
else:
|
|
206
|
+
output.error(f"Unknown action: {action!r}")
|
|
207
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Rich tree rendering for call-chain JSON (CLI-only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from bevor_sdk._shared import CallNode
|
|
8
|
+
from bevor_sdk.helpers.call_chain.enrich_impl import (
|
|
9
|
+
_format_line,
|
|
10
|
+
callable_display_parts,
|
|
11
|
+
format_resolved_storage_access_line,
|
|
12
|
+
)
|
|
13
|
+
from bevor_sdk.types.responses.graph import CallChainSchema
|
|
14
|
+
from rich.markup import escape as rich_escape
|
|
15
|
+
from rich.tree import Tree
|
|
16
|
+
|
|
17
|
+
_EDGE_COLORS = {
|
|
18
|
+
"calls": "green",
|
|
19
|
+
"calls_external": "yellow",
|
|
20
|
+
"reads": "blue",
|
|
21
|
+
"writes": "red",
|
|
22
|
+
"uses": "cyan",
|
|
23
|
+
"initializes": "magenta",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _rich_headline(cn: CallNode, parent: CallNode | None) -> str:
|
|
28
|
+
nm, sid = callable_display_parts(cn)
|
|
29
|
+
if cn.is_cyclic:
|
|
30
|
+
if sid:
|
|
31
|
+
return f"[bold red][CYCLIC][/bold red] {rich_escape(nm)} [{rich_escape(sid)}]"
|
|
32
|
+
return f"[bold red][CYCLIC][/bold red] {rich_escape(nm)}"
|
|
33
|
+
plain = _format_line(cn, parent)
|
|
34
|
+
et = cn.edge_type
|
|
35
|
+
if et:
|
|
36
|
+
c = _EDGE_COLORS.get(str(et), "white")
|
|
37
|
+
marker = f" {nm} ["
|
|
38
|
+
i = plain.find(marker)
|
|
39
|
+
if i >= 0:
|
|
40
|
+
prefix, rest = plain[:i], plain[i + 1 :]
|
|
41
|
+
return f"[{c}]{rich_escape(prefix)}[/{c}] {rich_escape(rest)}"
|
|
42
|
+
return rich_escape(plain)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _append_resolved_bindings(parent: Tree, cn: CallNode) -> None:
|
|
46
|
+
binds = cn.resolved_bindings
|
|
47
|
+
if not binds:
|
|
48
|
+
return
|
|
49
|
+
inject = parent.add("[dim]injected from parent call[/dim]")
|
|
50
|
+
for a in sorted(binds, key=lambda x: x.start_byte):
|
|
51
|
+
inject.add(rich_escape(format_resolved_storage_access_line(a)))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _append_branch(
|
|
55
|
+
parent: Tree,
|
|
56
|
+
cn: CallNode,
|
|
57
|
+
*,
|
|
58
|
+
depth: int = 0,
|
|
59
|
+
) -> None:
|
|
60
|
+
if cn.is_cyclic:
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
_append_resolved_bindings(parent, cn)
|
|
64
|
+
|
|
65
|
+
children = cn.children
|
|
66
|
+
arg_children = [ch for ch in children if ch.is_arg]
|
|
67
|
+
non_arg_children = [ch for ch in children if not ch.is_arg]
|
|
68
|
+
if arg_children:
|
|
69
|
+
args_branch = parent.add("[dim]args[/dim]")
|
|
70
|
+
for ch in arg_children:
|
|
71
|
+
sub = args_branch.add(_rich_headline(ch, cn))
|
|
72
|
+
_append_branch(sub, ch, depth=depth + 2)
|
|
73
|
+
for ch in non_arg_children:
|
|
74
|
+
sub = parent.add(_rich_headline(ch, cn))
|
|
75
|
+
_append_branch(sub, ch, depth=depth + 1)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def build_call_chain_rich_tree(root: CallChainSchema, label: str = "call chain") -> Tree:
|
|
79
|
+
"""Build a :class:`rich.tree.Tree` for a deserialized call chain."""
|
|
80
|
+
call_node = CallNode.from_dict(cast(dict, root.call_chain))
|
|
81
|
+
|
|
82
|
+
_, root_sid = callable_display_parts(call_node)
|
|
83
|
+
first_ref = str(root.short_id or call_node.edge_id or root_sid)
|
|
84
|
+
body = _rich_headline(call_node, None)
|
|
85
|
+
if label and first_ref and str(label) == first_ref:
|
|
86
|
+
root_label = f"[bold]{body}[/bold]"
|
|
87
|
+
else:
|
|
88
|
+
root_label = f"[bold]{body}[/bold] [dim]{rich_escape(str(label))}[/dim]"
|
|
89
|
+
tree = Tree(root_label)
|
|
90
|
+
_append_branch(tree, call_node, depth=0)
|
|
91
|
+
return tree
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def call_chain_nav_choices(
|
|
95
|
+
root: CallNode,
|
|
96
|
+
*,
|
|
97
|
+
skip_short_id: str | None = None,
|
|
98
|
+
skip_full_id: str | None = None,
|
|
99
|
+
) -> list[tuple[str, str]]:
|
|
100
|
+
"""Unique ``(short_id, label)`` pairs in the same visit order as :meth:`CallNode.to_text`."""
|
|
101
|
+
seen: set[str] = set()
|
|
102
|
+
out: list[tuple[str, str]] = []
|
|
103
|
+
|
|
104
|
+
def maybe_add(cn: CallNode) -> None:
|
|
105
|
+
nm, bracket = callable_display_parts(cn)
|
|
106
|
+
full = str(cn.node_id or "")
|
|
107
|
+
if skip_short_id and cn.short_id == skip_short_id:
|
|
108
|
+
return
|
|
109
|
+
if skip_full_id and full and full == skip_full_id:
|
|
110
|
+
return
|
|
111
|
+
key = f"{cn.short_id or ''}|{cn.edge_id or ''}|{cn.node_id or ''}"
|
|
112
|
+
if key in seen:
|
|
113
|
+
return
|
|
114
|
+
seen.add(key)
|
|
115
|
+
ref = str(cn.short_id or cn.edge_id or bracket)
|
|
116
|
+
et = cn.edge_type
|
|
117
|
+
if et:
|
|
118
|
+
lab = f"{et} {nm} [{ref[:12]}]"
|
|
119
|
+
else:
|
|
120
|
+
lab = f"{nm} [{ref[:12]}]"
|
|
121
|
+
out.append((ref, lab))
|
|
122
|
+
|
|
123
|
+
def visit(cn: CallNode) -> None:
|
|
124
|
+
maybe_add(cn)
|
|
125
|
+
if cn.is_cyclic:
|
|
126
|
+
return
|
|
127
|
+
arg_children = [ch for ch in cn.children if ch.is_arg]
|
|
128
|
+
non_arg_children = [ch for ch in cn.children if not ch.is_arg]
|
|
129
|
+
for ch in arg_children:
|
|
130
|
+
visit(ch)
|
|
131
|
+
for ch in non_arg_children:
|
|
132
|
+
visit(ch)
|
|
133
|
+
|
|
134
|
+
visit(root)
|
|
135
|
+
return out
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""bevor link — link the current directory to a Bevor project."""
|
|
2
|
+
|
|
3
|
+
from bevor_sdk.types.responses.manage import ProjectSchema
|
|
4
|
+
|
|
5
|
+
from bevor_cli import output
|
|
6
|
+
from bevor_cli.client import BevorInternalBusinessClient
|
|
7
|
+
from bevor_cli.commands.link.helpers import pick_project
|
|
8
|
+
from bevor_cli.commands.workspaces.pick import pick_workspace
|
|
9
|
+
from bevor_cli.configuration.creds import Credentials
|
|
10
|
+
from bevor_cli.configuration.state import State
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_link() -> None:
|
|
14
|
+
"""Link the current directory to a Bevor workspace and project.
|
|
15
|
+
|
|
16
|
+
Resets/creates the .bevor/config.json.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
creds = Credentials.load()
|
|
20
|
+
creds.require_token()
|
|
21
|
+
|
|
22
|
+
state = State.load()
|
|
23
|
+
|
|
24
|
+
workspaces = BevorInternalBusinessClient(creds=creds).list_workspaces()
|
|
25
|
+
|
|
26
|
+
resolved_workspace = pick_workspace(workspaces.results)
|
|
27
|
+
|
|
28
|
+
state.set_workspace(workspace_id=resolved_workspace.id)
|
|
29
|
+
|
|
30
|
+
# incrementally write this.
|
|
31
|
+
state.save()
|
|
32
|
+
|
|
33
|
+
client = BevorInternalBusinessClient(creds=creds, workspace_id=resolved_workspace.id)
|
|
34
|
+
projects = client.list_projects()
|
|
35
|
+
|
|
36
|
+
resolved_project: ProjectSchema | None = None
|
|
37
|
+
if projects.results:
|
|
38
|
+
resolved_project = pick_project(projects.results)
|
|
39
|
+
state.set_project(project_id=resolved_project.id)
|
|
40
|
+
|
|
41
|
+
state.save()
|
|
42
|
+
|
|
43
|
+
output.success("Linked.")
|
|
44
|
+
output.info(f" Workspace : {resolved_workspace.id}")
|
|
45
|
+
if resolved_project:
|
|
46
|
+
output.info(f" Project : {resolved_project.id}")
|
|
47
|
+
else:
|
|
48
|
+
output.info(" Project : (not set)")
|
|
49
|
+
output.info("\nNext steps:")
|
|
50
|
+
output.info(" bevor codes push # upload current directory as a code version")
|
|
51
|
+
output.info(" bevor analyses create # run analysis on the uploaded code")
|
|
52
|
+
output.info(" bevor codes nodes list # explore the code graph")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Helpers for ``bevor link``."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Literal
|
|
4
|
+
|
|
5
|
+
import questionary
|
|
6
|
+
import typer
|
|
7
|
+
from bevor_sdk.types.responses.manage import ProjectSchema
|
|
8
|
+
|
|
9
|
+
from bevor_cli.commands.projects.create import create_project
|
|
10
|
+
from bevor_cli.select import _select
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def pick_project(projects: List[ProjectSchema]) -> ProjectSchema:
|
|
14
|
+
"""Interactively choose an existing project or create a new one."""
|
|
15
|
+
|
|
16
|
+
choices = [questionary.Choice(title=f"{p.name} {p.id}", value=p) for p in projects]
|
|
17
|
+
choices.append(questionary.Choice(title="+ Create a new project", value="__new__"))
|
|
18
|
+
|
|
19
|
+
chosen: ProjectSchema | None | Literal["__new__"] = _select(
|
|
20
|
+
"Select a project (or create new):", choices
|
|
21
|
+
)
|
|
22
|
+
if chosen is None:
|
|
23
|
+
raise typer.Exit(0)
|
|
24
|
+
if chosen == "__new__":
|
|
25
|
+
return create_project(name=None, description=None)
|
|
26
|
+
return chosen
|
|
File without changes
|