wizz-method 1.6.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 +1 -1
- package/src/skills-lib/motion-3d-director/references/motion-paths.md +3 -1
- package/src/skills-lib/premium-landing-ui-researcher/SKILL.md +2 -2
- package/src/skills-lib/premium-landing-ui-researcher/references/component-sources.md +26 -0
- package/src/skills-lib/premium-landing-ui-researcher/references/source-first-protocol.md +3 -0
- package/src/skills-lib/premium-landing-ui-researcher/references/source-links.md +6 -0
- package/src/skills-lib/premium-landing-ui-researcher/references/stack-and-visual-direction.md +3 -1
- package/tools/installer/modules/env-vars.js +174 -16
- package/tools/installer/modules/mcp-config.js +44 -0
- package/tools/installer/ui.js +14 -2
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",
|
|
@@ -27,7 +27,7 @@ Usar quando precisa de **interação real**.
|
|
|
27
27
|
- Drei;
|
|
28
28
|
- GLSL / shaders;
|
|
29
29
|
- Framer Motion ou Motion para integração com scroll;
|
|
30
|
-
- GSAP
|
|
30
|
+
- GSAP (ScrollTrigger) quando o scroll orquestra a cena — 100% gratuito desde a v3.13, todos os plugins incluídos.
|
|
31
31
|
|
|
32
32
|
**Exemplos:** objeto 3D que segue o cursor; esfera de partículas mouse-reactive; câmera que avança no scroll; wireframe terrain em tempo real; produto 3D configurável; background WebGL interativo.
|
|
33
33
|
|
|
@@ -107,6 +107,8 @@ Usar quando 3D é exagero ou não aumenta conversão.
|
|
|
107
107
|
|
|
108
108
|
**Inclui:** text reveal; parallax; masked typography; image reveal; hover cards; SVG motion; scroll storytelling; magnetic buttons; animated mockups; cinematic section transitions; gradient background animado; background noise; reveal por seção; staggered animations.
|
|
109
109
|
|
|
110
|
+
**Stack recomendada:** Framer Motion/Motion para UI React declarativa; GSAP + ScrollTrigger/SplitText para scroll storytelling, pinning e text reveal avançado (gratuito desde a v3.13); anime.js v4 para stagger, SVG motion e microinterações imperativas leves (~10kb core, MIT).
|
|
111
|
+
|
|
110
112
|
**Usar quando:**
|
|
111
113
|
|
|
112
114
|
- o projeto é Advanced Site;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: premium-landing-ui-researcher
|
|
3
|
-
description: Pesquisar animações, componentes, referências visuais e padrões de conversão para criar landing pages premium em React, Next.js, Tailwind, shadcn/ui, Framer Motion, Three.js e React Three Fiber. Use esta skill quando o usuário pedir para analisar um projeto existente, classificar o nível de complexidade do site (básico, intermediário, avançado, 3D high-end com modo Signature), escolher componentes ou animações adequados, melhorar uma UI genérica, criar uma landing page completa, transformar uma oferta em página estratégica, projetar também um dashboard SaaS junto da landing, criar um site de autoridade/portfolio para agência, estúdio, consultoria, marca pessoal ou lead passivo, criar case studies e selected work editoriais, criar uma experiência 3D cinematográfica para marcas premium, buscar referências em React Bits, Cult UI, Componentry, Animmaster Lib, Ali Imam, Watermelon UI, StyleUI, Refero, Impeccable, Taste Skill, Design Motion Principles, MotionSites, Vibe Code Components, Landing Love, Godly, Design Spells, Mobbin, ScreensDesign, DesignVault, Spline, Unicorn Studio, Skiper UI, 21st.dev (Magic MCP ou 21st CLI), ou implementar uma experiência visual sofisticada orientada à conversão ou à autoridade.
|
|
3
|
+
description: Pesquisar animações, componentes, referências visuais e padrões de conversão para criar landing pages premium em React, Next.js, Tailwind, shadcn/ui, Framer Motion, Three.js e React Three Fiber. Use esta skill quando o usuário pedir para analisar um projeto existente, classificar o nível de complexidade do site (básico, intermediário, avançado, 3D high-end com modo Signature), escolher componentes ou animações adequados, melhorar uma UI genérica, criar uma landing page completa, transformar uma oferta em página estratégica, projetar também um dashboard SaaS junto da landing, criar um site de autoridade/portfolio para agência, estúdio, consultoria, marca pessoal ou lead passivo, criar case studies e selected work editoriais, criar uma experiência 3D cinematográfica para marcas premium, buscar referências em React Bits, Cult UI, Componentry, Animmaster Lib, Ali Imam, Watermelon UI, StyleUI, Bklit UI, Refero, Impeccable, Taste Skill, Design Motion Principles, MotionSites, Vibe Code Components, Landing Love, Godly, Design Spells, Mobbin, ScreensDesign, DesignVault, Spline, Unicorn Studio, Skiper UI, 21st.dev (Magic MCP ou 21st CLI), escolher entre GSAP, anime.js e Framer Motion para animações e scroll storytelling, ou implementar uma experiência visual sofisticada orientada à conversão ou à autoridade.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Premium Landing UI Researcher
|
|
@@ -49,7 +49,7 @@ Classificar o projeto em um dos 5 níveis (regras, motion permitido/proibido e b
|
|
|
49
49
|
| Processo obrigatório de 12 passos e checkpoint de honestidade | [mandatory-process](references/mandatory-process.md) |
|
|
50
50
|
| Source-First Mandate completo + protocolo em 5 fases (inventário, repos em cache, Magic MCP, 21st CLI, confirmação; 21st = fonte paga complementar) | [source-first-protocol](references/source-first-protocol.md) |
|
|
51
51
|
| Audit Protocol: Pass 1 Taste, Pass 2 Impeccable, Pass 3 Cross-check, Pass 4 A11y/Perf | [audit-protocol](references/audit-protocol.md) |
|
|
52
|
-
| Fontes de componentes (React Bits, Cult UI, Ali Imam, Watermelon, StyleUI, Skiper UI, 21st.dev + 21st CLI), fontes de referência e inspiração visual, Clone Policy, Paid Source Policy | [component-sources](references/component-sources.md) |
|
|
52
|
+
| Fontes de componentes (React Bits, Cult UI, Ali Imam, Watermelon, StyleUI, Skiper UI, Bklit UI, 21st.dev + 21st CLI), animation engines (GSAP, anime.js), fontes de referência e inspiração visual, Clone Policy, Paid Source Policy | [component-sources](references/component-sources.md) |
|
|
53
53
|
| Stack default, direção visual, paletas, tipografia e regras de seleção de animação | [stack-and-visual-direction](references/stack-and-visual-direction.md) |
|
|
54
54
|
| Estrutura obrigatória da landing, case studies/portfolio, copywriting, conversão e CTA externo/WhatsApp | [landing-page-strategy](references/landing-page-strategy.md) |
|
|
55
55
|
| Prompts base (landing completa e hero 3D com scroll) | [prompt-templates](references/prompt-templates.md) |
|
|
@@ -131,6 +131,14 @@ Usar para: templates, landing page layouts, páginas prontas, seções instaláv
|
|
|
131
131
|
- Mesmo gate de custo do 21st.dev: só com aprovação do usuário, quando as gratuitas não cobrirem
|
|
132
132
|
- O que o usuário já comprou/baixou dela vale como recurso local (inspecionar em `modelos lp/`)
|
|
133
133
|
|
|
134
|
+
### Bklit UI
|
|
135
|
+
|
|
136
|
+
- Site: https://bklit.com/ · GitHub: https://github.com/bklit/bklit-ui (MIT, gratuito, open source)
|
|
137
|
+
- Charts e componentes utilitários React/TypeScript/Tailwind via registry shadcn (~15 tipos: área, barras, linha, pizza, radar, Sankey, candlestick, choropleth, gauge)
|
|
138
|
+
- **Não clonar**: instalar direto pelo registry — `npx shadcn@latest add @bklit/<chart>` (ex: `@bklit/line-chart`)
|
|
139
|
+
- Fonte prioritária para: dashboards SaaS, data viz, seções de métricas/estatísticas na landing, gráficos animados premium (complementa/substitui Recharts cru)
|
|
140
|
+
- O Bklit Studio (playground) é proprietário; os componentes em si são MIT
|
|
141
|
+
|
|
134
142
|
### Skiper UI
|
|
135
143
|
|
|
136
144
|
Skiper UI **não deve ser clonado**.
|
|
@@ -147,6 +155,24 @@ Usar para: uncommon shadcn components, efeitos visuais diferenciados, seções p
|
|
|
147
155
|
|
|
148
156
|
Evitar: depender de componentes premium sem autorização; assumir acesso pago.
|
|
149
157
|
|
|
158
|
+
## Animation Engine Libraries (libs de código, via npm)
|
|
159
|
+
|
|
160
|
+
Motores de animação instaláveis como dependência npm normal (não são registries de componentes; não precisam de clone — os repos podem ser inspecionados para exemplos/demos).
|
|
161
|
+
|
|
162
|
+
### GSAP
|
|
163
|
+
|
|
164
|
+
- GitHub: https://github.com/greensock/GSAP.git · Docs: https://gsap.com/docs/
|
|
165
|
+
- **100% gratuito desde a v3.13** (aquisição pela Webflow, 2025), incluindo todos os plugins antes pagos: ScrollTrigger, ScrollSmoother, SplitText, MorphSVG, DrawSVG, etc. Instalar: `npm i gsap` (+ `@gsap/react` para o hook `useGSAP`)
|
|
166
|
+
- Usar para: scroll storytelling e animações scroll-driven complexas (ScrollTrigger), timelines longas e sequenciadas, text reveal por caractere/palavra (SplitText), morph de SVG, animações fora do ciclo do React, sites high-end estilo Awwwards
|
|
167
|
+
- Preferir GSAP sobre Framer Motion quando: a animação é orquestrada por scroll com pinning/scrub, a timeline tem muitos passos encadeados, ou o efeito anima elementos fora de componentes React
|
|
168
|
+
|
|
169
|
+
### anime.js
|
|
170
|
+
|
|
171
|
+
- GitHub: https://github.com/juliangarnier/anime.git · Docs: https://animejs.com/ (MIT, gratuito)
|
|
172
|
+
- v4: API modular e tree-shakeable (~10kb core), timelines, stagger, springs, draggable, scroll observer, SVG (draw/morph/motion path), WAAPI. Instalar: `npm i animejs`
|
|
173
|
+
- Usar para: microinterações e animações leves quando GSAP seria peso demais, stagger/grid animations, animação de SVG, contadores/números animados, projetos sem React ou fora do ecossistema Motion
|
|
174
|
+
- Regra de escolha: Framer Motion/Motion para UI React declarativa → anime.js para efeitos leves imperativos → GSAP para scroll orquestrado e timelines cinematográficas
|
|
175
|
+
|
|
150
176
|
## Authorized Reference Inspection Sources
|
|
151
177
|
|
|
152
178
|
Estas fontes podem ser inspecionadas ou consultadas para princípios, critérios de qualidade, motion e taste, mas **não devem ser tratadas como dependências de projeto**.
|
|
@@ -53,6 +53,9 @@ Para cada efeito visual ainda não resolvido pela fase 1, inspecionar o **cache
|
|
|
53
53
|
| SaaS components / dashboards / product UI blocks | `~/.claude/design-sources/watermelon/` | `git -C ~/.claude/design-sources/watermelon pull` |
|
|
54
54
|
| Templates / landing layouts prontos / páginas base | `~/.claude/design-sources/styleui/` | `git -C ~/.claude/design-sources/styleui pull` |
|
|
55
55
|
| Componentes shadcn premium específicos | **Skiper UI** | `npx shadcn add @skiper-ui/skiperXX` (sem clone, instala direto) |
|
|
56
|
+
| Charts / data viz / seções de métricas / gráficos de dashboard | **Bklit UI** | `npx shadcn@latest add @bklit/<chart>` (sem clone, registry shadcn) |
|
|
57
|
+
| Scroll storytelling / pinning / timelines cinematográficas / SplitText | **GSAP** (lib npm, 100% gratuita desde a v3.13) | `npm i gsap @gsap/react` (sem clone) |
|
|
58
|
+
| Microinterações imperativas leves / stagger / SVG motion / contadores | **anime.js v4** (lib npm, MIT) | `npm i animejs` (sem clone) |
|
|
56
59
|
|
|
57
60
|
**Fluxo de uso do cache:**
|
|
58
61
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- Watermelon UI: https://github.com/WatermelonCorp/watermellon-registry.git
|
|
11
11
|
- StyleUI: https://github.com/heyfabrika/styleui.git
|
|
12
12
|
- Skiper UI command: `npx shadcn add @skiper-ui/skiper40`
|
|
13
|
+
- Bklit UI (charts shadcn, MIT, gratuito): https://github.com/bklit/bklit-ui — `npx shadcn@latest add @bklit/<chart>`
|
|
13
14
|
- Componentry (React animado, gratuito/open source, Vercel OSS): https://componentry.dev/
|
|
14
15
|
- Componentry MCP (via shadcn MCP + registry `@componentry`): https://componentry.dev/docs/mcp
|
|
15
16
|
- Animmaster Lib (300 componentes animados, PAGO): https://animmasterlib.dev/
|
|
@@ -22,6 +23,11 @@
|
|
|
22
23
|
- 21st instalação: `21st add <user>/<slug>` ou `npx shadcn@latest add https://21st.dev/r/<user>/<slug>`
|
|
23
24
|
- 21st API keys (CI/headless, env API_KEY_21ST): https://21st.dev/settings/api-keys
|
|
24
25
|
|
|
26
|
+
## Animation Engine Libraries (npm)
|
|
27
|
+
|
|
28
|
+
- GSAP (100% gratuito desde a v3.13, todos os plugins incluídos): https://github.com/greensock/GSAP.git — `npm i gsap @gsap/react`
|
|
29
|
+
- anime.js v4 (MIT, ~10kb core): https://github.com/juliangarnier/anime.git — `npm i animejs`
|
|
30
|
+
|
|
25
31
|
## Reference Sources (taste, qualidade visual, motion)
|
|
26
32
|
|
|
27
33
|
- Impeccable: https://github.com/pbakaus/impeccable.git
|
package/src/skills-lib/premium-landing-ui-researcher/references/stack-and-visual-direction.md
CHANGED
|
@@ -8,11 +8,13 @@ Se o usuário não especificar stack, assumir:
|
|
|
8
8
|
- Tailwind CSS;
|
|
9
9
|
- shadcn/ui;
|
|
10
10
|
- Framer Motion ou Motion;
|
|
11
|
+
- GSAP (com ScrollTrigger/SplitText, 100% gratuito desde a v3.13) para scroll storytelling e timelines cinematográficas;
|
|
12
|
+
- anime.js v4 para microinterações imperativas leves e SVG motion;
|
|
11
13
|
- React Bits;
|
|
12
14
|
- React Three Fiber para projetos 3D;
|
|
13
15
|
- Three.js para WebGL avançado;
|
|
14
16
|
- lucide-react para ícones;
|
|
15
|
-
- Recharts para dashboards SaaS.
|
|
17
|
+
- Recharts ou Bklit UI (charts shadcn) para dashboards SaaS.
|
|
16
18
|
|
|
17
19
|
Se o projeto for simples, reduzir stack e evitar complexidade desnecessária.
|
|
18
20
|
|
|
@@ -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);
|