wizz-method 2.12.0 → 2.14.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/THIRD-PARTY-NOTICES.md +2 -0
- package/package.json +10 -3
- package/skills-registry.yaml +48 -48
- package/src/bmm-skills/1-analysis/research/wizz-domain-research/customize.toml +4 -3
- package/src/bmm-skills/1-analysis/research/wizz-market-research/customize.toml +4 -3
- package/src/bmm-skills/1-analysis/research/wizz-technical-research/customize.toml +3 -1
- package/src/bmm-skills/1-analysis/wizz-agent-analyst/customize.toml +3 -1
- package/src/bmm-skills/1-analysis/wizz-agent-tech-writer/customize.toml +4 -3
- package/src/bmm-skills/1-analysis/wizz-document-project/customize.toml +4 -3
- package/src/bmm-skills/1-analysis/wizz-prfaq/customize.toml +4 -3
- package/src/bmm-skills/1-analysis/wizz-product-brief/customize.toml +5 -6
- package/src/bmm-skills/2-plan-workflows/wizz-agent-pm/customize.toml +3 -1
- package/src/bmm-skills/2-plan-workflows/wizz-agent-ux-designer/customize.toml +3 -1
- package/src/bmm-skills/2-plan-workflows/wizz-create-prd/customize.toml +4 -3
- package/src/bmm-skills/2-plan-workflows/wizz-edit-prd/customize.toml +4 -3
- package/src/bmm-skills/2-plan-workflows/wizz-prd/customize.toml +5 -6
- package/src/bmm-skills/2-plan-workflows/wizz-ux/customize.toml +4 -3
- package/src/bmm-skills/2-plan-workflows/wizz-validate-prd/customize.toml +4 -3
- package/src/bmm-skills/3-solutioning/wizz-agent-architect/customize.toml +3 -1
- package/src/bmm-skills/3-solutioning/wizz-architecture/customize.toml +5 -6
- package/src/bmm-skills/3-solutioning/wizz-check-implementation-readiness/customize.toml +4 -3
- package/src/bmm-skills/3-solutioning/wizz-create-architecture/customize.toml +4 -3
- package/src/bmm-skills/3-solutioning/wizz-create-epics-and-stories/customize.toml +4 -3
- package/src/bmm-skills/3-solutioning/wizz-generate-project-context/steps/step-03-complete.md +7 -0
- package/src/bmm-skills/4-implementation/wizz-agent-dev/customize.toml +3 -1
- package/src/bmm-skills/4-implementation/wizz-checkpoint-preview/customize.toml +4 -3
- package/src/bmm-skills/4-implementation/wizz-code-review/customize.toml +4 -3
- package/src/bmm-skills/4-implementation/wizz-code-review/steps/step-03-triage.md +23 -14
- package/src/bmm-skills/4-implementation/wizz-code-review/steps/step-04-present.md +2 -2
- package/src/bmm-skills/4-implementation/wizz-correct-course/customize.toml +4 -3
- package/src/bmm-skills/4-implementation/wizz-create-story/customize.toml +4 -3
- package/src/bmm-skills/4-implementation/wizz-dev-story/customize.toml +4 -3
- package/src/bmm-skills/4-implementation/wizz-investigate/customize.toml +4 -3
- package/src/bmm-skills/4-implementation/wizz-qa-generate-e2e-tests/customize.toml +4 -3
- package/src/bmm-skills/4-implementation/wizz-quick-dev/customize.toml +4 -3
- package/src/bmm-skills/4-implementation/wizz-quick-dev/spec-template.md +6 -0
- package/src/bmm-skills/4-implementation/wizz-quick-dev/step-04-review.md +27 -16
- package/src/bmm-skills/4-implementation/wizz-quick-dev/step-oneshot.md +27 -6
- package/src/bmm-skills/4-implementation/wizz-retrospective/customize.toml +4 -3
- package/src/bmm-skills/4-implementation/wizz-sprint-planning/customize.toml +4 -3
- package/src/bmm-skills/4-implementation/wizz-sprint-status/customize.toml +4 -3
- package/src/bmm-skills/module.yaml +1 -1
- package/src/core-skills/wizz-brainstorming/customize.toml +5 -6
- package/src/core-skills/wizz-brainstorming/scripts/brain.py +19 -0
- package/src/core-skills/wizz-brainstorming/scripts/tests/test_brain.py +33 -0
- package/src/core-skills/wizz-forge-idea/scripts/resolve_personas.py +3 -1
- package/src/core-skills/wizz-forge-idea/scripts/tests/test_resolve_personas.py +21 -0
- package/src/core-skills/wizz-help/SKILL.md +5 -3
- package/src/core-skills/wizz-party-mode/customize.toml +5 -4
- package/src/core-skills/wizz-party-mode/scripts/resolve_party.py +3 -1
- package/src/core-skills/wizz-party-mode/scripts/tests/test_resolve_party.py +21 -0
- package/src/core-skills/wizz-spec/customize.toml +4 -5
- package/src/modules/wizz/_shared/model-ladder.md +4 -4
- package/src/modules/wizz/_shared/video-pipeline.md +1 -1
- package/src/modules/wizz/agents/wizz-maestro/SKILL.md +1 -1
- package/src/modules/wizz/subagents/opencode/wizz-exec-review.md +2 -2
- package/src/modules/wizz/subagents/opencode/wizz-exec-standard.md +2 -2
- package/src/modules/wizz/subagents/opencode/wizz-exec-strong.md +2 -2
- package/src/modules/wizz/subagents/spec.yaml +3 -3
- package/src/scripts/resolve_config.py +8 -0
- package/src/scripts/resolve_customization.py +24 -3
- package/src/scripts/tests/test_resolve_config.py +42 -0
- package/src/scripts/tests/test_resolve_customization.py +45 -0
- package/src/skills-lib/ctc-align/SKILL.md +1 -1
- package/src/skills-lib/wizz-agent-browser/SKILL.md +5 -0
- package/src/skills-lib/wizz-huashu-design/scripts/html2pptx.js +902 -1061
- package/src/skills-lib/wizz-humanizer/README.md +134 -71
- package/src/skills-lib/wizz-humanizer/SKILL.md +51 -37
- package/src/skills-lib/wizz-humanizer/references/ai-writing-patterns.md +208 -190
- 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/core/doctor.js +31 -1
- package/tools/installer/core/installer.js +32 -0
- package/tools/installer/core/project-instructions.js +199 -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/src/bmm-skills/3-solutioning/wizz-architecture/scripts/__pycache__/lint_spine.cpython-313.pyc +0 -0
- package/src/bmm-skills/3-solutioning/wizz-architecture/scripts/tests/__pycache__/test_lint_spine.cpython-313-pytest-9.1.1.pyc +0 -0
- package/src/bmm-skills/4-implementation/wizz-retrospective/scripts/__pycache__/sprint_status.cpython-313.pyc +0 -0
- package/src/bmm-skills/4-implementation/wizz-retrospective/scripts/tests/__pycache__/test_git_evidence.cpython-313-pytest-9.1.1.pyc +0 -0
- package/src/bmm-skills/4-implementation/wizz-retrospective/scripts/tests/__pycache__/test_sprint_status.cpython-313-pytest-9.1.1.pyc +0 -0
- package/src/bmm-skills/4-implementation/wizz-sprint-planning/scripts/__pycache__/sprint_plan.cpython-313.pyc +0 -0
- package/src/bmm-skills/4-implementation/wizz-sprint-planning/scripts/tests/__pycache__/test_sprint_plan.cpython-313-pytest-9.1.1.pyc +0 -0
- package/src/core-skills/wizz-advanced-elicitation/scripts/__pycache__/pick_methods.cpython-313.pyc +0 -0
- package/src/core-skills/wizz-advanced-elicitation/scripts/tests/__pycache__/test_pick_methods.cpython-313-pytest-9.1.1.pyc +0 -0
- package/src/core-skills/wizz-brainstorming/scripts/__pycache__/brain.cpython-313.pyc +0 -0
- package/src/core-skills/wizz-brainstorming/scripts/tests/__pycache__/test_brain.cpython-313-pytest-9.1.1.pyc +0 -0
- package/src/core-skills/wizz-brainstorming/scripts/tests/__pycache__/test_brain.cpython-314.pyc +0 -0
- package/src/core-skills/wizz-customize/scripts/__pycache__/list_customizable_skills.cpython-313.pyc +0 -0
- package/src/core-skills/wizz-customize/scripts/tests/__pycache__/test_list_customizable_skills.cpython-313-pytest-9.1.1.pyc +0 -0
- package/src/core-skills/wizz-forge-idea/scripts/__pycache__/resolve_personas.cpython-313.pyc +0 -0
- package/src/core-skills/wizz-forge-idea/scripts/__pycache__/resolve_personas.cpython-314.pyc +0 -0
- package/src/core-skills/wizz-forge-idea/scripts/tests/__pycache__/test_resolve_personas.cpython-313-pytest-9.1.1.pyc +0 -0
- package/src/core-skills/wizz-forge-idea/scripts/tests/__pycache__/test_resolve_personas.cpython-314.pyc +0 -0
- package/src/core-skills/wizz-party-mode/scripts/__pycache__/resolve_party.cpython-313.pyc +0 -0
- package/src/core-skills/wizz-party-mode/scripts/__pycache__/resolve_party.cpython-314.pyc +0 -0
- package/src/core-skills/wizz-party-mode/scripts/tests/__pycache__/test_resolve_party.cpython-313-pytest-9.1.1.pyc +0 -0
- package/src/core-skills/wizz-party-mode/scripts/tests/__pycache__/test_resolve_party.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/resolve_customization.cpython-314.pyc +0 -0
- package/src/skills-lib/wizz-huashu-design/package-lock.json +0 -846
- package/src/skills-lib/wizz-humanizer/WARP.md +0 -53
- package/src/skills-lib/wizz-humanizer/references/full-example.md +0 -72
- package/src/skills-lib/wizz-humanizer/references/personality-and-soul.md +0 -36
- package/src/skills-lib/wizz-react-components/package-lock.json +0 -231
- package/src/skills-lib/wizz-supabase-postgres-best-practices/CLAUDE.md +0 -68
- package/src/skills-lib/wizz-ui-ux-pro-max/scripts/__pycache__/core.cpython-314.pyc +0 -0
- package/src/skills-lib/wizz-ui-ux-pro-max/scripts/__pycache__/design_system.cpython-314.pyc +0 -0
|
@@ -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
|
}
|
|
@@ -25,7 +25,7 @@ const { HOOK_DEFS, isOurHookCommand } = require('./hooks-settings');
|
|
|
25
25
|
const { classifyPython, PYTHON_ONLY_SKILLS } = require('./python-check');
|
|
26
26
|
|
|
27
27
|
// Piso de versão do rtk que o hook rtk-rewrite.sh exige para `rtk rewrite`
|
|
28
|
-
// (o registry pin do CLI é mais alto, 0.
|
|
28
|
+
// (o registry pin do CLI é mais alto, 0.50.0, e é aplicado pelo detectClis
|
|
29
29
|
// no install; aqui reportamos o piso que quebra o hook hoje).
|
|
30
30
|
const RTK_HOOK_FLOOR = '0.23.0';
|
|
31
31
|
|
|
@@ -464,6 +464,28 @@ async function currentInputHash({ projectDir, relPath, skill, sources, fileExist
|
|
|
464
464
|
* @param {object} [options.deps] - checkers injetáveis (ver chamadas abaixo)
|
|
465
465
|
* @returns {Promise<{results: Array<{check, status, detail}>, exitCode: number, hasError: boolean}>}
|
|
466
466
|
*/
|
|
467
|
+
/**
|
|
468
|
+
* CLAUDE.md/AGENTS.md: bloco Wizz presente e sem regra legada fora dele.
|
|
469
|
+
* Sem o bloco, um CLAUDE.md antigo vence as regras do Wizz (2026-09-30).
|
|
470
|
+
* @returns {Promise<{check: 'instructions', status: 'ok'|'warn', detail: string}>}
|
|
471
|
+
*/
|
|
472
|
+
async function checkProjectInstructions({ projectDir, fileExists, readFile }) {
|
|
473
|
+
const { INSTRUCTION_FILES, BLOCK_BEGIN, scanLegacy } = require('./project-instructions');
|
|
474
|
+
const problems = [];
|
|
475
|
+
let seen = 0;
|
|
476
|
+
for (const name of INSTRUCTION_FILES) {
|
|
477
|
+
const file = path.join(projectDir, name);
|
|
478
|
+
if (!(await fileExists(file))) continue;
|
|
479
|
+
seen += 1;
|
|
480
|
+
const text = await readFile(file);
|
|
481
|
+
if (!text.includes(BLOCK_BEGIN)) problems.push(`${name} sem o bloco Wizz no topo (rode wizz install)`);
|
|
482
|
+
for (const f of scanLegacy(text)) problems.push(`${name}:${f.line} ${f.why}: ${f.snippet}`);
|
|
483
|
+
}
|
|
484
|
+
if (seen === 0) return { check: 'instructions', status: 'ok', detail: 'Sem CLAUDE.md/AGENTS.md no projeto.' };
|
|
485
|
+
if (problems.length === 0) return { check: 'instructions', status: 'ok', detail: 'Bloco Wizz presente, nenhuma regra legada.' };
|
|
486
|
+
return { check: 'instructions', status: 'warn', detail: problems.join('\n') };
|
|
487
|
+
}
|
|
488
|
+
|
|
467
489
|
async function runDoctor(options = {}) {
|
|
468
490
|
const deps = options.deps || {};
|
|
469
491
|
const {
|
|
@@ -578,6 +600,13 @@ async function runDoctor(options = {}) {
|
|
|
578
600
|
});
|
|
579
601
|
}
|
|
580
602
|
|
|
603
|
+
// 7) instructions — CLAUDE.md/AGENTS.md com bloco Wizz e sem regra legada.
|
|
604
|
+
if (skip.includes('instructions')) {
|
|
605
|
+
results.push({ check: 'instructions', status: 'ok', detail: 'pulado.' });
|
|
606
|
+
} else if (wizzDir) {
|
|
607
|
+
results.push(await checkProjectInstructions({ projectDir, fileExists, readFile }));
|
|
608
|
+
}
|
|
609
|
+
|
|
581
610
|
const hasError = results.some((r) => r.status === 'error');
|
|
582
611
|
return { results, exitCode: hasError ? 1 : 0, hasError };
|
|
583
612
|
}
|
|
@@ -631,6 +660,7 @@ function safeReadJson(file) {
|
|
|
631
660
|
}
|
|
632
661
|
|
|
633
662
|
module.exports = {
|
|
663
|
+
checkProjectInstructions,
|
|
634
664
|
CHECKS,
|
|
635
665
|
RTK_HOOK_FLOOR,
|
|
636
666
|
extractVersion,
|
|
@@ -20,6 +20,7 @@ const { buildHookEntries, filterHooksRegisteredGlobally, mergeHooksIntoSettings
|
|
|
20
20
|
|
|
21
21
|
const { ExistingInstall } = require('./existing-install');
|
|
22
22
|
const { warnPreNativeSkillsLegacy } = require('./legacy-warnings');
|
|
23
|
+
const { applyProjectInstructions, removeProjectInstructions } = require('./project-instructions');
|
|
23
24
|
const userFilePreservation = require('./user-file-preservation');
|
|
24
25
|
const moduleConfigWriter = require('./module-config-writer');
|
|
25
26
|
const helpCatalog = require('./help-catalog');
|
|
@@ -808,6 +809,11 @@ class Installer {
|
|
|
808
809
|
addResult('Hooks settings', 'ok', `${pendingHooksSettingsWrite.count} hook(s) → .claude/settings.json${skippedDetail}`);
|
|
809
810
|
}
|
|
810
811
|
|
|
812
|
+
// Bloco gerenciado no CLAUDE.md/AGENTS.md (2026-09-30): sem ele, um
|
|
813
|
+
// CLAUDE.md antigo vence as regras do Wizz, que chegam só por hook.
|
|
814
|
+
// Depois do swap pelo mesmo motivo do settings.json: vive fora de _wizz/.
|
|
815
|
+
await this._applyProjectInstructions(paths, config, addResult);
|
|
816
|
+
|
|
811
817
|
await this._appendPreservedSkillManifestRows(paths.wizzDir, previousSkillManifestRows, preservedModules);
|
|
812
818
|
|
|
813
819
|
// Apply post-install --set TOML patches. Runs after writeCentralConfig
|
|
@@ -1223,6 +1229,31 @@ class Installer {
|
|
|
1223
1229
|
* @param {string} file
|
|
1224
1230
|
* @returns {Promise<{settings: object, invalidJson: boolean}>}
|
|
1225
1231
|
*/
|
|
1232
|
+
async _applyProjectInstructions(paths, config, addResult) {
|
|
1233
|
+
if (!(await fs.pathExists(path.join(paths.wizzDir, 'wizz')))) return;
|
|
1234
|
+
try {
|
|
1235
|
+
const results = await applyProjectInstructions({
|
|
1236
|
+
projectRoot: paths.projectRoot,
|
|
1237
|
+
fs,
|
|
1238
|
+
createIfMissing: (config.ides || []).includes('claude-code'),
|
|
1239
|
+
});
|
|
1240
|
+
for (const r of results) {
|
|
1241
|
+
const name = path.basename(r.file);
|
|
1242
|
+
const legacy = r.findings.length > 0 ? `, ${r.findings.length} trecho(s) legado(s) marcados` : '';
|
|
1243
|
+
addResult('Project instructions', r.findings.length > 0 ? 'warn' : 'ok', `bloco Wizz no topo de ${name}${legacy}`);
|
|
1244
|
+
}
|
|
1245
|
+
const totalLegacy = results.reduce((n, r) => n + r.findings.length, 0);
|
|
1246
|
+
if (totalLegacy > 0) {
|
|
1247
|
+
await prompts.log.warn(
|
|
1248
|
+
`${totalLegacy} regra(s) antiga(s) no CLAUDE.md/AGENTS.md (GSD, superpowers, BMAD puro ou modelo aposentado). O bloco Wizz no topo tem prioridade, mas vale limpar: rode wizz doctor para ver as linhas.`,
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
await prompts.log.warn(`Falha ao gravar o bloco Wizz no CLAUDE.md/AGENTS.md: ${error.message}`);
|
|
1253
|
+
addResult('Project instructions', 'warn', error.message);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1226
1257
|
async _readSettingsJson(file) {
|
|
1227
1258
|
if (!(await fs.pathExists(file))) return { settings: {}, invalidJson: false };
|
|
1228
1259
|
try {
|
|
@@ -1600,6 +1631,7 @@ class Installer {
|
|
|
1600
1631
|
// 2. IDE CLEANUP (before _wizz/ deletion so configs are accessible)
|
|
1601
1632
|
if (options.removeIdeConfigs !== false) {
|
|
1602
1633
|
await this.uninstallIdeConfigs(projectDir, existingInstall, { silent: options.silent });
|
|
1634
|
+
await removeProjectInstructions({ projectRoot: projectDir, fs });
|
|
1603
1635
|
removed.ideConfigs = true;
|
|
1604
1636
|
}
|
|
1605
1637
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// project-instructions.js — bloco gerenciado no CLAUDE.md/AGENTS.md do projeto.
|
|
2
|
+
//
|
|
3
|
+
// Motivo (2026-09-30): o Claude Code carrega o CLAUDE.md do projeto como
|
|
4
|
+
// instrução de projeto (prioridade alta), enquanto as regras do Wizz chegam
|
|
5
|
+
// por hook e skill (contexto extra, prioridade menor). Num projeto que já
|
|
6
|
+
// tinha CLAUDE.md antigo (GSD, superpowers, BMAD puro, modelos aposentados),
|
|
7
|
+
// a regra velha vencia a nova. O installer agora grava um bloco no TOPO do
|
|
8
|
+
// arquivo dizendo que o Wizz vence em conflito e listando os trechos legados
|
|
9
|
+
// detectados. Nada fora do bloco é apagado ou reescrito.
|
|
10
|
+
//
|
|
11
|
+
// Lógica pura (texto → texto) + um aplicador fino com fs injetável, para os
|
|
12
|
+
// testes não dependerem de disco real.
|
|
13
|
+
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const fsp = require('node:fs/promises');
|
|
16
|
+
|
|
17
|
+
const BLOCK_BEGIN = '<!-- wizz:begin -->';
|
|
18
|
+
const BLOCK_END = '<!-- wizz:end -->';
|
|
19
|
+
const INSTRUCTION_FILES = ['CLAUDE.md', 'AGENTS.md'];
|
|
20
|
+
|
|
21
|
+
// IDs de modelo aposentados. Compartilhado com tools/validate-model-ids.mjs.
|
|
22
|
+
// claude-haiku-4-5 continua atual (não há Haiku mais novo em 2026-09-30).
|
|
23
|
+
// `claude-(opus|sonnet)-5` sem sufixo foi trocado por 5-5; o lookahead evita
|
|
24
|
+
// casar o próprio `-5-5`.
|
|
25
|
+
const RETIRED_MODEL_RE =
|
|
26
|
+
/\bclaude-(?:instant|2|3|3-5|3-7)(?:-[a-z0-9]+)*\b|\bclaude-(?:opus|sonnet)-4(?:-\d+)?\b|\bclaude-(?:opus|sonnet)-5\b(?!-)/gi;
|
|
27
|
+
|
|
28
|
+
const LEGACY_RULES = [
|
|
29
|
+
{ id: 'gsd', re: /\bGSD\b|get-shit-done|\/gsd[:-]/, why: 'GSD foi substituído pelas fases e checkpoints do Wizz Method' },
|
|
30
|
+
{ id: 'superpowers', re: /\bsuperpowers\b/i, why: 'superpowers foi substituído pelo Wizz Method' },
|
|
31
|
+
{
|
|
32
|
+
id: 'bmad',
|
|
33
|
+
re: /_bmad\/|\/bmad[-:]|\bbmad-(?:master|agent|method|help)\b/i,
|
|
34
|
+
why: 'comandos e pastas do BMAD puro viraram wizz-* e _wizz/',
|
|
35
|
+
},
|
|
36
|
+
{ id: 'modelo-aposentado', re: RETIRED_MODEL_RE, why: 'ID de modelo aposentado; a escada do Wizz usa os apelidos atuais' },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const MAX_SNIPPET = 120;
|
|
40
|
+
|
|
41
|
+
// project-context.md (2026-09-30, porte do BMAD 6.12 #2750): em vez de cada
|
|
42
|
+
// skill procurar o arquivo em toda ativação (persistent_facts), o bloco
|
|
43
|
+
// importa uma vez por sessão. Busca rasa, pulando pastas de dependência/build.
|
|
44
|
+
const CONTEXT_FILE = 'project-context.md';
|
|
45
|
+
const CONTEXT_MAX_DEPTH = 4;
|
|
46
|
+
const CONTEXT_SKIP_DIRS = new Set(['node_modules', '.git', '_wizz', '.claude', 'build', 'dist', '.next', 'vendor', '.venv']);
|
|
47
|
+
|
|
48
|
+
/** Caminho relativo (posix) do project-context.md mais raso, ou null. */
|
|
49
|
+
async function findProjectContext(projectRoot) {
|
|
50
|
+
let level = [projectRoot];
|
|
51
|
+
for (let depth = 0; depth <= CONTEXT_MAX_DEPTH && level.length > 0; depth++) {
|
|
52
|
+
const next = [];
|
|
53
|
+
for (const dir of level.sort()) {
|
|
54
|
+
let entries;
|
|
55
|
+
try {
|
|
56
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
57
|
+
} catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (entries.some((e) => e.isFile() && e.name === CONTEXT_FILE)) {
|
|
61
|
+
return path.relative(projectRoot, path.join(dir, CONTEXT_FILE)).split(path.sep).join('/');
|
|
62
|
+
}
|
|
63
|
+
for (const e of entries) if (e.isDirectory() && !CONTEXT_SKIP_DIRS.has(e.name)) next.push(path.join(dir, e.name));
|
|
64
|
+
}
|
|
65
|
+
level = next;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Linha de contexto por arquivo: CLAUDE.md importa com @; AGENTS.md só aponta. */
|
|
71
|
+
function contextLine(fileName, contextPath) {
|
|
72
|
+
if (!contextPath) return null;
|
|
73
|
+
return fileName === 'CLAUDE.md'
|
|
74
|
+
? `Contexto do projeto (regras de stack e padrões, importado uma vez por sessão):\n@${contextPath}`
|
|
75
|
+
: `- Contexto do projeto: leia \`${contextPath}\` antes de implementar (regras de stack e padrões).`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Separa o bloco gerenciado do resto do arquivo. */
|
|
79
|
+
function splitManagedBlock(text) {
|
|
80
|
+
const start = text.indexOf(BLOCK_BEGIN);
|
|
81
|
+
const end = text.indexOf(BLOCK_END);
|
|
82
|
+
if (start === -1 || end === -1 || end < start) return { block: null, rest: text };
|
|
83
|
+
const after = text.slice(end + BLOCK_END.length).replace(/^\r?\n+/, '');
|
|
84
|
+
return { block: text.slice(start, end + BLOCK_END.length), rest: text.slice(0, start) + after };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Procura regras legadas no texto, pulando as linhas do bloco gerenciado.
|
|
89
|
+
* `line` é a linha real no arquivo (1-based), para o doctor apontar.
|
|
90
|
+
* @returns {{id: string, line: number, snippet: string, why: string}[]}
|
|
91
|
+
*/
|
|
92
|
+
function scanLegacy(text) {
|
|
93
|
+
const findings = [];
|
|
94
|
+
let inBlock = false;
|
|
95
|
+
for (const [i, line] of text.split(/\r?\n/).entries()) {
|
|
96
|
+
if (line.includes(BLOCK_BEGIN)) inBlock = true;
|
|
97
|
+
if (inBlock) {
|
|
98
|
+
if (line.includes(BLOCK_END)) inBlock = false;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const rule = LEGACY_RULES.find((r) => new RegExp(r.re.source, r.re.flags.replace('g', '')).test(line));
|
|
102
|
+
if (rule) findings.push({ id: rule.id, line: i + 1, snippet: line.trim().slice(0, MAX_SNIPPET), why: rule.why });
|
|
103
|
+
}
|
|
104
|
+
return findings;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Monta o bloco. Sem número de linha: o próprio bloco desloca as linhas. */
|
|
108
|
+
function renderManagedBlock(findings = [], context = null) {
|
|
109
|
+
const lines = [
|
|
110
|
+
BLOCK_BEGIN,
|
|
111
|
+
'<!-- Gerenciado pelo Wizz Method: o install reescreve este bloco. Edite fora dele. -->',
|
|
112
|
+
'## Wizz Method: estas regras têm prioridade',
|
|
113
|
+
'',
|
|
114
|
+
'Este projeto usa o Wizz Method (`_wizz/`). Se alguma instrução deste arquivo, fora deste bloco, contradizer o Wizz Method, vale o Wizz Method.',
|
|
115
|
+
'',
|
|
116
|
+
'- Pedido não trivial: uma área vai para o agente da área; duas ou mais áreas vão para o `wizz-maestro`. Edição trivial de 1 linha e conversa executam direto.',
|
|
117
|
+
'- Planejamento, TDD, checkpoints e review seguem o Wizz Method. Não usar GSD, superpowers nem os comandos antigos do BMAD (`/bmad-*`, `_bmad/`).',
|
|
118
|
+
'- Modelos seguem a escada do Wizz (`wizz-exec-mechanical`, `-standard`, `-strong`, `-ultra`, `-review`), que usa os apelidos atuais. Ignore IDs de modelo fixos deste arquivo.',
|
|
119
|
+
];
|
|
120
|
+
if (context) lines.push('', ...context.split('\n'));
|
|
121
|
+
if (findings.length > 0) {
|
|
122
|
+
lines.push('', 'Trechos legados detectados abaixo (não valem mais; `wizz doctor` lista de novo):');
|
|
123
|
+
for (const f of findings) lines.push(`- \`${f.snippet.replaceAll('`', "'")}\` (${f.why})`);
|
|
124
|
+
}
|
|
125
|
+
lines.push(BLOCK_END);
|
|
126
|
+
return lines.join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Insere ou substitui o bloco no topo. Idempotente. */
|
|
130
|
+
function upsertManagedBlock(text, context = null) {
|
|
131
|
+
const { rest } = splitManagedBlock(text);
|
|
132
|
+
const findings = scanLegacy(rest);
|
|
133
|
+
const block = renderManagedBlock(findings, context);
|
|
134
|
+
const body = rest.replace(/^\s+/, '');
|
|
135
|
+
return { text: body ? `${block}\n\n${body}` : `${block}\n`, findings };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Remove o bloco (uninstall). Devolve o texto intacto se não houver bloco. */
|
|
139
|
+
function removeManagedBlock(text) {
|
|
140
|
+
return splitManagedBlock(text).rest;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Aplica o bloco nos arquivos de instrução que existem na raiz do projeto.
|
|
145
|
+
* Sem nenhum dos dois, cria CLAUDE.md só se `createIfMissing`.
|
|
146
|
+
* Com project-context.md no projeto, o bloco importa (CLAUDE.md) ou aponta (AGENTS.md) para ele.
|
|
147
|
+
* @returns {Promise<{file: string, changed: boolean, findings: object[]}[]>}
|
|
148
|
+
*/
|
|
149
|
+
async function applyProjectInstructions({ projectRoot, fs, createIfMissing = false }) {
|
|
150
|
+
const results = [];
|
|
151
|
+
const targets = [];
|
|
152
|
+
for (const name of INSTRUCTION_FILES) {
|
|
153
|
+
const file = path.join(projectRoot, name);
|
|
154
|
+
if (await fs.pathExists(file)) targets.push(file);
|
|
155
|
+
}
|
|
156
|
+
if (targets.length === 0 && createIfMissing) targets.push(path.join(projectRoot, 'CLAUDE.md'));
|
|
157
|
+
|
|
158
|
+
const contextPath = await findProjectContext(projectRoot);
|
|
159
|
+
for (const file of targets) {
|
|
160
|
+
const before = (await fs.pathExists(file)) ? await fs.readFile(file, 'utf8') : '';
|
|
161
|
+
const { text, findings } = upsertManagedBlock(before, contextLine(path.basename(file), contextPath));
|
|
162
|
+
const changed = text !== before;
|
|
163
|
+
if (changed) await fs.writeFile(file, text, 'utf8');
|
|
164
|
+
results.push({ file, changed, findings });
|
|
165
|
+
}
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Tira o bloco dos arquivos de instrução (uninstall). */
|
|
170
|
+
async function removeProjectInstructions({ projectRoot, fs }) {
|
|
171
|
+
const removed = [];
|
|
172
|
+
for (const name of INSTRUCTION_FILES) {
|
|
173
|
+
const file = path.join(projectRoot, name);
|
|
174
|
+
if (!(await fs.pathExists(file))) continue;
|
|
175
|
+
const before = await fs.readFile(file, 'utf8');
|
|
176
|
+
const after = removeManagedBlock(before);
|
|
177
|
+
if (after !== before) {
|
|
178
|
+
await fs.writeFile(file, after, 'utf8');
|
|
179
|
+
removed.push(file);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return removed;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
BLOCK_BEGIN,
|
|
187
|
+
BLOCK_END,
|
|
188
|
+
INSTRUCTION_FILES,
|
|
189
|
+
findProjectContext,
|
|
190
|
+
contextLine,
|
|
191
|
+
RETIRED_MODEL_RE,
|
|
192
|
+
splitManagedBlock,
|
|
193
|
+
scanLegacy,
|
|
194
|
+
renderManagedBlock,
|
|
195
|
+
upsertManagedBlock,
|
|
196
|
+
removeManagedBlock,
|
|
197
|
+
applyProjectInstructions,
|
|
198
|
+
removeProjectInstructions,
|
|
199
|
+
};
|
|
@@ -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 };
|