wizz-method 2.11.2 → 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 +38 -29
- package/src/bmm-skills/module.yaml +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/04-criativos.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/11-checklist-roi.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/12-mineracao-ofertas.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/pontos-em-aberto.md +2 -2
- package/src/modules/wizz/_shared/video-pipeline.md +12 -4
- package/src/modules/wizz/agents/wizz-designer/SKILL.md +1 -1
- package/src/modules/wizz/agents/wizz-maestro/SKILL.md +1 -1
- package/src/modules/wizz/agents/wizz-social/SKILL.md +1 -0
- package/src/skills-lib/wizz-remotion-best-practices/rules/sfx.md +3 -1
- package/src/skills-lib/wizz-router/references/routing-table-flat.md +3 -2
- package/src/skills-lib/wizz-video-effects/SKILL.md +85 -0
- package/src/skills-lib/wizz-video-effects/assets/effects.config.template.json +178 -0
- package/src/skills-lib/wizz-video-effects/assets/remotion/EffectsLayer.tsx +87 -0
- package/src/skills-lib/wizz-video-effects/assets/remotion/README.md +37 -0
- package/src/skills-lib/wizz-video-effects/assets/remotion/useEffects.ts +120 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/CREDITS.md +56 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/cash.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/chime.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/click-soft.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/click.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/coin.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/error.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/impact-bass-1.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/impact-bass-2.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/key-press.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/notif.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/notification.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/paste.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/ping.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/pop.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/riser-metal.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/riser.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/send.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/sparkle.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/stamp.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/success.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/type.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/typing.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/whoosh-cinematic.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/whoosh-short.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/whoosh.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/references/audio.md +87 -0
- package/src/skills-lib/wizz-video-effects/references/data-contract.md +145 -0
- package/src/skills-lib/wizz-video-effects/references/do-not-use.md +54 -0
- package/src/skills-lib/wizz-video-effects/references/editing-patterns.md +58 -0
- package/src/skills-lib/wizz-video-effects/references/gates.md +37 -0
- package/src/skills-lib/wizz-video-effects/references/sfx-licensing.md +40 -0
- package/src/skills-lib/wizz-video-effects/references/when-to-use.md +129 -0
- package/src/skills-lib/wizz-video-effects/scripts/budget.py +211 -0
- package/src/skills-lib/wizz-video-effects/scripts/check_excess.py +218 -0
- package/src/skills-lib/wizz-video-effects/scripts/check_loudness.py +175 -0
- package/src/skills-lib/wizz-video-effects/scripts/check_rhythm.py +137 -0
- package/src/skills-lib/wizz-video-effects/scripts/check_safe_zone.py +221 -0
- package/src/skills-lib/wizz-video-effects/scripts/config.py +583 -0
- package/src/skills-lib/wizz-video-effects/scripts/fill_breaths.py +355 -0
- package/src/skills-lib/wizz-video-effects/scripts/measure_density.py +122 -0
- package/src/skills-lib/wizz-video-effects/scripts/normalize_audio.py +189 -0
- package/src/skills-lib/wizz-video-effects/scripts/sound.py +342 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_budget.py +131 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_check_loudness.py +156 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_checks.py +190 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_config.py +135 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_fill_breaths.py +179 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_sound.py +173 -0
- package/src/skills-lib/wizz-video-effects/scripts/timeline.py +85 -0
- 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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Read and write the timeline JSON every script shares.
|
|
3
|
+
|
|
4
|
+
The shape is documented in references/data-contract.md. This module only
|
|
5
|
+
normalizes it: absent lists become empty lists, never errors, and every
|
|
6
|
+
instant comes back as a float in seconds.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TimelineError(Exception):
|
|
15
|
+
"""Timeline file missing or malformed."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load(path: str | Path) -> dict:
|
|
19
|
+
p = Path(path)
|
|
20
|
+
if not p.exists():
|
|
21
|
+
raise TimelineError(f"timeline not found: {p}")
|
|
22
|
+
try:
|
|
23
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
24
|
+
except json.JSONDecodeError as e:
|
|
25
|
+
raise TimelineError(f"{p} is not valid JSON: {e}") from e
|
|
26
|
+
if not isinstance(data, dict):
|
|
27
|
+
raise TimelineError(f"{p}: the timeline must be a JSON object")
|
|
28
|
+
return data
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def save(path: str | Path, data: dict) -> None:
|
|
32
|
+
Path(path).write_text(json.dumps(data, ensure_ascii=False, indent=1) + "\n",
|
|
33
|
+
encoding="utf-8")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def instants(items, key: str = "at") -> list[float]:
|
|
37
|
+
"""A list of numbers OR of objects {key: ...} as sorted floats."""
|
|
38
|
+
if not items:
|
|
39
|
+
return []
|
|
40
|
+
return sorted(float(i[key]) if isinstance(i, dict) else float(i) for i in items)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def transitions(scenes) -> list[float]:
|
|
44
|
+
"""Scene starts with a real transition (not empty, not 'cut')."""
|
|
45
|
+
out = []
|
|
46
|
+
for s in scenes or []:
|
|
47
|
+
tr = s.get("transition")
|
|
48
|
+
if tr and tr != "cut" and "start" in s:
|
|
49
|
+
out.append(float(s["start"]))
|
|
50
|
+
return sorted(out)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def duration(data: dict) -> tuple[float, bool]:
|
|
54
|
+
"""(duration, derived). Uses `duration`; else the largest scene end."""
|
|
55
|
+
if data.get("duration"):
|
|
56
|
+
return float(data["duration"]), False
|
|
57
|
+
ends = [float(s["start"]) + float(s.get("dur", 0.0))
|
|
58
|
+
for s in (data.get("scenes") or []) if "start" in s]
|
|
59
|
+
return (max(ends), True) if ends else (0.0, True)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def fps(data: dict, default: int) -> int:
|
|
63
|
+
return int(data.get("fps") or default)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def sounds(data: dict) -> list[dict]:
|
|
67
|
+
"""Every sound of the piece, sorted by `at`: sfx[] plus scene-level sfx
|
|
68
|
+
(a scene's `sfx` plays at the scene start)."""
|
|
69
|
+
out = [dict(s) for s in (data.get("sfx") or [])]
|
|
70
|
+
for sc in data.get("scenes") or []:
|
|
71
|
+
s = sc.get("sfx")
|
|
72
|
+
if isinstance(s, dict) and "start" in sc:
|
|
73
|
+
out.append({**s, "at": float(sc["start"]), "scene": True})
|
|
74
|
+
return sorted(out, key=lambda s: float(s["at"]))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def effect_events(data: dict) -> dict[str, list[float]]:
|
|
78
|
+
"""Visual effect instants by budget kind."""
|
|
79
|
+
return {
|
|
80
|
+
"punch": instants(data.get("punches")),
|
|
81
|
+
"shake": instants(data.get("shakes")),
|
|
82
|
+
"flash": instants(data.get("flashes")),
|
|
83
|
+
"interrupt": instants(data.get("interrupts")),
|
|
84
|
+
"transition": transitions(data.get("scenes")),
|
|
85
|
+
}
|
|
@@ -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 };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guided install: the default path for a first install in an interactive
|
|
3
|
+
* terminal. Four steps (folder, profile, AI tool, name), a review screen and
|
|
4
|
+
* sensible defaults for everything else. The full question-by-question flow
|
|
5
|
+
* stays available with --advanced, and every non-interactive flag keeps
|
|
6
|
+
* working exactly as before (those paths never reach this file).
|
|
7
|
+
*/
|
|
8
|
+
const prompts = require('../prompts');
|
|
9
|
+
const { t, LANGUAGES } = require('./messages');
|
|
10
|
+
const { getProfile, summarizeAreas } = require('./profiles');
|
|
11
|
+
const steps = require('./steps');
|
|
12
|
+
const { AREA_LABELS } = require('./labels');
|
|
13
|
+
|
|
14
|
+
// Flags that already answer part of the install: when present the user is
|
|
15
|
+
// scripting or knows what they want, so the legacy flow handles it verbatim.
|
|
16
|
+
const SCRIPTED_FLAGS = [
|
|
17
|
+
'yes',
|
|
18
|
+
'advanced',
|
|
19
|
+
'modules',
|
|
20
|
+
'tools',
|
|
21
|
+
'areas',
|
|
22
|
+
'mcps',
|
|
23
|
+
'clis',
|
|
24
|
+
'customSource',
|
|
25
|
+
'action',
|
|
26
|
+
'set',
|
|
27
|
+
'channel',
|
|
28
|
+
'allStable',
|
|
29
|
+
'allNext',
|
|
30
|
+
'next',
|
|
31
|
+
'pin',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether this run should take the guided path.
|
|
36
|
+
* @param {Object} options - Parsed CLI options
|
|
37
|
+
* @param {boolean} [isTTY] - Injection seam for tests
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
function isGuidedEligible(options = {}, isTTY = !!process.stdin.isTTY) {
|
|
41
|
+
if (!isTTY) return false;
|
|
42
|
+
return !SCRIPTED_FLAGS.some((flag) => {
|
|
43
|
+
const value = options[flag];
|
|
44
|
+
return Array.isArray(value) ? value.length > 0 : value !== undefined && value !== false;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function listModules() {
|
|
49
|
+
const { OfficialModules } = require('../modules/official-modules');
|
|
50
|
+
const { ExternalModuleManager } = require('../modules/external-manager');
|
|
51
|
+
const builtIn = ((await new OfficialModules().listAvailable()).modules || []).map((mod) => ({
|
|
52
|
+
code: mod.id,
|
|
53
|
+
name: mod.name,
|
|
54
|
+
description: mod.description,
|
|
55
|
+
defaultSelected: mod.defaultSelected,
|
|
56
|
+
fromBmad: false,
|
|
57
|
+
}));
|
|
58
|
+
const seen = new Set(builtIn.map((mod) => mod.code));
|
|
59
|
+
const external = (await new ExternalModuleManager().listAvailable())
|
|
60
|
+
.filter((mod) => !mod.builtIn && !mod.deprecated && !mod.advancedOnly && !seen.has(mod.code))
|
|
61
|
+
.map((mod) => ({
|
|
62
|
+
code: mod.code,
|
|
63
|
+
name: mod.name,
|
|
64
|
+
description: mod.description,
|
|
65
|
+
defaultSelected: false,
|
|
66
|
+
fromBmad: String(mod.url || '').includes('bmad-code-org'),
|
|
67
|
+
}));
|
|
68
|
+
return [...builtIn, ...external];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve what a profile installs. Custom asks modules and areas here.
|
|
73
|
+
* @returns {Promise<{modules: string[], areas: string[]}>} areas [] = all
|
|
74
|
+
*/
|
|
75
|
+
async function resolveSelection(profileId, lang, registry) {
|
|
76
|
+
const profile = getProfile(profileId);
|
|
77
|
+
if (profile.id !== 'custom') {
|
|
78
|
+
return { modules: [...profile.modules], areas: profile.areas ? [...profile.areas] : [] };
|
|
79
|
+
}
|
|
80
|
+
const modules = await steps.askModules(lang, await listModules());
|
|
81
|
+
const areas = modules.includes('bmm') ? await steps.askAreas(lang, registry) : [];
|
|
82
|
+
return { modules, areas };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function showReview(ctx) {
|
|
86
|
+
const { lang, profileId, selection, registry, ides } = ctx;
|
|
87
|
+
const color = await prompts.getColor();
|
|
88
|
+
const summary = summarizeAreas(registry, selection.areas);
|
|
89
|
+
const ok = color.green('✓');
|
|
90
|
+
const lines = [
|
|
91
|
+
`${ok} ${t(lang, 'reviewAgents', { n: summary.agents })} ${color.dim(summary.areas.map((key) => (AREA_LABELS[key] ? AREA_LABELS[key][lang].name : key)).join(' · '))}`,
|
|
92
|
+
`${ok} ${t(lang, 'reviewSkills', { n: summary.skills })} ${color.dim(t(lang, 'reviewSkillsHint'))}`,
|
|
93
|
+
`${ok} ${t(lang, 'reviewMemory')} ${color.dim(t(lang, 'reviewMemoryHint'))}`,
|
|
94
|
+
`${ok} ${t(lang, 'reviewTools')} ${color.dim(ides.join(', '))}`,
|
|
95
|
+
];
|
|
96
|
+
if (selection.modules.includes('lowticket')) lines.push(`${ok} ${t(lang, 'reviewLowticket')}`);
|
|
97
|
+
await prompts.note(lines.join('\n'), `${t(lang, 'reviewTitle')} (${getProfile(profileId)[lang].name})`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function chooseClis(ui, lang, selection) {
|
|
101
|
+
const plan = await ui.selectClis(selection.modules, selection.areas, {
|
|
102
|
+
cliMessage: t(lang, 'optionalTitle'),
|
|
103
|
+
cliOption: (cli) => steps.cliOption(cli, lang),
|
|
104
|
+
});
|
|
105
|
+
if (plan.alreadyInstalled.length > 0) {
|
|
106
|
+
await prompts.log.info(t(lang, 'alreadyInstalled', { list: plan.alreadyInstalled.map((c) => c.id).join(', ') }));
|
|
107
|
+
}
|
|
108
|
+
return plan;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function chooseMcps(ui, lang, profileId, selection) {
|
|
112
|
+
// Profiles never write MCP config on their own: they only print the
|
|
113
|
+
// command, like --yes does. Custom asks, same as the legacy flow.
|
|
114
|
+
if (profileId !== 'custom') return ui.selectMcps(selection.modules, selection.areas, { yes: true });
|
|
115
|
+
return ui.selectMcps(selection.modules, selection.areas, { mcpMessage: t(lang, 'mcpsQuestion') });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Steps 2 to 4 plus the review loop. Returns everything the installer
|
|
120
|
+
* needs except module configs.
|
|
121
|
+
*/
|
|
122
|
+
async function collectChoices(ui, lang, registry, ideManager) {
|
|
123
|
+
let profileId = 'everything';
|
|
124
|
+
let selection;
|
|
125
|
+
let ides;
|
|
126
|
+
let userName;
|
|
127
|
+
for (;;) {
|
|
128
|
+
await steps.stepHeader(lang, 2);
|
|
129
|
+
profileId = await steps.askProfile(lang, profileId);
|
|
130
|
+
selection = await resolveSelection(profileId, lang, registry);
|
|
131
|
+
if (!ides) {
|
|
132
|
+
await steps.stepHeader(lang, 3);
|
|
133
|
+
ides = await steps.askTools(lang, ideManager);
|
|
134
|
+
await steps.stepHeader(lang, 4);
|
|
135
|
+
userName = await steps.askName(lang);
|
|
136
|
+
}
|
|
137
|
+
await showReview({ lang, profileId, selection, registry, ides });
|
|
138
|
+
const cliPlan = await chooseClis(ui, lang, selection);
|
|
139
|
+
const action = await prompts.select({
|
|
140
|
+
message: t(lang, 'reviewAction'),
|
|
141
|
+
choices: [
|
|
142
|
+
{ name: t(lang, 'actionInstall'), value: 'install' },
|
|
143
|
+
{ name: t(lang, 'actionProfile'), value: 'profile' },
|
|
144
|
+
{ name: t(lang, 'actionCustomize'), value: 'customize' },
|
|
145
|
+
],
|
|
146
|
+
default: 'install',
|
|
147
|
+
});
|
|
148
|
+
if (action === 'install') {
|
|
149
|
+
const mcpPlan = await chooseMcps(ui, lang, profileId, selection);
|
|
150
|
+
return { profileId, selection, ides, userName, cliPlan, mcpPlan };
|
|
151
|
+
}
|
|
152
|
+
if (action === 'customize') profileId = 'custom';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Run the guided fresh install and return the same config shape the legacy
|
|
158
|
+
* flow returns, so the installer itself does not change.
|
|
159
|
+
* @param {Object} ui - The UI instance (reuses its validators and pickers)
|
|
160
|
+
* @param {Object} options - Parsed CLI options
|
|
161
|
+
* @param {{directory: string, lang: 'en'|'pt', channelOptions: Object}} ctx
|
|
162
|
+
*/
|
|
163
|
+
async function runGuidedInstall(ui, options, ctx) {
|
|
164
|
+
const { directory, lang, channelOptions } = ctx;
|
|
165
|
+
const registry = await ui._loadSkillsRegistry();
|
|
166
|
+
const { IdeManager } = require('../ide/manager');
|
|
167
|
+
const ideManager = new IdeManager();
|
|
168
|
+
await ideManager.ensureInitialized();
|
|
169
|
+
|
|
170
|
+
const choices = await collectChoices(ui, lang, registry, ideManager);
|
|
171
|
+
const modules = ['core', ...choices.selection.modules.filter((code) => code !== 'core')];
|
|
172
|
+
const language = LANGUAGES.find((l) => l.value === lang).communication;
|
|
173
|
+
const { moduleConfigs, setOverrides } = await ui.collectModuleConfigs(directory, modules, {
|
|
174
|
+
...options,
|
|
175
|
+
userName: choices.userName,
|
|
176
|
+
communicationLanguage: language,
|
|
177
|
+
documentOutputLanguage: language,
|
|
178
|
+
outputFolder: '_wizz-output',
|
|
179
|
+
yes: true,
|
|
180
|
+
quiet: true,
|
|
181
|
+
channelOptions,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
actionType: 'install',
|
|
186
|
+
directory,
|
|
187
|
+
modules,
|
|
188
|
+
ides: choices.ides,
|
|
189
|
+
skipIde: choices.ides.length === 0,
|
|
190
|
+
selectedAreas: choices.selection.areas,
|
|
191
|
+
mcpPlan: choices.mcpPlan,
|
|
192
|
+
cliPlan: choices.cliPlan,
|
|
193
|
+
coreConfig: moduleConfigs.core || {},
|
|
194
|
+
moduleConfigs,
|
|
195
|
+
setOverrides,
|
|
196
|
+
skipPrompts: false,
|
|
197
|
+
channelOptions,
|
|
198
|
+
traceEnabled: null,
|
|
199
|
+
guided: { lang, profile: choices.profileId },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = { isGuidedEligible, runGuidedInstall, SCRIPTED_FLAGS };
|