# CLI

Todos os comandos do ./nimbly: setup, builds, testes, manutenção, agendador e estatísticas.

O Nimbly traz uma CLI em `core/cli/nimbly.php`. A forma preferida de chamá-la é o lançador `./nimbly` na raiz:

```bash
./nimbly init
./nimbly up
./nimbly deps
./nimbly build
./nimbly watch
./nimbly test:run
./nimbly user:create
./nimbly module:install <name>
```

`init`, `deps`, `build` e `watch` usam npm internamente. Os comandos que usam PHP rodam no PHP do host quando ele existe e recorrem ao Docker automaticamente. Também há variantes explícitas:

```bash
php core/cli/nimbly.php system:setup   # require host PHP
./nimbly --docker system:setup         # force Docker Compose
```

Chamadas diretas equivalentes:

```bash
php core/cli/nimbly.php system:setup
php core/cli/nimbly.php user:create
php core/cli/nimbly.php module:install <name>
php core/cli/nimbly.php routes:sync
php core/cli/nimbly.php index:rebuild [resource]
php core/cli/nimbly.php data:cache:clear
php core/cli/nimbly.php user:email-index:rebuild
php core/cli/nimbly.php system:repair
php core/cli/nimbly.php help
```

## Consulta à documentação

Os comandos de documentação só leem, e só leem o `NIMBLY.md` local. Use-os para carregar a menor seção útil da referência enquanto trabalha:

```bash
./nimbly docs:list
./nimbly docs:section "Template Syntax"
./nimbly docs:section "Template Syntax > Route templates"
./nimbly docs:search "router_accept"
```

A busca não diferencia maiúsculas de minúsculas. Um caminho de seção pode indicar um título aninhado; um nome de título sem caminho precisa ser único. Seções ausentes ou ambíguas devolvem um erro.

## Comandos

`./nimbly help` lista os comandos. `./nimbly <command> --help` (ou `-h`) mostra o que um comando faz e o que recebe, sem executá-lo.

### `system:setup`
Prepara o sistema local. Pode ser executado de novo com segurança: arquivos e registros existentes nunca são sobrescritos.

O que ele faz:
- Cria o `.env` com `APP_ENV`, um `PEPPER` gerado e `BASE_PATH` apenas em instalações numa subpasta
- Gera o `.htaccess` a partir de um modelo
- Gera o `.user.ini` a partir de um modelo, para as configurações de runtime do PHP-FPM
- Cria a estrutura de pastas de `ext/` (`data/`, `static/`, `lib/`, `modules/`, `tpl/`, `uri/`, pastas temporárias)
- Dá a um site sem nenhuma página uma primeira página inicial (`ext/uri/index.tpl` e `main.tpl`): uma página curta e editável que mostra onde ficam páginas, conteúdo e dados, com um recurso de exemplo (`ext/data/messages/.meta`) e um formulário que grava nele (`ext/uri/message.json`), fechado para visitantes. Você pode mudá-la ou removê-la; um site que já tem uma rota não é tocado
- Cria `.config/site`, o recurso `.content`, os registros de `.routes` do core e as funções padrão (`admin`, `editor`)
- Cria o recurso `users` e um primeiro usuário admin

Perguntas: **Site name**, **Admin email**, **Admin password**. Etapas já concluídas são puladas em silêncio.

**Sem perguntas (CI/CD):** defina variáveis de ambiente para pular todas as perguntas:

| Variável | Descrição |
|---|---|
| `APP_ENV` | Nome do ambiente (`prod`, `stage`, `dev`; padrão `dev`) |
| `BASE_PATH` | Caminho base opcional na URL para instalações numa subpasta, por exemplo `/mysite/`; omita em instalações na raiz |
| `PEPPER` | Pepper de criptografia; defina para reutilizar um valor existente, omita para gerar automaticamente |
| `EXT_REPO` | URL do remoto Git do repositório ext (gravada em `ext/readme.md`) |
| `SITE_NAME` | Nome do site, gravado em `.config/site` |
| `SITE_LANGUAGE` | Idioma principal validado, com duas letras minúsculas, para um site novo (padrão `en`) |
| `ADMIN_EMAIL` | E-mail do primeiro usuário admin |
| `ADMIN_PASSWORD` | Senha do primeiro usuário admin (mínimo de 8 caracteres) |

