dovetail_cli

Um ponto de entrada para o toolkit. O que existia lá fora eram peças soltas — window_manager, tray_manager, hotkey_manager, auto_updater, Fastforge — cada uma de um autor, cada uma com a sua configuração. O que o Tauri tem e o Flutter não tinha é o arquivo único e o CLI que lê ele.

ship: a esteira inteira, a partir da config

dovetail ship --dry-run
→ build macos
→ sign darwin-universal
→ bundle darwin-universal
→ sign dmg darwin-universal
→ archive darwin-universal
→ release 4.2.0

O plano é dado, não execução — é por isso que o --dry-run sai de graça e o comando é testável sem tocar em toolchain nenhuma. Os testes do plano não chamam o Flutter, o wixl nem o codesign. E o que o plano já sabe que o último passo vai recusar — update.public-key ausente, notarize: true sem identidade ou credenciais exportadas — ele recusa antes do build, no --dry-run inclusive.

Três decisões que ele toma e vale conhecer. No macOS as arquiteturas declaradas viram um bundle universal, publicado sob darwin-universal — a chave que o releaseFor resolve quando um cliente pede darwin-aarch64. No Linux e no Windows é um pacote por arquitetura, porque um .deb carrega uma só.

E no macOS são duas assinaturas: o .app antes de empacotar, porque um dmg que embrulha um .app não assinado não fica assinado por assinar o dmg depois; e o .dmg depois de montado, porque é ele que o usuário baixa e o Gatekeeper avalia — e é ele que se notariza. No Windows e no Linux o que se assina é o instalador, então o bundle vem primeiro.

O nome de cada artefato não é adivinhado: o plano pergunta ao bundler que vai escrevê-lo. Enquanto ele adivinhava, o dmg saía app_1.0.0_universal.dmg e o manifesto apontava para app_1.0.0.dmg — um arquivo que não existia.

Alvos de outro sistema são deixados de lado. Um host cuja parte da matriz mora em outro lugar sai 0 sem fazer nada, porque é um runner sem trabalho nesta rodada e não um erro.

Escrever isso pegou cinco defeitos meus, e vale dizer como cada um apareceu. Dois pelo --dry-run, antes de qualquer coisa rodar: o macOS empacotava duas vezes para o mesmo arquivo de saída, e a arquitetura ia na grafia do fio (aarch64) onde o bundle só aceita a da Apple (arm64). Três só rodando de verdade no app deste repositório: o app-dir apontava para o diretório acima do .app, a assinatura vinha depois do dmg, e o nome previsto não era o nome que o bundler escrevia.

Instalar nesta máquina

tool/build_release.sh --install

Compila e deixa o binário em ~/.local/bin, avisando se o diretório não estiver no PATH. Um caminho diferente vai como segundo argumento.

$ dovetail --version
dovetail 0.1.0 (macos-arm64, dart 3.12.2)

Nada disso pede notarização nem passa pelo Gatekeeper: um binário compilado localmente não chega em quarentena. Isso só volta a importar quando o arquivo vier pela rede.

Distribuição: um binário, sem fonte

O CLI compila para um executável nativo autocontido. tool/build_release.sh produz o binário, o tarball e o SHA256, e recusa publicar se o binário não reportar a versão que o pubspec.yaml declara.

tool/build_release.sh dist

O que o binário carrega compilado: dovetail_bundler, dovetail_signer, dovetail_updater, dovetail_process_runner e o próprio CLI — como código de máquina. Não há fonte Dart recuperável dele. Literais de string ficam, como em qualquer compilação AOT: nomes de ferramenta, mensagens de erro e os fragmentos de template .wxs e .nsi. Esses fragmentos são a saída do comando de qualquer jeito, então não são segredo que o binário guarde.

tool/dovetail.rb é a fórmula Homebrew. Ela existe porque metade do problema de instalar não é o binário: é o minisign, o msitools, o osslsigncode e o rpm que o CLI invoca. A fórmula os declara como dependência e o caveats nomeia os dois que ela deliberadamente não instala — o flutter, que você já tem, e o makensis, que só faz falta se você quiser o instalador NSIS além do MSI.

