briar-cli 1.1.1__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 (179) hide show
  1. briar/__init__.py +8 -0
  2. briar/__main__.py +11 -0
  3. briar/_registry.py +36 -0
  4. briar/agent/__init__.py +16 -0
  5. briar/agent/_llm.py +90 -0
  6. briar/agent/_llms/__init__.py +40 -0
  7. briar/agent/_llms/anthropic_llm.py +185 -0
  8. briar/agent/_llms/bedrock.py +154 -0
  9. briar/agent/_llms/gemini.py +152 -0
  10. briar/agent/_llms/openai_llm.py +129 -0
  11. briar/agent/runner.py +364 -0
  12. briar/agent/tools.py +355 -0
  13. briar/auth/__init__.py +42 -0
  14. briar/auth/_acquirer.py +121 -0
  15. briar/auth/_acquirers/__init__.py +62 -0
  16. briar/auth/_acquirers/aws_sso.py +185 -0
  17. briar/auth/_acquirers/aws_static.py +54 -0
  18. briar/auth/_acquirers/bitbucket.py +60 -0
  19. briar/auth/_acquirers/github_device.py +129 -0
  20. briar/auth/_acquirers/github_pat.py +44 -0
  21. briar/auth/_acquirers/infisical.py +80 -0
  22. briar/auth/_acquirers/jira_session.py +102 -0
  23. briar/auth/_acquirers/jira_token.py +61 -0
  24. briar/auth/_acquirers/linear.py +46 -0
  25. briar/auth/_prompt.py +155 -0
  26. briar/cli.py +149 -0
  27. briar/commands/__init__.py +48 -0
  28. briar/commands/agent.py +692 -0
  29. briar/commands/auth.py +281 -0
  30. briar/commands/base.py +46 -0
  31. briar/commands/context.py +143 -0
  32. briar/commands/dashboard.py +61 -0
  33. briar/commands/extract.py +79 -0
  34. briar/commands/iac.py +44 -0
  35. briar/commands/runbook.py +110 -0
  36. briar/commands/secrets.py +165 -0
  37. briar/commands/version.py +17 -0
  38. briar/credentials/__init__.py +48 -0
  39. briar/credentials/_bootstrap.py +93 -0
  40. briar/credentials/_bootstraps/__init__.py +80 -0
  41. briar/credentials/_bootstraps/infisical.py +115 -0
  42. briar/credentials/_store.py +63 -0
  43. briar/credentials/aws_secrets.py +102 -0
  44. briar/credentials/envfile.py +171 -0
  45. briar/credentials/infisical.py +183 -0
  46. briar/credentials/ssm.py +83 -0
  47. briar/credentials/vault.py +109 -0
  48. briar/dashboard/__init__.py +13 -0
  49. briar/dashboard/collectors.py +1354 -0
  50. briar/dashboard/server.py +154 -0
  51. briar/dashboard/templates/index.html +678 -0
  52. briar/decorators.py +48 -0
  53. briar/env_vars.py +92 -0
  54. briar/error_policy.py +273 -0
  55. briar/errors.py +48 -0
  56. briar/extract/__init__.py +52 -0
  57. briar/extract/_cloud.py +157 -0
  58. briar/extract/_clouds/__init__.py +39 -0
  59. briar/extract/_clouds/aws.py +190 -0
  60. briar/extract/_clouds/azure.py +152 -0
  61. briar/extract/_clouds/gcp.py +135 -0
  62. briar/extract/_gh.py +159 -0
  63. briar/extract/_provider.py +218 -0
  64. briar/extract/_providers/__init__.py +60 -0
  65. briar/extract/_providers/bitbucket.py +276 -0
  66. briar/extract/_providers/github.py +277 -0
  67. briar/extract/_tracker.py +124 -0
  68. briar/extract/_trackers/__init__.py +44 -0
  69. briar/extract/_trackers/_jira_auth.py +258 -0
  70. briar/extract/_trackers/bitbucket.py +131 -0
  71. briar/extract/_trackers/github_issues.py +139 -0
  72. briar/extract/_trackers/jira.py +191 -0
  73. briar/extract/_trackers/linear.py +172 -0
  74. briar/extract/_user_filter.py +150 -0
  75. briar/extract/active_tickets.py +71 -0
  76. briar/extract/active_work.py +87 -0
  77. briar/extract/aws_infra.py +79 -0
  78. briar/extract/aws_services/__init__.py +31 -0
  79. briar/extract/aws_services/base.py +25 -0
  80. briar/extract/aws_services/ecs.py +43 -0
  81. briar/extract/aws_services/lambda_.py +38 -0
  82. briar/extract/aws_services/logs.py +39 -0
  83. briar/extract/aws_services/rds.py +35 -0
  84. briar/extract/aws_services/sqs.py +25 -0
  85. briar/extract/base.py +290 -0
  86. briar/extract/code_hotspots.py +134 -0
  87. briar/extract/codebase_conventions.py +72 -0
  88. briar/extract/composer.py +85 -0
  89. briar/extract/github_deployments.py +106 -0
  90. briar/extract/language_detectors/__init__.py +24 -0
  91. briar/extract/language_detectors/base.py +29 -0
  92. briar/extract/language_detectors/go.py +26 -0
  93. briar/extract/language_detectors/node.py +42 -0
  94. briar/extract/language_detectors/python.py +41 -0
  95. briar/extract/pr_archaeology.py +131 -0
  96. briar/extract/pr_review_context.py +123 -0
  97. briar/extract/reviewer_profile.py +141 -0
  98. briar/extract/ticket_archaeology.py +115 -0
  99. briar/extract/ticket_context.py +95 -0
  100. briar/formatting/__init__.py +67 -0
  101. briar/formatting/base.py +25 -0
  102. briar/formatting/csv.py +34 -0
  103. briar/formatting/json.py +19 -0
  104. briar/formatting/quiet.py +29 -0
  105. briar/formatting/table.py +97 -0
  106. briar/formatting/yaml.py +35 -0
  107. briar/iac/__init__.py +18 -0
  108. briar/iac/config_file.py +114 -0
  109. briar/iac/models.py +232 -0
  110. briar/iac/reference_map.py +33 -0
  111. briar/iac/runbook/__init__.py +32 -0
  112. briar/iac/runbook/executor.py +365 -0
  113. briar/iac/runbook/models.py +156 -0
  114. briar/iac/runbook/scheduler.py +187 -0
  115. briar/iac/scaffold/__init__.py +25 -0
  116. briar/iac/scaffold/_composer.py +308 -0
  117. briar/iac/scaffold/_knowledge.py +119 -0
  118. briar/iac/scaffold/archetypes/__init__.py +26 -0
  119. briar/iac/scaffold/archetypes/base.py +85 -0
  120. briar/iac/scaffold/archetypes/engineer.py +64 -0
  121. briar/iac/scaffold/archetypes/pr_ci_fixer.py +100 -0
  122. briar/iac/scaffold/archetypes/pr_conflict_resolver.py +83 -0
  123. briar/iac/scaffold/archetypes/pr_fixer.py +62 -0
  124. briar/iac/scaffold/archetypes/triager.py +50 -0
  125. briar/iac/scaffold/base.py +19 -0
  126. briar/iac/scaffold/implementation.py +59 -0
  127. briar/iac/scaffold/pr_fixes.py +52 -0
  128. briar/iac/scaffold/rules/__init__.py +64 -0
  129. briar/iac/scaffold/rules/base.py +121 -0
  130. briar/iac/scaffold/rules/commit_as_human.md +22 -0
  131. briar/iac/scaffold/rules/minimum_correct_fix.md +20 -0
  132. briar/iac/scaffold/rules/no_force_push.md +19 -0
  133. briar/iac/scaffold/rules/no_new_pr_creation.md +16 -0
  134. briar/iac/scaffold/rules/no_workflow_file_edits.md +21 -0
  135. briar/iac/scaffold/rules/read_all_comments_first.md +20 -0
  136. briar/iac/scaffold/rules/skip_approved_green_prs.md +17 -0
  137. briar/iac/scaffold/shapes/__init__.py +38 -0
  138. briar/iac/scaffold/shapes/base.py +17 -0
  139. briar/iac/scaffold/shapes/one_shot.py +48 -0
  140. briar/iac/scaffold/shapes/plan_approve_act.py +134 -0
  141. briar/iac/scaffold/shapes/triage.py +49 -0
  142. briar/iac/scaffold/sources/__init__.py +26 -0
  143. briar/iac/scaffold/sources/aws.py +69 -0
  144. briar/iac/scaffold/sources/base.py +61 -0
  145. briar/iac/scaffold/sources/bitbucket.py +164 -0
  146. briar/iac/scaffold/sources/github.py +155 -0
  147. briar/iac/scaffold/sources/jira.py +147 -0
  148. briar/iac/scaffold/triggers/__init__.py +23 -0
  149. briar/iac/scaffold/triggers/base.py +24 -0
  150. briar/iac/scaffold/triggers/bitbucket_webhook.py +54 -0
  151. briar/iac/scaffold/triggers/github_webhook.py +52 -0
  152. briar/iac/scaffold/triggers/manual.py +16 -0
  153. briar/iac/scaffold/triggers/schedule_cron.py +37 -0
  154. briar/log_context.py +73 -0
  155. briar/logging.py +68 -0
  156. briar/messaging/__init__.py +66 -0
  157. briar/messaging/_writer.py +90 -0
  158. briar/messaging/bitbucket_pr_comment.py +93 -0
  159. briar/messaging/github_pr_comment.py +90 -0
  160. briar/messaging/jira_comment.py +63 -0
  161. briar/messaging/jira_transition.py +71 -0
  162. briar/messaging/slack_channel.py +73 -0
  163. briar/messaging/telegram_chat.py +68 -0
  164. briar/notify/__init__.py +44 -0
  165. briar/notify/_sink.py +27 -0
  166. briar/notify/email.py +59 -0
  167. briar/notify/pagerduty.py +67 -0
  168. briar/notify/slack.py +49 -0
  169. briar/notify/telegram.py +48 -0
  170. briar/pagination.py +37 -0
  171. briar/settings.py +3 -0
  172. briar/storage/__init__.py +73 -0
  173. briar/storage/base.py +137 -0
  174. briar/storage/file.py +111 -0
  175. briar/storage/postgres.py +353 -0
  176. briar_cli-1.1.1.dist-info/METADATA +1031 -0
  177. briar_cli-1.1.1.dist-info/RECORD +179 -0
  178. briar_cli-1.1.1.dist-info/WHEEL +4 -0
  179. briar_cli-1.1.1.dist-info/entry_points.txt +2 -0
