@aksp/opencrew 1.6.2 → 1.7.0

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +70 -13
  3. package/package.json +2 -2
  4. package/src/cli.js +15 -38
  5. package/src/commands/init.js +41 -44
  6. package/src/commands/update.js +78 -75
  7. package/src/lib/blocos.js +148 -0
  8. package/src/lib/deteccao.js +69 -0
  9. package/src/lib/fsx.js +1 -55
  10. package/src/lib/ides.js +4 -0
  11. package/src/lib/legado.js +142 -0
  12. package/src/lib/manifest.js +67 -26
  13. package/src/lib/mcp.js +131 -0
  14. package/src/lib/migrations.js +77 -74
  15. package/src/lib/node-version.js +43 -0
  16. package/src/lib/prompts.js +25 -2
  17. package/src/lib/resumo.js +125 -0
  18. package/templates/.mcp.json +1 -1
  19. package/templates/AGENTS.md +20 -6
  20. package/templates/_opencrew/.opencrew-version +1 -1
  21. package/templates/_opencrew/core/best-practices/social-networks-publishing.md +14 -14
  22. package/templates/_opencrew/core/escritorio/animacao.js +64 -0
  23. package/templates/_opencrew/core/escritorio/app.js +137 -0
  24. package/templates/_opencrew/core/escritorio/cena.js +132 -0
  25. package/templates/_opencrew/core/escritorio/demo.js +79 -0
  26. package/templates/_opencrew/core/escritorio/escala.js +27 -0
  27. package/templates/_opencrew/core/escritorio/index.html +166 -0
  28. package/templates/_opencrew/core/escritorio/modelo-agentes.js +93 -0
  29. package/templates/_opencrew/core/escritorio/modelo-estado.js +71 -0
  30. package/templates/_opencrew/core/escritorio/modelo-mesas.js +81 -0
  31. package/templates/_opencrew/core/escritorio/modelo-pagina.js +95 -0
  32. package/templates/_opencrew/core/escritorio/modelo-textos.js +65 -0
  33. package/templates/_opencrew/core/escritorio/modelo-visao.js +91 -0
  34. package/templates/_opencrew/core/escritorio/modelo.js +29 -0
  35. package/templates/_opencrew/core/escritorio/painel.js +120 -0
  36. package/templates/_opencrew/core/escritorio/quadro.js +106 -0
  37. package/templates/_opencrew/core/escritorio/rota.js +62 -0
  38. package/templates/_opencrew/core/escritorio/rotulos.js +78 -0
  39. package/templates/_opencrew/core/escritorio/sprites-mesa.js +122 -0
  40. package/templates/_opencrew/core/escritorio/sprites-sala.js +92 -0
  41. package/templates/_opencrew/core/escritorio/sprites.js +187 -0
  42. package/templates/_opencrew/core/prompts/build.prompt.md +3 -3
  43. package/templates/_opencrew/core/prompts/export.prompt.md +1 -1
  44. package/templates/_opencrew/core/prompts/repair.prompt.md +7 -12
  45. package/templates/_opencrew/core/prompts/sherlock-shared.md +5 -5
  46. package/templates/_opencrew/core/runner.pipeline.md +76 -139
  47. package/templates/_opencrew/core/scripts/comum.mjs +49 -4
  48. package/templates/_opencrew/core/scripts/conferir-fontes/busca.mjs +42 -3
  49. package/templates/_opencrew/core/scripts/conferir-fontes/relatorio.mjs +18 -6
  50. package/templates/_opencrew/core/scripts/conferir-fontes.mjs +97 -39
  51. package/templates/_opencrew/core/scripts/escritorio/leitura.mjs +31 -0
  52. package/templates/_opencrew/core/scripts/escritorio/porta.mjs +98 -0
  53. package/templates/_opencrew/core/scripts/escritorio/projeto.mjs +29 -0
  54. package/templates/_opencrew/core/scripts/escritorio/servidor.mjs +78 -0
  55. package/templates/_opencrew/core/scripts/escritorio.mjs +117 -0
  56. package/templates/_opencrew/core/scripts/estado/argumentos.mjs +61 -0
  57. package/templates/_opencrew/core/scripts/estado/arquivo.mjs +53 -0
  58. package/templates/_opencrew/core/scripts/estado/decisao.mjs +56 -0
  59. package/templates/_opencrew/core/scripts/estado/elenco.mjs +58 -0
  60. package/templates/_opencrew/core/scripts/estado/nucleo.mjs +113 -0
  61. package/templates/_opencrew/core/scripts/estado/preferencia.mjs +24 -0
  62. package/templates/_opencrew/core/scripts/estado.mjs +96 -0
  63. package/templates/_opencrew/core/scripts/verificar.mjs +7 -4
  64. package/templates/_opencrew/core/skills.engine.md +7 -3
  65. package/templates/gitignore +1 -0
  66. package/templates/skills/blotato/SKILL.md +39 -10
  67. package/templates/skills/image-ai-generator/SKILL.md +18 -5
  68. package/templates/skills/image-ai-generator/scripts/generate.py +52 -10
  69. package/templates/skills/instagram-publisher/SKILL.md +4 -0
  70. package/templates/skills/opencrew-skill-creator/references/skill-format.md +1 -0
  71. package/templates/skills/resend/SKILL.md +52 -13
