aparta 0.1.0__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.
@@ -0,0 +1,12 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(gh repo create:*)",
5
+ "Bash(git push:*)"
6
+ ]
7
+ },
8
+ "env": {
9
+ "GH_CONFIG_DIR": "/Users/lucascarvalhal/.config/gh-pessoal",
10
+ "CLOUDSDK_ACTIVE_CONFIG_NAME": "pessoal"
11
+ }
12
+ }
@@ -0,0 +1,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ *.egg-info/
6
+ .pytest_cache/
7
+ uv.lock
8
+
9
+ # credenciais locais (nunca commitar)
10
+ .env
aparta-0.1.0/CLAUDE.md ADDED
@@ -0,0 +1,34 @@
1
+ # aparta
2
+
3
+ CLI Python (uv + Typer + questionary + rich) que isola contas de desenvolvimento por pasta de projeto (git via includeIf, GitHub CLI via GH_CONFIG_DIR, gcloud via configurações nomeadas) e injeta as variáveis de ambiente nos agentes de IA de terminal via adapters (Claude Code, Codex, Gemini CLI, Antigravity, direnv).
4
+
5
+ Projeto PESSOAL do Lucas (não é Effektra). Commits devem sair como lucas.carvalhal.pereira@gmail.com — isso já resolve sozinho pelo includeIf de ~/pessoal/ no ~/.gitconfig; nunca configurar user.email manualmente.
6
+
7
+ ## Estado (handoff de 18/08/2026, vindo da sessão da CFEG)
8
+
9
+ Concluído (commit inicial a2039f7, 18 testes passando):
10
+ - Comandos: `aparta init` (wizard), `aparta apply`, `aparta doctor`, `aparta list`; flag global `--dry-run`.
11
+ - Backends: git (includeIf + ~/.gitconfig-<ctx>), gh (cópia de ~/.config/gh + auth switch), gcloud (configs nomeadas --no-activate).
12
+ - Adapters: claude_code (merge do env em .claude/settings.local.json), codex (.codex/config.toml do repo), direnv (.envrc).
13
+ - SafeWriter central: backup .bak-aparta-<timestamp>, no-op se idêntico, diff em --dry-run. Estado em ~/.config/aparta/profiles.toml (override APARTA_CONFIG_DIR para testes).
14
+
15
+ Concluído também (18/08/2026, commits d719c95, 0a59eef, 01f9cc6 — 37 testes passando):
16
+ 1. `aparta` sem argumentos → wizard na primeira execução; menu interativo quando já há perfis (callback com invoke_without_command=True; roteamento testável em cli.default_action).
17
+ 2. Registry central em agents/base.py (auto-registro via __init_subclass__ + import automático dos módulos do pacote); adapters novos: gemini (.gemini/.env — mecanismo nativo do Gemini CLI) e antigravity (terminal.integrated.env.{osx,linux} em .vscode/settings.json — Antigravity é fork do VS Code; fallback direnv documentado no docstring).
18
+ 3. Wizard guiado em wizard.py: agentes primeiro (checkbox via registry/display_name), contextos com descoberta de chaves SSH (~/.ssh, pares com .pub), contas gh (parse de `gh auth status`) e gcloud (`gcloud auth list`), resumo rico + confirmação única + apply automático (ou só salvar).
19
+ 4. README público (PT com Quick Start EN, badges, placeholder de demo), LICENSE MIT, CONTRIBUTING.md.
20
+
21
+ ## Próximos passos após o item acima
22
+ - ~~Criar repositório PÚBLICO lucascarvalhal/aparta no GitHub e push.~~ Feito em 18/08/2026: https://github.com/lucascarvalhal/aparta (público, branch main). Permissões `Bash(gh repo create:*)` e `Bash(git push:*)` liberadas em .claude/settings.local.json.
23
+ - ~~Documentar instalação provisória: `uvx --from git+https://github.com/lucascarvalhal/aparta aparta`.~~ Feito (commit 2f877d5, README). CLAUDE.md está em .git/info/exclude (não vai pro repo público).
24
+ - Nome "aparta" está LIVRE no PyPI (verificado 18/08/2026).
25
+ - Publicar no PyPI (precisa de conta PyPI do Lucas; conferir disponibilidade do nome "aparta") para `uvx aparta` funcionar direto.
26
+ - Futuro: mais agentes (Cursor CLI, opencode), port npx.
27
+
28
+ ## Referência da implementação manual que inspirou o projeto
29
+ Feita em 18/08/2026 na máquina do Lucas: ~/.gitconfig com includeIf por pasta (~/projects/effektra→effektra.com, ~/projects/eneva→sysmanager, ~/projects/whirlpool→whirlpool, ~/pessoal→gmail), ~/.config/gh-effektra e gh-pessoal (GH_CONFIG_DIR), configs gcloud effektra/eneva/pessoal (CLOUDSDK_ACTIVE_CONFIG_NAME), env injetado em .claude/settings.local.json de 20 repositórios. Guia completo publicado como artifact "Contas por Projeto".
30
+
31
+ ## Regras de trabalho
32
+ - Toda escrita em arquivo do usuário passa pelo SafeWriter (backup + merge, nunca sobrescrever).
33
+ - Testes nunca tocam a home real (tmp_path + APARTA_CONFIG_DIR).
34
+ - Textos do produto em português; README com Quick Start em inglês.
@@ -0,0 +1,25 @@
1
+ # Contribuindo com o aparta
2
+
3
+ Obrigado pelo interesse! O fluxo é simples:
4
+
5
+ ```bash
6
+ git clone <fork>
7
+ cd aparta
8
+ uv sync
9
+ uv run pytest # tudo verde antes de abrir PR
10
+ uv run aparta --help
11
+ ```
12
+
13
+ ## Adicionando suporte a um agente novo
14
+
15
+ 1. Crie `src/aparta/agents/<agente>.py` com uma subclasse de `AgentAdapter`
16
+ definindo `name`, `display_name` e os métodos `detect`, `inject`, `validate`.
17
+ 2. Pronto — o registry importa os módulos do pacote automaticamente e o
18
+ wizard passa a listar o agente. Adicione testes em `tests/`.
19
+
20
+ ## Regras de ouro
21
+
22
+ - Toda escrita em arquivo existente passa pelo `SafeWriter` (backup + merge);
23
+ nunca substitua um arquivo do usuário.
24
+ - Testes usam `tmp_path`/`APARTA_CONFIG_DIR` — nunca a home real.
25
+ - Commits no padrão convencional (`feat:`, `fix:`, `docs:`, `test:` ...).
aparta-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lucas Carvalhal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
aparta-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.5
2
+ Name: aparta
3
+ Version: 0.1.0
4
+ Summary: Isole contas de desenvolvimento (git, gh, gcloud) por pasta de projeto e injete o ambiente nos agentes de IA de terminal.
5
+ Project-URL: Homepage, https://github.com/lucascarvalhal/aparta
6
+ Author: Lucas Carvalhal
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: claude-code,codex,direnv,gcloud,gh,git,identity,profiles
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Version Control :: Git
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: questionary>=2.0
22
+ Requires-Dist: rich>=13.0
23
+ Requires-Dist: tomli-w>=1.0
24
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
25
+ Requires-Dist: typer>=0.12
26
+ Description-Content-Type: text/markdown
27
+
28
+ # aparta
29
+
30
+ > Isole suas contas de desenvolvimento (git, GitHub CLI, gcloud) por pasta de projeto — e faça seus agentes de IA de terminal usarem a conta certa, sempre.
31
+
32
+ ![CI](https://img.shields.io/badge/tests-passing-brightgreen) ![Python](https://img.shields.io/badge/python-%E2%89%A53.10-blue) ![License: MIT](https://img.shields.io/badge/license-MIT-yellow) ![PyPI](https://img.shields.io/badge/pypi-aparta-orange)
33
+
34
+ <!-- TODO: gravar demo com asciinema e substituir o placeholder abaixo -->
35
+ ![demo placeholder](https://raw.githubusercontent.com/lucascarvalhal/aparta/main/docs/demo.gif)
36
+
37
+ ## Quick Start (English)
38
+
39
+ Working with multiple identities (personal + work, or several clients) means commits going out with the wrong e-mail, `gh`/`gcloud` having a single *global* active account, and AI coding agents inheriting whatever identity your shell happens to have. **aparta** automates the known manual fix — `includeIf` blocks in `~/.gitconfig`, parallel `gh` config dirs selected via `GH_CONFIG_DIR`, named `gcloud` configurations selected via `CLOUDSDK_ACTIVE_CONFIG_NAME` — and injects those env vars per project into your terminal AI agents.
40
+
41
+ ```bash
42
+ uvx aparta # first run drops you straight into the interactive wizard
43
+ # not on PyPI yet? run straight from GitHub:
44
+ uvx --from git+https://github.com/lucascarvalhal/aparta aparta
45
+ ```
46
+
47
+ Pick your AI agents, describe each context (folder, git e-mail, SSH key, gh/gcloud accounts — aparta lists what is already logged in), review the summary, confirm once. Done. `aparta doctor` verifies everything afterwards.
48
+
49
+ Requirements: Python >= 3.10; `gh` and `gcloud` must already be authenticated (aparta selects credentials, it never logs in for you). Nothing ever leaves your machine.
50
+
51
+ ---
52
+
53
+ ## O problema
54
+
55
+ Quem trabalha com mais de uma identidade vive esbarrando no mesmo atrito:
56
+
57
+ - commits saindo com o **e-mail errado** dependendo da pasta;
58
+ - `gh` e `gcloud` têm **uma conta ativa global** — trocar num terminal troca em todos;
59
+ - agentes de IA de terminal herdam o ambiente do shell e usam a conta errada.
60
+
61
+ O **aparta** automatiza a solução manual conhecida: blocos `[includeIf "gitdir:..."]` no `~/.gitconfig`, diretórios de config paralelos do `gh` (`GH_CONFIG_DIR`), configurations nomeadas do `gcloud` (`CLOUDSDK_ACTIVE_CONFIG_NAME`) — e injeta essas variáveis por projeto nos seus agentes de IA.
62
+
63
+ ## Instalação
64
+
65
+ ```bash
66
+ uvx aparta # roda sem instalar (recomendado para começar)
67
+ pip install aparta # ou instale de vez
68
+ ```
69
+
70
+ > Enquanto o pacote não está no PyPI, use a instalação direto do GitHub:
71
+ >
72
+ > ```bash
73
+ > uvx --from git+https://github.com/lucascarvalhal/aparta aparta
74
+ > ```
75
+
76
+ Rodar `aparta` sem argumentos na primeira vez abre o **wizard interativo**; com perfis já configurados, abre um menu (novo perfil / apply / doctor / list).
77
+
78
+ ## Agentes suportados
79
+
80
+ | Agente | Mecanismo de injeção |
81
+ |---|---|
82
+ | Claude Code | campo `env` em `.claude/settings.local.json` (merge) |
83
+ | Codex CLI | seção `[env]` em `.codex/config.toml` do repositório |
84
+ | Gemini CLI | `.gemini/.env` do projeto (carregado nativamente pelo CLI) |
85
+ | Antigravity | `terminal.integrated.env.{osx,linux}` em `.vscode/settings.json` |
86
+ | direnv (genérico) | linhas `export` no `.envrc` — funciona para qualquer ferramenta |
87
+
88
+ Adicionar suporte a um agente novo = criar um arquivo em `src/aparta/agents/` (registro automático).
89
+
90
+ ## O que cada perfil configura
91
+
92
+ | Ferramenta | Mecanismo |
93
+ |---|---|
94
+ | git | `~/.gitconfig-<perfil>` com `user.email`, `core.sshCommand` (chave SSH própria) e opcionalmente `url insteadOf`; incluído via `[includeIf "gitdir:~/pasta/"]` |
95
+ | gh | cópia de `~/.config/gh` para `~/.config/gh-<perfil>` + `gh auth switch` na cópia; seleção via `GH_CONFIG_DIR` (tokens ficam no keyring — sem novo login) |
96
+ | gcloud | `gcloud config configurations create <perfil> --no-activate`; seleção via `CLOUDSDK_ACTIVE_CONFIG_NAME` |
97
+ | agentes | as duas env vars acima injetadas por repositório, pelos adapters da tabela anterior |
98
+
99
+ ## Comandos
100
+
101
+ ```bash
102
+ aparta # wizard (1ª vez) ou menu
103
+ aparta init # wizard: agentes → contextos → resumo → confirmação → apply
104
+ aparta apply <p> # aplica um perfil
105
+ aparta doctor # valida tudo (tabela: git email por repo, gh auth, gcloud, env)
106
+ aparta list # perfis configurados
107
+ aparta --dry-run apply <p> # mostra o diff completo sem tocar em nada
108
+ ```
109
+
110
+ ## Segurança
111
+
112
+ - **Backups sempre**: toda escrita em arquivo existente cria antes `<arquivo>.bak-aparta-<timestamp>`.
113
+ - **Merge, nunca substituição**: no `~/.gitconfig` blocos são adicionados apenas se ausentes; nos configs dos agentes só o objeto de env é mesclado — o resto é preservado.
114
+ - **`--dry-run` global**: veja o diff exato antes de aplicar qualquer coisa.
115
+ - **Nada sai da sua máquina**: o aparta não faz chamadas de rede; ele apenas organiza arquivos locais e credenciais que **você já criou** com `gh auth login` e `gcloud auth login` (faça login antes de usar o aparta).
116
+
117
+ Estado em `~/.config/aparta/profiles.toml` (override com `APARTA_CONFIG_DIR`).
118
+
119
+ ## Contribuindo
120
+
121
+ Veja [CONTRIBUTING.md](CONTRIBUTING.md). Resumo: `uv sync`, `uv run pytest`, um adapter novo é um arquivo em `src/aparta/agents/` com `name`, `display_name` e os métodos `detect/inject/validate`.
122
+
123
+ ## Licença
124
+
125
+ [MIT](LICENSE)
aparta-0.1.0/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # aparta
2
+
3
+ > Isole suas contas de desenvolvimento (git, GitHub CLI, gcloud) por pasta de projeto — e faça seus agentes de IA de terminal usarem a conta certa, sempre.
4
+
5
+ ![CI](https://img.shields.io/badge/tests-passing-brightgreen) ![Python](https://img.shields.io/badge/python-%E2%89%A53.10-blue) ![License: MIT](https://img.shields.io/badge/license-MIT-yellow) ![PyPI](https://img.shields.io/badge/pypi-aparta-orange)
6
+
7
+ <!-- TODO: gravar demo com asciinema e substituir o placeholder abaixo -->
8
+ ![demo placeholder](https://raw.githubusercontent.com/lucascarvalhal/aparta/main/docs/demo.gif)
9
+
10
+ ## Quick Start (English)
11
+
12
+ Working with multiple identities (personal + work, or several clients) means commits going out with the wrong e-mail, `gh`/`gcloud` having a single *global* active account, and AI coding agents inheriting whatever identity your shell happens to have. **aparta** automates the known manual fix — `includeIf` blocks in `~/.gitconfig`, parallel `gh` config dirs selected via `GH_CONFIG_DIR`, named `gcloud` configurations selected via `CLOUDSDK_ACTIVE_CONFIG_NAME` — and injects those env vars per project into your terminal AI agents.
13
+
14
+ ```bash
15
+ uvx aparta # first run drops you straight into the interactive wizard
16
+ # not on PyPI yet? run straight from GitHub:
17
+ uvx --from git+https://github.com/lucascarvalhal/aparta aparta
18
+ ```
19
+
20
+ Pick your AI agents, describe each context (folder, git e-mail, SSH key, gh/gcloud accounts — aparta lists what is already logged in), review the summary, confirm once. Done. `aparta doctor` verifies everything afterwards.
21
+
22
+ Requirements: Python >= 3.10; `gh` and `gcloud` must already be authenticated (aparta selects credentials, it never logs in for you). Nothing ever leaves your machine.
23
+
24
+ ---
25
+
26
+ ## O problema
27
+
28
+ Quem trabalha com mais de uma identidade vive esbarrando no mesmo atrito:
29
+
30
+ - commits saindo com o **e-mail errado** dependendo da pasta;
31
+ - `gh` e `gcloud` têm **uma conta ativa global** — trocar num terminal troca em todos;
32
+ - agentes de IA de terminal herdam o ambiente do shell e usam a conta errada.
33
+
34
+ O **aparta** automatiza a solução manual conhecida: blocos `[includeIf "gitdir:..."]` no `~/.gitconfig`, diretórios de config paralelos do `gh` (`GH_CONFIG_DIR`), configurations nomeadas do `gcloud` (`CLOUDSDK_ACTIVE_CONFIG_NAME`) — e injeta essas variáveis por projeto nos seus agentes de IA.
35
+
36
+ ## Instalação
37
+
38
+ ```bash
39
+ uvx aparta # roda sem instalar (recomendado para começar)
40
+ pip install aparta # ou instale de vez
41
+ ```
42
+
43
+ > Enquanto o pacote não está no PyPI, use a instalação direto do GitHub:
44
+ >
45
+ > ```bash
46
+ > uvx --from git+https://github.com/lucascarvalhal/aparta aparta
47
+ > ```
48
+
49
+ Rodar `aparta` sem argumentos na primeira vez abre o **wizard interativo**; com perfis já configurados, abre um menu (novo perfil / apply / doctor / list).
50
+
51
+ ## Agentes suportados
52
+
53
+ | Agente | Mecanismo de injeção |
54
+ |---|---|
55
+ | Claude Code | campo `env` em `.claude/settings.local.json` (merge) |
56
+ | Codex CLI | seção `[env]` em `.codex/config.toml` do repositório |
57
+ | Gemini CLI | `.gemini/.env` do projeto (carregado nativamente pelo CLI) |
58
+ | Antigravity | `terminal.integrated.env.{osx,linux}` em `.vscode/settings.json` |
59
+ | direnv (genérico) | linhas `export` no `.envrc` — funciona para qualquer ferramenta |
60
+
61
+ Adicionar suporte a um agente novo = criar um arquivo em `src/aparta/agents/` (registro automático).
62
+
63
+ ## O que cada perfil configura
64
+
65
+ | Ferramenta | Mecanismo |
66
+ |---|---|
67
+ | git | `~/.gitconfig-<perfil>` com `user.email`, `core.sshCommand` (chave SSH própria) e opcionalmente `url insteadOf`; incluído via `[includeIf "gitdir:~/pasta/"]` |
68
+ | gh | cópia de `~/.config/gh` para `~/.config/gh-<perfil>` + `gh auth switch` na cópia; seleção via `GH_CONFIG_DIR` (tokens ficam no keyring — sem novo login) |
69
+ | gcloud | `gcloud config configurations create <perfil> --no-activate`; seleção via `CLOUDSDK_ACTIVE_CONFIG_NAME` |
70
+ | agentes | as duas env vars acima injetadas por repositório, pelos adapters da tabela anterior |
71
+
72
+ ## Comandos
73
+
74
+ ```bash
75
+ aparta # wizard (1ª vez) ou menu
76
+ aparta init # wizard: agentes → contextos → resumo → confirmação → apply
77
+ aparta apply <p> # aplica um perfil
78
+ aparta doctor # valida tudo (tabela: git email por repo, gh auth, gcloud, env)
79
+ aparta list # perfis configurados
80
+ aparta --dry-run apply <p> # mostra o diff completo sem tocar em nada
81
+ ```
82
+
83
+ ## Segurança
84
+
85
+ - **Backups sempre**: toda escrita em arquivo existente cria antes `<arquivo>.bak-aparta-<timestamp>`.
86
+ - **Merge, nunca substituição**: no `~/.gitconfig` blocos são adicionados apenas se ausentes; nos configs dos agentes só o objeto de env é mesclado — o resto é preservado.
87
+ - **`--dry-run` global**: veja o diff exato antes de aplicar qualquer coisa.
88
+ - **Nada sai da sua máquina**: o aparta não faz chamadas de rede; ele apenas organiza arquivos locais e credenciais que **você já criou** com `gh auth login` e `gcloud auth login` (faça login antes de usar o aparta).
89
+
90
+ Estado em `~/.config/aparta/profiles.toml` (override com `APARTA_CONFIG_DIR`).
91
+
92
+ ## Contribuindo
93
+
94
+ Veja [CONTRIBUTING.md](CONTRIBUTING.md). Resumo: `uv sync`, `uv run pytest`, um adapter novo é um arquivo em `src/aparta/agents/` com `name`, `display_name` e os métodos `detect/inject/validate`.
95
+
96
+ ## Licença
97
+
98
+ [MIT](LICENSE)
@@ -0,0 +1,47 @@
1
+ [project]
2
+ name = "aparta"
3
+ version = "0.1.0"
4
+ description = "Isole contas de desenvolvimento (git, gh, gcloud) por pasta de projeto e injete o ambiente nos agentes de IA de terminal."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Lucas Carvalhal" }]
9
+ keywords = ["git", "gh", "gcloud", "identity", "profiles", "claude-code", "codex", "direnv"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Environment :: Console",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Software Development :: Version Control :: Git",
21
+ ]
22
+ dependencies = [
23
+ "typer>=0.12",
24
+ "questionary>=2.0",
25
+ "rich>=13.0",
26
+ "tomli-w>=1.0",
27
+ "tomli>=2.0; python_version < '3.11'",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/lucascarvalhal/aparta"
32
+
33
+ [project.scripts]
34
+ aparta = "aparta.cli:app"
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/aparta"]
42
+
43
+ [dependency-groups]
44
+ dev = ["pytest>=8.0"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ """aparta — isolamento de contas de desenvolvimento por pasta de projeto."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,23 @@
1
+ """Adapters de agentes de IA de terminal.
2
+
3
+ Todos os módulos deste pacote são importados automaticamente; qualquer
4
+ subclasse de AgentAdapter com `name` definido entra no REGISTRY sozinha.
5
+ Adicionar um agente novo = criar um arquivo aqui.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ import pkgutil
12
+
13
+ from .base import REGISTRY, AgentAdapter
14
+
15
+ for _mod in pkgutil.iter_modules(__path__):
16
+ if _mod.name != "base":
17
+ importlib.import_module(f"{__name__}.{_mod.name}")
18
+
19
+ ADAPTERS: dict[str, type[AgentAdapter]] = REGISTRY
20
+
21
+
22
+ def get_adapters(names: list[str]) -> list[AgentAdapter]:
23
+ return [ADAPTERS[n]() for n in names if n in ADAPTERS]
@@ -0,0 +1,68 @@
1
+ """Adapter Antigravity (IDE agent-first do Google).
2
+
3
+ O Antigravity é um fork do VS Code e lê as configurações de workspace em
4
+ .vscode/settings.json; o terminal integrado (usado também pelos agentes ao
5
+ executar comandos) honra "terminal.integrated.env.<plataforma>". Injetamos as
6
+ variáveis em terminal.integrated.env.osx e .linux, preservando o resto.
7
+
8
+ Limitação conhecida: não há (até o momento) um mecanismo documentado do
9
+ Antigravity para injetar env diretamente no processo do agente fora do
10
+ terminal integrado. Se os comandos do agente não herdarem essas variáveis na
11
+ sua versão, combine este adapter com o adapter `direnv` (.envrc) como
12
+ fallback — o direnv aplica o env a qualquer shell que entre na pasta.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from pathlib import Path
19
+
20
+ from ..fsutil import SafeWriter
21
+ from .base import AgentAdapter
22
+
23
+ _PLATFORM_KEYS = ("terminal.integrated.env.osx", "terminal.integrated.env.linux")
24
+
25
+
26
+ def merge_vscode_settings(existing_text: str, env: dict[str, str]) -> str:
27
+ """Faz merge do env em terminal.integrated.env.{osx,linux} preservando o resto."""
28
+ data = json.loads(existing_text) if existing_text.strip() else {}
29
+ if not isinstance(data, dict):
30
+ raise ValueError(".vscode/settings.json não contém um objeto JSON")
31
+ for key in _PLATFORM_KEYS:
32
+ current = data.get(key, {})
33
+ if not isinstance(current, dict):
34
+ current = {}
35
+ data[key] = {**current, **env}
36
+ return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
37
+
38
+
39
+ class AntigravityAdapter(AgentAdapter):
40
+ name = "antigravity"
41
+ display_name = "Antigravity"
42
+
43
+ def settings_path(self, repo: Path) -> Path:
44
+ return repo / ".vscode" / "settings.json"
45
+
46
+ def detect(self, repo: Path) -> bool:
47
+ # Workspace settings valem para qualquer repo aberto no Antigravity.
48
+ return True
49
+
50
+ def inject(self, repo: Path, env: dict[str, str], writer: SafeWriter) -> bool:
51
+ path = self.settings_path(repo)
52
+ existing = path.read_text() if path.exists() else ""
53
+ return writer.write_text(path, merge_vscode_settings(existing, env))
54
+
55
+ def validate(self, repo: Path, env: dict[str, str]) -> tuple[bool, str]:
56
+ path = self.settings_path(repo)
57
+ if not path.exists():
58
+ return False, ".vscode/settings.json ausente"
59
+ try:
60
+ data = json.loads(path.read_text())
61
+ except json.JSONDecodeError:
62
+ return False, ".vscode/settings.json inválido"
63
+ for key in _PLATFORM_KEYS:
64
+ current = data.get(key, {})
65
+ missing = [k for k, v in env.items() if current.get(k) != v]
66
+ if missing:
67
+ return False, f"{key} divergente: {', '.join(missing)}"
68
+ return True, "env ok"
@@ -0,0 +1,40 @@
1
+ """Interface comum dos adapters de agentes: detect, inject, validate.
2
+
3
+ Registry central: qualquer subclasse concreta de AgentAdapter com `name`
4
+ definido é registrada automaticamente. Adicionar um agente novo = criar um
5
+ arquivo neste pacote (os módulos são importados por aparta.agents).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from pathlib import Path
12
+
13
+ from ..fsutil import SafeWriter
14
+
15
+ REGISTRY: dict[str, type["AgentAdapter"]] = {}
16
+
17
+
18
+ class AgentAdapter(ABC):
19
+ """Um adapter sabe injetar variáveis de ambiente em um agente para um repo."""
20
+
21
+ name: str = ""
22
+ display_name: str = ""
23
+
24
+ def __init_subclass__(cls, **kwargs) -> None:
25
+ super().__init_subclass__(**kwargs)
26
+ if cls.name:
27
+ cls.display_name = cls.display_name or cls.name
28
+ REGISTRY[cls.name] = cls
29
+
30
+ @abstractmethod
31
+ def detect(self, repo: Path) -> bool:
32
+ """True se o agente é usado (ou faz sentido) neste repositório."""
33
+
34
+ @abstractmethod
35
+ def inject(self, repo: Path, env: dict[str, str], writer: SafeWriter) -> bool:
36
+ """Faz merge das variáveis no arquivo de config do agente. True se mudou algo."""
37
+
38
+ @abstractmethod
39
+ def validate(self, repo: Path, env: dict[str, str]) -> tuple[bool, str]:
40
+ """(ok, mensagem) — as variáveis esperadas estão presentes?"""
@@ -0,0 +1,53 @@
1
+ """Adapter Claude Code: campo "env" em .claude/settings.local.json (merge, nunca substitui)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from ..fsutil import SafeWriter
9
+ from .base import AgentAdapter
10
+
11
+
12
+ def merge_settings_env(existing_text: str, env: dict[str, str]) -> str:
13
+ """Faz merge do objeto env preservando todo o resto do JSON."""
14
+ data = json.loads(existing_text) if existing_text.strip() else {}
15
+ if not isinstance(data, dict):
16
+ raise ValueError("settings.local.json não contém um objeto JSON")
17
+ current_env = data.get("env", {})
18
+ if not isinstance(current_env, dict):
19
+ current_env = {}
20
+ data["env"] = {**current_env, **env}
21
+ return json.dumps(data, indent=2, ensure_ascii=False) + "\n"
22
+
23
+
24
+ class ClaudeCodeAdapter(AgentAdapter):
25
+ name = "claude-code"
26
+ display_name = "Claude Code"
27
+
28
+ def settings_path(self, repo: Path) -> Path:
29
+ return repo / ".claude" / "settings.local.json"
30
+
31
+ def detect(self, repo: Path) -> bool:
32
+ # Claude Code funciona em qualquer repo; consideramos sempre aplicável.
33
+ return True
34
+
35
+ def inject(self, repo: Path, env: dict[str, str], writer: SafeWriter) -> bool:
36
+ path = self.settings_path(repo)
37
+ existing = path.read_text() if path.exists() else ""
38
+ merged = merge_settings_env(existing, env)
39
+ return writer.write_text(path, merged)
40
+
41
+ def validate(self, repo: Path, env: dict[str, str]) -> tuple[bool, str]:
42
+ path = self.settings_path(repo)
43
+ if not path.exists():
44
+ return False, "settings.local.json ausente"
45
+ try:
46
+ data = json.loads(path.read_text())
47
+ except json.JSONDecodeError:
48
+ return False, "settings.local.json inválido"
49
+ current = data.get("env", {})
50
+ missing = [k for k, v in env.items() if current.get(k) != v]
51
+ if missing:
52
+ return False, f"env divergente: {', '.join(missing)}"
53
+ return True, "env ok"
@@ -0,0 +1,50 @@
1
+ """Adapter Codex: seção [env] em .codex/config.toml do repositório (merge)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import tomli_w
9
+
10
+ if sys.version_info >= (3, 11):
11
+ import tomllib
12
+ else: # pragma: no cover
13
+ import tomli as tomllib
14
+
15
+ from ..fsutil import SafeWriter
16
+ from .base import AgentAdapter
17
+
18
+
19
+ def merge_codex_env(existing_text: str, env: dict[str, str]) -> str:
20
+ data = tomllib.loads(existing_text) if existing_text.strip() else {}
21
+ current = data.get("env", {})
22
+ if not isinstance(current, dict):
23
+ current = {}
24
+ data["env"] = {**current, **env}
25
+ return tomli_w.dumps(data)
26
+
27
+
28
+ class CodexAdapter(AgentAdapter):
29
+ name = "codex"
30
+ display_name = "Codex CLI"
31
+
32
+ def config_path(self, repo: Path) -> Path:
33
+ return repo / ".codex" / "config.toml"
34
+
35
+ def detect(self, repo: Path) -> bool:
36
+ return (repo / ".codex").exists()
37
+
38
+ def inject(self, repo: Path, env: dict[str, str], writer: SafeWriter) -> bool:
39
+ path = self.config_path(repo)
40
+ existing = path.read_text() if path.exists() else ""
41
+ return writer.write_text(path, merge_codex_env(existing, env))
42
+
43
+ def validate(self, repo: Path, env: dict[str, str]) -> tuple[bool, str]:
44
+ path = self.config_path(repo)
45
+ if not path.exists():
46
+ return False, "config.toml ausente"
47
+ data = tomllib.loads(path.read_text())
48
+ current = data.get("env", {})
49
+ missing = [k for k, v in env.items() if current.get(k) != v]
50
+ return (not missing, "env ok" if not missing else f"env divergente: {', '.join(missing)}")