theforge-cli 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,7 +3,447 @@
3
3
  Todas as mudanças notáveis deste projeto são documentadas aqui, no
4
4
  formato [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/).
5
5
 
6
- ## [Não lançado]
6
+ ## [0.2.1] - 2026-10-18
7
+
8
+ ### Fixed
9
+
10
+ - Plan purity `--plan` em todas as superfícies registry
11
+ - Probe de hosts com timeout (FORGE_CLI_PROBE_TIMEOUT_MS)
12
+ - Install `[mcp]` extras + pin/update por evidência real
13
+
14
+ ## [0.2.0] - 2026-10-17
15
+
16
+ ### Cycle 11.1 — hardening de produção, MCP real e Trusted Publishing
17
+
18
+ #### P0-001 — npm Trusted Publishing (OIDC)
19
+
20
+ - `publish-npm.yml` reescrito: `workflow_dispatch(version, tag?)` →
21
+ job `validate` (gates completos + `npm pack --dry-run` + release
22
+ gate) → job `publish` (`environment: npm`, `id-token: write`,
23
+ checkout pinado no SHA validado — sem TOCTOU), provenance e
24
+ smoke pós-publish (registry check + install limpo + `--help`).
25
+ - `scripts/release/verify-version.mjs`: recusas nomeadas —
26
+ `version-invalid`, `version-mismatch`, `version-exists`,
27
+ `tag-missing`, `tag-mismatch`, `repo-url`, `license-missing`.
28
+ - `docs/releasing/npm-trusted-publishing.md`: setup do publisher,
29
+ environment, gates, troubleshooting e recovery.
30
+ - Setup externo (publisher no npmjs.com + environment `npm` com
31
+ aprovação) é ação do mantenedor — `BLOCKED_EXTERNAL` até então.
32
+
33
+ #### P0-002 — pureza de `--plan`/`--check`/`--dry-run`
34
+
35
+ - `resolveRelease` ganha `ephemeral`: em plano, o fetch PyPI pode
36
+ acontecer mas **nada persiste** — nem o cache TTL de metadados
37
+ (`resolveRelease` não chama mais `writeCache` sob ephemeral).
38
+ - `tests/plan-purity.test.ts`: snapshot sha256 da árvore do home
39
+ antes/depois — `install --plan`, `update --check` e uninstall
40
+ não-TTY sem `--yes` (needsYes) produzem zero mutação observável.
41
+
42
+ #### P1-002 — verificação MCP de protocolo real
43
+
44
+ - `src/mcp/handshake.ts`: probe stdio JSON-RPC — spawn (argv array,
45
+ sem shell) → `initialize` → `notifications/initialized` →
46
+ `tools/list` → kill. Níveis `MCP_INITIALIZED`/`TOOLS_DISCOVERED`;
47
+ timeouts por etapa e global, caps de tamanho/contagem de linhas,
48
+ `tools/call` nunca executado.
49
+ - `forge-cli mcp --verify` agora roda o probe real (não mais
50
+ `--version`): JSON reporta `probe.{ok,levels,tools,server,protocol,
51
+ error}` com erros nomeados (`stdout-non-json`, `server-exit`,
52
+ `initialize-invalid`, `tools-list-invalid`, `timeout`, `spawn-failed`).
53
+
54
+ #### P1-001 — registry-first na experiência
55
+
56
+ - README: instalação npm primeiro (sem clone), contribuidor via
57
+ clone depois; seção PyPI marcada como método recomendado
58
+ (registry-first, não registry-only).
59
+
60
+ ### Cycle 11 — instalação registry-first (PyPI)
61
+
62
+ #### Wave 7 — performance (números medidos, não inventados)
63
+
64
+ - `scripts/bench.mjs`: mede startup (`--help`), `status`, `registry`,
65
+ resolve `--plan` (rede), install frio, reinstall e `update --check`
66
+ (cache). Mediana de N runs (`--runs`), JSON
67
+ `forge-cli/BenchReport/v1` em stdout, `FORGE_CLI_HOME` hermético.
68
+ - Medido local (Node 24, Linux, 3 runs): `--help` ~259ms,
69
+ `status` ~72ms, `registry` ~67ms, `resolve --plan` ~290ms (PyPI
70
+ online), install frio ~3.2s e reinstall ~3.5s (venv+pip dominam —
71
+ fora do nosso controle), `update --check` com cache ~217ms.
72
+ - **`FORGE_CLI_HOME`**: novo env override para o home de estado
73
+ (`~/.forge-cli`) — bench/testes/containers rodam isolados sem tocar
74
+ o estado real. Não afeta detecção de hosts (esses ficam no home real).
75
+
76
+ #### Wave 8 — supply chain / hardening
77
+
78
+ - **Retry com backoff no PyPI**: `fetchWithRetry` — 429/5xx e erros de
79
+ rede retentam 2x (250ms→500ms); 404 e respostas definitivas retornam
80
+ imediato. Retry nunca mascara "não existe".
81
+ - **`receipt.provenance`**: `{registry:'pypi.org', file, url,
82
+ resolvedVia:'pypi'|'offline-cache'}` — proveniência auditável do
83
+ artefato instalado junto ao `verifiedHash`.
84
+ - **Permissões do state dir**: `~/.forge-cli` e `forges/` recebem
85
+ `chmod 0700` no POSIX (Windows: ACL do perfil, chmod é no-op).
86
+
87
+ #### Wave 4 — hosts/MCP registry-aware
88
+
89
+ - **`forge-cli mcp [forja…]`**: resolve o argv MCP por Forja e reporta
90
+ presença do binário. Fonte: `forge.json` do clone, ou — sem clone —
91
+ receipt registry + template `mcp` do catálogo (`{bin}` → bin da venv,
92
+ `{binDir}/x` → script irmão; `.exe` no Windows). `--verify` roda
93
+ `<argv[0]> --version` como handshake mínimo. `--json` emit
94
+ `{forge, mcp_command, binPresent, handshake?}` por item. Forja sem
95
+ `mcp_command` honesto (ex.: the-forge) sai `ok:false`, nunca inventa.
96
+ - **`hosts plan|register|validate|rollback` sem clone**: `forgeDescriptor`
97
+ cai para o receipt + catálogo quando o manifest não está no
98
+ workspace — `hosts register claude api-forge` numa instalação
99
+ registry grava `mcpServers['api-forge'].command` apontando para o
100
+ `apiforge-mcp` da venv. Backup/rollback/`--yes` inalterados.
101
+ - Catálogo: campo `mcp` por Forja espelhando o `mcp_command` de cada
102
+ `forge.json` (api-forge `apiforge-mcp`, platform-forge
103
+ `platformforge-mcp`, doctors `[bin] mcp`, spark-forge `[bin] mcp serve`).
104
+ - `CliOptions.verify` + flag `--verify` registrada (parser é strict).
105
+
106
+ #### Wave 3 — lifecycle registry
107
+
108
+ - **`update --method registry`**: resolve a mais nova no PyPI, compara
109
+ com o receipt e reinstala limpo quando há versão nova. `--check`/
110
+ `--plan` mostram o plano sem mutar; `--ref <ver>` é override pontual
111
+ (não cria nem remove pin). Versões anteriores são empilhadas em
112
+ `receipt.history` — insumo do rollback.
113
+ - **`reinstall --method registry`**: reinstala a mesma versão do
114
+ receipt (reparo de drift — venv apagada mas state presente recria).
115
+ Preserva `pinned` e `history` — reinstall não é lifecycle de versão.
116
+ - **`rollback --method registry`**: desempilha `history.at(-1)` e
117
+ empilha a versão abandonada — rollback de rollback retorna à versão
118
+ original. Sem histórico → recusa honesta (`rollbackNoPrevious`), exit 1.
119
+ - **`pin`/`unpin --method registry`**: trava/remove a versão instalada
120
+ no state; `update` respeita (`skipped: fixada em <ver>`).
121
+ - **Ghost-forge no lifecycle**: Forja removida do catálogo mas com
122
+ receipt continua atualizável/reinstalável — `ghostEntry()` deriva
123
+ dist/bin do receipt e o piso Python do interpretador que a instalou.
124
+ - **Contrato uniforme**: `isSafeRepoName`/`resolveInside` nos mesmos
125
+ pontos do uninstall; flags git ignoradas avisam também no lifecycle;
126
+ `update` com todos os alvos não-instalados sai 1 (nada aconteceu);
127
+ confirmação interativa só em TTY duplo (automação segue com `--yes`).
128
+
129
+ #### Wave 2 — TUI registry + delegação nativa (fase 2)
130
+
131
+ - **Menu guiado `--method registry`**: `shouldShowMenu` não exclui mais
132
+ o método — `menuItemsFor` filtra o menu para os verbos suportados
133
+ (install/update/reinstall/uninstall/status/exit). `status` mapeia
134
+ para `forge-cli registry` (receipts). Install escolhe no catálogo
135
+ PyPI com hint de dist+piso Python; lifecycle escolhe nas instaladas.
136
+ - **Delegação ao instalador nativo** (`delegateNative`): `--native` ou
137
+ qualquer flag de fase 2 (`--profile/--scope/--components/--host/
138
+ --gateway-profile`) roda `<bin> install` **pelo bin da venv
139
+ verificada** — nunca um shim global. Sem `--yes` roda `--dry-run`
140
+ (default-deny). `{python} scripts/...` (forge-doctor-api) é skip
141
+ honesto — o script vive no repositório, não na wheel.
142
+ - **Native em lifecycle**: update/reinstall rodam a op `update`
143
+ documentada; `uninstall --native` roda `ops.uninstall` **antes** de
144
+ remover a venv. Tudo gravado em `receipt.native` (argv, ran, dryRun,
145
+ exitCode, skipped, ignored).
146
+ - **Validação fail-fast**: `--profile/--scope/--components/--host/
147
+ --gateway-profile` inválidos recusam antes de gastar rede (exit 1),
148
+ mesmo contrato do fluxo git. Flags saem de REGISTRY_IGNORED_FLAGS —
149
+ agora são honradas.
150
+ - **`.env` preservado**: instalações registry vivem em venvs isoladas
151
+ sob `~/.forge-cli/` — nunca tocam `.env` de projetos do usuário.
152
+
153
+ #### Corrigido após revisão (7ª rodada)
154
+
155
+ - **Uninstall não-TTY sem `--yes` (crítico)**: `uninstall --method
156
+ registry` pendurava em `confirm()` com stdin não-TTY (CI, pipes,
157
+ `--json --report` em automação). Agora recusa limpa — imprime
158
+ `needsYes`, grava o relatório se `--report` e sai 0, mesmo contrato
159
+ do fluxo git. Verificado real: `uninstall < /dev/null` → exit 0.
160
+ - **`state.json` com valores não-objeto não derruba**: `{"forges":
161
+ {"api-forge":"garbage"}}` é filtrado em `loadForgeState`; `registry`,
162
+ `status` e `list` não crasham (antes `TypeError` em `inst.verify.ok`)
163
+ — verificado real com o payload exato da revisão.
164
+ - **Own-property no state**: `state.forges[name]` via `Object.hasOwn` —
165
+ `uninstall constructor`/`toString` sai 2 como desconhecido em vez de
166
+ casar membros do `Object.prototype` (a mesma classe do review-9, no
167
+ resolver — agora também no state).
168
+ - **Recusa antes da resolução**: venv existente sem `--clean` falha
169
+ `already-installed` antes de gastar a chamada PyPI (fail fast,
170
+ inclusive offline).
171
+ - **`--ref` pós-dedup**: `install api-forge apiforge-af --ref` é uma só
172
+ Forja (dedup por alias na validação do dispatcher).
173
+ - **Confirmação mostra a versão resolvida**: o note de confirmação do
174
+ install interativo exibe `dist==versão` real (resolução antes do
175
+ confirm usa o cache no install seguinte — sem rede duplicada).
176
+ - **Piso composto**: `minPython` do catálogo compõe com
177
+ `requires_python` (max implícito) — release que alargue o spec para
178
+ baixo do piso da forge-cli não habilita interpretadores sem suporte.
179
+ - **`binPresent` estruturado**: `forge-cli registry --json` emite
180
+ `binPresent: boolean` em vez de embutir "(ausente)" no campo `bin`.
181
+ - **`mergeRegistryPhase` tolerante**: own-property + campos checados —
182
+ receipt sem `verify`/`bin` não derruba o `status`/`list`.
183
+
184
+ #### Corrigido após revisão (6ª rodada)
185
+
186
+ - **Path traversal no uninstall (bloqueio)**: `name`/`dist` vindos do
187
+ `state.json`/receipt (dado não confiável — entrada fora do catálogo é
188
+ desinstalável por design) agora passam por `isSafeRepoName` +
189
+ `resolveInside`, a mesma convenção do `safety.ts` para caminhos
190
+ destrutivos. Nome inseguro = recusa nomeada com exit 2, nunca chega
191
+ ao `rmSync`; `dist` maliciosa só impede a limpeza do wheel cache
192
+ (venv+state ainda saem).
193
+ - **Pin não-exato**: `--version 0.1` casa a chave `0.1.0` do PyPI por
194
+ equivalência parseada (PEP 440 zero-padding — o pip aceita `==0.1`).
195
+ - **Confirmação interativa no install**: TTY + sem `--yes` mostra o
196
+ plano (`confirmRun`) antes de instalar, mesmo contrato do fluxo git;
197
+ `--json`/pipes/`--yes` seguem sem perguntar.
198
+ - **`requires_python` autoritativo**: o piso `minPython` do catálogo só
199
+ se aplica quando os metadados do PyPI não declaram spec — o release
200
+ pode alargar o suporte sem depender de atualização do catálogo.
201
+ - **`info.requires_python` escopado**: o campo descreve a release mais
202
+ nova; como fallback só vale quando a versão resolvida é ela (um pin
203
+ antigo não recebe o spec novo por engano).
204
+ - **Cache e leitura defensivos**: `writeCache` atômico; cap de 16 MiB
205
+ aplicado antes de bufar o corpo (content-length + cancel do stream);
206
+ `state` com `forges: []` tratado como vazio.
207
+ - **Quarentena em hash-mismatch**: artefato divergente é renomeado para
208
+ `.rejected` — preserva evidência, desbloqueia o retry e nunca é
209
+ selecionável pelo `--find-links` (extensão fora de `.whl`/`.tar.gz`).
210
+ - **stderr redacted**: detalhes de erro de subprocesso passam por
211
+ `redactSecrets` — credenciais de `PIP_INDEX_URL` não vazam no stdout
212
+ JSON, no `--report` nem no log humano.
213
+
214
+ #### Corrigido após revisão (5ª rodada)
215
+
216
+ - **Dedupe pós-normalização**: `install api-forge apiforge-af` (nome +
217
+ alias) não falha mais com `already-installed` espúrio; uninstall
218
+ planeja uma única remoção.
219
+ - **`status` honesto sob drift**: receipt registry com venv/bin ausente
220
+ vira `drifted` + "(bin ausente)" (antes marcava `installed` só pelo
221
+ state). `forge-cli registry` e `status` agora concordam.
222
+ - **`--report` honrado**: escreve o documento JSON no arquivo (mesmo
223
+ contrato do fluxo git) em vez de só desviar o modo de saída;
224
+ `--json` sozinho continua emitindo no stdout.
225
+ - **Flags ignoradas no uninstall**: `--dir`/`--profile`/etc. agora
226
+ avisam no uninstall também (antes só no install).
227
+ - **Uninstall completo**: remove venv + `cache/wheels/<dist>` + state —
228
+ reclaim de disco; Forja fora do catálogo mas presente no state
229
+ continua desinstalável (entrada removida do catálogo não trava).
230
+ - **Escritas atômicas**: `state.json` e `install.json` usam
231
+ `writeFileAtomic` (convenção do codebase) — kill no meio não corrompe.
232
+ - **Sem dir órfão**: falha de install remove `forges/<nome>/` vazio.
233
+ - **`--find-links` só no offline**: online as deps resolvem no índice —
234
+ wheel cache não compete desnecessariamente. Limite documentado:
235
+ offline, deps transitivas resolvem por nome sem pin de hash.
236
+ - **Cache não envenenável**: payload 200 sem `releases` não sobrescreve
237
+ cache sadio — cai no cache ou falha `malformed`.
238
+ - **Menu sem dead-end**: `--method registry` sozinho não abre o menu
239
+ interativo (que só roteava o fluxo git) — recusa limpa no dispatcher.
240
+ - **`--method git` em comandos alheios**: agora avisa em vez de engolir
241
+ (ex.: `status --method git`).
242
+ - **pip sem prompts**: `--no-input` em download/install; timeout do
243
+ subprocesso preserva o stderr real; payload PyPI tem cap de 16 MiB;
244
+ `available` do `version-not-found` vem ordenado.
245
+ - **Banner do plano honesto**: "venv e estado não alterados" (o cache
246
+ de metadados pode ser atualizado — mesmo modelo de `update --check`).
247
+ - Docs: `arquitetura.md` e `capabilities.md` descrevem o método
248
+ registry; decisões registradas como diferidas — `--python`
249
+ selecionável (§4.7), pipx/uv (§4.6), canal rc/beta (§4.1), lock de
250
+ state.json entre processos concorrentes.
251
+
252
+ #### Corrigido após revisão (4ª rodada)
253
+
254
+ - **Membership own-property**: `--version toString` (ou qualquer membro
255
+ do prototype) não é mais tratado como release existente — `plan`
256
+ chegava a certificar `apiforge-af==toString` como instalável.
257
+ - **Instalação por artefato verificado**: o pip recebe o caminho local
258
+ da wheel/sdist com sha256 conferido — um arquivo plantado no wheel
259
+ cache com nome da dist nunca é escolhido por resolução de nome.
260
+ Sem artefato verificável → `hash-mismatch`, não instala. (Limite
261
+ honesto: dependências transitivas resolvem por nome em
262
+ `--find-links`; só o artefato da dist é vinculado por hash.)
263
+ - **Reinstall sem `--clean`** recusa com `already-installed` — antes
264
+ `python -m venv` recriava sobre a venv boa e uma falha posterior a
265
+ destruía com o state intacto.
266
+ - **`--report`/`--verbose`**: label correto (`--report`, não
267
+ `--report-path`) e `--verbose` entra na lista de avisados.
268
+ - **Exit code consistente**: uninstall com nome desconhecido sai 2 em
269
+ modo humano também (antes só `--json` retornava erro).
270
+ - **Cache TTL robusto a clock skew**: `fetchedAt` futuro conta como
271
+ stale, não como eternamente fresh.
272
+
273
+ #### Corrigido após revisão (3ª rodada)
274
+
275
+ - **PEP 440**: `XaN.postM` agora compara `post` dentro da fase de
276
+ pre-release — antes `1.0a1.post1` empatava com `1.0a1` e `!=`
277
+ avaliava errado.
278
+ - **Alias de dist normalizado**: `registry apiforge-af` filtra a linha
279
+ certa e `uninstall --method registry apiforge-af` resolve o estado
280
+ pelo nome canônico — antes passavam validação e produziam saída
281
+ vazia/`não instalado` espúrio.
282
+ - **`--clean` limpa o `state.json`** junto com venv/receipt — falha
283
+ posterior não deixa versão antiga registrada sem artefatos.
284
+ - **subprocessos com timeout** (venv/download/pip/verify, 5 min
285
+ default) — antes só o fetch era limitado.
286
+ - `selectInterpreter` ordena internamente (não depende do chamador);
287
+ `checkDistHashes` verifica **todos** os artefatos presentes;
288
+ `--version vX.Y.Z` aceita prefixo `v` no método registry.
289
+
290
+ #### Corrigido após revisão (dispatch + integridade, 2ª rodada)
291
+
292
+ - **`--plan`/`--dry-run`/`--check` honrados no registry**: resolvem e
293
+ mostram o plano (dist==versão, interpretador, venv, wheel cache) sem
294
+ criar venv, baixar artefatos ou gravar estado — antes executavam uma
295
+ instalação real.
296
+ - **`--method registry` recusado no topo do dispatch** (`versions`,
297
+ `status`, `list`, `self update`, `doctor`, …) — antes só
298
+ `update`/`clone`/`reinstall`/`rollback`/`pin`/`unpin` alcançavam o
299
+ check e o resto executava o fluxo git.
300
+ - **`--json` limpo nos caminhos de erro/aviso**: nomes desconhecidos,
301
+ skips de uninstall e a exigência de `--yes` emitem JSON estruturado
302
+ (`error`, `planned`, `skipped`, `warnings`) — antes logs humanos
303
+ poluíam stdout e quebravam `JSON.parse`.
304
+ - **Flags ignoradas cobertas por completo**: `--skip-scripts`,
305
+ `--shallow`, `--fix`, `--rollback`, `--gateway-profile`,
306
+ `--trust-custom`, `--run-after-sync-failure`, `--report`,
307
+ `--only-changed` agora avisam também; `--clean` é honrado (recria a
308
+ venv) e `--check` funciona como plano.
309
+ - **HTTP 5xx/429 do PyPI cai no cache local** (antes só exceções de
310
+ transporte); sem cache continua falhando honestamente.
311
+ - **`--offline` verifica sha256 do wheel cacheado** contra o digest do
312
+ PyPI — cache adulterado aborta com `hash-mismatch`.
313
+ - **Interpretador pré-release** só é escolhido quando nenhum estável
314
+ satisfaz `requires_python` (alinha com `packaging`).
315
+ - **`distArtifactFiles`** usa prefixo normalizado com separador —
316
+ `dist_extra-…` não colide e sdists legados com `-` são encontrados.
317
+ - **Ordenação PEP 440**: `XaN.devM`/`X.postN.devM` ordenam antes do
318
+ mesmo marco sem `.dev` (e branch morto removido).
319
+ - **`registry <nome>`** filtra o catálogo; nome fora do catálogo →
320
+ erro 2.
321
+ - **Pós-instalação** mostra o caminho do binário instalado (venv
322
+ isolada — PATH é decisão do usuário).
323
+
324
+ #### Corrigido após revisão (dispatch + integridade)
325
+
326
+ - **`--ref`/`--version` em multi-Forja** agora recusa como no fluxo git
327
+ (`errRefNeedsOne`) — antes aplicava o mesmo pin a todas as Forjas.
328
+ - **`uninstall --method registry`**: dispatch movido para antes do
329
+ branch legado (sem nomes não executa mais a remoção do PATH),
330
+ `--all`/`--only` resolvem o conjunto instalado, e a remoção segue a
331
+ convenção destrutiva do repo — plano visível + confirmação/`--yes`.
332
+ - **`--method registry` em comandos não suportados** (`update`, `clone`,
333
+ `reinstall`, `rollback`, …) é recusado em vez de cair no fluxo git.
334
+ - **Offline real**: instalação online baixa artefatos para
335
+ `cache/wheels/<dist>/`; `--offline` instala com
336
+ `--no-index --find-links` e recusa com o artefato faltante nomeado
337
+ (nunca finge instalar sem rede).
338
+ - **Integridade**: sha256 do artefato da dist é conferido byte a byte
339
+ após o download e gravado como `verifiedHash` no receipt; divergência
340
+ aborta antes do pip.
341
+ - **Yanked/empty releases** não entram na escolha automática
342
+ (PEP 592); pin explícito instala e marca `yanked` no receipt.
343
+ - **`status`** mostra Forjas instaladas via registry (fase
344
+ `installed`, `pypi dist==ver`) em vez de `not-installed`.
345
+ - **`registry`** marca `(verify falhou)` em installs que não passaram
346
+ no verify; `--json` expõe `verifyOk`.
347
+ - **`--json` no install/uninstall registry** emite JSON limpo (banner,
348
+ spinner e logs suprimidos); flags do fluxo git ignoradas viram aviso
349
+ explícito (`--dir`, `--profile`, `--scope`, `--components`,
350
+ `--host`, `--native`, `--skip-verify`, `--timeout`, `--jobs`,
351
+ `--retry-failed`).
352
+ - **`requires_python` por arquivo**: prefere o da wheel (o que o pip
353
+ instala) ao do sdist/`info`.
354
+ - **Especificador fora do subconjunto** (`===`, `~=X`, wildcard com
355
+ operador errado) vira `specifier-unknown`, não `python-incompatible`.
356
+ - **Interpretador pré-release** (`Python 3.14.0a1`) parseia com sufixo.
357
+ - **`state.json` corrompido** é preservado em `.corrupt` antes de ser
358
+ ignorado — nunca clobber silencioso.
359
+ - **Venv órfã** removida em falha de venv/download/pip.
360
+ - **Pisos do catálogo** corrigidos para o `requires_python` real
361
+ publicado (`apiforge-af` ≥3.12, doctors ≥3.11).
362
+
363
+ #### Added
364
+
365
+ - **`install --method registry`**: instala Forjas direto do PyPI sem
366
+ exigir clone do repositório nem o The Forge instalado. Cada Forja vai
367
+ para uma venv isolada em `~/.forge-cli/forges/<nome>/`, com
368
+ `pip install <dist>==<versão>` pinado e verify do console script.
369
+ - **`forge-cli registry`**: lista o catálogo PyPI (7 dists), a versão
370
+ instalada e o binário de cada Forja; `--json` exporta.
371
+ - **`uninstall --method registry`**: remove a venv e o receipt de uma
372
+ Forja instalada via registry.
373
+ - **Resolver PyPI** (`src/registry/`): consulta o índice JSON do PyPI,
374
+ escolhe a estável mais nova (pré-releases ignoradas), honra
375
+ `--version`/`--ref` como pin e registra `source` no receipt
376
+ (`pinned|latest-stable|cache`).
377
+ - **Compatibilidade Python pré-install**: `requires_python` do release é
378
+ avaliado por um subconjunto PEP 440 (`~=`, `==`/`!=` com `.*`, `>=`,
379
+ `>`, `<=`, `<`) contra os interpretadores detectados — o mais novo que
380
+ satisfaz a faixa vence, e falta de interpretador compatível é erro
381
+ honesto antes de qualquer mutação.
382
+ - **Cache offline**: `--offline` resolve apenas pelo cache local
383
+ (`~/.forge-cli/cache/pypi/`, TTL 1h); falha de rede cai no cache
384
+ automaticamente quando existe.
385
+ - **Receipts auditáveis**: `install.json` por Forja + `state.json`
386
+ consolidado registram dist, versão, `requires_python`, interpretador
387
+ escolhido, digests sha256 dos artefatos e o verify (comando + ok) —
388
+ incluindo installs que falharam no verify, para diagnóstico.
389
+
390
+ #### Fixed
391
+
392
+ - `install` sem nomes no modo registry agora recusa com orientação
393
+ explícita em vez de cair no fluxo interativo do git.
394
+
395
+ ### Hardening pós-release
396
+
397
+ #### Fixed
398
+
399
+ - **`update --check` sem upstream**: repositório sem remote configurado
400
+ (ou sem tracking no branch) reportava `não foi possível verificar
401
+ (unknown)` — implicando falha transitória onde o estado honesto é
402
+ `no-upstream`. Novo literal em `SyncFailure`; `updateCheck` resolve
403
+ `@{u}` **antes** do fetch (cobre `branch.<name>.remote` apontando para
404
+ `.` ou URL sem remote nomeado) e só então classifica `no-upstream`;
405
+ o texto mostra `sem upstream configurado` e o JSON propaga
406
+ `failure: "no-upstream"`.
407
+
408
+ #### Changed
409
+
410
+ - **`planActionsFor` exportado** (`src/wizard.ts`): o construtor das
411
+ ações do plano revisado no wizard vira superfície testável, junto a
412
+ `renderOperationPlan` — o fluxo interativo (@clack/prompts) continua
413
+ fronteira de TUI não simulada.
414
+
415
+ #### Tests
416
+
417
+ - `tests/wizard.test.ts` (novo, 15 casos): renderização do plano
418
+ (hash truncado, expiração, passos, marcador de não-reversível,
419
+ truncamento de arquivos em 8) e `planActionsFor` (clone vs pull,
420
+ gating por passo, `.env` e `verify` sempre presentes).
421
+ - `tests/hosts.test.ts` (+12 casos): adapter **cursor** completo
422
+ (detect absent→installed→configured→integrated, merge-json com
423
+ preview e sem copyable, manual quando integrado, JSON inválido
424
+ recusado sem clobber, dry-run), **copilot** (detect via `gh copilot`
425
+ - `.config/gh`, plan gated por `mcp-config.json` existente) e
426
+ **devin** (detect via `.config/devin`, apply/validate recusam com
427
+ `hostUnsupported`).
428
+ - `tests/config.test.ts` (+6 casos): bloco `ai` do `loadConfig` —
429
+ chaves válidas, limites (512–262144 bytes, 1000–300000 ms,
430
+ 0–86400000 ms TTL), tipos errados descartados, `ai` não-objeto
431
+ ignorado sem erro.
432
+ - `tests/ai-cache.test.ts` (novo, 8 casos): chave canônica (ordem de
433
+ chaves irrelevante, tipos distintos não colidem), TTL (expirado,
434
+ timestamp futuro, `ttl<=0`), entrada corrompida/shape inválido →
435
+ miss, eviction bounded por idade com tiebreak por nome.
436
+ - `tests/git-revs.test.ts` (+6 casos): `updateCheck` reporta
437
+ `no-upstream` com e sem remote configurado, tracking local
438
+ (`branch.<name>.remote=.`, `git remote` vazio) resolve `@{u}` e não é
439
+ `no-upstream`, fetch falho classificado sem throw, `remoteTags` em
440
+ remote `file://` real (ordem semver, `--limit`, remote inacessível →
441
+ `null`).
442
+ - `tests/e2e-cli.test.ts` (+1 caso): `update --check` ponta a ponta
443
+ reporta `no-upstream`/`sem upstream configurado` sem pendência.
444
+ - `tests/doctor2.test.ts` (+4 casos): helpers de contexto (`listDir`
445
+ ordenado/nulo, `readJson` ok/erro, `boundEntries` com flag de
446
+ truncamento, `mkCheck`/`errMsg`).
7
447
 
