@aksp/opencrew 1.6.2 → 1.6.3

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 (36) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +20 -8
  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/_opencrew/.opencrew-version +1 -1
  20. package/templates/_opencrew/core/best-practices/social-networks-publishing.md +14 -14
  21. package/templates/_opencrew/core/prompts/export.prompt.md +1 -1
  22. package/templates/_opencrew/core/prompts/sherlock-shared.md +5 -5
  23. package/templates/_opencrew/core/runner.pipeline.md +32 -12
  24. package/templates/_opencrew/core/scripts/comum.mjs +49 -4
  25. package/templates/_opencrew/core/scripts/conferir-fontes/busca.mjs +42 -3
  26. package/templates/_opencrew/core/scripts/conferir-fontes/relatorio.mjs +18 -6
  27. package/templates/_opencrew/core/scripts/conferir-fontes.mjs +97 -39
  28. package/templates/_opencrew/core/scripts/verificar.mjs +7 -4
  29. package/templates/_opencrew/core/skills.engine.md +7 -3
  30. package/templates/gitignore +1 -0
  31. package/templates/skills/blotato/SKILL.md +39 -10
  32. package/templates/skills/image-ai-generator/SKILL.md +18 -5
  33. package/templates/skills/image-ai-generator/scripts/generate.py +52 -10
  34. package/templates/skills/instagram-publisher/SKILL.md +4 -0
  35. package/templates/skills/opencrew-skill-creator/references/skill-format.md +1 -0
  36. package/templates/skills/resend/SKILL.md +52 -13
package/CHANGELOG.md CHANGED
@@ -3,6 +3,57 @@
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.6.3] — 2026-10-06
7
+
8
+ **O Node mínimo subiu para o 20.17, numa versão de correção.** O pacote dizia 20.0, mas a lista
9
+ de IDEs do `init` só abria a partir do 20.12 e as dependências só garantem o 20.17. Quem está no
10
+ Node 20.0 a 20.16 precisa atualizar o Node antes de rodar `init` ou `update` (saída de emergência:
11
+ `npx @aksp/opencrew@1.6.2 update`).
12
+
13
+ Fase R2 "update e envio seguros" (`specs/fase-r2-update-e-envio-seguros.md`): só defeitos já
14
+ achados em revisão. Chega a quem já usa com um `npx @aksp/opencrew@latest update`.
15
+
16
+ ### Changed
17
+ - **`blotato` e `resend` pedem confirmação antes de agir.** Mostram a prévia (contas ou
18
+ destinatários, texto, quando) e só publicam, enviam, agendam ou apagam depois da palavra
19
+ `publicar`, `enviar` ou `apagar`. Crew que hoje envia sem perguntar vai parar e pedir. Em falha,
20
+ não repetem sozinhas.
21
+ - **`.mcp.json`: o servidor Playwright é entregue uma última vez.** Depois, se você o remover ou
22
+ apagar o arquivo, o `update` não repõe. A versão fixada não é trocada, e o arquivo é copiado
23
+ antes de qualquer regravação, mantendo a indentação.
24
+ - **Primeiro `update` para esta versão copia o `.gitignore`** para `.opencrew-backup/`, mesmo sem
25
+ edição sua: o bloco do OpenCrew mudou (passa a ignorar `.opencrew-backup/`) e as versões
26
+ anteriores não registravam o bloco.
27
+ - **O reparo de pontes só roda com o pacote na mesma versão do projeto.** Com outra versão, para
28
+ sem alterar nada e pede o `update`.
29
+ - As frases do resumo do `update` saem em português e só afirmam o que foi feito.
30
+
31
+ ### Fixed
32
+ - **Ponte de IDE que você não instalou**: um `CLAUDE.md`, `GEMINI.md`, `QWEN.md` ou
33
+ `copilot-instructions.md` seu que só citava "opencrew" fazia o `update` criar a ponte e pôr um
34
+ bloco no seu arquivo. A IDE agora se prova pelo arquivo de ponte. O resumo não cita mais o
35
+ Codex sem ele estar instalado.
36
+ - **Bloco do OpenCrew editado por dentro** (em `AGENTS.md`, `CLAUDE.md`, `.gitignore`…) era
37
+ regravado sem cópia. Agora o arquivo inteiro é copiado antes, e o fim de linha é mantido.
38
+ - **Texto antigo das pontes** (instalações até a 1.2.2), que mandava adotar o papel do OpenCrew
39
+ sempre, é retirado, com cópia, quando está idêntico ao gerado; se foi editado, só aviso.
40
+ - **Manifesto ilegível** vira aviso e a atualização segue; `.mcp.json` fora do formato não derruba
41
+ mais o `update` no meio.
42
+ - **A dica de reinstalar** não manda mais apagar `_opencrew/`, que guarda a sua memória.
43
+ - **Restos do OpenSquad**: o aviso não promete mais "apagar com segurança" para arquivo que pode
44
+ ser seu, e cobre mais sete caminhos. Nada é apagado.
45
+ - **Texto do usuário em linha de comando**: o prompt de imagem vai por arquivo (`--prompt-file`),
46
+ nome de arquivo com caractere inseguro não entra em comando, e caminhos e URLs vão entre aspas.
47
+ - **Conferência de fontes**: o `--corrigir` não grava mais fora da crew por um link; caminho de
48
+ rede e endereço de site citados viram alerta "não conferido", sem tocar a rede e sem parar a
49
+ execução; erro de leitura sai com mensagem.
50
+ - **Publicação**: a tag só publica depois do CI verde (Ubuntu e Windows, Node 20.17 e 22) e da
51
+ auditoria de segurança. Três das cinco últimas versões saíram com o CI vermelho.
52
+
53
+ ### Internal
54
+ - CLI em módulos novos (`blocos`, `deteccao`, `legado`, `mcp`, `resumo`, `node-version`); 47
55
+ cenários R2 com teste de mesmo ID; teste de upgrade 1.6.2 → 1.6.3.
56
+
6
57
  ## [1.6.2] — 2026-10-05
