@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 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. 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
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, Windsurf |
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/ ← 23 guias de melhores práticas
163
- │ │ └── prompts/ ← 12 prompts de fase (discovery, design, build, etc.)
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` **nunca destrói seus dados**. Ele atualiza apenas:
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 -->` nos bridges | Arquivos de IDE (fora do bloco) |
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 automaticamente, sem perder suas instruções.
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(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.0",
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 --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
  }
@@ -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
- export async function init(opts = {}) {
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 repairBridges = opts['repair-bridges'];
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 (repairBridges && alreadyInstalled) {
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 (alreadyInstalled) {
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 = { count: 0 };
46
- await copyDir(path.join(templatesDir, '_opencrew'), path.join(target, '_opencrew'), {
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 agentsBridge = '# opencrew\n\n'
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
- await writeFileSafe(path.join(target, '.env.example'), await tpl('.env.example'));
87
- const giWritten = await writeFileSafe(path.join(target, '.gitignore'), await tpl('gitignore'), {
88
- overwrite: false,
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, '_opencrew', '.opencrew-version'), version + '\n');
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
- return list.map((s) => s.trim()).filter(Boolean);
183
+ const ids = list.map((s) => s.trim()).filter(Boolean);
184
+ return ids.length ? ids : null;
159
185
  }