package/CHANGELOG.md CHANGED
@@ -3,6 +3,108 @@
3
3
  All notable changes to opencrew are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
5
5
 
6
+ ## [1.7.0] — 2026-10-06
7
+
8
+ Fase E1 "Escritório ao vivo — a equipe trabalhando, em 8 bits"
9
+ (`specs/fase-e1-escritorio-ao-vivo.md`). Chega a quem já usa com um
10
+ `npx @aksp/opencrew@latest update`.
11
+
12
+ ### Added
13
+ - **Escritório ao vivo.** Uma página em pixel-art, aberta no navegador, mostra a crew
14
+ trabalhando: cada agente na sua mesa, digitando na vez dele, levando o papel ao colega na
15
+ passagem de bastão, de mão levantada quando espera a sua resposta, com ✓ quando termina e "!"
16
+ quando falha. Ao lado, o passo atual, a lista dos agentes com o status por extenso e o que cada
17
+ um fez, e a última passagem de bastão. O título da aba acompanha a execução.
18
+ - **`/opencrew dashboard`** liga o Escritório, sobe a página e mostra o endereço
19
+ (`http://127.0.0.1:4747`, ou a porta livre seguinte); repetir o comando devolve o mesmo
20
+ endereço. **`/opencrew dashboard off`** desliga. Continua desligado por padrão.
21
+ - Roda só no seu computador, sem internet e sem medição: o servidor
22
+ (`_opencrew/core/scripts/escritorio.mjs`) escuta só em `127.0.0.1`, só lê e não escreve em disco.
23
+ - Sem execução nenhuma, a página roda uma demonstração e troca sozinha para a execução real
24
+ quando ela aparece (`?demo` no endereço força a demonstração). Com mais de uma crew, mostra a
25
+ de atualização mais recente e um seletor com as outras.
26
+ - A página não mente sobre o que não sabe: execução há mais de 2 minutos sem novidade mostra há
27
+ quanto tempo foi a última atualização; há mais de 20, o agente sai da pose de digitar e aparece
28
+ "sem sinal". Servidor fora do ar: a página avisa e tenta de novo sozinha.
29
+
30
+ ### Changed
31
+ - **Quem avisa o Escritório é um script, não a IA escrevendo JSON.** Com o Escritório ligado, o
32
+ runner roda um comando curto por passo (`_opencrew/core/scripts/estado.mjs`). Checkpoint, agente
33
+ pulado e execução que falha passam a aparecer; antes nunca eram gravados. Falha desse comando
34
+ não para a execução: o runner avisa uma vez e segue.
35
+ - **Com o Escritório ligado, o estado final deixa de ser copiado para
36
+ `crews/<crew>/output/<run>/state.json`.** O estado da execução mora só em
37
+ `crews/<crew>/state.json`.
38
+ - `state.json`: os agentes ganham os status `checkpoint` e `failed` e o campo `label`; a
39
+ execução ganha `checkpoint` e `failed`; `desk` e `delivering` deixam de ser gravados. Arquivo
40
+ gravado por versão anterior continua sendo lido pela página.
41
+ - Os prompts de criar e de consertar crew não escrevem mais `state.json`.
42
+ - README: a nota "o dashboard não é instalado" deu lugar à seção "Escritório ao vivo".
43
+
44
+ ### Removed
45
+ - A pasta `dashboard/` do repositório (o desenho antigo, que nunca foi instalado pelo `init`). O
46
+ que servia migrou para `_opencrew/core/escritorio/`, sem os defeitos apontados na auditoria:
47
+ nome de agente entrava na página como HTML, o modo ao vivo lia o arquivo no lugar errado e a
48
+ página congelava se a primeira leitura falhasse.
49
+
50
+ ### Internal
51
+ - `npm run verify`: o lint passa a cobrir `templates/_opencrew/core/escritorio/` (com os nomes
52
+ globais de navegador) e o alerta de tamanho mede os `.js` dessa pasta. Travas novas:
53
+ `tests/estado*.test.js`, `tests/escritorio*.test.js`, `tests/runtime-contracts-e1.test.js`,
54
+ `tests/docs-e1.test.js` e os cenários E1-07a, E1-07c e E1-upg nos testes de pacote, de
55
+ referências e de upgrade. A trava de conteúdo do mantenedor passa a ler também `.mjs` e `.css`.
56
+
57
+ ## [1.6.3] — 2026-10-06
58
+
59
+ **O Node mínimo subiu para o 20.17, numa versão de correção.** O pacote dizia 20.0, mas a lista
60
+ de IDEs do `init` só abria a partir do 20.12 e as dependências só garantem o 20.17. Quem está no
61
+ Node 20.0 a 20.16 precisa atualizar o Node antes de rodar `init` ou `update` (saída de emergência:
62
+ `npx @aksp/opencrew@1.6.2 update`).
63
+
64
+ Fase R2 "update e envio seguros" (`specs/fase-r2-update-e-envio-seguros.md`): só defeitos já
65
+ achados em revisão. Chega a quem já usa com um `npx @aksp/opencrew@latest update`.
66
+
67
+ ### Changed
68
+ - **`blotato` e `resend` pedem confirmação antes de agir.** Mostram a prévia (contas ou
69
+ destinatários, texto, quando) e só publicam, enviam, agendam ou apagam depois da palavra
70
+ `publicar`, `enviar` ou `apagar`. Crew que hoje envia sem perguntar vai parar e pedir. Em falha,
71
+ não repetem sozinhas.
72
+ - **`.mcp.json`: o servidor Playwright é entregue uma última vez.** Depois, se você o remover ou
73
+ apagar o arquivo, o `update` não repõe. A versão fixada não é trocada, e o arquivo é copiado
74
+ antes de qualquer regravação, mantendo a indentação.
75
+ - **Primeiro `update` para esta versão copia o `.gitignore`** para `.opencrew-backup/`, mesmo sem
76
+ edição sua: o bloco do OpenCrew mudou (passa a ignorar `.opencrew-backup/`) e as versões
77
+ anteriores não registravam o bloco.
78
+ - **O reparo de pontes só roda com o pacote na mesma versão do projeto.** Com outra versão, para
79
+ sem alterar nada e pede o `update`.
80
+ - As frases do resumo do `update` saem em português e só afirmam o que foi feito.
81
+
82
+ ### Fixed
83
+ - **Ponte de IDE que você não instalou**: um `CLAUDE.md`, `GEMINI.md`, `QWEN.md` ou
84
+ `copilot-instructions.md` seu que só citava "opencrew" fazia o `update` criar a ponte e pôr um
85
+ bloco no seu arquivo. A IDE agora se prova pelo arquivo de ponte. O resumo não cita mais o
86
+ Codex sem ele estar instalado.
87
+ - **Bloco do OpenCrew editado por dentro** (em `AGENTS.md`, `CLAUDE.md`, `.gitignore`…) era
88
+ regravado sem cópia. Agora o arquivo inteiro é copiado antes, e o fim de linha é mantido.
89
+ - **Texto antigo das pontes** (instalações até a 1.2.2), que mandava adotar o papel do OpenCrew
90
+ sempre, é retirado, com cópia, quando está idêntico ao gerado; se foi editado, só aviso.
91
+ - **Manifesto ilegível** vira aviso e a atualização segue; `.mcp.json` fora do formato não derruba
92
+ mais o `update` no meio.
93
+ - **A dica de reinstalar** não manda mais apagar `_opencrew/`, que guarda a sua memória.
94
+ - **Restos do OpenSquad**: o aviso não promete mais "apagar com segurança" para arquivo que pode
95
+ ser seu, e cobre mais sete caminhos. Nada é apagado.
96
+ - **Texto do usuário em linha de comando**: o prompt de imagem vai por arquivo (`--prompt-file`),
97
+ nome de arquivo com caractere inseguro não entra em comando, e caminhos e URLs vão entre aspas.
98
+ - **Conferência de fontes**: o `--corrigir` não grava mais fora da crew por um link; caminho de
99
+ rede e endereço de site citados viram alerta "não conferido", sem tocar a rede e sem parar a
100
+ execução; erro de leitura sai com mensagem.
101
+ - **Publicação**: a tag só publica depois do CI verde (Ubuntu e Windows, Node 20.17 e 22) e da
102
+ auditoria de segurança. Três das cinco últimas versões saíram com o CI vermelho.
103
+
104
+ ### Internal
105
+ - CLI em módulos novos (`blocos`, `deteccao`, `legado`, `mcp`, `resumo`, `node-version`); 47
106
+ cenários R2 com teste de mesmo ID; teste de upgrade 1.6.2 → 1.6.3.
107
+
6
108
  ## [1.6.2] — 2026-10-05
