wtp é uma CLI em Python que cria, serve, testa e remove git worktrees de projetos
PHP, cada um com o seu vhost no Apache:
~/Projects/outserv_agenda-fix-login -> http://fix-login.agenda.localhost/
~/Projects/outserv_agenda-relatorio -> http://relatorio.agenda.localhost/
Um comando cria o worktree, copia o .env reescrevendo o baseURL, copia a config
local que o git ignora, ajusta a permissão do writable/, cria e habilita o vhost e
recarrega o Apache. Outro comando desfaz exatamente isso.
O motivo: vários agentes de IA (Claude Code, Codex) trabalhando no mesmo repositório ao mesmo tempo, cada um com o seu código e o seu servidor, sem pisar nos outros.
- Linux ou WSL2, com Apache 2.4 no layout Debian/Ubuntu (
sites-available,a2ensite,apache2ctl) esystemctl. - PHP-FPM com socket em
/run/php/php<versão>-fpm.sock(pode haver várias versões). - Python 3.12 e uv.
sudosem senha para o seu usuário (veja Sobre o sudo).- Git 2.31 ou mais novo.
Os hosts são *.localhost: o navegador e o curl resolvem esses nomes para
127.0.0.1 sozinhos, sem mexer em /etc/hosts nem no hosts do Windows.
git clone https://github.com/gustavofrdev/worktree-php-manager.git
cd worktree-php-manager
# Coloca o comando wtp em ~/.local/bin, em modo editável
uv tool install -e . --python python3.12
wtp --version
# Cadastra um projeto: rode dentro do checkout dele
cd ~/Projects/meu-projeto
wtp generate-config --print # confere o que seria gravado
wtp generate-config # grava em ~/.config/wtp/config.tomlO generate-config descobre o que der pelo código:
- Framework, por triangulação. Cruza três sinais independentes: o pacote no
composer.json, o arquivo de entrada (artisanouspark) e a chave de URL base no.env. Dois ou três sinais concordando confirmam; um só é palpite; sinais de frameworks diferentes viram tarefa para resolver. Hoje ele conhece CodeIgniter 4 e Laravel, e cada um traz seu perfil: chave da URL base, chave do banco, migrations, pastas graváveis e pastas que precisam existir. - Do vhost que já aponta para o checkout: domínio, versão do PHP e
document_root. - Do git: a branch principal (
origin/HEAD) e os arquivos de config ignorados, que viramcopy_files.
O que ele não conseguir descobrir sai como PARA O AGENTE, com onde procurar e com
qual flag rodar de novo (por exemplo --base-url-key <chave>). A ideia é o agente
ler o código e resolver; só se o código não mostrar é que ele pergunta a você. O
comando nunca sobrescreve um projeto que já está no config. Se preferir escrever à
mão, o molde é o config.example.toml.
O ~/.local/bin precisa estar no PATH.
Cada projeto é uma seção [projects.<nome>]. O nome da seção é o que você passa
na linha de comando.
[defaults]
worktrees_dir = "~/Projects" # worktrees em <worktrees_dir>/<projeto>-<nome>
[projects.outserv_agenda]
main_checkout = "~/Projects/outserv_agenda"
domain = "agenda.localhost" # worktree "x" vira http://x.agenda.localhost/
php_version = "8.1" # entre aspas
env_file = ".env"
base_url_key = "app.baseURL"
main_branch = "main"
document_root = "public"
db_override_key = "database.dbportal.database"
migrations_dir = "app/Database/Migrations"
framework = "codeigniter4"
writable_dirs = ["writable"]
copy_files = ["app/Config/App.php", "app/Config/Constants.php"]
ensure_dirs = []Um projeto Laravel, como o generate-config gera:
[projects.produto-main]
main_checkout = "/home/nk/Projects/produto-main"
domain = "produto-main.localhost"
php_version = "8.4"
base_url_key = "APP_URL"
db_override_key = "DB_DATABASE"
migrations_dir = "database/migrations"
framework = "laravel"
writable_dirs = ["storage", "bootstrap/cache"]
ensure_dirs = ["storage/framework/cache/data", "storage/framework/sessions",
"storage/framework/views", "storage/logs", "bootstrap/cache"]| Chave | Obrigatória | Para que serve |
|---|---|---|
main_checkout |
sim | checkout principal do repositório; é de onde saem o .env e os copy_files |
domain |
sim | domínio base dos hosts dos worktrees |
php_version |
sim | escolhe o socket do PHP-FPM no vhost e o pool em /etc/php/<v>/fpm/pool.d |
env_file, base_url_key |
não | arquivo .env e a chave da URL base que é reescrita, no estilo do próprio arquivo (app.baseURL = 'x' ou APP_URL=x). Vazio: o wtp não mexe na URL e avisa |
main_branch |
não | base das branches novas e referência do +/- no wtp ls |
document_root |
não | pasta pública, relativa ao worktree |
db_override_key |
não | chave do .env que o --db preenche (no CodeIgniter 4, database.<grupo>.database) |
migrations_dir |
não | o wtp avisa quando a branch traz migrations que a main não tem |
framework |
não | codeigniter4, laravel ou vazio; liga cuidados de cada um, como o aviso de cache de config do Laravel |
writable_dirs |
não | pastas em que o usuário do pool do PHP-FPM precisa gravar (o antigo writable_dir, texto, ainda vale) |
copy_files |
não | arquivos ignorados pelo git que o app precisa para subir (credenciais, config local) |
ensure_dirs |
não | pastas ignoradas pelo git que o app precisa encontrar criadas; o wtp cria antes do composer install |
Para descobrir o que colocar em copy_files, rode no checkout principal:
git ls-files --others --ignored --exclude-standard --directorySem esses arquivos, o worktree costuma responder HTTP 500.
wtp new outserv_agenda fix-login --branch feat/fix-login # cria e serve
wtp doctor outserv_agenda fix-login # confere tudo, exit 0 = ok
wtp open outserv_agenda fix-login # abre no navegador (Windows, via WSL)
wtp ls # lista worktrees e estado
wtp rm outserv_agenda fix-login --delete-branch # desfaz tudo| Comando | O que faz |
|---|---|
wtp new <projeto> <nome> [--branch b] [--from main] [--db banco] [--no-composer] |
git fetch, worktree numa branch nova a partir de origin/<from> (ou numa branch local que já existe), .env, copy_files, composer install se faltar vendor/, writable/, vhost e reload |
wtp adopt <projeto> <nome> |
serve um worktree que já existe em <worktrees_dir>/<projeto>-<nome>, sem tocar no git |
wtp ls [projeto] |
nome, branch, commits à frente e atrás da main, se há alteração pendente, URL, se é do wtp |
wtp doctor [projeto] [nome] |
.env e baseURL, copy_files, vhost, PHP-FPM, writable/, HTTP e se os assets vêm do host do worktree |
wtp open <projeto> <nome> |
abre a URL com wslview, explorer.exe ou xdg-open |
wtp rm <projeto> <nome> [--delete-branch] |
recusa se houver alteração não commitada; remove vhost, worktree e, se pedido, a branch |
wtp which [dir] |
diz de qual projeto e de qual worktree é um diretório |
wtp generate-config [dir] [--print] [--base-url-key k] [--name n] |
cadastra o projeto de um checkout no config: descobre o que der pelo código e devolve o resto como tarefa para o agente |
Opções de cada comando (vão depois do subcomando):
--json: resultado em JSON no stdout, mensagens no stderr. É o formato para agentes.--yes(ou-y): não pergunta antes de usar o sudo. Sem terminal e sem--yes, o wtp para antes de mexer em qualquer coisa.
Opções globais (vão antes do subcomando, por exemplo wtp --debug ls):
--debug: mostra o traceback em erros inesperados.--config <arquivo>ou a variávelWTP_CONFIG: usa outro config.
Rodar wtp new de novo com os mesmos argumentos é seguro: ele só completa o que
faltou, por exemplo depois de uma execução interrompida.
A skill em skills/wtp/SKILL.md ensina o fluxo para os
agentes: descobrir o projeto, escolher um nome único, criar com --yes --json,
trabalhar só dentro do worktree, testar com o doctor e remover quando pedido.
Também traz o que nunca fazer, como git stash ou migrations no banco compartilhado.
Instale como skill global com symlinks, assim ela se atualiza junto com o repositório:
mkdir -p ~/.claude/skills ~/.codex/skills
ln -s "$PWD/skills/wtp" ~/.claude/skills/wtp # Claude Code
ln -s "$PWD/skills/wtp" ~/.codex/skills/wtp # CodexDepois disso, basta pedir a um agente uma tarefa num projeto cadastrado ("prepare um
ambiente isolado para corrigir X"); ele encontra a skill e usa o wtp sozinho.
- Só mexe no que é dele. Os vhosts se chamam
wtp-<projeto>-<nome>.confe têm uma marca na primeira linha. O wtp recusa tocar em qualquer outro arquivo, e os scripts que rodam com sudo conferem isso de novo. composer installcom o PHP do projeto. Roda comophp<versão> composer, porque os scripts do composer (opackage:discoverdo Laravel, por exemplo) usam o mesmo PHP. Se ele falhar no meio, o próximowtp newroda de novo em vez de confiar novendor/pela metade.- Um sudo por operação. Instalar ou remover um vhost é uma única chamada de
sudo, que roda oapache2ctl configtestantes do reload. Se o configtest falhar, o arquivo anterior volta (ou o novo sai) e o Apache não é recarregado. - Manifesto. Cada worktree tem um JSON em
~/.local/state/wtp/worktrees/com o que o wtp criou: worktree, branch,.env, arquivos copiados, vhost. Ormlê esse arquivo e desfaz só isso. Num worktree adotado, por exemplo, ele remove o.enve o vhost, mas deixa a pasta e a branch. - Branch. O
--delete-branchsó apaga uma branch criada pelo wtp cujos commits já estão na base de onde ela saiu. Nunca força. - Vários agentes ao mesmo tempo.
git fetch,git worktree adde o reload do Apache são serializados comflock(em~/.local/state/wtp/locks/). writable/. O usuário do pool vem de/etc/php/<v>/fpm/pool.d/*.conf. Se não for o seu usuário, o wtp dá escrita ao grupo do pool, sem seguir symlinks.
- O banco é compartilhado. Todos os worktrees usam o banco do
.envdo checkout principal. Uma migration rodada numa branch muda o esquema para todos. Owtp newavisa quando a branch traz migrations novas; para isolar, use--db <banco>(o banco precisa existir). git stashé compartilhado entre todos os worktrees de um repositório. Evite.- Host errado não dá erro. Um
*.localhostsem vhost próprio cai no primeiro vhost do Apache e responde com o código de outro site. Um HTTP 200 não prova que você está no worktree certo; owtp doctorprova, porque confere de onde vêm os assets. - Login por host. Cada worktree tem cookie de sessão próprio; é preciso logar em cada um.
O wtp chama sudo -n bash -c <script> para instalar e remover vhosts e para ajustar
o writable/. O -n faz o sudo falhar em vez de pedir senha, então o seu usuário
precisa de sudo sem senha. Como o comando é bash, uma regra de sudoers restrita a
ele equivale, na prática, a acesso total; o wtp foi pensado para uma máquina de
desenvolvimento pessoal. Os scripts ficam em wtp/adapters/apache.py e wtp/adapters/permissions.py
para você ler antes de usar.
uv sync
.venv/bin/pytest
.venv/bin/mypy
.venv/bin/ruff check . && .venv/bin/ruff format .Os testes não tocam no Apache nem no git reais: usam classes falsas nomeadas em
tests/fakes.py e tmp_path. Os scripts que vão para o sudo são testados de verdade
em bash, com os comandos do Apache trocados por stubs (tests/adapters/test_sudo_scripts.py).
O código é dividido em camadas, e cada uma só importa as de baixo:
| Pasta | O que tem |
|---|---|
wtp/core/ |
regras puras: config, nomes, validação, perfis de framework, manifesto, .env |
wtp/adapters/ |
efeitos colaterais: git, Apache e sudo, PHP-FPM, composer, HTTP, terminal |
wtp/actions/ |
o que cada comando faz: new/adopt, rm, ls, doctor, which |
wtp/detection/ |
generate-config: triangulação de framework e escrita do config |
wtp/cli/ |
parser, um handler por subcomando e o main |
Os testes em tests/ seguem as mesmas pastas.
As regras de código, as decisões e as armadilhas encontradas estão no
AGENTS.md (o mesmo arquivo que o CLAUDE.md). Leia antes de contribuir.