wizz-method 1.3.0 → 1.3.1
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.
|
|
4
|
+
"version": "1.3.1",
|
|
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",
|
package/skills-registry.yaml
CHANGED
|
@@ -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
|
|
@@ -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:
|
|
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 = {
|
|
295
|
+
module.exports = {
|
|
296
|
+
resolveMcps,
|
|
297
|
+
toServerConfig,
|
|
298
|
+
renderAddCommand,
|
|
299
|
+
writeMcpConfig,
|
|
300
|
+
prepareMcp,
|
|
301
|
+
prepareMcps,
|
|
302
|
+
resolveBinPath,
|
|
303
|
+
shellQuote,
|
|
304
|
+
substituteBin,
|
|
305
|
+
};
|