7
109
 
8
110
  Correção da 1.6.1. Chega a quem já usa com um `npx @aksp/opencrew@latest update`.
package/README.md CHANGED
@@ -40,6 +40,9 @@ dentro da sua IDE.**
40
40
  termos que você proibiu e `[PREENCHER]` pendentes, e aponta afirmações a confirmar.
41
41
  Bloqueio não passa, seja qual for a nota do revisor. A crew não inventa casos nem números:
42
42
  quando falta um dado real, ela pergunta na aprovação final.
43
+ - 🖥️ **Escritório ao vivo** — veja a equipe trabalhando numa sala em pixel-art, no navegador:
44
+ quem está digitando, quem passou o bastão, quem espera a sua resposta. Opcional, desligado por
45
+ padrão, só neste computador. Liga com `/opencrew dashboard`.
43
46
  - 📂 **Crew que conhece o projeto** — liste em `fontes:` os arquivos e pastas do seu projeto
44
47
  (decisões, calendário, manual de marca) e a crew os lê em todo run, tratando-os como verdade.
45
48
  Reorganizou as pastas? No início do run ela confere os caminhos, acha para onde o arquivo foi
@@ -69,7 +72,7 @@ dentro da sua IDE.**
69
72
 
70
73
  ### Pré-requisitos
