@aksp/opencrew 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +38 -17
  3. package/bin/opencrew.js +4 -4
  4. package/package.json +4 -3
  5. package/src/cli.js +93 -43
  6. package/src/commands/init.js +79 -53
  7. package/src/commands/update.js +44 -17
  8. package/src/lib/errors.js +12 -0
  9. package/src/lib/fsx.js +18 -9
  10. package/src/lib/ides.js +9 -34
  11. package/templates/AGENTS.md +42 -63
  12. package/templates/_opencrew/.opencrew-version +1 -1
  13. package/templates/_opencrew/core/best-practices/copywriting.md +4 -1
  14. package/templates/_opencrew/core/best-practices/image-design.md +5 -5
  15. package/templates/_opencrew/core/best-practices/instagram-feed.md +4 -4
  16. package/templates/_opencrew/core/best-practices/instagram-reels.md +1 -1
  17. package/templates/_opencrew/core/best-practices/review.md +7 -0
  18. package/templates/_opencrew/core/best-practices/social-networks-publishing.md +8 -8
  19. package/templates/_opencrew/core/prompts/build.prompt.md +25 -0
  20. package/templates/_opencrew/core/prompts/design.prompt.md +9 -5
  21. package/templates/_opencrew/core/runner.pipeline.md +56 -7
  22. package/templates/_opencrew/core/scripts/verificar/leitura.mjs +99 -0
  23. package/templates/_opencrew/core/scripts/verificar/regras.mjs +127 -0
  24. package/templates/_opencrew/core/scripts/verificar.mjs +118 -0
  25. package/templates/gitignore +3 -1
  26. package/templates/skills/image-ai-generator/SKILL.md +9 -5
  27. package/templates/skills/image-creator/SKILL.md +5 -3
  28. package/templates/skills/image-fetcher/SKILL.md +1 -1
  29. package/templates/skills/instagram-publisher/SKILL.md +34 -16
  30. package/templates/skills/instagram-publisher/scripts/publish.js +58 -27
  31. package/templates/skills/template-designer/SKILL.md +3 -3
  32. package/templates/skills/template-designer/base-templates/model-a.html +1 -1
  33. package/templates/skills/template-designer/base-templates/model-b.html +1 -1
  34. package/templates/skills/template-designer/base-templates/model-c.html +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,111 @@
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.5.0] — 2026-10-02
7
+
8
+ Trilha U1 "Revisor com dentes" — primeira melhoria vinda do uso real
9
+ (`docs/jornada/2026-10-02-uso-real.md`, `specs/fase-u1-revisor-com-dentes.md`).
10
+
11
+ ### Added
12
+ - **Verificador automático** (`_opencrew/core/scripts/verificar.mjs`, Node puro): mede o texto
13
+ ANTES do revisor usando os limites `constraints:` dos best-practices — título e meta description
14
+ do blog, legenda e hashtags do Instagram, slides do carrossel, post do LinkedIn, tweets, links
15
+ (alerta quando abaixo do mínimo). Bloqueia placeholders (`wa.me/55…9999…`, `[Empresa X]`,
16
+ `lorem ipsum`…), termos entre aspas em `## Proibições Explícitas` da memória da crew e
17
+ `[PREENCHER: …]`; alerta afirmações em 1ª pessoa com dado concreto (R$, %, ano passado,
18
+ "N clientes"). Relatório em PT-BR com valor medido × limite; última linha
19
+ `VERIFICACAO:OK | BLOQUEADA | AGUARDANDO_USUARIO`.
20
+ - **Regras de veracidade** injetadas em todo passo de criação: nunca inventar casos, depoimentos,
21
+ números ou histórias em 1ª pessoa — usar `[PREENCHER: o que falta]`.
22
+
23
+ ### Changed
24
+ - **Revisão com trava**: antes de todo passo com `on_reject`, o runner verifica **todas** as saídas
25
+ desde o redator (não só a entrada do revisor — no uso real as legendas nunca eram revisadas);
26
+ `VERIFICACAO:BLOQUEADA` força REJECT seja qual for a nota; no limite de ciclos o usuário escolhe
27
+ corrigir, aceitar (registrado) ou abortar; a aprovação final mostra o resumo e pede os
28
+ `[PREENCHER]`.
29
+ - `review.md`: o revisor copia os números do relatório (nunca estima), não aprova com bloqueio e
30
+ tem nota máxima 7/10 com alerta não resolvido. `copywriting.md` e o build: regra de não inventar.
31
+ - **Instagram em 4:5**: carrossel/feed agora 1080×1350 (a API do Instagram só publica de 4:5 a
32
+ 1,91:1), no máximo 10 slides; presets do `image-creator`, `template-designer` (e modelos-base),
33
+ `image-fetcher` e best-practices atualizados. Limites com nomes canônicos (`hashtags_max`).
34
+
35
+ ### Internal
36
+ - Docs de jornada (`docs/jornada/`: uso real, roteiro de teste U0, medições) e roadmap de trilhas U.
37
+ - Regras de dev 12 (limite só vale se medido) e 13 (PT-BR para o usuário); alerta de tamanho e
38
+ lint cobrem os scripts do runtime.
39
+
40
+ ## [1.4.2] — 2026-10-02
41
+
42
+ Hotfix "parar de causar dano" (Fase 1 da auditoria — `specs/fase-1-hotfix.md`).
43
+
44
+ ### Fixed
45
+ - **`CLAUDE.md` gerado não traz mais o fluxo STATUS.md do mantenedor** (vazado na 1.4.0/1.4.1);
46
+ o `update` remove a seção de instalações existentes, sem tocar no texto do usuário.
47
+ `STATUS.md` saiu do `.gitignore` do template.
48
+ - **`--help` nunca executa comando**: `update --help` / `-h` e `init --help` só mostram a ajuda.
49
+ Parser estrito (`node:util.parseArgs`): opção desconhecida, `--ide` sem id válido ou
50
+ argumento solto (`init minha-pasta`) falham com exit 1 **antes** de escrever qualquer arquivo.
51
+ `--ide claude-code` (com espaço) e `-yv` funcionam. `update --dry-run` = `--check`.
52
+ - **`update` sem `AGENTS.md`** cria a ponte em vez de quebrar com ENOENT.
53
+ - **Migração do `AGENTS.md` legado faz backup** em `AGENTS.md.bak` (ou `.bak-<timestamp>`).
54
+ - **`.env.example` e `.gitignore` do usuário não são mais sobrescritos/ignorados**: recebem um
55
+ bloco `# opencrew:start … # opencrew:end` no fim; as linhas do usuário ficam intactas.
56
+ O bloco do `.gitignore` agora cobre `.claude/settings.local.json`, `crews/*/state.json` e
57
+ `crews/*/_investigations/`.
58
+ - **Ctrl+C no prompt de IDEs não deixa instalação pela metade**: as IDEs são escolhidas antes
59
+ da primeira escrita; cancelamento sai com 130 e "Cancelled — nothing was written.". Uma
60
+ instalação interrompida (core sem stamp de versão) é **retomada** pelo próximo `init`.
61
+ - Erros inesperados mostram uma linha `✗ <mensagem>` (stack só com `OPENCREW_DEBUG=1`).
62
+ - Marcadores de bloco: um `start` órfão (fim apagado à mão) não faz mais a regravação engolir
63
+ linhas do usuário.
64
+ - Skills: caminho do `image-ai-generator` corrigido (`{skill_path}/scripts/generate.py`, nota
65
+ `py -3` no Windows); `instagram-publisher` lê as imagens da pasta do run atual; o
66
+ `image-creator` renderiza JPEG quando o destino é Instagram.
67
+ - Runner: o toggle do dashboard reconhece o formato `- **Dashboard:** enabled` gravado no
68
+ onboarding.
69
+
70
+ ### Security
71
+ - **Publicar/enviar é sempre o último trecho do pipeline**: `… → Review → Final Approval
72
+ checkpoint → [Publish/Send]` (design), com o novo Gate 2c BLOCKING no build. Passos com
73
+ `side_effects: irreversible` rodam inline e **nunca** têm retry automático nem auto-correção
74
+ de veto — o runner avisa que a ação pode já ter acontecido e pergunta.
75
+ - **`instagram-publisher`**: legenda via `--caption-file` (nunca interpolada no shell); só
76
+ aceita `.jpg`/`.jpeg` dentro de `crews/*/output/`; upload no imgBB expira em 24h; token da
77
+ Graph API no corpo dos POST, não na URL; fluxo preview → `--dry-run` → confirmação explícita
78
+ ("publish"/"publicar") → publicação única.
79
+
80
+ ### Changed
81
+ - Template `templates/AGENTS.md` (o `system.md` instalado) compactado (−24%) sem mudar o roteamento.
82
+
83
+ ### Internal
84
+ - Testes por cenário da spec (F1-01a…F1-13a); trava nova: nenhum arquivo de `templates/`
85
+ carrega conteúdo do mantenedor. `KNOWN_BROKEN` de referências zerado.
86
+
87
+ ### Added (governança do repositório — Fase 0)
88
+ - Auditoria geral da v1.4.1 com roadmap por fases: `docs/auditoria/2026-10-02-auditoria-geral.md`.
89
+ - Regras de desenvolvimento em `AGENTS.md` (project-standards T3, tabela Regra → Trava);
90
+ `CLAUDE.md` da raiz vira apontador versionado.
91
+ - Porta de verificação única `npm run verify` (`scripts/verify.js`): lint (agora inclui
92
+ `bin/`), testes (descobertos automaticamente em `tests/`), version-sync e alerta de
93
+ tamanho (`scripts/check-size.js`). CI e publish chamam a mesma porta.
94
+ - Travas novas: `tests/package.test.js` (conteúdo do tarball × README),
95
+ `tests/template-refs.test.js` (caminhos citados nos prompts existem),
96
+ `tests/verify.test.js` e `tests/check-size.test.js` (provetas).
97
+ - `GLOSSARIO.md`; `IDEIAS.md` reformatado com triagem e `Alocação: →`.
98
+
99
+ ### Changed (governança do repositório — Fase 0)
100
+ - Dogfood do mantenedor sai da raiz e vai para `sandbox/` (fora do git).
101
+ - `publish.yml` confere a tag contra a versão do `package.json` e roda `npm run verify`;
102
+ CI e publish usam só `npm ci` (sem fallback que esconde drift do lockfile).
103
+
104
+ ### Docs (Fase 0)
105
+ - README: o dashboard não é instalado pelo `init`; `update` não atualiza as pontes de IDE
106
+ (use `init --repair-bridges`); migração do `AGENTS.md` legado perde instruções extras;
107
+ flags `upgrade`, `--ide`, `--all`/`-y`, `--repair-bridges` documentadas; contagens
108
+ corrigidas (22 guias, 13 prompts); Windsurf removido da lista; promessa "30-70% de
109
+ economia" sem base removida.
110
+
6
111
  ## [1.4.1] — 2026-08-04
