wizz-method 1.3.0 → 1.3.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "wizz-method",
4
- "version": "1.3.0",
4
+ "version": "1.3.2",
5
5
  "description": "Wizz Method — método de agência orientado por IA em PT-BR (fork independente do BMad Method)",
6
6
  "keywords": [
7
7
  "agile",
@@ -196,6 +196,17 @@ areas:
196
196
  server:
197
197
  command: scrapling
198
198
  args: ["mcp"]
199
+ # The MCP server lives in the `[ai]` extra, and fetching needs browsers.
200
+ # Without this the installer would write a config for a binary that
201
+ # isn't installed → the client fails with ENOENT. `setup` makes the
202
+ # installer install the package, download browsers, resolve `scrapling`
203
+ # to an absolute path (server.command is rewritten), and verify it boots
204
+ # before writing; if verify fails the server is skipped, not written.
205
+ setup:
206
+ bin: scrapling
207
+ install: 'uv tool install "scrapling[ai]" || pipx install "scrapling[ai]" || pip install --user "scrapling[ai]"'
208
+ post_install: "{bin} install"
209
+ verify: "{bin} --version"
199
210
 
200
211
  social:
201
212
  agent: wizz-social
@@ -18,6 +18,9 @@ Nunca abra arquivos grandes "para entender o contexto" sem antes tentar os 3 pas
18
18
 
19
19
  ## Output
20
20
 
21
+ - **Não narre o passo a passo enquanto trabalha.** Nada de "agora vou fazer X, depois Y". Isso gasta token e polui a conversa. Trabalhe direto e só mostre o resultado.
22
+ - **Pause e pergunte no que é importante.** Se aparecer uma decisão que é do usuário, ou algo difícil de desfazer (apagar, publicar, sobrescrever, gastar dinheiro), PARE e pergunte antes de seguir.
23
+ - **Feche com resumo curto.** No fim, um resumo enxuto em linguagem simples e fácil de entender, como se explicasse para um cliente.
21
24
  - Resposta enxuta, em PT-BR fácil. Sem repetir o que o usuário já sabe.
22
25
  - Não narre opções que você não vai seguir. Recomende e aja.
23
26
  - Não re-explique o que já foi dito na sessão.
@@ -12,6 +12,7 @@ activation_steps_prepend = [
12
12
  ]
13
13
 
14
14
  activation_steps_append = [
15
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
15
16
  "ENCERRAMENTO: finalize com '✅ O que fiz' + '➡️ Próximo passo' + '🎯 Comando'. Acrescente '💾 Quer que eu salve no cerebro?' se definiu estrutura de campanha.",
16
17
  ]
17
18
 
@@ -12,6 +12,7 @@ activation_steps_prepend = [
12
12
  ]
13
13
 
14
14
  activation_steps_append = [
15
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
15
16
  "ENCERRAMENTO: finalize com '✅ O que fiz' + '➡️ Próximo passo' + '🎯 Comando'. Acrescente '💾 Quer que eu salve no cerebro?' se definiu tom de voz/posicionamento.",
16
17
  ]
17
18
 
@@ -13,6 +13,7 @@ activation_steps_prepend = [
13
13
  ]
14
14
 
15
15
  activation_steps_append = [
16
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
16
17
  "ENCERRAMENTO: finalize SEMPRE com '✅ O que fiz' (1-2 frases simples) + '➡️ Próximo passo' (normalmente chamar o wizz-dev para construir) + '🎯 Comando: /wizz:<algo>'. Se decidiu algo de marca, acrescente '💾 Quer que eu salve no cerebro?'.",
17
18
  ]
18
19
 
@@ -12,6 +12,7 @@ activation_steps_prepend = [
12
12
  ]
13
13
 
14
14
  activation_steps_append = [
15
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
15
16
  "ENCERRAMENTO: finalize com '✅ O que fiz' + '➡️ Próximo passo' + '🎯 Comando'. Acrescente '💾 Quer que eu salve no cerebro?' se definiu estratégia/preço.",
16
17
  ]
17
18
 
@@ -15,6 +15,7 @@ activation_steps_prepend = [
15
15
 
16
16
  # Após saudar: fixa o protocolo de encerramento.
17
17
  activation_steps_append = [
18
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
18
19
  "ENCERRAMENTO: ao terminar qualquer tarefa, finalize SEMPRE neste formato — uma linha '✅ O que fiz' com 1-2 frases simples, uma linha '➡️ Próximo passo' com o agente a chamar ou o que fazer, e uma linha '🎯 Comando: /wizz:<algo>'. Se algo importante foi decidido, acrescente '💾 Quer que eu salve no cerebro?'.",
19
20
  ]
20
21
 
@@ -11,6 +11,7 @@ activation_steps_prepend = [
11
11
  ]
12
12
 
13
13
  activation_steps_append = [
14
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
14
15
  "ENCERRAMENTO: finalize com '✅ O que fiz' + '➡️ Próximo passo' + '🎯 Comando'.",
15
16
  ]
16
17
 
@@ -12,6 +12,7 @@ activation_steps_prepend = [
12
12
  ]
13
13
 
14
14
  activation_steps_append = [
15
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
15
16
  "ENCERRAMENTO: finalize com '✅ O que fiz' + '➡️ Próximo passo' (voltar pro wizz-dev se achou bug, ou seguir pra entrega) + '🎯 Comando'. Acrescente '💾 Quer que eu salve no cerebro?' se achou um bug não-óbvio.",
16
17
  ]
17
18
 
@@ -12,6 +12,7 @@ activation_steps_prepend = [
12
12
  ]
13
13
 
14
14
  activation_steps_append = [
15
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
15
16
  "ENCERRAMENTO: finalize com '✅ O que fiz' + '➡️ Próximo passo' + '🎯 Comando'. Acrescente '💾 Quer que eu salve no cerebro?' se definiu estratégia de keywords.",
16
17
  ]
17
18
 
@@ -13,6 +13,7 @@ activation_steps_prepend = [
13
13
  ]
14
14
 
15
15
  activation_steps_append = [
16
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
16
17
  "APRESENTAÇÃO: na primeira mensagem da sessão use uma variação de 'E aí! Sou o Rafa, teu parceiro de viralização haha. Me fala o tema e a gente monta um roteiro que vai parar o scroll. Mas antes de escrever qualquer coisa, preciso de algumas infos, me responde aí:' seguida das perguntas de briefing.",
17
18
  "ENCERRAMENTO: finalize com '✅ O que fiz' + '➡️ Próximo passo' + '🎯 Comando'. Acrescente '💾 Quer que eu salve no cerebro?' se definiu nicho, tom ou palavras-chave de CTA.",
18
19
  ]
@@ -10,6 +10,7 @@ activation_steps_prepend = [
10
10
  ]
11
11
 
12
12
  activation_steps_append = [
13
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
13
14
  "WIZZ ENCERRAMENTO: ao terminar qualquer tarefa, finalize SEMPRE com '✅ O que fiz' (1-2 frases simples) + '➡️ Próximo passo' (qual agente chamar) + '🎯 Comando: /wizz:<algo>'. Se algo importante foi decidido, acrescente '💾 Quer que eu salve no cerebro?'.",
14
15
  ]
15
16
 
@@ -9,6 +9,7 @@ activation_steps_prepend = [
9
9
  ]
10
10
 
11
11
  activation_steps_append = [
12
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
12
13
  "WIZZ ENCERRAMENTO: finalize SEMPRE com '✅ O que fiz' + '➡️ Próximo passo' (ex: chamar o dev) + '🎯 Comando: /wizz:<algo>'. Acrescente '💾 Quer que eu salve no cerebro?' (decisão de arquitetura quase sempre vale salvar).",
13
14
  ]
14
15
 
@@ -9,6 +9,7 @@ activation_steps_prepend = [
9
9
  ]
10
10
 
11
11
  activation_steps_append = [
12
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
12
13
  "WIZZ ENCERRAMENTO: finalize SEMPRE com '✅ O que fiz' + '➡️ Próximo passo' (ex: chamar o wizz-qa para testar) + '🎯 Comando: /wizz:<algo>'. Acrescente '💾 Quer que eu salve no cerebro?' se resolveu um bug não-óbvio ou armadilha.",
13
14
  ]
14
15
 
@@ -9,6 +9,7 @@ activation_steps_prepend = [
9
9
  ]
10
10
 
11
11
  activation_steps_append = [
12
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
12
13
  "WIZZ ENCERRAMENTO: finalize SEMPRE com '✅ O que fiz' + '➡️ Próximo passo' (ex: chamar o arquiteto ou o dev) + '🎯 Comando: /wizz:<algo>'. Acrescente '💾 Quer que eu salve no cerebro?' se decidiu escopo/prioridade.",
13
14
  ]
14
15
 
@@ -9,6 +9,7 @@ activation_steps_prepend = [
9
9
  ]
10
10
 
11
11
  activation_steps_append = [
12
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
12
13
  "WIZZ ENCERRAMENTO: finalize SEMPRE com '✅ O que fiz' + '➡️ Próximo passo' + '🎯 Comando: /wizz:<algo>'.",
13
14
  ]
14
15
 
@@ -11,6 +11,7 @@ activation_steps_prepend = [
11
11
  ]
12
12
 
13
13
  activation_steps_append = [
14
+ "WIZZ COMUNICAÇÃO: não narre o passo a passo enquanto trabalha. Isso gasta token e polui a conversa. Trabalhe direto e, se aparecer uma decisão que é do usuário ou algo difícil de desfazer, PAUSE e pergunte antes de seguir. Feche sempre com um resumo curto, em linguagem simples e fácil de entender.",
14
15
  "WIZZ ENCERRAMENTO: finalize SEMPRE com '✅ O que fiz' + '➡️ Próximo passo' + '🎯 Comando: /wizz:<algo>'. Para visual premium/landing/motion, indique o wizz-designer.",
15
16
  ]
16
17
 
@@ -3,7 +3,7 @@ const fs = require('../fs-native');
3
3
  const { Manifest } = require('./manifest');
4
4
  const { OfficialModules } = require('../modules/official-modules');
5
5
  const { installSkillsLib } = require('../modules/skills-lib');
6
- const { writeMcpConfig, renderAddCommand } = require('../modules/mcp-config');
6
+ const { writeMcpConfig, renderAddCommand, prepareMcps } = require('../modules/mcp-config');
7
7
  const { installClis, renderInstallCommand } = require('../modules/cli-config');
8
8
  const { writeDepsCache } = require('../modules/deps-cache');
9
9
  const { IdeManager } = require('../ide/manager');
@@ -334,13 +334,26 @@ class Installer {
334
334
  const toWrite = mcpPlan.toWrite || [];
335
335
  if (toWrite.length > 0) {
336
336
  message('Configuring MCP servers...');
337
+ // Some servers (e.g. scrapling) shell out to a binary that must be
338
+ // installed and resolved to an absolute path first — otherwise we
339
+ // would write a config the client can't launch (ENOENT). prepareMcps
340
+ // installs + verifies those, patching server.command to the absolute
341
+ // path; a server whose setup fails is dropped here (never written)
342
+ // and reported so the user can fix it, instead of leaving a broken
343
+ // config behind. Servers without a `setup` block pass through as-is.
344
+ const { ready: mcpsReady, failed: mcpsFailed } = await prepareMcps({ mcps: toWrite });
345
+ for (const f of mcpsFailed) {
346
+ await prompts.log.warn(
347
+ `MCP ${f.id} não pôde ser preparado (${f.error}). Config NÃO escrita para evitar ENOENT — instale manualmente e adicione depois com \`claude mcp add\`.`,
348
+ );
349
+ }
337
350
  // Intentionally NOT tracked in installedFiles: .mcp.json is a
338
351
  // shared, user-owned config we merge into (it may hold the user's
339
352
  // own servers). Tracking it would expose it to uninstall removal
340
353
  // and manifest hash-management, which must never touch it.
341
354
  const mcpResult = await writeMcpConfig({
342
355
  projectDir: paths.projectRoot,
343
- mcps: toWrite,
356
+ mcps: mcpsReady,
344
357
  });
345
358
  if (mcpResult.added.length > 0) {
346
359
  addResult('MCP servers', 'ok', `${mcpResult.added.join(', ')} → .mcp.json`);
@@ -20,6 +20,7 @@
20
20
 
21
21
  const path = require('node:path');
22
22
  const fs = require('../fs-native');
23
+ const { defaultExec } = require('./cli-config');
23
24
 
24
25
  /**
25
26
  * Resolve the recommended MCP entries for the chosen areas, deduped by id.
@@ -48,6 +49,12 @@ function resolveMcps(registry, selectedAreas) {
48
49
  id: mcp.id,
49
50
  when: mcp.when || '',
50
51
  server: mcp.server,
52
+ // `setup` (optional) declares how to make a `command:`-relative server
53
+ // actually usable before it is written: install the backing package,
54
+ // download runtime assets, resolve the binary to an absolute path, and
55
+ // verify it boots. Carried here so prepareMcps() can act on it; dropped
56
+ // from what toServerConfig() writes (it never belongs in .mcp.json).
57
+ setup: mcp.setup || null,
51
58
  areas: areaKey ? [areaKey] : [],
52
59
  });
53
60
  };
@@ -74,6 +81,157 @@ function toServerConfig(server) {
74
81
  return out;
75
82
  }
76
83
 
84
+ /**
85
+ * POSIX-quote a path so it survives interpolation into a shell command even
86
+ * when it contains spaces or shell metacharacters. Single-quote wrapped, with
87
+ * embedded single quotes escaped the classic `'\''` way.
88
+ * @param {string} value
89
+ * @returns {string}
90
+ */
91
+ function shellQuote(value) {
92
+ return `'${String(value).replaceAll("'", `'\\''`)}'`;
93
+ }
94
+
95
+ /**
96
+ * Substitute the `{bin}` placeholder in a setup command with the shell-quoted
97
+ * absolute path of the resolved binary. Lets the registry write portable setup
98
+ * commands (`{bin} install`, `{bin} --version`) that bind to the real path at
99
+ * run time instead of relying on PATH.
100
+ * @param {string} command - Command template possibly containing `{bin}`
101
+ * @param {string} binPath - Absolute path to substitute
102
+ * @returns {string}
103
+ */
104
+ function substituteBin(command, binPath) {
105
+ return String(command).replaceAll('{bin}', shellQuote(binPath));
106
+ }
107
+
108
+ /**
109
+ * Resolve the absolute path of a binary an MCP server shells out to. A bare
110
+ * `command: "scrapling"` is fragile: the MCP subprocess spawned by the client
111
+ * does not always inherit the user's ~/.local/bin on PATH, so a relative name
112
+ * fails with ENOENT even when the tool is installed. We resolve and write the
113
+ * absolute path instead. Tries `command -v` first (honors the current PATH),
114
+ * then the conventional user-local bin dir where `uv tool` / `pipx` / `pip
115
+ * --user` land executables. Returns null when the binary is nowhere to be found.
116
+ *
117
+ * @param {string} bin - Binary name to locate
118
+ * @param {Function} [exec] - Injected runner (command, opts) => {ok, stdout,...}
119
+ * @returns {Promise<string|null>} Absolute path, or null if not found.
120
+ */
121
+ async function resolveBinPath(bin, exec = defaultExec) {
122
+ if (!bin) return null;
123
+ const viaWhich = await exec(`command -v ${bin}`, { timeoutMs: 5000 });
124
+ if (viaWhich.ok) {
125
+ const found = (viaWhich.stdout || '').trim().split('\n')[0].trim();
126
+ if (found) return found;
127
+ }
128
+ const home = process.env.HOME || process.env.USERPROFILE || '';
129
+ if (home) {
130
+ const candidate = path.join(home, '.local', 'bin', bin);
131
+ if (await fs.pathExists(candidate)) return candidate;
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Run an MCP entry's `setup` block so the server is genuinely usable BEFORE it
138
+ * lands in .mcp.json. This closes the ENOENT gap where the installer wrote a
139
+ * server config but never installed the backing tool. Contract (detect-first,
140
+ * mirroring cli-config's install philosophy):
141
+ *
142
+ * 1. DETECT — resolve the binary; if already present we skip installation.
143
+ * 2. INSTALL — on a miss, run `setup.install` (package + its MCP extra), then
144
+ * re-resolve. `setup.post_install` (e.g. downloading browsers) runs after,
145
+ * only on a fresh install, with `{bin}` bound to the resolved path.
146
+ * 3. RESOLVE — rewrite `server.command` to the ABSOLUTE path so the client's
147
+ * MCP subprocess never depends on PATH inheriting ~/.local/bin.
148
+ * 4. VERIFY — run `setup.verify` ({bin} bound). A failure means writing the
149
+ * config would point at a broken/missing binary, so we DO NOT write it —
150
+ * the entry is reported failed and the caller drops it with a clear message.
151
+ *
152
+ * Immutable: returns a new entry with a patched `server.command`; the input is
153
+ * never mutated. Entries without a `setup` block pass through untouched.
154
+ *
155
+ * @param {Object} args
156
+ * @param {Object} args.mcp - Resolved MCP entry (id, server, setup?)
157
+ * @param {Function} [args.exec] - Injected runner (command, opts) => {ok,...}
158
+ * @returns {Promise<{mcp: Object, ok: boolean, ran: boolean, error: string|null}>}
159
+ */
160
+ async function prepareMcp({ mcp, exec = defaultExec }) {
161
+ const setup = mcp && mcp.setup;
162
+ if (!setup) return { mcp, ok: true, ran: false, error: null };
163
+
164
+ const bin = setup.bin || (mcp.server && mcp.server.command);
165
+ if (!bin) {
166
+ return { mcp, ok: false, ran: false, error: 'setup declarado sem `bin` nem `server.command` para resolver' };
167
+ }
168
+
169
+ // 1. DETECT — install only when the binary is not already resolvable.
170
+ let binPath = await resolveBinPath(bin, exec);
171
+ const freshInstall = !binPath;
172
+
173
+ if (freshInstall) {
174
+ if (!setup.install) {
175
+ return { mcp, ok: false, ran: false, error: `binário '${bin}' ausente e setup não tem comando \`install\`` };
176
+ }
177
+ const installed = await exec(setup.install, { timeoutMs: 900_000 });
178
+ if (!installed.ok) {
179
+ return { mcp, ok: false, ran: true, error: `instalação falhou: ${(installed.stderr || 'erro desconhecido').trim()}` };
180
+ }
181
+ binPath = await resolveBinPath(bin, exec);
182
+ if (!binPath) {
183
+ return {
184
+ mcp,
185
+ ok: false,
186
+ ran: true,
187
+ error: `'${bin}' não encontrado no PATH nem em ~/.local/bin após a instalação`,
188
+ };
189
+ }
190
+ // 2. POST-INSTALL (e.g. browser download) — fresh installs only, idempotent.
191
+ if (setup.post_install) {
192
+ const post = await exec(substituteBin(setup.post_install, binPath), { timeoutMs: 900_000 });
193
+ if (!post.ok) {
194
+ return { mcp, ok: false, ran: true, error: `pós-instalação falhou: ${(post.stderr || 'erro desconhecido').trim()}` };
195
+ }
196
+ }
197
+ }
198
+
199
+ // 4. VERIFY — never write a config pointing at a binary that cannot boot.
200
+ if (setup.verify) {
201
+ const verified = await exec(substituteBin(setup.verify, binPath), { timeoutMs: 60_000 });
202
+ if (!verified.ok) {
203
+ return { mcp, ok: false, ran: freshInstall, error: `verificação falhou: ${(verified.stderr || 'erro desconhecido').trim()}` };
204
+ }
205
+ }
206
+
207
+ // 3. RESOLVE — patch the absolute path; leave args/env/everything else intact.
208
+ const patched = { ...mcp, server: { ...mcp.server, command: binPath } };
209
+ return { mcp: patched, ok: true, ran: freshInstall, error: null };
210
+ }
211
+
212
+ /**
213
+ * Prepare every MCP entry (see prepareMcp), splitting the set into `ready`
214
+ * (safe to write, with any `server.command` patched to an absolute path) and
215
+ * `failed` (setup did not complete — the caller must NOT write these and should
216
+ * surface the error so the user can fix it manually). Never throws: each entry
217
+ * is isolated so one broken tool never blocks the others or the wider install.
218
+ *
219
+ * @param {Object} args
220
+ * @param {Array<Object>} args.mcps - Resolved MCP entries to prepare
221
+ * @param {Function} [args.exec] - Injected runner (command, opts) => {ok,...}
222
+ * @returns {Promise<{ready: Array<Object>, failed: Array<{id: string, error: string}>}>}
223
+ */
224
+ async function prepareMcps({ mcps, exec = defaultExec }) {
225
+ const ready = [];
226
+ const failed = [];
227
+ for (const mcp of mcps || []) {
228
+ const res = await prepareMcp({ mcp, exec });
229
+ if (res.ok) ready.push(res.mcp);
230
+ else failed.push({ id: mcp.id, error: res.error || 'falha desconhecida' });
231
+ }
232
+ return { ready, failed };
233
+ }
234
+
77
235
  /**
78
236
  * Render the `claude mcp add` command for an MCP entry — used to recommend
79
237
  * servers the user chose NOT to write (nothing is lost; they can add later).
@@ -134,4 +292,14 @@ async function writeMcpConfig({ projectDir, mcps, trackFile = () => {} }) {
134
292
  return { added, skipped, file };
135
293
  }
136
294
 
137
- module.exports = { resolveMcps, toServerConfig, renderAddCommand, writeMcpConfig };
295
+ module.exports = {
296
+ resolveMcps,
297
+ toServerConfig,
298
+ renderAddCommand,
299
+ writeMcpConfig,
300
+ prepareMcp,
301
+ prepareMcps,
302
+ resolveBinPath,
303
+ shellQuote,
304
+ substituteBin,
305
+ };