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.
- aparta-0.1.0/.claude/settings.local.json +12 -0
- aparta-0.1.0/.gitignore +10 -0
- aparta-0.1.0/CLAUDE.md +34 -0
- aparta-0.1.0/CONTRIBUTING.md +25 -0
- aparta-0.1.0/LICENSE +21 -0
- aparta-0.1.0/PKG-INFO +125 -0
- aparta-0.1.0/README.md +98 -0
- aparta-0.1.0/pyproject.toml +47 -0
- aparta-0.1.0/src/aparta/__init__.py +3 -0
- aparta-0.1.0/src/aparta/agents/__init__.py +23 -0
- aparta-0.1.0/src/aparta/agents/antigravity.py +68 -0
- aparta-0.1.0/src/aparta/agents/base.py +40 -0
- aparta-0.1.0/src/aparta/agents/claude_code.py +53 -0
- aparta-0.1.0/src/aparta/agents/codex.py +50 -0
- aparta-0.1.0/src/aparta/agents/direnv.py +49 -0
- aparta-0.1.0/src/aparta/agents/gemini.py +59 -0
- aparta-0.1.0/src/aparta/backends/__init__.py +0 -0
- aparta-0.1.0/src/aparta/backends/gcloud.py +50 -0
- aparta-0.1.0/src/aparta/backends/gh.py +59 -0
- aparta-0.1.0/src/aparta/backends/git.py +84 -0
- aparta-0.1.0/src/aparta/cli.py +191 -0
- aparta-0.1.0/src/aparta/doctor.py +116 -0
- aparta-0.1.0/src/aparta/fsutil.py +63 -0
- aparta-0.1.0/src/aparta/profiles.py +87 -0
- aparta-0.1.0/src/aparta/wizard.py +262 -0
- aparta-0.1.0/tests/test_agents_merge.py +67 -0
- aparta-0.1.0/tests/test_dry_run.py +49 -0
- aparta-0.1.0/tests/test_gemini_antigravity.py +70 -0
- aparta-0.1.0/tests/test_git_merge.py +72 -0
- aparta-0.1.0/tests/test_no_args_flow.py +62 -0
- aparta-0.1.0/tests/test_profiles.py +53 -0
- aparta-0.1.0/tests/test_registry.py +45 -0
- aparta-0.1.0/tests/test_wizard_helpers.py +39 -0
aparta-0.1.0/.gitignore
ADDED
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
|
+
   
|
|
33
|
+
|
|
34
|
+
<!-- TODO: gravar demo com asciinema e substituir o placeholder abaixo -->
|
|
35
|
+

|
|
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
|
+
   
|
|
6
|
+
|
|
7
|
+
<!-- TODO: gravar demo com asciinema e substituir o placeholder abaixo -->
|
|
8
|
+

|
|
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,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)}")
|