briar/commands/auth.py ADDED
@@ -0,0 +1,281 @@
1
+ """`briar auth` — interactive credential management.
2
+
3
+ Surface mirrors ``gh auth login`` / ``vault login`` / ``op signin``:
4
+ the thing you're logging into is the **positional target**, not a
5
+ named flag. One unified verb for both directions:
6
+
7
+ briar auth login infisical # bootstrap a password manager
8
+ briar auth login github-pat --company acme # acquire vendor credentials
9
+ briar auth login aws-sso --company acme --store infisical
10
+
11
+ The acquirer's ``destination_policy`` decides whether ``--store``
12
+ applies (vendor flows) or is forced to envfile (bootstrap flows that
13
+ populate the credentials needed to talk to a store).
14
+
15
+ Five subcommands:
16
+ login run an acquirer's interactive flow + persist
17
+ logout delete credentials a target's login would have written
18
+ refresh renew without re-prompting where the acquirer supports it
19
+ list enumerate credentials currently held in a store
20
+ status show one target × company bundle's coverage (set / missing)
21
+
22
+ Dispatch via the ``_ACTIONS`` map (one entry per subcommand) — same
23
+ table-driven dispatch as ``commands/secrets.py``."""
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import logging
29
+ from datetime import datetime, timezone
30
+ from typing import ClassVar, Dict, List
31
+
32
+ from briar.auth import AcquirerRegistry, CredentialExpired, Credentials, TerminalPromptIO
33
+ from briar.auth._acquirer import DestinationPolicy
34
+ from briar.commands.base import Command
35
+ from briar.credentials import CredentialStoreRegistry, make_credential_store
36
+ from briar.errors import CliError
37
+
38
+
39
+ log = logging.getLogger(__name__)
40
+
41
+
42
+ class CommandAuth(Command):
43
+ name = "auth"
44
+ help = "Acquire + persist credentials interactively (login / logout / list / status / refresh)."
45
+
46
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
47
+ sub = parser.add_subparsers(dest="auth_action", required=True)
48
+
49
+ targets = list(AcquirerRegistry.kinds())
50
+ store_kinds = list(CredentialStoreRegistry.kinds())
51
+ default_store = _resolve_default_store(store_kinds)
52
+
53
+ login = sub.add_parser(
54
+ "login",
55
+ help=(
56
+ "Log into a target. Positional `target` is the thing you're authenticating to: "
57
+ "a password manager (infisical) bootstraps a connection; "
58
+ "a vendor (github-pat / aws-sso / jira-session) acquires credentials and stores them."
59
+ ),
60
+ )
61
+ login.add_argument("target", choices=targets,
62
+ help="What to log into. See `briar auth login -h` for the registry.")
63
+ login.add_argument("--company", default="", help="Per-company namespace (required for vendor targets).")
64
+ login.add_argument(
65
+ "--store", default=default_store, choices=store_kinds,
66
+ help=(
67
+ "Where to persist acquired credentials. Defaults to $BRIAR_DEFAULT_STORE then envfile. "
68
+ "IGNORED for bootstrap targets (e.g. infisical) — those always persist locally."
69
+ ),
70
+ )
71
+
72
+ logout = sub.add_parser("logout", help="Delete the credentials a target's login would have written.")
73
+ logout.add_argument("target", choices=targets)
74
+ logout.add_argument("--company", default="")
75
+ logout.add_argument("--store", default=default_store, choices=store_kinds)
76
+ logout.add_argument("--yes", action="store_true", help="Skip confirmation prompt.")
77
+
78
+ refresh = sub.add_parser(
79
+ "refresh",
80
+ help=(
81
+ "Renew an OAuth / SSO bundle without re-prompting. Paste-based targets (PAT, app-password, "
82
+ "Jira API token, Jira session cookie, Infisical machine identity) cannot refresh — re-run login."
83
+ ),
84
+ )
85
+ refresh.add_argument("target", choices=targets)
86
+ refresh.add_argument("--company", default="")
87
+ refresh.add_argument("--store", default=default_store, choices=store_kinds)
88
+
89
+ listing = sub.add_parser("list", help="Show which credential env-vars are populated (names only, no values).")
90
+ listing.add_argument("--store", default=default_store, choices=store_kinds)
91
+ listing.add_argument("--company", default="", help="Filter to one company (matches the company token in env-var names).")
92
+
93
+ status = sub.add_parser("status", help="Show one target × company bundle's coverage (names + set/missing).")
94
+ status.add_argument("target", choices=targets)
95
+ status.add_argument("--company", default="")
96
+ status.add_argument("--store", default=default_store, choices=store_kinds)
97
+
98
+ _ACTIONS: ClassVar[Dict[str, str]] = {
99
+ "login": "_login",
100
+ "logout": "_logout",
101
+ "refresh": "_refresh",
102
+ "list": "_list",
103
+ "status": "_status",
104
+ }
105
+
106
+ def run(self, args: argparse.Namespace) -> int:
107
+ handler_name = self._ACTIONS.get(args.auth_action)
108
+ if handler_name is None:
109
+ known = ", ".join(sorted(self._ACTIONS.keys()))
110
+ print(f"unknown auth action: {args.auth_action} (known: {known})")
111
+ return 1
112
+ try:
113
+ return getattr(self, handler_name)(args)
114
+ except CliError as exc:
115
+ print(f"error: {exc}")
116
+ return 1
117
+ except CredentialExpired as exc:
118
+ print(f"credential expired: {exc}")
119
+ return 2
120
+
121
+ # ────────────────────────────── login ──────────────────────────────
122
+
123
+ @staticmethod
124
+ def _login(args: argparse.Namespace) -> int:
125
+ acquirer = AcquirerRegistry.make(args.target)
126
+ effective_store = _effective_store_kind(acquirer, requested=args.store)
127
+ store = make_credential_store(effective_store)
128
+ prompt = TerminalPromptIO()
129
+ log.info("auth-login: target=%s company=%s store=%s policy=%s",
130
+ args.target, args.company or "(none)", effective_store,
131
+ type(acquirer).destination_policy.value)
132
+ creds = acquirer.acquire(company=args.company, prompt=prompt)
133
+ return _persist_and_report(creds, store=store, store_kind=effective_store)
134
+
135
+ # ────────────────────────────── logout ─────────────────────────────
136
+
137
+ @staticmethod
138
+ def _logout(args: argparse.Namespace) -> int:
139
+ acquirer = AcquirerRegistry.make(args.target)
140
+ acquirer_cls = type(acquirer)
141
+ names = acquirer_cls.writes(company=args.company)
142
+ effective_store = _effective_store_kind(acquirer, requested=args.store)
143
+ if not names:
144
+ print(f"auth-logout: nothing to delete for target={args.target} company={args.company or '(none)'}")
145
+ return 0
146
+ if not args.yes:
147
+ from briar.commands.base import confirm
148
+
149
+ print(f"auth-logout: will delete {len(names)} env vars from store={effective_store}:")
150
+ for n in names:
151
+ print(f" - {n}")
152
+ if not confirm("proceed? "):
153
+ print("aborted")
154
+ return 1
155
+ store = make_credential_store(effective_store)
156
+ removed = 0
157
+ for n in names:
158
+ if store.delete(n):
159
+ removed += 1
160
+ print(f"auth-logout: removed {removed}/{len(names)} entries from store={effective_store}")
161
+ return 0
162
+
163
+ # ────────────────────────────── refresh ────────────────────────────
164
+
165
+ @staticmethod
166
+ def _refresh(args: argparse.Namespace) -> int:
167
+ acquirer = AcquirerRegistry.make(args.target)
168
+ effective_store = _effective_store_kind(acquirer, requested=args.store)
169
+ store = make_credential_store(effective_store)
170
+
171
+ # Reconstruct an "existing" bundle from the store so the
172
+ # acquirer's refresh() can use it (OAuth refresh tokens,
173
+ # expiry timestamps, etc.).
174
+ acquirer_cls = type(acquirer)
175
+ names = acquirer_cls.writes(company=args.company)
176
+ existing = Credentials(
177
+ provider_kind=args.target,
178
+ entries={n: store.read(n) for n in names if store.read(n)},
179
+ )
180
+ new = acquirer.refresh(company=args.company, existing=existing)
181
+ return _persist_and_report(new, store=store, store_kind=effective_store)
182
+
183
+ # ────────────────────────────── list ───────────────────────────────
184
+
185
+ @staticmethod
186
+ def _list(args: argparse.Namespace) -> int:
187
+ store = make_credential_store(args.store)
188
+ names = store.list()
189
+ if args.company:
190
+ tok = args.company.upper().replace("-", "_")
191
+ names = [n for n in names if f"_{tok}_" in f"_{n}_"]
192
+ if not names:
193
+ print(f"(no credentials in store={args.store}{' for company=' + args.company if args.company else ''})")
194
+ return 0
195
+ print(f"store={args.store} {len(names)} entry(ies):")
196
+ for n in names:
197
+ print(f" {n}")
198
+ return 0
199
+
200
+ # ────────────────────────────── status ─────────────────────────────
201
+
202
+ @staticmethod
203
+ def _status(args: argparse.Namespace) -> int:
204
+ acquirer = AcquirerRegistry.make(args.target)
205
+ acquirer_cls = type(acquirer)
206
+ names = acquirer_cls.writes(company=args.company)
207
+ effective_store = _effective_store_kind(acquirer, requested=args.store)
208
+ if not names:
209
+ print(f"auth-status: target={args.target} writes nothing for company={args.company or '(none)'}")
210
+ return 0
211
+ store = make_credential_store(effective_store)
212
+ print(f"auth-status: target={args.target} company={args.company or '(none)'} store={effective_store}")
213
+ any_missing = False
214
+ for n in names:
215
+ val = store.read(n)
216
+ mark = "ok " if val else "MISS"
217
+ print(f" {mark} {n}")
218
+ if not val:
219
+ any_missing = True
220
+ return 1 if any_missing else 0
221
+
222
+
223
+ def _resolve_default_store(known_kinds: List[str]) -> str:
224
+ """Default destination for ``--store``. Priority:
225
+ 1. ``$BRIAR_DEFAULT_STORE`` if it names a registered store
226
+ 2. ``envfile`` (always present, requires no setup)"""
227
+ import os
228
+
229
+ env = (os.environ.get("BRIAR_DEFAULT_STORE", "") or "").strip()
230
+ if env and env in known_kinds:
231
+ return env
232
+ return "envfile"
233
+
234
+
235
+ def _effective_store_kind(acquirer, *, requested: str) -> str:
236
+ """Honour the acquirer's destination policy. Bootstrap acquirers
237
+ (Infisical machine identity, future Vault VAULT_ADDR/TOKEN) MUST
238
+ persist locally — they can't store the credentials that describe
239
+ how to reach their own store. Warn the operator if their
240
+ ``--store`` choice gets overridden."""
241
+ policy = type(acquirer).destination_policy
242
+ if policy is DestinationPolicy.BOOTSTRAP_LOCAL and requested != "envfile":
243
+ log.warning(
244
+ "auth: target=%s is a bootstrap flow — forcing store=envfile "
245
+ "(requested store=%s ignored; you can't store the credentials "
246
+ "for a store inside that same store)",
247
+ type(acquirer).kind,
248
+ requested,
249
+ )
250
+ return "envfile"
251
+ return requested
252
+
253
+
254
+ def _persist_and_report(creds: Credentials, *, store, store_kind: str) -> int:
255
+ """Common login/refresh tail: write every entry through the store
256
+ and print a summary. Values are NEVER printed — only the key
257
+ names and counts."""
258
+ log.info("auth-persist: store=%s count=%d provider=%s", store_kind, len(creds.entries), creds.provider_kind)
259
+ failures: List[str] = []
260
+ for name, value in creds.entries.items():
261
+ try:
262
+ store.write(name, value)
263
+ except Exception as exc: # noqa: BLE001
264
+ log.exception("auth-persist: write failed name=%s store=%s", name, store_kind)
265
+ failures.append(f"{name}: {exc}")
266
+
267
+ print(f"auth: persisted {len(creds.entries) - len(failures)}/{len(creds.entries)} entries to store={store_kind}")
268
+ for n in creds.names:
269
+ if n in {f.split(':', 1)[0] for f in failures}:
270
+ print(f" FAIL {n}")
271
+ else:
272
+ print(f" ok {n}")
273
+ if creds.expires_at:
274
+ delta = creds.expires_at - datetime.now(tz=timezone.utc)
275
+ days = delta.total_seconds() / 86400
276
+ print(f"expires: {creds.expires_at.isoformat()} ({days:+.1f} days from now)")
277
+ if failures:
278
+ for f in failures:
279
+ print(f" reason: {f}")
280
+ return 3
281
+ return 0
briar/commands/base.py ADDED
@@ -0,0 +1,46 @@
1
+ """Command base class + shared helpers.
2
+
3
+ Lives in its own module so sibling concretes can import it without
4
+ triggering the package `__init__` (which itself imports the concretes
5
+ to assemble the registry — a classic circular-import trap)."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ from abc import ABC, abstractmethod
11
+
12
+
13
+ class Command(ABC):
14
+ """Implementation contract for every CLI verb.
15
+
16
+ Subclasses set `name` + `help` and implement `add_arguments` /
17
+ `run`. Subclasses must NOT touch the global registry or argparse
18
+ state directly — the entry point in `briar.cli` is the only
19
+ place that does that."""
20
+
21
+ name: str = ""
22
+ help: str = ""
23
+
24
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
25
+ """Default: no extra arguments."""
26
+
27
+ @abstractmethod
28
+ def run(self, args: argparse.Namespace) -> int:
29
+ """Execute the command. Returns the process exit code."""
30
+
31
+ @staticmethod
32
+ def confirm(prompt: str) -> bool:
33
+ """Yes/no prompt; treats EOF (piped input) as a no.
34
+
35
+ Subclasses use this to gate destructive operations behind a
36
+ --yes / interactive-prompt fork (`args.yes or self.confirm(...)`)."""
37
+ try:
38
+ return input(prompt).strip().lower() in {"y", "yes"}
39
+ except EOFError:
40
+ return False
41
+
42
+
43
+ # Module-level back-compat shim — `confirm()` was the historic name
44
+ # imported by sibling concretes. Kept so call-sites remain a one-symbol
45
+ # import without per-class qualification.
46
+ confirm = Command.confirm
@@ -0,0 +1,143 @@
1
+ """`briar context` — CRUD over local markdown knowledge blobs.
2
+
3
+ A blob holds arbitrary markdown — extracted knowledge, accumulated
4
+ memory, codified lessons, ad-hoc notes — keyed by `category:name`.
5
+ Backed by the local file `KnowledgeStore`."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Callable, Dict
13
+
14
+ from briar.commands.base import Command, confirm
15
+ from briar.errors import CliError
16
+ from briar.formatting import render
17
+ from briar.storage import KNOWLEDGE_STORE_NAMES, KnowledgeRef, make_store
18
+
19
+
20
+ Handler = Callable[[argparse.Namespace], int]
21
+
22
+
23
+ class ContextCommand(Command):
24
+ name = "context"
25
+ help = "Store and read named local markdown blobs."
26
+
27
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
28
+ parser.add_argument(
29
+ "--store",
30
+ default="file",
31
+ choices=list(KNOWLEDGE_STORE_NAMES),
32
+ help="Knowledge store backend (default: file)",
33
+ )
34
+ parser.add_argument(
35
+ "--root",
36
+ default="./knowledge",
37
+ help="Local file root",
38
+ )
39
+
40
+ sub = parser.add_subparsers(dest="op", required=True)
41
+
42
+ put = sub.add_parser("put", help="create or update a blob")
43
+ put.add_argument("blob_name", help="e.g. knowledge:acme")
44
+ put.add_argument("--content", help="inline content (or '-' for stdin)")
45
+ put.add_argument("--from-file", help="read content from this path")
46
+ put.add_argument(
47
+ "--category",
48
+ default="",
49
+ help="explicit category (default: derived from blob_name prefix)",
50
+ )
51
+
52
+ gp = sub.add_parser("get", help="print the markdown body to stdout")
53
+ gp.add_argument("blob_name")
54
+
55
+ lst = sub.add_parser("list", help="list stored blobs")
56
+ lst.add_argument(
57
+ "--prefix",
58
+ default="",
59
+ help="filter to names starting with this prefix",
60
+ )
61
+
62
+ de = sub.add_parser("delete", help="remove a blob")
63
+ de.add_argument("blob_name")
64
+ de.add_argument("--yes", action="store_true")
65
+
66
+ sub.add_parser("categories", help="print distinct category prefixes")
67
+
68
+ def run(self, args: argparse.Namespace) -> int:
69
+ handlers: Dict[str, Handler] = {
70
+ "put": self._put,
71
+ "get": self._get,
72
+ "list": self._list,
73
+ "delete": self._delete,
74
+ "categories": self._categories,
75
+ }
76
+ return handlers[args.op](args)
77
+
78
+ @staticmethod
79
+ def _read_content(args: argparse.Namespace) -> str:
80
+ ns = vars(args)
81
+ inline = ns.get("content")
82
+ if inline is not None:
83
+ return inline if inline != "-" else sys.stdin.read()
84
+ file_path = ns.get("from_file")
85
+ if file_path:
86
+ return Path(file_path).read_text()
87
+ if sys.stdin.isatty():
88
+ raise CliError("no content provided — pass --content '<text>', " "--from-file <path>, or pipe in via stdin")
89
+ return sys.stdin.read()
90
+
91
+ @staticmethod
92
+ def _ref_to_dict(ref: KnowledgeRef) -> dict:
93
+ return {
94
+ "name": ref.name,
95
+ "category": ref.category,
96
+ "byte_count": ref.byte_count,
97
+ "updated_at": ref.updated_at,
98
+ **ref.extra,
99
+ }
100
+
101
+ def _store(self, args: argparse.Namespace):
102
+ return make_store(args.store, file_root=Path(args.root))
103
+
104
+ def _put(self, args: argparse.Namespace) -> int:
105
+ ref = self._store(args).put(
106
+ args.blob_name,
107
+ self._read_content(args),
108
+ category=args.category,
109
+ )
110
+ render(self._ref_to_dict(ref), args.format)
111
+ return 0
112
+
113
+ def _get(self, args: argparse.Namespace) -> int:
114
+ body = self._store(args).get(args.blob_name)
115
+ if not body:
116
+ raise CliError(f"blob not found: {args.blob_name}")
117
+ sys.stdout.write(body)
118
+ if not body.endswith("\n"):
119
+ sys.stdout.write("\n")
120
+ return 0
121
+
122
+ def _list(self, args: argparse.Namespace) -> int:
123
+ refs = self._store(args).list(prefix=args.prefix)
124
+ items = [self._ref_to_dict(r) for r in refs]
125
+ render(items, args.format, ["name", "category", "byte_count", "updated_at"])
126
+ return 0
127
+
128
+ def _delete(self, args: argparse.Namespace) -> int:
129
+ ok = bool(args.yes) or confirm(f"Delete blob {args.blob_name} from store {args.store}? [y/N] ")
130
+ if not ok:
131
+ print("aborted")
132
+ return 1
133
+ removed = self._store(args).delete(args.blob_name)
134
+ print(f"{'deleted' if removed else 'not found'} {args.blob_name}")
135
+ return 0
136
+
137
+ def _categories(self, args: argparse.Namespace) -> int:
138
+ seen: Dict[str, int] = {}
139
+ for ref in self._store(args).list():
140
+ seen[ref.category] = seen.get(ref.category, 0) + 1
141
+ items = [{"category": cat or "(none)", "blob_count": n} for cat, n in sorted(seen.items())]
142
+ render(items, args.format, ["category", "blob_count"])
143
+ return 0
@@ -0,0 +1,61 @@
1
+ """`briar dashboard` — boot the read-only HTML dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+ from briar.commands.base import Command
9
+ from briar.dashboard.collectors import CollectorRegistry, DashboardPaths, DashboardSelf
10
+ from briar.dashboard.server import DashboardServer
11
+ from briar.env_vars import CredEnv
12
+ from briar.storage import KNOWLEDGE_STORE_NAMES, make_store
13
+
14
+
15
+ class CommandDashboard(Command):
16
+ name = "dashboard"
17
+ help = "Serve a read-only HTML dashboard summarising the droplet state."
18
+
19
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
20
+ parser.add_argument("--host", default="0.0.0.0", help="bind address (default: 0.0.0.0 — public)")
21
+ parser.add_argument("--port", type=int, default=8080, help="bind port (default: 8080)")
22
+ parser.add_argument("--examples", default="./examples", help="directory of runbook YAMLs")
23
+ parser.add_argument(
24
+ "--knowledge-store",
25
+ default="",
26
+ choices=[""] + list(KNOWLEDGE_STORE_NAMES),
27
+ help="which KnowledgeStore to read from (default: postgres if BRIAR_DATABASE_URL is set, else file)",
28
+ )
29
+ parser.add_argument("--knowledge", default="./knowledge", help="knowledge file root (only used when --knowledge-store=file)")
30
+ parser.add_argument("--log-file", default="/var/log/briar/scheduler.log", help="path to the scheduler log")
31
+ parser.add_argument("--disk-path", default="/", help="filesystem path used for disk-usage stats")
32
+ parser.add_argument("--repo-path", default=".", help="path of the deployed git checkout")
33
+ parser.add_argument("--secrets-file", default="/etc/briar/secrets.env", help="secrets.env path (names+lengths only)")
34
+ parser.add_argument("--du-path", action="append", default=[], help="directory whose size to report (repeatable)")
35
+ parser.add_argument("--once", action="store_true", help="render once to stdout and exit")
36
+
37
+ def run(self, args: argparse.Namespace) -> int:
38
+ store_name = args.knowledge_store or ("postgres" if CredEnv.BRIAR_DATABASE_URL.read() else "file")
39
+ knowledge_store = make_store(store_name, file_root=Path(args.knowledge))
40
+
41
+ server = DashboardServer(host=args.host, port=args.port)
42
+ paths = DashboardPaths(
43
+ examples_dir=Path(args.examples),
44
+ knowledge_store=knowledge_store,
45
+ log_path=Path(args.log_file),
46
+ disk_path=Path(args.disk_path),
47
+ repo_path=Path(args.repo_path),
48
+ secrets_path=Path(args.secrets_file),
49
+ du_paths=[Path(p) for p in (args.du_path or [args.repo_path, args.knowledge, "/var/log/briar"])],
50
+ )
51
+ dash = DashboardSelf(
52
+ started_at=server.started_at,
53
+ request_count_fn=server.request_count,
54
+ last_render_ms_fn=server.last_render_ms,
55
+ )
56
+ server.set_collectors(CollectorRegistry.from_paths(paths, dash))
57
+ if args.once:
58
+ print(server.render_index())
59
+ return 0
60
+ server.serve()
61
+ return 0
@@ -0,0 +1,79 @@
1
+ """`briar extract` — run one or more knowledge extractors, write the
2
+ result to a local markdown file."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ from pathlib import Path
8
+ from typing import List
9
+
10
+ from briar.commands.base import Command
11
+ from briar.errors import CliError
12
+ from briar.extract import EXTRACTORS
13
+ from briar.extract.base import ExtractedSection
14
+ from briar.extract.composer import render_json, render_markdown
15
+ from briar.storage import KNOWLEDGE_STORE_NAMES, make_store
16
+
17
+
18
+ class CommandExtract(Command):
19
+ name = "extract"
20
+ help = "Mine the live state of GitHub / AWS / etc. into a markdown " "knowledge blob written to local disk."
21
+
22
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
23
+ parser.add_argument(
24
+ "--company",
25
+ required=True,
26
+ help="Company name (drives the markdown title + blob name)",
27
+ )
28
+ parser.add_argument(
29
+ "--include",
30
+ action="append",
31
+ default=[],
32
+ choices=sorted(EXTRACTORS.keys()),
33
+ help="Which extractor(s) to run (repeatable; default: all available)",
34
+ )
35
+ parser.add_argument(
36
+ "--storage",
37
+ default="file",
38
+ choices=list(KNOWLEDGE_STORE_NAMES),
39
+ help="Where to write the result (default: file)",
40
+ )
41
+ parser.add_argument("--blob-name", default="", help="Storage blob name (default: knowledge:<company>)")
42
+ parser.add_argument("--root", default="./knowledge", help="Local file root (only used when --storage=file)")
43
+ parser.add_argument("--out-json", default="", help="Parallel JSON output path (empty = skip)")
44
+ for ext in EXTRACTORS.values():
45
+ ext.add_arguments(parser)
46
+
47
+ def run(self, args: argparse.Namespace) -> int:
48
+ selected = args.include or list(EXTRACTORS.keys())
49
+
50
+ sections: List[ExtractedSection] = []
51
+ for name in selected:
52
+ ext = EXTRACTORS[name]
53
+ if not ext.is_available(args):
54
+ print(f" skipped {name} (not available in this env)")
55
+ continue
56
+ print(f" running {name} ...")
57
+ section = ext.extract(args)
58
+ if section.is_empty:
59
+ print(f" {name}: no data")
60
+ continue
61
+ sections.append(section)
62
+
63
+ if not sections:
64
+ raise CliError("nothing extracted — every enabled extractor returned empty")
65
+
66
+ md = render_markdown(company=args.company, sections=sections)
67
+ blob_name = args.blob_name or f"knowledge:{args.company}"
68
+
69
+ store = make_store(args.storage, file_root=Path(args.root))
70
+ ref = store.put(blob_name, md, category="knowledge")
71
+ print(f"\nwrote blob '{ref.name}' " f"({ref.byte_count} bytes, {len(sections)} sections) " f"via store={args.storage}")
72
+
73
+ if args.out_json:
74
+ json_path = Path(args.out_json)
75
+ json_path.parent.mkdir(parents=True, exist_ok=True)
76
+ json_path.write_text(render_json(company=args.company, sections=sections))
77
+ print(f"wrote {json_path}")
78
+
79
+ return 0
briar/commands/iac.py ADDED
@@ -0,0 +1,44 @@
1
+ """IaC scaffold command — emits JSON the user pastes into the web UI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ from pathlib import Path
8
+
9
+ from briar.commands.base import Command
10
+ from briar.errors import CliError
11
+ from briar.iac import TEMPLATES
12
+
13
+
14
+ class CommandScaffold(Command):
15
+ name = "scaffold"
16
+ help = "Generate a starter config file for a built-in template."
17
+
18
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
19
+ sub = parser.add_subparsers(
20
+ dest="template",
21
+ required=True,
22
+ metavar="TEMPLATE",
23
+ )
24
+ for name, tmpl in TEMPLATES.items():
25
+ tp = sub.add_parser(name, help=tmpl.description)
26
+ tp.add_argument(
27
+ "--out",
28
+ "-o",
29
+ default="-",
30
+ help="output path (default: stdout)",
31
+ )
32
+ tmpl.add_arguments(tp)
33
+
34
+ def run(self, args: argparse.Namespace) -> int:
35
+ tmpl = TEMPLATES.get(args.template)
36
+ if tmpl is None:
37
+ raise CliError(f"unknown template: {args.template}")
38
+ text = json.dumps(tmpl.build(args), indent=2)
39
+ if args.out == "-":
40
+ print(text)
41
+ return 0
42
+ Path(args.out).write_text(text)
43
+ print(f"wrote {args.out}")
44
+ return 0