7
112
 
8
113
  ### Fixed
@@ -25,6 +130,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
25
130
  - `AGENTS.md` — step 7b na seção Loading the Pipeline Runner.
26
131
  - 3 novos testes de contrato em `docs.test.js`.
27
132
 
133
+ > Itens desta versão que ficaram de fora da entrada original (acrescentados na auditoria
134
+ > de 2026-10-02):
135
+ - **Pontes `.agents/`** para Antigravity, Gemini CLI e Qwen Code
136
+ (`.agents/skills/opencrew/SKILL.md`, `.agents/workflows/opencrew.md`).
137
+ - **`init --repair-bridges`**: regrava as pontes de IDE num workspace existente.
138
+ - **`update` migra `AGENTS.md` legado** (pré-1.3, sistema completo) para a ponte fina.
139
+ - **Fix Antigravity**: frontmatter no workflow para registrar `/opencrew`.
140
+ - ⚠️ **Regressão**: o `CLAUDE.md` gerado passou a conter a seção "STATUS.md (gestão de
141
+ sessão)" do fluxo pessoal do mantenedor, e `templates/gitignore` ganhou `STATUS.md`.
142
+ Correção prevista na 1.4.2 (F1-01).
143
+
28
144
  ## [1.3.3] — 2026-08-03
