Ir para o conteúdo
Trilha
Capítulos

Referência

CLI

Os comandos de trilha e suas opções.

trilha new <dir> [--module caminho] [--template blog|app] [--with receitas] [--lang en|pt] [--agents]
    [--trilha-dir ../trilha] [--no-tidy]
trilha gen [--check] [--package nome]
trilha generate page|route|test <url> | component <Nome>
    [--methods GET,POST] [--bind Tipo] [--form Tipo] [--layout arquivo] [--force] [--dir caminho] [--lang en|pt]
trilha dev [--addr :3000]
trilha build [-o bin/<nome>]
trilha export [-o out] [--base /prefixo]
trilha openapi [-o arquivo] [--title T] [--version V] [--server URL] [--check]
trilha routes
trilha check [--json] [--fix]
trilha ctx [--json] [--routes|--types|--all]
trilha audit [--no-vuln]
trilha ui [--force] [--css-only|--js-only]
trilha ui describe [Nome] [--json]
trilha ui patterns [nome] [--json]
trilha agents [--force] [--lang en|pt]
trilha mcp [--write]
trilha mcp --from-routes [--include /api/v1/*]
trilha version
ComandoO que faz
newcria um projeto com go.mod, layout, página inicial, 404, uma rota de API, public/style.css e .gitignore; roda go mod tidy e gen
genvarre app/ e escreve trilha_gen.go; falha com uma linha por convenção violada
generategrava um esqueleto — página, rota de API ou componente — na pasta que a convenção pede
generate crudlê um struct e escreve tudo: lista, criar, editar, excluir, um store — em memória ou em SQL — e um teste
addescreve uma receita do framework no projeto: a trilha de auditoria, chaves de API, uma seção de configurações
devgen + go build + executa o app em uma porta interna + proxy em --addr + recarga por SSE + inspetor de rotas em /_trilha/routes
buildgen + go build -trimpath -ldflags="-s -w" com CGO_ENABLED=0
exportgen + go build + executa com TRILHA_EXPORT para gerar HTML estático
openapiescreve o documento OpenAPI 3.1 das rotas de API (-o - na saída padrão)
routesimprime MÉTODOS PADRÃO ORIGEM para cada rota
islands buildcompila islands/*.ts para public/ com o tsc do projeto; --check compara
checko portão único: gen, gofmt, vet, test, audit e openapi, nesta ordem, parando na primeira falha
ctxo mapa do projeto — rotas, API, tipos, setup — numa leitura só, em Markdown ou JSON
auditchecklist de segurança antes de publicar (veja Segurança); entre as checagens, a rota que guarda módulo segurado por unidade — auth.Grant("editar", auth.ScopeUnit) — e nunca chama o Requirement.In(c, unidade) com a unidade do registro
agentsgrava AGENTS.md, CLAUDE.md, a skill de qualidade e o hook de pre-commit para um agente de código achar as convenções
hooksgrava .githooks/pre-commit e aponta o core.hooksPath para ele
precommitos arquivos staged contra os limites de tamanho: 5.000 tokens por arquivo, 500 linhas por arquivo de código
mcpservidor MCP por stdio, para agente sem shell; somente leitura até passar --write

Os comandos rodam na pasta que contém app/. O caminho de import do projeto vem do go.mod mais próximo, mais a subpasta, então um app pode viver dentro de um módulo maior.

trilha new --template#

O --template escolhe o formato do projeto:

FormatoO que grava
blog (padrão)uma home, uma rota de API e um 404: a menor coisa que roda
appo app de gestão — login, shell, painel com gráficos, uma listagem e um formulário, com testes

O projeto app já nasce verde: compila, o trilha check passa e o go test ./... passa sem nenhuma edição. Toda rota abaixo de app/ está atrás de uma sessão porque o middleware está na raiz da pasta — inclusive as rotas que você escrever amanhã — e o primeiro administrador sai de ADMIN_EMAIL e ADMIN_PASSWORD, que a tela de entrar diz em voz alta, em dev, quando faltam. Nenhuma senha é escrita dentro do projeto.

O que vem no app#

Além do esqueleto, ele pede ao trilha add o login e as telas que toda aplicação interna ganha no primeiro mês:

TelaO que é
/entrar, /saira sessão, da receita login — o template não tem login próprio
/perfila conta de quem pede: nome, senha, e-mail confirmado no endereço novo, sessões
/organizacoesem que organização a sessão está: trocar, criar, desativar, as configurações dela
/admin/auditoriaa trilha de quem fez o quê, com o ui.AuditTable
/admin/chaveschaves de API: emitir, revogar, e a chave mostrada uma vez
/admin/configuma seção de configurações, desenhada do struct que a declara
/admin/usuariosa gente: convidar, papel, desativar, resetar
/admin/permissoesa matriz de permissões, os papéis, e o que quem olha pode fazer

São as receitas, e não uma segunda cópia — uma fonte só, para o template não envelhecer separado do que o trilha add escreve. Isso inclui o login desde a 0.91.0 e, desde a 0.100.0, as telas de users, permissions, profile e tenant: elas são escritas sobre a tabela que a receita login é dona. O app/admin/ exige o papel admin, e quem está logado sem ele recebe 403, e não um redirecionamento para o login: a pessoa é conhecida, só não autorizada, e mandá-la de volta a um login que ela já passou é um laço sem saída. O /perfil e o /organizacoes ficam na raiz, porque são sobre quem pede, e não sobre administrar.

O --with decide com quais receitas um projeto novo começa, em qualquer template:

trilha new minha-app --template app --with ""              # só login: o esqueleto precisa dele
trilha new meu-site --with audit                           # no template blog também
trilha new minha-app --template app --with audit,settings  # escolha a sua — o login acompanha
trilha new loja --with login,blob,mail                     # várias, no template blog

Um --with vazio é uma escolha, e não uma ausência: quem digita isso está pedindo o esqueleto, e recebe o esqueleto. O --template app já pede as suas oito receitas quando --with está ausente; digitar --with — mesmo com uma receita só — substitui essa lista em vez de somar a ela, então manter as oito e acrescentar uma é nomear as nove.

O app/layout.go e o app/middleware.go do template app importam a sessão que a receita login grava, então login entra na lista que o --with deu quando ela não está lá — avisando em stderr, para não passar por algo que foi pedido. Um nome que o --with dá e nenhuma receita responde recusa antes de escrever qualquer coisa, com a lista dos que respondem:

$ trilha new demo --template app --with ""
template "app" needs recipe "login"; adding it automatically
...
$ trilha new demo --with bogus
error: unknown recipe "bogus"; valid recipes: api-keys, approvals, audit, blob, ...

Idioma#

As mensagens da CLI seguem TRILHA_LANG, depois LC_ALL, LC_MESSAGES e LANG: um valor começando com pt (em qualquer caixa) seleciona português; qualquer outro, inclusive variável indefinida, seleciona inglês. As mensagens do runtime, do scanner e do gerador (as que acabam no seu código e nos seus logs) são sempre em inglês.

trilha new --lang en|pt escolhe o idioma dos textos gerados (página inicial, 404, <html lang>); o padrão é o idioma da CLI.

trilha dev#

Além do proxy e da recarga, o supervisor serve o inspetor de rotas em /_trilha/routes: a tabela de rotas em ordem de precedência com layouts e middlewares de cada uma, e uma caixa que responde qual padrão atenderia um caminho. A página é do supervisor, não do app, então ela não existe no binário que o trilha build produz — veja Dev e produção.

O que só o navegador vê#

Dois dos erros que mais doem no começo nunca chegam ao terminal sozinhos: a recusa do CSP que deixa o iframe cinza, e o fragmento que volta como página inteira e acaba dentro de si mesmo — um 200, então nada erra em lugar nenhum. Em dev o navegador tem para quem contar:

⚠ CSP refused frame-src: http://localhost:8080/nota.pdf
  at /documentos/12 — use ui.Preview to show the file, or allow it with Security.CSPExtra{"frame-src": {…}}

⚠ the fragment of /documentos came back as the whole page
  the route answered without looking at c.Fragment(): return only the piece when it is asked for

A política ganha report-uri e Reporting-Endpoints só em dev, apontando para o trilha.ReportPath (/_trilha/csp) — respondido pelo trilha dev e não pela aplicação, porque a graça do relatório é o terminal — e o script do kit só reporta quando o window.__trilha existe, que o script de dev escreve e a produção nunca escreve. No binário que o trilha build produz não há endpoint de relatório, nem diretiva a mais, nem nada para desligar.

O servidor pega um terceiro sozinho: um c.Flash numa resposta que não vai a lugar nenhum. A mensagem apareceria na navegação seguinte, do nada, e o log diz isso na hora, com a rota.

trilha build#

O -o dá o nome do binário; sem ele, bin/<pasta do projeto>. No Windows a saída recebe o .exe que o sistema exige para executá-la, tenha o nome vindo do -o ou do padrão — trilha build -o bin/app escreve bin\app.exe, porque um arquivo chamado app é um que o exec.LookPath recusa executar. Nome que já termina em .exe fica como está, e em Linux e macOS nada muda.

trilha generate#

A convenção é o que custa lembrar: que /blog/{slug} mora em app/blog/slug_/, que uma pasta catch-all termina em __, que um grupo termina em -. O generate recebe a URL e faz a tradução:

trilha generate page /blog/{slug}     # app/blog/slug_/page.go
trilha generate route /api/itens/{id} # app/api/itens/id_/route.go
trilha generate component Aviso       # internal/components/aviso.go

A página e a rota saem compilando, com c.Param já lendo cada parâmetro, e o trilha_gen.go é regerado no fim — a URL responde antes de você abrir o editor. Um componente é uma função que devolve h.Node, então compõe como qualquer outra; --dir põe em outro lugar (internal/icones, por exemplo).

O nome do pacote é o que já está declarado na pasta, quando existe; senão vem do nome da pasta (slug_ → slug, relatorio.csv → relatoriocsv, type → type_).

Um arquivo existente não é sobrescrito sem --force, e o --force não cobre a única recusa que é convenção: uma pasta responde ou uma página ou uma rota, nunca as duas.

O contrato, não só a pasta#

Sem flags o esqueleto é genérico, e o que sobra para escrever — a struct, o Bind, a validação, a resposta, o teste — é justamente onde se erra assinatura. As flags escrevem essa parte:

trilha generate route /api/posts/{id}/comments --methods GET,POST --bind Comment
trilha generate page /contato --form Contact --layout app/layout.go
trilha generate test /api/posts
  • --methods escreve um handler por método, na assinatura que o scanner lê, com o c.Param("id") já pronto para cada parâmetro do caminho.
  • --bind Tipo faz os métodos com corpo chamarem c.BindJSON(&in): devolver esse erro já é o 422 com os campos, não sobra tratamento. Um tipo que o projeto já declara é importado de onde está; um que não existe nasce no pacote da rota com tags json e validate de exemplo. Um nome declarado em dois pacotes é recusa, e a mensagem manda escrever posts.Comment.
  • --form Tipo numa página escreve a ida e volta inteira: trilha.CSRFInput, um ui.Field por campo com a mensagem ao lado, 422 com trilha.FieldErrors quando o Bind recusa e POST → redirect → GET quando ele aceita.
  • --layout <arquivo> grava o layout.go que falta acima da página. Um caminho que não envolve a página é recusado: o scanner nunca o aplicaria, e descobrir isso custa uma ida e volta.
  • generate test <url> escreve o teste ao lado da rota, no pacote dela, com um caso por método que o scanner encontra — e um corpo montado das tags quando dá para ler o tipo que o handler lê. Logo depois de gerar, o trilha check fica verde sem ninguém editar nada.

O --lang en|pt escolhe o idioma dos comentários do esqueleto; identificadores, nomes de campo e mensagens de erro seguem em inglês.

trilha ui#

Grava ou atualiza o kit de interface em public/: ui.theme.css (só criado; é o seu tema), ui.css e ui.js (atualizados; se editados localmente, só com --force). --css-only e --js-only limitam o que é tocado. trilha new roda o mesmo passo. Veja Interface com ui.

trilha ui describe#

Imprime o catálogo do kit: sem argumento, todos os componentes agrupados e resumidos em uma linha cada; com um nome, a assinatura, para que serve, os campos da struct de opções, um exemplo e os símbolos que a documentação cita.

trilha ui describe            # tudo, agrupado
trilha ui describe Field      # um componente
trilha ui describe ui.field   # o mesmo: o prefixo e a caixa são opcionais
trilha ui describe --json     # o catálogo inteiro em JSON

O catálogo é montado a partir dos próprios comentários do kit e viaja dentro do binário, então responde com ou sem projeto por perto e não tem como divergir do código que descreve. Nome desconhecido sai com status diferente de zero e a lista dos nomes mais próximos. O --json é para o agente que escreve a tela: uma chamada e ele sabe o que existe e como cada coisa se chama, em vez de chutar um nome e descobrir na hora de compilar.

trilha ui patterns#

Imprime os padrões de tela do kit — as composições, onde o describe imprime as partes: uma listagem com filtro, um formulário assíncrono, uma caixa de aprovações, um painel, mestre-detalhe, um envio com progresso. Com um nome, o padrão inteiro: os componentes, os dados de que a página precisa, as notas de acessibilidade e um page.go completo de no máximo 60 linhas que compila.

trilha ui patterns                   # uma linha cada
trilha ui patterns list-with-filter  # um deles, inteiro
trilha ui patterns --json            # todos os padrões, no formato que um agente lê

Os snippets são os arquivos de examples/patterns, compilados junto com o repositório; o mesmo dado é o ui.Patterns() em Go e a ferramenta get_pattern no MCP. Veja Padrões.

trilha client#

Gera o cliente Go de uma API que já existe, a partir do documento OpenAPI que essa API publica. É o sentido contrário do trilha openapi, que escreve o documento das suas rotas.

trilha client openapi.json                       # grava internal/api/client.go
trilha client https://api.example.com/openapi.json --out internal/acervo
trilha client openapi.json --check               # falha quando o arquivo está desatualizado
trilha client openapi.json --verbose             # nomeia cada operação sem tipo
trilha client openapi.json --fail-on-untyped     # código de saída, para a CI

Um arquivo só, determinístico, commitado como o trilha_gen.go. Dentro dele: uma struct por esquema de components/schemas, com tags json e — onde o documento diz required, minLength, format: email, enum — as tags validate da Validação, para o mesmo tipo ser a resposta da API e o Bind de um formulário. Um método por operação, agrupado por tag: parâmetro de caminho na assinatura, parâmetros de query numa struct, corpo JSON com o tipo do esquema, e resposta binária como o *http.Response, que passa em stream para o c.Pipe.

O nome do método é o operationId menos a cauda que só repete caminho e método, que é o que a FastAPI escreve: list_documents_api_documents_get vira ListDocuments. O que sobra perde a tag numa posição e em nenhuma outra — a tag escrita exatamente, no começo do nome, em fronteira de palavra —, então config_reset na tag config vira Config.Reset(), e esse corte é uma linha do relatório, porque um nome que o gerador encurtou é um nome que o documento não tem. Em todo o resto o nome fica inteiro: a tag no fim (obter_resumo_dos_idiomas na tag idiomas vira Idiomas.ObterResumoDosIdiomas()), o singular da tag (responder_card na tag cards vira Cards.ResponderCard()) e a tag que é apenas o começo de uma palavra (config em configurar_regra vira Config.ConfigurarRegra()). Documents.ListDocuments() é repetitivo ao lado do grupo e está certo: um nome repetitivo se lê, e um nome amputado — ObterResumoDos(), uma preposição sem objeto — manda quem está escrevendo a tela abrir o client.go.

Todo identificador do arquivo é ASCII, e não há dois iguais. A tag é o rótulo de uma página — verificação de assinaturas é o que aparece no /docs da API —, então a letra acentuada cai para a letra debaixo dela: o grupo é c.DadosPublicosMCP(), e não um nome que alguém precise digitar com acento morto, e o rótulo que o documento escreveu fica no comentário do tipo, que é onde ele é lido. Quando a tag tem o nome de um esquema — a tag auditoria e o esquema Auditoria —, quem cede é o grupo, porque o nome do esquema veio do documento e o do grupo é invenção deste gerador: o tipo vira AuditoriaAPI, enquanto a chamada continua c.Auditoria() e a struct de query continua começando por Auditoria. Cada nome que o comando teve de inventar é uma linha do relatório, em vez de uma notícia que espera o go build de quem chama o cliente.

Um parâmetro de query que o documento declara como T | null — que é como o Pydantic escreve todo competencia: str | None = None — é um ponteiro na struct, e o ponteiro é a resposta para "este vai ser mandado?". nil deixa o parâmetro fora da URL; qualquer outra coisa manda o valor que está atrás dele, inclusive a string vazia, que é como se filtra pelo valor vazio. Um campo que não é ponteiro mantém a regra que já tinha: o valor zero fica de fora, porque ali essa é a única forma de dizer "este não". Uma lista anulável continua sendo uma lista — um parâmetro por elemento, nenhum quando é nil.

Um escalar opcional com default no OpenAPI é ponteiro pela mesma razão. Isso preserva omitido contra zero enviado de propósito (false, 0, ""), que um campo por valor com omitempty não consegue representar. Nomes e comentários gerados continuam UTF-8 válidos quando uma descrição começa por caractere multibyte.

competencia := "2026-09"
page, err := c.Folhas().Listar(ctx, api.FolhasListarParams{
	Page:        1,
	Competencia: &competencia, // ?competencia=2026-09
	Situacao:    nil,          // não aparece na URL
})

Um campo de esquema segue a mesma regra, de qualquer jeito que o documento escreva o anulável — "type": ["string", "null"] no 3.1, nullable: true no 3.0, o anyOf do Pydantic — e segue ainda quando o documento também diz required:

// no contrato: a cor sempre vem, e vale null quando ninguém escolheu uma
"primaria": { "type": ["string", "null"], "description": "Cor primária em hexadecimal, ou nula." }
// Cor primária em hexadecimal, ou nula.
Primaria *string `json:"primaria"`

O ponteiro existe para o nulo ter como ser dito: sem ele, null decodifica para o valor zero e o app não distingue "não configurou" de "configurou vazio" — inócuo numa cor, e uma diferença que some em silêncio num ["integer","null"] cujo zero significa algo. O required não entra na tag validate, porque sobre um ponteiro required quer dizer "não é nil", e "primaria": null é uma resposta que o servidor está certo de dar: uma tag que a chamasse de inválida culparia o servidor por estar correto. As outras regras que o documento afirma — minLength, format, enum — continuam. Fatia, mapa e json.RawMessage ficam como estão: ali o nil já diz nulo, e *[]string não é um tipo que alguém queira escrever.

Um corpo multipart/form-data é lido como o formulário que ele é — através do $ref, porque uma FastAPI nunca escreve esse esquema inline: ela declara um componente Body_<operação> e aponta para ele. Um único campo binário e mais nada continua sendo dois argumentos — file io.Reader, filename string. Qualquer outra forma vira um struct tipado, para nenhum campo do formulário sumir em silêncio:

_, err := c.Certificates().UploadCertificate(ctx, api.CertificatesUploadCertificateForm{
	File:  api.FilePart{Filename: "cert.pfx", Content: f}, // io.Reader: vai em stream
	Senha: "…",                                            // obrigatório, então sempre viaja
})

_, err = c.Documents().ImportDocuments(ctx, api.DocumentsImportDocumentsForm{
	Files:   []api.FilePart{a, b, c}, // três partes com o mesmo nome, nesta ordem
	Comment: "lote de setembro",      // um campo de texto do mesmo formulário
})

Um []FilePart vira várias partes com um nome só, na ordem do slice, que é o que um list[UploadFile] do outro lado lê. Escalar opcional deixado no valor zero não viaja — a mesma regra da query. Nada fica em memória: o corpo é um io.Pipe, então um arquivo maior que a memória do processo atravessa.

New(base, WithHeader(...), WithClient(...), WithResponse(...)) é toda a superfície do construtor: o cliente não guarda credencial nenhuma, e o WithHeader roda por requisição, que é onde o token da sessão entra. O arquivo gerado importa a biblioteca padrão e mais nada — nem a Trilha — então serve também num job, num teste, num binário que não é web.

O WithResponse é o sentido contrário: roda uma vez para cada resposta que chega — inclusive as que viram *Error — e entrega o status e os cabeçalhos dela. É como se lê o que não está no corpo: o Set-Cookie de um login numa API autenticada por cookie, o ETag ou o Location de um POST que cria, o Link de uma paginação, o Retry-After de uma recusa. O gancho recebe o context da chamada, e é isso que o torna seguro num servidor: o coletor mora no context, então cada requisição enche o seu. Um http.CookieJar no http.Client não teria essa propriedade — o jar é do processo, então a credencial de uma pessoa sairia na chamada da seguinte.

type colheitaKey struct{}

var Acervo = api.New("http://api:8801", api.WithResponse(func(ctx context.Context, r *http.Response) {
	if h, ok := ctx.Value(colheitaKey{}).(*[]*http.Cookie); ok {
		*h = r.Cookies()
	}
}))

func entrar(c *trilha.Ctx, in api.Credenciais) error {
	var colhidos []*http.Cookie
	ctx := context.WithValue(c.Context(), colheitaKey{}, &colhidos)
	if _, err := Acervo.Sessoes().Login(ctx, in); err != nil {
		return err
	}
	for _, ck := range colhidos { // a credencial desta requisição, e de mais nenhuma
		c.SetCookie(ck)
	}
	return nil
}

O corpo não faz parte do acordo: ele é da operação, que decodifica — ou de quem chamou, quando a resposta é bytes —, então lê-lo dentro do gancho é tirá-lo de quem pediu. O que o corpo tem, a operação já devolve.

Status fora de 2xx vira um *Error com Status, Body, o Detail tirado do problem+json ou do {"detail": ...} que uma FastAPI escreve, e — quando a resposta é o erro de validação de uma FastAPI, {"detail": [{"loc": [...], "msg": "..."}]} — uma mensagem por campo em Fields. AsError é o errors.As sem a variável, e Fields é um map[string]string, que é o FieldErrors por baixo: o formulário se redesenha com cada mensagem ao lado do seu campo em vez de reparsear o Body à mão em toda página:

if _, err := c.Usuarios().Criar(ctx, in); err != nil {
	if e, ok := api.AsError(err); ok && e.Status == 422 {
		return c.Render(pagina(in, trilha.FieldErrors(e.Fields)))
	}
	return err
}

O loc perde a parte que só diz por onde o valor viajou — body, query, path, header, cookie — então a chave é o nome que o formulário usa: email, itens.0.valor. Um erro fora dessa forma deixa Fields nulo e o Detail exatamente como era.

O que ele não vai adivinhar, ele diz: oneOf, anyOf e esquema sem tipo chegam como json.RawMessage, e cada um é uma linha do relatório que o comando imprime. allOf é achatado numa struct só, um $ref que fecha ciclo vira ponteiro, e operação sem operationId ganha o nome do método e do caminho — também uma linha do relatório.

Operação cuja resposta não tem esquema é a mesma coisa uma ordem de grandeza maior: uma API escrita sem response_model não tipa nada, e um cliente que devolve json.RawMessage cem vezes compila e não ajuda em nada. Por isso o comando termina com a conta:

  97/110 operações sem schema de resposta — devolvidas como json.RawMessage.
  Na FastAPI, declare response_model=... para o cliente tipar a resposta.
  internal/api/client.go gravado, pacote api.

--verbose nomeia cada uma — elas entram na conta em vez do relatório acima, porque noventa e sete linhas dizendo a mesma coisa não são um relatório — e --fail-on-untyped transforma a conta em código de saída para uma CI que quer vê-la chegar a zero. O arquivo é gravado nos dois casos.

A receita Um app na frente de uma API que já existe tem as duas metades lado a lado: a página lendo a API por este cliente, as ilhas lendo pelo Config.Upstreams, um token de sessão para as duas.

trilha vendor#

Põe um módulo JavaScript no repositório. A ilha que precisa de um ajudante — uma biblioteca de template, um gráfico, o runtime de um componente — recebe ele como arquivo em public/vendor/, baixado uma vez e fixado:

trilha vendor preact@10.19.3           # public/vendor/preact.js + uma linha no vendor.lock
trilha vendor htm@3.1.1
trilha vendor                          # o que está fixado
trilha vendor --check                  # confere o hash dos arquivos com o vendor.lock
trilha vendor preact@10.19.3 --from https://cdn.exemplo.com

Ele baixa exatamente o que foi pedido e não resolve nada: não há árvore de dependências, não há node_modules, não há passo de instalação, e um módulo que precisa de resolvedor é o módulo errado para uma ilha. O vendor.lock guarda nome, versão, sha256 e URL, e é commitado — então subir de versão é um diff, e arquivo que mudou sob uma versão que não mudou é o que o --check pega. O trilha audit diz a mesma coisa de um arquivo em public/vendor/ que o vendor.lock não nomeia.

A origem padrão é o https://esm.sh, que serve pacotes npm como módulos ES; --from ou TRILHA_VENDOR_BASE aponta para qualquer outro lugar. Nada disso é dependência do framework: o arquivo é servido como qualquer outro estático, e a ilha importa ele pelo caminho.

trilha islands build#

A ilha que passou de um arquivo pode ser escrita em TypeScript, e o kit continua sem bundler. As fontes ficam em islands/, espelhando public/, com o próprio islands/tsconfig.json:

npm install --save-dev typescript      # o compilador do projeto; o kit não traz nenhum
trilha islands build                   # islands/jogo/ilha.ts → public/jogo/ilha.js
trilha islands build --check           # compila em outro lugar e compara com public/

Ele roda tsc -p islands/tsconfig.json --rootDir islands --outDir public — o compilador do TRILHA_TSC, senão o node_modules/.bin/tsc, senão o do PATH — e não grava nada quando os tipos não fecham. Todo .js e .d.ts que ele grava começa com // Code generated by trilha islands build from islands/<arquivo>.ts. DO NOT EDIT., e o source map anda essa linha e aponta para a fonte a partir de public/. A saída é commitada, porque o go build não tem node: o --check (e o portão islands do trilha check, que só roda no projeto com islands/tsconfig.json) reprova o arquivo diferente, o que falta e o que tem o cabeçalho mas cuja fonte sumiu.

trilha migrate#

Lê um projeto Next.js e grava as duas coisas mecânicas de uma migração: a árvore de pastas do app/, com um arquivo Go por tela, e um relatório de tudo o que não é mecânico.

trilha migrate next ../web --dry-run   # imprime o relatório e não grava nada
trilha migrate next ../web             # grava o app/ e o MIGRATION.md
trilha migrate next ../web --out app --report MIGRATION.md --force
trilha migrate next ../web --out /tmp/x/app     # o relatório vai para /tmp/x/MIGRATION.md
trilha migrate next ../web --actions   # só a tabela de Server Actions, não grava nada

O relatório fica ao lado da árvore, que é o que a primeira linha dele promete: sem --out isso é o ./MIGRATION.md ao lado do ./app, e com --out /tmp/x/app é o /tmp/x/MIGRATION.md. Um --report explícito continua mandando, e o comando termina imprimindo os dois caminhos inteiros.

O que ele grava compila: cada página é uma função Page com título e os parâmetros da rota, cada route.ts vira os handlers que exportava devolvendo 501, e um trilha gen na sequência deixa o projeto verde. Nada é sobrescrito sem --force, então rodar em um projeto que já tem telas acrescenta o que falta e mantém o que está lá — a contagem no fim diz quantos foram gravados e quantos foram mantidos.

O comentário acima de cada função é a parte que importa: diz de qual arquivo veio, quantas linhas tinha, quais hooks usava, quais endpoints chamava e qual dos três formatos a tela provavelmente é — A formulário ou lista sem ilha, B página com uma ilha, C app que é cliente de verdade. É uma sugestão impressa com o motivo e com a linha de onde o motivo saiu — C — live svg (fluxos/FlowCanvas.tsx:12) —, não um veredito; a linha está ali para a sugestão errada ser descartada sem abrir o arquivo. O MIGRATION.md junta isso numa tabela só, mais a lista do que não tem equivalente aqui: estados de carregamento, templates, rotas paralelas e interceptadoras, middleware e rewrites, cada um com a frase que explica o que ocupa o lugar.

A sugestão é sobre a tela, e só sobre ela. Um componente que a página importa conta para ela — um page.tsx de cinquenta linhas na frente de um componente de trezentas não é uma porta de cinquenta linhas —, mas a moldura não é a tela: o que um layout.tsx alcança é listado uma vez, em Dependências globais, e não promove as páginas que envolve. O barril também não: import { DataTable } from "@/components" segue a linha do index.ts que reexporta DataTable e mais nenhuma, porque reexportar não é usar. Sem essas duas regras, um shell que carrega um chat com <svg> classificou todas as telas da aplicação como C — uma ordem de trabalho que manda começar por qualquer lugar.

Um módulo conta pelo que a página importou dele, pelo nome. import { entrarNoPortal, portalPublico } from "@/lib/portalApi" é lido como essas duas funções e como o que elas chamam dentro do arquivo; o portalUpload ao lado é a tela de outra pessoa. Sem isso, um formulário que lê um convite e define uma senha voltava como ilha de upload, e o motivo apontava uma linha que ela nunca executa. O que o módulo faz por conta própria continua contando: corpo de função só roda quando alguém chama, mas um valor de topo — const es = new EventSource("/api/chat") ao lado das funções — é avaliado no instante em que qualquer coisa importa o arquivo, então é de toda tela que o toca. Import default ou de namespace (import * as api) não diz nome nenhum, então ali o módulo conta inteiro e o motivo diz whole module — é a marca a procurar quando uma sugestão parece conservadora demais.

As Server Actions ganham uma tabela própria. Uma página que escreve por "use server" não chama endpoint nenhum, então um relatório organizado por URL descrevia o que cada tela desenha e não dizia nada sobre o que ela muda — numa aplicação de App Router isso é metade do produto, e o A impresso ao lado dessas telas se lê como "não há o que fazer". A seção Server Actions lista cada ação que as telas importam, com o arquivo, a linha e as telas que a chamam; a célula da classe carrega a contagem (A — no island signal · 4 server actions); e o --actions imprime só essa tabela, que é por onde começa quem vai escrever os contratos. Cada uma vira um handler de escrita e um formulário que posta nele — o handler grava e redireciona, a página desenha. Ação não é sinal de ilha: é o oposto de JavaScript no cliente, e não muda a classe.

Um modal é sinal B mesmo quando o componente foi renomeado: identificadores terminados em Dialog, Modal, Drawer, Sheet ou Popover contam, assim como showModal(), confirm(), role="dialog" e aria-modal. O relatório reconhece, portanto, o StartInstanceDialog do projeto em vez de exigir o nome literal do componente do kit.

Pelo mesmo motivo, um <svg> sozinho não é superfície de desenho. Um logo, um ícone, uma seta: quase toda tela tem um, e ler a tag como C colocou quatro das vinte telas de uma migração real na classe mais difícil que existe. O que faz dela C é algo trabalhando nela — ref no elemento, onWheel, requestAnimationFrame, biblioteca de desenho — ou um tratador de ponteiro e uma biblioteca de gráfico, que já classificam sozinhos de todo jeito. Um <canvas> continua contando por si: ninguém põe um ali de enfeite.

Mídia puxa para o outro lado, e a direção do erro é o que importa. Uma tela que grava voz — getUserMedia, MediaRecorder, SpeechRecognition — não tem <canvas> e pode não ter tratador nenhum, e voltava A: um formulário. Mas permissão de dispositivo, stream e um objeto com ciclo de vida próprio são o caso mais puro de "quem trabalha é o browser" que existe, e não há post-redirect-get que ocupe esse lugar: capturar é C. Reproduzir não é capturar: new Audio(, um <audio> ou um <video> é um elemento que o servidor desenha, então é B. Um falso positivo custa uma conferência; um falso negativo faz alguém portar meia tela como formulário antes de achar o microfone.

Todo sinal é procurado com os comentários tirados. Um JSDoc dizendo "a transcrição do SpeechRecognition" num módulo puro fazia C toda tela que o importasse — nem que fosse um tipo; as strings ficam, porque type="file" e role="dialog" são sinais.

As telas em si não são traduzidas. O corpo de uma página é regra de negócio, e máquina chutando isso custa mais para revisar do que para escrever — o guia Do Next.js para a Trilha tem o padrão de React ao lado da linha que toma o lugar dele.

trilha agents#

Grava três arquivos, e só quando é pedido: suporte a agentes de código é opt-in, então trilha new sozinho não deixa nenhum dos dois. trilha new --agents já os cria junto com o projeto.

ArquivoDe quem é
AGENTS.mddo framework: as convenções, os comandos, as dez regras de como o código é escrito aqui e o que não fazer
.claude/skills/trilha-quality/SKILL.mddo framework: o checklist por trás das dez regras — renders, carregamento sob demanda, requisições e cache, consultas e N+1, imagens, fontes e scripts, padrões de projeto, código limpo, reúso, revisão pelo tamanho, OWASP e NIST — com o que procurar. O Claude Code o carrega como a skill trilha-quality; outro agente o lê pelo link no AGENTS.md
CLAUDE.mdseu: três linhas apontando para o AGENTS.md, mais o que este repositório pedir

O AGENTS.md e a skill levam um carimbo com o hash do próprio corpo (o da skill fica no frontmatter), a mesma regra do kit ui. Uma cópia intocada de uma versão anterior é atualizada em silêncio na próxima rodada; uma que você editou só é sobrescrita com --force, e sem ele o comando para e avisa. O CLAUDE.md nunca é sobrescrito.

--lang en|pt escolhe a língua dos dois arquivos e por padrão é a da CLI.

Rode de novo depois de atualizar a CLI: o AGENTS.md nomeia os comandos da versão que o gravou, então uma cópia de uma release anterior continua mandando o agente para comandos que foram substituídos. A sequência inteira para um projeto vindo de versão anterior está na receita de migração.

trilha hooks e trilha precommit#

O trilha agents também grava .githooks/pre-commit e, num repositório git, aponta o core.hooksPath para ele; o trilha hooks faz só isso. O hook mora no repositório, então todo clone tem o mesmo, mas o core.hooksPath é local: quem clona roda trilha hooks uma vez. Um repositório cujo core.hooksPath já aponta para outro lugar fica como está — chame o .githooks/pre-commit do seu próprio hook. O script é do framework, carimbado como o AGENTS.md.

O hook roda o trilha precommit (ou o go run da versão do go.mod, sem a CLI no PATH), que lê os arquivos staged do índice — o que o commit leva, não a árvore de trabalho — e barra o commit quando um passa de um limite:

LimiteVale para
5.000 tokens (bytes/4)todo arquivo que um agente escreve, Markdown e testes inclusive
500 linhascódigo: .go, .js/.ts/.tsx, .sql, .sh, .py e afins; testes (_test.go, .test., .spec., testdata/), docs, CSS e Markdown estão isentos

Não são conferidos: lockfiles (go.sum, package-lock.json, yarn.lock…), vendor/ e node_modules/, binários, arquivos .golden e arquivos gerados — um cabeçalho Code generated ... DO NOT EDIT., os carimbos dos arquivos do próprio framework, ou linguist-generated / linguist-vendored no .gitattributes. A mensagem lista cada arquivo com a contagem e diz como dividir: pelas costuras, em arquivos do mesmo pacote com o nome do que guardam, nunca por contagem de linhas. O --all confere todo arquivo versionado, para medir um projeto antes de ligar o hook.

Lê app/, deduz o documento a partir dos handlers e escreve openapi.json. -o - escreve na saída padrão; --title, --version e --server preenchem o que o código não tem como saber (o padrão é o nome do módulo, 0.0.0 e nenhum servidor). --check compara com o arquivo no disco e sai com 1 quando divergem — a mesma linha que o gen --check é, pelo mesmo motivo:

- run: trilha openapi --check

O documento leva também o que a aplicação manda: cada Emit do webhook com o nome do evento escrito nele vira uma entrada na seção webhooks do OpenAPI 3.1, com o schema do payload e os cabeçalhos da entrega. Um documento só com paths descreve metade de uma integração.

O que é deduzido e as diretivas openapi: estão em APIs.

trilha check#

Oito portões num comando, na ordem que falha mais barato primeiro: gen, islands (só se o projeto tem islands/tsconfig.json; veja o islands build), gofmt, vet, test, audit (sem a varredura de vulnerabilidades, que precisa de rede), openapi (só se o projeto guarda o documento — o openapi.json da raiz e cada cópia dentro de app/, como a que mcp.FromRoutes embute) e i18n (só se o projeto guarda uma pasta de catálogo). Ele para na primeira falha — o que vem depois de uma compilação quebrada não diz nada sobre o projeto — e os passos que não rodaram dizem isso:

✓ gen
✗ gofmt (failed)
    app/blog/page.go: not gofmt'd
    → run gofmt -w (or trilha check --fix)
- vet (not run)
- test (not run)
- audit (not run)
- openapi (not run)
- i18n (not run)

Todo problema vem com o arquivo, a linha e a frase que resolve. O --fix regrava o trilha_gen.go e a formatação antes de julgá-los, e aí o passo reporta fixed. O --json escreve o relatório que uma ferramenta lê, com os mesmos campos:

{
  "ok": false,
  "steps": [{ "tool": "gen", "status": "failed" }],
  "problems": [
    {
      "tool": "gen",
      "file": "app/page.go",
      "line": 3,
      "message": "page.go must export func Page(c *trilha.Ctx) (h.Node, error); found func Render",
      "fix": "rename the function to Page, or delete page.go if this directory is not a page"
    }
  ]
}

Sai com 1 quando algo falhou, então no CI é a linha única:

- run: trilha check

trilha i18n#

As chaves que o c.T usa moram no código; as frases moram em <locale>.json dentro da pasta do catálogo — app/i18n/, ao lado do pacote cujo //go:embed i18n a carrega, ou i18n/ na raiz do projeto; a CLI pega a que existir. Estes dois comandos são como os dois andam juntos — sem build, sem tipos: o código é lido atrás de c.T("chave") dentro de app/ e de internal/.

$ trilha i18n extract                 # todas as chaves que o código pede, em ordem
atendimento.bemvindo
atendimento.protocolos
$ trilha i18n extract --write         # acrescenta as que faltam no idioma padrão
✓ 24 chaves usadas, 3 acrescentadas em app/i18n/pt-BR.json
$ trilha i18n missing ht              # o que falta no crioulo (sai 1 se faltar)
atendimento.protocolos

O --write põe a própria chave como texto, para a tela dizer algo legível enquanto a tradução não vem, e nunca encosta numa mensagem que já está escrita. O idioma padrão é o primeiro do cfg.Locales no app/setup.go, ou en quando o projeto nunca disse.

O passo i18n do trilha check é a mesma leitura: chave usada no código e faltando no idioma padrão reprova — é a tela mostrando atendimento.titulo para alguém — e chave que outro idioma ainda não traduziu é uma linha de aviso, porque tradução sempre chega depois do código que precisa dela e a cadeia de fallback responde enquanto isso.

trilha generate crud#

Entre o trilha new --template app, que traz um CRUD pronto de exemplo, e o trilha generate page, que escreve uma página vazia, mora a tarefa que mais se repete: tenho um struct, quero a tela. Dez vezes no mesmo projeto, cada uma com o mesmo esqueleto e um errinho diferente — uma não confirma o excluir, outra não mostra o erro de validação no campo, outra perde a página ao voltar.

trilha generate crud docs.Tipo --at app/admin/tipos
  + internal/docs/tipo_store.go       TipoStore: List/Get/Create/Update/Delete, e a memória atrás
  + app/admin/tipos/page.go           lista: ui.DataTable, busca, ordenação, paginação, excluir
  + app/admin/tipos/new/page.go       criar: ui.Field por campo, 422 volta com as mensagens
  + app/admin/tipos/id_/page.go       editar: o mesmo formulário, preenchido
  + tipo_crud_test.go                 lista vazia, cria, edita, 422, exclui
  + app/setup.go                      o store, onde as páginas acham

O que sai é código que alguém lê e edita, na forma que o templates/app já provou ser idiomática — e não um runtime que esconde as telas atrás de uma chamada. Ele existe para a décima tela do projeto sair como a primeira.

Como os campos viram tela#

O struct é lido por análise estática, do mesmo jeito que o --form já lê: sem compilador e sem baixar módulo.

  • O ID é a chave, e um struct sem ela é recusado com essa frase. O gerador não escolhe um campo: a chave errada só aparece no primeiro Update, e aí já há cinco telas escritas em cima.
  • CriadoEm/CreatedAt/AtualizadoEm/UpdatedAt e json:"-" ficam fora do formulário. São do sistema, e uma data preenchida à mão é um bug esperando. O store carimba: o "criado em" continua o mesmo num update, porque é a única data que as pessoas vão procurar.
  • validate:"required,max=80" vira required e maxlength no controle, e a mensagem no campo quando falha.
  • O rótulo vem da tag label: quando o struct tem uma, e do nome do campo em título quando não tem. Acento e unidade — "Retenção (anos)" — não são deriváveis de um campo chamado Retencao, e o mesmo rótulo vai para a coluna e para o campo.
  • bool sai como Sim/Não, no idioma pedido: true numa tela é o nome de uma variável, não uma resposta. time.Time que o store carimba vira coluna somente-leitura, escrita pelo ui.Date no idioma e no fuso do app — e fica fora do formulário, porque data que alguém digita é bug esperando.
  • Campo que o CRUD ainda não sabe desenhar é dito em voz alta, uma linha por campo, e o arquivo leva um comentário onde ele entraria. Campo que some em silêncio é campo que alguém procura depois.
  • Os quatro primeiros campos viram as colunas da tabela. Quatro é o que uma tabela mostra antes de começar a rolar para o lado.

Um CRUD debaixo de uma pasta fechada#

O gerador olha o que está acima do destino. Se existe um middleware.go entre o app/ e o --at, as telas estão atrás de alguma coisa, e o teste gerado precisa abrir uma sessão antes da primeira requisição — senão ele responde 401 e se lê como "o gerador está quebrado".

Quando o projeto tem um jeito conhecido de fazer isso — o internal/sessao/sessaotest da receita login, ou o internal/session/sessiontest do template app — o teste chama e segue provando o que ele existe para provar. Quando não tem, o teste vem com um t.Skip nomeando o arquivo que fecha a pasta: um Skip que explica é melhor que um 401 que não explica. Nos dois casos o comando diz qual dos dois aconteceu.

Nada é sobrescrito, e não há --force#

Arquivo que já existe é recusa nomeando-o. É de propósito: gerador que sobrescreve é gerador que ninguém roda duas vezes, e um CRUD é justamente o que se gera depois de já ter editado um.

Rodar de novo vale alguma coisa mesmo assim, porque o motivo de alguém rodar é que o struct ganhou um campo. Com todas as telas no lugar, o comando não escreve nada e diz o que mudou:

$ trilha generate crud docs.Tipo
já gerado — nada foi escrito.

  Descricao não está na lista
    acrescente {Key: "descricao", …} app/tipos/page.go:21
  Descricao não está no formulário
    acrescente ui.Field("descricao", …) app/tipos/new/page.go:34
  Descricao não está no formulário
    acrescente ui.Field("descricao", …) app/tipos/id_/page.go:37

Ele sai com 0: rodar de novo é uma coisa normal de se fazer, e o que ele imprime é informação. Ele não acrescenta o campo por você — escrever dentro de um arquivo que você já editou é sobrescrever com mais passos — ele entrega a linha e o lugar. Com o struct igual, ele diz isso também, em uma linha. Meio CRUD no disco continua sendo a recusa que nomeia o arquivo: esse é outro problema.

A única exceção é o app/setup.go, que o gerador edita em vez de recusar — uma linha, dentro de uma função cuja forma o framework define. Sem ela o CRUD compila e responde 500 na primeira requisição, que é o pior resultado que um gerador pode ter, porque parece que funcionou. O comando nomeia esse arquivo na saída, porque um gerador que mexe no que você não pediu deve essa frase.

O store é interface, com memória atrás e SQL ao lado#

A mesma escolha de todo store deste framework, e aqui ela vale duas vezes: o que sai do gerador roda — a tela abre, o formulário grava, o teste gerado passa — e o banco são os mesmos cinco métodos, que o --store escreve para você.

type TipoStore interface {
	List(ctx context.Context, q TipoQuery) ([]Tipo, int, error)
	Get(ctx context.Context, id string) (Tipo, error)
	Create(ctx context.Context, v Tipo) (Tipo, error)
	Update(ctx context.Context, id string, v Tipo) (Tipo, error)
	Delete(ctx context.Context, id string) error
}

As duas metades dessa assinatura se pagam. O contexto é o da requisição, tirado do c.Context() por toda tela: quando a pessoa fecha a aba, ou o prazo da rota estoura, a consulta para em vez de seguir para ninguém. "Não está lá" é um erro — o trilha.ErrNotFound, que o framework já transforma na página de 404 do app — e não um segundo valor de retorno: o handler o devolve em uma linha, e um banco fora do ar nunca é registrado como uma linha que sumiu. Essa é a mentira que uma tela não pode contar, e um store que só sabe dizer sim ou não não tem como evitá-la.

--store sqlite|postgres#

trilha add store                 # uma vez por projeto: pool, dialeto, migrações
trilha generate crud docs.Tipo --at app/admin/tipos --store sqlite
  + internal/docs/tipo_store_sql.go        a mesma interface em database/sql
  + internal/docs/tipo_store_sql_test.go   os comandos, conferidos sem banco
  + migrations/0002_tipos.sql              a tabela, no dialeto pedido
  ~ app/setup.go                           o store SQL, depois do store.Setup

Ele é escrito contra a receita store, que é para isso que ela existe: store.Sortable e store.OrderBy para a ordenação que veio da URL, store.Paginate para o teto de página, store.Like para a caixa de busca, d.Arg(n) para todo valor, store.NotFound para o sql.ErrNoRows. Nada do que um visitante digita é concatenado em um comando, e o teste gerado afirma exatamente isso, no seu projeto, sem banco nenhum para rodar. Sem a receita, a bandeira é uma recusa que nomeia o trilha add store: um gerador que escreve contra um pacote que não existe entrega um projeto que não compila.

Três coisas que vale saber antes de rodar:

  • O store de memória continua sendo escrito, de propósito: é a implementação de referência das cinco assinaturas, e o store que um teste de unidade seu pega sem subir banco. Um var _ TipoStore = (*TipoSQL)(nil) em tempo de compilação impede que os dois se separem.
  • O Provide vai no fim do Setup, depois do store.Setup(a) — o store.DB não existe antes dele, e um pool guardado nulo falha na primeira requisição em vez de no boot.
  • O teste gerado do CRUD pula sem DATABASE_URL. Subir o app é abrir o banco, e o driver atrás dele é um go get que o seu projeto faz e o framework não pode fazer por ele. O trilha check fica verde do mesmo jeito, e o teste do store — o dos comandos — roda sempre.

Tenant, política e formulário compacto por schema#

As três bandeiras opcionais se combinam, inclusive com --store:

trilha generate crud docs.Tipo --at app/admin/tipos \
  --store postgres --tenant --policy docs --schema
  • --tenant leva c.Actor().Tenant em toda leitura e escrita, acrescenta tenant_id às migrações e cláusulas SQL e escreve middleware com RequireTenant. A receita login precisa existir.
  • --policy docs escreve o middleware da pasta usando a matriz da receita permissions no nível administrar. O valor é o módulo da política, não o nome de um papel.
  • --schema mantém o Bind e o store tipado, mas desenha o formulário a partir de um trilha.Schema compacto com ui.SchemaForm, em vez de um ui.Field explícito por campo.

Quando --tenant e --policy aparecem juntos, o middleware gerado aplica as duas verificações. O comando recusa antes de escrever quando faltam as receitas login ou permissions; adivinhar o pacote de autenticação só produziria código que parece gerado.

trilha add#

Metade do que um app de gestão precisa é um padrão, e não um primitivo: a trilha de quem fez o quê, a tela que emite chaves de API, a seção de configurações que alguém edita em vez de fazer um deploy. No framework isso fica rígido; como documentação, vira trabalho de copiar. O trilha add escreve no projeto, como arquivos que ele passa a possuir.

trilha add              # o que existe, uma linha cada
trilha add audit
trilha add audit --dry-run
trilha add login users audit --lang pt   # várias de uma vez, na ordem; a flag vale em qualquer posição
trilha add --list --json    # para o servidor MCP e o agente do editor
  + internal/auditoria/store.go
  + app/auditoria/page.go
  ~ app/setup.go (uma linha acrescentada)

Rode `trilha dev` e abra /auditoria. Guarde a rota: a trilha diz quem fez o quê, e isso
não é para todo mundo.
Doc: https://trilha.dev/reference/observability

A diferença para o generate é a direção: o generate crud escreve a partir do seu código — um struct vira tela — e o add escreve a partir de uma receita do framework. Os dois terminam em gen, e os dois deixam o trilha check verde.

Vários nomes vão numa chamada só e são aplicados na ordem dada, cada um com seu bloco de saída; --lang e --dry-run valem em qualquer posição. Todo nome é conferido antes do primeiro arquivo ser escrito, então trilha add login typo não escreve nada e aponta o erro.

O preço de cada receita#

Cada linha do trilha add (e cada item do --list --json, como ctx_pack_tokens) traz um preço: quanto custa o trilha ctx --pack <receita>, em tokens estimados, com a receita num projeto mínimo — a leitura que um agente faz para saber o que chegou. É medido, não chutado: um teste instala cada receita e segura o número contra o pack, então uma receita que ganha uma tela ganha o preço no mesmo commit. O add termina com o preço medido no seu projeto, depois do gen — um projeto de verdade, com layouts e as convenções que usa, custa um pouco mais que o mínimo —, então é o número que o trilha ctx --pack vai custar ali, rotulado est. como todo número do estimador comum.

  billing           ~366 tok (est.)   cobrança sem provedor acoplado: …

Rodar duas vezes acrescenta; não recomeça#

Arquivo que já está lá é pulado com um aviso — não é sobrescrito nem vira recusa. Na segunda vez o arquivo é de quem o recebeu, e provavelmente já foi editado.

O único arquivo que uma receita edita é o app/setup.go, e a inserção é marcada:

func Setup(a *trilha.App) error {
	// trilha:add audit
	a.Config().Audit = auditoria.Store
	return nil
}

É essa marca que faz a segunda execução reconhecer a própria linha, em vez de acrescentar uma segunda cópia de um store que ninguém queria duas vezes. Três receitas deixam um bloco de imports, e não três grupos de uma linha — o arquivo é de alguém, e essa pessoa vai lê-lo.

O --dry-run imprime tudo isso e não escreve nada, que é o que se roda antes de deixar um comando mexer num projeto que já tem código.

As receitas#

ReceitaO que escreve
admino backoffice: app/admin/ negado por padrão, feito de users, audit, approvals e search (página)
audito destino que o Config.Audit recebe, e a tela que o lê com o ui.AuditTable
billingcobrança sem provedor acoplado: planos, assinaturas numa máquina de estados fechada, webhook assinado e idempotente, dunning, CSV (página)
blobguardar arquivo que alguém mandou: enviar, listar, servir — com a chave sendo o digest
api-keyso emissor, a tela que cria e revoga, e a chave mostrada uma vez com o ui.SecretOnce
approvalsa fila que espera uma pessoa: abrir, decidir, e o prazo que vence
channel-whatsapprecebendo da Cloud API do WhatsApp: assinatura, mensagem normalizada, dedup e os dois jeitos de responder
connectionsos serviços externos com que este app fala: nome, URL, segredo selado e o botão Testar
loginuma sessão própria: a tabela de gente, a tela de entrar e a saída
mailo arquivo de onde este app manda e-mail: uma função por mensagem, e teste sem rede
notifynotificações por e-mail, webhook ou WhatsApp: preferências, horário silencioso, digesto diário, limite por canal, fila (página)
permissionsa matriz de permissões como dado, a tela que a edita, papéis criados e removidos, quem tem o quê
profilea tela da própria conta: nome, senha, e-mail confirmado no endereço novo, sessões — o id vem da sessão
public-lookupum código mais um segundo fator: a linha do tempo pública de um caso, sem conta
share-linkum link assinado com prazo: acesso a uma coisa, sem conta
settingsuma seção declarada como struct, e a tela que o ui.SettingsForm desenha a partir dela
taskso trabalho que não cabe numa requisição: fila, dedupe, retry, e a tela
tenantorganizações: criar, trocar, ativar ou não, membros, configurações por organização
webhookso que este app avisa para fora: entrega assinada com retry, e a tela dela
usersa tela de gente: convidar, papel, desativar, resetar — escrita sobre a tabela da receita login

Nove delas — admin, approvals, billing, channel-whatsapp, notify, permissions, profile, tenant, users — declaram um Needs: um arquivo que outra receita escreve e que precisa estar lá antes. O trilha add confere isso antes de escrever qualquer coisa, e recusa pelo nome — "rode trilha add login primeiro" — em vez de deixar um projeto com arquivos que não compilam porque o pacote deles nunca foi escrito. É por causa do Needs que vale a pena somar o login antes das outras: é o que as outras esperam.

O login é o que as outras esperam — uma tela que convida alguém ou troca o papel dela é uma tela sobre uma tabela de gente. Ele não escreve senha nenhuma: o usuarios.New lê ADMIN_EMAIL e ADMIN_PASSWORD do ambiente, e sem os dois a tabela sobe vazia e diz isso, porque uma receita que semeasse admin/admin num projeto de verdade seria uma porta deixada aberta por uma ferramenta em que alguém confiou. Ele também escreve o próprio teste — entrar, ser recusado e sair — que é o que o trilha check roda um minuto depois.

A users é a primeira receita escrita em cima de outra: ela acrescenta um arquivo ao pacote que a login abriu, então trilha add users sem ela recusa e diz o que rodar antes, em vez de deixar cinco arquivos que não compilam num projeto que alguém agora tem de limpar. Convidar cria a pessoa sem senha e entrega um link — um administrador que escolhe a senha de alguém é um administrador que sabe a senha de alguém — e o token do link fica guardado como hash, valendo 48 horas e um uso. Desativar não apaga: a trilha de auditoria aponta para quem fez o quê, e uma linha apagada deixa a trilha falando de um id que não existe mais. A página do convite mora fora da pasta guardada, porque quem abre o link ainda não tem sessão.

A permissions é a que tira mais código: com uma matriz, o if papel == "admin" espalhado por dezoito arquivos deixa de ser escrito. Ela escreve a política como dado — módulos, níveis ordenados, o que cada papel tem —, as funções que a leem e a tela do ui.PolicyGrid. A tela que edita a matriz é guardada pela própria matriz, porque uma tela de permissões atrás de um if no papel é uma matriz com uma exceção do lado de fora. Abaixo da grade vêm os papéis: quem tem cada um, um formulário que cria um (nasce com o que o leitor tem, para ser seguro antes de alguém editar a linha dele) e um botão que remove — recusado enquanto alguém ainda o tem, porque tirar o papel de uma pessoa em silêncio é uma pessoa que não consegue fazer nada e não sabe por quê. Por fim, o que eu posso: a matriz do lado de quem olha, módulo por módulo, que é a resposta ao "por que não consigo" antes de virar chamado.

A profile é aquela onde mais se escreve o mesmo bug: o formulário manda o id e o servidor confia. Nessa tela não existe id em formulário nenhum — ele vem da sessão, o único lugar que sabe de quem é a conta. Trocar a senha pede a senha atual mesmo com a sessão aberta (um computador destravado por dois minutos não deve virar uma conta perdida), encerra todas as outras sessões com o LogoutOthers e depois esta, porque entrar de novo é a prova de que a senha nova é a que a pessoa quis. O cartão de sessões lista onde mais a conta está aberta e oferece encerrar as outras. Trocar o e-mail é em dois passos: um link de um uso (c.Link, uma hora) vai para o endereço novo, e só a rota que ele aponta — debaixo de /perfil, para a sessão e o link terem de concordar — muda alguma coisa. Sem ninguém para mandar o link, a tela recusa e diz isso: um e-mail trocado sem confirmação no endereço novo é uma conta entregue a quem digitou.

A tenant é o seletor de organização, e o único lugar sobre organizações: criar uma (quem cria está nela), trocar — recusado onde a pessoa não é membro, e recusado para uma organização desativada —, desativar e ativar de volta (nada é apagado; quem está dentro fica até a sessão acabar, porque a troca é a hora em que a checagem roda), a contagem de membros, e as configurações da organização em que a sessão está: uma seção trilha.Settings por organização, sob a própria chave, desenhada com o ui.SettingsForm, para o fuso da ACME não virar o de todo mundo.

Algumas receitas se amarram quando as duas estão lá. users e permissions: a matriz é dona da lista de papéis, então o app/setup.go ganha usuarios.Papeis = acesso.Papeis e o formulário de convite oferece os papéis da matriz. profile e mail: a confirmação de e-mail sai pelo correio.Confirmacao. Cada par carrega a mesma linha sob o mesmo marcador, condicionada ao arquivo da outra, então a amarração acontece com qualquer uma sendo a segunda, uma vez só — e nunca uma linha que importa um pacote que o projeto não tem.

A webhooks é aquela cujas peças estavam mais longe de serem achadas: o módulo webhook existe desde a 0.65.0 e o examples/blog usa. Ela escreve a lista fechada de eventos que a aplicação avisa, o entregador ligado ao Env do app — http:// só em dev, endereço privado recusado nos dois — e a tela de quem integra com você. Ela não escreve middleware, porque não sabe como este projeto autentica; a última linha do comando manda guardar a pasta, e o arquivo também.

Cada uma vem com memória atrás, para a tela funcionar desde a primeira requisição, e um comentário dizendo onde entra um banco. Cada uma também diz, dentro do arquivo, que a pasta precisa ser guardada: uma trilha de auditoria nomeia pessoas, e uma tela de chaves emite credencial.

São todas as receitas que a #116 listou. A próxima é o padrão que uma aplicação repete e que esta lista não tem.

Por que a CI aplica todas elas#

Uma receita que quebrou em silêncio é pior que nenhuma receita, porque quem a rodou já está com o código dela dentro do projeto. Então o teste não lê os templates: ele cria um projeto, aplica todas as receitas nele e roda o trilha check — compilar, vet, testar, auditar — sem ninguém editar nada.

trilha ctx#

O mapa do projeto numa leitura só: o módulo, se o trilha_gen.go está em dia, cada rota com seu arquivo, métodos, parâmetros, layouts e middlewares, cada operação de API com sua query, corpo e respostas, os tipos que essas operações trocam e o que o app/setup.go provê:

# example.com/loja

- trilha 0.37.0 · 8 routes (6 pages, 2 APIs)
- trilha_gen.go: up to date
- app/setup.go: Setup, Config

## Routes

- `GET /` — app/page.go · layouts: app/layout.go
...

Duas seções a mais aparecem só quando o projeto as tem, então quem não usa nenhuma das duas não paga nada — nem linha, nem token:

- `GET POST /docs` — app/docs/page.go · middleware: app/docs/middleware.go · needs docs:ver · POST needs docs:editar

## Enums

- `doc.situacao` — `rascunho` (Rascunho), `enviado` (Enviado · info) · app/setup.go

O que uma rota exige é lido do middleware.go que declara, e herdado do jeito que a cadeia de middleware é herdada: a pasta mais funda ganha, e um MiddlewarePOST aparece só naquele método. A leitura resolve as duas formas que a documentação ensina — o guarda chamado na hora e a var de pacote que o Middleware devolve —, inclusive um embrulho seu que entrega duas strings ao RequirePolicy, que é o idioma do examples/local-login. O que ela não consegue ler não aparece: um módulo que vem de variável precisaria de um compilador, e um mapa que chuta é pior que um mapa calado sobre uma pasta.

Os enums são as listas de domínio que o trilha.RegisterEnum registrou, casadas com a declaração de onde vieram. Estão aqui porque quem não sabe que uma lista existe inventa uma segunda, e aí duas telas escrevem "em análise" de dois jeitos. As mesmas listas são legíveis em tempo de execução com o trilha.LookupEnum e o trilha.RegisteredEnums.

O padrão é Markdown compacto, para ler. --routes e --types imprimem uma seção sozinha, --all não elide nada (os middlewares por método, todas as respostas de erro, o tipo Problem) e --json escreve o mesmo modelo como documento, ordenado e sem relógio nem caminho absoluto, de modo que duas execuções na mesma árvore dão os mesmos bytes.

A seção de API e os tipos saem da mesma inferência que está por trás do trilha openapi, então o mapa e o documento não têm como divergir. Como o openapi.json, a saída é um documento de máquina e não é traduzida.

trilha gen --check#

Gera em memória, compara com o trilha_gen.go commitado e sai com 1 mostrando as linhas que divergem — uma linha no CI, e uma pasta nova em app/ sem trilha gen depois deixa de ser um 404 que ninguém explica:

- run: trilha gen --check

O trilha check faz essa mesma comparação no primeiro portão, e é por isso que um projeto que usa o check não precisa de uma linha gen --check à parte. O trilha audit faz a comparação como aviso, e ainda compara a versão da CLI com a da biblioteca no go.mod: uma CLI mais nova escreve código que a biblioteca pode ainda não ter, e o erro aparece dentro de código gerado — o pior lugar para procurar.

Arquivo gerado#

trilha_gen.go é determinístico (mesma árvore, mesmos bytes), tem o cabeçalho // Code generated by trilha. DO NOT EDIT., mais a diretiva //go:generate trilha gen (para go generate ./... funcionar sem ninguém precisar saber o nome da ferramenta), e deve ser commitado: go build ./... funciona sem a CLI instalada. Ele define newApp() *trilha.App e main(); se outro arquivo do pacote já tem func main(), o gerador omite o dele (veja App).

Um app dentro de um binário que já existe#

O arquivo gerado adota o pacote que a pasta declara, então um app Trilha pode ser um pacote comum, importável, dentro de um servidor net/http que você já roda:

// internal/crm/crm.go — package crm, escrito à mão
// internal/crm/app/…    — as rotas
// internal/crm/trilha_gen.go — package crm, func NewApp() *trilha.App

mux.Handle("/", crm.NewApp().Handler())

A precedência, do mais explícito ao menos: --package <nome>; o pacote declarado pelos .go escritos à mão da pasta; o pacote declarado por um trilha_gen.go que já esteja lá; main. O terceiro passo é o que faz a bandeira valer uma vez só — o arquivo gerado lembra a escolha, e o trilha gen --check do CI não precisa dela.

Fora do package main o construtor é exportado (NewApp, porque quem chama mora em outro pacote) e nenhum func main() é escrito. O trilha dev e o trilha build recusam um app assim e dizem quem o roda: não há binário aqui, o hospedeiro é que tem um.

Códigos de saída#

0 sucesso; 1 erro de geração, compilação ou execução; 2 uso incorreto.