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.
- briar/__init__.py +8 -0
- briar/__main__.py +11 -0
- briar/_registry.py +36 -0
- briar/agent/__init__.py +16 -0
- briar/agent/_llm.py +90 -0
- briar/agent/_llms/__init__.py +40 -0
- briar/agent/_llms/anthropic_llm.py +185 -0
- briar/agent/_llms/bedrock.py +154 -0
- briar/agent/_llms/gemini.py +152 -0
- briar/agent/_llms/openai_llm.py +129 -0
- briar/agent/runner.py +364 -0
- briar/agent/tools.py +355 -0
- briar/auth/__init__.py +42 -0
- briar/auth/_acquirer.py +121 -0
- briar/auth/_acquirers/__init__.py +62 -0
- briar/auth/_acquirers/aws_sso.py +185 -0
- briar/auth/_acquirers/aws_static.py +54 -0
- briar/auth/_acquirers/bitbucket.py +60 -0
- briar/auth/_acquirers/github_device.py +129 -0
- briar/auth/_acquirers/github_pat.py +44 -0
- briar/auth/_acquirers/infisical.py +80 -0
- briar/auth/_acquirers/jira_session.py +102 -0
- briar/auth/_acquirers/jira_token.py +61 -0
- briar/auth/_acquirers/linear.py +46 -0
- briar/auth/_prompt.py +155 -0
- briar/cli.py +149 -0
- briar/commands/__init__.py +48 -0
- briar/commands/agent.py +692 -0
- briar/commands/auth.py +281 -0
- briar/commands/base.py +46 -0
- briar/commands/context.py +143 -0
- briar/commands/dashboard.py +61 -0
- briar/commands/extract.py +79 -0
- briar/commands/iac.py +44 -0
- briar/commands/runbook.py +110 -0
- briar/commands/secrets.py +165 -0
- briar/commands/version.py +17 -0
- briar/credentials/__init__.py +48 -0
- briar/credentials/_bootstrap.py +93 -0
- briar/credentials/_bootstraps/__init__.py +80 -0
- briar/credentials/_bootstraps/infisical.py +115 -0
- briar/credentials/_store.py +63 -0
- briar/credentials/aws_secrets.py +102 -0
- briar/credentials/envfile.py +171 -0
- briar/credentials/infisical.py +183 -0
- briar/credentials/ssm.py +83 -0
- briar/credentials/vault.py +109 -0
- briar/dashboard/__init__.py +13 -0
- briar/dashboard/collectors.py +1354 -0
- briar/dashboard/server.py +154 -0
- briar/dashboard/templates/index.html +678 -0
- briar/decorators.py +48 -0
- briar/env_vars.py +92 -0
- briar/error_policy.py +273 -0
- briar/errors.py +48 -0
- briar/extract/__init__.py +52 -0
- briar/extract/_cloud.py +157 -0
- briar/extract/_clouds/__init__.py +39 -0
- briar/extract/_clouds/aws.py +190 -0
- briar/extract/_clouds/azure.py +152 -0
- briar/extract/_clouds/gcp.py +135 -0
- briar/extract/_gh.py +159 -0
- briar/extract/_provider.py +218 -0
- briar/extract/_providers/__init__.py +60 -0
- briar/extract/_providers/bitbucket.py +276 -0
- briar/extract/_providers/github.py +277 -0
- briar/extract/_tracker.py +124 -0
- briar/extract/_trackers/__init__.py +44 -0
- briar/extract/_trackers/_jira_auth.py +258 -0
- briar/extract/_trackers/bitbucket.py +131 -0
- briar/extract/_trackers/github_issues.py +139 -0
- briar/extract/_trackers/jira.py +191 -0
- briar/extract/_trackers/linear.py +172 -0
- briar/extract/_user_filter.py +150 -0
- briar/extract/active_tickets.py +71 -0
- briar/extract/active_work.py +87 -0
- briar/extract/aws_infra.py +79 -0
- briar/extract/aws_services/__init__.py +31 -0
- briar/extract/aws_services/base.py +25 -0
- briar/extract/aws_services/ecs.py +43 -0
- briar/extract/aws_services/lambda_.py +38 -0
- briar/extract/aws_services/logs.py +39 -0
- briar/extract/aws_services/rds.py +35 -0
- briar/extract/aws_services/sqs.py +25 -0
- briar/extract/base.py +290 -0
- briar/extract/code_hotspots.py +134 -0
- briar/extract/codebase_conventions.py +72 -0
- briar/extract/composer.py +85 -0
- briar/extract/github_deployments.py +106 -0
- briar/extract/language_detectors/__init__.py +24 -0
- briar/extract/language_detectors/base.py +29 -0
- briar/extract/language_detectors/go.py +26 -0
- briar/extract/language_detectors/node.py +42 -0
- briar/extract/language_detectors/python.py +41 -0
- briar/extract/pr_archaeology.py +131 -0
- briar/extract/pr_review_context.py +123 -0
- briar/extract/reviewer_profile.py +141 -0
- briar/extract/ticket_archaeology.py +115 -0
- briar/extract/ticket_context.py +95 -0
- briar/formatting/__init__.py +67 -0
- briar/formatting/base.py +25 -0
- briar/formatting/csv.py +34 -0
- briar/formatting/json.py +19 -0
- briar/formatting/quiet.py +29 -0
- briar/formatting/table.py +97 -0
- briar/formatting/yaml.py +35 -0
- briar/iac/__init__.py +18 -0
- briar/iac/config_file.py +114 -0
- briar/iac/models.py +232 -0
- briar/iac/reference_map.py +33 -0
- briar/iac/runbook/__init__.py +32 -0
- briar/iac/runbook/executor.py +365 -0
- briar/iac/runbook/models.py +156 -0
- briar/iac/runbook/scheduler.py +187 -0
- briar/iac/scaffold/__init__.py +25 -0
- briar/iac/scaffold/_composer.py +308 -0
- briar/iac/scaffold/_knowledge.py +119 -0
- briar/iac/scaffold/archetypes/__init__.py +26 -0
- briar/iac/scaffold/archetypes/base.py +85 -0
- briar/iac/scaffold/archetypes/engineer.py +64 -0
- briar/iac/scaffold/archetypes/pr_ci_fixer.py +100 -0
- briar/iac/scaffold/archetypes/pr_conflict_resolver.py +83 -0
- briar/iac/scaffold/archetypes/pr_fixer.py +62 -0
- briar/iac/scaffold/archetypes/triager.py +50 -0
- briar/iac/scaffold/base.py +19 -0
- briar/iac/scaffold/implementation.py +59 -0
- briar/iac/scaffold/pr_fixes.py +52 -0
- briar/iac/scaffold/rules/__init__.py +64 -0
- briar/iac/scaffold/rules/base.py +121 -0
- briar/iac/scaffold/rules/commit_as_human.md +22 -0
- briar/iac/scaffold/rules/minimum_correct_fix.md +20 -0
- briar/iac/scaffold/rules/no_force_push.md +19 -0
- briar/iac/scaffold/rules/no_new_pr_creation.md +16 -0
- briar/iac/scaffold/rules/no_workflow_file_edits.md +21 -0
- briar/iac/scaffold/rules/read_all_comments_first.md +20 -0
- briar/iac/scaffold/rules/skip_approved_green_prs.md +17 -0
- briar/iac/scaffold/shapes/__init__.py +38 -0
- briar/iac/scaffold/shapes/base.py +17 -0
- briar/iac/scaffold/shapes/one_shot.py +48 -0
- briar/iac/scaffold/shapes/plan_approve_act.py +134 -0
- briar/iac/scaffold/shapes/triage.py +49 -0
- briar/iac/scaffold/sources/__init__.py +26 -0
- briar/iac/scaffold/sources/aws.py +69 -0
- briar/iac/scaffold/sources/base.py +61 -0
- briar/iac/scaffold/sources/bitbucket.py +164 -0
- briar/iac/scaffold/sources/github.py +155 -0
- briar/iac/scaffold/sources/jira.py +147 -0
- briar/iac/scaffold/triggers/__init__.py +23 -0
- briar/iac/scaffold/triggers/base.py +24 -0
- briar/iac/scaffold/triggers/bitbucket_webhook.py +54 -0
- briar/iac/scaffold/triggers/github_webhook.py +52 -0
- briar/iac/scaffold/triggers/manual.py +16 -0
- briar/iac/scaffold/triggers/schedule_cron.py +37 -0
- briar/log_context.py +73 -0
- briar/logging.py +68 -0
- briar/messaging/__init__.py +66 -0
- briar/messaging/_writer.py +90 -0
- briar/messaging/bitbucket_pr_comment.py +93 -0
- briar/messaging/github_pr_comment.py +90 -0
- briar/messaging/jira_comment.py +63 -0
- briar/messaging/jira_transition.py +71 -0
- briar/messaging/slack_channel.py +73 -0
- briar/messaging/telegram_chat.py +68 -0
- briar/notify/__init__.py +44 -0
- briar/notify/_sink.py +27 -0
- briar/notify/email.py +59 -0
- briar/notify/pagerduty.py +67 -0
- briar/notify/slack.py +49 -0
- briar/notify/telegram.py +48 -0
- briar/pagination.py +37 -0
- briar/settings.py +3 -0
- briar/storage/__init__.py +73 -0
- briar/storage/base.py +137 -0
- briar/storage/file.py +111 -0
- briar/storage/postgres.py +353 -0
- briar_cli-1.1.1.dist-info/METADATA +1031 -0
- briar_cli-1.1.1.dist-info/RECORD +179 -0
- briar_cli-1.1.1.dist-info/WHEEL +4 -0
- 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
|