71
74
 
72
- - **Node.js 20+** ([baixar](https://nodejs.org/))
75
+ - **Node.js 20.17 ou mais novo** ([baixar](https://nodejs.org/))
73
76
  - Uma IDE de IA com acesso a arquivos locais: **Claude Code**, **Cursor**,
74
77
  **Codex (OpenAI)**, **Gemini CLI**, **Google Antigravity**, **OpenCode**,
75
78
  **VS Code + Copilot**, **Qwen Code** ou **Trae**.
@@ -175,6 +178,8 @@ meu-projeto/
175
178
  │ │ ├── skills.engine.md ← gerenciador de skills
176
179
  │ │ ├── architect.agent.yaml ← definição do Arquiteto
177
180
  │ │ ├── best-practices/ ← 22 guias de melhores práticas + _catalog.yaml
181
+ │ │ ├── scripts/ ← verificador, conferência de fontes e os scripts do Escritório
182
+ │ │ ├── escritorio/ ← página do Escritório ao vivo (abre com /opencrew dashboard)
178
183
  │ │ └── prompts/ ← 13 prompts de fase (discovery, design, build, etc.)
179
184
  │ ├── agents/ ← 5 agentes base compartilhados
180
185
  │ │ ├── researcher.agent.md
@@ -184,7 +189,7 @@ meu-projeto/
184
189
  │ │ └── strategist.agent.md
185
190
  │ ├── _memory/
186
191
  │ │ ├── company.md ← perfil da sua empresa (onboarding)
187
- │ │ └── preferences.md ← idioma, tier padrão, dashboard
192
+ │ │ └── preferences.md ← idioma, tier padrão, Escritório ligado ou desligado
188
193
  │ └── .opencrew-version
189
194
  │
190
195
  ├── crews/ ← suas crews vivem aqui
@@ -202,9 +207,47 @@ meu-projeto/
202
207
  │ └── ...
203
208
  ```
204
209
 
205
- > O dashboard visual (`dashboard/index.html`) **não é instalado** pelo `init` — ele vive
206
- > só no repositório do OpenCrew e ainda é experimental (decisão de publicar ou remover:
207
- > fase U3a — ver `IDEIAS.md` no repositório).
210
+ ---
211
+
212
+ ## Escritório ao vivo
213
+
214
+ Quer ver a equipe trabalhando? O **Escritório** é uma página em pixel-art, aberta no navegador,
215
+ em que cada agente tem a sua mesa: digita quando é a vez dele, leva o papel ao colega na passagem
216
+ de bastão e levanta a mão quando espera uma resposta sua. Ao lado do desenho ficam o passo atual,
217
+ a lista dos agentes (com o status por extenso e o que cada um fez) e a última passagem de bastão.
218
+
219
+ **Como abrir:** no chat da sua IDE, digite `/opencrew dashboard`. O comando liga o Escritório,
220
+ sobe a página e mostra o endereço — `http://127.0.0.1:4747`, ou a porta livre seguinte. A próxima
221
+ execução de crew aparece ali; enquanto não há nenhuma, a página roda uma demonstração. Repetir o
222
+ comando é seguro: ele devolve o mesmo endereço.
223
+
224
+ Se a sua IDE não roda comando em segundo plano, ela mostra o comando para você rodar em outro
225
+ terminal, na pasta do projeto:
226
+
227
+ ```bash
228
+ node _opencrew/core/scripts/escritorio.mjs # Ctrl+C para fechar
229
+ node _opencrew/core/scripts/escritorio.mjs --porta 5000
230
+ ```
231
+
232
+ Para desligar: `/opencrew dashboard off`.
233
+
234
+ O que vale saber antes de ligar:
235
+
236
+ - **Vem desligado.** Sem o `/opencrew dashboard`, nada muda nas suas execuções.
237
+ - **Roda só neste computador, sem internet e sem medição.** A página é servida em `127.0.0.1`,
238
+ só lê o estado das suas crews (`crews/<crew>/state.json`), não carrega nada de fora e não envia
239
+ dado nenhum para lugar nenhum.
240
+ - **Com ele ligado, cada passo custa um comando curto a mais.** É assim que a IA avisa o que está
241
+ fazendo: um comando de terminal por passo. Em IDE que pede aprovação a cada comando, libere o
242
+ `estado.mjs` uma vez.
243
+ - **A tela mostra o que a IA avisa, e pode atrasar.** Se a IA pular um aviso, o desenho só se
244
+ acerta no passo seguinte. Depois de 2 minutos sem novidade a página diz há quanto tempo foi a
245
+ última atualização; depois de 20, o agente aparece "sem sinal" — o que não quer dizer que
246
+ travou: um passo longo é normal.
247
+ - **O Escritório nunca para a execução.** Se o aviso falhar, a crew segue trabalhando.
248
+ - **Até 12 mesas.** Numa crew maior, os agentes a mais aparecem só na lista ao lado.
249
+
250
+ Quem já usa o OpenCrew recebe o Escritório com um `npx @aksp/opencrew@latest update`.
208
251
 
209
252
  ---
210
253
 
@@ -219,14 +262,24 @@ o que você fez:
219
262
 
220
263
  | O que é atualizado | O que NUNCA é tocado |
221
264
  |---|---|
222
- | `_opencrew/core/` (framework) e skills do catálogo | `crews/` (suas crews) |
223
- | Pastas novas do framework (agentes-base, config) — só o que falta | `_opencrew/_memory/` (perfil, preferências) |
265
+ | `_opencrew/core/` (framework) e skills do catálogo | As crews que você criou em `crews/` |
266
+ | Pastas novas do framework (agentes-base, config) e modelos de crew — só o que falta | `_opencrew/_memory/` (perfil, preferências) |
224
267
  | Pontes das IDEs **que você já tem instaladas** (nunca cria de IDE nova) | `_opencrew/best-practices.local/` (suas best-practices) |
225
- | Bloco `<!-- opencrew -->` do `AGENTS.md`/`CLAUDE.md` (o resto do arquivo fica intacto) | `.env` (suas chaves) |
226
- | Servidor Playwright no `.mcp.json` (outros servidores intactos) | |
227
-
228
- - **Editou um arquivo do framework ou um skill do catálogo?** Antes de substituir, o `update`
229
- guarda a sua versão em `.opencrew-backup/<data>/` e lista o que copiou.
268
+ | Bloco do OpenCrew em `AGENTS.md`, `CLAUDE.md` e `.gitignore` (o resto do arquivo fica intacto) | `.env` (suas chaves) |
269
+ | Servidor Playwright no `.mcp.json`, entregue uma vez (outros servidores intactos) | |
270
+
271
+ - **Editou um arquivo do framework, um skill do catálogo ou o bloco do OpenCrew?** Antes de
272
+ substituir, o `update` guarda o arquivo inteiro em `.opencrew-backup/<data>/` e lista o que
273
+ copiou. Essa pasta fica fora do git (entra no bloco do `.gitignore`). No primeiro `update` para
274
+ a 1.6.3 há cópia do `.gitignore` mesmo sem edição sua: as versões anteriores não registravam o
275
+ bloco.
276
+ - **Apagou um modelo de crew ou um skill do catálogo?** Ele volta no `update`, e a saída diz o
277
+ que foi entregue de novo.
278
+ - **Removeu o servidor Playwright do `.mcp.json`?** O `update` o entrega uma única vez; se você
279
+ remover de novo, não volta. A versão fixada no arquivo não é trocada.
280
+ - **Uma IDE só conta como instalada pelo arquivo de ponte dela**, não por um arquivo seu que cite
281
+ o OpenCrew. Texto antigo das pontes (instalações até a 1.2.2) é retirado, com cópia, quando
282
+ está idêntico ao que o OpenCrew gravou.
230
283
  - **Versão mais nova instalada?** O `update` não volta para uma versão mais antiga (cache do
231
284
  `npx`): ele para e pede `npx @aksp/opencrew@latest update`.
232
285
 
@@ -241,6 +294,8 @@ npx @aksp/opencrew@latest init --repair-bridges --all # as 9 IDEs
241
294
  Sem `--ide` e sem `--all`, o `init --repair-bridges` usa a mesma detecção do `update` (aqui o
242
295
  `--yes` não escolhe IDE); se não encontra nenhuma ponte, para com erro e pede `--ide=<id>`.
243
296
  O reparo não instala: numa pasta sem workspace do OpenCrew ele para com erro e pede o `init`.
297
+ Ele também só roda com o pacote na mesma versão do projeto: com outra versão, para sem alterar
298
+ nada e pede o `update`.
244
299
  Ponte de arquivo inteiro que você editou (ex.: `.claude/skills/opencrew/SKILL.md`) é copiada
245
300
  antes para `.opencrew-backup/<data>/`, e o resumo do `init --repair-bridges` lista cada cópia.
246
301
 
@@ -272,7 +327,9 @@ npx @aksp/opencrew update --check
272
327
  | `/opencrew delete <nome>` | Remove uma crew |
273
328
  | `/opencrew skills` | Navega, instala ou remove skills |
274
329
  | `/opencrew install <skill>` | Instala uma skill do catálogo |
275
- | `/opencrew settings` | Altera preferências (idioma, tier, dashboard) |
330
+ | `/opencrew settings` | Altera preferências (idioma, tier, Escritório) |
331
+ | `/opencrew dashboard` | Liga e abre o Escritório ao vivo (a equipe trabalhando, no navegador) |
332
+ | `/opencrew dashboard off` | Desliga o Escritório |
276
333
  | `/opencrew show-company` | Mostra o perfil da empresa |
277
334
  | `/opencrew edit-company` | Reconfigura o perfil da empresa |
278
335
  | `/opencrew help` | Mostra a lista de comandos |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aksp/opencrew",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "license": "MIT",
52
52
  "engines": {
53
- "node": ">=20.0.0"
53
+ "node": ">=20.17.0"
54
54
  },
55
55
  "dependencies": {
56
56
  "@inquirer/checkbox": "^5.1.0",
package/src/cli.js CHANGED
@@ -5,34 +5,9 @@ import { allIdeIds } from './lib/ides.js';
5
5
  import { UsageError, isPromptCancel } from './lib/errors.js';
6
6
  import { init } from './commands/init.js';
7
7
  import { update } from './commands/update.js';
8
+ import { nodeBelowFloor } from './lib/node-version.js';
8
9
  import { c, log, err, warn, info } from './lib/ui.js';
9
10
 
10
- // Extract the minimum required Node version from an engines.node range string.
11
- // Handles: ">=20.0.0", "^20.5", ">=18.0.0 || >=20.0.0", plain "20.0.0".
12
- function minNodeVersion(range) {
13
- // Split on || and take the lowest version (user is expected to meet at least one).
14
- const parts = range.split(/\s*\|\|\s*/);
15
- let lowest = null;
16
- for (const part of parts) {
17
- const v = part.replace(/[^0-9.]/g, '');
18
- if (!v) continue;
19
- if (!lowest || lt(v, lowest)) lowest = v;
20
- }
21
- return lowest;
22
- }
23
-
24
- // Simple semver comparison (no prerelease tags). Returns true if a < b.
25
- function lt(a, b) {
26
- const pa = a.split('.').map(Number);
27
- const pb = b.split('.').map(Number);
28
- for (let i = 0; i < 3; i++) {
29
- const na = pa[i] || 0;
30
- const nb = pb[i] || 0;
31
- if (na !== nb) return na < nb;
32
- }
33
- return false; // equal
34
- }
35
-
36
11
  const OPTION_SPEC = {
37
12
  help: { type: 'boolean', short: 'h' },
38
13
  version: { type: 'boolean', short: 'v' },
@@ -128,11 +103,15 @@ export function reportError(e) {
128
103
  }
129
104
  err(e?.message ?? String(e));
130
105
  if (e instanceof UsageError) info(`Run ${c.cyan('npx @aksp/opencrew help')} for usage.`);
131
- else if (process.env.OPENCREW_DEBUG) console.error(e?.stack);
106
+ else if (process.env.OPENCREW_DEBUG) console.error(e?.cause ? e : e?.stack); // with a cause: both stacks
132
107
  return 1;
133
108
  }
134
109
 
135
- export async function run(argv, { commands = { init, update } } = {}) {
110
+ /**
111
+ * @param {string[]} argv
112
+ * @param {{ commands?: object, nodeVersion?: string }} deps injectable for tests
113
+ */
114
+ export async function run(argv, { commands = { init, update }, nodeVersion = process.versions.node } = {}) {
136
115
  let command, opts;
137
116
  try {
138
117
  ({ command, opts } = parseArgs(argv));
@@ -154,16 +133,14 @@ export async function run(argv, { commands = { init, update } } = {}) {
154
133
  return;
155
134
  }
156
135
 
157
- // Validate Node version against engines.node requirement.
158
- if (engines.node) {
159
- const required = minNodeVersion(engines.node);
160
- const current = process.versions.node;
161
- if (required && lt(current, required)) {
162
- warn(`opencrew requires Node.js ${engines.node}. You have v${current}.`);
163
- info(`Upgrade Node or use a compatible version.`);
164
- process.exitCode = 1;
165
- return;
166
- }
136
+ // Below the floor of engines.node every command stops here — --version and help too —
137
+ // before anything is written (spec R2, rule 27).
138
+ const tooOld = nodeBelowFloor(engines.node, nodeVersion);
139
+ if (tooOld) {
140
+ err(tooOld[0]);
141
+ info(tooOld[1]);
142
+ process.exitCode = 1;
143
+ return;
167
144
  }
168
145
 
169
146
  // --version / --help never run a command (they may follow any command).
@@ -1,17 +1,19 @@
1
1
  import path from 'node:path';
2
2
  import { promises as fs } from 'node:fs';
3
3
  import { templatesDir, packageJsonPath } from '../lib/paths.js';
4
- import { exists, writeFileSafe, readJson, writeBridgeFile } from '../lib/fsx.js';
5
- import { newDelivery, deliverTree, deliverFile, writeManifest, readManifest } from '../lib/manifest.js';
4
+ import { exists, readJson } from '../lib/fsx.js';
5
+ import { newDelivery, deliverTree, deliverFile, writeManifest, readManifest, manifestUnreadable, UNREADABLE } from '../lib/manifest.js';
6
+ import { deliverBlock, deliverBridges } from '../lib/blocos.js';
7
+ import { createMcp } from '../lib/mcp.js';
6
8
  import { ideById, allIdeIds, AGENTS_BRIDGE } from '../lib/ides.js';
7
9
  import { pickIdes as promptIdes } from '../lib/prompts.js';
8
10
  import { UsageError } from '../lib/errors.js';
9
- import { repairIdeIds, backupSummary, recordRepair, NO_BRIDGES_FOUND, NO_WORKSPACE } from '../lib/migrations.js';
11
+ import { withoutLegacy, legacyLines } from '../lib/legado.js';
12
+ import { repairIdeIds, repairVersionGuard, backupSummary, recordRepair, NO_BRIDGES_FOUND, NO_WORKSPACE } from '../lib/migrations.js';
13
+ import { ALREADY_INSTALLED } from '../lib/resumo.js';
10
14
  import { c, log, info, ok, warn, step } from '../lib/ui.js';
11
15
 
12
16
  const STAMP = path.join('_opencrew', '.opencrew-version');
13
- // .gitignore / .env.example belong to the user: opencrew only owns a marked block at the end.
14
- const SHARED_BLOCK = { comment: 'hash', position: 'append' };
15
17
 
16
18
  /**
17
19
  * @param {object} opts parsed CLI options
@@ -28,9 +30,8 @@ export async function init(opts = {}, { pickIdes = promptIdes } = {}) {
28
30
 
29
31
  if (state === 'complete') {
30
32
  warn('An opencrew workspace already exists here.');
31
- info(`To update only the framework, use: ${c.cyan('npx @aksp/opencrew@latest update')}`);
33
+ info(ALREADY_INSTALLED); // R2 rule 15: never tells the user to delete _opencrew/
32
34
  info(`To repair IDE bridges, use: ${c.cyan('npx @aksp/opencrew@latest init --repair-bridges')}`);
33
- info(`To reinstall from scratch, delete _opencrew/ first, then run init again.`);
34
35
  return;
35
36
  }
36
37
 
@@ -56,23 +57,22 @@ export async function init(opts = {}, { pickIdes = promptIdes } = {}) {
56
57
  await deliverFile(ctx, path.join(target, '_opencrew', 'core', 'system.md'), await tpl('AGENTS.md'), { overwrite: true });
57
58
  ok('_opencrew/core/system.md (full system definition)');
58
59
 
59
- const agentsResult = await writeBridgeFile(path.join(target, 'AGENTS.md'), AGENTS_BRIDGE);
60
- if (agentsResult.merged) info('AGENTS.md (merged — existing content preserved)');
60
+ const agents = await deliverBlock(ctx, 'AGENTS.md', AGENTS_BRIDGE);
61
+ if (agents.action === 'added') info('AGENTS.md (merged — existing content preserved)');
61
62
  else ok('AGENTS.md (bridge to system.md)');
62
63
 
63
- const mcpWritten = await writeFileSafe(path.join(target, '.mcp.json'), await tpl('.mcp.json'), {
64
- overwrite: false,
65
- });
66
- info(mcpWritten ? '.mcp.json' : '.mcp.json (kept existing)');
64
+ // Created here → recorded in the manifest as delivered; the user's own file is kept as it is.
65
+ info((await createMcp(ctx, await tpl('.mcp.json'))) ? '.mcp.json' : '.mcp.json (kept existing)');
67
66
 
67
+ // .gitignore / .env.example belong to the user: opencrew only owns a marked block at the end.
68
68
  for (const [file, template] of [['.env.example', '.env.example'], ['.gitignore', 'gitignore']]) {
69
- const res = await writeBridgeFile(path.join(target, file), await tpl(template), SHARED_BLOCK);
70
- info(res.merged ? `${file} (opencrew block added at the end — your lines kept)` : file);
69
+ const res = await deliverBlock(ctx, file, await tpl(template));
70
+ info(res.action === 'added' ? `${file} (opencrew block added at the end — your lines kept)` : file);
71
71
  }
72
72
 
73
73
  // 3. IDE bridge files.
74
74
  step('Configuring AI IDEs');
75
- await writeBridges(target, ids, { overwrite: false, ctx });
75
+ await writeBridges(ctx, ids, false);
76
76
 
77
77
  if (ids.includes('claude-code')) {
78
78
  warn(`opencrew ships its own Playwright MCP server (.mcp.json) — disable Claude Code's`);
@@ -83,6 +83,7 @@ export async function init(opts = {}, { pickIdes = promptIdes } = {}) {
83
83
  // version stamp LAST: it is what marks the install as complete.
84
84
  await writeManifest(target, version, ctx.files);
85
85
  await fs.writeFile(path.join(target, STAMP), version + '\n');
86
+ reportBackups(ctx); // a reinstall over blocks the user edited copies those files first
86
87
 
87
88
  // 5. Done.
88
89
  log(`\n${c.green(c.bold('Done!'))} opencrew is installed.\n`);
@@ -95,20 +96,30 @@ export async function init(opts = {}, { pickIdes = promptIdes } = {}) {
95
96
  /**
96
97
  * --repair-bridges: rewrite IDE bridge files in an existing workspace. --ide wins (even next
97
98
  * to --all); --all alone means every IDE; otherwise only the IDEs `update` would detect.
99
+ * Only with the package at the version of the workspace (R2 rule 6), whatever the options.
98
100
  */
99
101
  async function repairBridges(target, version, opts) {
102
+ const otherVersion = await repairVersionGuard(target, version);
103
+ if (otherVersion) throw new Error(otherVersion); // before any write; exit 1, no usage hint
100
104
  const ids = await resolveIdes({ ide: opts.ide }, () => repairIdeIds(target, opts));
101
105
  if (!ids.length) throw new UsageError(NO_BRIDGES_FOUND); // before the first write
102
106
  log(`\n${c.bold(c.cyan('opencrew'))} ${c.dim('v' + version)} — repairing IDE bridges`);
103
107
  log(c.dim(`Target: ${target}\n`));
104
108
  const ctx = newDelivery(target, await readManifest(target));
105
- await writeBridges(target, ids, { overwrite: true, ctx });
106
- await recordRepair(ctx, version); // only where a manifest already exists
109
+ await withoutLegacy(ctx, ids.map(ideById), () => writeBridges(ctx, ids, true)); // R2 rule 3
110
+ for (const line of legacyLines(ctx)) warn(line);
111
+ if (await manifestUnreadable(target)) warn(UNREADABLE.repair); // R2 rule 12: copied as with none
112
+ await recordRepair(ctx, version); // only where a manifest already exists (and can be read)
113
+ reportBackups(ctx);
114
+ log(`\n${c.green(c.bold('Done!'))} IDE bridges regenerated.\n`);
115
+ log(`${c.bold('Next step:')} Restart your IDE, then type ${c.cyan('/opencrew')} to verify.\n`);
116
+ }
117
+
118
+ /** Say where the backup copies of this run are (nothing when none was made). */
119
+ function reportBackups(ctx) {
107
120
  const [copied, ...copies] = backupSummary(ctx);
108
121
  if (copied) warn(copied);
109
122
  for (const copy of copies) log(copy);
110
- log(`\n${c.green(c.bold('Done!'))} IDE bridges regenerated.\n`);
111
- log(`${c.bold('Next step:')} Restart your IDE, then type ${c.cyan('/opencrew')} to verify.\n`);
112
123
  }
113
124
 
114
125
  /**
@@ -155,31 +166,17 @@ async function resolveIdes(opts, fallback) {
155
166
  }
156
167
 
157
168
  /**
158
- * Write IDE bridge files to the target directory.
159
- * @param {string} target — project root
160
- * @param {string[]} ids — validated IDE ids to configure
161
- * @param {{ overwrite: boolean }} opts
169
+ * Write the bridge files of the IDEs `ids` (validated) and say what happened to each.
170
+ * `overwrite`: replace a whole-file bridge that differs (repair) or keep it (init).
162
171
  */
163
- async function writeBridges(target, ids, { overwrite, ctx }) {
164
- const writtenPaths = new Set();
165
-
166
- for (const id of ids) {
167
- const ide = ideById(id);
168
- for (const f of ide.files) {
169
- if (writtenPaths.has(f.path)) {
170
- info(`${f.path} (shared path — written once)`);
171
- continue;
172
- }
173
- writtenPaths.add(f.path);
174
- const fp = path.join(target, f.path);
175
- const hasFrontmatter = f.content.startsWith('---');
176
- if (hasFrontmatter) {
177
- await deliverFile(ctx, fp, f.content, { overwrite });
178
- } else {
179
- const result = await writeBridgeFile(fp, f.content);
180
- if (result.merged) info(`${f.path} (merged — existing content preserved)`);
181
- else if (result.written && overwrite) info(`${f.path} (regenerated)`);
182
- }
172
+ async function writeBridges(ctx, ids, overwrite) {
173
+ const ides = ids.map(ideById);
174
+ const written = await deliverBridges(ctx, ides, { overwrite });
175
+ for (const ide of ides) {
176
+ for (const f of written.filter((w) => w.ide === ide)) {
177
+ if (f.shared) info(`${f.file} (shared path — written once)`);
178
+ else if (f.action === 'added') info(`${f.file} (merged — existing content preserved)`);
179
+ else if (f.block && f.action !== 'kept' && overwrite) info(`${f.file} (regenerated)`);
183
180
  }
184
181
  ok(`${ide.label} → ${ide.files.map((f) => f.path).join(', ')}`);
185
182
  }