29
145
 
30
146
  ### Fixed
package/README.md CHANGED
@@ -34,7 +34,12 @@ 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. Economia de 30-70% de tokens quando agentes são pulados.
37
+ ajusta com um clique. Agentes pulados não gastam tokens naquele run.
38
+ - 🔎 **Revisor com dentes** — antes da revisão, um verificador automático mede o texto
39
+ (tamanho de título, meta description, legenda, hashtags, slides), barra placeholders,
40
+ termos que você proibiu e `[PREENCHER]` pendentes, e aponta afirmações a confirmar.
41
+ Bloqueio não passa, seja qual for a nota do revisor. A crew não inventa casos nem números:
42
+ quando falta um dado real, ela pergunta na aprovação final.
38
43
 
39
44
  ---
40
45
 
@@ -126,12 +131,12 @@ enxuto — todos apontam para a mesma fonte.
126
131
  |---|---|
127
132
  | `AGENTS.md` (ponte) + `.claude/skills/opencrew/SKILL.md` + `CLAUDE.md` | Claude Code |
128
133
  | `AGENTS.md` (ponte) + `.agents/skills/opencrew/SKILL.md` | OpenAI Codex, Codex CLI |
129
- | `AGENTS.md` (ponte) + `.cursor/rules/opencrew.mdc` | Cursor, Windsurf |
134
+ | `AGENTS.md` (ponte) + `.cursor/rules/opencrew.mdc` | Cursor |
130
135
  | `AGENTS.md` (ponte) + `.github/copilot-instructions.md` | VS Code + GitHub Copilot |