```bash
SITE_NAME="My Site" ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=secret123 ./nimbly system:setup
```

Sem terminal (ao iniciar um contêiner, a partir de um agendador), a configuração nunca espera por uma resposta. Um `SITE_NAME` ausente vira `My Nimbly Site`. Um site sem usuários precisa de `ADMIN_EMAIL` e `ADMIN_PASSWORD`: quando falta um deles, a configuração para com uma linha que o nomeia e código de saída 1, para que um contêiner de produção num volume vazio só inicie quando ambos estiverem definidos.

### `user:create`
Cria mais uma conta de usuário. Pergunta de forma interativa o e-mail, a função e a senha. As funções disponíveis vêm de `ext/data/roles/`. O usuário sempre recebe também a função `user`. Exige que a configuração já tenha sido executada.

### `module:install`
Executa o script `.install.inc` de um módulo. Procura primeiro em `ext/modules/` e depois em `core/modules/`. Exige que a configuração já tenha sido executada.

```bash
./nimbly module:install event
```

### `routes:sync`
Percorre `ext/uri/**/route.inc` e cria em `.routes` os registros que faltam para rotas dinâmicas. Use depois de acrescentar uma nova pasta de rota com segmentos `(param)`.

```bash
./nimbly routes:sync
```

### `index:rebuild`
Reconstrói as entradas de índice de um recurso indexado. Use depois de acrescentar `index` ao `.meta` de um recurso existente, ou depois de importar registros fora do fluxo normal da API.

```bash
php core/cli/nimbly.php index:rebuild             # interactive: lists indexed resources, prompts for choice
php core/cli/nimbly.php index:rebuild articles    # direct: reindex the 'articles' resource
```

O comando percorre todos os registros do recurso e cria os arquivos de índice que faltam. Pode ser executado várias vezes; entradas existentes não são tocadas.

### `data:cache:clear`
Descarta os resultados de consulta em cache de todos os recursos; a próxima leitura os reconstrói. Use depois de editar à mão, no próprio lugar, um arquivo de registro (veja Cache de dados). Salvar pelo site, pela API, pelo Git ou por `ext:sync` não exige limpar.

```bash
php core/cli/nimbly.php data:cache:clear
```

### `user:email-index:rebuild`
Acrescenta ao recurso `users` os metadados de busca por e-mail e reconstrói o índice de e-mails.

```bash
php core/cli/nimbly.php user:email-index:rebuild
php core/cli/nimbly.php user:email-index:rebuild --yes
```

Essa migração faz parte do modelo em que o UUID do registro de usuário é fixo e o e-mail é um campo de busca indexado. Ela acrescenta `email` a `users/.meta.index`, acrescenta `email` a `users/.meta.unique` quando não há e-mails duplicados e cria entradas de índice para todos os usuários existentes, inclusive usuários antigos cujo UUID era originalmente `md5(email)`.

Se encontrar e-mails duplicados, o comando os informa e só acrescenta `unique=email` depois que forem resolvidos. O índice de e-mails é reconstruído mesmo assim, para que o login continue rápido sempre que possível.

### `test:run`
Executa a suíte E2E. O comando cria dados de teste temporários com `test:setup`, instala o navegador Chromium do Playwright se necessário, executa o Playwright com `core/tests/playwright.config.js` e depois remove os dados temporários com `test:teardown`.

```bash
./nimbly test:run
./nimbly --docker test:run
```

Use `--docker` na CI ou sempre que os arquivos de dados do PHP precisarem ser gravados pelo mesmo modelo de usuário do Docker que o runtime web. Os testes do core não chamam provedores externos pagos. Substitua o provedor por um mock e teste de forma previsível a requisição, o tratamento da resposta, a validação e o comportamento da interface do Nimbly. A disponibilidade de um provedor é uma verificação operacional de integração, não parte da suíte de testes do core.

