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,105 @@
1
+ """``bevor analysis create`` — create a new root analysis."""
2
+
3
+ from pathlib import Path
4
+ from typing import Literal, Optional
5
+
6
+ import typer
7
+ from bevor_sdk.helpers.source_fingerprint import zip_source_tree
8
+ from bevor_sdk.types import ID
9
+ from bevor_sdk.types.requests.security.analyses import CreateAnalysisRootBodyDict
10
+ from bevor_sdk.types.responses.security import AnalysisCollisionResult, AnalysisCreatedResult
11
+ from rich.prompt import Confirm
12
+ from rich.table import Table
13
+
14
+ from bevor_cli import output
15
+ from bevor_cli.annotations.confirmations import FORCE, JSON_OUT, YES
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
+
21
+
22
+ def apply_create_analysis(
23
+ *,
24
+ name: str | None = None,
25
+ collision: Literal["surface", "force", "merge"] | None = None,
26
+ force: bool = False,
27
+ json_out: bool = False,
28
+ yes: bool = False,
29
+ ) -> None:
30
+ """Create a root analysis for the configured code version. Bevor will check whether an analysis
31
+ exists for the given code version or not, and early return if so. Use the --collision argument to bypass this,
32
+ but it's advised not to."""
33
+ creds = Credentials.load()
34
+ creds.require_token()
35
+
36
+ state = State.load()
37
+ workspace_id = state.validate_workspace()
38
+ code_id = state.validate_code()
39
+
40
+ if not force:
41
+ _, _, content_hash = zip_source_tree(Path.cwd())
42
+ if content_hash != state.code_fingerprint:
43
+ output.error(
44
+ "Working directory does not match the configured code fingerprint "
45
+ "(run `bevor codes push` after your latest changes).",
46
+ hint="Pass `--force` only if you mean to create without syncing.",
47
+ )
48
+ raise typer.Exit(1)
49
+
50
+ if not yes:
51
+ if not Confirm.ask(f"Create new analysis using code {code_id}?"):
52
+ raise typer.Exit(0)
53
+
54
+ client = BevorInternalClient(
55
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
56
+ )
57
+ created = client.analyses.create_root(
58
+ CreateAnalysisRootBodyDict(code_id=ID(code_id), name=name, on_collision=collision)
59
+ )
60
+
61
+ if isinstance(created, AnalysisCreatedResult):
62
+ if json_out:
63
+ console.print_json(created.analysis.model_dump_json(indent=2))
64
+ else:
65
+ output.success(
66
+ f"Analysis created: {created.analysis.id}. "
67
+ "`bevor analyses run` or `bevor findings add` next."
68
+ )
69
+
70
+ state.set_analysis(created.analysis)
71
+ state.save()
72
+ elif isinstance(created, AnalysisCollisionResult):
73
+ if json_out:
74
+ console.print_json(created.model_dump_json(indent=2))
75
+ else:
76
+ output.warning(
77
+ f"{len(created.candidates)} analysis versions already exist for this code version. They're listed out with the proposed actions. "
78
+ "if you want to create the analysis anyways, use the --collision=force flag."
79
+ )
80
+ table = Table("ID", "Name", "Created", "User", "Action")
81
+ for candidate in created.candidates:
82
+ args = [
83
+ candidate.id,
84
+ candidate.analysis_meta.name,
85
+ str(candidate.created_at.date()),
86
+ candidate.analysis_meta.user.username,
87
+ candidate.on_collision,
88
+ ]
89
+ table.add_row(*args)
90
+ console.print(table)
91
+
92
+
93
+ def create_analysis(
94
+ name: Optional[str] = typer.Option(None, "--name", help="Optional analysis name."),
95
+ collision: Optional[Literal["surface", "force", "merge"]] = typer.Option(
96
+ "surface",
97
+ "--collision",
98
+ help="What Bevor should do if analysis already exists for the given code",
99
+ ),
100
+ yes: YES = False,
101
+ json_out: JSON_OUT = False,
102
+ force: FORCE = False,
103
+ ) -> None:
104
+ """Create a root analysis for the configured code version."""
105
+ apply_create_analysis(name=name, collision=collision, force=force, yes=yes, json_out=json_out)
@@ -0,0 +1,65 @@
1
+ """``bevor analysis delete {full|branch|version}``."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from bevor_sdk.types import ID
7
+
8
+ from bevor_cli import output
9
+ from bevor_cli.client import BevorInternalClient
10
+ from bevor_cli.configuration.creds import Credentials
11
+ from bevor_cli.configuration.state import State
12
+
13
+
14
+ def delete_analysis_full() -> None:
15
+ """Delete the entire currently configured analysis container"""
16
+ creds = Credentials.load()
17
+ creds.require_token()
18
+
19
+ state = State.load()
20
+ workspace_id = state.validate_workspace()
21
+ analysis_id = state.validate_analysis()
22
+
23
+ client = BevorInternalClient(
24
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
25
+ )
26
+ client.analyses.delete(analysis_id)
27
+
28
+ state.clear_analysis()
29
+ state.save()
30
+
31
+ output.success(f"Deleted analysis {analysis_id}. Cleared analysis from `.bevor`.")
32
+
33
+
34
+ def delete_analysis_version(
35
+ version: Optional[str] = typer.Argument(
36
+ None,
37
+ help="Version id to delete (default: configured analysis head).",
38
+ ),
39
+ ) -> None:
40
+ """Delete a single analysis version (leaf)."""
41
+ creds = Credentials.load()
42
+ creds.require_token()
43
+
44
+ state = State.load()
45
+ workspace_id = state.validate_workspace()
46
+
47
+ client = BevorInternalClient(
48
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
49
+ )
50
+
51
+ if not version:
52
+ analysis_id = state.validate_analysis()
53
+ analysis = client.analyses.get(analysis_id)
54
+ id_to_delete = analysis.version.id
55
+ else:
56
+ id_to_delete = ID(version)
57
+
58
+ result = client.analysis_versions.delete(id_to_delete)
59
+
60
+ # HEAD may have moved back to parent — refresh config if still same container.
61
+ if state.analysis_id == result.id:
62
+ state.set_analysis(result)
63
+ state.save()
64
+
65
+ output.success(f"Deleted version {id_to_delete}. Updated HEAD to {result.version.id}")
@@ -0,0 +1,87 @@
1
+ """``bevor analysis diff``."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from bevor_sdk.types import ID
7
+ from bevor_sdk.types.requests.security import VersionQueryParams
8
+ from bevor_sdk.types.responses.security import FindingBareSchema, FindingTransitionSchema
9
+ from rich.table import Table
10
+ from rich.text import Text
11
+
12
+ from bevor_cli.annotations.confirmations import JSON_OUT
13
+ from bevor_cli.client import BevorInternalClient
14
+ from bevor_cli.commands.findings.get import LEVEL_COLORS
15
+ from bevor_cli.configuration.creds import Credentials
16
+ from bevor_cli.configuration.state import State
17
+ from bevor_cli.output import console
18
+
19
+
20
+ def _snapshot(item: FindingTransitionSchema) -> FindingBareSchema:
21
+ to_render = item.after_finding or item.before_finding
22
+ assert to_render
23
+ return to_render
24
+
25
+
26
+ def diff_analysis(
27
+ version: Optional[str] = typer.Option(
28
+ None,
29
+ "--version",
30
+ help="Version id to diff (default: configured analysis head).",
31
+ ),
32
+ staged: bool = typer.Option(
33
+ False,
34
+ "--staged",
35
+ help="Show staged drafts instead of version-vs-parent diffs.",
36
+ ),
37
+ json_out: JSON_OUT = False,
38
+ ) -> None:
39
+ """Show finding diffs for the configured analysis head (or ``--version``). --staged flag will show
40
+ the diff of the staged commits. Excluding it, it'll show the diff of committed findings between the analysis
41
+ and its parent."""
42
+ creds = Credentials.load()
43
+ creds.require_token()
44
+
45
+ state = State.load()
46
+ workspace_id = state.validate_workspace()
47
+
48
+ client = BevorInternalClient(
49
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
50
+ )
51
+
52
+ opts = VersionQueryParams()
53
+ if version:
54
+ v_ = client.analysis_versions.get(ID(version))
55
+ analysis_id = v_.analysis_meta.id
56
+ opts["version_id"] = v_.id
57
+ else:
58
+ analysis_id = state.validate_analysis()
59
+
60
+ analysis_source = client.analyses(analysis_id)
61
+
62
+ if staged:
63
+ page = analysis_source.diff_staged(opts)
64
+ else:
65
+ page = analysis_source.diff_version(opts)
66
+
67
+ if json_out:
68
+ console.print_json(page.model_dump_json(indent=2))
69
+ return
70
+
71
+ if not page.results:
72
+ console.print("No diffs.")
73
+ return
74
+
75
+ table = Table("Reason", "Type", "Level", "Name", "Changed")
76
+ for item in page.results:
77
+ snapshot = _snapshot(item)
78
+
79
+ table.add_row(
80
+ item.reason,
81
+ snapshot.type,
82
+ Text(snapshot.level, style=LEVEL_COLORS[snapshot.level]),
83
+ snapshot.name,
84
+ ", ".join(item.changed_fields),
85
+ )
86
+
87
+ console.print(table)
@@ -0,0 +1,56 @@
1
+ """``bevor analysis entrypoints``."""
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.security import VersionQueryParams
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 list_entrypoints(
18
+ version: Optional[str] = typer.Option(
19
+ None,
20
+ "--version",
21
+ help="Version id (default: configured analysis head).",
22
+ ),
23
+ json_out: JSON_OUT = False,
24
+ ) -> None:
25
+ """List entrypoints for the configured analysis head (or ``--version``)."""
26
+ creds = Credentials.load()
27
+ creds.require_token()
28
+
29
+ state = State.load()
30
+ workspace_id = state.validate_workspace()
31
+
32
+ client = BevorInternalClient(
33
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
34
+ )
35
+
36
+ opts = VersionQueryParams()
37
+ if version:
38
+ v_ = client.analysis_versions.get(ID(version))
39
+ analysis_id = v_.analysis_meta.id
40
+ version_id = v_.id
41
+ opts["version_id"] = version_id
42
+ else:
43
+ analysis_id = state.validate_analysis()
44
+
45
+ page = client.analyses.entrypoints(analysis_id, opts)
46
+
47
+ if json_out:
48
+ console.print_json(json.dumps(page.model_dump(mode="json"), indent=2))
49
+ return
50
+
51
+ if not page.results:
52
+ console.print("No scopes.")
53
+ return
54
+
55
+ for s in page.results:
56
+ console.print(f"{s.node.short_id} {s.node.name} ({s.node.path}) [{s.pipeline_status}]")
@@ -0,0 +1,129 @@
1
+ """``bevor finding list``."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+ import typer
8
+ from bevor_sdk import Graph
9
+ from bevor_sdk.helpers.converters.sarif import bevor_to_sarif_sync
10
+ from bevor_sdk.types import ID
11
+ from bevor_sdk.types.literals import FindingDraftOperation, FindingLevel, FindingType
12
+ from bevor_sdk.types.requests.security import VersionQueryParams
13
+ from bevor_sdk.types.responses.security import FindingSchema
14
+
15
+ from bevor_cli import output
16
+ from bevor_cli.annotations.confirmations import JSON_OUT
17
+ from bevor_cli.client import BevorInternalClient
18
+ from bevor_cli.commands.findings.interactive import (
19
+ browse_findings_with_actions,
20
+ print_findings_table_with_ops,
21
+ )
22
+ from bevor_cli.configuration.creds import Credentials
23
+ from bevor_cli.configuration.state import State
24
+ from bevor_cli.output import console
25
+
26
+
27
+ def _on_node(finding: FindingSchema, node_id: Optional[str]) -> bool:
28
+ if not node_id:
29
+ return True
30
+ if str(finding.node_id) == node_id:
31
+ return True
32
+ return finding.location_id is not None and str(finding.location_id) == node_id
33
+
34
+
35
+ def _matches(
36
+ finding: FindingSchema,
37
+ *,
38
+ finding_type: Optional[FindingType],
39
+ level: Optional[FindingLevel],
40
+ is_acknowledged: Optional[bool],
41
+ operation: Optional[FindingDraftOperation],
42
+ created_by: Optional[str],
43
+ node_id: Optional[str],
44
+ ) -> bool:
45
+ """Filters the version findings endpoint does not support server-side."""
46
+ if finding_type and finding.type != finding_type:
47
+ return False
48
+ if level and finding.level != level:
49
+ return False
50
+ if is_acknowledged is not None and finding.is_acknowledged is not is_acknowledged:
51
+ return False
52
+ if operation and finding.operation != operation:
53
+ return False
54
+ if created_by and str(finding.created_by_user.id) != created_by:
55
+ return False
56
+ return _on_node(finding, node_id)
57
+
58
+
59
+ def list_analysis_findings(
60
+ version: Optional[str] = typer.Option(
61
+ None,
62
+ "--version",
63
+ help="Version id to list findings for (default: configured analysis head).",
64
+ ),
65
+ interactive: bool = typer.Option(
66
+ False, "--interactive", help="Browse findings and expand the focused row."
67
+ ),
68
+ json_out: JSON_OUT = False,
69
+ sarif: Optional[Path] = typer.Option(
70
+ None, "--sarif", help="Write SARIF for the configured analysis to this path."
71
+ ),
72
+ ) -> None:
73
+ """List findings on the configured analysis, or ``--version``. Including a --sarif path will output
74
+ the findings into a SARIF format. --interactive will open an interactive terminal to explore further."""
75
+ creds = Credentials.load()
76
+ creds.require_token()
77
+
78
+ state = State.load()
79
+ workspace_id = state.validate_workspace()
80
+
81
+ if sarif and not sarif.parent.exists():
82
+ output.error(
83
+ f"Directory does not exist: {sarif.parent}",
84
+ hint="Create it first, or pass a different --sarif path.",
85
+ )
86
+ raise typer.Exit(1)
87
+
88
+ client = BevorInternalClient(
89
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
90
+ )
91
+
92
+ opts = VersionQueryParams()
93
+ if version:
94
+ v_ = client.analysis_versions.get(ID(version))
95
+ analysis_id = v_.analysis_meta.id
96
+ version_id = v_.id
97
+ opts["version_id"] = version_id
98
+ else:
99
+ analysis_id = state.validate_analysis()
100
+
101
+ result = client.analyses.list_findings(analysis_id, options=opts)
102
+
103
+ if json_out:
104
+ console.print_json(json.dumps([f.model_dump(mode="json") for f in result.results], indent=2))
105
+ return
106
+
107
+ if sarif:
108
+ analysis = client.analyses.get(analysis_id)
109
+ code_id = analysis.version.code_version.id
110
+
111
+ graph = Graph(client=client, code_id=code_id)
112
+ graph.build()
113
+
114
+ sarif_schema = bevor_to_sarif_sync(graph=graph, findings=result.results)
115
+ sarif.write_text(sarif_schema.model_dump_json(indent=2, exclude_none=True))
116
+ output.success(f"Wrote sarif outputs to {sarif.name}")
117
+ return
118
+
119
+ if not result.results:
120
+ console.print("No findings.")
121
+ return
122
+
123
+ def refetch() -> List[FindingSchema]:
124
+ return client.analyses.list_findings(analysis_id, options=opts).results
125
+
126
+ if interactive:
127
+ browse_findings_with_actions(client=client, refetch=refetch, rows=result.results)
128
+ else:
129
+ print_findings_table_with_ops(result.results)
@@ -0,0 +1,47 @@
1
+ """``bevor analysis fork``."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from bevor_sdk.types import ID
7
+ from bevor_sdk.types.requests.security import VersionBodyDict
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
+
14
+
15
+ def fork_analysis(
16
+ version: Optional[str] = typer.Option(
17
+ None,
18
+ "--version",
19
+ help="Version id to fork (default: configured analysis head).",
20
+ ),
21
+ ) -> None:
22
+ """Fork an analysis version and set the new analysis as configured."""
23
+ creds = Credentials.load()
24
+ creds.require_token()
25
+
26
+ state = State.load()
27
+ workspace_id = state.validate_workspace()
28
+
29
+ client = BevorInternalClient(
30
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
31
+ )
32
+
33
+ body = VersionBodyDict()
34
+ if version:
35
+ v_ = client.analysis_versions.get(ID(version))
36
+ analysis_id = v_.analysis_meta.id
37
+ version_id = v_.id
38
+ body["version_id"] = version_id
39
+ else:
40
+ analysis_id = state.validate_analysis()
41
+
42
+ created = client.analyses.fork(analysis_id, body)
43
+
44
+ state.set_analysis(created)
45
+ state.save()
46
+
47
+ output.success(f"Forked to analysis {created.id}. Active analysis updated in `.bevor`.")
@@ -0,0 +1,85 @@
1
+ """``bevor analyses list``."""
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.security import AnalysesQueryParams
9
+ from rich.table import Table
10
+
11
+ from bevor_cli.annotations.confirmations import JSON_OUT
12
+ from bevor_cli.annotations.query import ORDER, ORDER_BY, PAGE, PAGE_SIZE
13
+ from bevor_cli.client import BevorInternalClient
14
+ from bevor_cli.configuration.creds import Credentials
15
+ from bevor_cli.configuration.settings import Settings
16
+ from bevor_cli.configuration.state import State
17
+ from bevor_cli.output import console, info
18
+
19
+
20
+ def list_analyses(
21
+ page: PAGE = None,
22
+ page_size: PAGE_SIZE = None,
23
+ order: ORDER = None,
24
+ order_by: ORDER_BY = None,
25
+ project: Optional[str] = typer.Option(
26
+ None,
27
+ "--project",
28
+ help="Project ID (defaults to .bevor config).",
29
+ ),
30
+ user: Optional[str] = typer.Option(None, "--user", help="Analysis creator to filter by"),
31
+ is_owner: Optional[bool] = typer.Option(None, "--is-owner", help="Analysis filter by owner only"),
32
+ json_out: JSON_OUT = False,
33
+ ) -> None:
34
+ """List analyses for the configured project, or use the --project flag to list for a specific project."""
35
+ creds = Credentials.load()
36
+ creds.require_token()
37
+
38
+ state = State.load()
39
+ settings = Settings.load()
40
+
41
+ workspace_id = state.validate_workspace()
42
+
43
+ client = BevorInternalClient(
44
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project(priority=project)
45
+ )
46
+
47
+ query_params = AnalysesQueryParams()
48
+ if page:
49
+ query_params["page"] = page
50
+ query_params["page_size"] = page_size or settings.default_page_size
51
+ if order:
52
+ query_params["order"] = order
53
+ if order_by:
54
+ query_params["order_by"] = order_by
55
+ if user:
56
+ query_params["user_id"] = ID(user)
57
+ if is_owner:
58
+ query_params["is_owner"] = is_owner
59
+
60
+ analyses = client.analyses.list(query_params)
61
+
62
+ if not analyses.results:
63
+ console.print("No analyses found.")
64
+ return
65
+
66
+ if json_out:
67
+ console.print_json(
68
+ json.dumps([a.model_dump(mode="json") for a in analyses.results], indent=2)
69
+ )
70
+ return
71
+
72
+ table = Table("ID", "Name", "Code", "Owner", "Created")
73
+ for a in analyses.results:
74
+ args = [a.id, a.name, a.version.code_version.id, a.user.username, str(a.created_at.date())]
75
+ if a.id == state.analysis_id:
76
+ args.append("●")
77
+
78
+ table.add_row(*args)
79
+
80
+ console.print(table)
81
+
82
+ if analyses.more:
83
+ info(
84
+ f"\nShowing {len(analyses.results)} of {analyses.total_observations} results. Use --page --page-size to navigate."
85
+ )
@@ -0,0 +1,46 @@
1
+ """``bevor analysis merge``."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from bevor_sdk.types import ID
7
+ from bevor_sdk.types.requests.security import MergeAnalysisBodyDict
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
+
14
+
15
+ def merge_analysis(
16
+ from_analysis: str = typer.Argument(..., help="Source analysis container id."),
17
+ to_analysis: Optional[str] = typer.Option(
18
+ None, "--to", help="Destination analysis container id."
19
+ ),
20
+ ) -> None:
21
+ """Merge one analysis into another. --from is required, but --to defaults to your active analysis's HEAD.
22
+ This will create a child analysis version of the --to, and update its HEAD."""
23
+ creds = Credentials.load()
24
+ creds.require_token()
25
+
26
+ state = State.load()
27
+ workspace_id = state.validate_workspace()
28
+ client = BevorInternalClient(
29
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
30
+ )
31
+
32
+ from_ = client.analyses.get(ID(from_analysis))
33
+
34
+ to_analysis_id = state.validate_analysis(priority=to_analysis)
35
+ to_ = client.analyses.get(to_analysis_id)
36
+
37
+ created = client.analyses.merge(
38
+ MergeAnalysisBodyDict(to_version_id=to_.version.id, from_version_id=from_.version.id)
39
+ )
40
+
41
+ state.set_analysis(created)
42
+ state.save()
43
+
44
+ output.success(
45
+ f"Merge complete → analysis {created.id} head {created.version.id}. Config updated."
46
+ )