wizz-method 2.12.0 → 2.13.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/README.md +2 -1
- package/package.json +1 -1
- package/skills-registry.yaml +20 -25
- package/src/bmm-skills/module.yaml +1 -1
- package/src/modules/wizz/_shared/video-pipeline.md +1 -1
- package/src/modules/wizz/agents/wizz-maestro/SKILL.md +1 -1
- package/src/skills-lib/wizz-router/references/routing-table-flat.md +2 -2
- package/tools/installer/banner.js +80 -29
- package/tools/installer/commands/catalog.js +87 -0
- package/tools/installer/commands/install.js +9 -0
- package/tools/installer/guided/detect-tools.js +53 -0
- package/tools/installer/guided/flow.js +203 -0
- package/tools/installer/guided/labels.js +185 -0
- package/tools/installer/guided/messages.js +132 -0
- package/tools/installer/guided/outro.js +32 -0
- package/tools/installer/guided/profiles.js +95 -0
- package/tools/installer/guided/steps.js +154 -0
- package/tools/installer/install-messages.yaml +3 -3
- package/tools/installer/modules/external-manager.js +4 -0
- package/tools/installer/ui.js +57 -22
- package/wizz-modules.yaml +1 -0
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ The Wizz layer applies to everyone: standard closing (✅/➡️/🎯), token ec
|
|
|
72
72
|
npx wizz-method install
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
The guided install takes four steps (folder, what you'll use it for, your AI tool, your name) and shows what will be installed before it starts. Choose Português on the first screen or pass `--lang pt`; `--advanced` asks the complete question list. Then open your AI tool (Claude Code, Cursor, Codex and others) in the project folder.
|
|
76
76
|
|
|
77
77
|
### 2. Customize the Wizz layer
|
|
78
78
|
|
|
@@ -106,6 +106,7 @@ Without a TTY, the default language is "Português (BR)". Use `--lang` or the `W
|
|
|
106
106
|
| Command | Does |
|
|
107
107
|
|---|---|
|
|
108
108
|
| `npx wizz-method install` | Installs modules, hooks, skills and MCPs into the project |
|
|
109
|
+
| `npx wizz-method catalog [--area <area>]` | Lists every area, skill and tool, marking what's installed |
|
|
109
110
|
| `npx wizz-method status` | Shows what's installed and each module's version |
|
|
110
111
|
| `npx wizz-method doctor` | Diagnostics: hooks, rtk, jq, optional Python and `_wizz/` drift |
|
|
111
112
|
| `npx wizz-method resolve [--skill <name>]` | Regenerates the customization snapshots in `_wizz/_resolved/` |
|
package/package.json
CHANGED
package/skills-registry.yaml
CHANGED
|
@@ -242,7 +242,7 @@ areas:
|
|
|
242
242
|
not_when:
|
|
243
243
|
- "Analisar/resumir um vídeo que já se tem em mãos: é claude-video."
|
|
244
244
|
- "Cortar/montar footage já captado: é buttercut (corte fino) ou wizz-remotion-best-practices (montagem final)."
|
|
245
|
-
- "Gerar imagem/vídeo por IA: é
|
|
245
|
+
- "Gerar imagem/vídeo por IA: é wizz-ad-creative, que escreve o prompt para o usuário colar no Flow ou em outra IA. O get-brolls busca material REAL, não sintético."
|
|
246
246
|
- "Desenhar uma tela/cena em HTML: é hyperframes."
|
|
247
247
|
- "Minerar/estudar criativo ou oferta de concorrente (Biblioteca de Anúncios, 'baixa esse anúncio pra eu ver'): é wizz-lowticket-minerador na área ads. O get-brolls capta insert para a SUA edição, com procedência registrada — não é ferramenta de inteligência competitiva, e o fato de saber baixar um Reel não o torna uma."
|
|
248
248
|
rel:
|
|
@@ -302,14 +302,6 @@ areas:
|
|
|
302
302
|
check: "test -x ./tools/ctc-align/.venv/bin/ctc-forced-aligner"
|
|
303
303
|
install: 'mkdir -p tools/ctc-align && uv venv --python 3.12 tools/ctc-align/.venv && uv pip install --python tools/ctc-align/.venv/bin/python "git+https://github.com/MahmoudAshraf97/ctc-forced-aligner.git"'
|
|
304
304
|
verify: "command -v ffmpeg >/dev/null 2>&1 && command -v uv >/dev/null 2>&1"
|
|
305
|
-
- id: fal-video
|
|
306
|
-
when: "GERAR vídeo/imagem por IA (Veo/Kling/Wan e outros modelos via API da fal.ai), estilo UGC/ad-ready. Substitui o arcads: mesmo caso de uso (vídeo gerado por IA), custo 50-95% menor por vídeo, sem clone-and-run pesado. Precisa FAL_KEY em .env. Sem CLI binária oficial: usa o client Node oficial @fal-ai/client como dependência do projeto."
|
|
307
|
-
rel:
|
|
308
|
-
pairs_with: [wizz-ad-creative]
|
|
309
|
-
platform: [darwin, linux]
|
|
310
|
-
check: "test -f node_modules/@fal-ai/client/package.json"
|
|
311
|
-
# Pin de supply chain (2026-08-25): versão fixa (client oficial fal.ai); atualizar conscientemente.
|
|
312
|
-
install: "npm install @fal-ai/client@1.10.1"
|
|
313
305
|
- id: apple-design
|
|
314
306
|
when: "Revisar/auditar UI NATIVA (iOS/macOS: SwiftUI, Flutter, React Native, Tauri, Electron) contra o Apple HIG e o Liquid Glass — compliance com o HIG, acessibilidade, dark mode, navegação. Cobre o gap que o wizz-taste-skill declara fora de escopo ('native mobile: use Apple HIG directly')."
|
|
315
307
|
not_when: "UI web: aí é wizz-ui-ux-pro-max / wizz-impeccable."
|
|
@@ -573,9 +565,10 @@ areas:
|
|
|
573
565
|
# para as CLIs de vídeo quando o roteiro vira vídeo", prometendo um handoff
|
|
574
566
|
# que a área não pode fazer: as CLIs de vídeo (get-brolls, hyperframes,
|
|
575
567
|
# buttercut, claude-video) moram em `designer`, e o único cli daqui é o
|
|
576
|
-
# postiz. Medido: essa frase puxava pedido
|
|
577
|
-
# meu Reel", "carrossel pro Instagram")
|
|
578
|
-
# ferramenta. A fronteira ficou no summary;
|
|
568
|
+
# buffer (era o postiz até 2026-09-25). Medido: essa frase puxava pedido
|
|
569
|
+
# de ASSET ("imagens de apoio pro meu Reel", "carrossel pro Instagram")
|
|
570
|
+
# para social, que não tem a ferramenta. A fronteira ficou no summary;
|
|
571
|
+
# a nota ficou aqui.
|
|
579
572
|
summary: "Social e roteiros virais: o TEXTO e o ROTEIRO de Reels/Shorts, hooks, legenda, thread, calendário de conteúdo. Persona Rafa, aplica o Blueprint de Roteiro Viral. Captar, desenhar ou montar o asset em si (b-roll, vídeo, carrossel, imagem) não é desta área."
|
|
580
573
|
skills:
|
|
581
574
|
- id: wizz-social-content
|
|
@@ -584,7 +577,7 @@ areas:
|
|
|
584
577
|
- "Editar/revisar texto de social já escrito: aí é wizz-copy-editing."
|
|
585
578
|
- "Pedir SÓ o asset de um Reel/vídeo — b-roll, 'imagens de apoio', 'cenas de apoio', imagem de corte, footage — sem que o texto ou o roteiro façam parte do pedido: a captação é get-brolls, na área designer. Esta entrada entra quando o TEXTO ou o ROTEIRO é parte do que se pede; se o pedido junta roteiro E captação, são 2 áreas."
|
|
586
579
|
rel:
|
|
587
|
-
before: [get-brolls
|
|
580
|
+
before: [buffer, get-brolls]
|
|
588
581
|
pairs_with: [wizz-canvas-design, wizz-content-strategy, wizz-copy-editing, wizz-launch-strategy]
|
|
589
582
|
- id: wizz-content-strategy
|
|
590
583
|
when: "Decidir O QUE produzir de conteúdo (topic clusters, calendário, pilares)."
|
|
@@ -596,20 +589,22 @@ areas:
|
|
|
596
589
|
after: [wizz-copywriting]
|
|
597
590
|
pairs_with: [wizz-social-content]
|
|
598
591
|
clis:
|
|
599
|
-
- id:
|
|
600
|
-
when: "PUBLICAR/agendar de verdade em redes sociais (
|
|
592
|
+
- id: buffer
|
|
593
|
+
when: "PUBLICAR/agendar de verdade em redes sociais (Instagram, TikTok, YouTube, LinkedIn, Facebook, Threads, X, Pinterest, Bluesky, Mastodon, Google Business) a partir do conteúdo que o wizz-social já gerou. Separa 'gerar' (Wizz) de 'publicar' (Buffer). Precisa de API key (`buffer init` ou env `BUFFER_API_KEY`). Imagem/vídeo NÃO sobe do disco: a mídia precisa estar numa URL pública HTTPS e direta (Cloudinary, Cloudflare R2), estável até o post publicar."
|
|
601
594
|
not_when: "Escrever o roteiro ou a legenda: isso é skill da área (wizz-social-content, wizz-copy-editing)."
|
|
602
595
|
rel:
|
|
603
596
|
after: [wizz-social-content]
|
|
604
|
-
#
|
|
605
|
-
# (
|
|
597
|
+
# Substituiu o postiz em 2026-09-25: a nuvem da Postiz não tem plano
|
|
598
|
+
# grátis (a partir de US$ 29/mês) e o self-host exige servidor próprio
|
|
599
|
+
# mais um app de desenvolvedor por rede. O plano grátis do Buffer tem
|
|
600
|
+
# 3 canais, 10 posts na fila por canal e API/CLI/MCP inclusos.
|
|
606
601
|
# CLI, não MCP: o schema de `mcps:` deste registry só aceita stdio
|
|
607
|
-
# ({command,args,env}) e o MCP
|
|
608
|
-
# (https://mcp.
|
|
609
|
-
#
|
|
610
|
-
#
|
|
611
|
-
check: "command -v
|
|
612
|
-
install: "npm install -g
|
|
602
|
+
# ({command,args,env}) e o MCP do Buffer é HTTP com bearer
|
|
603
|
+
# (https://mcp.buffer.com/mcp) — não encaixa.
|
|
604
|
+
# Pin de supply chain (2026-09-25): versão publicada fixa no npm,
|
|
605
|
+
# atualizar conscientemente.
|
|
606
|
+
check: "command -v buffer >/dev/null 2>&1"
|
|
607
|
+
install: "npm install -g @bufferapp/cli@1.2.0"
|
|
613
608
|
|
|
614
609
|
ads:
|
|
615
610
|
agent: wizz-ads
|
|
@@ -620,9 +615,9 @@ areas:
|
|
|
620
615
|
rel:
|
|
621
616
|
pairs_with: [wizz-ad-creative, wizz-tracking-audit]
|
|
622
617
|
- id: wizz-ad-creative
|
|
623
|
-
when: "Gerar/iterar criativos e headlines de anúncio em escala (RSA, variações)."
|
|
618
|
+
when: "Gerar/iterar criativos e headlines de anúncio em escala (RSA, variações). Inclui escrever o PROMPT de imagem/vídeo por IA (Veo, Kling, Flow e similares) para o usuário gerar fora do método: nenhuma API de geração está integrada."
|
|
624
619
|
rel:
|
|
625
|
-
pairs_with: [
|
|
620
|
+
pairs_with: [wizz-copywriting, wizz-paid-ads, wizz-video-effects]
|
|
626
621
|
- id: wizz-analytics-tracking
|
|
627
622
|
when: "Tracking de conversão pros anúncios (compartilhada com growth)."
|
|
628
623
|
rel:
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
| **Source B-roll / inserts: find real footage of what the script names** | `get-brolls` (darwin) |
|
|
28
28
|
| Edit/cut recorded footage (cuts, selects, roughcut) | `buttercut` (darwin-arm64 only) |
|
|
29
29
|
| Analyze an existing reference video (frames + transcription) | `claude-video` |
|
|
30
|
-
| AI-generated video/image (Veo/Kling/Wan), UGC / ad-ready style | `
|
|
30
|
+
| AI-generated video/image (Veo/Kling/Wan), UGC / ad-ready style | `wizz-ad-creative` (ads area) writes the prompt; the user generates it in Flow or another AI tool, outside the method |
|
|
31
31
|
|
|
32
32
|
## Full chain (script → publishing)
|
|
33
33
|
|
|
@@ -76,7 +76,7 @@ The Diretor (`wizz-router`) has already done the triage and handed it to you bec
|
|
|
76
76
|
- `areas:` — `agent` (must match the menu's) and `skills:` (each with an `id` + short `when`). Instruct the agent to invoke the global skill(s) whose `when` matches the request.
|
|
77
77
|
- `utility:` — cross-cutting skills (find-skills, enhance-prompt, wizz-router). Offer when it fits.
|
|
78
78
|
- `mcps:` (per area) and `mcp_utility:` (cross-cutting) — MCP servers the area uses to actually ACT (e.g. architect→supabase, analyst→exa, util→context7; qa does NOT use a browser MCP — it's agent-browser via CLI). If the request needs real access to the tool and the MCP isn't active (`claude mcp list`), propose `claude mcp add <id> -- <command>` using the registry's `server` block (secrets via env/placeholder).
|
|
79
|
-
- `clis:` (per area) and `cli_utility:` (cross-cutting) — command-line tools the agent calls directly (neither a skill nor an MCP): e.g. qa→agent-browser; designer→hyperframes/get-brolls/claude-video/buttercut/voicebox
|
|
79
|
+
- `clis:` (per area) and `cli_utility:` (cross-cutting) — command-line tools the agent calls directly (neither a skill nor an MCP): e.g. qa→agent-browser; designer→hyperframes/get-brolls/claude-video/buttercut/voicebox (video); growth→scrapling; seo→distribb; social→buffer. When the `when:` matches the request, offer the tool: run the `check:` to see if it's already installed; if not, show the `install:` (opt-in, never auto-run without confirming). Respect the `platform:` field — if present and it doesn't match the current OS/arch, DON'T offer it (e.g., `buttercut` is `darwin-arm64`, Apple Silicon only). Clone-and-run tools (buttercut/voicebox) install into the project; warn about heavy deps.
|
|
80
80
|
- `rel:` (on any entry) — **this is where the method knows that a request about A also needs C.** `pairs_with` (they go together), `before`/`after` (mandatory order) and `instead_of` (one or the other, not both). Every edge is reciprocal by construction: the schema validator rejects one-way edges, so the relation shows up whichever side the user enters from. As the multi-area orchestrator you are the biggest beneficiary of this — roughly a third of the edges cross areas (count them in the registry when you need the number; don't quote one from memory). When building the dispatch, follow the same order the area agents follow ([rel protocol](../../_shared/rel-protocol.md)): LIST every id the chosen entry's `rel` names — including the ones you don't recognise, which are exactly the point — resolve each id → area via the `ids:` map in `{project-root}/_wizz/_config/registry/index.yaml`, read that area's slice for its `when:`, and then either bring it in or discard it with a stated reason. A target in another area becomes a dispatch to THAT area's agent, and a `before` target sets the ORDER of the plan, not a parallel item. Never deliver only the half the user named when `rel` says the other half is missing: a related capability you never named is one the method lost.
|
|
81
81
|
- `squads:` — advisory panels (run via `wizz-party-mode`). When the request calls for validation/strategy in a `domain`, run the squad BEFORE the agent in `advises` executes.
|
|
82
82
|
|
|
@@ -58,7 +58,7 @@ Use this table to map skills/CLIs/MCPs directly **outside a Wizz project** (no `
|
|
|
58
58
|
| Effects and sound of a short vertical video (ad or Reels): which effect goes where and when not, which SFX for each event and at what level, ducking, master loudness, safe-zone/density/excess/rhythm gates | `wizz-video-effects` (designer registry; decides the effects, Remotion assembles them) | 2 |
|
|
59
59
|
| **Find B-roll / inserts / cutaways / supporting footage** for a video or Reel (real footage of the fact, person, product, news item or screen the script names; YouTube/Instagram/TikTok/Pexels/Pixabay/Commons/NASA) | CLI `get-brolls` (designer registry; **macOS**, global `skills add` + its own `install.sh`; needs ffmpeg with drawtext, Node 22+, Python 3.11+). Ends in a Storyboard the HUMAN approves — the agent never self-approves. Stock only when the user explicitly asks for stock | 1 |
|
|
60
60
|
| **Edit/cut video** (cuts, selects, roughcut) | CLI `buttercut` (designer registry; **Apple Silicon only**, clone-and-run in the project, heavy deps) | 1 |
|
|
61
|
-
| Generate an AI video/image (Veo/Kling/Wan), UGC / ad-ready style |
|
|
61
|
+
| Generate an AI video/image (Veo/Kling/Wan), UGC / ad-ready style | `wizz-ad-creative` writes the prompt for the user to paste into Flow or another AI tool. No generation API is integrated in the method | 1 |
|
|
62
62
|
| Analyze/understand an existing video (frames + transcription) | CLI `claude-video` (designer registry; `npx skills add bradautomates/claude-video`) | 2 |
|
|
63
63
|
| Narration / voice / TTS / voice cloning for video | CLI `voicebox` (designer registry; local app with MCP endpoint) | 2 |
|
|
64
64
|
| Research components compatible with the project | `wizz-ui-component-curator` (demos, code, and public registries with evidence) | 1 |
|
|
@@ -83,7 +83,7 @@ Use this table to map skills/CLIs/MCPs directly **outside a Wizz project** (no `
|
|
|
83
83
|
| EXECUTE the SEO distribution: publish articles, build backlinks through the Distribb network, connect Search Console, autopublish news | CLI `distribb` (seo registry) — the doing half; the strategy half is the SEO row above | 2 |
|
|
84
84
|
| Competitor comparison, alternatives page, war room | `wizz-competitor-alternatives` | 2 |
|
|
85
85
|
| Content, social media, newsletter, threads | `wizz-content-strategy` + `wizz-social-content` + `wizz-copy-editing` | 2 |
|
|
86
|
-
| Actually PUBLISH/schedule to social networks (
|
|
86
|
+
| Actually PUBLISH/schedule to social networks (Instagram, TikTok, YouTube, LinkedIn and more), not write it | CLI `buffer` (social registry; needs `BUFFER_API_KEY`; media must be a public HTTPS URL). Separates "generate" (Wizz skills above) from "publish" | 2 |
|
|
87
87
|
| Cold email, B2B outbound, prospecting | `wizz-cold-email` | 2 |
|
|
88
88
|
| Referral, referrals, affiliate program | `wizz-referral-program` | 2 |
|
|
89
89
|
| Sales material, pitch, deck, proposal | `wizz-sales-enablement` | 2 |
|
|
@@ -1,40 +1,91 @@
|
|
|
1
1
|
const prompts = require('./prompts');
|
|
2
|
+
const { t } = require('./guided/messages');
|
|
3
|
+
|
|
4
|
+
// Wizz brand palette (brand book): Wizz Orange and its deep shade.
|
|
5
|
+
const BRAND_ORANGE = [255, 69, 0];
|
|
6
|
+
const BRAND_DEEP = [204, 55, 0];
|
|
7
|
+
// Built from the char code on purpose: a literal escape byte in source turns
|
|
8
|
+
// the file binary for git (see the review-prep.mjs incident).
|
|
9
|
+
const ESC = String.fromCodePoint(27);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Paint text with a 24-bit brand color, or leave it plain when the terminal
|
|
13
|
+
* has no color support (NO_COLOR, pipes, CI logs).
|
|
14
|
+
* @param {number[]} rgb - [r, g, b]
|
|
15
|
+
* @param {string} text - Text to paint
|
|
16
|
+
* @param {boolean} enabled - Whether color is supported
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
function paint(rgb, text, enabled) {
|
|
20
|
+
if (!enabled) return text;
|
|
21
|
+
return `${ESC}[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${text}${ESC}[39m`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const LOGO_WIDE = [
|
|
25
|
+
'██╗ ██╗ ██╗ ███████╗ ███████╗ ███╗ ███╗ ███████╗ ████████╗ ██╗ ██╗ ██████╗ ██████╗ ™',
|
|
26
|
+
'██║ ██║ ██║ ╚══███╔╝ ╚══███╔╝ ████╗ ████║ ██╔════╝ ╚══██╔══╝ ██║ ██║ ██╔═══██╗ ██╔══██╗',
|
|
27
|
+
'██║ █╗ ██║ ██║ ███╔╝ ███╔╝ ██╔████╔██║ █████╗ ██║ ███████║ ██║ ██║ ██║ ██║',
|
|
28
|
+
'██║███╗██║ ██║ ███╔╝ ███╔╝ ██║╚██╔╝██║ ██╔══╝ ██║ ██╔══██║ ██║ ██║ ██║ ██║',
|
|
29
|
+
'╚███╔███╔╝ ██║ ███████╗ ███████╗ ██║ ╚═╝ ██║ ███████╗ ██║ ██║ ██║ ╚██████╔╝ ██████╔╝',
|
|
30
|
+
' ╚══╝╚══╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const LOGO_NARROW = [
|
|
34
|
+
'██╗ ██╗ ██╗ ███████╗ ███████╗ ™',
|
|
35
|
+
'██║ ██║ ██║ ╚══███╔╝ ╚══███╔╝',
|
|
36
|
+
'██║ █╗ ██║ ██║ ███╔╝ ███╔╝ ',
|
|
37
|
+
'██║███╗██║ ██║ ███╔╝ ███╔╝ ',
|
|
38
|
+
'╚███╔███╔╝ ██║ ███████╗ ███████╗',
|
|
39
|
+
' ╚══╝╚══╝ ╚═╝ ╚══════╝ ╚══════╝',
|
|
40
|
+
];
|
|
2
41
|
|
|
3
42
|
const CLIUtils = {
|
|
4
43
|
/**
|
|
5
|
-
* Display Wizz Method
|
|
44
|
+
* Display the Wizz Method opening screen. With `steps`, it also lists the
|
|
45
|
+
* guided path's four steps so the user knows what comes next.
|
|
46
|
+
* @param {Object} [opts]
|
|
47
|
+
* @param {boolean} [opts.steps=false] - Show the "what happens next" list
|
|
48
|
+
* @param {'en'|'pt'} [opts.lang='en'] - Display language
|
|
49
|
+
* @param {string} [opts.version] - Version shown under the logo
|
|
6
50
|
*/
|
|
7
|
-
async displayLogo() {
|
|
51
|
+
async displayLogo(opts = {}) {
|
|
52
|
+
const { steps = false, lang = 'en', version } = opts;
|
|
8
53
|
const color = await prompts.getColor();
|
|
54
|
+
const colorOn = color.isColorSupported !== false;
|
|
9
55
|
const termWidth = process.stdout.columns || 80;
|
|
10
56
|
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
const wide = termWidth >= 100;
|
|
58
|
+
const logoLines = wide ? LOGO_WIDE : LOGO_NARROW;
|
|
59
|
+
// Top half in Wizz Orange, bottom half in the deep shade: reads as a
|
|
60
|
+
// light-to-shadow gradient without a per-character color ramp.
|
|
61
|
+
const half = Math.ceil(logoLines.length / 2);
|
|
62
|
+
const logo = logoLines.map((line, i) => paint(i < half ? BRAND_ORANGE : BRAND_DEEP, line, colorOn));
|
|
63
|
+
if (!wide) logo.push(paint(BRAND_ORANGE, 'M E T H O D', colorOn));
|
|
64
|
+
|
|
65
|
+
const lines = [...logo, ''];
|
|
66
|
+
const versionTag = version ? color.dim(`v${version}`) : '';
|
|
67
|
+
lines.push(`${color.white(t(lang, 'tagline'))} ${versionTag}`.trimEnd());
|
|
68
|
+
|
|
69
|
+
if (steps) {
|
|
70
|
+
lines.push('', `${color.bold(t(lang, 'whatHappens'))} ${color.dim(`(${t(lang, 'duration')})`)}`);
|
|
71
|
+
for (const [i, name] of t(lang, 'stepNames').entries()) {
|
|
72
|
+
lines.push(`${paint(BRAND_ORANGE, String(i + 1), colorOn)} ▸ ${name}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
lines.push('', color.dim('© Wizz! comms. · method.wizzcomms.com'));
|
|
77
|
+
|
|
78
|
+
// A terminal that reports 0 columns (some ptys, expect, CI with a TTY)
|
|
79
|
+
// makes clack's box compute a negative width and throw. Print the same
|
|
80
|
+
// lines without the frame instead of failing the install.
|
|
81
|
+
if (!process.stdout.columns || process.stdout.columns < 40) {
|
|
82
|
+
process.stdout.write(`${lines.join('\n')}\n\n`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await prompts.box(lines.join('\n'), '', {
|
|
86
|
+
contentAlign: steps ? 'left' : 'center',
|
|
36
87
|
rounded: true,
|
|
37
|
-
formatBorder:
|
|
88
|
+
formatBorder: (text) => paint(BRAND_DEEP, text, colorOn),
|
|
38
89
|
});
|
|
39
90
|
},
|
|
40
91
|
|
|
@@ -50,4 +101,4 @@ const CLIUtils = {
|
|
|
50
101
|
},
|
|
51
102
|
};
|
|
52
103
|
|
|
53
|
-
module.exports = { CLIUtils };
|
|
104
|
+
module.exports = { CLIUtils, paint, BRAND_ORANGE, BRAND_DEEP };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const { getProjectRoot } = require('../project-root');
|
|
5
|
+
const { loadYamlCached } = require('../core/yaml-cache');
|
|
6
|
+
const { AREA_LABELS, cliBadges, cliDescription } = require('../guided/labels');
|
|
7
|
+
const { normalizeLang, t } = require('../guided/messages');
|
|
8
|
+
const { paint, BRAND_ORANGE } = require('../banner');
|
|
9
|
+
|
|
10
|
+
const SKILL_DIRS = ['.claude/skills', '.agents/skills'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether a skill folder exists in the project or in the global Claude
|
|
14
|
+
* skills folder. Only a hint for the ✓ mark; nothing depends on it.
|
|
15
|
+
*/
|
|
16
|
+
function isSkillInstalled(id, projectDir, home) {
|
|
17
|
+
const places = [...SKILL_DIRS.map((dir) => path.join(projectDir, dir, id)), path.join(home, '.claude', 'skills', id)];
|
|
18
|
+
return places.some((place) => fs.existsSync(place));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** First sentence of a registry `when:`, cut to fit one terminal line. */
|
|
22
|
+
function shortWhen(when, max = 90) {
|
|
23
|
+
const first = String(when || '').split(/(?<=[.!?])\s/)[0];
|
|
24
|
+
return first.length > max ? `${first.slice(0, max - 1)}…` : first;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Render the catalog as plain text lines.
|
|
29
|
+
* @param {Object} registry - Parsed skills-registry.yaml
|
|
30
|
+
* @param {Object} opts
|
|
31
|
+
* @param {'en'|'pt'} opts.lang - Display language
|
|
32
|
+
* @param {string|null} opts.area - Only this area, or null for all
|
|
33
|
+
* @param {(id: string) => boolean} opts.installed - Installed check
|
|
34
|
+
* @param {boolean} opts.color - Whether to paint
|
|
35
|
+
* @returns {string[]}
|
|
36
|
+
*/
|
|
37
|
+
function renderCatalog(registry, opts) {
|
|
38
|
+
const { lang, area, installed, color } = opts;
|
|
39
|
+
const areas = Object.entries(registry.areas || {}).filter(([key]) => !area || key === area);
|
|
40
|
+
const lines = [];
|
|
41
|
+
for (const [key, entry] of areas) {
|
|
42
|
+
const label = AREA_LABELS[key] ? AREA_LABELS[key][lang] : { name: key, help: entry.summary || '' };
|
|
43
|
+
lines.push('', `${paint(BRAND_ORANGE, `■ ${label.name}`, color)} (${key}) · ${label.help}`);
|
|
44
|
+
for (const skill of entry.skills || []) {
|
|
45
|
+
const mark = installed(skill.id) ? '✓' : ' ';
|
|
46
|
+
lines.push(` ${mark} ${skill.id.padEnd(40)} ${shortWhen(skill.when)}`);
|
|
47
|
+
}
|
|
48
|
+
for (const cli of entry.clis || []) {
|
|
49
|
+
const badges = cliBadges(cli, lang);
|
|
50
|
+
const needs = badges.length > 0 ? ` [${badges.join(', ')}]` : ` [${t(lang, 'free')}]`;
|
|
51
|
+
lines.push(` ⌘ ${cli.id.padEnd(40)} ${cliDescription(cli.id, lang)}${needs}`);
|
|
52
|
+
}
|
|
53
|
+
for (const mcp of entry.mcps || []) {
|
|
54
|
+
lines.push(` ◇ ${mcp.id.padEnd(40)} ${shortWhen(mcp.when)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
command: 'catalog',
|
|
62
|
+
description: 'List every area, skill and tool Wizz Method can install (✓ = installed)',
|
|
63
|
+
options: [
|
|
64
|
+
['--area <area>', 'Only one area, e.g. social or dev'],
|
|
65
|
+
['--lang <lang>', 'Language for area names and tool descriptions: en (default) or pt'],
|
|
66
|
+
],
|
|
67
|
+
action: async (options) => {
|
|
68
|
+
const registry = await loadYamlCached(path.join(getProjectRoot(), 'skills-registry.yaml'));
|
|
69
|
+
if (options.area && !(registry.areas || {})[options.area]) {
|
|
70
|
+
process.stderr.write(`Unknown area "${options.area}". Areas: ${Object.keys(registry.areas).join(', ')}\n`);
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const projectDir = process.cwd();
|
|
75
|
+
const home = os.homedir();
|
|
76
|
+
const lang = normalizeLang(options.lang) || 'en';
|
|
77
|
+
const lines = renderCatalog(registry, {
|
|
78
|
+
lang,
|
|
79
|
+
area: options.area || null,
|
|
80
|
+
installed: (id) => isSkillInstalled(id, projectDir, home),
|
|
81
|
+
color: !!process.stdout.isTTY && !process.env.NO_COLOR,
|
|
82
|
+
});
|
|
83
|
+
lines.push('', t(lang, 'catalogLegend'), '');
|
|
84
|
+
process.stdout.write(lines.join('\n'));
|
|
85
|
+
},
|
|
86
|
+
renderCatalog,
|
|
87
|
+
};
|
|
@@ -47,6 +47,11 @@ module.exports = {
|
|
|
47
47
|
['--output-folder <path>', 'Output folder path relative to project root (default: _wizz-output)'],
|
|
48
48
|
['--custom-source <sources>', 'Comma-separated Git URLs or local paths to install custom modules from'],
|
|
49
49
|
['-y, --yes', 'Accept all defaults and skip prompts where possible'],
|
|
50
|
+
['--lang <lang>', 'Installer language for the guided install: en (default) or pt'],
|
|
51
|
+
[
|
|
52
|
+
'--advanced',
|
|
53
|
+
'Skip the guided install and ask every question: module list, community modules, release channels, artifact folders and the routing meter',
|
|
54
|
+
],
|
|
50
55
|
[
|
|
51
56
|
'--channel <channel>',
|
|
52
57
|
'Apply channel (stable|next) to all external modules being installed. --all-stable and --all-next are aliases.',
|
|
@@ -171,6 +176,10 @@ module.exports = {
|
|
|
171
176
|
process.exit(1);
|
|
172
177
|
}
|
|
173
178
|
if (result.success) {
|
|
179
|
+
if (config.guided) {
|
|
180
|
+
const { showGuidedOutro } = require('../guided/outro');
|
|
181
|
+
await showGuidedOutro(config);
|
|
182
|
+
}
|
|
174
183
|
process.exit(0);
|
|
175
184
|
}
|
|
176
185
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const os = require('node:os');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Signs that an AI tool is present on this machine: a binary on PATH, a
|
|
7
|
+
* config folder in the home directory or an installed app. Any one is
|
|
8
|
+
* enough; this only decides which option comes pre-checked, never what
|
|
9
|
+
* gets installed.
|
|
10
|
+
*/
|
|
11
|
+
const SIGNS = {
|
|
12
|
+
'claude-code': { bins: ['claude'], homeDirs: ['.claude'], apps: [] },
|
|
13
|
+
codex: { bins: ['codex'], homeDirs: ['.codex'], apps: [] },
|
|
14
|
+
cursor: { bins: ['cursor'], homeDirs: ['.cursor'], apps: ['Cursor.app'] },
|
|
15
|
+
'github-copilot': { bins: [], homeDirs: [], apps: [], vscodeExtension: 'github.copilot' },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function onPath(bin) {
|
|
19
|
+
const dirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
20
|
+
const exts = process.platform === 'win32' ? ['.exe', '.cmd', ''] : [''];
|
|
21
|
+
return dirs.some((dir) => exts.some((ext) => fs.existsSync(path.join(dir, bin + ext))));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function hasVscodeExtension(prefix, home) {
|
|
25
|
+
const extDir = path.join(home, '.vscode', 'extensions');
|
|
26
|
+
try {
|
|
27
|
+
return fs.readdirSync(extDir).some((name) => name.toLowerCase().startsWith(prefix));
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Which of the preferred AI tools look installed.
|
|
35
|
+
* @param {Object} [deps] - Injection seam for tests
|
|
36
|
+
* @param {string} [deps.home] - Home directory
|
|
37
|
+
* @returns {string[]} Tool ids, e.g. ['claude-code', 'cursor']
|
|
38
|
+
*/
|
|
39
|
+
function detectAiTools(deps = {}) {
|
|
40
|
+
const home = deps.home || os.homedir();
|
|
41
|
+
const found = [];
|
|
42
|
+
for (const [id, sign] of Object.entries(SIGNS)) {
|
|
43
|
+
const present =
|
|
44
|
+
sign.bins.some((bin) => onPath(bin)) ||
|
|
45
|
+
sign.homeDirs.some((dir) => fs.existsSync(path.join(home, dir))) ||
|
|
46
|
+
sign.apps.some((app) => fs.existsSync(path.join('/Applications', app))) ||
|
|
47
|
+
(sign.vscodeExtension ? hasVscodeExtension(sign.vscodeExtension, home) : false);
|
|
48
|
+
if (present) found.push(id);
|
|
49
|
+
}
|
|
50
|
+
return found;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { detectAiTools };
|