### `test:setup` / `test:teardown`
Comandos de baixo nível usados por `./nimbly test:run`. `test:setup` cria a função `test`, o usuário `test@nimbly.dev`, o recurso `test-records` e registros iniciais. `test:teardown` remove esses registros e as entradas temporárias de rota `.test`.

### `system:repair`

Iguala os arquivos gerados e os registros do sistema ao core atual. Uma atualização do core nunca os reescreve, então um site mais antigo pode não ter o que uma instalação nova recebe de `system:setup`. Cada verificação informa primeiro; nada muda sem `--yes`.

| Verificação | Reparado quando |
|-------|---------------|
| `.htaccess` da raiz | Ele difere de `core/cli/setup/htaccess.tpl`. O pepper e a base de reescrita vêm do arquivo existente; o arquivo anterior é mantido como `.htaccess.before-repair`. |
| Proteções de pasta | Um `.htaccess` em `ext/` ou `core/` falta ou ainda está na forma antiga `<files *.*>`. Uma proteção com conteúdo próprio é informada e mantida. |
| `ext/.gitignore` | Ele não ignora o cache de miniaturas, a fila de jobs, o estado do agendador ou as conversas e execuções dos agentes. |
| `.config/.meta` | Ele não tem `upsert`, de modo que as configurações de página não podem ser salvas para uma página que ainda não as tem. |
| Rotas do core | Uma rota do core acrescentada depois da configuração do site não está registrada. |
| Índice de e-mails dos usuários | `users/.meta` não indexa `email`. `unique` só é acrescentado quando não há dois usuários com o mesmo endereço. |

```bash
./nimbly system:repair         # report only
./nimbly system:repair --yes   # apply
```

As pastas ignoradas mudam sozinhas em cada servidor (o estado do agendador a cada minuto), então acompanhar uma delas no Git faz o `ext:sync` entrar em conflito e pode deixar o `ext/` do servidor preso num rebase. A regra de ignorar não tira do Git uma pasta que já é acompanhada:

```bash
git -C ext rm -r --cached data/.state
```

### `schedule:run`
Executa os comandos agendados que estão na hora para um projeto. Em servidores VPS manuais com vários projetos Nimbly, não crie uma entrada de cron por projeto. Use o orquestrador de agendamento no nível do servidor e deixe-o chamar o `schedule:run` de cada projeto registrado, um depois do outro.

Os projetos podem definir agendamentos em `ext/cli/`. O agendador escolhe os arquivos nesta ordem:

1. `SCHEDULE_FILE` do `.env` ou do ambiente do processo, como caminho de arquivo explícito.
2. `ext/cli/schedule.<env>.inc`, em que `<env>` vem de `SCHEDULE_ENV`, `APP_ENV` ou `NIMBLY_ENV`.
3. `ext/cli/schedule.inc`.
4. `core/cli/schedule.inc`.

Os nomes de ambiente são normalizados: `production` → `prod`, `staging` → `stage`, `development` e `local` → `dev`.

Use arquivos de agendamento por ambiente quando o staging ou o desenvolvimento precisam rodar jobs em segundo plano, mas não tarefas exclusivas de produção, como lembretes para membros.

As tarefas agendadas podem usar `every_minutes` para intervalos maiores que um minuto:

```php
[
    'id' => 'uptime-poll',
    'command' => 'uptime:poll',
    'every_minutes' => 2,
]
```

O valor precisa ser um inteiro positivo. Uma tarefa nova está na hora imediatamente; depois de rodar, volta a estar na hora quando o relógio entra no próximo intervalo. Os agendamentos existentes com `every`, `daily_at` e o `timezone` opcional continuam suportados.

O estado das últimas execuções do agendador fica em `ext/data/.state/schedule`. Instalações existentes com o estado antigo em `ext/data/.config/schedule` são lidas como alternativa de migração até o agendador gravar o novo arquivo de estado.