7
58
 
8
59
  Correção da 1.6.1. Chega a quem já usa com um `npx @aksp/opencrew@latest update`.
package/README.md CHANGED
@@ -69,7 +69,7 @@ dentro da sua IDE.**
69
69
 
70
70
  ### Pré-requisitos
71
71
 
72
- - **Node.js 20+** ([baixar](https://nodejs.org/))
72
+ - **Node.js 20.17 ou mais novo** ([baixar](https://nodejs.org/))
73
73
  - Uma IDE de IA com acesso a arquivos locais: **Claude Code**, **Cursor**,
74
74
  **Codex (OpenAI)**, **Gemini CLI**, **Google Antigravity**, **OpenCode**,
75
75
  **VS Code + Copilot**, **Qwen Code** ou **Trae**.
@@ -219,14 +219,24 @@ o que você fez:
219
219
 
220
220
  | O que é atualizado | O que NUNCA é tocado |
221
221
  |---|---|
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) |
222
+ | `_opencrew/core/` (framework) e skills do catálogo | As crews que você criou em `crews/` |
223
+ | Pastas novas do framework (agentes-base, config) e modelos de crew — só o que falta | `_opencrew/_memory/` (perfil, preferências) |
224
224
  | 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.
225
+ | Bloco do OpenCrew em `AGENTS.md`, `CLAUDE.md` e `.gitignore` (o resto do arquivo fica intacto) | `.env` (suas chaves) |
226
+ | Servidor Playwright no `.mcp.json`, entregue uma vez (outros servidores intactos) | |
227
+
228
+ - **Editou um arquivo do framework, um skill do catálogo ou o bloco do OpenCrew?** Antes de
229
+ substituir, o `update` guarda o arquivo inteiro em `.opencrew-backup/<data>/` e lista o que
230
+ copiou. Essa pasta fica fora do git (entra no bloco do `.gitignore`). No primeiro `update` para
231
+ a 1.6.3 há cópia do `.gitignore` mesmo sem edição sua: as versões anteriores não registravam o
232
+ bloco.
233
+ - **Apagou um modelo de crew ou um skill do catálogo?** Ele volta no `update`, e a saída diz o
234
+ que foi entregue de novo.
235
+ - **Removeu o servidor Playwright do `.mcp.json`?** O `update` o entrega uma única vez; se você
236
+ remover de novo, não volta. A versão fixada no arquivo não é trocada.
237
+ - **Uma IDE só conta como instalada pelo arquivo de ponte dela**, não por um arquivo seu que cite
238
+ o OpenCrew. Texto antigo das pontes (instalações até a 1.2.2) é retirado, com cópia, quando
239
+ está idêntico ao que o OpenCrew gravou.
230
240
  - **Versão mais nova instalada?** O `update` não volta para uma versão mais antiga (cache do
231
241
  `npx`): ele para e pede `npx @aksp/opencrew@latest update`.
232
242
 
@@ -241,6 +251,8 @@ npx @aksp/opencrew@latest init --repair-bridges --all # as 9 IDEs
241
251
  Sem `--ide` e sem `--all`, o `init --repair-bridges` usa a mesma detecção do `update` (aqui o
242
252
  `--yes` não escolhe IDE); se não encontra nenhuma ponte, para com erro e pede `--ide=<id>`.
243
253
  O reparo não instala: numa pasta sem workspace do OpenCrew ele para com erro e pede o `init`.
254
+ Ele também só roda com o pacote na mesma versão do projeto: com outra versão, para sem alterar
255
+ nada e pede o `update`.
244
256
  Ponte de arquivo inteiro que você editou (ex.: `.claude/skills/opencrew/SKILL.md`) é copiada
245
257
  antes para `.opencrew-backup/<data>/`, e o resumo do `init --repair-bridges` lista cada cópia.
246
258
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aksp/opencrew",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
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
  }
@@ -1,23 +1,37 @@
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, readFile } from '../lib/fsx.js';
5
- import { AGENTS_BRIDGE, LEAKED_STATUS_SECTION, ideById } from '../lib/ides.js';
6
- import { readManifest, writeManifest, newDelivery, deliverTree, deliverFile } from '../lib/manifest.js';
7
- import { compareVersions, detectInstalledIdes, refreshBridges, mergeMcp, findLegacyBridges } from '../lib/migrations.js';
4
+ import { exists, writeFileSafe, readJson, readFile } from '../lib/fsx.js';
5
+ import { AGENTS_BRIDGE, LEAKED_STATUS_SECTION } from '../lib/ides.js';
6
+ import { readManifest, manifestUnreadable, UNREADABLE, writeManifest, newDelivery, deliverTree, deliverFile } from '../lib/manifest.js';
7
+ import { deliverBlock, deliverBridges, blockHas } from '../lib/blocos.js';
8
+ import { detectInstalledIdes } from '../lib/deteccao.js';
9
+ import { withoutLegacy } from '../lib/legado.js';
10
+ import { updateMcp } from '../lib/mcp.js';
11
+ import { compareVersions, installedVersion, findLeftovers } from '../lib/migrations.js';
12
+ import { say, recreatedLines, updateSummary, UNTOUCHED } from '../lib/resumo.js';
8
13
  import { c, log, info, ok, warn, err, step } from '../lib/ui.js';
9
14
 
15
+ const tpl = (...p) => path.join(templatesDir, ...p);
16
+
10
17
  // `update` brings EVERY improvement to people who already use OpenCrew (AGENTS.md rule 14),
11
18
  // without losing what they made:
12
19
  // - _opencrew/core and catalog skills are replaced — a file the user edited is copied to
13
20
  // .opencrew-backup/<date>/ first (manifest of hashes; none = copy whatever differs);
14
- // - new framework folders (agents, config, crew templates) arrive without overwriting;
15
- // - bridges of the IDEs already installed are refreshed (never new IDEs);
16
- // - crews/, _opencrew/_memory/, _opencrew/best-practices.local/ and .env are never touched.
21
+ // - new framework folders (agents, config, crew templates) arrive without overwriting — a
22
+ // crew template the user deleted comes back, and the output lists it;
23
+ // - bridges of the IDEs already installed are refreshed (never new IDEs — an IDE is proved by
24
+ // a file, src/lib/deteccao.js), and so are the opencrew blocks of AGENTS.md and .gitignore
25
+ // — a block the user edited is copied first; the text bridges had up to 1.2.2 leaves the
26
+ // outside of the block, with a copy, only when it is still exactly what was generated;
27
+ // - the Playwright server of .mcp.json is delivered once (the manifest records it) and never
28
+ // put back; the file is copied before any rewrite (src/lib/mcp.js);
29
+ // - the crews the user created, _opencrew/_memory/, _opencrew/best-practices.local/ and .env
30
+ // are never touched.
31
+ // Every step returns what it did, and the summary only says that (src/lib/resumo.js).
17
32
  export async function update(opts = {}) {
18
33
  const target = process.cwd();
19
- const pkg = await readJson(packageJsonPath);
20
- const version = pkg.version;
34
+ const { version } = await readJson(packageJsonPath);
21
35
 
22
36
  if (!(await exists(path.join(target, '_opencrew', 'core')))) {
23
37
  warn('No opencrew workspace found here.');
@@ -25,15 +39,15 @@ export async function update(opts = {}) {
25
39
  return;
26
40
  }
27
41
 
28
- const versionFile = path.join(target, '_opencrew', '.opencrew-version');
29
- const current = (await exists(versionFile))
30
- ? (await fs.readFile(versionFile, 'utf8')).trim()
31
- : 'unknown';
32
- const newer = current !== 'unknown' && compareVersions(current, version) > 0;
33
-
42
+ const current = (await installedVersion(target)) ?? 'unknown';
34
43
  log(`\n${c.bold(c.cyan('opencrew update'))}`);
35
44
  log(c.dim(`Installed: ${current} → Package: ${version}\n`));
45
+ if (canApply(opts, current, version)) await apply(target, version);
46
+ }
36
47
 
48
+ /** `--check` only reports, and a package older than the workspace stops: false = write nothing. */
49
+ function canApply(opts, current, version) {
50
+ const newer = current !== 'unknown' && compareVersions(current, version) > 0;
37
51
  if (opts.check) {
38
52
  if (current === version) ok(`Up to date (v${version}).`);
39
53
  else if (newer) info(`A versão instalada (v${current}) é mais nova que este pacote (v${version}).`);
@@ -42,82 +56,82 @@ export async function update(opts = {}) {
42
56
  info(`Run ${c.cyan('npx @aksp/opencrew update')} to apply.`);
43
57
  process.exitCode = 1;
44
58
  }
45
- return;
59
+ return false;
46
60
  }
47
-
48
61
  if (newer) {
49
62
  err(`Você tem a v${current} instalada e este pacote é a v${version} (mais antigo). Nada foi alterado.`);
50
63
  info(`Use ${c.cyan('npx @aksp/opencrew@latest update')}.`);
51
64
  process.exitCode = 1;
52
- return;
53
65
  }
66
+ return !newer;
67
+ }
54
68
 
69
+ async function apply(target, version) {
70
+ // A manifest that exists but cannot be used is said so, then treated as none (R2 rule 12).
55
71
  const manifest = await readManifest(target);
72
+ const unreadable = !manifest && (await manifestUnreadable(target));
73
+ if (unreadable) warn(UNREADABLE.update);
56
74
  const ctx = newDelivery(target, manifest);
57
- const tpl = (...p) => path.join(templatesDir, ...p);
58
- const dest = (...p) => path.join(target, ...p);
59
75
 
60
76
  step('Refreshing framework');
61
- await deliverTree(ctx, tpl('_opencrew', 'core'), dest('_opencrew', 'core'), { overwrite: true });
62
- await deliverFile(ctx, dest('_opencrew', 'core', 'system.md'), await fs.readFile(tpl('AGENTS.md')), { overwrite: true });
63
- await deliverTree(ctx, tpl('skills'), dest('skills'), { overwrite: true });
64
- // New framework folders (e.g. base agents since 1.3.2): only what is missing.
65
- for (const dir of ['agents', 'config', '_investigations']) {
66
- await deliverTree(ctx, tpl('_opencrew', dir), dest('_opencrew', dir), { overwrite: false });
67
- }
68
- await deliverTree(ctx, tpl('crews'), dest('crews'), { overwrite: false });
77
+ const crews = await refreshFramework(ctx);
69
78
  ok(`Framework and catalog skills refreshed (${ctx.written} files written)`);
79
+ say(recreatedLines(crews));
70
80
 
71
81
  step('Refreshing IDE bridges');
72
- await refreshAgentsBridge(target);
73
- const ides = await detectInstalledIdes(target);
74
- await refreshBridges(ctx, ides);
75
- ok(ides.length ? `Bridges refreshed: ${ides.map((i) => i.label).join(', ')}` : 'No IDE bridges found to refresh');
76
- await removeLeakedStatusSection(target);
77
-
78
- const mcp = await mergeMcp(target, tpl('.mcp.json'));
79
- if (mcp === 'updated' || mcp === 'created') ok(`.mcp.json (Playwright: ${mcp === 'created' ? 'created' : 'saída em _opencrew/logs/playwright/'})`);
80
- if (mcp === 'invalid') warn('.mcp.json não é um JSON válido — não alterado. Confira o arquivo.');
81
-
82
- for (const legacy of await findLegacyBridges(target)) {
83
- warn(`Ponte antiga encontrada: ${legacy} (aponta para _opensquad/, que não existe neste projeto). Pode apagar com segurança.`);
84
- }
85
-
86
- if (ctx.copied.length) {
87
- const rel = path.relative(target, ctx.backupDir).split(path.sep).join('/');
88
- const what = manifest ? 'que você tinha editado' : 'diferentes do pacote novo';
89
- warn(`${ctx.copied.length} arquivo(s) ${what} foram copiados para ${rel}/ antes de serem substituídos:`);
90
- for (const f of ctx.copied.slice(0, 15)) log(` ${f}`);
91
- if (ctx.copied.length > 15) log(` … e mais ${ctx.copied.length - 15}`);
92
- if (!manifest) info('Primeira atualização com proteção: sem registro anterior, guardamos tudo o que diferia. Daqui em diante, só o que você editar.');
93
- }
82
+ const done = { unreadable, ...(await refreshBridges(ctx)) };
83
+ done.mcp = await updateMcp(ctx, await readFile(tpl('.mcp.json')));
84
+ done.leftovers = await findLeftovers(target);
85
+ say(updateSummary(ctx, done));
94
86
 
95
87
  await writeManifest(target, version, ctx.files);
96
88
  // Stamp last: a crash above leaves the old version, so the next update retries.
97
- await fs.writeFile(versionFile, version + '\n');
89
+ await fs.writeFile(path.join(target, '_opencrew', '.opencrew-version'), version + '\n');
98
90
  log(`\n${c.green(c.bold('Updated to v' + version))}.`);
99
- log(c.dim('Your crews, memory, local best-practices and .env were left untouched.\n'));
91
+ log(c.dim(`${UNTOUCHED}\n`));
100
92
  }
101
93
 
102
- // Root AGENTS.md: create it if missing; a legacy full-system doc (pre-v1.3) is backed up
103
- // byte for byte and replaced by the thin bridge; otherwise only the marked block changes.
104
- async function refreshAgentsBridge(target) {
105
- const agentsPath = path.join(target, 'AGENTS.md');
106
- if (!(await exists(agentsPath))) {
107
- await writeBridgeFile(agentsPath, AGENTS_BRIDGE);
108
- ok('AGENTS.md (bridge created)');
109
- return;
94
+ /** @returns what `deliverTree` did to each crew template (only the missing ones are written). */
95
+ async function refreshFramework(ctx) {
96
+ const dest = (...p) => path.join(ctx.target, ...p);
97
+ await deliverTree(ctx, tpl('_opencrew', 'core'), dest('_opencrew', 'core'), { overwrite: true });
98
+ await deliverFile(ctx, dest('_opencrew', 'core', 'system.md'), await fs.readFile(tpl('AGENTS.md')), { overwrite: true });
99
+ await deliverTree(ctx, tpl('skills'), dest('skills'), { overwrite: true });
100
+ // New framework folders (e.g. base agents since 1.3.2): only what is missing.
101
+ for (const dir of ['agents', 'config', '_investigations']) {
102
+ await deliverTree(ctx, tpl('_opencrew', dir), dest('_opencrew', dir), { overwrite: false });
110
103
  }
111
- const existing = await readFile(agentsPath);
104
+ return deliverTree(ctx, tpl('crews'), dest('crews'), { overwrite: false });
105
+ }
106
+
107
+ /** AGENTS.md, .gitignore and the bridges of the IDEs installed: what happened to each. */
108
+ async function refreshBridges(ctx) {
109
+ const agents = await refreshAgentsBridge(ctx);
110
+ // The copies in .opencrew-backup/ stay out of the user's git (R2 rule 7).
111
+ const gitignore = await deliverBlock(ctx, '.gitignore', await readFile(tpl('gitignore')));
112
+ const ides = await detectInstalledIdes(ctx.target);
113
+ // 1.4.0/1.4.1 shipped the maintainer's STATUS.md workflow inside CLAUDE.md's opencrew block.
114
+ // It leaves with the block; the same title outside the block is the user's and stays.
115
+ const leaked = () => blockHas(ctx.target, 'CLAUDE.md', LEAKED_STATUS_SECTION);
116
+ const hadLeak = await leaked();
117
+ const bridges = await withoutLegacy(ctx, ides, () => deliverBridges(ctx, ides)); // R2 rule 3 (src/lib/legado.js)
118
+ return { agents, gitignore, ides, bridges, leak: hadLeak && !(await leaked()) };
119
+ }
120
+
121
+ // Root AGENTS.md: a legacy full-system doc (pre-v1.3) is backed up byte for byte and replaced by
122
+ // the thin bridge (said here; returns null); otherwise only the marked block is delivered (a
123
+ // block the user edited is copied first — src/lib/blocos.js) and its result goes to the summary.
124
+ async function refreshAgentsBridge(ctx) {
125
+ const agentsPath = path.join(ctx.target, 'AGENTS.md');
126
+ const existing = (await exists(agentsPath)) ? await readFile(agentsPath) : '';
112
127
  if (existing.includes('# opencrew Instructions') && !existing.includes('<!-- opencrew:start -->')) {
113
128
  const backup = await freeBackupPath(agentsPath);
114
129
  await fs.copyFile(agentsPath, backup);
115
130
  await writeFileSafe(agentsPath, AGENTS_BRIDGE);
116
131
  ok(`AGENTS.md (migrated from legacy full-system to thin bridge — backed up to ${path.basename(backup)})`);
117
- return;
132
+ return null;
118
133
  }
119
- await writeBridgeFile(agentsPath, AGENTS_BRIDGE);
120
- ok('AGENTS.md refreshed');
134
+ return deliverBlock(ctx, 'AGENTS.md', AGENTS_BRIDGE);
121
135
  }
122
136
 
123
137
  async function freeBackupPath(file) {
@@ -125,14 +139,3 @@ async function freeBackupPath(file) {
125
139
  if (!(await exists(bak))) return bak;
126
140
  return `${file}.bak-${new Date().toISOString().replace(/[:.]/g, '-')}`;
127
141
  }
128
-
129
- // 1.4.0/1.4.1 shipped the maintainer's STATUS.md workflow inside CLAUDE.md's opencrew
130
- // block. Rewrite that block (only that block, only if the leak is there).
131
- async function removeLeakedStatusSection(target) {
132
- const claudePath = path.join(target, 'CLAUDE.md');
133
- if (!(await exists(claudePath))) return;
134
- if (!(await readFile(claudePath)).includes(LEAKED_STATUS_SECTION)) return;
135
- const bridge = ideById('claude-code').files.find((f) => f.path === 'CLAUDE.md');
136
- await writeBridgeFile(claudePath, bridge.content);
137
- ok('CLAUDE.md (removed the STATUS.md section shipped by mistake in 1.4.0/1.4.1)');
138
- }