131
136
  | `AGENTS.md` (ponte) + `.opencode/commands/opencrew.md` | OpenCode |
132
137
  | `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 |
138
+ | `GEMINI.md` (ponte) + `.agents/skills/opencrew/SKILL.md` | Gemini CLI |
139
+ | `QWEN.md` (ponte) + `.agents/skills/opencrew/SKILL.md` | Qwen Code |
135
140
  | `AGENTS.md` (ponte) + `.trae/rules/opencrew.md` | Trae |
136
141
 
137
142
  > ⚠️ **Importante:** `CLAUDE.md`, `GEMINI.md` e os demais arquivos de IDE são
@@ -150,8 +155,8 @@ meu-projeto/
150
155
  ├── CLAUDE.md ← ponte fina + suas instruções (merge)
151
156
  ├── GEMINI.md ← ponte fina (Gemini CLI)
152
157
  ├── .mcp.json ← servidor Playwright do OpenCrew
153
- ├── .gitignore
154
- ├── .env.example
158
+ ├── .gitignore ← bloco `# opencrew` no fim; suas linhas são mantidas
159
+ ├── .env.example ← idem
155
160
  │
156
161
  ├── _opencrew/
157
162
  │ ├── core/
@@ -159,8 +164,8 @@ meu-projeto/
159
164
  │ │ ├── runner.pipeline.md ← executor de pipeline
160
165
  │ │ ├── skills.engine.md ← gerenciador de skills
161
166
  │ │ ├── architect.agent.yaml ← definição do Arquiteto
162
- │ │ ├── best-practices/ ← 23 guias de melhores práticas
163
- │ │ └── prompts/ ← 12 prompts de fase (discovery, design, build, etc.)
167
+ │ │ ├── best-practices/ ← 22 guias de melhores práticas + _catalog.yaml
168
+ │ │ └── prompts/ ← 13 prompts de fase (discovery, design, build, etc.)
164
169
  │ ├── agents/ ← 5 agentes base compartilhados
165
170
  │ │ ├── researcher.agent.md
166
171
  │ │ ├── copywriter.agent.md
@@ -185,11 +190,12 @@ meu-projeto/
185
190
  │ ├── instagram-publisher/ ← publicação no Instagram
186
191
  │ ├── resend/ ← envio de emails
187
192
  │ └── ...
188
- │
189
- └── dashboard/
190
- └── index.html ← dashboard visual (opcional, offline)
191
193
  ```
192
194
 
195
+ > O dashboard visual (`dashboard/index.html`) **não é instalado** pelo `init` — ele vive
196
+ > só no repositório do OpenCrew e ainda é experimental (decisão de publicar ou remover:
197
+ > Fase 4 da auditoria em `docs/auditoria/`).
198
+
193
199
  ---
194
200
 
195
201
  ## Mantendo o OpenCrew atualizado
@@ -198,18 +204,29 @@ meu-projeto/
198
204
  npx @aksp/opencrew update
199
205
  ```
200
206
 
201
- O `update` **nunca destrói seus dados**. Ele atualiza apenas:
207
+ O `update` não toca nas suas crews nem na sua memória. Ele atualiza apenas:
202
208
 
203
209
  | O que é atualizado | O que NUNCA é tocado |
204
210
  |---|---|
205
- | `_opencrew/core/` (framework) | `crews/` (suas crews) |
206
- | Skills do catálogo | `_opencrew/_memory/` (perfil, preferências) |
211
+ | `_opencrew/core/` (framework — sobrescrito por inteiro) | `crews/` (suas crews) |
212
+ | Skills do catálogo (sobrescritas — edições locais se perdem) | `_opencrew/_memory/` (perfil, preferências) |
207
213
  | `_opencrew/core/system.md` | `.env` (suas chaves) |
