wizz-method 1.7.0 → 1.8.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.
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.
|
|
4
|
+
"version": "1.8.0",
|
|
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",
|
|
@@ -29,17 +29,36 @@
|
|
|
29
29
|
// placeholder stays in `.mcp.json`, and the summary explains how to
|
|
30
30
|
// configure it later.
|
|
31
31
|
//
|
|
32
|
+
// GLOBAL KEY STORE (`~/.claude/wizz-env.json`): a key the user already typed
|
|
33
|
+
// in ANY project is reused silently in every new install — the store is read
|
|
34
|
+
// by the installer only (C7 still holds: the runtime never reads it), and a
|
|
35
|
+
// hit is copied into the new project's `settings.local.json`. A key typed at
|
|
36
|
+
// the prompt is saved to the store too, so it is only ever asked once.
|
|
37
|
+
//
|
|
32
38
|
// API (decomposed per E3 so each piece is unit-testable without a TTY):
|
|
33
39
|
// extractEnvPlaceholders(mcps) — pure
|
|
34
40
|
// resolveEnvVars(vars, opts) — I/O read (providers + prompt)
|
|
35
41
|
// persistEnvValues(toPersist, opts) — I/O write (settings.local.json)
|
|
36
42
|
// persistProjectEnv(projectDir, envRecord) — the actual writer, reusable
|
|
43
|
+
// persistGlobalEnv(storePath, envRecord) — writer do store global
|
|
37
44
|
// promptMissingEnvVars(mcps, opts) — thin orchestrator of the above
|
|
38
45
|
|
|
39
46
|
const path = require('node:path');
|
|
47
|
+
const os = require('node:os');
|
|
40
48
|
const fs = require('../fs-native');
|
|
41
49
|
const prompts = require('../prompts');
|
|
42
50
|
|
|
51
|
+
// Global key store, read by the INSTALLER only (never by the Claude Code
|
|
52
|
+
// runtime — C7 still holds). A value found here is copied into the project's
|
|
53
|
+
// `.claude/settings.local.json` at install time, which IS what reaches the
|
|
54
|
+
// MCP subprocess. This is what makes a key typed once in project A resolve
|
|
55
|
+
// silently in projects B, C, D... without ever living in the global
|
|
56
|
+
// settings.json `env` (which would expose it to every session of every
|
|
57
|
+
// project — the exact pattern the 360° audit flagged as a security critical).
|
|
58
|
+
function defaultGlobalEnvPath() {
|
|
59
|
+
return path.join(os.homedir(), '.claude', 'wizz-env.json');
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
// Deliberately POSIX-strict (uppercase + underscore only): this both matches
|
|
44
63
|
// standard env var naming and doubles as a defensive filter against false
|
|
45
64
|
// positives like `{bin}` (no `$` prefix at all, so it never matches) or a
|
|
@@ -168,12 +187,51 @@ function createDotenvFileProvider(dotenvPath) {
|
|
|
168
187
|
}
|
|
169
188
|
|
|
170
189
|
/**
|
|
171
|
-
*
|
|
190
|
+
* The global key store (`~/.claude/wizz-env.json`, flat `{ "VAR": "value" }`
|
|
191
|
+
* map, chmod 600) as a provider. Read-only here — `persistGlobalEnv` is the
|
|
192
|
+
* writer. Marked `persistToProject: true`: unlike `process.env`, a value from
|
|
193
|
+
* this store is NOT in the runtime's environment, so the resolver must copy
|
|
194
|
+
* it into the project's `settings.local.json` for it to actually reach the
|
|
195
|
+
* MCP subprocess (C7).
|
|
196
|
+
* @param {string} storePath - Absolute path to the global store file
|
|
197
|
+
* @returns {{name: string, persistToProject: boolean, available: () => Promise<boolean>, get: (name: string) => Promise<string|undefined>}}
|
|
198
|
+
*/
|
|
199
|
+
function createGlobalStoreProvider(storePath) {
|
|
200
|
+
let cache = null;
|
|
201
|
+
|
|
202
|
+
async function load() {
|
|
203
|
+
if (cache) return cache;
|
|
204
|
+
cache = {};
|
|
205
|
+
if (!storePath || !(await fs.pathExists(storePath))) return cache;
|
|
206
|
+
try {
|
|
207
|
+
const parsed = JSON.parse(await fs.readFile(storePath, 'utf8'));
|
|
208
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) cache = parsed;
|
|
209
|
+
} catch {
|
|
210
|
+
// Malformed/unreadable store — treat as empty, never throw.
|
|
211
|
+
cache = {};
|
|
212
|
+
}
|
|
213
|
+
return cache;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
name: 'global-store',
|
|
218
|
+
persistToProject: true,
|
|
219
|
+
available: async () => !!storePath && (await fs.pathExists(storePath)),
|
|
220
|
+
get: async (name) => {
|
|
221
|
+
const value = (await load())[name];
|
|
222
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Try each provider in order, returning the first non-empty value found and
|
|
229
|
+
* the provider that had it (so the caller can honor `persistToProject`).
|
|
172
230
|
* A provider throwing or being unavailable is skipped, never fatal — one
|
|
173
231
|
* broken provider must not block the chain (e.g. an unreadable `.env`).
|
|
174
232
|
* @param {string} name - Var name to look up
|
|
175
233
|
* @param {Array<Object>} providers
|
|
176
|
-
* @returns {Promise<string|undefined>}
|
|
234
|
+
* @returns {Promise<{value: string, provider: Object}|undefined>}
|
|
177
235
|
*/
|
|
178
236
|
async function findInProviders(name, providers) {
|
|
179
237
|
for (const provider of providers || []) {
|
|
@@ -182,7 +240,7 @@ async function findInProviders(name, providers) {
|
|
|
182
240
|
const isAvailable = typeof provider.available === 'function' ? await provider.available() : true;
|
|
183
241
|
if (!isAvailable) continue;
|
|
184
242
|
const value = await provider.get(name);
|
|
185
|
-
if (value !== undefined && value !== null && value !== '') return value;
|
|
243
|
+
if (value !== undefined && value !== null && value !== '') return { value, provider };
|
|
186
244
|
} catch {
|
|
187
245
|
continue;
|
|
188
246
|
}
|
|
@@ -221,7 +279,14 @@ function createDefaultPrompter() {
|
|
|
221
279
|
* @param {boolean} [opts.interactive=false] - Whether to prompt for missing vars
|
|
222
280
|
* @param {Array<Object>} [opts.providers] - Provider chain, tried in order
|
|
223
281
|
* @param {(entry) => Promise<string|undefined>} [opts.prompter] - Injectable prompt fn
|
|
224
|
-
* @returns {Promise<{filled: Array, skipped: Array, existing: Array,
|
|
282
|
+
* @returns {Promise<{filled: Array, skipped: Array, existing: Array, imported: Array,
|
|
283
|
+
* toPersist: Record<string,string>, toPersistGlobal: Record<string,string>}>}
|
|
284
|
+
* `existing` = found in a runtime-visible source (process.env/.env), nothing
|
|
285
|
+
* to write. `imported` = found in a `persistToProject` provider (the global
|
|
286
|
+
* store): resolved without prompting, but must be written to the project's
|
|
287
|
+
* settings.local.json (included in `toPersist`). `toPersistGlobal` = the
|
|
288
|
+
* subset of typed answers that should ALSO be saved to the global store so
|
|
289
|
+
* the next project never asks.
|
|
225
290
|
*/
|
|
226
291
|
async function resolveEnvVars(vars, opts = {}) {
|
|
227
292
|
const { interactive = false, providers = [createProcessEnvProvider()], prompter = null } = opts;
|
|
@@ -229,7 +294,9 @@ async function resolveEnvVars(vars, opts = {}) {
|
|
|
229
294
|
const filled = [];
|
|
230
295
|
const skipped = [];
|
|
231
296
|
const existing = [];
|
|
297
|
+
const imported = [];
|
|
232
298
|
const toPersist = {};
|
|
299
|
+
const toPersistGlobal = {};
|
|
233
300
|
|
|
234
301
|
for (const entry of vars || []) {
|
|
235
302
|
// A default is resolved by the Claude Code runtime itself; asking would
|
|
@@ -242,7 +309,12 @@ async function resolveEnvVars(vars, opts = {}) {
|
|
|
242
309
|
|
|
243
310
|
const found = await findInProviders(entry.name, providers);
|
|
244
311
|
if (found !== undefined) {
|
|
245
|
-
|
|
312
|
+
if (found.provider && found.provider.persistToProject) {
|
|
313
|
+
imported.push(entry);
|
|
314
|
+
toPersist[entry.name] = found.value;
|
|
315
|
+
} else {
|
|
316
|
+
existing.push(entry);
|
|
317
|
+
}
|
|
246
318
|
continue;
|
|
247
319
|
}
|
|
248
320
|
|
|
@@ -259,9 +331,10 @@ async function resolveEnvVars(vars, opts = {}) {
|
|
|
259
331
|
|
|
260
332
|
filled.push(entry);
|
|
261
333
|
toPersist[entry.name] = answer;
|
|
334
|
+
toPersistGlobal[entry.name] = answer;
|
|
262
335
|
}
|
|
263
336
|
|
|
264
|
-
return { filled, skipped, existing, toPersist };
|
|
337
|
+
return { filled, skipped, existing, imported, toPersist, toPersistGlobal };
|
|
265
338
|
}
|
|
266
339
|
|
|
267
340
|
/**
|
|
@@ -352,23 +425,80 @@ async function persistEnvValues(toPersist, opts = {}) {
|
|
|
352
425
|
return persistProjectEnv(projectDir, toPersist);
|
|
353
426
|
}
|
|
354
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Merge `envRecord` into the flat global store (`~/.claude/wizz-env.json`).
|
|
430
|
+
* Additive only — a key already present is never overwritten (same rule as
|
|
431
|
+
* `persistProjectEnv`; a prompt only ever fires for a var no provider had,
|
|
432
|
+
* so an overwrite here would always mean clobbering something newer). File
|
|
433
|
+
* is chmod 600 after any write that changed it, same best-effort semantics
|
|
434
|
+
* as the project writer.
|
|
435
|
+
*
|
|
436
|
+
* @param {string} storePath - Absolute path to the global store file
|
|
437
|
+
* @param {Record<string,string>} envRecord - Vars to merge
|
|
438
|
+
* @returns {Promise<string|null>} Path written/merged, or null when empty
|
|
439
|
+
*/
|
|
440
|
+
async function persistGlobalEnv(storePath, envRecord) {
|
|
441
|
+
if (!storePath || !envRecord || Object.keys(envRecord).length === 0) return null;
|
|
442
|
+
|
|
443
|
+
await fs.ensureDir(path.dirname(storePath));
|
|
444
|
+
|
|
445
|
+
let store = {};
|
|
446
|
+
if (await fs.pathExists(storePath)) {
|
|
447
|
+
try {
|
|
448
|
+
const parsed = JSON.parse(await fs.readFile(storePath, 'utf8'));
|
|
449
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) store = parsed;
|
|
450
|
+
} catch {
|
|
451
|
+
// A corrupt store must never eat keys the user just typed: keep the
|
|
452
|
+
// broken file aside and start a fresh store with the new values.
|
|
453
|
+
try {
|
|
454
|
+
await fs.rename(storePath, `${storePath}.bak`);
|
|
455
|
+
} catch {
|
|
456
|
+
// Even the rename failing must not block the install.
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let changed = false;
|
|
462
|
+
for (const [key, value] of Object.entries(envRecord)) {
|
|
463
|
+
if (Object.prototype.hasOwnProperty.call(store, key)) continue;
|
|
464
|
+
store[key] = value;
|
|
465
|
+
changed = true;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (changed) {
|
|
469
|
+
await fs.writeJson(storePath, store, { spaces: 2 });
|
|
470
|
+
if (process.platform !== 'win32') {
|
|
471
|
+
try {
|
|
472
|
+
await fs.chmod(storePath, 0o600);
|
|
473
|
+
} catch {
|
|
474
|
+
// Best-effort, same as persistProjectEnv.
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return storePath;
|
|
480
|
+
}
|
|
481
|
+
|
|
355
482
|
/**
|
|
356
483
|
* Render the DX summary (E6) — the highest-value part of this feature: turn
|
|
357
484
|
* a skipped var from a dead end into a 30-second fix. One line per var,
|
|
358
485
|
* grouped by outcome. Never includes a raw secret value, only names/status.
|
|
359
|
-
* @param {{filled: Array, skipped: Array, existing: Array}} resolved
|
|
486
|
+
* @param {{filled: Array, skipped: Array, existing: Array, imported: Array}} resolved
|
|
360
487
|
* @returns {string|null} Formatted block, or null when there is nothing to say
|
|
361
488
|
*/
|
|
362
|
-
function formatSummary({ filled, skipped, existing }) {
|
|
363
|
-
const total = (filled?.length || 0) + (skipped?.length || 0) + (existing?.length || 0);
|
|
489
|
+
function formatSummary({ filled, skipped, existing, imported }) {
|
|
490
|
+
const total = (filled?.length || 0) + (skipped?.length || 0) + (existing?.length || 0) + (imported?.length || 0);
|
|
364
491
|
if (total === 0) return null;
|
|
365
492
|
|
|
366
493
|
const lines = [];
|
|
367
494
|
for (const entry of existing || []) {
|
|
368
495
|
lines.push(` ✓ ${entry.name.padEnd(28)} já existia no ambiente`);
|
|
369
496
|
}
|
|
497
|
+
for (const entry of imported || []) {
|
|
498
|
+
lines.push(` ✓ ${entry.name.padEnd(28)} importada do global (~/.claude/wizz-env.json)`);
|
|
499
|
+
}
|
|
370
500
|
for (const entry of filled || []) {
|
|
371
|
-
lines.push(` ✓ ${entry.name.padEnd(28)} configurada agora (
|
|
501
|
+
lines.push(` ✓ ${entry.name.padEnd(28)} configurada agora (+ salva no global p/ próximos projetos)`);
|
|
372
502
|
}
|
|
373
503
|
for (const entry of skipped || []) {
|
|
374
504
|
if (entry.hasDefault) {
|
|
@@ -403,18 +533,26 @@ function formatSummary({ filled, skipped, existing }) {
|
|
|
403
533
|
* @param {string} opts.projectDir - Project root (for persistence + the
|
|
404
534
|
* default `.env` provider)
|
|
405
535
|
* @param {boolean} [opts.interactive=false]
|
|
406
|
-
* @param {Array<Object>} [opts.providers] - Defaults to `[processEnv,
|
|
536
|
+
* @param {Array<Object>} [opts.providers] - Defaults to `[processEnv,
|
|
537
|
+
* dotenvFile(<projectDir>/.env), globalStore(~/.claude/wizz-env.json)]`
|
|
538
|
+
* @param {string} [opts.globalEnvPath] - Global store path (default
|
|
539
|
+
* `~/.claude/wizz-env.json`); used for both the default provider chain and
|
|
540
|
+
* the save-on-prompt write. Injectable so tests never touch the real home.
|
|
407
541
|
* @param {(entry) => Promise<string|undefined>} [opts.prompter] - Defaults to
|
|
408
542
|
* the masked `password()` prompter
|
|
409
|
-
* @returns {Promise<{filled: Array, skipped: Array, existing: Array, envFile: string|null}>}
|
|
543
|
+
* @returns {Promise<{filled: Array, skipped: Array, existing: Array, imported: Array, envFile: string|null}>}
|
|
410
544
|
*/
|
|
411
545
|
async function promptMissingEnvVars(mcps, opts = {}) {
|
|
412
|
-
const { projectDir, interactive = false, providers, prompter } = opts;
|
|
546
|
+
const { projectDir, interactive = false, providers, prompter, globalEnvPath = defaultGlobalEnvPath() } = opts;
|
|
413
547
|
|
|
414
548
|
const vars = extractEnvPlaceholders(mcps);
|
|
415
|
-
if (vars.length === 0) return { filled: [], skipped: [], existing: [], envFile: null };
|
|
549
|
+
if (vars.length === 0) return { filled: [], skipped: [], existing: [], imported: [], envFile: null };
|
|
416
550
|
|
|
417
|
-
const resolvedProviders = providers || [
|
|
551
|
+
const resolvedProviders = providers || [
|
|
552
|
+
createProcessEnvProvider(),
|
|
553
|
+
createDotenvFileProvider(path.join(projectDir, '.env')),
|
|
554
|
+
createGlobalStoreProvider(globalEnvPath),
|
|
555
|
+
];
|
|
418
556
|
const resolvedPrompter = interactive ? prompter || createDefaultPrompter() : null;
|
|
419
557
|
|
|
420
558
|
const resolved = await resolveEnvVars(vars, {
|
|
@@ -428,10 +566,27 @@ async function promptMissingEnvVars(mcps, opts = {}) {
|
|
|
428
566
|
envFile = await persistEnvValues(resolved.toPersist, { projectDir, target: 'settings-local' });
|
|
429
567
|
}
|
|
430
568
|
|
|
569
|
+
// Typed answers also go to the global store so the NEXT project resolves
|
|
570
|
+
// them silently. Failure here must never block the install — the project
|
|
571
|
+
// write above already succeeded, which is what this install needs.
|
|
572
|
+
if (Object.keys(resolved.toPersistGlobal).length > 0) {
|
|
573
|
+
try {
|
|
574
|
+
await persistGlobalEnv(globalEnvPath, resolved.toPersistGlobal);
|
|
575
|
+
} catch {
|
|
576
|
+
// Global save is a convenience for future installs, never a blocker.
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
431
580
|
const summary = formatSummary(resolved);
|
|
432
581
|
if (summary) await prompts.log.info(summary);
|
|
433
582
|
|
|
434
|
-
return {
|
|
583
|
+
return {
|
|
584
|
+
filled: resolved.filled,
|
|
585
|
+
skipped: resolved.skipped,
|
|
586
|
+
existing: resolved.existing,
|
|
587
|
+
imported: resolved.imported,
|
|
588
|
+
envFile,
|
|
589
|
+
};
|
|
435
590
|
}
|
|
436
591
|
|
|
437
592
|
module.exports = {
|
|
@@ -439,8 +594,11 @@ module.exports = {
|
|
|
439
594
|
resolveEnvVars,
|
|
440
595
|
persistEnvValues,
|
|
441
596
|
persistProjectEnv,
|
|
597
|
+
persistGlobalEnv,
|
|
442
598
|
promptMissingEnvVars,
|
|
443
599
|
createProcessEnvProvider,
|
|
444
600
|
createDotenvFileProvider,
|
|
601
|
+
createGlobalStoreProvider,
|
|
602
|
+
defaultGlobalEnvPath,
|
|
445
603
|
formatSummary,
|
|
446
604
|
};
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// a `claude mcp add` command renderer for the recommend path.
|
|
20
20
|
|
|
21
21
|
const path = require('node:path');
|
|
22
|
+
const os = require('node:os');
|
|
22
23
|
const crypto = require('node:crypto');
|
|
23
24
|
const fs = require('../fs-native');
|
|
24
25
|
const { defaultExec } = require('./cli-config');
|
|
@@ -325,6 +326,48 @@ async function partitionAlreadyConfigured({ projectDir, mcps }) {
|
|
|
325
326
|
return { toPrepare, alreadyConfigured };
|
|
326
327
|
}
|
|
327
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Split resolved MCP entries into those still worth offering/installing in
|
|
331
|
+
* the project and those the user already configured GLOBALLY (user scope, the
|
|
332
|
+
* `mcpServers` key of `~/.claude.json`). A server configured there is live in
|
|
333
|
+
* every project already — usually with the real key embedded — so writing the
|
|
334
|
+
* registry's `${VAR}`-placeholder copy into the project `.mcp.json` would at
|
|
335
|
+
* best duplicate it and at worst shadow a working global config with a broken
|
|
336
|
+
* placeholder one, then prompt the user for a key they already provided.
|
|
337
|
+
*
|
|
338
|
+
* Read failures (missing/malformed `~/.claude.json`) fall back to "nothing is
|
|
339
|
+
* global", the safe default: worst case the user sees the old behavior.
|
|
340
|
+
*
|
|
341
|
+
* @param {Object} args
|
|
342
|
+
* @param {Array<{id: string}>} args.mcps - Resolved MCP entries to partition
|
|
343
|
+
* @param {string} [args.claudeJsonPath] - Override of `~/.claude.json` (tests)
|
|
344
|
+
* @returns {Promise<{toInstall: Array<Object>, globallyConfigured: string[]}>}
|
|
345
|
+
*/
|
|
346
|
+
async function partitionGloballyConfigured({ mcps, claudeJsonPath }) {
|
|
347
|
+
if (!mcps || mcps.length === 0) return { toInstall: [], globallyConfigured: [] };
|
|
348
|
+
|
|
349
|
+
const file = claudeJsonPath || path.join(os.homedir(), '.claude.json');
|
|
350
|
+
let globalIds = new Set();
|
|
351
|
+
if (await fs.pathExists(file)) {
|
|
352
|
+
try {
|
|
353
|
+
const config = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
354
|
+
const servers =
|
|
355
|
+
config && typeof config === 'object' && config.mcpServers && !Array.isArray(config.mcpServers) ? config.mcpServers : null;
|
|
356
|
+
if (servers) globalIds = new Set(Object.keys(servers));
|
|
357
|
+
} catch {
|
|
358
|
+
globalIds = new Set();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const toInstall = [];
|
|
363
|
+
const globallyConfigured = [];
|
|
364
|
+
for (const mcp of mcps) {
|
|
365
|
+
if (mcp && mcp.id && globalIds.has(mcp.id)) globallyConfigured.push(mcp.id);
|
|
366
|
+
else toInstall.push(mcp);
|
|
367
|
+
}
|
|
368
|
+
return { toInstall, globallyConfigured };
|
|
369
|
+
}
|
|
370
|
+
|
|
328
371
|
/**
|
|
329
372
|
* Merge the chosen MCP entries into `<projectDir>/.mcp.json`, additively.
|
|
330
373
|
* Reads any existing file (preserving unknown keys and existing servers),
|
|
@@ -442,6 +485,7 @@ module.exports = {
|
|
|
442
485
|
prepareMcp,
|
|
443
486
|
prepareMcps,
|
|
444
487
|
partitionAlreadyConfigured,
|
|
488
|
+
partitionGloballyConfigured,
|
|
445
489
|
resolveBinPath,
|
|
446
490
|
shellQuote,
|
|
447
491
|
substituteBin,
|
package/tools/installer/ui.js
CHANGED
|
@@ -17,7 +17,7 @@ const {
|
|
|
17
17
|
bundledTargetWarnings,
|
|
18
18
|
} = require('./modules/channel-plan');
|
|
19
19
|
const channelResolver = require('./modules/channel-resolver');
|
|
20
|
-
const { resolveMcps } = require('./modules/mcp-config');
|
|
20
|
+
const { resolveMcps, partitionGloballyConfigured } = require('./modules/mcp-config');
|
|
21
21
|
const { resolveClis, detectClis } = require('./modules/cli-config');
|
|
22
22
|
const prompts = require('./prompts');
|
|
23
23
|
const { parseSetEntries } = require('./set-overrides');
|
|
@@ -571,9 +571,21 @@ class UI {
|
|
|
571
571
|
if (!selectedModules.includes('bmm')) return { toWrite: [], toRecommend: [] };
|
|
572
572
|
|
|
573
573
|
const registry = this._loadSkillsRegistry();
|
|
574
|
-
|
|
574
|
+
let resolved = resolveMcps(registry, selectedAreas);
|
|
575
575
|
if (resolved.length === 0) return { toWrite: [], toRecommend: [] };
|
|
576
576
|
|
|
577
|
+
// A server the user already configured globally (user scope, `mcpServers`
|
|
578
|
+
// in ~/.claude.json) is live in every project — offering it again would
|
|
579
|
+
// duplicate config and re-prompt for a key that already works. Filter it
|
|
580
|
+
// out of every path below (multiselect, --mcps, --yes) with an info line
|
|
581
|
+
// so nothing disappears silently.
|
|
582
|
+
const { toInstall, globallyConfigured } = await partitionGloballyConfigured({ mcps: resolved });
|
|
583
|
+
if (globallyConfigured.length > 0) {
|
|
584
|
+
await prompts.log.info(`MCPs já configurados no seu Claude global (~/.claude.json), pulados: ${globallyConfigured.join(', ')}`);
|
|
585
|
+
resolved = toInstall;
|
|
586
|
+
if (resolved.length === 0) return { toWrite: [], toRecommend: [] };
|
|
587
|
+
}
|
|
588
|
+
|
|
577
589
|
const byId = new Map(resolved.map((m) => [m.id, m]));
|
|
578
590
|
const split = (writeIds) => {
|
|
579
591
|
const writeSet = new Set(writeIds);
|