8
448
  ### Programa ecosystem-2026 (cyc8-05)
9
449
 
package/README.md CHANGED
@@ -41,23 +41,19 @@ feedback visual em tempo real.
41
41
 
42
42
  ## Instalação
43
43
 
44
- Clone este repositório e instale as dependências:
44
+ **Usuário final — via npm (recomendado):**
45
45
 
46
46
  ```bash
47
- git clone https://github.com/EdgarSocrates98/forge-cli.git
48
- cd forge-cli
49
- npm install
50
- ```
51
-
52
- Para rodar sem compilar:
53
-
54
- ```bash
55
- npm run dev
47
+ npm i -g theforge-cli # pacote publicado — sem clone necessário
48
+ forge-cli # TUI guiada
56
49
  ```
57
50
 
58
- Para compilar e usar como comando global:
51
+ **Contribuidor — via clone:**
59
52
 
60
53
  ```bash
54
+ git clone https://github.com/EdgarSocrates98/forge-cli.git
55
+ cd forge-cli
56
+ npm install
61
57
  npm run build
62
58
  npm link # registra o binário `forge-cli`
63
59
  forge-cli # em qualquer diretório
@@ -95,7 +91,33 @@ forge-cli install spark-forge-aws --profile recommended --scope project --compon
95
91
 
96
92
  O forge-cli só delega ao `<cli> install` que a Forja documenta; Forjas sem
97
93
  instalador nativo (ex.: forge-portal) recebem um aviso, sem substituto
98
- silencioso. Veja [docs/capabilities.md](docs/capabilities.md).
94
+ silencioso. Veja [docs/capabilities.md](https://github.com/EdgarSocrates98/forge-cli/blob/v0.1.0/docs/capabilities.md).
95
+
96
+ ## Instalação via PyPI (registry-first — método recomendado)
97
+
98
+ `forge-cli` sem argumentos abre a TUI guiada com **registry como opção
99
+ recomendada** para as distribuições oficiais. O método git (clone +
100
+ scripts) continua disponível para contribuidores — registry-first, não
101
+ registry-only. `install --method registry`
102
+ instala cada Forja direto do PyPI — sem clonar o repositório e sem
103
+ exigir o The Forge. Cada Forja vai para uma venv isolada em
104
+ `~/.forge-cli/forges/<nome>/` com `pip install <dist>==<versão>`
105
+ pinado na estável mais nova (ou `--version`/`--ref` para pinar, uma
106
+ Forja por comando). O interpretador escolhido é o mais novo que
107
+ satisfaz `requires_python` do release; sem compatível, o install é
108
+ recusado antes de qualquer mutação.
109
+
110
+ - `forge-cli registry` lista o catálogo (7 dists) e o instalado.
111
+ - Receipts em `install.json`/`state.json`: dist, versão, interpretador,
112
+ digests sha256 e verify — o artefato da dist é conferido byte a byte
113
+ após o download.
114
+ - `--offline` resolve do cache local (metadados em
115
+ `~/.forge-cli/cache/pypi/`, wheels em `cache/wheels/<dist>/`);
116
+ artefato ausente = recusa honesta, nunca instalação fingida.
117
+ - Releases yanked (PEP 592) são puladas na escolha automática; só
118
+ instalam por pin explícito e ficam marcadas no receipt.
119
+ - `uninstall --method registry` remove venv + receipt com plano e
120
+ confirmação (`--yes`); `--all`/`--only` resolvem o conjunto instalado.
99
121
 
100
122
  ## Independência
101
123
 
@@ -111,6 +133,10 @@ forge-cli status # estado local (clonado, versão, a
111
133
  forge-cli update [--check] # atualiza (ou só verifica) Forjas clonadas
112
134
  forge-cli versions api-forge # revisão e tags
113
135
  forge-cli install api-forge --version 1.2.0 # fixa em tag/sha (checkout destacado)
136
+ forge-cli install api-forge --method registry # instala do PyPI em venv isolada (~/.forge-cli/forges/)
137
+ forge-cli install --all --method registry # todas as Forjas publicadas via PyPI
138
+ forge-cli registry # catálogo PyPI + instaladas (--json estrutura)
139
+ forge-cli uninstall api-forge --method registry --yes # remove venv + receipt registry
114
140
  forge-cli pin|unpin|rollback api-forge # fixação e retorno ao HEAD anterior
115
141
  forge-cli reinstall api-forge [--clean] # mantém a revisão; --clean reclona com backup
116
142
  forge-cli uninstall api-forge [--yes] # plano + backup de .env/configs
@@ -235,12 +261,12 @@ factory/ # fluxo spec-driven (Loop Factory)
235
261
 
236
262
  ## Documentação
237
263
 
238
- - [Arquitetura e decisões de design](docs/arquitetura.md)
239
- - [Matriz de capacidades das Forjas](docs/capabilities.md)
240
- - [Solução de problemas](docs/solucao-de-problemas.md)
241
- - [Como contribuir](CONTRIBUTING.md)
242
- - [Registro de mudanças](CHANGELOG.md)
264
+ - [Arquitetura e decisões de design](https://github.com/EdgarSocrates98/forge-cli/blob/v0.1.0/docs/arquitetura.md)
265
+ - [Matriz de capacidades das Forjas](https://github.com/EdgarSocrates98/forge-cli/blob/v0.1.0/docs/capabilities.md)
266
+ - [Solução de problemas](https://github.com/EdgarSocrates98/forge-cli/blob/v0.1.0/docs/solucao-de-problemas.md)
267
+ - [Como contribuir](https://github.com/EdgarSocrates98/forge-cli/blob/v0.1.0/CONTRIBUTING.md)
268
+ - [Registro de mudanças](https://github.com/EdgarSocrates98/forge-cli/blob/v0.1.0/CHANGELOG.md)
243
269
 
244
270
  ## Licença
245
271
 
246
- [MIT](LICENSE) © 2026 Edgar Sócrates Caria
272
+ [MIT](https://github.com/EdgarSocrates98/forge-cli/blob/v0.1.0/LICENSE) © 2026 Edgar Sócrates Caria