wizz-method 2.20.1 → 2.22.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/THIRD-PARTY-NOTICES.md +18 -0
- package/package.json +4 -2
- package/skills-registry.yaml +41 -0
- package/src/bmm-skills/1-analysis/wizz-set-feature/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-code-review/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-create-story/SKILL.md +1 -1
- package/src/bmm-skills/4-implementation/wizz-dev-story/SKILL.md +8 -1
- package/src/bmm-skills/4-implementation/wizz-quick-dev/SKILL.md +2 -2
- package/src/bmm-skills/4-implementation/wizz-quick-dev/step-03-implement.md +2 -0
- package/src/bmm-skills/4-implementation/wizz-sprint-status/SKILL.md +1 -1
- package/src/core-skills/wizz-party-mode/customize.toml +3 -3
- package/src/modules/lowticket/skills/wizz-lowticket-funil/SKILL.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-guia/SKILL.md +2 -2
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/SKILL.md +5 -5
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/01-principios.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/02-diagnostico-funil.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/03-campanha-bidcap.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/04-criativos.md +2 -2
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/04a-criativos-ia.md +6 -6
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/04b-criativos-formatos.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/04c-criativos-roteiro.md +5 -5
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/05-oferta.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/06-pagina-vendas.md +3 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/06a-instagram.md +2 -2
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/06b-player-vsl.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/07-funil-upsell.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/07a-copy-humanizacao.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/08-pesquisa-publico.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/09-especialista-coproducao.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/10-levantar-caixa.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/12a-mineracao-veredito.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/12b-fabrica-de-ofertas.md +2 -2
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/12c-anatomia-low-ticket.md +3 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/13-benchmarks-vsl.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/14-vocabulario.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/15-como-estender.md +2 -2
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/17-rastreamento.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/18-copy-desejo-oferta.md +3 -3
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/INDEX.md +14 -12
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/guardrails.md +18 -70
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/pontos-em-aberto.md +9 -9
- package/src/modules/lowticket/skills/wizz-lowticket-minerador/SKILL.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-pagina/SKILL.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-trafego/SKILL.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-trafego/references/reguas.md +2 -0
- package/src/modules/wizz/_shared/cerebro.md +1 -1
- package/src/modules/wizz/_shared/organizacao.md +2 -2
- package/src/modules/wizz/agents/wizz-copy/SKILL.md +1 -0
- package/src/modules/wizz/agents/wizz-copy/customize.toml +5 -0
- package/src/modules/wizz/agents/wizz-copy/knowledge/playbook.md +2 -1
- package/src/modules/wizz/agents/wizz-growth/knowledge/playbook.md +1 -1
- package/src/modules/wizz/agents/wizz-social/knowledge/blueprint-roteiro-viral.md +14 -0
- package/src/modules/wizz/agents/wizz-social/knowledge/legenda-instagram.md +12 -0
- package/src/skills-lib/wizz-cerebro/SKILL.md +18 -5
- package/src/skills-lib/wizz-design-needs/SKILL.md +7 -0
- package/src/skills-lib/wizz-design-needs/references/design-state.md +34 -2
- package/src/skills-lib/wizz-design-needs/references/gap-analysis.md +1 -1
- package/src/skills-lib/wizz-design-needs/scripts/design_state.py +305 -0
- package/src/skills-lib/wizz-design-needs/scripts/tests/test_design_state.py +143 -0
- package/src/skills-lib/wizz-impeccable/references/design-rules.md +18 -0
- package/src/skills-lib/wizz-motion-3d-director/references/motion-paths.md +2 -0
- package/src/skills-lib/wizz-paid-ads/SKILL.md +2 -1
- package/src/skills-lib/wizz-premium-landing-ui-researcher/SKILL.md +6 -2
- package/src/skills-lib/wizz-premium-landing-ui-researcher/references/audit-protocol.md +7 -34
- package/src/skills-lib/wizz-premium-landing-ui-researcher/references/component-sources.md +2 -0
- package/src/skills-lib/wizz-premium-landing-ui-researcher/references/design-sources.yaml +252 -0
- package/src/skills-lib/wizz-premium-landing-ui-researcher/references/mandatory-process.md +1 -1
- package/src/skills-lib/wizz-premium-landing-ui-researcher/references/mining-rubric.md +123 -0
- package/src/skills-lib/wizz-premium-landing-ui-researcher/references/source-first-protocol.md +10 -4
- package/src/skills-lib/wizz-premium-landing-ui-researcher/references/web-access-and-evidence.md +1 -1
- package/src/skills-lib/wizz-premium-landing-ui-researcher/scripts/design_sources.py +221 -0
- package/src/skills-lib/wizz-premium-landing-ui-researcher/scripts/tests/test_design_sources.py +130 -0
- package/src/skills-lib/wizz-router/references/routing-table-flat.md +2 -0
- package/src/skills-lib/wizz-taste-skill/references/anti-slop-tells.md +8 -0
- package/src/skills-lib/wizz-ui-component-curator/SKILL.md +8 -7
- package/src/skills-lib/wizz-video-effects/LICENSE-motionmaxxing.txt +202 -0
- package/src/skills-lib/wizz-video-effects/SKILL.md +5 -2
- package/src/skills-lib/wizz-video-effects/assets/chrome/collect_layout.js +113 -0
- package/src/skills-lib/wizz-video-effects/assets/effects.config.template.json +35 -0
- package/src/skills-lib/wizz-video-effects/references/data-contract.md +30 -1
- package/src/skills-lib/wizz-video-effects/references/gates.md +3 -1
- package/src/skills-lib/wizz-video-effects/scripts/check_chrome.py +376 -0
- package/src/skills-lib/wizz-video-effects/scripts/check_dead_frames.py +193 -0
- package/src/skills-lib/wizz-video-effects/scripts/config.py +13 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_chrome.py +181 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_dead_frames.py +171 -0
- package/src/skills-lib/wizz-video-styles/SKILL.md +6 -2
- package/src/skills-lib/wizz-video-styles/references/motion-laws.md +62 -0
- package/src/skills-lib/wizz-video-styles/references/styles/desafio-numero.md +1 -0
- package/src/skills-lib/wizz-video-styles/scripts/blind_review.py +159 -0
- package/src/skills-lib/wizz-video-styles/scripts/taste.mjs +110 -0
- package/src/skills-lib/wizz-video-styles/scripts/test_blind_review.py +82 -0
- package/src/skills-lib/wizz-vsl/LICENSE +49 -0
- package/src/skills-lib/wizz-vsl/SKILL.md +122 -0
- package/src/skills-lib/wizz-vsl/references/assembly-qa.md +123 -0
- package/src/skills-lib/wizz-vsl/references/blocks-and-leads.md +149 -0
- package/src/skills-lib/wizz-vsl/references/diagnosis.md +95 -0
- package/src/skills-lib/wizz-vsl/references/intake.md +200 -0
- package/src/skills-lib/wizz-vsl/references/mechanisms.md +138 -0
- package/src/skills-lib/wizz-vsl/references/niches.md +40 -0
- package/src/skills-lib/wizz-vsl/references/offer.md +91 -0
- package/src/skills-lib/wizz-vsl/references/production.md +75 -0
- package/tools/hooks/README.md +10 -9
- package/tools/hooks/wizz-arrumar.js +1 -0
- package/tools/hooks/wizz-artifact-guard.js +31 -5
- package/tools/hooks/wizz-cerebro-lib.js +36 -13
- package/tools/hooks/wizz-feature-lib.js +4 -1
- package/tools/hooks/wizz-feature.js +66 -76
- package/tools/hooks/wizz-sprint-archive-lib.js +311 -0
- package/tools/installer/core/hooks-settings.js +1 -0
- package/tools/installer/core/installer.js +6 -0
- package/tools/installer/modules/core-seed.js +72 -0
- package/tools/installer/modules/mcp-config.js +3 -0
- package/tools/installer/ui.js +16 -26
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// wizz-sprint-archive-lib.js — arquivamento do sprint que `encerrar` fecha.
|
|
2
|
+
//
|
|
3
|
+
// Fonte de verdade: wizz-method/tools/hooks/. Copiado para `_wizz/hooks/`
|
|
4
|
+
// junto com o wizz-feature.js (SUPPORT_FILES do installer). Não é hook.
|
|
5
|
+
//
|
|
6
|
+
// Planeja tudo antes de tocar no disco e aplica com desfazer: o encerrar do
|
|
7
|
+
// Lucrare (2026-10-19) quebrou com ENOTDIR no meio, num sprint reaberto cujo
|
|
8
|
+
// nome já existia em `_arquivo/<slug>/`, e deixou metade movida.
|
|
9
|
+
//
|
|
10
|
+
// 1. Planejar: os moves (mesclando com o arquivo morto; arquivo de mesmo
|
|
11
|
+
// nome e conteúdo diferente vai para o lado como `<nome>.anterior-<data>`,
|
|
12
|
+
// idêntico só some da origem), o dono do `sprint-status.yaml` da raiz, os
|
|
13
|
+
// links dos `.md` do sprint e as referências de fora (CLAUDE.md, roteiros,
|
|
14
|
+
// html de design). Código e config de fora só são listados, nunca editados.
|
|
15
|
+
// 2. Aplicar: edições, depois moves. Qualquer erro desfaz o que já foi feito,
|
|
16
|
+
// na ordem inversa, e relança.
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const lib = require('./wizz-feature-lib.js');
|
|
21
|
+
const { walkText } = require('./wizz-arrumar.js');
|
|
22
|
+
|
|
23
|
+
const STATUS_FILE = 'sprint-status.yaml';
|
|
24
|
+
// Texto de gente: a referência ao sprint é reescrita. Código, json e yaml de
|
|
25
|
+
// fora são só listados (reescrever import ou config às cegas quebra build).
|
|
26
|
+
const MARKDOWN_EXT = new Set(['.md', '.mdx', '.markdown']);
|
|
27
|
+
const REWRITE_EXT = new Set([...MARKDOWN_EXT, '.txt', '.html', '.htm']);
|
|
28
|
+
|
|
29
|
+
function escapeRe(s) {
|
|
30
|
+
return s.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const toPosix = (p) => p.split(path.sep).join('/');
|
|
34
|
+
|
|
35
|
+
function sameContent(a, b) {
|
|
36
|
+
try {
|
|
37
|
+
return fs.readFileSync(a).equals(fs.readFileSync(b));
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `spec-x.md` → `spec-x.anterior-<data>.md`, com sufixo -2, -3... se já existir. */
|
|
44
|
+
function asideName(dest, taken) {
|
|
45
|
+
const dir = path.dirname(dest);
|
|
46
|
+
const base = path.basename(dest);
|
|
47
|
+
const dot = base.lastIndexOf('.');
|
|
48
|
+
const [stem, ext] = dot > 0 ? [base.slice(0, dot), base.slice(dot)] : [base, ''];
|
|
49
|
+
for (let n = 1; ; n++) {
|
|
50
|
+
const candidate = path.join(dir, `${stem}.anterior-${lib.today()}${n > 1 ? `-${n}` : ''}${ext}`);
|
|
51
|
+
if (!fs.existsSync(candidate) && !taken.has(candidate)) {
|
|
52
|
+
taken.add(candidate);
|
|
53
|
+
return candidate;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Planeja levar `src` para `dest`, mesclando com o que o arquivo morto já tem. */
|
|
59
|
+
function planTree(src, dest, plan) {
|
|
60
|
+
if (!fs.existsSync(dest)) {
|
|
61
|
+
plan.renames.push({ from: src, to: dest });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const srcIsDir = fs.lstatSync(src).isDirectory();
|
|
65
|
+
const destIsDir = fs.lstatSync(dest).isDirectory();
|
|
66
|
+
if (srcIsDir && destIsDir) {
|
|
67
|
+
for (const name of fs.readdirSync(src)) planTree(path.join(src, name), path.join(dest, name), plan);
|
|
68
|
+
plan.emptied.push(src);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!srcIsDir && !destIsDir && sameContent(src, dest)) {
|
|
72
|
+
plan.duplicates.push(src);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
plan.asides.push({ from: dest, to: asideName(dest, plan.taken) });
|
|
76
|
+
plan.renames.push({ from: src, to: dest });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function markdownFiles(dir) {
|
|
80
|
+
let entries = [];
|
|
81
|
+
try {
|
|
82
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
return entries.flatMap((e) => {
|
|
87
|
+
const abs = path.join(dir, e.name);
|
|
88
|
+
if (e.isDirectory()) return markdownFiles(abs);
|
|
89
|
+
return e.isFile() && e.name.endsWith('.md') ? [abs] : [];
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Valor cru de `chave:` no topo de um yaml simples (sem parser: o hook não tem dependência). */
|
|
94
|
+
function yamlTopValue(text, key) {
|
|
95
|
+
const m = new RegExp(String.raw`^${key}:\s*(.*)$`, 'm').exec(text);
|
|
96
|
+
return m
|
|
97
|
+
? m[1]
|
|
98
|
+
.replace(/\s+#.*$/, '')
|
|
99
|
+
.replaceAll(/^["']|["']$/g, '')
|
|
100
|
+
.trim()
|
|
101
|
+
: '';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Chaves de `development_status:` (stories, épicos, retros). */
|
|
105
|
+
function statusKeys(text) {
|
|
106
|
+
const lines = text.split('\n');
|
|
107
|
+
const head = lines.findIndex((l) => /^development_status:\s*$/.test(l));
|
|
108
|
+
if (head === -1) return [];
|
|
109
|
+
const keys = [];
|
|
110
|
+
for (let i = head + 1; i < lines.length && (/^\s/.test(lines[i]) || !lines[i].trim()); i++) {
|
|
111
|
+
const m = /^\s+([\w.-]+):/.exec(lines[i]);
|
|
112
|
+
if (m) keys.push(m[1]);
|
|
113
|
+
}
|
|
114
|
+
return keys;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function openSprints(dirs) {
|
|
118
|
+
const names = new Set();
|
|
119
|
+
for (const base of [dirs.planning, dirs.implementation]) {
|
|
120
|
+
try {
|
|
121
|
+
for (const e of fs.readdirSync(base, { withFileTypes: true })) if (e.isDirectory() && e.name !== lib.ARCHIVE_DIR) names.add(e.name);
|
|
122
|
+
} catch {
|
|
123
|
+
// pasta de artefato ainda não existe
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return [...names];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* De quem é o `sprint-status.yaml` da raiz da implementação. Só vai junto
|
|
131
|
+
* quando é do sprint que fecha; na dúvida fica (e a linha avisa).
|
|
132
|
+
* @returns {{owned: boolean, motivo: string}}
|
|
133
|
+
*/
|
|
134
|
+
function statusOwnership(root, slug, dirs, file) {
|
|
135
|
+
if (fs.existsSync(path.join(dirs.implementation, slug, STATUS_FILE)))
|
|
136
|
+
return { owned: false, motivo: `o sprint já tem o dele em ${slug}/${STATUS_FILE}` };
|
|
137
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
138
|
+
const declared = lib.toSlug(yamlTopValue(text, 'feature') || yamlTopValue(text, 'sprint'));
|
|
139
|
+
if (declared) return declared === slug ? { owned: true, motivo: '' } : { owned: false, motivo: `declara o sprint ${declared}` };
|
|
140
|
+
const location = yamlTopValue(text, 'story_location');
|
|
141
|
+
if (location) {
|
|
142
|
+
const abs = lib.resolveConfigPath(location, root, location);
|
|
143
|
+
const sprint = path.relative(dirs.implementation, abs).split(path.sep)[0];
|
|
144
|
+
if (sprint && sprint !== '..' && sprint !== '' && !path.isAbsolute(sprint) && path.relative(dirs.implementation, abs) !== '')
|
|
145
|
+
return sprint === slug ? { owned: true, motivo: '' } : { owned: false, motivo: `story_location aponta ${sprint}` };
|
|
146
|
+
}
|
|
147
|
+
const sprints = openSprints(dirs);
|
|
148
|
+
const hasStory = (sprint, key) => fs.existsSync(path.join(dirs.implementation, sprint, `${key}.md`));
|
|
149
|
+
const keys = statusKeys(text);
|
|
150
|
+
const owners = new Set(sprints.filter((s) => keys.some((k) => hasStory(s, k))));
|
|
151
|
+
if (owners.size === 1)
|
|
152
|
+
return owners.has(slug) ? { owned: true, motivo: '' } : { owned: false, motivo: `as stories são de ${[...owners][0]}` };
|
|
153
|
+
if (owners.size > 1) return { owned: false, motivo: `as stories são de ${[...owners].join(', ')}` };
|
|
154
|
+
const others = sprints.filter((s) => s !== slug);
|
|
155
|
+
return others.length === 0
|
|
156
|
+
? { owned: true, motivo: '' }
|
|
157
|
+
: { owned: false, motivo: `dono incerto (sprints abertos: ${others.join(', ')})` };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Referências de fora do sprint. Texto de gente é reescrito (link relativo
|
|
162
|
+
* de markdown + caminho a partir da raiz); o resto que ainda cita a pasta
|
|
163
|
+
* velha volta como lista.
|
|
164
|
+
*/
|
|
165
|
+
function planExternalRefs(root, moves, mapAbs, pending) {
|
|
166
|
+
const skip = moves.map((m) => m.src);
|
|
167
|
+
try {
|
|
168
|
+
for (const e of fs.readdirSync(root, { withFileTypes: true }))
|
|
169
|
+
if (e.isDirectory() && e.name.startsWith('.')) skip.push(path.join(root, e.name));
|
|
170
|
+
} catch {
|
|
171
|
+
// raiz ilegível: sem referências externas
|
|
172
|
+
}
|
|
173
|
+
const prefixes = moves.map((m) => ({
|
|
174
|
+
re: new RegExp(String.raw`(?<![\w-])${escapeRe(toPosix(path.relative(root, m.src)))}(?![\w.-])`, 'g'),
|
|
175
|
+
to: toPosix(path.relative(root, m.dest)),
|
|
176
|
+
}));
|
|
177
|
+
const segments = moves.map(
|
|
178
|
+
(m) => new RegExp(String.raw`(?<![\w-])${escapeRe(`${path.basename(path.dirname(m.src))}/${path.basename(m.src)}`)}(?![\w.-])`),
|
|
179
|
+
);
|
|
180
|
+
const listed = [];
|
|
181
|
+
for (const file of walkText(root, skip)) {
|
|
182
|
+
const text = pending.get(file) ?? fs.readFileSync(file, 'utf8');
|
|
183
|
+
const ext = path.extname(file).toLowerCase();
|
|
184
|
+
const isMarkdown = MARKDOWN_EXT.has(ext);
|
|
185
|
+
if (!isMarkdown && !segments.some((re) => re.test(text))) continue;
|
|
186
|
+
let next = text;
|
|
187
|
+
if (isMarkdown) next = lib.rebaseMarkdownLinks(next, path.dirname(file), path.dirname(file), mapAbs, { onlyMoved: true });
|
|
188
|
+
if (REWRITE_EXT.has(ext)) for (const { re, to } of prefixes) next = next.replace(re, to);
|
|
189
|
+
if (next !== text) pending.set(file, next);
|
|
190
|
+
if (segments.some((re) => re.test(next))) listed.push(path.relative(root, file));
|
|
191
|
+
}
|
|
192
|
+
return listed;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Planeja o arquivamento sem tocar no disco.
|
|
197
|
+
* @param {string} root
|
|
198
|
+
* @param {string} slug
|
|
199
|
+
* @param {{edits?: Map<string, string>}} [opts] - edições já decididas (a spec com o status final)
|
|
200
|
+
*/
|
|
201
|
+
function planArchive(root, slug, { edits = new Map() } = {}) {
|
|
202
|
+
const dirs = lib.artifactDirs(root);
|
|
203
|
+
const pending = new Map(edits);
|
|
204
|
+
const moves = [dirs.planning, dirs.implementation].map((base) => ({
|
|
205
|
+
src: path.join(base, slug),
|
|
206
|
+
dest: path.join(base, lib.ARCHIVE_DIR, slug),
|
|
207
|
+
}));
|
|
208
|
+
const mapAbs = (abs) => {
|
|
209
|
+
const hit = moves.find((m) => abs === m.src || abs.startsWith(m.src + path.sep));
|
|
210
|
+
return hit ? hit.dest + abs.slice(hit.src.length) : abs;
|
|
211
|
+
};
|
|
212
|
+
const plan = { renames: [], asides: [], duplicates: [], emptied: [], taken: new Set(), pending, moves, status: null, externas: [] };
|
|
213
|
+
|
|
214
|
+
// Links relativos (`../`) descem um nível junto com o arquivo.
|
|
215
|
+
for (const { src } of moves) {
|
|
216
|
+
for (const file of markdownFiles(src)) {
|
|
217
|
+
const text = pending.get(file) ?? fs.readFileSync(file, 'utf8');
|
|
218
|
+
const next = lib.rebaseMarkdownLinks(text, path.dirname(file), path.dirname(mapAbs(file)), mapAbs);
|
|
219
|
+
if (next !== text) pending.set(file, next);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
plan.externas = planExternalRefs(root, moves, mapAbs, pending);
|
|
223
|
+
for (const { src, dest } of moves) if (fs.existsSync(src)) planTree(src, dest, plan);
|
|
224
|
+
|
|
225
|
+
const status = path.join(dirs.implementation, STATUS_FILE);
|
|
226
|
+
if (fs.existsSync(status)) {
|
|
227
|
+
const { owned, motivo } = statusOwnership(root, slug, dirs, status);
|
|
228
|
+
const dest = path.join(moves[1].dest, STATUS_FILE);
|
|
229
|
+
plan.status = { file: status, dest, owned, motivo };
|
|
230
|
+
if (owned) {
|
|
231
|
+
plan.externas = plan.externas.filter((f) => path.join(root, f) !== status);
|
|
232
|
+
if (fs.existsSync(dest) || plan.renames.some((r) => r.to === dest)) plan.asides.push({ from: dest, to: asideName(dest, plan.taken) });
|
|
233
|
+
plan.renames.push({ from: status, to: dest });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return plan;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** mkdir -p que devolve o desfazer (só as pastas que ele criou). */
|
|
240
|
+
function mkdirTracked(dir, undo) {
|
|
241
|
+
const first = fs.mkdirSync(dir, { recursive: true });
|
|
242
|
+
if (!first) return;
|
|
243
|
+
const created = [];
|
|
244
|
+
for (let d = dir; ; d = path.dirname(d)) {
|
|
245
|
+
created.push(d);
|
|
246
|
+
if (d === first || path.dirname(d) === d) break;
|
|
247
|
+
}
|
|
248
|
+
undo.push(() => {
|
|
249
|
+
for (const d of created) fs.rmdirSync(d);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Aplica o plano. Erro no meio desfaz tudo na ordem inversa e relança.
|
|
255
|
+
* @returns {{asides: string[]}}
|
|
256
|
+
*/
|
|
257
|
+
function applyArchive(plan) {
|
|
258
|
+
const undo = [];
|
|
259
|
+
const rename = (from, to) => {
|
|
260
|
+
mkdirTracked(path.dirname(to), undo);
|
|
261
|
+
fs.renameSync(from, to);
|
|
262
|
+
undo.push(() => fs.renameSync(to, from));
|
|
263
|
+
};
|
|
264
|
+
try {
|
|
265
|
+
for (const [file, text] of plan.pending) {
|
|
266
|
+
const before = fs.existsSync(file) ? fs.readFileSync(file) : null;
|
|
267
|
+
mkdirTracked(path.dirname(file), undo);
|
|
268
|
+
fs.writeFileSync(file, text);
|
|
269
|
+
undo.push(() => (before === null ? fs.unlinkSync(file) : fs.writeFileSync(file, before)));
|
|
270
|
+
}
|
|
271
|
+
// O que já está no arquivo morto vai para o lado antes de o novo chegar.
|
|
272
|
+
for (const { from, to } of plan.asides) rename(from, to);
|
|
273
|
+
for (const { from, to } of plan.renames) rename(from, to);
|
|
274
|
+
for (const file of plan.duplicates) {
|
|
275
|
+
const content = fs.readFileSync(file);
|
|
276
|
+
fs.unlinkSync(file);
|
|
277
|
+
undo.push(() => fs.writeFileSync(file, content));
|
|
278
|
+
}
|
|
279
|
+
// Mais fundo primeiro: a pasta-mãe só esvazia depois das filhas.
|
|
280
|
+
for (const dir of [...plan.emptied].sort((a, b) => b.length - a.length)) {
|
|
281
|
+
fs.rmdirSync(dir);
|
|
282
|
+
undo.push(() => fs.mkdirSync(dir));
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
const failures = [];
|
|
286
|
+
for (const step of undo.toReversed()) {
|
|
287
|
+
try {
|
|
288
|
+
step();
|
|
289
|
+
} catch (undoError) {
|
|
290
|
+
failures.push(undoError.message);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
error.desfeito = failures.length === 0;
|
|
294
|
+
if (failures.length > 0) error.message += ` (desfazer incompleto: ${failures.join('; ')})`;
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
return { asides: plan.asides.map((a) => a.to) };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* O sprint já foi fechado: nenhuma pasta aberta e o arquivo morto existe.
|
|
302
|
+
* @param {string} root
|
|
303
|
+
* @param {string} slug
|
|
304
|
+
*/
|
|
305
|
+
function isArchived(root, slug) {
|
|
306
|
+
const dirs = lib.artifactDirs(root);
|
|
307
|
+
const bases = [dirs.planning, dirs.implementation];
|
|
308
|
+
return !bases.some((b) => fs.existsSync(path.join(b, slug))) && bases.some((b) => fs.existsSync(path.join(b, lib.ARCHIVE_DIR, slug)));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = { planArchive, applyArchive, isArchived, statusOwnership, asideName, STATUS_FILE };
|
|
@@ -24,6 +24,7 @@ const { warnPreNativeSkillsLegacy } = require('./legacy-warnings');
|
|
|
24
24
|
const { applyProjectInstructions, removeProjectInstructions } = require('./project-instructions');
|
|
25
25
|
const userFilePreservation = require('./user-file-preservation');
|
|
26
26
|
const moduleConfigWriter = require('./module-config-writer');
|
|
27
|
+
const { absoluteOutputFolder } = require('../modules/core-seed');
|
|
27
28
|
const helpCatalog = require('./help-catalog');
|
|
28
29
|
const quickUpdateModule = require('./quick-update');
|
|
29
30
|
|
|
@@ -295,6 +296,11 @@ class Installer {
|
|
|
295
296
|
) {
|
|
296
297
|
const isQuickUpdate = config.isQuickUpdate();
|
|
297
298
|
const moduleConfigs = officialModules.moduleConfigs;
|
|
299
|
+
// Quick update copies the prior answers verbatim: a relative output_folder
|
|
300
|
+
// left by 2.20.1 heals here, before any config file is written.
|
|
301
|
+
if (moduleConfigs.core?.output_folder) {
|
|
302
|
+
moduleConfigs.core = { ...moduleConfigs.core, output_folder: absoluteOutputFolder(moduleConfigs.core.output_folder) };
|
|
303
|
+
}
|
|
298
304
|
|
|
299
305
|
const dirResults = { createdDirs: [], movedDirs: [], createdWdsFolders: [] };
|
|
300
306
|
// Set true inside configTask the moment the A14 tmp→real swap actually
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semente do [core] para os caminhos sem pergunta (`--yes`, flags de linha
|
|
3
|
+
* de comando e o install guiado), fora do ui.js.
|
|
4
|
+
*
|
|
5
|
+
* Duas regras que o sync da 2.20.1 quebrou no Lucrare (2026-10-19):
|
|
6
|
+
* 1. Reinstalar nunca troca uma resposta já dada. O caminho `--yes` lia as
|
|
7
|
+
* respostas antigas de `collectedConfig.core` (vazio àquela altura) em
|
|
8
|
+
* vez do armazém do `loadExistingConfig`, e caía nos defaults fixos:
|
|
9
|
+
* `user_name` virava o login da máquina e o idioma virava English.
|
|
10
|
+
* 2. `output_folder` é sempre `{project-root}/<pasta>`, igual ao `result:`
|
|
11
|
+
* do core/module.yaml e a planning/implementation. Valor cru (default
|
|
12
|
+
* fixo, `--output-folder`, install guiado) pulava o template e gravava
|
|
13
|
+
* relativo; skills que trocam `{output_folder}` literal escreviam
|
|
14
|
+
* relativo ao cwd (worktrees em `.claude/worktrees/`).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const os = require('node:os');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
|
|
20
|
+
const PROJECT_ROOT = '{project-root}';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `_wizz-output` → `{project-root}/_wizz-output`. Valor que já começa com
|
|
24
|
+
* `{project-root}` ou é absoluto fica como está.
|
|
25
|
+
* @param {string} value
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function absoluteOutputFolder(value) {
|
|
29
|
+
if (typeof value !== 'string' || !value.trim()) return value;
|
|
30
|
+
const raw = value.trim();
|
|
31
|
+
if (raw.startsWith(PROJECT_ROOT) || path.isAbsolute(raw)) return raw;
|
|
32
|
+
const clean = raw.replace(/^\.\//, '').replaceAll('\\', '/').replace(/\/+$/, '');
|
|
33
|
+
return `${PROJECT_ROOT}/${clean}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function systemUsername() {
|
|
37
|
+
let name;
|
|
38
|
+
try {
|
|
39
|
+
name = os.userInfo().username;
|
|
40
|
+
} catch {
|
|
41
|
+
name = process.env.USER || process.env.USERNAME || 'User';
|
|
42
|
+
}
|
|
43
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Defaults do [core] para um install do zero com `--yes`.
|
|
48
|
+
* {directory_name} default per src/core-skills/module.yaml — matches what the
|
|
49
|
+
* interactive flow resolves via buildQuestion()'s {directory_name} placeholder.
|
|
50
|
+
* @param {string} directory
|
|
51
|
+
*/
|
|
52
|
+
function defaultCoreConfig(directory) {
|
|
53
|
+
return {
|
|
54
|
+
user_name: systemUsername(),
|
|
55
|
+
project_name: path.basename(directory),
|
|
56
|
+
communication_language: 'English',
|
|
57
|
+
document_output_language: 'English',
|
|
58
|
+
output_folder: absoluteOutputFolder('_wizz-output'),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* O [core] da instalação anterior, como o `loadExistingConfig` leu
|
|
64
|
+
* (config.toml + config.user.toml). `{}` sem instalação.
|
|
65
|
+
* @param {{_existingConfig?: Record<string, Record<string, unknown>> | null}} collector
|
|
66
|
+
*/
|
|
67
|
+
function existingCoreConfig(collector) {
|
|
68
|
+
const core = collector && collector._existingConfig && collector._existingConfig.core;
|
|
69
|
+
return core && typeof core === 'object' && !Array.isArray(core) ? { ...core } : {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { absoluteOutputFolder, defaultCoreConfig, existingCoreConfig };
|
|
@@ -54,6 +54,9 @@ function resolveMcps(registry, selectedAreas) {
|
|
|
54
54
|
// verify it boots. Carried here so prepareMcps() can act on it; dropped
|
|
55
55
|
// from what toServerConfig() writes (it never belongs in .mcp.json).
|
|
56
56
|
setup: mcp.setup || null,
|
|
57
|
+
// `optional` (2026-10-24): the UI leaves it UNCHECKED in the multiselect,
|
|
58
|
+
// so it is only recommended unless the user picks it explicitly.
|
|
59
|
+
optional: mcp.optional === true,
|
|
57
60
|
}),
|
|
58
61
|
});
|
|
59
62
|
}
|
package/tools/installer/ui.js
CHANGED
|
@@ -28,6 +28,7 @@ const { resolveMcps, partitionGloballyConfigured } = require('./modules/mcp-conf
|
|
|
28
28
|
const { resolveClis, detectClis } = require('./modules/cli-config');
|
|
29
29
|
const prompts = require('./prompts');
|
|
30
30
|
const { parseSetEntries } = require('./set-overrides');
|
|
31
|
+
const { absoluteOutputFolder, defaultCoreConfig, existingCoreConfig } = require('./modules/core-seed');
|
|
31
32
|
const { isGuidedEligible, runGuidedInstall } = require('./guided/flow');
|
|
32
33
|
const guidedSteps = require('./guided/steps');
|
|
33
34
|
const { normalizeLang, t: guidedT } = require('./guided/messages');
|
|
@@ -739,11 +740,13 @@ class UI {
|
|
|
739
740
|
const selected = await prompts.multiselect({
|
|
740
741
|
message: options.mcpMessage || it('mcpQuestion'),
|
|
741
742
|
options: resolved.map((m) => ({
|
|
742
|
-
label: m.id,
|
|
743
|
+
label: m.optional ? `${m.id} (opcional)` : m.id,
|
|
743
744
|
value: m.id,
|
|
744
745
|
hint: m.when,
|
|
745
746
|
})),
|
|
746
|
-
|
|
747
|
+
// Entries flagged `optional` in the registry start unchecked: they are
|
|
748
|
+
// recommended, never written to .mcp.json without an explicit pick.
|
|
749
|
+
initialValues: resolved.filter((m) => !m.optional).map((m) => m.id),
|
|
747
750
|
required: false,
|
|
748
751
|
});
|
|
749
752
|
|
|
@@ -1239,14 +1242,16 @@ class UI {
|
|
|
1239
1242
|
if (!options.quiet) await prompts.log.info(`Using document output language from command-line: ${options.documentOutputLanguage}`);
|
|
1240
1243
|
}
|
|
1241
1244
|
if (options.outputFolder) {
|
|
1242
|
-
coreConfig.output_folder = options.outputFolder;
|
|
1245
|
+
coreConfig.output_folder = absoluteOutputFolder(options.outputFolder);
|
|
1243
1246
|
if (!options.quiet) await prompts.log.info(`Using output folder from command-line: ${options.outputFolder}`);
|
|
1244
1247
|
}
|
|
1245
1248
|
|
|
1246
|
-
// Load existing config to merge with provided options
|
|
1249
|
+
// Load existing config to merge with provided options: a flag changes
|
|
1250
|
+
// only its own key, the other prior answers stay.
|
|
1247
1251
|
await configCollector.loadExistingConfig(directory);
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1252
|
+
const merged = { ...existingCoreConfig(configCollector), ...coreConfig };
|
|
1253
|
+
if (merged.output_folder) merged.output_folder = absoluteOutputFolder(merged.output_folder);
|
|
1254
|
+
configCollector.collectedConfig.core = merged;
|
|
1250
1255
|
|
|
1251
1256
|
// If not all options are provided, collect the missing ones interactively (unless --yes flag)
|
|
1252
1257
|
if (
|
|
@@ -1256,27 +1261,12 @@ class UI {
|
|
|
1256
1261
|
await configCollector.collectModuleConfig('core', directory, false, true);
|
|
1257
1262
|
}
|
|
1258
1263
|
} else if (options.yes) {
|
|
1259
|
-
// Use all defaults when --yes flag is set
|
|
1264
|
+
// Use all defaults when --yes flag is set. With a prior install, core is
|
|
1265
|
+
// NOT seeded here: collectAllConfigurations collects it with the prior
|
|
1266
|
+
// answers as defaults (reinstall never changes config.user.toml).
|
|
1260
1267
|
await configCollector.loadExistingConfig(directory);
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
if (Object.keys(existingConfig).length === 0) {
|
|
1264
|
-
let safeUsername;
|
|
1265
|
-
try {
|
|
1266
|
-
safeUsername = os.userInfo().username;
|
|
1267
|
-
} catch {
|
|
1268
|
-
safeUsername = process.env.USER || process.env.USERNAME || 'User';
|
|
1269
|
-
}
|
|
1270
|
-
const defaultUsername = safeUsername.charAt(0).toUpperCase() + safeUsername.slice(1);
|
|
1271
|
-
configCollector.collectedConfig.core = {
|
|
1272
|
-
user_name: defaultUsername,
|
|
1273
|
-
// {directory_name} default per src/core-skills/module.yaml — matches what the
|
|
1274
|
-
// interactive flow resolves via buildQuestion()'s {directory_name} placeholder.
|
|
1275
|
-
project_name: path.basename(directory),
|
|
1276
|
-
communication_language: 'English',
|
|
1277
|
-
document_output_language: 'English',
|
|
1278
|
-
output_folder: '_wizz-output',
|
|
1279
|
-
};
|
|
1268
|
+
if (Object.keys(existingCoreConfig(configCollector)).length === 0) {
|
|
1269
|
+
configCollector.collectedConfig.core = defaultCoreConfig(directory);
|
|
1280
1270
|
if (!options.quiet) await prompts.log.info('Using default configuration (--yes flag)');
|
|
1281
1271
|
}
|
|
1282
1272
|
}
|