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 +441 -1
- package/README.md +44 -18
- package/dist/index.js +5553 -1716
- package/docs/arquitetura.md +28 -26
- package/docs/capabilities.md +47 -0
- package/docs/registry-state-contract.md +123 -0
- package/docs/solucao-de-problemas.md +46 -0
- package/package.json +1 -1
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
|
-
## [
|
|
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
|
-
|
|
44
|
+
**Usuário final — via npm (recomendado):**
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|