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.
Files changed (83) hide show
  1. package/README.md +2 -1
  2. package/package.json +1 -1
  3. package/skills-registry.yaml +38 -29
  4. package/src/bmm-skills/module.yaml +1 -1
  5. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/04-criativos.md +1 -1
  6. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/11-checklist-roi.md +1 -1
  7. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/12-mineracao-ofertas.md +1 -1
  8. package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/pontos-em-aberto.md +2 -2
  9. package/src/modules/wizz/_shared/video-pipeline.md +12 -4
  10. package/src/modules/wizz/agents/wizz-designer/SKILL.md +1 -1
  11. package/src/modules/wizz/agents/wizz-maestro/SKILL.md +1 -1
  12. package/src/modules/wizz/agents/wizz-social/SKILL.md +1 -0
  13. package/src/skills-lib/wizz-remotion-best-practices/rules/sfx.md +3 -1
  14. package/src/skills-lib/wizz-router/references/routing-table-flat.md +3 -2
  15. package/src/skills-lib/wizz-video-effects/SKILL.md +85 -0
  16. package/src/skills-lib/wizz-video-effects/assets/effects.config.template.json +178 -0
  17. package/src/skills-lib/wizz-video-effects/assets/remotion/EffectsLayer.tsx +87 -0
  18. package/src/skills-lib/wizz-video-effects/assets/remotion/README.md +37 -0
  19. package/src/skills-lib/wizz-video-effects/assets/remotion/useEffects.ts +120 -0
  20. package/src/skills-lib/wizz-video-effects/assets/sfx/CREDITS.md +56 -0
  21. package/src/skills-lib/wizz-video-effects/assets/sfx/cash.mp3 +0 -0
  22. package/src/skills-lib/wizz-video-effects/assets/sfx/chime.mp3 +0 -0
  23. package/src/skills-lib/wizz-video-effects/assets/sfx/click-soft.mp3 +0 -0
  24. package/src/skills-lib/wizz-video-effects/assets/sfx/click.mp3 +0 -0
  25. package/src/skills-lib/wizz-video-effects/assets/sfx/coin.mp3 +0 -0
  26. package/src/skills-lib/wizz-video-effects/assets/sfx/error.mp3 +0 -0
  27. package/src/skills-lib/wizz-video-effects/assets/sfx/impact-bass-1.mp3 +0 -0
  28. package/src/skills-lib/wizz-video-effects/assets/sfx/impact-bass-2.mp3 +0 -0
  29. package/src/skills-lib/wizz-video-effects/assets/sfx/key-press.mp3 +0 -0
  30. package/src/skills-lib/wizz-video-effects/assets/sfx/notif.mp3 +0 -0
  31. package/src/skills-lib/wizz-video-effects/assets/sfx/notification.mp3 +0 -0
  32. package/src/skills-lib/wizz-video-effects/assets/sfx/paste.mp3 +0 -0
  33. package/src/skills-lib/wizz-video-effects/assets/sfx/ping.mp3 +0 -0
  34. package/src/skills-lib/wizz-video-effects/assets/sfx/pop.mp3 +0 -0
  35. package/src/skills-lib/wizz-video-effects/assets/sfx/riser-metal.mp3 +0 -0
  36. package/src/skills-lib/wizz-video-effects/assets/sfx/riser.mp3 +0 -0
  37. package/src/skills-lib/wizz-video-effects/assets/sfx/send.mp3 +0 -0
  38. package/src/skills-lib/wizz-video-effects/assets/sfx/sparkle.mp3 +0 -0
  39. package/src/skills-lib/wizz-video-effects/assets/sfx/stamp.mp3 +0 -0
  40. package/src/skills-lib/wizz-video-effects/assets/sfx/success.mp3 +0 -0
  41. package/src/skills-lib/wizz-video-effects/assets/sfx/type.mp3 +0 -0
  42. package/src/skills-lib/wizz-video-effects/assets/sfx/typing.mp3 +0 -0
  43. package/src/skills-lib/wizz-video-effects/assets/sfx/whoosh-cinematic.mp3 +0 -0
  44. package/src/skills-lib/wizz-video-effects/assets/sfx/whoosh-short.mp3 +0 -0
  45. package/src/skills-lib/wizz-video-effects/assets/sfx/whoosh.mp3 +0 -0
  46. package/src/skills-lib/wizz-video-effects/references/audio.md +87 -0
  47. package/src/skills-lib/wizz-video-effects/references/data-contract.md +145 -0
  48. package/src/skills-lib/wizz-video-effects/references/do-not-use.md +54 -0
  49. package/src/skills-lib/wizz-video-effects/references/editing-patterns.md +58 -0
  50. package/src/skills-lib/wizz-video-effects/references/gates.md +37 -0
  51. package/src/skills-lib/wizz-video-effects/references/sfx-licensing.md +40 -0
  52. package/src/skills-lib/wizz-video-effects/references/when-to-use.md +129 -0
  53. package/src/skills-lib/wizz-video-effects/scripts/budget.py +211 -0
  54. package/src/skills-lib/wizz-video-effects/scripts/check_excess.py +218 -0
  55. package/src/skills-lib/wizz-video-effects/scripts/check_loudness.py +175 -0
  56. package/src/skills-lib/wizz-video-effects/scripts/check_rhythm.py +137 -0
  57. package/src/skills-lib/wizz-video-effects/scripts/check_safe_zone.py +221 -0
  58. package/src/skills-lib/wizz-video-effects/scripts/config.py +583 -0
  59. package/src/skills-lib/wizz-video-effects/scripts/fill_breaths.py +355 -0
  60. package/src/skills-lib/wizz-video-effects/scripts/measure_density.py +122 -0
  61. package/src/skills-lib/wizz-video-effects/scripts/normalize_audio.py +189 -0
  62. package/src/skills-lib/wizz-video-effects/scripts/sound.py +342 -0
  63. package/src/skills-lib/wizz-video-effects/scripts/test_budget.py +131 -0
  64. package/src/skills-lib/wizz-video-effects/scripts/test_check_loudness.py +156 -0
  65. package/src/skills-lib/wizz-video-effects/scripts/test_checks.py +190 -0
  66. package/src/skills-lib/wizz-video-effects/scripts/test_config.py +135 -0
  67. package/src/skills-lib/wizz-video-effects/scripts/test_fill_breaths.py +179 -0
  68. package/src/skills-lib/wizz-video-effects/scripts/test_sound.py +173 -0
  69. package/src/skills-lib/wizz-video-effects/scripts/timeline.py +85 -0
  70. package/tools/installer/banner.js +80 -29
  71. package/tools/installer/commands/catalog.js +87 -0
  72. package/tools/installer/commands/install.js +9 -0
  73. package/tools/installer/guided/detect-tools.js +53 -0
  74. package/tools/installer/guided/flow.js +203 -0
  75. package/tools/installer/guided/labels.js +185 -0
  76. package/tools/installer/guided/messages.js +132 -0
  77. package/tools/installer/guided/outro.js +32 -0
  78. package/tools/installer/guided/profiles.js +95 -0
  79. package/tools/installer/guided/steps.js +154 -0
  80. package/tools/installer/install-messages.yaml +3 -3
  81. package/tools/installer/modules/external-manager.js +4 -0
  82. package/tools/installer/ui.js +57 -22
  83. 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 logo and version using @clack intro + box
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
- // Full "WIZZ METHOD" logo for wide terminals, "WIZZ" only for narrow
12
- const logoWide = [
13
- '██╗ ██╗ ██╗ ███████╗ ███████╗ ███╗ ███╗ ███████╗ ████████╗ ██╗ ██╗ ██████╗ ██████╗ ™',
14
- '██║ ██║ ██║ ╚══███╔╝ ╚══███╔╝ ████╗ ████║ ██╔════╝ ╚══██╔══╝ ██║ ██║ ██╔═══██╗ ██╔══██╗',
15
- '██║ █╗ ██║ ██║ ███╔╝ ███╔╝ ██╔████╔██║ █████╗ ██║ ███████║ ██║ ██║ ██║ ██║',
16
- '██║███╗██║ ██║ ███╔╝ ███╔╝ ██║╚██╔╝██║ ██╔══╝ ██║ ██╔══██║ ██║ ██║ ██║ ██║',
17
- '╚███╔███╔╝ ██║ ███████╗ ███████╗ ██║ ╚═╝ ██║ ███████╗ ██║ ██║ ██║ ╚██████╔╝ ██████╔╝',
18
- ' ╚══╝╚══╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ',
19
- ];
20
-
21
- const logoNarrow = [
22
- ' ██╗ ██╗ ██╗ ███████╗ ███████╗ ™',
23
- ' ██║ ██║ ██║ ╚══███╔╝ ╚══███╔╝',
24
- ' ██║ █╗ ██║ ██║ ███╔╝ ███╔╝ ',
25
- ' ██║███╗██║ ██║ ███╔╝ ███╔╝ ',
26
- ' ╚███╔███╔╝ ██║ ███████╗ ███████╗',
27
- ' ╚══╝╚══╝ ╚═╝ ╚══════╝ ╚══════╝',
28
- ];
29
-
30
- const logoLines = termWidth >= 95 ? logoWide : logoNarrow;
31
- const logo = logoLines.map((line) => color.blue(line)).join('\n');
32
- const tagline = color.white(' Método de agência orientado por IA\n © Wizz! comms. · method.wizzcomms.com');
33
-
34
- await prompts.box(`${logo}\n${tagline}`, '', {
35
- contentAlign: 'center',
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: color.blue,
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 };