O que o binário não cobre. Os pacotes de runtime — dovetail_platform_channel, dovetail_shortcut_channel, dovetail_updater e o guarda-chuva dovetail — são compilados dentro do app de quem consome. O Dart não tem formato binário para dependência de pub: um pacote sem lib/*.dart não pode ser importado. Quem usar a janela, a bandeja, o painel ou o atalho precisa da fonte desses pacotes; quem usar só a esteira de build, empacotamento, assinatura e publicação não precisa de nenhuma.

O canal de release e o instalador

O runtime viaja num SDK versionado, servido por um canal:

<base>/latest                                           → a versão mais nova
<base>/<versão>/dovetail-sdk-<versão>-<os>-<arch>.tar.gz
<base>/<versão>/dovetail-sdk-<versão>-<os>-<arch>.tar.gz.sha256

O caminho curl|sh é o tool/sdk/install.sh:

curl -fsSL https://<host>/install.sh | sh

E o mesmo trabalho existe de dentro do binário, para quem já o tem:

dovetail self-install --base-url https://<host>   # instala o SDK da versão do binário
dovetail self-update  --base-url https://<host>   # troca para o latest do canal

A base vem de --base-url ou de DOVETAIL_INSTALL_URL; sem os dois o comando recusa nomeando os dois — um instalador que adivinha o host instala o que o host decidir. O download passa por TLS (o fetcher do updater recusa http) e o tarball é conferido contra o .sha256 publicado antes de um byte tocar o disco. O self-update instala a versão nova ao lado da antiga e troca o binário: sdk/<antiga> fica no lugar, e um app cujo pubspec_overrides.yaml aponta para ela continua resolvendo — é por isso que o SDK é versionado por diretório e não sobrescrito. Um latest igual ou mais velho é saída 0 sem tocar nada, nunca um downgrade. O doctor reporta o que vê: versão instalada, e quando ela discorda da do binário, qual comando fecha a distância.

dovetail.yaml

dovetail init lê o projeto e escreve o arquivo; ele não pergunta nada que consiga descobrir sozinho.

dovetail init

Ele deduz os alvos dos diretórios macos/, windows/ e linux/ que existem, o identificador do AppInfo.xcconfig (nunca do RunnerTests, que é o único PRODUCT_BUNDLE_IDENTIFIER de um project.pbxproj recém-criado), e o nome do pubspec.yaml. Quando as plataformas declaram identificadores diferentes ele delata os dois em vez de escolher em silêncio — a instância única e o deep link se ancoram nesse nome, então divergir ali é um defeito.

Se o dovetail.yaml já existe, o comando recusa; --force sobrescreve, descartando o que foi configurado à mão — por isso não é o padrão.

identifier: com.example.demo
name: Demo

targets:
  - darwin-aarch64
  - linux-x86_64

update:
  key: keys/update.key
  password-env: DOVETAIL_UPDATE_KEY_PASSWORD
  base-url: https://cdn.example.com/releases
  public-key: |
    untrusted comment: minisign public key D18395BE8A6B994E
    RWROmWuKvpWD0RErEh4kcn0sjuu4dQYX5MERE9dNGuImxQXHzNuRYLVP
  manifest: dist/latest.json

sign:
  macos:
    identity-env: DOVETAIL_MACOS_IDENTITY
    notarize: false
  windows:
    certificate-env: DOVETAIL_WINDOWS_CERTIFICATE
    timestamp-url: http://timestamp.digicert.com

Não há campo de versão. Ela mora no pubspec.yaml e é lida de lá, sem o +build. Duplicá-la só criaria dois lugares para discordar.

Nenhuma identidade é escrita aqui, só o nome da variável que a carrega — por isso o arquivo pode ser commitado.

Um targets é validado contra o mesmo vocabulário do manifesto de update. Uma chave que o cliente nunca pede é um release que ninguém enxerga, e darwin-arm64 é exatamente esse erro: arm64 é como Apple e WiX escrevem, aarch64 é como o protocolo escreve.

O runtime pelo SDK instalado

Fora do monorepo o runtime não tem path: ../ para onde apontar: ele vem de um SDK instalado em ~/.dovetail/sdk/<versão>/packages (ou $DOVETAIL_HOME, quando definido).

dovetail init --sdk

A flag escreve um pubspec_overrides.yaml no app, cada pacote do runtime apontado para a cópia do SDK — o mecanismo nativo do Dart para resolver fora do pubspec, o mesmo que o Flutter SDK usa para entregar os pacotes dele. A versão preferida é a do próprio binário; quando ela não está instalada, a mais recente serve. Sem SDK instalado o comando recusa antes de escrever nada, e a mensagem nomeia o instalador. Os caminhos são locais, então o arquivo de overrides não é para versionar. O modelo completo — instalação, ciclo de versão e o bridge — está em docs/instalador.md.

upgrade: o ciclo de versão do consumidor

O self-update troca o SDK instalado sem mover os apps: cada app carrega um pubspec_overrides.yaml cujos path apontam para sdk/<versão>/packages, e a versão antiga fica no disco para quem ainda a usa. O upgrade é o comando que re-aponta esse arquivo para o SDK preferido:

dovetail upgrade

Sem um pubspec.yaml no diretório ele recusa, porque o override pertence a um projeto. O doctor reporta a discordância quando vê um app cujo override aponta para uma versão mais velha que a instalada — e o upgrade é quem fecha a distância.

update: o ciclo de versão num comando só

O self-update troca o SDK e o upgrade re-aponta o app — dois comandos que andam juntos em todo ciclo de versão. O update colapsa os dois: troca o SDK para o latest do canal e, quando o app é dado, re-aponta o pubspec_overrides.yaml dele:

dovetail update --base-url https://<host>                # só o SDK
dovetail update --base-url https://<host> --root app     # SDK + app

Sem --root só o SDK muda, e re-apontar o app fica para um upgrade posterior. Com --root apontando para um diretório sem pubspec.yaml ele pula o re-apontar com um aviso, em vez de quebrar — o override pertence a um projeto. Já estar no latest é saída 0 sem tocar nada, nunca um downgrade, e sem SDK nenhum no disco para onde o app possa apontar, ele recusa nomeando o self-install.

O serviço privilegiado

Um VPN não conecta sem o helper privilegiado, e nenhum pacote o instalava: a unit systemd, a política polkit e os scripts de manutenção existiam e o comando que empacota não os alcançava. Agora eles vêm da config.

service:
  name: demo-helper.service
  description: Privileged helper for the tunnel
  exec-start: /usr/lib/demo/demo-helper
  capabilities: [CAP_NET_ADMIN]
  runtime-directory: demo
  purge-paths: [/var/lib/demo]
  polkit:
    action: com.example.demo.manage
    vendor: Example Ltda
    description: Manage the connection
    message: Authentication is required to change the connection

As capacidades entram no CapabilityBoundingSet e no AmbientCapabilities — uma ambiente fora do bounding set é descartada em silêncio, e o helper sobe sem o privilégio de que precisa. A ação polkit tem que estar dentro do namespace do identifier, porque o polkit nomeia o arquivo pelo namespace e ignora ação declarada fora dele.

Com a config no lugar

dovetail doctor
dovetail release --artifact "darwin-aarch64=dist/Demo.dmg"

O doctor sem --target percorre todos os alvos declarados, um relatório por alvo, e sai 1 se alguma ferramenta está presente mas quebrada — pior que ausente, porque um build reporta sucesso que não ganhou.

O release tira do arquivo a versão, a chave, a variável de senha, o destino do manifesto e a URL de cada artefato (<base-url>/<versão>/<arquivo>), e recusa uma chave de plataforma que não esteja em targets. A forma longa platformKey=url=path continua valendo para quando a URL não segue o padrão — inclusive com query string, que a forma antiga truncava no primeiro =.

Instalação

dependencies:
  dovetail_cli:
    path: ../dovetail_cli
dart run bin/dovetail.dart --help

A esteira, na ordem em que roda

init      lê o projeto e escreve o dovetail.yaml que os outros leem
new       cria o projeto do zero, já ligado ao dovetail
ship      a esteira inteira, a partir da config
doctor    esta máquina consegue construir para esse par de SO e arquitetura?
dev       a ponte nunca fica para trás — vigia e regenera
bridge    gera o plugin ffiPlugin que fala com o núcleo Rust do produto
build     compila o que este host pode compilar, e recusa alto o que não pode
icon      de um PNG saem .ico, .icns e os PNGs do tema do Linux
bundle    nsis | msi | dmg | deb | rpm — nunca assina nada
sign      codesign e notarização, ou Authenticode — nunca empacota nada
keygen    gera o par minisign e imprime a chave pública
release   assina cada artefato e escreve o manifesto que aponta para eles
inspect   o que o arquivo é, não o que o nome dele diz
manifest  o manifesto sozinho, quando a assinatura já existe
probe     pergunta ao endpoint se ele serve o que o cliente lê
self-install  instala o runtime do SDK ao lado do binário, do canal de release
self-update   troca o SDK para o latest do canal, mantendo a versão que os apps apontam
update        troca o SDK para o latest e re-aponta o app, num comando só
upgrade       re-aponta o pubspec_overrides.yaml do app para o SDK instalado

A separação entre bundle e sign não é organizacional: assinatura acontece depois do build e nunca dentro dele. Um build que assina é um build que não se pode reproduzir sem a chave.

doctor pergunta se a ferramenta funciona

Não se ela está no PATH. Cada sonda entrega ao binário real uma entrada trivial real e confere a saída real: o makensis compila um script de quatro linhas e tem que produzir o .exe; o ditto copia um arquivo e tem que produzir a cópia; o codesign lê o /bin/ls.

Sem --target ele percorre todos os alvos do dovetail.yaml. Com, ele pergunta por um só:

dovetail doctor --target windows --arch arm64
target: windows/arm64
missing  aarch64-pc-windows-msvc  rustup target add aarch64-pc-windows-msvc
broken   makensis  libc++abi: terminating due to uncaught exception of type std::bad_alloc
missing  wix  compiles the MSI, when one is asked for
broken   signtool  You must specify a key with which to sign.

Uma ferramenta presente e quebrada sai como broken, com o texto de erro dela, e o comando termina em 1 — porque um build que reporta sucesso que não mereceu é pior do que um build que falha.

Isso não é hipotético: nesta máquina o makensis do Homebrew aborta com std::bad_alloc num script de quatro linhas, e existe um signtool que não é o do Windows no PATH. Uma sonda de presença chama os dois de prontos.

bundle não precisa de Windows para empacotar Windows

O WiX faz P/Invoke em msi.dll, que é a biblioteca do Windows Installer — por isso a documentação do Tauri diz que um MSI só pode ser criado no Windows. Isso vale para o WiX, não para o formato: o wixl, do msitools, escreve o banco MSI diretamente.

brew install msitools
dovetail bundle --target windows --arch x86_64 --windows-format msi \
  --upgrade-code 3F2504E0-4F89-11D3-9A0C-0305E82C3301 ...

MsiBackend.forHost escolhe: wix no Windows, wixl em qualquer outro lugar. O dialeto muda junto — o wixl lê o esquema v3 (<Product>), e o WiX v4 lê o seu próprio. Provado aqui: o MSI sai com o cabeçalho OLE2, o msiinfo lê nome, fabricante e versão, e o msiextract tira do CAB embutido cada arquivo que foi preparado.

doctor relata o projeto antes das ferramentas

Sem --target, ele responde primeiro se o projeto está pronto — e distingue o que falta do que só não foi configurado:

project
  ok       identifier  com.acme.client
  ok       name  Acme Client by Acme Ltda
  ok       version  1.2.0  (from pubspec)
  ok       targets  darwin-x86_64, darwin-aarch64 on this host of 4
  off      update  keys/update.key — no base-url, so every artefact needs its url spelled out
  ok       signing  DOVETAIL_MACOS_IDENTITY, not notarised
  off      service  no privileged helper is installed by the linux packages

A distinção é o ponto. Um host que não constrói nenhum dos alvos declarados sai off, não missing — é um runner cuja parte da matriz mora em outro lugar, e chamar isso de erro derrubaria um job de CI que está correto. O que sai missing é o que impede publicar: config ausente, ou um pubspec sem versão.

sign não precisa de Windows para assinar Windows

O signtool vem com o SDK do Windows e não existe em outro lugar — o que responde por esse nome num Mac é o assinador de JAR do nss. O caminho multiplataforma é o osslsigncode, e é o que o comando usa em qualquer host que não seja Windows.

dovetail sign --target windows \
  --file dist/setup.exe \
  --certificate keys/publisher.pem \
  --private-key keys/publisher.key \
  --timestamp-url http://timestamp.digicert.com

As flags sobrepõem o ambiente; sem elas ele lê WINDOWS_CERTIFICATE_FILE, WINDOWS_PRIVATE_KEY_FILE e WINDOWS_TIMESTAMP_URL. Um PKCS#12 entra pelo --certificate sem --private-key, e aí a senha vem de WINDOWS_CERTIFICATE_PASSWORD — vazia nunca é assumida, porque o osslsigncode pergunta no terminal e uma pergunta numa esteira é um build que trava em vez de falhar.

No Windows, com um WINDOWS_CERTIFICATE_THUMBPRINT, ele continua usando o signtool de verdade contra o certificado que está na loja.

sign --entitlements-for para o que está aninhado

Um helper privilegiado, uma Network Extension ou um login item dentro do bundle precisam das próprias entitlements. Dar a do app é como um daemon acaba com o sandbox do app.

dovetail sign --target macos --bundle build/Example.app \
  --entitlements macos/Release.entitlements \
  --entitlements-for "Contents/Helpers/daemon=macos/Daemon.entitlements"

build conhece o teto

Assinar e empacotar podem ser centralizados numa máquina; compilar não. O Flutter recusa na origem: "build windows" only supported on Windows hosts. Nenhuma ferramenta nossa contorna isso, e o build recusa antes de invocar o Flutter, dizendo qual passo precisa de outra máquina em vez de deixar o erro aparecer no meio do build.

dovetail build

Sem --target, ele olha os alvos do dovetail.yaml e constrói o que este host alcança. Um host cuja parte da matriz está em outro lugar sai 0 sem fazer nada — não é erro, é um runner sem trabalho nesta rodada.

icon nunca aumenta

De um PNG quadrado de 512px ou mais saem sete tamanhos no .ico, nove entradas no .icns e oito PNGs no tema do Linux. Os dois contêineres são escritos em Dart puro, sem ferramenta externa.

dovetail icon --source brand/icon.png --out build/icons --app-id io.exemplo.cliente

Quatro recusas, e cada uma existe porque a falha é silenciosa em vez de alta: um arquivo que não é PNG, uma fonte não quadrada (as duas plataformas esticam em vez de recusar), uma fonte abaixo de 512px (aumentar dá ícone borrado que nenhum revisor rejeita e todo usuário vê), e pedir os ícones do tema sem --app-id — sem ele os arquivos entram com um nome que o .desktop não aponta, e o launcher não mostra ícone nenhum.

inspect lê o arquivo, não o nome

dovetail inspect dist/*.dmg dist/*.exe dist/*.deb

Para um bundle macOS ou um Mach-O reporta as arquiteturas presentes, o estado da assinatura e — o motivo do comando — se a arquitetura no nome concorda com o conteúdo. Discordância sai com 1: um artefato cujo nome mente é pior do que um que falha ao construir, porque ele sobe.

Um bundle onde parte dos binários é gorda e parte é magra sai como universal só em parte, com o aviso de que não vai abrir em todo Mac onde instalar. Para .deb, .rpm, .msi e .exe ele diz que só leu o nome, para que uma linha que passou nunca seja confundida com uma linha verificada.

keygen recusa sobrescrever

dovetail keygen --out keys/update.key

Gera o par minisign e imprime a chave pública na forma que o manifesto carrega, para colar direto na config. Recusa quando já existe um par no destino: perder a privada de update não é perder um arquivo, é perder o caminho de atualização de toda instalação no campo, e a recuperação é reinstalar em cada máquina. Avisa também quando o destino não está no .gitignore.

manifest para quando a assinatura já existe

dovetail manifest --version 2.1.0 \
  --release darwin-universal=https://cdn/app.tar.gz=dist/app.tar.gz.minisig \
  --out dist/latest.json

O release assina e escreve o manifesto num passo só; o manifest é o passo de escrever sozinho, para quando as assinaturas vieram de outro lugar — de um HSM, ou de uma máquina que guarda a chave e não roda a esteira.

Ele embute o conteúdo do .minisig, nunca o caminho: um caminho ali seria silenciosamente inverificável na máquina de quem baixa.

probe pergunta ao endpoint se ele serve

dovetail probe --url https://api.exemplo.com/manifest/darwin \
  --public-key keys/update.pub --installed 1.0.0 --download

O formato do manifesto está em código, em teste e em prosa, e nada disso diz a quem escreve o servidor se o que subiu funciona. Este comando diz — de fora, pela rede, com o mesmo ManifestParser, a mesma UpdatePolicy e o mesmo verificador minisign que o app carrega.

Ele decodifica base64 antes de parsear, sem alternativa, porque é o que um cliente no campo faz: aceitar as duas formas é o que deixa uma release sair na forma que ninguém lê. Com --public-key confere o key id, e recusa --download sem chave, porque verificar um artefato contra a chave que o assinou só prova que os dois vieram do mesmo lugar.

Sai 0 só quando todos os alvos passam. Sem --target ele pergunta pelos três que o produto publica, então um endpoint que serve um e quinhentos os outros dois diz isso sem ninguém precisar pedir.

new: um projeto que já consome a biblioteca

dovetail new demo
cd demo && flutter pub get && flutter test && dovetail doctor

O init lê um projeto que já existe e escreve o dovetail.yaml. O new faz o caminho inverso: cria o projeto do zero, já com a biblioteca dovetail ligada. Ele escreve um pubspec.yaml com a dependência de caminho, um lib/main.dart, um teste de widget e um dovetail.yaml com os padrões que o doctor aceita — o mesmo ConfigTemplate que o init usa.

O que o new gera é o projeto desktop completo, montado a partir do template do SDK (tool/sdk/templates/app), e não um lib/main.dart solto:

demo/
├── lib/               clean-arch (o refinamento do Manguinho que o time
│   ├── data/            mobile usa): data → domain → infra → presentation
│   ├── domain/          → main, com presenters em interface + impl
│   ├── infra/           change_notifier_*, DI e rotas pelo weave_di
│   ├── main/            (repo privado, pinado em v3.3.0)
│   ├── presentation/
│   └── shared/
├── core/              o núcleo Rust (crate <nome>_core, fachada CoreHandle)
├── core_bridge/       o plugin FFI que repassa o núcleo, do mesmo template
│                       que o `bridge init` usa — com um repasse de exemplo e
│                       o portão de cobertura que recusa núcleo crescido sem
│                       repasse
├── scripts/checks/    as duas suites de conformidade do time, adaptadas:
│   ├── flutter/         a do mobile (camadas, nomenclatura, Weave, tamanho),
│   └── rust/            a do desktop (comentários, inglês, tamanho, URLs)
├── .githooks/         pre-commit e pre-push rodando o gate
├── run_app.sh         gate das duas suites + flutter run -d macos
└── Makefile           quality, codegen, tests, run, hooks, build

O laço do dia a dia no projeto gerado: make codegen regenera a ponte quando o núcleo cresce (o Dart gerado não é versionado), make quality roda as duas suites, e ./run_app.sh se recusa a subir o app se o gate reprovar.

O caminho para a biblioteca é calculado sozinho, nesta ordem: dentro do repositório ele sobe até achar toolkit/dovetail/lib/dovetail.dart e grava o caminho relativo a partir do projeto novo; fora dele — o caso do binário compilado, que não tem repo — ele aponta para o SDK instalado, em caminho absoluto. --dovetail-path sobrescreve qualquer um dos dois, e sem nenhuma das três fontes o comando recusa nomeando-as, porque um scaffold cujo path aponta para lugar nenhum mente no primeiro flutter pub get. O teste de widget que o new escreve exercita de verdade o runtime do próprio dovetail — um smoke que só renderiza o app provaria o Flutter, não o SDK que o scaffold prometeu ligar.

O weave_di — DI e rotas do template — resolve pela mesma regra, e por um motivo que vale escrever: ele era uma dependência git com a url git@weave-di.github.com:, um alias de SSH que existe no ~/.ssh/config de uma máquina só. Todo projeto que este comando gerou herdava isso, então o primeiro flutter pub get de qualquer outra pessoa morria num host que o DNS não resolve. O repositório é privado, então a url https também não salvaria quem está de fora: o caminho certo é o mesmo dos outros pacotes do runtime — viajar dentro do SDK. A ordem é --weave-path, depois $DOVETAIL_WEAVE_PATH, depois o SDK instalado; sem nenhuma das três o comando recusa antes de escrever qualquer byte, porque scaffold pela metade é pior do que nenhum. Um caminho que existe mas não tem pubspec.yaml conta como ausente, e uma variável exportada vazia também — as duas produziriam um path: que só falha lá na frente.

Nenhum pubspec.yaml que este comando escreve carrega git: — há teste prendendo isso.

O template do app resolve como o do bridge: --template explícito, depois o SDK instalado (sdk/<versão>/templates/app), depois o repo. Sem nenhum, o comando recusa nomeando as fontes — e o bridge vem do mesmo lugar, porque template e dovetail_rust_core têm que casar.

O nome vem do diretório, ou de --name, e precisa começar com letra e conter só letras, dígitos e sublinhados. O identificador padrão é com.example.<nome>, trocável com --identifier. Um diretório que já existe faz o comando recusar em vez de sobrescrever.

O dovetail new --sdk resolve o runtime do SDK instalado via pubspec_overrides.yaml, em vez de um path no pubspec.

Desenvolvimento

dart test
dart analyze

bridge: o plugin que fala com o núcleo

dovetail bridge init --core <crate> --name <nome> [--out dir] [--template dir]

Gera o plugin ffiPlugin (Windows/macOS/Linux) que repassa o núcleo Rust de um produto para o Flutter — o mecanismo do qual o product/desktop_core_bridge é o fixture, reproduzido pelo template do SDK. O --core é o diretório do crate (o que segura o Cargo.toml dele), e o comando lê o nome real do crate do manifesto em vez de adivinhar. O dovetail_rust_core entra do SDK instalado — o template e o runtime vêm do mesmo lugar, senão o gerado aponta para um e resolve outro.

O template entrega a mecânica, não o conteúdo: rust/src/api/ sai vazio (os repasses são do produto), lib/src/rust/ é gerado pelo codegen, e o example/ não existe. O laço do gerado é o mesmo do fixture:

flutter_rust_bridge_codegen generate
cd rust && cargo check
flutter test test/core_coverage_test.dart

O último é o portão de forma que o template carrega: lê o handle.rs do crate e as chamadas em rust/src/api/, e recusa quando o núcleo cresceu um método que nenhum repasse expõe. Sem template nenhum — nem --template, nem SDK instalado — o comando recusa nomeando o instalador.

dev: a ponte nunca fica para trás

A armadilha está em dois documentos deste repo: você escreve um método em Rust, tudo compila, e ele não existe do lado Dart — sem erro em lugar nenhum, até o verify frb gritar. O dev faz a ponte falar antes:

dovetail dev            # recusa se o Dart está atrás (nomeando o método); senão vigia e regenera
dovetail dev --check    # só o veredito, sem vigiar
dovetail dev --once     # regenera agora e diz o que entrou

Ele roda dentro de um package que tem flutter_rust_bridge.yaml (ou com --root apontando para ele). O check gera num diretório temporário com o codegen real e compara os debugName — nunca parseia Rust na mão — então a recusa nomeia exatamente o método que o Dart não tem, e a árvore fica intocada.

O que não dá para provar nesta máquina

O bundle --target windows e o --windows-format msi nunca rodaram: o makensis local está quebrado e o wix não está instalado. O sign --target windows nunca assinou nada, porque não há certificado nem signtool de verdade aqui.

O que está provado é a composição: o manifesto que o release escreve é lido de volta e verificado pelo dovetail_updater, e o doctor reporta os seis alvos desta máquina com o resultado real de cada sonda.

Libraries

dovetail_cli