@aksp/opencrew 1.6.1 → 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.
- package/CHANGELOG.md +66 -0
- package/README.md +20 -8
- package/package.json +2 -2
- package/src/cli.js +15 -38
- package/src/commands/init.js +41 -44
- package/src/commands/update.js +78 -75
- package/src/lib/blocos.js +148 -0
- package/src/lib/deteccao.js +69 -0
- package/src/lib/fsx.js +1 -55
- package/src/lib/ides.js +4 -0
- package/src/lib/legado.js +142 -0
- package/src/lib/manifest.js +67 -26
- package/src/lib/mcp.js +131 -0
- package/src/lib/migrations.js +77 -74
- package/src/lib/node-version.js +43 -0
- package/src/lib/prompts.js +25 -2
- package/src/lib/resumo.js +125 -0
- package/templates/.mcp.json +1 -1
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/best-practices/social-networks-publishing.md +14 -14
- package/templates/_opencrew/core/prompts/export.prompt.md +1 -1
- package/templates/_opencrew/core/prompts/sherlock-shared.md +5 -5
- package/templates/_opencrew/core/runner.pipeline.md +32 -12
- package/templates/_opencrew/core/scripts/comum.mjs +49 -4
- package/templates/_opencrew/core/scripts/conferir-fontes/busca.mjs +42 -3
- package/templates/_opencrew/core/scripts/conferir-fontes/relatorio.mjs +18 -6
- package/templates/_opencrew/core/scripts/conferir-fontes.mjs +97 -39
- package/templates/_opencrew/core/scripts/verificar/regras.mjs +32 -1
- package/templates/_opencrew/core/scripts/verificar.mjs +7 -4
- package/templates/_opencrew/core/skills.engine.md +7 -3
- package/templates/gitignore +1 -0
- package/templates/skills/blotato/SKILL.md +39 -10
- package/templates/skills/image-ai-generator/SKILL.md +18 -5
- package/templates/skills/image-ai-generator/scripts/generate.py +52 -10
- package/templates/skills/instagram-publisher/SKILL.md +4 -0
- package/templates/skills/opencrew-skill-creator/references/skill-format.md +1 -0
- package/templates/skills/resend/SKILL.md +52 -13
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,72 @@
|
|
|
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
|
+
|
|
57
|
+
## [1.6.2] — 2026-10-05
|
|
58
|
+
|
|
59
|
+
Correção da 1.6.1. Chega a quem já usa com um `npx @aksp/opencrew@latest update`.
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
- **Node 20: o verificador não esgota mais a memória com texto grande.** Um post, tweet ou
|
|
63
|
+
legenda de dezenas de milhares de caracteres numa peça só derrubava o verificador no Node 20,
|
|
64
|
+
sem relatório (o runner avisava que a verificação não rodou). A contagem de caracteres passou a
|
|
65
|
+
ser feita em janelas, com o mesmo resultado. O defeito vinha da 1.5.0; Node 22 e 24 não o
|
|
66
|
+
tinham.
|
|
67
|
+
|
|
68
|
+
### Internal
|
|
69
|
+
- Release: a tag só sai depois do CI verde nas quatro células (Ubuntu e Windows, Node 20 e 22).
|
|
70
|
+
A 1.6.1 foi publicada com o CI do Node 20 vermelho, porque a publicação roda só no Node 22.
|
|
71
|
+
|
|
6
72
|
## [1.6.1] — 2026-10-05
|
|
7
73
|
|
|
8
74
|
Fase R1 "Reparos da 1.6.0: o verificador mede de verdade" (`specs/fase-r1-reparos-1-6-1.md`),
|
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ dentro da sua IDE.**
|
|
|
69
69
|
|
|
70
70
|
### Pré-requisitos
|
|
71
71
|
|
|
72
|
-
- **Node.js 20
|
|
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 |
|
|
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
|
|
226
|
-
| Servidor Playwright no `.mcp.json
|
|
227
|
-
|
|
228
|
-
- **Editou um arquivo do framework
|
|
229
|
-
guarda
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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).
|
package/src/commands/init.js
CHANGED
|
@@ -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,
|
|
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 {
|
|
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(
|
|
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
|
|
60
|
-
if (
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
70
|
-
info(res.
|
|
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(
|
|
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
|
|
106
|
-
|
|
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
|
|
159
|
-
*
|
|
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(
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
for (const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
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
|
}
|
package/src/commands/update.js
CHANGED
|
@@ -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,
|
|
5
|
-
import { AGENTS_BRIDGE, LEAKED_STATUS_SECTION
|
|
6
|
-
import { readManifest, writeManifest, newDelivery, deliverTree, deliverFile } from '../lib/manifest.js';
|
|
7
|
-
import {
|
|
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
|
-
//
|
|
16
|
-
// -
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
await
|
|
75
|
-
|
|
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(
|
|
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(
|
|
91
|
+
log(c.dim(`${UNTOUCHED}\n`));
|
|
100
92
|
}
|
|
101
93
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|