@aksp/opencrew 1.4.0 → 1.4.2
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 +87 -0
- package/README.md +33 -17
- package/bin/opencrew.js +4 -4
- package/package.json +4 -3
- package/src/cli.js +93 -43
- package/src/commands/init.js +79 -53
- package/src/commands/update.js +44 -17
- package/src/lib/errors.js +12 -0
- package/src/lib/fsx.js +18 -9
- package/src/lib/ides.js +9 -34
- package/templates/AGENTS.md +42 -63
- package/templates/_opencrew/.opencrew-version +1 -1
- package/templates/_opencrew/core/prompts/build.prompt.md +21 -0
- package/templates/_opencrew/core/prompts/design.prompt.md +9 -5
- package/templates/_opencrew/core/runner.pipeline.md +14 -2
- package/templates/gitignore +3 -1
- package/templates/skills/image-ai-generator/SKILL.md +8 -4
- package/templates/skills/image-creator/SKILL.md +3 -1
- package/templates/skills/instagram-publisher/SKILL.md +34 -16
- package/templates/skills/instagram-publisher/scripts/publish.js +58 -27
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,82 @@
|
|
|
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.4.2] — 2026-10-02
|
|
7
|
+
|
|
8
|
+
Hotfix "parar de causar dano" (Fase 1 da auditoria — `specs/fase-1-hotfix.md`).
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **`CLAUDE.md` gerado não traz mais o fluxo STATUS.md do mantenedor** (vazado na 1.4.0/1.4.1);
|
|
12
|
+
o `update` remove a seção de instalações existentes, sem tocar no texto do usuário.
|
|
13
|
+
`STATUS.md` saiu do `.gitignore` do template.
|
|
14
|
+
- **`--help` nunca executa comando**: `update --help` / `-h` e `init --help` só mostram a ajuda.
|
|
15
|
+
Parser estrito (`node:util.parseArgs`): opção desconhecida, `--ide` sem id válido ou
|
|
16
|
+
argumento solto (`init minha-pasta`) falham com exit 1 **antes** de escrever qualquer arquivo.
|
|
17
|
+
`--ide claude-code` (com espaço) e `-yv` funcionam. `update --dry-run` = `--check`.
|
|
18
|
+
- **`update` sem `AGENTS.md`** cria a ponte em vez de quebrar com ENOENT.
|
|
19
|
+
- **Migração do `AGENTS.md` legado faz backup** em `AGENTS.md.bak` (ou `.bak-<timestamp>`).
|
|
20
|
+
- **`.env.example` e `.gitignore` do usuário não são mais sobrescritos/ignorados**: recebem um
|
|
21
|
+
bloco `# opencrew:start … # opencrew:end` no fim; as linhas do usuário ficam intactas.
|
|
22
|
+
O bloco do `.gitignore` agora cobre `.claude/settings.local.json`, `crews/*/state.json` e
|
|
23
|
+
`crews/*/_investigations/`.
|
|
24
|
+
- **Ctrl+C no prompt de IDEs não deixa instalação pela metade**: as IDEs são escolhidas antes
|
|
25
|
+
da primeira escrita; cancelamento sai com 130 e "Cancelled — nothing was written.". Uma
|
|
26
|
+
instalação interrompida (core sem stamp de versão) é **retomada** pelo próximo `init`.
|
|
27
|
+
- Erros inesperados mostram uma linha `✗ <mensagem>` (stack só com `OPENCREW_DEBUG=1`).
|
|
28
|
+
- Marcadores de bloco: um `start` órfão (fim apagado à mão) não faz mais a regravação engolir
|
|
29
|
+
linhas do usuário.
|
|
30
|
+
- Skills: caminho do `image-ai-generator` corrigido (`{skill_path}/scripts/generate.py`, nota
|
|
31
|
+
`py -3` no Windows); `instagram-publisher` lê as imagens da pasta do run atual; o
|
|
32
|
+
`image-creator` renderiza JPEG quando o destino é Instagram.
|
|
33
|
+
- Runner: o toggle do dashboard reconhece o formato `- **Dashboard:** enabled` gravado no
|
|
34
|
+
onboarding.
|
|
35
|
+
|
|
36
|
+
### Security
|
|
37
|
+
- **Publicar/enviar é sempre o último trecho do pipeline**: `… → Review → Final Approval
|
|
38
|
+
checkpoint → [Publish/Send]` (design), com o novo Gate 2c BLOCKING no build. Passos com
|
|
39
|
+
`side_effects: irreversible` rodam inline e **nunca** têm retry automático nem auto-correção
|
|
40
|
+
de veto — o runner avisa que a ação pode já ter acontecido e pergunta.
|
|
41
|
+
- **`instagram-publisher`**: legenda via `--caption-file` (nunca interpolada no shell); só
|
|
42
|
+
aceita `.jpg`/`.jpeg` dentro de `crews/*/output/`; upload no imgBB expira em 24h; token da
|
|
43
|
+
Graph API no corpo dos POST, não na URL; fluxo preview → `--dry-run` → confirmação explícita
|
|
44
|
+
("publish"/"publicar") → publicação única.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- Template `templates/AGENTS.md` (o `system.md` instalado) compactado (−24%) sem mudar o roteamento.
|
|
48
|
+
|
|
49
|
+
### Internal
|
|
50
|
+
- Testes por cenário da spec (F1-01a…F1-13a); trava nova: nenhum arquivo de `templates/`
|
|
51
|
+
carrega conteúdo do mantenedor. `KNOWN_BROKEN` de referências zerado.
|
|
52
|
+
|
|
53
|
+
### Added (governança do repositório — Fase 0)
|
|
54
|
+
- Auditoria geral da v1.4.1 com roadmap por fases: `docs/auditoria/2026-10-02-auditoria-geral.md`.
|
|
55
|
+
- Regras de desenvolvimento em `AGENTS.md` (project-standards T3, tabela Regra → Trava);
|
|
56
|
+
`CLAUDE.md` da raiz vira apontador versionado.
|
|
57
|
+
- Porta de verificação única `npm run verify` (`scripts/verify.js`): lint (agora inclui
|
|
58
|
+
`bin/`), testes (descobertos automaticamente em `tests/`), version-sync e alerta de
|
|
59
|
+
tamanho (`scripts/check-size.js`). CI e publish chamam a mesma porta.
|
|
60
|
+
- Travas novas: `tests/package.test.js` (conteúdo do tarball × README),
|
|
61
|
+
`tests/template-refs.test.js` (caminhos citados nos prompts existem),
|
|
62
|
+
`tests/verify.test.js` e `tests/check-size.test.js` (provetas).
|
|
63
|
+
- `GLOSSARIO.md`; `IDEIAS.md` reformatado com triagem e `Alocação: →`.
|
|
64
|
+
|
|
65
|
+
### Changed (governança do repositório — Fase 0)
|
|
66
|
+
- Dogfood do mantenedor sai da raiz e vai para `sandbox/` (fora do git).
|
|
67
|
+
- `publish.yml` confere a tag contra a versão do `package.json` e roda `npm run verify`;
|
|
68
|
+
CI e publish usam só `npm ci` (sem fallback que esconde drift do lockfile).
|
|
69
|
+
|
|
70
|
+
### Docs (Fase 0)
|
|
71
|
+
- README: o dashboard não é instalado pelo `init`; `update` não atualiza as pontes de IDE
|
|
72
|
+
(use `init --repair-bridges`); migração do `AGENTS.md` legado perde instruções extras;
|
|
73
|
+
flags `upgrade`, `--ide`, `--all`/`-y`, `--repair-bridges` documentadas; contagens
|
|
74
|
+
corrigidas (22 guias, 13 prompts); Windsurf removido da lista; promessa "30-70% de
|
|
75
|
+
economia" sem base removida.
|
|
76
|
+
|
|
77
|
+
## [1.4.1] — 2026-08-04
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
- **NPM publish**: v1.4.0 já existia no registro. Re-publicado como 1.4.1.
|
|
81
|
+
|
|
6
82
|
## [1.4.0] — 2026-08-04
|
|
7
83
|
|
|
8
84
|
### Added
|
|
@@ -20,6 +96,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
|
|
|
20
96
|
- `AGENTS.md` — step 7b na seção Loading the Pipeline Runner.
|
|
21
97
|
- 3 novos testes de contrato em `docs.test.js`.
|
|
22
98
|
|
|
99
|
+
> Itens desta versão que ficaram de fora da entrada original (acrescentados na auditoria
|
|
100
|
+
> de 2026-10-02):
|
|
101
|
+
- **Pontes `.agents/`** para Antigravity, Gemini CLI e Qwen Code
|
|
102
|
+
(`.agents/skills/opencrew/SKILL.md`, `.agents/workflows/opencrew.md`).
|
|
103
|
+
- **`init --repair-bridges`**: regrava as pontes de IDE num workspace existente.
|
|
104
|
+
- **`update` migra `AGENTS.md` legado** (pré-1.3, sistema completo) para a ponte fina.
|
|
105
|
+
- **Fix Antigravity**: frontmatter no workflow para registrar `/opencrew`.
|
|
106
|
+
- ⚠️ **Regressão**: o `CLAUDE.md` gerado passou a conter a seção "STATUS.md (gestão de
|
|
107
|
+
sessão)" do fluxo pessoal do mantenedor, e `templates/gitignore` ganhou `STATUS.md`.
|
|
108
|
+
Correção prevista na 1.4.2 (F1-01).
|
|
109
|
+
|
|
23
110
|
## [1.3.3] — 2026-08-03
|
|
24
111
|
|
|
25
112
|
### Fixed
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ dentro da sua IDE.**
|
|
|
34
34
|
sem abrir editor nenhum.
|
|
35
35
|
- 🎛️ **Seleção inteligente de agentes** — o sistema analisa seu pedido e
|
|
36
36
|
sugere quais agentes são necessários para aquela tarefa. Você confirma ou
|
|
37
|
-
ajusta com um clique.
|
|
37
|
+
ajusta com um clique. Agentes pulados não gastam tokens naquele run.
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
@@ -126,12 +126,12 @@ enxuto — todos apontam para a mesma fonte.
|
|
|
126
126
|
|---|---|
|
|
127
127
|
| `AGENTS.md` (ponte) + `.claude/skills/opencrew/SKILL.md` + `CLAUDE.md` | Claude Code |
|
|
128
128
|
| `AGENTS.md` (ponte) + `.agents/skills/opencrew/SKILL.md` | OpenAI Codex, Codex CLI |
|
|
129
|
-
| `AGENTS.md` (ponte) + `.cursor/rules/opencrew.mdc` | Cursor
|
|
129
|
+
| `AGENTS.md` (ponte) + `.cursor/rules/opencrew.mdc` | Cursor |
|
|
130
130
|
| `AGENTS.md` (ponte) + `.github/copilot-instructions.md` | VS Code + GitHub Copilot |
|
|
131
131
|
| `AGENTS.md` (ponte) + `.opencode/commands/opencrew.md` | OpenCode |
|
|
132
132
|
| `AGENTS.md` (ponte) + `.agent/rules/opencrew.md` + `.agent/workflows/opencrew.md` + `.agents/skills/opencrew/SKILL.md` + `.agents/workflows/opencrew.md` | Google Antigravity |
|
|
133
|
-
| `GEMINI.md` (ponte) | Gemini CLI |
|
|
134
|
-
| `QWEN.md` (ponte) | Qwen Code |
|
|
133
|
+
| `GEMINI.md` (ponte) + `.agents/skills/opencrew/SKILL.md` | Gemini CLI |
|
|
134
|
+
| `QWEN.md` (ponte) + `.agents/skills/opencrew/SKILL.md` | Qwen Code |
|
|
135
135
|
| `AGENTS.md` (ponte) + `.trae/rules/opencrew.md` | Trae |
|
|
136
136
|
|
|
137
137
|
> ⚠️ **Importante:** `CLAUDE.md`, `GEMINI.md` e os demais arquivos de IDE são
|
|
@@ -150,8 +150,8 @@ meu-projeto/
|
|
|
150
150
|
├── CLAUDE.md ← ponte fina + suas instruções (merge)
|
|
151
151
|
├── GEMINI.md ← ponte fina (Gemini CLI)
|
|
152
152
|
├── .mcp.json ← servidor Playwright do OpenCrew
|
|
153
|
-
├── .gitignore
|
|
154
|
-
├── .env.example
|
|
153
|
+
├── .gitignore ← bloco `# opencrew` no fim; suas linhas são mantidas
|
|
154
|
+
├── .env.example ← idem
|
|
155
155
|
│
|
|
156
156
|
├── _opencrew/
|
|
157
157
|
│ ├── core/
|
|
@@ -159,8 +159,8 @@ meu-projeto/
|
|
|
159
159
|
│ │ ├── runner.pipeline.md ← executor de pipeline
|
|
160
160
|
│ │ ├── skills.engine.md ← gerenciador de skills
|
|
161
161
|
│ │ ├── architect.agent.yaml ← definição do Arquiteto
|
|
162
|
-
│ │ ├── best-practices/ ←
|
|
163
|
-
│ │ └── prompts/ ←
|
|
162
|
+
│ │ ├── best-practices/ ← 22 guias de melhores práticas + _catalog.yaml
|
|
163
|
+
│ │ └── prompts/ ← 13 prompts de fase (discovery, design, build, etc.)
|
|
164
164
|
│ ├── agents/ ← 5 agentes base compartilhados
|
|
165
165
|
│ │ ├── researcher.agent.md
|
|
166
166
|
│ │ ├── copywriter.agent.md
|
|
@@ -185,11 +185,12 @@ meu-projeto/
|
|
|
185
185
|
│ ├── instagram-publisher/ ← publicação no Instagram
|
|
186
186
|
│ ├── resend/ ← envio de emails
|
|
187
187
|
│ └── ...
|
|
188
|
-
│
|
|
189
|
-
└── dashboard/
|
|
190
|
-
└── index.html ← dashboard visual (opcional, offline)
|
|
191
188
|
```
|
|
192
189
|
|
|
190
|
+
> O dashboard visual (`dashboard/index.html`) **não é instalado** pelo `init` — ele vive
|
|
191
|
+
> só no repositório do OpenCrew e ainda é experimental (decisão de publicar ou remover:
|
|
192
|
+
> Fase 4 da auditoria em `docs/auditoria/`).
|
|
193
|
+
|
|
193
194
|
---
|
|
194
195
|
|
|
195
196
|
## Mantendo o OpenCrew atualizado
|
|
@@ -198,18 +199,29 @@ meu-projeto/
|
|
|
198
199
|
npx @aksp/opencrew update
|
|
199
200
|
```
|
|
200
201
|
|
|
201
|
-
O `update`
|
|
202
|
+
O `update` não toca nas suas crews nem na sua memória. Ele atualiza apenas:
|
|
202
203
|
|
|
203
204
|
| O que é atualizado | O que NUNCA é tocado |
|
|
204
205
|
|---|---|
|
|
205
|
-
| `_opencrew/core/` (framework) | `crews/` (suas crews) |
|
|
206
|
-
| Skills do catálogo | `_opencrew/_memory/` (perfil, preferências) |
|
|
206
|
+
| `_opencrew/core/` (framework — sobrescrito por inteiro) | `crews/` (suas crews) |
|
|
207
|
+
| Skills do catálogo (sobrescritas — edições locais se perdem) | `_opencrew/_memory/` (perfil, preferências) |
|
|
207
208
|
| `_opencrew/core/system.md` | `.env` (suas chaves) |
|
|
208
|
-
| Bloco `<!-- opencrew -->`
|
|
209
|
+
| Bloco `<!-- opencrew -->` no `AGENTS.md` | Pontes das IDEs (`CLAUDE.md`, `.cursor/`, `.agents/`…) |
|
|
210
|
+
|
|
211
|
+
As **pontes das IDEs não são atualizadas** pelo `update`. Para regravá-las com a versão
|
|
212
|
+
atual, use:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
npx @aksp/opencrew init --repair-bridges --ide=claude-code
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
(troque `claude-code` pelas IDEs que você usa, separadas por vírgula; sem `--ide`, o
|
|
219
|
+
comando grava as pontes de **todas** as IDEs suportadas).
|
|
209
220
|
|
|
210
221
|
Se você está migrando de uma versão anterior a v1.3, o `update` detecta
|
|
211
222
|
AGENTS.md legados (sistema completo de 150 linhas) e os substitui pela ponte
|
|
212
|
-
fina
|
|
223
|
+
fina. Desde a v1.4.2 o arquivo original é copiado antes para `AGENTS.md.bak` (até a v1.4.1,
|
|
224
|
+
instruções suas adicionadas a esse `AGENTS.md` legado eram perdidas).
|
|
213
225
|
|
|
214
226
|
Para verificar se há atualização disponível sem aplicar:
|
|
215
227
|
|
|
@@ -245,7 +257,11 @@ npx @aksp/opencrew update --check
|
|
|
245
257
|
|---|---|
|
|
246
258
|
| `npx @aksp/opencrew init` | Instala o OpenCrew na pasta atual |
|
|
247
259
|
| `npx @aksp/opencrew update` | Atualiza o framework |
|
|
248
|
-
| `npx @aksp/opencrew update --check` | Verifica se há update disponível |
|
|
260
|
+
| `npx @aksp/opencrew update --check` (ou `--dry-run`) | Verifica se há update disponível, sem alterar nada |
|
|
261
|
+
| `npx @aksp/opencrew upgrade` | Atalho para `update` |
|
|
262
|
+
| `npx @aksp/opencrew init --ide=claude-code,cursor` | Instala só as pontes das IDEs indicadas |
|
|
263
|
+
| `npx @aksp/opencrew init --all` (ou `-y`) | Instala as pontes de todas as IDEs |
|
|
264
|
+
| `npx @aksp/opencrew init --repair-bridges` | Regrava as pontes de IDE num workspace existente |
|
|
249
265
|
| `npx @aksp/opencrew version` | Mostra a versão instalada |
|
|
250
266
|
| `npx @aksp/opencrew help` | Mostra ajuda dos comandos CLI |
|
|
251
267
|
|
package/bin/opencrew.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// opencrew — CLI entry point
|
|
3
|
-
import { run } from '../src/cli.js';
|
|
3
|
+
import { run, reportError } from '../src/cli.js';
|
|
4
4
|
|
|
5
|
-
run(
|
|
6
|
-
|
|
7
|
-
process.
|
|
5
|
+
// run() reports its own errors; this only catches a failure inside the reporter itself.
|
|
6
|
+
run(process.argv.slice(2)).catch((e) => {
|
|
7
|
+
process.exitCode = reportError(e);
|
|
8
8
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aksp/opencrew",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
],
|
|
20
20
|
"scripts": {
|
|
21
21
|
"start": "node bin/opencrew.js",
|
|
22
|
-
"test": "node
|
|
23
|
-
"lint": "
|
|
22
|
+
"test": "node scripts/verify.js test",
|
|
23
|
+
"lint": "node scripts/verify.js lint",
|
|
24
|
+
"verify": "node scripts/verify.js",
|
|
24
25
|
"version": "node scripts/stamp-version.js && git add templates/_opencrew/.opencrew-version"
|
|
25
26
|
},
|
|
26
27
|
"keywords": [
|
package/src/cli.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { parseArgs as nodeParseArgs } from 'node:util';
|
|
1
2
|
import { readJson } from './lib/fsx.js';
|
|
2
3
|
import { packageJsonPath } from './lib/paths.js';
|
|
3
4
|
import { allIdeIds } from './lib/ides.js';
|
|
5
|
+
import { UsageError, isPromptCancel } from './lib/errors.js';
|
|
4
6
|
import { init } from './commands/init.js';
|
|
5
7
|
import { update } from './commands/update.js';
|
|
6
8
|
import { c, log, err, warn, info } from './lib/ui.js';
|
|
@@ -31,27 +33,56 @@ function lt(a, b) {
|
|
|
31
33
|
return false; // equal
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
const OPTION_SPEC = {
|
|
37
|
+
help: { type: 'boolean', short: 'h' },
|
|
38
|
+
version: { type: 'boolean', short: 'v' },
|
|
39
|
+
ide: { type: 'string' },
|
|
40
|
+
all: { type: 'boolean' },
|
|
41
|
+
yes: { type: 'boolean', short: 'y' },
|
|
42
|
+
'repair-bridges': { type: 'boolean' },
|
|
43
|
+
check: { type: 'boolean' },
|
|
44
|
+
'dry-run': { type: 'boolean' },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const GLOBAL_OPTIONS = ['help', 'version'];
|
|
48
|
+
const COMMAND_OPTIONS = {
|
|
49
|
+
init: ['ide', 'all', 'yes', 'repair-bridges'],
|
|
50
|
+
update: ['check', 'dry-run'],
|
|
51
|
+
upgrade: ['check', 'dry-run'],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Strict argument parsing: unknown options, options of another command and stray
|
|
56
|
+
* positionals are UsageErrors — raised before any command can write a file.
|
|
57
|
+
*/
|
|
58
|
+
export function parseArgs(argv) {
|
|
59
|
+
let parsed;
|
|
60
|
+
try {
|
|
61
|
+
parsed = nodeParseArgs({ args: argv, options: OPTION_SPEC, allowPositionals: true, strict: true, tokens: true });
|
|
62
|
+
} catch (e) {
|
|
63
|
+
const unknown = e.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION' && e.message.match(/'([^']+)'/);
|
|
64
|
+
const cmd = argv.find((a) => !a.startsWith('-')) ?? 'init';
|
|
65
|
+
throw new UsageError(unknown
|
|
66
|
+
? `Unknown option '${unknown[1]}' for "${cmd}".`
|
|
67
|
+
: e.message.split('. ')[0].replace(/\.$/, '') + '.');
|
|
49
68
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
69
|
+
const [cmd, ...extra] = parsed.positionals;
|
|
70
|
+
const { values } = parsed;
|
|
71
|
+
const command = cmd ?? (values.version ? 'version' : values.help ? 'help' : 'init');
|
|
72
|
+
|
|
73
|
+
const allowed = new Set([...GLOBAL_OPTIONS, ...(COMMAND_OPTIONS[command] ?? [])]);
|
|
74
|
+
const foreign = parsed.tokens.find((t) => t.kind === 'option' && !allowed.has(t.name));
|
|
75
|
+
if (foreign) throw new UsageError(`Unknown option '${foreign.rawName}' for "${command}".`);
|
|
76
|
+
|
|
77
|
+
if (extra.length) {
|
|
78
|
+
throw new UsageError(command === 'init'
|
|
79
|
+
? 'init does not take a directory — cd into the project folder first.'
|
|
80
|
+
: `Unexpected argument "${extra[0]}" for "${command}".`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const opts = { ...values, _: parsed.positionals };
|
|
84
|
+
if (opts['dry-run']) opts.check = true;
|
|
85
|
+
return { command, opts };
|
|
55
86
|
}
|
|
56
87
|
|
|
57
88
|
function help(version) {
|
|
@@ -65,8 +96,8 @@ ${c.bold('Commands')}
|
|
|
65
96
|
init Scaffold an opencrew workspace in the current folder
|
|
66
97
|
update Refresh the framework (keeps your crews, memory and .env)
|
|
67
98
|
upgrade Alias for update
|
|
68
|
-
help Show this help
|
|
69
|
-
version Print the version
|
|
99
|
+
help Show this help (also: <command> --help, -h)
|
|
100
|
+
version Print the version (also: --version, -v)
|
|
70
101
|
|
|
71
102
|
${c.bold('Options for init')}
|
|
72
103
|
--ide=a,b Preselect IDEs (skip the prompt). Valid: ${allIdeIds().join(', ')}
|
|
@@ -75,7 +106,8 @@ ${c.bold('Options for init')}
|
|
|
75
106
|
--repair-bridges Regenerate IDE bridge files in an existing workspace
|
|
76
107
|
|
|
77
108
|
${c.bold('Options for update')}
|
|
78
|
-
--check
|
|
109
|
+
--check Report whether an update is available without making changes
|
|
110
|
+
--dry-run Same as --check
|
|
79
111
|
|
|
80
112
|
${c.bold('Examples')}
|
|
81
113
|
npx @aksp/opencrew init
|
|
@@ -87,8 +119,26 @@ ${c.bold('Examples')}
|
|
|
87
119
|
`);
|
|
88
120
|
}
|
|
89
121
|
|
|
90
|
-
|
|
91
|
-
|
|
122
|
+
/** Turn any error into one readable line and an exit code. Stack only with OPENCREW_DEBUG=1. */
|
|
123
|
+
export function reportError(e) {
|
|
124
|
+
if (isPromptCancel(e)) {
|
|
125
|
+
warn('Cancelled — nothing was written.');
|
|
126
|
+
return 130;
|
|
127
|
+
}
|
|
128
|
+
err(e?.message ?? String(e));
|
|
129
|
+
if (e instanceof UsageError) info(`Run ${c.cyan('npx @aksp/opencrew help')} for usage.`);
|
|
130
|
+
else if (process.env.OPENCREW_DEBUG) console.error(e?.stack);
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function run(argv, { commands = { init, update } } = {}) {
|
|
135
|
+
let command, opts;
|
|
136
|
+
try {
|
|
137
|
+
({ command, opts } = parseArgs(argv));
|
|
138
|
+
} catch (e) {
|
|
139
|
+
process.exitCode = reportError(e);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
92
142
|
|
|
93
143
|
let version = 'unknown';
|
|
94
144
|
let engines = {};
|
|
@@ -115,23 +165,23 @@ export async function run(argv) {
|
|
|
115
165
|
}
|
|
116
166
|
}
|
|
117
167
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
168
|
+
// --version / --help never run a command (they may follow any command).
|
|
169
|
+
if (opts.version || command === 'version') return log(version);
|
|
170
|
+
if (opts.help || command === 'help') return help(version);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
switch (command) {
|
|
174
|
+
case 'init':
|
|
175
|
+
return await commands.init(opts);
|
|
176
|
+
case 'update':
|
|
177
|
+
case 'upgrade':
|
|
178
|
+
return await commands.update(opts);
|
|
179
|
+
default:
|
|
180
|
+
err(`Unknown command: ${command}`);
|
|
181
|
+
help(version);
|
|
182
|
+
process.exitCode = 1;
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
process.exitCode = reportError(e);
|
|
136
186
|
}
|
|
137
187
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -2,26 +2,30 @@ import path from 'node:path';
|
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import { templatesDir, packageJsonPath } from '../lib/paths.js';
|
|
4
4
|
import { copyDir, exists, writeFileSafe, readJson, writeBridgeFile } from '../lib/fsx.js';
|
|
5
|
-
import { ideById, allIdeIds } from '../lib/ides.js';
|
|
6
|
-
import { pickIdes } from '../lib/prompts.js';
|
|
5
|
+
import { ideById, allIdeIds, AGENTS_BRIDGE } from '../lib/ides.js';
|
|
6
|
+
import { pickIdes as promptIdes } from '../lib/prompts.js';
|
|
7
|
+
import { UsageError } from '../lib/errors.js';
|
|
7
8
|
import { c, log, info, ok, warn, step } from '../lib/ui.js';
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
const STAMP = path.join('_opencrew', '.opencrew-version');
|
|
11
|
+
// .gitignore / .env.example belong to the user: opencrew only owns a marked block at the end.
|
|
12
|
+
const SHARED_BLOCK = { comment: 'hash', position: 'append' };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} opts parsed CLI options
|
|
16
|
+
* @param {{ pickIdes?: () => Promise<string[]> }} deps injectable for tests
|
|
17
|
+
*/
|
|
18
|
+
export async function init(opts = {}, { pickIdes = promptIdes } = {}) {
|
|
10
19
|
const target = process.cwd();
|
|
11
20
|
const pkg = await readJson(packageJsonPath);
|
|
12
21
|
const version = pkg.version;
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const alreadyInstalled = await exists(path.join(target, '_opencrew', 'core'));
|
|
22
|
+
const state = await workspaceState(target);
|
|
16
23
|
|
|
17
24
|
// --repair-bridges mode: regenerate IDE bridge files in an existing workspace.
|
|
18
|
-
if (
|
|
25
|
+
if (opts['repair-bridges'] && state !== 'none') {
|
|
26
|
+
const ids = await resolveIdes(opts, async () => allIdeIds());
|
|
19
27
|
log(`\n${c.bold(c.cyan('opencrew'))} ${c.dim('v' + version)} — repairing IDE bridges`);
|
|
20
28
|
log(c.dim(`Target: ${target}\n`));
|
|
21
|
-
|
|
22
|
-
let ids = normalizeIdes(opts.ide);
|
|
23
|
-
if (opts.all) ids = allIdeIds();
|
|
24
|
-
if (opts.yes || !ids) ids = allIdeIds();
|
|
25
29
|
await writeBridges(target, ids, { overwrite: true });
|
|
26
30
|
|
|
27
31
|
log(`\n${c.green(c.bold('Done!'))} IDE bridges regenerated.\n`);
|
|
@@ -29,7 +33,7 @@ export async function init(opts = {}) {
|
|
|
29
33
|
return;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
if (
|
|
36
|
+
if (state === 'complete') {
|
|
33
37
|
warn('An opencrew workspace already exists here.');
|
|
34
38
|
info(`To update only the framework, use: ${c.cyan('npx @aksp/opencrew update')}`);
|
|
35
39
|
info(`To repair IDE bridges, use: ${c.cyan('npx @aksp/opencrew init --repair-bridges')}`);
|
|
@@ -37,28 +41,18 @@ export async function init(opts = {}) {
|
|
|
37
41
|
return;
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
// Every choice is resolved BEFORE the first write: a bad --ide or Ctrl+C on the
|
|
45
|
+
// prompt leaves the folder exactly as it was.
|
|
46
|
+
const ids = await resolveIdes(opts, pickIdes);
|
|
47
|
+
|
|
40
48
|
log(`\n${c.bold(c.cyan('opencrew'))} ${c.dim('v' + version)} — scaffolding a crew workspace`);
|
|
41
49
|
log(c.dim(`Target: ${target}\n`));
|
|
50
|
+
if (state === 'partial') info('A previous install was interrupted — resuming (existing files are kept).');
|
|
42
51
|
|
|
43
52
|
// 1. Copy the framework payload (never clobber user work).
|
|
44
53
|
step('Installing framework files');
|
|
45
|
-
const copied =
|
|
46
|
-
|
|
47
|
-
overwrite: false,
|
|
48
|
-
// Never ship stray logs or browser sessions; keep the empty dir via .gitkeep only.
|
|
49
|
-
skip: (rel) =>
|
|
50
|
-
(rel.startsWith('logs/') && rel !== 'logs/.gitkeep') ||
|
|
51
|
-
rel.startsWith('_browser_profile/'),
|
|
52
|
-
onCopy: () => (copied.count += 1),
|
|
53
|
-
});
|
|
54
|
-
await copyDir(path.join(templatesDir, 'skills'), path.join(target, 'skills'), {
|
|
55
|
-
overwrite: false,
|
|
56
|
-
onCopy: () => (copied.count += 1),
|
|
57
|
-
});
|
|
58
|
-
await copyDir(path.join(templatesDir, 'crews'), path.join(target, 'crews'), {
|
|
59
|
-
overwrite: false,
|
|
60
|
-
});
|
|
61
|
-
ok(`Framework files ready (${copied.count} written, existing files preserved)`);
|
|
54
|
+
const copied = await installPayload(target);
|
|
55
|
+
ok(`Framework files ready (${copied} written, existing files preserved)`);
|
|
62
56
|
|
|
63
57
|
// 2. System doc + root configs.
|
|
64
58
|
step('Writing configuration');
|
|
@@ -68,13 +62,7 @@ export async function init(opts = {}) {
|
|
|
68
62
|
await writeFileSafe(path.join(target, '_opencrew', 'core', 'system.md'), await tpl('AGENTS.md'));
|
|
69
63
|
ok('_opencrew/core/system.md (full system definition)');
|
|
70
64
|
|
|
71
|
-
const
|
|
72
|
-
+ 'The opencrew system definition lives at `_opencrew/core/system.md`.\n'
|
|
73
|
-
+ 'Read that file and adopt the opencrew system role — follow all initialization,\n'
|
|
74
|
-
+ 'command routing, and workflow instructions defined there.\n\n'
|
|
75
|
-
+ 'Type `/opencrew` to open the main menu.\n';
|
|
76
|
-
|
|
77
|
-
const agentsResult = await writeBridgeFile(path.join(target, 'AGENTS.md'), agentsBridge);
|
|
65
|
+
const agentsResult = await writeBridgeFile(path.join(target, 'AGENTS.md'), AGENTS_BRIDGE);
|
|
78
66
|
if (agentsResult.merged) info('AGENTS.md (merged — existing content preserved)');
|
|
79
67
|
else ok('AGENTS.md (bridge to system.md)');
|
|
80
68
|
|
|
@@ -83,18 +71,13 @@ export async function init(opts = {}) {
|
|
|
83
71
|
});
|
|
84
72
|
info(mcpWritten ? '.mcp.json' : '.mcp.json (kept existing)');
|
|
85
73
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
info(giWritten ? '.gitignore' : '.gitignore (kept existing)');
|
|
74
|
+
for (const [file, template] of [['.env.example', '.env.example'], ['.gitignore', 'gitignore']]) {
|
|
75
|
+
const res = await writeBridgeFile(path.join(target, file), await tpl(template), SHARED_BLOCK);
|
|
76
|
+
info(res.merged ? `${file} (opencrew block added at the end — your lines kept)` : file);
|
|
77
|
+
}
|
|
91
78
|
|
|
92
79
|
// 3. IDE bridge files.
|
|
93
80
|
step('Configuring AI IDEs');
|
|
94
|
-
let ids = normalizeIdes(opts.ide);
|
|
95
|
-
if (opts.all) ids = allIdeIds();
|
|
96
|
-
if (opts.yes) ids = allIdeIds();
|
|
97
|
-
if (!ids) ids = await pickIdes();
|
|
98
81
|
await writeBridges(target, ids, { overwrite: false });
|
|
99
82
|
|
|
100
83
|
if (ids.includes('claude-code')) {
|
|
@@ -102,8 +85,8 @@ export async function init(opts = {}) {
|
|
|
102
85
|
warn(`native Playwright plugin/extension to avoid the two conflicting.`);
|
|
103
86
|
}
|
|
104
87
|
|
|
105
|
-
// 4. Version stamp.
|
|
106
|
-
await fs.writeFile(path.join(target,
|
|
88
|
+
// 4. Version stamp — written LAST: it is what marks the install as complete.
|
|
89
|
+
await fs.writeFile(path.join(target, STAMP), version + '\n');
|
|
107
90
|
|
|
108
91
|
// 5. Done.
|
|
109
92
|
log(`\n${c.green(c.bold('Done!'))} opencrew is installed.\n`);
|
|
@@ -113,10 +96,56 @@ export async function init(opts = {}) {
|
|
|
113
96
|
log(` No API keys needed up front — opencrew asks for them in chat only if a skill you use requires one.\n`);
|
|
114
97
|
}
|
|
115
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Copy the framework payload into `target` without overwriting anything.
|
|
101
|
+
* Never copies the version stamp: only a finished init writes it.
|
|
102
|
+
* @returns {Promise<number>} files written
|
|
103
|
+
*/
|
|
104
|
+
export async function installPayload(target) {
|
|
105
|
+
let count = 0;
|
|
106
|
+
const onCopy = () => (count += 1);
|
|
107
|
+
await copyDir(path.join(templatesDir, '_opencrew'), path.join(target, '_opencrew'), {
|
|
108
|
+
overwrite: false,
|
|
109
|
+
// Never ship stray logs, browser sessions or the template's own stamp.
|
|
110
|
+
skip: (rel) =>
|
|
111
|
+
(rel.startsWith('logs/') && rel !== 'logs/.gitkeep') ||
|
|
112
|
+
rel.startsWith('_browser_profile/') ||
|
|
113
|
+
rel === '.opencrew-version',
|
|
114
|
+
onCopy,
|
|
115
|
+
});
|
|
116
|
+
await copyDir(path.join(templatesDir, 'skills'), path.join(target, 'skills'), { overwrite: false, onCopy });
|
|
117
|
+
await copyDir(path.join(templatesDir, 'crews'), path.join(target, 'crews'), { overwrite: false });
|
|
118
|
+
return count;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 'none' (no core) · 'partial' (core without stamp: interrupted install) · 'complete'. */
|
|
122
|
+
async function workspaceState(target) {
|
|
123
|
+
if (!(await exists(path.join(target, '_opencrew', 'core')))) return 'none';
|
|
124
|
+
return (await exists(path.join(target, STAMP))) ? 'complete' : 'partial';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Decide which IDEs to configure. --all / --yes → every IDE; --ide → validated list;
|
|
129
|
+
* nothing → `fallback()` (the interactive prompt). Throws UsageError if --ide names no
|
|
130
|
+
* valid IDE.
|
|
131
|
+
*/
|
|
132
|
+
async function resolveIdes(opts, fallback) {
|
|
133
|
+
if (opts.all || opts.yes) return allIdeIds();
|
|
134
|
+
const ids = normalizeIdes(opts.ide);
|
|
135
|
+
if (!ids) return fallback();
|
|
136
|
+
const invalid = ids.filter((id) => !ideById(id));
|
|
137
|
+
const valid = ids.filter((id) => ideById(id));
|
|
138
|
+
if (!valid.length) {
|
|
139
|
+
throw new UsageError(`Unknown IDE "${invalid.join('", "')}". Valid: ${allIdeIds().join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
for (const id of invalid) warn(`Unknown IDE "${id}" — skipped. Valid: ${allIdeIds().join(', ')}`);
|
|
142
|
+
return valid;
|
|
143
|
+
}
|
|
144
|
+
|
|
116
145
|
/**
|
|
117
146
|
* Write IDE bridge files to the target directory.
|
|
118
147
|
* @param {string} target — project root
|
|
119
|
-
* @param {string[]} ids — IDE ids to configure
|
|
148
|
+
* @param {string[]} ids — validated IDE ids to configure
|
|
120
149
|
* @param {{ overwrite: boolean }} opts
|
|
121
150
|
*/
|
|
122
151
|
async function writeBridges(target, ids, { overwrite }) {
|
|
@@ -124,10 +153,6 @@ async function writeBridges(target, ids, { overwrite }) {
|
|
|
124
153
|
|
|
125
154
|
for (const id of ids) {
|
|
126
155
|
const ide = ideById(id);
|
|
127
|
-
if (!ide) {
|
|
128
|
-
warn(`Unknown IDE "${id}" — skipped. Valid: ${allIdeIds().join(', ')}`);
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
156
|
for (const f of ide.files) {
|
|
132
157
|
if (writtenPaths.has(f.path)) {
|
|
133
158
|
info(`${f.path} (shared path — written once)`);
|
|
@@ -155,5 +180,6 @@ async function tpl(name) {
|
|
|
155
180
|
function normalizeIdes(val) {
|
|
156
181
|
if (!val || val === true) return null;
|
|
157
182
|
const list = Array.isArray(val) ? val : String(val).split(',');
|
|
158
|
-
|
|
183
|
+
const ids = list.map((s) => s.trim()).filter(Boolean);
|
|
184
|
+
return ids.length ? ids : null;
|
|
159
185
|
}
|