### Manutenção obrigatória e expiração de sessões

Toda chamada de `schedule:run` inclui as tarefas do core antes de carregar o agendamento da aplicação. Os agendamentos da aplicação acrescentam tarefas; não podem remover nem reagendar estas chamadas obrigatórias:

| ID da tarefa | Comando | Intervalo |
|---|---|---|
| `sessions-prune` | `sessions:prune` | 30 minutos |
| `jobs-run` | `jobs:run 10` | 1 minuto |
| `jobs-prune` | `jobs:prune --days=30` | 24 horas |
| `history-prune` | `history:prune --days=90` | 24 horas |
| `agent-prune` | `agent:prune --days=30` | 24 horas |
| `stats-rollup` | `stats:rollup` | 1 hora |

Entradas existentes da aplicação com esses IDs ou comandos são deduplicadas. Substitua uma **implementação**, não o agendamento dela: defina o mesmo comando em `ext/cli/commands.php`, seguindo o contrato normal do registro de comandos. O core confia no status de saída da implementação e não inspeciona o trabalho interno. O registro do host lista projetos, não escolhas de tarefas; flags `enabled` antigas não impedem mais projetos registrados. Remova do registro os deployments desativados. Antes de registrar um projeto antigo sem agendamento, revise os jobs na fila e o agendamento da aplicação: ativar o processamento pode entregar e-mails pendentes ou executar outro trabalho da fila.

`./nimbly schedule:status` informa se as tarefas obrigatórias foram concluídas com sucesso e sai com erro quando uma tarefa falhou, nunca teve sucesso ou está atrasada mais de dois intervalos. Uma execução bem-sucedida com fila vazia é saudável. A faixa **Needs attention** do admin e a auditoria do host leem esse estado cada uma por conta própria, então também detectam um agendador parado. Verifique esse comando depois de todo deployment; só instalar o cron não prova que a manutenção funciona. A auditoria do host instalada traz, ao lado do coletor, um auxiliar `nimbly-maintenance.php` pertencente ao root.

Em instalações VPS, use o orquestrador existente do host e registre **todos** os projetos ativos. As imagens de contêiner já chamam o agendador do projeto a cada minuto. Outras plataformas de nuvem precisam chamar a mesma CLI a cada minuto, com um único agendador que tenha acesso ao `ext/data` persistente e à configuração de runtime da aplicação. Monitore `schedule:status` de forma independente desse executor. Não é preciso nenhum daemon extra de limpeza de sessões nem registro por projeto.

As funções de sessão, incluindo a inspeção e a limpeza de arquivos, ficam em `core/lib/session.php`. O padrão são limites contínuos de inatividade: anônimo 4 horas, usuários autenticados 3 dias, admins 7 dias. **Não há limite absoluto de idade do login**: atividade regular mantém viva uma sessão autenticada. Trocar o ID da sessão não muda a identidade nem desconecta o usuário. Leituras anônimas e verificações de permissão não criam sessões; formulários e outros recursos com estado podem criar. Os cookies de usuários autenticados persistem depois de reiniciar o navegador; os cookies anônimos continuam sendo cookies de sessão do navegador.

Todo cookie que o core define tem as mesmas opções (`session_cookie_options()`): o caminho base do site, `HttpOnly`, `SameSite=Lax` e `Secure` em https. Isso vale para o cookie de sessão, para a chave de formulário de um visitante sem sessão (`key`) e para uma variável guardada com `[#set session ...#]` quando não há sessão; esses dois últimos duram 30 dias (`persistent_cookie_options()`). Scripts na página não conseguem lê-los.

Os projetos podem configurar as durações em segundos em `session` no `.config/site`:

```json
{
  "session": {
    "anonymous": 14400,
    "authenticated": 259200,
    "roles": { "admin": 604800, "editor": 259200 }
  }
}
```

