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,77 @@
1
+ """``bevor analysis retry`` — retry a failed pipeline."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from bevor_sdk._sync.resources._modules import EventResource
7
+ from bevor_sdk.types import ID
8
+ from bevor_sdk.types.requests.security import VersionBodyDict
9
+
10
+ from bevor_cli import output
11
+ from bevor_cli.annotations.confirmations import NO_WAIT, SILENT
12
+ from bevor_cli.client import BevorInternalClient
13
+ from bevor_cli.commands.analyses.run import render_status
14
+ from bevor_cli.configuration.creds import Credentials
15
+ from bevor_cli.configuration.state import State
16
+
17
+
18
+ def retry_analysis(
19
+ version: Optional[str] = typer.Option(
20
+ None,
21
+ "--version",
22
+ help="Version id to retry (default: configured analysis head).",
23
+ ),
24
+ no_wait: NO_WAIT = False,
25
+ silent: SILENT = False,
26
+ ) -> None:
27
+ """Retry failed segments of a Bevor Security pipeline run for the configured analysis head (or ``--version``). See `bevor analyses run`."""
28
+ creds = Credentials.load()
29
+ creds.require_token()
30
+
31
+ state = State.load()
32
+ workspace_id = state.validate_workspace()
33
+
34
+ client = BevorInternalClient(
35
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
36
+ )
37
+
38
+ wait = not no_wait
39
+
40
+ opts: VersionBodyDict = {}
41
+ if version:
42
+ v_ = client.analysis_versions.get(ID(version))
43
+ analysis_id = v_.analysis_meta.id
44
+ version_id = v_.id
45
+ opts["version_id"] = version_id
46
+ else:
47
+ analysis_id = state.validate_analysis()
48
+
49
+ run_result = client.analyses.retry(analysis_id, opts, with_streaming=wait)
50
+
51
+ if wait and isinstance(run_result, EventResource):
52
+ ep_map = {}
53
+ eps = client.analyses.entrypoints(analysis_id, opts)
54
+ for ep in eps.results:
55
+ ep_map[ep.node.id] = (ep.node.short_id, ep.node.name, ep.pipeline_status)
56
+
57
+ if not silent:
58
+ with output.spinner("Analysis running…"):
59
+ for event in run_result.stream_completion():
60
+ if event.id in ep_map:
61
+ short_id, name, _ = ep_map[event.id]
62
+ ep_map[event.id] = (short_id, name, event.status)
63
+ render_status(ep_map)
64
+ else:
65
+ for event in run_result.stream_completion():
66
+ if event.id in ep_map:
67
+ short_id, name, _ = ep_map[event.id]
68
+ ep_map[event.id] = (short_id, name, event.status)
69
+
70
+ n_failures = sum(1 if status == "failed" else 0 for _, _, status in ep_map.values())
71
+ if n_failures > 0:
72
+ output.success(f"Analysis finished, {n_failures} failed.")
73
+ else:
74
+ output.success("Analysis finished")
75
+
76
+ else:
77
+ output.success("Retry queued.")
@@ -0,0 +1,87 @@
1
+ """``bevor analysis run`` — trigger analysis execution."""
2
+
3
+ from typing import Mapping, Optional, Tuple
4
+
5
+ import typer
6
+ from bevor_sdk._sync.resources._modules import EventResource
7
+ from bevor_sdk.types import ID
8
+ from bevor_sdk.types.literals import AnalysisRunStatus
9
+ from bevor_sdk.types.requests.security import RunAnalysisBodyDict
10
+ from rich.live import Live
11
+ from rich.table import Table
12
+
13
+ from bevor_cli import output
14
+ from bevor_cli.annotations.confirmations import NO_WAIT, SILENT
15
+ from bevor_cli.client import BevorInternalClient
16
+ from bevor_cli.configuration.creds import Credentials
17
+ from bevor_cli.configuration.state import State
18
+
19
+
20
+ def render_status(ep_map: Mapping[ID, Tuple[str, str, AnalysisRunStatus]]) -> Table:
21
+ table = Table.grid(padding=(0, 1))
22
+ for k in sorted(ep_map.keys()):
23
+ short_id, name, status = ep_map[k]
24
+ table.add_row(f"Entrypoint: {name} ({short_id})", f"Status: {status}")
25
+ return table
26
+
27
+
28
+ def run_analysis(
29
+ version: Optional[str] = typer.Option(
30
+ None,
31
+ "--version",
32
+ help="Version id to run (default: configured analysis head).",
33
+ ),
34
+ no_wait: NO_WAIT = False,
35
+ silent: SILENT = False,
36
+ ) -> None:
37
+ """Start a run of Bevor Security pipeline for the configured analysis head (or ``--version``)."""
38
+ creds = Credentials.load()
39
+ creds.require_token()
40
+
41
+ state = State.load()
42
+ workspace_id = state.validate_workspace()
43
+
44
+ wait = not no_wait
45
+ client = BevorInternalClient(
46
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
47
+ )
48
+
49
+ opts: RunAnalysisBodyDict = {}
50
+ if version:
51
+ v_ = client.analysis_versions.get(ID(version))
52
+ analysis_id = v_.analysis_meta.id
53
+ version_id = v_.id
54
+ opts["version_id"] = version_id
55
+ else:
56
+ analysis_id = state.validate_analysis()
57
+
58
+ run_result = client.analyses.run(analysis_id, opts, with_streaming=wait)
59
+
60
+ if wait and isinstance(run_result, EventResource):
61
+ ep_map = {}
62
+ eps = client.analyses.entrypoints(analysis_id, opts)
63
+ for ep in eps.results:
64
+ if ep.pipeline_status != "not_run" and ep.is_analyzed_in_current:
65
+ ep_map[ep.node.id] = (ep.node.short_id, ep.node.name, ep.pipeline_status)
66
+
67
+ if not silent:
68
+ with Live(render_status(ep_map), console=output.console, refresh_per_second=8) as live:
69
+ for event in run_result.stream_completion():
70
+ if event.id in ep_map:
71
+ short_id, name, _ = ep_map[event.id]
72
+ ep_map[event.id] = (short_id, name, event.status)
73
+ live.update(render_status(ep_map))
74
+ else:
75
+ for event in run_result.stream_completion():
76
+ if event.id in ep_map:
77
+ short_id, name, _ = ep_map[event.id]
78
+ ep_map[event.id] = (short_id, name, event.status)
79
+
80
+ n_failures = sum(1 if status == "failed" else 0 for _, _, status in ep_map.values())
81
+ if n_failures > 0:
82
+ output.success(f"Analysis finished, {n_failures} failed.")
83
+ else:
84
+ output.success("Analysis finished")
85
+
86
+ else:
87
+ output.success("Run queued")
@@ -0,0 +1,85 @@
1
+ """``bevor analyses select`` — set the selected analysis."""
2
+
3
+ from typing import Optional
4
+
5
+ import questionary
6
+ import typer
7
+ from bevor_sdk.exceptions import NotFoundError
8
+ from bevor_sdk.types import ID
9
+ from bevor_sdk.types.requests.security import AnalysesQueryParams
10
+ from bevor_sdk.types.responses.security import AnalysisSchema
11
+
12
+ from bevor_cli import output
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.select import _CANCEL, _select
18
+
19
+
20
+ def select_analysis(
21
+ analysis_id: Optional[str] = typer.Argument(
22
+ None, help="Analysis id to select. Prompts with a list when omitted."
23
+ ),
24
+ page: Optional[int] = typer.Option(None, "--page", help="Page number to query"),
25
+ page_size: Optional[int] = typer.Option(None, "--page-size", help="Max analyses to fetch."),
26
+ ) -> None:
27
+ """Select an analysis in the selected project by id, or pick one from a list."""
28
+
29
+ creds = Credentials.load()
30
+ creds.require_token()
31
+
32
+ state = State.load()
33
+ workspace_id = state.validate_workspace()
34
+
35
+ client = BevorInternalClient(
36
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
37
+ )
38
+
39
+ if analysis_id:
40
+ try:
41
+ analysis_ = client.analyses.get(ID(analysis_id))
42
+ except NotFoundError:
43
+ output.error("analysis not found in the configured project")
44
+ raise typer.Exit(1)
45
+
46
+ if state.code_id != analysis_.version.code_version.id:
47
+ output.info("Updating code configuration")
48
+
49
+ state.set_analysis(analysis_)
50
+ state.save()
51
+ return
52
+
53
+ settings = Settings.load()
54
+
55
+ analyses = client.analyses.list(
56
+ AnalysesQueryParams(page_size=page_size or settings.default_page_size, page=page)
57
+ )
58
+
59
+ if not analyses.results:
60
+ output.error("No analyses found.", hint="Run `bevor analyses create` to create one.")
61
+ raise typer.Exit(1)
62
+
63
+ choices = [
64
+ questionary.Choice(
65
+ title=(
66
+ f"{a.name} · {a.version.code_version.id} "
67
+ f"{a.user.username} "
68
+ f"{a.id} {a.created_at.date()}"
69
+ ),
70
+ value=a,
71
+ )
72
+ for a in analyses.results
73
+ ] + [questionary.Choice(title="↵ Cancel", value=_CANCEL)]
74
+
75
+ analysis: AnalysisSchema | None = _select("Select an analysis:", choices)
76
+ if analysis is None:
77
+ raise typer.Exit(0)
78
+
79
+ if analysis.version.code_version.id != state.code_id:
80
+ output.info("Updating code configuration")
81
+
82
+ state.set_analysis(analysis)
83
+
84
+ output.info("Updated analysis configuration")
85
+ state.save()
@@ -0,0 +1,45 @@
1
+ """``bevor analyses set-head`` — move HEAD to a version; keep analysis_id in config."""
2
+
3
+ import typer
4
+ from bevor_sdk.types import ID
5
+ from bevor_sdk.types.requests.security import SetHeadBodyDict
6
+
7
+ from bevor_cli import output
8
+ from bevor_cli.client import BevorInternalClient
9
+ from bevor_cli.configuration.creds import Credentials
10
+ from bevor_cli.configuration.state import State
11
+
12
+
13
+ def set_analysis_head(
14
+ version_id: str = typer.Argument(..., help="Analysis version id to set as HEAD."),
15
+ ) -> None:
16
+ """Move the analysis HEAD to ``version_id``. This will change which analysis versions you act on."""
17
+ creds = Credentials.load()
18
+ creds.require_token()
19
+
20
+ state = State.load()
21
+ workspace_id = state.validate_workspace()
22
+ analysis_id = state.validate_analysis()
23
+
24
+ client = BevorInternalClient(
25
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
26
+ )
27
+
28
+ analysis_source = client.analyses(analysis_id)
29
+
30
+ analysis = analysis_source.get()
31
+ version = client.analysis_versions.get(ID(version_id))
32
+
33
+ if version.analysis_meta.id != analysis.id:
34
+ output.error(
35
+ f"Version {version_id} belongs to analysis {version.analysis_meta.id}, "
36
+ f"not the configured analysis {analysis_id}.",
37
+ hint="Run `bevor analyses select <analysis-id>` first, or pass a version of the configured analysis.",
38
+ )
39
+ raise typer.Exit(1)
40
+
41
+ result = analysis_source.set_head(SetHeadBodyDict(version_id=ID(version_id)))
42
+ state.set_analysis(result)
43
+ state.save()
44
+
45
+ output.success(f"HEAD set to {result.version.id}. Analysis {result.id} retained in config.")
@@ -0,0 +1,90 @@
1
+ """``bevor analysis transitions``."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from bevor_sdk.types import ID
7
+ from bevor_sdk.types.literals import FindingTransitionReason
8
+ from bevor_sdk.types.requests.security import TransitionQueryParams
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.annotations.query import ORDER, ORDER_BY, PAGE, PAGE_SIZE
14
+ from bevor_cli.client import BevorInternalClient
15
+ from bevor_cli.commands.analyses.diff import _snapshot
16
+ from bevor_cli.commands.findings.get import LEVEL_COLORS
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_transitions(
24
+ page: PAGE = None,
25
+ page_size: PAGE_SIZE = None,
26
+ order: ORDER = None,
27
+ order_by: ORDER_BY = None,
28
+ reason: Optional[FindingTransitionReason] = typer.Option(
29
+ None, "--reason", help="Only show transitions with this reason (e.g. remediated)."
30
+ ),
31
+ version: Optional[str] = typer.Option(
32
+ None,
33
+ "--version",
34
+ help="Version id (default: configured analysis head).",
35
+ ),
36
+ project_wide: bool = typer.Option(
37
+ False, "--project-wide", help="List transitions across the whole project instead."
38
+ ),
39
+ json_out: JSON_OUT = False,
40
+ ) -> None:
41
+ """List finding transitions into the configured analysis head (or ``--version``), or across the whole project
42
+ with ``--project-wide``. Filter with ``--reason``, e.g. ``--reason remediated``."""
43
+ creds = Credentials.load()
44
+ creds.require_token()
45
+
46
+ state = State.load()
47
+ settings = Settings.load()
48
+ workspace_id = state.validate_workspace()
49
+
50
+ client = BevorInternalClient(
51
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
52
+ )
53
+
54
+ query_params = TransitionQueryParams(
55
+ page=page,
56
+ page_size=page_size or settings.default_page_size,
57
+ order=order,
58
+ order_by=order_by,
59
+ reason=reason,
60
+ )
61
+
62
+ if version:
63
+ query_params["after_version_id"] = ID(version)
64
+ elif not project_wide:
65
+ analysis = client.analyses.get(state.validate_analysis())
66
+ query_params["after_version_id"] = analysis.version.id
67
+
68
+ result = client.transitions.list(options=query_params)
69
+
70
+ if json_out:
71
+ console.print_json(result.model_dump_json(indent=2))
72
+ return
73
+
74
+ if not result.results:
75
+ console.print("No transitions.")
76
+ return
77
+
78
+ table = Table("Reason", "Type", "Level", "Name", "Version", "Created")
79
+ for item in result.results:
80
+ snapshot = _snapshot(item)
81
+ table.add_row(
82
+ item.reason,
83
+ snapshot.type,
84
+ Text(snapshot.level, style=LEVEL_COLORS[snapshot.level]),
85
+ snapshot.name,
86
+ str(item.after_version.id),
87
+ str(item.created_at.date()),
88
+ )
89
+
90
+ console.print(table)
@@ -0,0 +1,94 @@
1
+ """``bevor analyses view`` — show an analysis (the selected one by default) or a specific version."""
2
+
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from bevor_sdk.types import ID
7
+ from rich.table import Table
8
+
9
+ from bevor_cli.annotations.confirmations import JSON_OUT
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 view_analysis(
17
+ analysis_id: Optional[str] = typer.Argument(
18
+ None, help="Analysis id. Defaults to the selected analysis."
19
+ ),
20
+ version: Optional[str] = typer.Option(
21
+ None,
22
+ "--version",
23
+ help="Act on this analysis version id instead of the configured analysis head.",
24
+ ),
25
+ json_out: JSON_OUT = False,
26
+ ) -> None:
27
+ """Print details for an analysis head (the selected analysis by default), or a specific ``--version``."""
28
+ creds = Credentials.load()
29
+ creds.require_token()
30
+
31
+ state = State.load()
32
+ workspace_id = state.validate_workspace()
33
+
34
+ client = BevorInternalClient(
35
+ creds=creds, workspace_id=workspace_id, project_id=state.validate_project()
36
+ )
37
+
38
+ if version is not None:
39
+ version_ = client.analysis_versions.get(ID(version))
40
+ analysis_meta_ = version_.analysis_meta
41
+ else:
42
+ analysis = client.analyses.get(state.validate_analysis(analysis_id))
43
+ version_ = analysis.version
44
+ analysis_meta_ = analysis
45
+
46
+ if json_out:
47
+ console.print_json(version_.model_dump_json(indent=2))
48
+ return
49
+
50
+ in_sync = state.code_id is not None and str(state.code_id) == str(version_.code_version.id)
51
+
52
+ table = Table(show_header=False, box=None, padding=(0, 1))
53
+ table.add_column(style="bold")
54
+ table.add_column()
55
+
56
+ table.add_row("Analysis ID", str(analysis_meta_.id))
57
+ table.add_row("Version ID", str(version_.id))
58
+ table.add_row("Name", analysis_meta_.name)
59
+ table.add_row("Trigger", str(version_.trigger))
60
+ table.add_row("Owner", str(analysis_meta_.is_owner))
61
+ table.add_row("Findings", str(version_.n_findings))
62
+ table.add_row(
63
+ "Code version",
64
+ version_.code_version.id,
65
+ )
66
+ table.add_row("Is head", str(version_.is_head))
67
+ table.add_row("Created at", str(version_.created_at.date()))
68
+ table.add_row("Created by", analysis_meta_.user.username)
69
+
70
+ if version_.merged_from_version:
71
+ table.add_row(
72
+ "Merged from",
73
+ f"{version_.merged_from_version.id}",
74
+ )
75
+ if version_.parent_version:
76
+ table.add_row(
77
+ "Parent version",
78
+ f"{version_.parent_version.id}",
79
+ )
80
+ if version_.forked_from_version:
81
+ table.add_row(
82
+ "Forked from",
83
+ f"{version_.forked_from_version.id}",
84
+ )
85
+ if version_.child_versions:
86
+ table.add_row("N Children", str(len(version_.child_versions)))
87
+ if version_.forked_versions:
88
+ table.add_row("N Forks", str(len(version_.forked_versions)))
89
+ if version_.merged_versions:
90
+ table.add_row("N Merges From", str(len(version_.merged_versions)))
91
+
92
+ table.add_row("In sync w/ .bevor code", str(in_sync))
93
+
94
+ console.print(table)
@@ -0,0 +1,7 @@
1
+ """Top-level auth commands."""
2
+
3
+ from .login import login
4
+ from .logout import logout
5
+ from .whoami import whoami
6
+
7
+ __all__ = ["login", "logout", "whoami"]
@@ -0,0 +1,135 @@
1
+ """``bevor login``."""
2
+
3
+ import json
4
+ import platform
5
+ import secrets
6
+ import socket
7
+ import threading
8
+ from queue import Empty, Queue
9
+ from typing import List
10
+ from urllib.parse import urlencode
11
+
12
+ import httpx
13
+ import typer
14
+
15
+ from bevor_cli import output
16
+ from bevor_cli.client import BevorInternalBusinessClient
17
+ from bevor_cli.configuration.creds import Credentials
18
+ from bevor_cli.settings import API_URL, APP_URL
19
+
20
+ _LISTEN_TIMEOUT_S = 360.0
21
+
22
+
23
+ def _cli_client_ip() -> str:
24
+ try:
25
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
26
+ s.settimeout(0.2)
27
+ s.connect(("8.8.8.8", 80))
28
+ return s.getsockname()[0]
29
+ except OSError:
30
+ return "127.0.0.1"
31
+
32
+
33
+ def _sse_listen(token: str, result: Queue) -> None:
34
+ url = f"{API_URL.rstrip('/')}/v1/events/cli?token={token}"
35
+ try:
36
+ timeout = httpx.Timeout(_LISTEN_TIMEOUT_S + 30.0, connect=30.0)
37
+ with httpx.Client(timeout=timeout) as client:
38
+ with client.stream("GET", url) as response:
39
+ response.raise_for_status()
40
+ pending: List[str] = []
41
+ for line in response.iter_lines():
42
+ if line == "":
43
+ for row in pending:
44
+ if row.startswith("data: "):
45
+ payload = json.loads(row[6:])
46
+ t = payload.get("type")
47
+ if t == "authorized":
48
+ result.put(("ok", payload))
49
+ return
50
+ if t == "closed":
51
+ result.put(("closed", payload))
52
+ return
53
+ pending = []
54
+ continue
55
+ if line.startswith(":"):
56
+ continue
57
+ pending.append(line)
58
+ result.put(("closed", {"reason": "stream_ended"}))
59
+ except Exception as exc:
60
+ result.put(("error", exc))
61
+
62
+
63
+ def login() -> None:
64
+ """Log in via browser.
65
+
66
+ Opens your default browser to authenticate with Bevor. Your credentials
67
+ are stored locally and used for subsequent commands.
68
+ """
69
+
70
+ creds = Credentials.load()
71
+
72
+ if creds.is_authenticated:
73
+ output.info("Already authenticated.")
74
+ return
75
+
76
+ session_id = secrets.token_urlsafe(24)
77
+ auth_url = f"{APP_URL.rstrip('/')}/cli-auth?{urlencode({'session_id': session_id})}"
78
+
79
+ device_name = socket.gethostname() or "unknown"
80
+ os_name = platform.platform()
81
+ ip_address = _cli_client_ip()
82
+
83
+ client = BevorInternalBusinessClient(creds=creds)
84
+
85
+ exchange_data = client.post(
86
+ "/v1/cli-tokens/exchange",
87
+ json={
88
+ "session_id": session_id,
89
+ "ip_address": ip_address,
90
+ "device_name": device_name,
91
+ "os_name": os_name,
92
+ },
93
+ ).json()
94
+
95
+ token = exchange_data["token"]
96
+
97
+ result: Queue = Queue(maxsize=1)
98
+ thread = threading.Thread(
99
+ target=_sse_listen,
100
+ args=(token, result),
101
+ daemon=True,
102
+ )
103
+ thread.start()
104
+
105
+ output.console.print(
106
+ f"If the browser does not open automatically, visit [bold blue link {auth_url}]{auth_url}[/]!"
107
+ )
108
+ typer.launch(auth_url)
109
+
110
+ with output.console.status("Waiting for login (complete in the browser)...", spinner="dots"):
111
+ try:
112
+ kind, payload = result.get(timeout=_LISTEN_TIMEOUT_S)
113
+ except Empty:
114
+ output.error("Timed out waiting for login.")
115
+ raise typer.Exit(1)
116
+
117
+ if kind == "error":
118
+ output.error(f"SSE error: {payload}")
119
+ raise typer.Exit(1)
120
+ if kind == "closed":
121
+ reason = payload.get("reason", "")
122
+ msg = "Login cancelled or closed."
123
+ if reason:
124
+ msg += f" ({reason})"
125
+ output.error(msg)
126
+ raise typer.Exit(1)
127
+
128
+ raw = payload["token"]
129
+ bearer = raw if raw.startswith("cli_") else f"cli_{raw}"
130
+ creds = Credentials.save(token=bearer)
131
+
132
+ client = BevorInternalBusinessClient(creds=creds)
133
+ user = client.me()
134
+
135
+ output.success(f"Logged in as {user.email or user.username}")
@@ -0,0 +1,23 @@
1
+ """``bevor logout``."""
2
+
3
+ from bevor_cli import output
4
+ from bevor_cli.client import BevorInternalBusinessClient
5
+ from bevor_cli.configuration.creds import Credentials
6
+
7
+
8
+ def logout() -> None:
9
+ """Remove stored credentials."""
10
+ creds = Credentials.load()
11
+
12
+ if not creds.is_authenticated:
13
+ output.info("Not currently authenticated")
14
+ return
15
+
16
+ client = BevorInternalBusinessClient(creds=creds)
17
+
18
+ try:
19
+ client.delete("/v1/cli-tokens/revoke/current")
20
+ except Exception:
21
+ pass # still clear local credentials
22
+ creds.delete()
23
+ output.success("Logged out.")
@@ -0,0 +1,21 @@
1
+ """``bevor whoami``."""
2
+
3
+ from bevor_cli import output
4
+ from bevor_cli.annotations.confirmations import JSON_OUT
5
+ from bevor_cli.client import BevorInternalBusinessClient
6
+ from bevor_cli.configuration.creds import Credentials
7
+
8
+
9
+ def whoami(json_out: JSON_OUT = False) -> None:
10
+ """Show the currently authenticated user."""
11
+ creds = Credentials.load()
12
+ creds.require_token()
13
+
14
+ client = BevorInternalBusinessClient(creds=creds)
15
+
16
+ user = client.me()
17
+
18
+ if json_out:
19
+ output.console.print_json(user.model_dump_json(indent=2))
20
+ else:
21
+ output.console.print(user.email or user.username)
File without changes