baucli 1.0.1__tar.gz → 1.0.3__tar.gz
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.
- {baucli-1.0.1 → baucli-1.0.3}/PKG-INFO +1 -1
- {baucli-1.0.1 → baucli-1.0.3}/baucli/__init__.py +1 -1
- {baucli-1.0.1 → baucli-1.0.3}/baucli/api.py +5 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/cli.py +203 -4
- {baucli-1.0.1 → baucli-1.0.3}/baucli/config.py +160 -10
- {baucli-1.0.1 → baucli-1.0.3}/baucli/project.py +8 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli.egg-info/PKG-INFO +1 -1
- baucli-1.0.3/baucli.egg-info/entry_points.txt +2 -0
- {baucli-1.0.1 → baucli-1.0.3}/setup.py +2 -2
- baucli-1.0.1/baucli.egg-info/entry_points.txt +0 -2
- {baucli-1.0.1 → baucli-1.0.3}/README.md +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/apex_rebuild.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/apexlang.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/db/__init__.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/db/mssql.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/db/mysql.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/db/oracle.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/db/postgresql.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/docmig.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/git.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/legacy/__init__.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/legacy/delphi.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/mcp_server.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/rag_embed.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/sample_scan.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/scope_scan.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/screenshot.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/seed_check.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/stress.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli/sync.py +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli.egg-info/SOURCES.txt +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli.egg-info/dependency_links.txt +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli.egg-info/requires.txt +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/baucli.egg-info/top_level.txt +0 -0
- {baucli-1.0.1 → baucli-1.0.3}/setup.cfg +0 -0
|
@@ -822,6 +822,11 @@ class BauApi:
|
|
|
822
822
|
"""GET /quota — Full quota status."""
|
|
823
823
|
return self._request("GET", "quota")
|
|
824
824
|
|
|
825
|
+
def whoami(self) -> dict:
|
|
826
|
+
"""GET /whoami — dono (BAU_USERS) da API key atual: user_email/name/role,
|
|
827
|
+
tenant_code, key_prefix. Usado por `status` e pelo cabeçalho."""
|
|
828
|
+
return self._request("GET", "whoami")
|
|
829
|
+
|
|
825
830
|
# ── DOCAPEX endpoints ──
|
|
826
831
|
|
|
827
832
|
def apex_sync(self, payload: dict) -> dict:
|
|
@@ -36,6 +36,7 @@ from baucli.config import (
|
|
|
36
36
|
load_config, save_config, is_configured, get_database,
|
|
37
37
|
add_database, remove_database, list_databases, set_db_password,
|
|
38
38
|
get_db_password, delete_db_password, DEFAULT_CONFIG,
|
|
39
|
+
BauConfigError,
|
|
39
40
|
)
|
|
40
41
|
from baucli.api import BauApi, BauApiError, BauSessionError
|
|
41
42
|
from baucli.sync import sync_database, sync_all, get_extractor, get_sync_recommendation, PARALLEL_THRESHOLD
|
|
@@ -54,6 +55,13 @@ def show_db_panel(title: str, db_name: str = None, db_config: dict = None,
|
|
|
54
55
|
lines = [f"[bold red]{__product__} v{__version__}[/bold red] — {title}"]
|
|
55
56
|
lines.append("")
|
|
56
57
|
|
|
58
|
+
# Dono da API key (cacheado por `status`/`init` via GET /whoami).
|
|
59
|
+
if cfg.get("key_user_email"):
|
|
60
|
+
_who = cfg["key_user_email"]
|
|
61
|
+
if cfg.get("key_user_name"):
|
|
62
|
+
_who += f" ({cfg['key_user_name']})"
|
|
63
|
+
lines.append(f" Usuário: [cyan]{_who}[/cyan]")
|
|
64
|
+
|
|
57
65
|
if db_name and db_config:
|
|
58
66
|
engine = db_config.get("engine", "?").upper()
|
|
59
67
|
host = db_config.get("host", "?")
|
|
@@ -79,7 +87,8 @@ def show_db_panel(title: str, db_name: str = None, db_config: dict = None,
|
|
|
79
87
|
if uuid != "—":
|
|
80
88
|
lines.append(f" BAU UUID: {uuid}")
|
|
81
89
|
else:
|
|
82
|
-
|
|
90
|
+
if cfg.get("license_key"):
|
|
91
|
+
lines.append(f" License: [cyan]{cfg['license_key']}[/cyan]")
|
|
83
92
|
lines.append(f" Server: {cfg.get('api_url', '—')}")
|
|
84
93
|
lines.append(f" Tenant: {cfg.get('tenant_code', '—')}")
|
|
85
94
|
lines.append(f" Language: [cyan]{cfg.get('language') or 'en_US (default)'}[/cyan]")
|
|
@@ -134,7 +143,9 @@ def _do_login(cfg: dict, email: str, password: str) -> bool:
|
|
|
134
143
|
# ═══════════════════════════════════════════════════════════
|
|
135
144
|
@click.group()
|
|
136
145
|
@click.version_option(__version__, prog_name=__product__)
|
|
137
|
-
|
|
146
|
+
@click.option("--profile", "profile", default=None,
|
|
147
|
+
help="Perfil de credencial a usar nesta invocação (sobrepõe bau.toml/default).")
|
|
148
|
+
def main(profile):
|
|
138
149
|
"""BAUCLI — AI-powered database documentation.
|
|
139
150
|
|
|
140
151
|
\b
|
|
@@ -144,7 +155,8 @@ def main():
|
|
|
144
155
|
baucli sync
|
|
145
156
|
baucli context
|
|
146
157
|
"""
|
|
147
|
-
|
|
158
|
+
if profile:
|
|
159
|
+
os.environ["BAUCLI_PROFILE"] = profile
|
|
148
160
|
|
|
149
161
|
|
|
150
162
|
# ── Aviso global: "golden desatualizado" ────────────────────────────
|
|
@@ -390,6 +402,18 @@ def _run_init(license_key: str, api_key_manual: str, api_url: str,
|
|
|
390
402
|
except BauApiError:
|
|
391
403
|
pass
|
|
392
404
|
|
|
405
|
+
# Dono da API key (cacheia p/ o cabeçalho + confirma no init)
|
|
406
|
+
try:
|
|
407
|
+
me = api.whoami()
|
|
408
|
+
if isinstance(me, dict) and me.get("status") == "ok":
|
|
409
|
+
cfg["key_user_email"] = me.get("user_email")
|
|
410
|
+
cfg["key_user_name"] = me.get("user_name")
|
|
411
|
+
save_config(cfg)
|
|
412
|
+
_uname = f" ({me.get('user_name')})" if me.get("user_name") else ""
|
|
413
|
+
console.print(f" Usuário: [green]{me.get('user_email')}[/green]{_uname}")
|
|
414
|
+
except BauApiError:
|
|
415
|
+
pass
|
|
416
|
+
|
|
393
417
|
from .config import CONFIG_FILE
|
|
394
418
|
console.print()
|
|
395
419
|
console.print(f"[green]Config saved to {CONFIG_FILE}[/green]")
|
|
@@ -1846,6 +1870,113 @@ def _write_root_pointer_md(current_schema: str, primary_locale: str,
|
|
|
1846
1870
|
pointer.write_text(new_content, encoding="utf-8")
|
|
1847
1871
|
|
|
1848
1872
|
|
|
1873
|
+
# ═══════════════════════════════════════════════════════════
|
|
1874
|
+
# baucli profile (Fase 1 — perfis de credencial por tenant)
|
|
1875
|
+
# ═══════════════════════════════════════════════════════════
|
|
1876
|
+
@main.group()
|
|
1877
|
+
def profiles():
|
|
1878
|
+
"""Perfis de credencial (1 BAUCLI_HOME, N tenants).
|
|
1879
|
+
|
|
1880
|
+
\b
|
|
1881
|
+
profiles add NOME --tenant T --api-key K [--base-url U] [--db DB] [--default]
|
|
1882
|
+
profiles ls
|
|
1883
|
+
profiles use NOME
|
|
1884
|
+
profiles rm NOME
|
|
1885
|
+
O bau.toml do projeto escolhe o perfil por [bau].profile (sem segredo no repo).
|
|
1886
|
+
"""
|
|
1887
|
+
pass
|
|
1888
|
+
|
|
1889
|
+
|
|
1890
|
+
@profiles.command("add")
|
|
1891
|
+
@click.argument("name")
|
|
1892
|
+
@click.option("--tenant", "tenant_code", default=None, help="Código do tenant (ex.: GRUPO_DASS).")
|
|
1893
|
+
@click.option("--api-key", "api_key", required=True, help="API key do dev nesse tenant (bau_...).")
|
|
1894
|
+
@click.option("--base-url", "base_url", default=None, help="URL da API (default: a do config).")
|
|
1895
|
+
@click.option("--db", "default_db", default=None, help="Alias de database default do perfil.")
|
|
1896
|
+
@click.option("--default", "make_default", is_flag=True, help="Torna este o perfil default do BAUCLI_HOME.")
|
|
1897
|
+
def profile_add(name, tenant_code, api_key, base_url, default_db, make_default):
|
|
1898
|
+
"""Cria/atualiza um perfil e grava sua api_key no keyring."""
|
|
1899
|
+
from .config import load_config as _lc, save_config, set_profile_api_key
|
|
1900
|
+
cfg = _lc(apply_profile=False)
|
|
1901
|
+
profiles = cfg.setdefault("profiles", {})
|
|
1902
|
+
entry = profiles.get(name, {})
|
|
1903
|
+
if tenant_code:
|
|
1904
|
+
entry["tenant_code"] = tenant_code
|
|
1905
|
+
if base_url:
|
|
1906
|
+
entry["base_url"] = base_url
|
|
1907
|
+
if default_db:
|
|
1908
|
+
entry["default_db"] = default_db
|
|
1909
|
+
profiles[name] = entry
|
|
1910
|
+
if make_default or not cfg.get("default_profile"):
|
|
1911
|
+
cfg["default_profile"] = name
|
|
1912
|
+
save_config(cfg)
|
|
1913
|
+
in_kr = set_profile_api_key(name, api_key)
|
|
1914
|
+
tail = " (default do BAUCLI_HOME)" if cfg.get("default_profile") == name else ""
|
|
1915
|
+
console.print(f"[green]Perfil '{name}' salvo{tail}[/green] — chave no "
|
|
1916
|
+
f"{'keyring' if in_kr else '.credentials'}.")
|
|
1917
|
+
|
|
1918
|
+
|
|
1919
|
+
@profiles.command("ls")
|
|
1920
|
+
def profile_ls():
|
|
1921
|
+
"""Lista os perfis do BAUCLI_HOME e qual está ativo."""
|
|
1922
|
+
from .config import load_config as _lc, resolve_profile_name, get_profile_api_key
|
|
1923
|
+
cfg = _lc(apply_profile=False)
|
|
1924
|
+
profiles = cfg.get("profiles", {})
|
|
1925
|
+
if not profiles:
|
|
1926
|
+
console.print(" [yellow]Nenhum perfil.[/yellow] Crie: "
|
|
1927
|
+
"[cyan]baucli profile add NOME --tenant T --api-key K[/cyan]")
|
|
1928
|
+
return
|
|
1929
|
+
try:
|
|
1930
|
+
active = resolve_profile_name(cfg)
|
|
1931
|
+
except Exception:
|
|
1932
|
+
active = None
|
|
1933
|
+
default = cfg.get("default_profile")
|
|
1934
|
+
table = RichTable(title=f"Perfis ({len(profiles)})", border_style="dim")
|
|
1935
|
+
table.add_column("Nome", style="cyan")
|
|
1936
|
+
table.add_column("Tenant")
|
|
1937
|
+
table.add_column("DB default")
|
|
1938
|
+
table.add_column("Chave")
|
|
1939
|
+
table.add_column("Default", style="yellow")
|
|
1940
|
+
table.add_column("Ativo", style="green")
|
|
1941
|
+
for n, p in profiles.items():
|
|
1942
|
+
table.add_row(n, p.get("tenant_code", "—"), p.get("default_db", "—"),
|
|
1943
|
+
"sim" if get_profile_api_key(n) else "—",
|
|
1944
|
+
"★" if n == default else "", "●" if n == active else "")
|
|
1945
|
+
console.print(table)
|
|
1946
|
+
console.print(" [dim]Ativo = env BAUCLI_PROFILE > bau.toml [bau].profile > default.[/dim]")
|
|
1947
|
+
|
|
1948
|
+
|
|
1949
|
+
@profiles.command("use")
|
|
1950
|
+
@click.argument("name")
|
|
1951
|
+
def profile_use(name):
|
|
1952
|
+
"""Define o perfil default do BAUCLI_HOME."""
|
|
1953
|
+
from .config import load_config as _lc, save_config
|
|
1954
|
+
cfg = _lc(apply_profile=False)
|
|
1955
|
+
if name not in cfg.get("profiles", {}):
|
|
1956
|
+
console.print(f"[red]Perfil '{name}' não existe.[/red] Veja: baucli profile ls")
|
|
1957
|
+
raise SystemExit(1)
|
|
1958
|
+
cfg["default_profile"] = name
|
|
1959
|
+
save_config(cfg)
|
|
1960
|
+
console.print(f"[green]Perfil default = '{name}'.[/green]")
|
|
1961
|
+
|
|
1962
|
+
|
|
1963
|
+
@profiles.command("rm")
|
|
1964
|
+
@click.argument("name")
|
|
1965
|
+
def profile_rm(name):
|
|
1966
|
+
"""Remove um perfil e sua api_key."""
|
|
1967
|
+
from .config import load_config as _lc, save_config, delete_profile_api_key
|
|
1968
|
+
cfg = _lc(apply_profile=False)
|
|
1969
|
+
if name not in cfg.get("profiles", {}):
|
|
1970
|
+
console.print(f"[yellow]Perfil '{name}' já não existe.[/yellow]")
|
|
1971
|
+
return
|
|
1972
|
+
cfg["profiles"].pop(name, None)
|
|
1973
|
+
if cfg.get("default_profile") == name:
|
|
1974
|
+
cfg["default_profile"] = next(iter(cfg["profiles"]), None)
|
|
1975
|
+
save_config(cfg)
|
|
1976
|
+
delete_profile_api_key(name)
|
|
1977
|
+
console.print(f"[green]Perfil '{name}' removido.[/green]")
|
|
1978
|
+
|
|
1979
|
+
|
|
1849
1980
|
# ═══════════════════════════════════════════════════════════
|
|
1850
1981
|
# baucli status
|
|
1851
1982
|
# ═══════════════════════════════════════════════════════════
|
|
@@ -1854,6 +1985,21 @@ def status():
|
|
|
1854
1985
|
"""Show agent config, quota, and pending deploys."""
|
|
1855
1986
|
cfg = load_config()
|
|
1856
1987
|
|
|
1988
|
+
if cfg.get("_active_profile"):
|
|
1989
|
+
console.print(f"[dim]Perfil ativo:[/dim] [cyan]{cfg['_active_profile']}[/cyan] "
|
|
1990
|
+
f"· tenant {cfg.get('tenant_code') or '—'}")
|
|
1991
|
+
|
|
1992
|
+
# Resolve o dono da API key (ao vivo) e cacheia p/ o cabeçalho.
|
|
1993
|
+
if cfg.get("api_key"):
|
|
1994
|
+
try:
|
|
1995
|
+
me = get_api(cfg).whoami()
|
|
1996
|
+
if isinstance(me, dict) and me.get("status") == "ok":
|
|
1997
|
+
cfg["key_user_email"] = me.get("user_email")
|
|
1998
|
+
cfg["key_user_name"] = me.get("user_name")
|
|
1999
|
+
save_config(cfg)
|
|
2000
|
+
except Exception:
|
|
2001
|
+
pass
|
|
2002
|
+
|
|
1857
2003
|
show_db_panel("Status")
|
|
1858
2004
|
|
|
1859
2005
|
# Databases table
|
|
@@ -7750,5 +7896,58 @@ def rag_search_cmd(pergunta, schema, top):
|
|
|
7750
7896
|
f"[bold]{it['key']}[/bold]: {it['text'][:140]}")
|
|
7751
7897
|
|
|
7752
7898
|
|
|
7899
|
+
def _report_crash(exc: Exception):
|
|
7900
|
+
"""Salva o traceback completo num log local e mostra mensagem de Suporte."""
|
|
7901
|
+
import traceback
|
|
7902
|
+
import time
|
|
7903
|
+
from baucli.config import BAUCLI_DIR
|
|
7904
|
+
log_path = None
|
|
7905
|
+
try:
|
|
7906
|
+
BAUCLI_DIR.mkdir(parents=True, exist_ok=True)
|
|
7907
|
+
log_path = BAUCLI_DIR / "last_error.log"
|
|
7908
|
+
with open(log_path, "w", encoding="utf-8") as f:
|
|
7909
|
+
f.write(f"{__product__} v{__version__}\n")
|
|
7910
|
+
f.write(f"cmd: {' '.join(sys.argv)}\n")
|
|
7911
|
+
f.write(f"time: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
|
7912
|
+
f.write("".join(traceback.format_exception(type(exc), exc, exc.__traceback__)))
|
|
7913
|
+
except Exception:
|
|
7914
|
+
log_path = None
|
|
7915
|
+
console.print()
|
|
7916
|
+
console.print("[red]Ops — o BAUCLI encontrou um erro inesperado e não conseguiu concluir a operação.[/red]")
|
|
7917
|
+
console.print(f" [dim]{type(exc).__name__}: {exc}[/dim]")
|
|
7918
|
+
console.print(" Contate o [bold]Suporte BEG[/bold]: [cyan]www.begcloud.com[/cyan]")
|
|
7919
|
+
if log_path:
|
|
7920
|
+
console.print(f" Anexe este arquivo ao chamado: [cyan]{log_path}[/cyan]")
|
|
7921
|
+
console.print(" [dim]Detalhe técnico (devs): rode de novo com BAUCLI_DEBUG=1[/dim]")
|
|
7922
|
+
|
|
7923
|
+
|
|
7924
|
+
def cli():
|
|
7925
|
+
"""Entry point do console com captura GLOBAL de erros (UX amigável).
|
|
7926
|
+
|
|
7927
|
+
- Erros de configuração (JSON inválido) → mensagem clara e auto-corrigível.
|
|
7928
|
+
- Ctrl-C → 'Cancelado.'.
|
|
7929
|
+
- Qualquer erro não tratado → mensagem de Suporte + traceback salvo em
|
|
7930
|
+
BAUCLI_HOME/last_error.log. BAUCLI_DEBUG=1 mostra o traceback cru.
|
|
7931
|
+
Erros de uso do Click (opção inválida etc.) seguem o comportamento padrão.
|
|
7932
|
+
"""
|
|
7933
|
+
try:
|
|
7934
|
+
main(standalone_mode=True)
|
|
7935
|
+
except SystemExit:
|
|
7936
|
+
raise
|
|
7937
|
+
except KeyboardInterrupt:
|
|
7938
|
+
console.print("\n[dim]Cancelado.[/dim]")
|
|
7939
|
+
sys.exit(130)
|
|
7940
|
+
except BauConfigError as e:
|
|
7941
|
+
console.print(f"[red]Configuração inválida.[/red]\n{e}")
|
|
7942
|
+
sys.exit(1)
|
|
7943
|
+
except click.ClickException:
|
|
7944
|
+
raise
|
|
7945
|
+
except Exception as e: # noqa: BLE001 — captura geral proposital
|
|
7946
|
+
if os.environ.get("BAUCLI_DEBUG"):
|
|
7947
|
+
raise
|
|
7948
|
+
_report_crash(e)
|
|
7949
|
+
sys.exit(1)
|
|
7950
|
+
|
|
7951
|
+
|
|
7753
7952
|
if __name__ == "__main__":
|
|
7754
|
-
|
|
7953
|
+
cli()
|
|
@@ -38,6 +38,12 @@ try:
|
|
|
38
38
|
except Exception:
|
|
39
39
|
_keyring = None
|
|
40
40
|
|
|
41
|
+
class BauConfigError(Exception):
|
|
42
|
+
"""Problema no arquivo de configuração que o próprio usuário pode corrigir
|
|
43
|
+
(ex.: JSON malformado). Tratado pela CLI com mensagem clara — sem traceback,
|
|
44
|
+
sem 'contate o Suporte' (é auto-corrigível)."""
|
|
45
|
+
|
|
46
|
+
|
|
41
47
|
DEFAULT_CONFIG = {
|
|
42
48
|
"license_key": None,
|
|
43
49
|
"api_url": "https://begbrokerdev.begcloud.com/ords/bauctx/api/v1",
|
|
@@ -52,6 +58,8 @@ DEFAULT_CONFIG = {
|
|
|
52
58
|
"session_expires_at": None,
|
|
53
59
|
"user_email": None,
|
|
54
60
|
"language": "en_US",
|
|
61
|
+
"profiles": {},
|
|
62
|
+
"default_profile": None,
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
|
|
@@ -63,23 +71,73 @@ def ensure_dir():
|
|
|
63
71
|
os.chmod(BAU_DIR, 0o700)
|
|
64
72
|
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
_in_profile_apply = False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def load_config(apply_profile: bool = True) -> dict:
|
|
78
|
+
"""Load config from ~/.baucli/config.json.
|
|
79
|
+
|
|
80
|
+
When profiles are defined AND ``apply_profile`` is True, overlays the active
|
|
81
|
+
profile's credential (api_url / tenant_code / default_db / api_key) onto the
|
|
82
|
+
flat config so every consumer (CLI + MCP) transparently uses the right
|
|
83
|
+
(tenant, key). Precedence of the active profile: env BAUCLI_PROFILE >
|
|
84
|
+
bau.toml [bau].profile > config.json default_profile. No profiles / no match
|
|
85
|
+
→ identical to the legacy flat behavior. Pass ``apply_profile=False`` to get
|
|
86
|
+
the raw config (used by the profile-management commands themselves).
|
|
87
|
+
"""
|
|
68
88
|
if not CONFIG_FILE.exists():
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
merged = DEFAULT_CONFIG.copy()
|
|
90
|
+
else:
|
|
91
|
+
try:
|
|
92
|
+
with open(CONFIG_FILE) as f:
|
|
93
|
+
cfg = json.load(f)
|
|
94
|
+
except json.JSONDecodeError as e:
|
|
95
|
+
raise BauConfigError(
|
|
96
|
+
f"O arquivo de configuração está com JSON inválido:\n"
|
|
97
|
+
f" {CONFIG_FILE}\n"
|
|
98
|
+
f" → linha {e.lineno}, coluna {e.colno} ({e.msg}).\n"
|
|
99
|
+
f"Dica: todo valor de texto precisa de aspas (ex.: "
|
|
100
|
+
f"\"tenant_code\": \"GRUPO_DASS\"); só null/true/false/números ficam sem.\n"
|
|
101
|
+
f"Valide com: python3 -m json.tool {CONFIG_FILE}"
|
|
102
|
+
) from e
|
|
103
|
+
# Merge with defaults for missing keys
|
|
104
|
+
merged = DEFAULT_CONFIG.copy()
|
|
105
|
+
merged.update(cfg)
|
|
106
|
+
|
|
107
|
+
global _in_profile_apply
|
|
108
|
+
if apply_profile and merged.get("profiles") and not _in_profile_apply:
|
|
109
|
+
try:
|
|
110
|
+
_in_profile_apply = True
|
|
111
|
+
name = resolve_profile_name(merged)
|
|
112
|
+
if name:
|
|
113
|
+
_apply_profile(merged, name)
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
finally:
|
|
117
|
+
_in_profile_apply = False
|
|
75
118
|
return merged
|
|
76
119
|
|
|
77
120
|
|
|
78
121
|
def save_config(cfg: dict):
|
|
79
|
-
"""Save config to ~/.baucli/config.json.
|
|
122
|
+
"""Save config to ~/.baucli/config.json.
|
|
123
|
+
|
|
124
|
+
Never persists a profile overlay into the flat config: strips the transient
|
|
125
|
+
`_active_profile`/`_pre_profile` markers and, for each key the active profile
|
|
126
|
+
overlaid, writes the PRE-profile (raw) value back — unless the caller changed
|
|
127
|
+
it on purpose (then the caller's value wins). Keeps profile secrets out of
|
|
128
|
+
config.json and prevents a load→mutate→save from leaking one tenant's key.
|
|
129
|
+
"""
|
|
80
130
|
ensure_dir()
|
|
131
|
+
to_write = dict(cfg)
|
|
132
|
+
pre = to_write.pop("_pre_profile", None)
|
|
133
|
+
to_write.pop("_active_profile", None)
|
|
134
|
+
if pre:
|
|
135
|
+
for k, ov in pre.items():
|
|
136
|
+
raw, overlaid = ov[0], ov[1]
|
|
137
|
+
if to_write.get(k) == overlaid: # caller didn't change it → restore raw
|
|
138
|
+
to_write[k] = raw
|
|
81
139
|
with open(CONFIG_FILE, "w") as f:
|
|
82
|
-
json.dump(
|
|
140
|
+
json.dump(to_write, f, indent=2, ensure_ascii=False)
|
|
83
141
|
if sys.platform != "win32":
|
|
84
142
|
os.chmod(CONFIG_FILE, 0o600)
|
|
85
143
|
|
|
@@ -148,6 +206,98 @@ def delete_db_password(db_name: str):
|
|
|
148
206
|
save_credentials(creds)
|
|
149
207
|
|
|
150
208
|
|
|
209
|
+
# ── Profiles (Fase 1: 1 BAUCLI_HOME, N perfis por tenant) ──────────────────
|
|
210
|
+
# Um "profile" é uma credencial NOMEADA: {tenant_code, base_url, default_db} no
|
|
211
|
+
# config.json + a api_key no keyring (namespaced). O bau.toml do projeto escolhe
|
|
212
|
+
# o perfil por nome lógico ([bau].profile) — sem segredo no repo. Precedência do
|
|
213
|
+
# perfil ativo: env BAUCLI_PROFILE > [bau].profile > config.json default_profile.
|
|
214
|
+
|
|
215
|
+
def _profile_keyring_user(name: str) -> str:
|
|
216
|
+
return f"__profile__:{name}"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def get_profile_api_key(name: str) -> Optional[str]:
|
|
220
|
+
"""API key do perfil: keyring primeiro, senão o arquivo .credentials."""
|
|
221
|
+
if _keyring:
|
|
222
|
+
try:
|
|
223
|
+
k = _keyring.get_password(KEYRING_SERVICE, _profile_keyring_user(name))
|
|
224
|
+
if k:
|
|
225
|
+
return k
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
return load_credentials().get(_profile_keyring_user(name), {}).get("api_key")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def set_profile_api_key(name: str, api_key: str) -> bool:
|
|
232
|
+
"""Grava a api_key do perfil (keyring se houver, senão .credentials)."""
|
|
233
|
+
if _keyring:
|
|
234
|
+
try:
|
|
235
|
+
_keyring.set_password(KEYRING_SERVICE, _profile_keyring_user(name), api_key)
|
|
236
|
+
return True
|
|
237
|
+
except Exception:
|
|
238
|
+
pass
|
|
239
|
+
creds = load_credentials()
|
|
240
|
+
creds.setdefault(_profile_keyring_user(name), {})["api_key"] = api_key
|
|
241
|
+
save_credentials(creds)
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def delete_profile_api_key(name: str):
|
|
246
|
+
"""Remove a api_key do perfil (keyring + .credentials). Idempotente."""
|
|
247
|
+
if _keyring:
|
|
248
|
+
try:
|
|
249
|
+
_keyring.delete_password(KEYRING_SERVICE, _profile_keyring_user(name))
|
|
250
|
+
except Exception:
|
|
251
|
+
pass
|
|
252
|
+
creds = load_credentials()
|
|
253
|
+
if _profile_keyring_user(name) in creds:
|
|
254
|
+
creds.pop(_profile_keyring_user(name), None)
|
|
255
|
+
save_credentials(creds)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _project_profile() -> Optional[str]:
|
|
259
|
+
"""[bau].profile do bau.toml mais próximo (walk-up). None em qualquer falha."""
|
|
260
|
+
try:
|
|
261
|
+
from .project import find_project
|
|
262
|
+
proj = find_project()
|
|
263
|
+
return proj.profile if proj else None
|
|
264
|
+
except Exception:
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def resolve_profile_name(cfg: dict) -> Optional[str]:
|
|
269
|
+
"""Nome do perfil ativo: env BAUCLI_PROFILE > [bau].profile > default_profile."""
|
|
270
|
+
return (os.environ.get("BAUCLI_PROFILE")
|
|
271
|
+
or _project_profile()
|
|
272
|
+
or cfg.get("default_profile"))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _apply_profile(cfg: dict, name: str) -> dict:
|
|
276
|
+
"""Overlay dos campos do perfil sobre o cfg flat (in-place).
|
|
277
|
+
|
|
278
|
+
Registra em `_pre_profile` {campo: [raw, overlaid]} para o save_config poder
|
|
279
|
+
reverter (nunca persistir a chave do perfil nos campos flat)."""
|
|
280
|
+
prof = (cfg.get("profiles") or {}).get(name)
|
|
281
|
+
if not prof:
|
|
282
|
+
return cfg
|
|
283
|
+
pre = {}
|
|
284
|
+
|
|
285
|
+
def _ov(key, val):
|
|
286
|
+
if val is None:
|
|
287
|
+
return
|
|
288
|
+
pre[key] = [cfg.get(key), val]
|
|
289
|
+
cfg[key] = val
|
|
290
|
+
|
|
291
|
+
_ov("api_url", prof.get("base_url"))
|
|
292
|
+
_ov("tenant_code", prof.get("tenant_code"))
|
|
293
|
+
_ov("default_db", prof.get("default_db"))
|
|
294
|
+
_ov("api_key", get_profile_api_key(name))
|
|
295
|
+
cfg["_active_profile"] = name
|
|
296
|
+
if pre:
|
|
297
|
+
cfg["_pre_profile"] = pre
|
|
298
|
+
return cfg
|
|
299
|
+
|
|
300
|
+
|
|
151
301
|
def get_database(cfg: dict, db_name: Optional[str] = None) -> tuple:
|
|
152
302
|
"""Get database config by name or default.
|
|
153
303
|
|
|
@@ -88,6 +88,14 @@ class Project:
|
|
|
88
88
|
v = self._get("project", "scope")
|
|
89
89
|
return v.upper() if v else None
|
|
90
90
|
|
|
91
|
+
# -- [bau] -------------------------------------------------------------
|
|
92
|
+
@property
|
|
93
|
+
def profile(self) -> str | None:
|
|
94
|
+
"""[bau].profile — nome lógico do perfil de credencial (BAUCLI_HOME).
|
|
95
|
+
Seleciona qual (tenant, chave) o baucli/MCP usam neste projeto, sem
|
|
96
|
+
segredo no repo. Precedência: env BAUCLI_PROFILE > este > default_profile."""
|
|
97
|
+
return self._get("bau", "profile")
|
|
98
|
+
|
|
91
99
|
# -- [connection] ------------------------------------------------------
|
|
92
100
|
@property
|
|
93
101
|
def connection_name(self) -> str | None:
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="baucli",
|
|
5
|
-
version="1.0.
|
|
5
|
+
version="1.0.3",
|
|
6
6
|
description="BAUCLI — AI-powered database documentation CLI for BAU",
|
|
7
7
|
long_description=open("README.md").read(),
|
|
8
8
|
long_description_content_type="text/markdown",
|
|
@@ -28,7 +28,7 @@ setup(
|
|
|
28
28
|
},
|
|
29
29
|
entry_points={
|
|
30
30
|
"console_scripts": [
|
|
31
|
-
"baucli=baucli.cli:
|
|
31
|
+
"baucli=baucli.cli:cli",
|
|
32
32
|
],
|
|
33
33
|
},
|
|
34
34
|
classifiers=[
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|