Vence a exceção mais longa entre as funções atribuídas que correspondem; senão vale o padrão de usuários autenticados. Trocar de função temporariamente não muda o registro das funções atribuídas. Login, cookies e limpeza seguem a mesma regra. O core desativa a coleta de lixo do PHP disparada por requisição na sua pasta privada de sessões.

`./nimbly sessions:prune --dry-run` informa totais sem mudar sessões. A execução normal lê os arquivos um a um, pula arquivos bloqueados e links simbólicos e remove só sessões expiradas. Arquivos malformados, objetos serializados ou referências não suportados e arquivos maiores que 1 MiB são informados como falha em vez de excluídos. O leitor aceita as codificações `php` e `php_serialize` com valores e arrays. A ação separada **Sign out everyone** remove de propósito todas as sessões. Uma opção "lembrar de mim" com duração maior ficou para depois.

### Estatísticas de requisições (`stats:*`)

O core registra toda requisição que chega a `index.php`, sem cookies nem JavaScript. O registro fica ligado para `APP_ENV=prod` e `stage`; defina `STATS_ENABLED=1` ou `0` para mudar isso. Os arquivos servidos diretamente pelo `.htaccess` (`ext/static`, `core/static`, miniaturas em `/img/`) e as sondagens `.php` bloqueadas nunca chegam ao PHP e não são registrados.

Uma função de encerramento acrescenta uma linha JSON por requisição a `ext/data/.tmp/stats/running-YYYY-MM-DD.log`: hora UTC, método, host, caminho com consulta (relativo à URL base), status, tipo de conteúdo, duração, IP, user agent, referenciador, a primeira tag de `Accept-Language` e uma indicação de login. Acima de `STATS_MAX_DAY_BYTES` (padrão 200 MB), um dia passa a só contar as requisições seguintes como excedente.

`stats:rollup` (manutenção obrigatória, a cada hora) arquiva os dias encerrados em `ext/data/.stats/<env>/`, que o `ext:sync` faz commit:

| Caminho | Conteúdo |
|---|---|
| `raw/YYYY/YYYY-MM-DD.log.gz.enc` | o dia completo, compactado com gzip e criptografado com AES-256-GCM |
| `raw/YYYY/YYYY-MM-DD.apache.log.gz.enc` | complemento opcional do Apache, de `apache-YYYY-MM-DD.log` |
| `months/YYYY-MM.json` | contagens completas por dia, por status, rota, tipo de visitante, dispositivo, navegador, idioma, bot, referenciador e caminho, mais `hours` (requisições, visualizações de página, visitantes, bots, scanners por hora UTC) |
| `years/YYYY.json` | as mesmas contagens somadas por mês, mais um total do ano |

Todas as datas, nomes de arquivo e viradas de dia estão em UTC. As interfaces convertem para o fuso horário de quem vê reagrupando `hours`; um visitante conta na hora da primeira visualização de página do dia UTC. Aproxime de ano para mês, para dia, até as requisições brutas; semanas e totais de todo o período são derivados desses arquivos. Acima do nível de dia, `visitors` vira `visits`, a soma dos visitantes únicos diários. Os arquivos JSON guardam só contagens, nunca rankings ou endereços IP; as interfaces derivam as listas de destaque a partir deles. A classificação (humano, editor, bot, scanner, suspeito, monitor, ferramenta) acontece no rollup, então `stats:rollup --rebuild` recalcula todos os meses a partir de `raw/` quando as regras melhoram.

`stats:import-apache` lê linhas do log de acesso do Apache (`combined` ou `vhost_combined`) pela entrada padrão, de modo que só a leitura do log precisa de root:

```bash
zcat -f /var/log/apache2/site-access.log.* | sudo -u www-data ./nimbly stats:import-apache --mode=full --before=2026-09-25T02:02:00Z
zcat -f /var/log/apache2/site-access.log.1 | sudo -u www-data ./nimbly stats:import-apache
```

