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,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