208
- | Bloco `<!-- opencrew -->` nos bridges | Arquivos de IDE (fora do bloco) |
214
+ | Bloco `<!-- opencrew -->` no `AGENTS.md` | Pontes das IDEs (`CLAUDE.md`, `.cursor/`, `.agents/`…) |
215
+
216
+ As **pontes das IDEs não são atualizadas** pelo `update`. Para regravá-las com a versão
217
+ atual, use:
218
+
219
+ ```bash
220
+ npx @aksp/opencrew init --repair-bridges --ide=claude-code
221
+ ```
222
+
223
+ (troque `claude-code` pelas IDEs que você usa, separadas por vírgula; sem `--ide`, o
224
+ comando grava as pontes de **todas** as IDEs suportadas).
209
225
 
210
226
  Se você está migrando de uma versão anterior a v1.3, o `update` detecta
211
227
  AGENTS.md legados (sistema completo de 150 linhas) e os substitui pela ponte
212
- fina automaticamente, sem perder suas instruções.
228
+ fina. Desde a v1.4.2 o arquivo original é copiado antes para `AGENTS.md.bak` (até a v1.4.1,
229
+ instruções suas adicionadas a esse `AGENTS.md` legado eram perdidas).
213
230
 
214
231
  Para verificar se há atualização disponível sem aplicar:
215
232
 
@@ -245,7 +262,11 @@ npx @aksp/opencrew update --check
245
262
  |---|---|
246
263
  | `npx @aksp/opencrew init` | Instala o OpenCrew na pasta atual |
247
264
  | `npx @aksp/opencrew update` | Atualiza o framework |
248
- | `npx @aksp/opencrew update --check` | Verifica se há update disponível |
265
+ | `npx @aksp/opencrew update --check` (ou `--dry-run`) | Verifica se há update disponível, sem alterar nada |
266
+ | `npx @aksp/opencrew upgrade` | Atalho para `update` |
267
+ | `npx @aksp/opencrew init --ide=claude-code,cursor` | Instala só as pontes das IDEs indicadas |
268
+ | `npx @aksp/opencrew init --all` (ou `-y`) | Instala as pontes de todas as IDEs |
269
+ | `npx @aksp/opencrew init --repair-bridges` | Regrava as pontes de IDE num workspace existente |
249
270
  | `npx @aksp/opencrew version` | Mostra a versão instalada |
250
271
  | `npx @aksp/opencrew help` | Mostra ajuda dos comandos CLI |
251
272
 
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(process.argv.slice(2)).catch((err) => {
6
- console.error(err?.stack || String(err));
7
- process.exit(1);
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.1",
3
+ "version": "1.5.0",
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 --test tests/cli.test.js tests/docs.test.js tests/fsx.test.js tests/ides.test.js tests/init.test.js tests/paths.test.js tests/ui.test.js tests/update.test.js",
23
- "lint": "eslint src/ tests/ scripts/",
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
- function parseArgs(argv) {
35
- const opts = { _: [] };
36
- for (const a of argv) {
37
- if (a.startsWith('--')) {
38
- const eq = a.indexOf('=');
39
- const k = eq > -1 ? a.slice(2, eq) : a.slice(2);
40
- const v = eq > -1 ? a.slice(eq + 1) : true;
41
- opts[k] = v;
42
- } else if (a.startsWith('-') && a.length === 2) {
43
- // Short flags: -y → yes, -v → version, -h → help
44
- const short = a[1];
45
- opts[short] = true;
46
- } else {
47
- opts._.push(a);
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
- // Normalize short flags to their long-form equivalents.
51
- if (opts.y) opts.yes = true;
52
- if (opts.v) opts.version = true;
53
- if (opts.h) opts.help = true;
54
- return opts;
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 Dry-run: report whether an update is available without making changes
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
- export async function run(argv) {
91
- const opts = parseArgs(argv);
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
- const cmd = opts._[0] || (opts.version ? 'version' : opts.help ? 'help' : 'init');
119
-
120
- switch (cmd) {
121
- case 'init':
122
- return init(opts);
123
- case 'update':
124
- case 'upgrade':
125
- return update(opts);
126
- case 'version':
127
- case '--version':
128
- return log(version);
129
- case 'help':
130
- case '--help':
131
- return help(version);
132
- default:
133
- err(`Unknown command: ${cmd}`);
134
- help(version);
135
- process.exitCode = 1;
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
  }