`--mode=full` preenche todas as linhas antes de `--before` (dias antes do início do registro). O padrão `--mode=enrich` acrescenta só o que nunca chega ao PHP: arquivos servidos de `ext/static` ou `core/static`, miniaturas em `/img/` e sondagens `.php` bloqueadas. As linhas são gravadas em `apache-YYYY-MM-DD.log` (datas UTC) e arquivadas no rollup seguinte; uma marca em `.tmp/stats/apache.watermark` pula as linhas já importadas. Use `--base=/subdir/` em instalações numa subpasta e `--host=` quando `SITE_URL` não está definido. As linhas do Apache não têm `Accept-Language`, tipo de conteúdo nem estado de login: os tipos de conteúdo são deduzidos do caminho e os editores contam como visitantes.

`STATS_KEY` (64 caracteres hexadecimais, gerada por `system:setup`) criptografa o arquivo bruto. Faça backup dela: sem ela, o histórico bruto não pode ser lido. Enquanto ela faltar, o rollup falha e mantém os logs em andamento. `stats:raw YYYY-MM-DD [--apache]` mostra um dia descriptografado.

### `scheduler:*`
Executa os agendadores de vários projetos a partir de um único cron no nível do servidor. Os arquivos de agendamento continuam no próprio projeto, em `ext/cli/`; o registro do servidor é a única fonte sobre quais projetos são orquestrados.

O orquestrador usa por padrão estes arquivos, gerenciados no servidor:

| Caminho | Finalidade |
|---|---|
| `/usr/local/bin/nimbly-scheduler-orchestrator` | Wrapper executado pelo cron |
| `/etc/nimbly/scheduler-projects.json` | Projetos registrados e padrões |
| `/etc/cron.d/nimbly-scheduler` | Entrada de cron do sistema |
| `/var/log/nimbly-scheduler.log` | Log do agendador |
| `/etc/logrotate.d/nimbly-scheduler` | Rotação semanal do log, oito semanas guardadas |

Instale uma vez por servidor, a partir de qualquer checkout atual do core do Nimbly:

```bash
sudo php core/cli/nimbly.php scheduler:install
sudo php core/cli/nimbly.php scheduler:project:add site-name /var/www/site-name
sudo php core/cli/nimbly.php scheduler:cron:install --user=www-data
```

Comandos úteis de gerenciamento:

```bash
php core/cli/nimbly.php scheduler:project:list
sudo -u www-data php core/cli/nimbly.php scheduler:run --dry-run
sudo -u www-data /usr/local/bin/nimbly-scheduler-orchestrator
php core/cli/nimbly.php scheduler:cron:status
```

`scheduler:run` pega um lock global curto e dispara dois workers por projeto registrado: um executa `jobs:run 10` para entregar a fila sem demora, e o outro executa o `schedule:run` do projeto. Os workers registram a conclusão e o código de saída no log do agendador do host. Assim, uma tarefa longa de uma aplicação não atrasa as tarefas de minuto de outro projeto nem os próprios alertas na fila. Os executores de jobs usam um lock por projeto, para que o worker direto da fila e a tarefa obrigatória `jobs-run` não peguem o mesmo job ao mesmo tempo. O agendador do projeto mantém o próprio lock, para que os ticks seguintes do cron não dupliquem uma execução longa. Valores antigos de `default_delay_after_seconds` no registro são ignorados.

`scheduler:run` sempre dispara imediatamente os workers de todos os projetos registrados (nunca espera antes de iniciar), para que um projeto lento não atrase o disparo no tick seguinte do cron. Em vez disso, a concorrência é limitada dentro de cada `scheduler:worker`: antes de executar o `jobs:run`/`schedule:run` real do projeto, um worker espera por uma vaga num semáforo compartilhado e a libera ao terminar. Isso evita que uma frota grande jogue todos os workers num host de uma CPU no mesmo instante. O limite padrão é de 5 workers simultâneos no host inteiro e pode ser configurado com `NIMBLY_SCHEDULER_MAX_CONCURRENCY`; os arquivos de lock das vagas ficam em `NIMBLY_SCHEDULER_SEMAPHORE_DIR` (padrão: a pasta temporária do sistema).
